Repository: rsteube/carapace (go)

Issue:
color configuration
Provide generic color configuration. Basic idea:

```go
var Style = struct {
	Branch string
	Commit string
	Head   string
	Stash  string
	Tag    string
}{
	Branch: "blue",
	Commit: "default",
	Head:   "bold",
	Stash:  "green",
	Tag:    "yellow",
}

carapace.Gen(cmd).Style(&Style)
```

implicitly override defaults with values from `~/.config/carapace/styles/${cmd.Root().Name}.json`

Pull request:
color config
fix #472


Patch:
diff --git a/action.go b/action.go
index 2fd1012..1d0ddbf 100644
--- a/action.go
+++ b/action.go
@@ -101,11 +101,22 @@ func (a Action) NoSpace() Action {
 }
 
 // Style sets the style
+//   ActionValues("yes").Style(style.Green)
+//   ActionValues("no").Style(style.Red)
 func (a Action) Style(style string) Action {
+	return a.StyleF(func(s string) string {
+		return style
+	})
+}
+
+// Style sets the style using a function
+//   ActionValues("dir/", "test.txt").StyleF(style.ForPathExt)
+//   ActionValues("true", "false").StyleF(style.ForKeyword)
+func (a Action) StyleF(f func(s string) string) Action {
 	return ActionCallback(func(c Context) Action {
 		invoked := a.Invoke(c)
-		for index := range invoked.rawValues {
-			invoked.rawValues[index].Style = style
+		for index, v := range invoked.rawValues {
+			invoked.rawValues[index].Style = f(v.Value)
 		}
 		return invoked.ToA()
 	})
diff --git a/carapace.go b/carapace.go
index 9882ea5..252aebb 100644
--- a/carapace.go
+++ b/carapace.go
@@ -10,6 +10,7 @@ import (
 	"strings"
 
 	"github.com/rsteube/carapace/internal/common"
+	"github.com/rsteube/carapace/internal/config"
 	"github.com/rsteube/carapace/internal/shell/bash"
 	"github.com/rsteube/carapace/internal/shell/bash_ble"
 	"github.com/rsteube/carapace/internal/shell/elvish"
@@ -24,6 +25,7 @@ import (
 	"github.com/rsteube/carapace/internal/shell/zsh"
 	"github.com/rsteube/carapace/internal/uid"
 	"github.com/rsteube/carapace/pkg/ps"
+	"github.com/rsteube/carapace/pkg/style"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 )
@@ -136,9 +138,14 @@ func addCompletionCommand(cmd *cobra.Command) {
 			return
 		}
 	}
-	cmd.AddCommand(&cobra.Command{
+	carapaceCmd := &cobra.Command{
 		Use:    "_carapace",
 		Hidden: true,
+		PreRun: func(cmd *cobra.Command, args []string) {
+			if len(args) > 2 && strings.HasPrefix(args[2], "_") {
+				cmd.Hidden = false
+			}
+		},
 		Run: func(cmd *cobra.Command, args []string) {
 			logger.Println(os.Args) // TODO replace last with '' if empty
 			if s, err := complete(cmd, args); err != nil {
@@ -151,7 +158,65 @@ func addCompletionCommand(cmd *cobra.Command) {
 			UnknownFlags: true,
 		},
 		DisableFlagParsing: true,
-	})
+	}
+	cmd.AddCommand(carapaceCmd)
+	Carapace{carapaceCmd}.PositionalCompletion(
+		ActionStyledValues(
+			"bash", "#d35673",
+			"bash-ble", "#c2039a",
+			"elvish", "#ffd6c9",
+			"export", style.Default,
+			"fish", "#7ea8fc",
+			"ion", "#0e5d6d",
+			"nushell", "#29d866",
+			"oil", "#373a36",
+			"powershell", "#e8a16f",
+			"tcsh", "#412f09",
+			"xonsh", "#a8ffa9",
+			"zsh", "#efda53",
+		),
+		ActionValues(cmd.Root().Name()),
+	)
+	Carapace{carapaceCmd}.PositionalAnyCompletion(
+		ActionCallback(func(c Context) Action {
+			args := []string{"_carapace", "export", ""}
+			args = append(args, c.Args[2:]...)
+			args = append(args, c.CallbackValue)
+			return ActionExecCommand(uid.Executable(), args...)(func(output []byte) Action {
+				if string(output) == "" {
+					return ActionValues()
+				}
+				return ActionImport(output)
+			})
+		}),
+	)
+
+	styleCmd := &cobra.Command{
+		Use:  "style",
+		Args: cobra.ExactArgs(1),
+		Run:  func(cmd *cobra.Command, args []string) {},
+	}
+	carapaceCmd.AddCommand(styleCmd)
+
+	styleSetCmd := &cobra.Command{
+		Use:  "set",
+		Args: cobra.MinimumNArgs(1),
+		Run: func(cmd *cobra.Command, args []string) {
+			for _, arg := range args {
+				if splitted := strings.SplitN(arg, "=", 2); len(splitted) == 2 {
+					if err := style.Set(splitted[0], splitted[1]); err != nil {
+						fmt.Fprint(cmd.ErrOrStderr(), err.Error())
+					}
+				} else {
+					fmt.Fprintf(cmd.ErrOrStderr(), "invalid format: '%v'", arg)
+				}
+			}
+		},
+	}
+	styleCmd.AddCommand(styleSetCmd)
+	Carapace{styleSetCmd}.PositionalAnyCompletion(
+		ActionStyleConfig(),
+	)
 }
 
 func complete(cmd *cobra.Command, args []string) (string, error) {
@@ -175,6 +240,10 @@ func complete(cmd *cobra.Command, args []string) (string, error) {
 			current := args[len(args)-1]
 			previous := args[len(args)-2]
 
+			if err := config.Load(); err != nil {
+				return ActionMessage("failed to load config: "+err.Error()).Invoke(Context{CallbackValue: current}).value(shell, current), nil
+			}
+
 			targetCmd, targetArgs, err := findTarget(cmd, args)
 			context := Context{
 				CallbackValue: current,
diff --git a/defaultActions.go b/defaultActions.go
index 762ddb7..9ae746c 100644
--- a/defaultActions.go
+++ b/defaultActions.go
@@ -3,6 +3,7 @@ package carapace
 import (
 	"bytes"
 	"encoding/json"
+	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -12,6 +13,7 @@ import (
 	exec "golang.org/x/sys/execabs"
 
 	"github.com/rsteube/carapace/internal/common"
+	"github.com/rsteube/carapace/internal/config"
 	"github.com/rsteube/carapace/internal/shell/export"
 	"github.com/rsteube/carapace/pkg/style"
 	"github.com/spf13/cobra"
@@ -101,14 +103,14 @@ func stripAnsi(str string) string {
 // ActionDirectories completes directories
 func ActionDirectories() Action {
 	return ActionCallback(func(c Context) Action {
-		return actionPath([]string{""}, true).Invoke(c).ToMultiPartsA("/").noSpace(true)
+		return actionPath([]string{""}, true).Invoke(c).ToMultiPartsA("/").noSpace(true).StyleF(style.ForPath)
 	})
 }
 
 // ActionFiles completes files with optional suffix filtering
 func ActionFiles(suffix ...string) Action {
 	return ActionCallback(func(c Context) Action {
-		return actionPath(suffix, false).Invoke(c).ToMultiPartsA("/").noSpace(true)
+		return actionPath(suffix, false).Invoke(c).ToMultiPartsA("/").noSpace(true).StyleF(style.ForPath)
 	})
 }
 
@@ -319,3 +321,152 @@ func actionFlags(cmd *cobra.Command) Action {
 		return ActionValuesDescribed(vals...)
 	})
 }
+
+// ActionStyleConfig completes style configuration
+//   carapace.Value=blue
+//   carapace.Description=magenta
+func ActionStyleConfig() Action {
+	return ActionMultiParts("=", func(c Context) Action {
+		switch len(c.Parts) {
+		case 0:
+			return ActionMultiParts(".", func(c Context) Action {
+				switch len(c.Parts) {
+				case 0:
+					return ActionValues(config.GetStyleConfigs()...).Invoke(c).Suffix(".").ToA()
+
+				case 1:
+					fields, err := config.GetStyleFields(c.Parts[0])
+					if err != nil {
+						return ActionMessage(err.Error())
+					}
+					return ActionStyledValuesDescribed(fields...).Invoke(c).Suffix("=").ToA()
+
+				default:
+					return ActionValues()
+				}
+			})
+		case 1:
+			return ActionMultiParts(",", func(c Context) Action {
+				return ActionStyles(c.Parts...).Invoke(c).Filter(c.Parts).ToA()
+			})
+		default:
+			return ActionValues()
+		}
+	})
+}
+
+// Actionstyles completes styles
+//   blue
+//   bg-magenta
+func ActionStyles(styles ...string) Action {
+	return ActionCallback(func(c Context) Action {
+		fg := false
+		bg := false
+
+		for _, s := range styles {
+			if strings.HasPrefix(s, "bg-") {
+				bg = true
+			}
+			if s == style.Black ||
+				s == style.Red ||
+				s == style.Green ||
+				s == style.Yellow ||
+				s == style.Blue ||
+				s == style.Magenta ||
+				s == style.Cyan ||
+				s == style.White ||
+				s == style.Gray ||
+				s == style.BrightBlack ||
+				s == style.BrightRed ||
+				s == style.BrightGreen ||
+				s == style.BrightYellow ||
+				s == style.BrightBlue ||
+				s == style.BrightMagenta ||
+				s == style.BrightCyan ||
+				s == style.BrightWhite ||
+				strings.HasPrefix(s, "#") ||
+				strings.HasPrefix(s, "color") ||
+				strings.HasPrefix(s, "fg-") {
+				fg = true
+			}
+		}
+
+		batch := Batch()
+
+		if !fg {
+			batch = append(batch, ActionStyledValues(
+				style.Black, style.Of(append(styles, style.Black)...),
+				style.Red, style.Of(append(styles, style.Red)...),
+				style.Green, style.Of(append(styles, style.Green)...),
+				style.Yellow, style.Of(append(styles, style.Yellow)...),
+				style.Blue, style.Of(append(styles, style.Blue)...),
+				style.Magenta, style.Of(append(styles, style.Magenta)...),
+				style.Cyan, style.Of(append(styles, style.Cyan)...),
+				style.White, style.Of(append(styles, style.White)...),
+				style.Gray, style.Of(append(styles, style.Gray)...),
+
+				style.BrightBlack, style.Of(append(styles, style.BrightBlack)...),
+				style.BrightRed, style.Of(append(styles, style.BrightRed)...),
+				style.BrightGreen, style.Of(append(styles, style.BrightGreen)...),
+				style.BrightYellow, style.Of(append(styles, style.BrightYellow)...),
+				style.BrightBlue, style.Of(append(styles, style.BrightBlue)...),
+				style.BrightMagenta, style.Of(append(styles, style.BrightMagenta)...),
+				style.BrightCyan, style.Of(append(styles, style.BrightCyan)...),
+				style.BrightWhite, style.Of(append(styles, style.BrightWhite)...),
+			))
+
+			if strings.HasPrefix(c.CallbackValue, "color") {
+				for i := 0; i <= 255; i++ {
+					batch = append(batch, ActionStyledValues(
+						fmt.Sprintf("color%v", i), style.Of(append(styles, style.XTerm256Color(uint8(i)))...),
+					))
+				}
+			} else {
+				batch = append(batch, ActionStyledValues("color", style.Of(styles...)))
+			}
+		}
+
+		if !bg {
+			batch = append(batch, ActionStyledValues(
+				style.BgBlack, style.Of(append(styles, style.BgBlack)...),
+				style.BgRed, style.Of(append(styles, style.BgRed)...),
+				style.BgGreen, style.Of(append(styles, style.BgGreen)...),
+				style.BgYellow, style.Of(append(styles, style.BgYellow)...),
+				style.BgBlue, style.Of(append(styles, style.BgBlue)...),
+				style.BgMagenta, style.Of(append(styles, style.BgMagenta)...),
+				style.BgCyan, style.Of(append(styles, style.BgCyan)...),
+				style.BgWhite, style.Of(append(styles, style.BgWhite)...),
+
+				style.BgBrightBlack, style.Of(append(styles, style.BgBrightBlack)...),
+				style.BgBrightRed, style.Of(append(styles, style.BgBrightRed)...),
+				style.BgBrightGreen, style.Of(append(styles, style.BgBrightGreen)...),
+				style.BgBrightYellow, style.Of(append(styles, style.BgBrightYellow)...),
+				style.BgBrightBlue, style.Of(append(styles, style.BgBrightBlue)...),
+				style.BgBrightMagenta, style.Of(append(styles, style.BgBrightMagenta)...),
+				style.BgBrightCyan, style.Of(append(styles, style.BgBrightCyan)...),
+				style.BgBrightWhite, style.Of(append(styles, style.BgBrightWhite)...),
+			))
+
+			if strings.HasPrefix(c.CallbackValue, "bg-color") {
+				for i := 0; i <= 255; i++ {
+					batch = append(batch, ActionStyledValues(
+						fmt.Sprintf("bg-color%v", i), style.Of(append(styles, "bg-"+style.XTerm256Color(uint8(i)))...),
+					))
+				}
+			} else {
+				batch = append(batch, ActionStyledValues("bg-color", style.Of(styles...)))
+			}
+		}
+
+		batch = append(batch, ActionStyledValues(
+			style.Bold, style.Of(append(styles, style.Bold)...),
+			style.Dim, style.Of(append(styles, style.Dim)...),
+			style.Italic, style.Of(append(styles, style.Italic)...),
+			style.Underlined, style.Of(append(styles, style.Underlined)...),
+			style.Blink, style.Of(append(styles, style.Blink)...),
+			style.Inverse, style.Of(append(styles, style.Inverse)...),
+		))
+
+		return batch.ToA()
+	})
+}
diff --git a/example/cmd/action.go b/example/cmd/action.go
index caf30b1..c827d11 100644
--- a/example/cmd/action.go
+++ b/example/cmd/action.go
@@ -1,6 +1,8 @@
 package cmd
 
 import (
+	"strings"
+
 	"github.com/rsteube/carapace"
 	"github.com/rsteube/carapace/example/cmd/action/net"
 	"github.com/rsteube/carapace/example/cmd/action/os"
@@ -12,7 +14,14 @@ var actionCmd = &cobra.Command{
 	Use:     "action",
 	Short:   "action example",
 	Aliases: []string{"alias"},
-	Run:     func(cmd *cobra.Command, args []string) {},
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if f := cmd.Flag("style"); f.Changed {
+			if splitted := strings.Split(f.Value.String(), "="); len(splitted) == 2 {
+				return style.Set(splitted[0], strings.Replace(splitted[1], ",", " ", -1))
+			}
+		}
+		return nil
+	},
 }
 
 func init() {
@@ -34,6 +43,7 @@ func init() {
 	//actionCmd.Flags().StringS("shorthandonly", "s", "", "shorthandonly flag")
 	actionCmd.Flags().StringP("kill", "k", "", "kill signals")
 	actionCmd.Flags().StringP("optarg", "o", "", "optional arg with default value blue")
+	actionCmd.Flags().String("style", "", "set style")
 	actionCmd.Flag("optarg").NoOptDefVal = "blue"
 
 	carapace.Gen(actionCmd).FlagCompletion(carapace.ActionMap{
@@ -83,6 +93,7 @@ func init() {
 		),
 		"kill":   os.ActionKillSignals(),
 		"optarg": carapace.ActionValues("blue", "red", "green", "yellow"),
+		"style":  carapace.ActionStyleConfig(),
 	})
 
 	carapace.Gen(actionCmd).PositionalCompletion(
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..b23bdcc
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,133 @@
+package config
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"reflect"
+	"strings"
+)
+
+type configMap map[string]interface{}
+
+func (c configMap) Keys() []string {
+	keys := make([]string, 0)
+	for k := range c {
+		keys = append(keys, k)
+	}
+	return keys
+}
+
+func (c configMap) Fields(name string) ([]string, error) {
+	if i, ok := c[name]; ok {
+		fields := make([]string, 0)
+		t := reflect.TypeOf(i).Elem()
+		v := reflect.ValueOf(i).Elem()
+		for index := 0; index < t.NumField(); index++ {
+			field := t.Field(index)
+			if field.Type.Name() != "string" {
+				return nil, fmt.Errorf("invalid field type [name: '%v', type: '%v']", field.Name, field.Type.Name())
+			}
+			fields = append(fields, field.Name, field.Tag.Get("desc"), v.FieldByName(field.Name).String())
+		}
+		return fields, nil
+	}
+	return nil, fmt.Errorf("unknown config: '%v'", name)
+}
+
+var config = struct {
+	Styles configMap
+}{
+	Styles: make(configMap),
+}
+
+func RegisterStyle(name string, i interface{}) {
+	config.Styles[name] = i
+}
+
+func Load() error {
+	if err := load("styles", config.Styles); err != nil {
+		return err
+	}
+	return nil
+}
+
+func load(name string, c configMap) error {
+	if dir, err := os.UserConfigDir(); err == nil {
+		content, err := os.ReadFile(fmt.Sprintf("%v/carapace/%v.json", dir, name))
+		if err != nil {
+			if os.IsNotExist(err) {
+				return nil
+			}
+			return err
+		}
+
+		var unmarshalled map[string]map[string]string
+		if err := json.Unmarshal(content, &unmarshalled); err != nil {
+			return err
+		}
+
+		for key, value := range unmarshalled {
+			if s, ok := c[key]; ok {
+				elem := reflect.ValueOf(s).Elem()
+				for k, v := range value {
+					if field := elem.FieldByName(k); field != (reflect.Value{}) {
+						field.SetString(v)
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func GetStyleConfigs() []string                    { return config.Styles.Keys() }
+func GetStyleFields(name string) ([]string, error) { return config.Styles.Fields(name) }
+func SetStyle(key, value string) error {
+	return set("styles", key, strings.Replace(value, ",", " ", -1))
+}
+
+func set(name, key, value string) error {
+	dir, err := os.UserConfigDir()
+	if err != nil {
+		return err
+	}
+
+	file := fmt.Sprintf("%v/carapace/%v.json", dir, name)
+	content, err := os.ReadFile(file)
+	if err != nil {
+		if !os.IsNotExist(err) {
+			return err
+		}
+		os.MkdirAll(filepath.Dir(file), os.ModePerm)
+		content = []byte("{}")
+	}
+
+	var config map[string]map[string]string
+	if err := json.Unmarshal(content, &config); err != nil {
+		return err
+	}
+
+	if splitted := strings.Split(key, "."); len(splitted) != 2 {
+		return errors.New("invalid key")
+	} else {
+		if _, ok := config[splitted[0]]; !ok {
+			config[splitted[0]] = make(map[string]string, 0)
+		}
+		if strings.TrimSpace(value) == "" {
+			delete(config[splitted[0]], splitted[1])
+		} else {
+			config[splitted[0]][splitted[1]] = value
+		}
+	}
+
+	marshalled, err := json.MarshalIndent(config, "", "  ")
+	if err != nil {
+		return err
+	}
+	os.WriteFile(file, marshalled, os.ModePerm)
+
+	return nil
+}
diff --git a/internal/shell/elvish/action.go b/internal/shell/elvish/action.go
index f94709e..6da5f69 100644
--- a/internal/shell/elvish/action.go
+++ b/internal/shell/elvish/action.go
@@ -5,6 +5,8 @@ import (
 	"strings"
 
 	"github.com/rsteube/carapace/internal/common"
+	"github.com/rsteube/carapace/pkg/style"
+	"github.com/rsteube/carapace/third_party/github.com/elves/elvish/pkg/ui"
 )
 
 var sanitizer = strings.NewReplacer(
@@ -22,11 +24,12 @@ func sanitize(values []common.RawValue) []common.RawValue {
 }
 
 type complexCandidate struct {
-	Value       string
-	Display     string
-	Description string
-	CodeSuffix  string
-	Style       string
+	Value            string
+	Display          string
+	Description      string
+	DescriptionStyle string
+	CodeSuffix       string
+	Style            string
 }
 
 // ActionRawValues formats values for elvish
@@ -36,12 +39,22 @@ func ActionRawValues(currentWord string, nospace bool, values common.RawValues)
 		suffix = ""
 	}
 
+	valueStyle := "default"
+	if s := style.Carapace.Value; s != "" && ui.ParseStyling(s) != nil {
+		valueStyle = s
+	}
+
+	descriptionStyle := "default"
+	if s := style.Carapace.Description; s != "" && ui.ParseStyling(s) != nil {
+		descriptionStyle = s
+	}
+
 	vals := make([]complexCandidate, len(values))
 	for index, val := range sanitize(values) {
-		if val.Style == "" {
-			val.Style = "default"
+		if val.Style == "" || ui.ParseStyling(val.Style) == nil {
+			val.Style = valueStyle
 		}
-		vals[index] = complexCandidate{Value: val.Value, Display: val.Display, Description: val.Description, CodeSuffix: suffix, Style: val.Style}
+		vals[index] = complexCandidate{Value: val.Value, Display: val.Display, Description: val.Description, CodeSuffix: suffix, Style: val.Style, DescriptionStyle: descriptionStyle}
 	}
 	m, _ := json.Marshal(vals)
 	return string(m)
diff --git a/internal/shell/elvish/snippet.go b/internal/shell/elvish/snippet.go
index 68fd980..0e3a8d1 100644
--- a/internal/shell/elvish/snippet.go
+++ b/internal/shell/elvish/snippet.go
@@ -3,6 +3,7 @@ package elvish
 
 import (
 	"fmt"
+
 	"github.com/rsteube/carapace/internal/uid"
 	"github.com/spf13/cobra"
 )
@@ -14,7 +15,7 @@ func Snippet(cmd *cobra.Command) string {
         if (eq $c[Description] "") {
             edit:complex-candidate $c[Value] &display=(styled $c[Display] $c[Style]) &code-suffix=$c[CodeSuffix]
         } else {
-            edit:complex-candidate $c[Value] &display=(styled $c[Display] $c[Style])" ("(styled $c[Description] magenta)")" &code-suffix=$c[CodeSuffix]
+            edit:complex-candidate $c[Value] &display=(styled $c[Display] $c[Style])" ("(styled $c[Description] $c[DescriptionStyle])")" &code-suffix=$c[CodeSuffix]
         }
     }
 }
diff --git a/internal/shell/zsh/action.go b/internal/shell/zsh/action.go
index eca899e..89ad329 100644
--- a/internal/shell/zsh/action.go
+++ b/internal/shell/zsh/action.go
@@ -45,7 +45,11 @@ func ActionRawValues(currentWord string, nospace bool, values common.RawValues)
 		}
 
 		if strings.TrimSpace(val.Description) == "" {
-			vals[index] = fmt.Sprintf("%v\t%v", val.Value, val.Display)
+			if hasDescriptions {
+				vals[index] = fmt.Sprintf("%v\t%v\002", val.Value, val.Display)
+			} else {
+				vals[index] = fmt.Sprintf("%v\t%v", val.Value, val.Display)
+			}
 		} else {
 			vals[index] = fmt.Sprintf("%v\t%v\002 %v-- %v", val.Value, val.Display, strings.Repeat(" ", maxLength-len(val.Display)), val.TrimmedDescription())
 		}
@@ -54,7 +58,8 @@ func ActionRawValues(currentWord string, nospace bool, values common.RawValues)
 	if len(zstyles) > 1000 { // TODO disable styling for large amount of values (bad performance)
 		zstyles = make([]string, 0)
 	}
-	return fmt.Sprintf(":%v\n%v", strings.Join(zstyles, ":"), strings.Join(vals, "\n")) // first line is intentionally never empty (single `:`) for snippet
+	zstyles = append(zstyles, fmt.Sprintf("=(#b)(*)(\002*)=0=%v=%v", style.SGR(style.Carapace.Value), style.SGR(style.Carapace.Description)))
+	return fmt.Sprintf("%v\n%v", strings.Join(zstyles, ":"), strings.Join(vals, "\n"))
 }
 
 // formatZstyle creates a zstyle matcher for given display stings.
@@ -62,7 +67,7 @@ func ActionRawValues(currentWord string, nospace bool, values common.RawValues)
 // To ease matching in list mode, the display values have a hidden `\002` suffix.
 func formatZstyle(s, _style string) string {
 	if sgr := style.SGR(_style); sgr != "" {
-		return fmt.Sprintf("=(#b)(%v)((\002*|))=0=%v", strings.Replace(s, "#", `\#`, -1), sgr)
+		return fmt.Sprintf("=(#b)(%v)(\002*|)=0=%v=%v", strings.Replace(s, "#", `\#`, -1), sgr, style.SGR(style.Carapace.Description))
 	}
 	return ""
 }
diff --git a/invokedAction.go b/invokedAction.go
index c5d7e88..b75a4cc 100644
--- a/invokedAction.go
+++ b/invokedAction.go
@@ -111,7 +111,7 @@ func (a InvokedAction) ToMultiPartsA(divider string) Action {
 							Value:       v,
 							Display:     v,
 							Description: "",
-							Style:       val.Style,
+							Style:       "",
 						}
 					}
 				}
diff --git a/pkg/style/config.go b/pkg/style/config.go
new file mode 100644
index 0000000..e0b561a
--- /dev/null
+++ b/pkg/style/config.go
@@ -0,0 +1,35 @@
+package style
+
+import (
+	"github.com/rsteube/carapace/internal/config"
+)
+
+// Register a style configuration
+//   var Carapace = struct {
+//   	Value       string `desc:"default style for values"`
+//   	Description string `desc:"default style for descriptions"`
+//   }{
+//   	Value:       Default,
+//   	Description: Gray,
+//   }
+//
+//   func init() {
+//   	Register("carapace", &Carapace)
+//   }
+func Register(name string, i interface{}) { config.RegisterStyle(name, i) }
+
+// Set a style
+//   Set("carapace.Value", "bold magenta")
+func Set(key, value string) error { return config.SetStyle(key, value) }
+
+var Carapace = struct {
+	Value       string `desc:"default style for values"`
+	Description string `desc:"default style for descriptions"`
+}{
+	Value:       Default,
+	Description: Gray,
+}
+
+func init() {
+	Register("carapace", &Carapace)
+}
diff --git a/pkg/style/keyword.go b/pkg/style/keyword.go
new file mode 100644
index 0000000..7171582
--- /dev/null
+++ b/pkg/style/keyword.go
@@ -0,0 +1,58 @@
+package style
+
+import "strings"
+
+var keywords = map[string]string{
+	"yes": Green,
+	"no":  Red,
+
+	"true":  Green,
+	"false": Red,
+
+	"on":  Green,
+	"off": Red,
+
+	"all":  Green,
+	"none": Red,
+
+	"always": Green,
+	"auto":   Yellow,
+	"never":  Red,
+
+	"start":      Red,
+	"started":    Red,
+	"running":    Yellow,
+	"inprogress": Yellow,
+	"pause":      Yellow,
+	"paused":     Yellow,
+	"stop":       Red,
+	"stopped":    Red,
+
+	"onsuccess": Green,
+	"onfailure": Red,
+	"onerror":   Red,
+
+	"success": Green,
+	"unknown": Gray,
+	"warn":    Yellow,
+	"error":   Red,
+	"failed":  Red,
+
+	"nonblock": Yellow,
+	"block":    Red,
+
+	"ondemand": Yellow,
+}
+
+var keywordReplacer = strings.NewReplacer(
+	"-", "",
+	"_", "",
+)
+
+// ForKeyword returns the style for given keyword
+func ForKeyword(s string) string {
+	if _style, ok := keywords[keywordReplacer.Replace(strings.ToLower(s))]; ok {
+		return _style
+	}
+	return Default
+}
diff --git a/pkg/style/path.go b/pkg/style/path.go
new file mode 100644
index 0000000..95e34e2
--- /dev/null
+++ b/pkg/style/path.go
@@ -0,0 +1,44 @@
+package style
+
+import (
+	"github.com/rsteube/carapace/third_party/github.com/elves/elvish/pkg/cli/lscolors"
+	"github.com/rsteube/carapace/third_party/github.com/elves/elvish/pkg/ui"
+)
+
+// ForPath returns the style for given path
+//   /tmp/locally/reachable/file.txt
+func ForPath(path string) string { return fromSGR(lscolors.GetColorist().GetStyle(path)) }
+
+// ForPath returns the style for given path by extension only
+//   /tmp/non/existing/file.txt
+func ForPathExt(path string) string { return fromSGR(lscolors.GetColorist().GetStyleExt(path)) }
+
+func fromSGR(sgr string) string {
+	s := ui.StyleFromSGR(sgr)
+	result := []string{"fg-default", "bg-default"}
+	if s.Foreground != nil {
+		result = append(result, s.Foreground.String())
+	}
+	if s.Background != nil {
+		result = append(result, "bg-"+s.Background.String())
+	}
+	if s.Bold {
+		result = append(result, Bold)
+	}
+	if s.Dim {
+		result = append(result, Dim)
+	}
+	if s.Italic {
+		result = append(result, Italic)
+	}
+	if s.Underlined {
+		result = append(result, Underlined)
+	}
+	if s.Blink {
+		result = append(result, Blink)
+	}
+	if s.Inverse {
+		result = append(result, Inverse)
+	}
+	return Of(result...)
+}
diff --git a/pkg/style/style.go b/pkg/style/style.go
index 5dcb797..7e0e9ce 100644
--- a/pkg/style/style.go
+++ b/pkg/style/style.go
@@ -4,7 +4,6 @@ package style
 import (
 	"strings"
 
-	"github.com/rsteube/carapace/third_party/github.com/elves/elvish/pkg/cli/lscolors"
 	"github.com/rsteube/carapace/third_party/github.com/elves/elvish/pkg/ui"
 )
 
@@ -57,9 +56,7 @@ var (
 )
 
 // Of combines different styles
-func Of(s ...string) string {
-	return strings.Join(s, " ")
-}
+func Of(s ...string) string { return strings.Join(s, " ") }
 
 // XTerm256Color returns a color from the xterm 256-color palette.
 func XTerm256Color(i uint8) string { return ui.XTerm256Color(i).String() }
@@ -67,47 +64,15 @@ func XTerm256Color(i uint8) string { return ui.XTerm256Color(i).String() }
 // TrueColor returns a 24-bit true color.
 func TrueColor(r, g, b uint8) string { return ui.TrueColor(r, g, b).String() }
 
-// ForPath returns the style for given path
-func ForPath(path string) string {
-	s := ui.StyleFromSGR(lscolors.GetColorist().GetStyle(path))
-
-	result := make([]string, 0)
-	if s.Foreground != nil {
-		result = append(result, s.Foreground.String())
-	}
-	if s.Background != nil {
-		result = append(result, "bg-"+s.Background.String())
-	}
-	if s.Bold {
-		result = append(result, Bold)
-	}
-	if s.Dim {
-		result = append(result, Dim)
-	}
-	if s.Italic {
-		result = append(result, Italic)
-	}
-	if s.Underlined {
-		result = append(result, Underlined)
-	}
-	if s.Blink {
-		result = append(result, Blink)
-	}
-	if s.Inverse {
-		result = append(result, Inverse)
-	}
-	return Of(result...)
-}
-
 // SGR returns the SGR sequence for given style
-func SGR(s string) string {
-	return parseStyle(s).SGR()
-}
+func SGR(s string) string { return parseStyle(s).SGR() }
 
 func parseStyle(s string) ui.Style {
 	stylings := make([]ui.Styling, 0)
 	for _, word := range strings.Split(s, " ") {
-		stylings = append(stylings, ui.ParseStyling(word))
+		if styling := ui.ParseStyling(word); styling != nil {
+			stylings = append(stylings, styling)
+		}
 	}
 	return ui.ApplyStyling(ui.Style{}, stylings...)
 }
diff --git a/third_party/github.com/elves/elvish/pkg/cli/lscolors/lscolors.go b/third_party/github.com/elves/elvish/pkg/cli/lscolors/lscolors.go
index 04df746..8b0ceb2 100644
--- a/third_party/github.com/elves/elvish/pkg/cli/lscolors/lscolors.go
+++ b/third_party/github.com/elves/elvish/pkg/cli/lscolors/lscolors.go
@@ -16,6 +16,8 @@ import (
 type Colorist interface {
 	// GetStyle returns the style for the named file.
 	GetStyle(fname string) string
+	// GetStyle returns the style for the named file only by its extension.
+	GetStyleExt(fname string) string
 }
 
 type colorist struct {
@@ -122,3 +124,14 @@ func (lc *colorist) GetStyle(fname string) string {
 	}
 	return lc.styleForFeature[feature]
 }
+
+func (lc *colorist) GetStyleExt(fname string) string {
+	if !strings.HasSuffix(fname, "/") {
+		if ext := path.Ext(fname); ext != "" {
+			if style, ok := lc.styleForExt[ext]; ok {
+				return style
+			}
+		}
+	}
+	return lc.styleForFeature[featureDirectory]
+}


Test patch:
diff --git a/action_test.go b/action_test.go
index c593b0d..f1fa3a8 100644
--- a/action_test.go
+++ b/action_test.go
@@ -108,38 +108,38 @@ func TestNoSpace(t *testing.T) {
 func TestActionDirectories(t *testing.T) {
 	assertEqual(t,
 		ActionStyledValues(
-			"example/", style.Of(style.Blue, style.Bold),
-			"docs/", style.Of(style.Blue, style.Bold),
-			"internal/", style.Of(style.Blue, style.Bold),
-			"pkg/", style.Of(style.Blue, style.Bold),
-			"third_party/", style.Of(style.Blue, style.Bold),
+			"example/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"docs/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"internal/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"pkg/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"third_party/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
 		).noSpace(true).Invoke(Context{}),
 		ActionDirectories().Invoke(Context{CallbackValue: ""}).Filter([]string{"vendor/"}),
 	)
 
 	assertEqual(t,
 		ActionStyledValues(
-			"example/", style.Of(style.Blue, style.Bold),
-			"docs/", style.Of(style.Blue, style.Bold),
-			"internal/", style.Of(style.Blue, style.Bold),
-			"pkg/", style.Of(style.Blue, style.Bold),
-			"third_party/", style.Of(style.Blue, style.Bold),
+			"example/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"docs/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"internal/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"pkg/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"third_party/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
 		).noSpace(true).Invoke(Context{}).Prefix("./"),
 		ActionDirectories().Invoke(Context{CallbackValue: "./"}).Filter([]string{"./vendor/"}),
 	)
 
 	assertEqual(t,
 		ActionStyledValues(
-			"_test/", style.Of(style.Blue, style.Bold),
-			"cmd/", style.Of(style.Blue, style.Bold),
+			"_test/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"cmd/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
 		).noSpace(true).Invoke(Context{}).Prefix("example/"),
 		ActionDirectories().Invoke(Context{CallbackValue: "example/"}),
 	)
 
 	assertEqual(t,
 		ActionStyledValues(
-			"_test/", style.Of(style.Blue, style.Bold),
-			"cmd/", style.Of(style.Blue, style.Bold),
+			"_test/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"cmd/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
 		).noSpace(true).Invoke(Context{}).Prefix("example/"),
 		ActionDirectories().Invoke(Context{CallbackValue: "example/cm"}),
 	)
@@ -148,22 +148,22 @@ func TestActionDirectories(t *testing.T) {
 func TestActionFiles(t *testing.T) {
 	assertEqual(t,
 		ActionStyledValues(
-			"README.md", style.Default,
-			"example/", style.Of(style.Blue, style.Bold),
-			"docs/", style.Of(style.Blue, style.Bold),
-			"internal/", style.Of(style.Blue, style.Bold),
-			"pkg/", style.Of(style.Blue, style.Bold),
-			"third_party/", style.Of(style.Blue, style.Bold),
+			"README.md", style.Of("fg-default", "bg-default"),
+			"example/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"docs/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"internal/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"pkg/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"third_party/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
 		).noSpace(true).Invoke(Context{}),
 		ActionFiles(".md").Invoke(Context{CallbackValue: ""}).Filter([]string{"vendor/"}),
 	)
 
 	assertEqual(t,
 		ActionStyledValues(
-			"_test/", style.Of(style.Blue, style.Bold),
-			"cmd/", style.Of(style.Blue, style.Bold),
-			"main.go", style.Default,
-			"main_test.go", style.Default,
+			"_test/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"cmd/", style.Of("fg-default", "bg-default", style.Blue, style.Bold),
+			"main.go", style.Of("fg-default", "bg-default"),
+			"main_test.go", style.Of("fg-default", "bg-default"),
 		).noSpace(true).Invoke(Context{}).Prefix("example/"),
 		ActionFiles().Invoke(Context{CallbackValue: "example/"}).Filter([]string{"example/example"}),
 	)
@@ -183,7 +183,10 @@ func TestActionFilesChdir(t *testing.T) {
 	)
 
 	assertEqual(t,
-		ActionValues("action.go", "snippet.go").noSpace(true).Invoke(Context{}).Prefix("elvish/"),
+		ActionStyledValues(
+			"action.go", style.Of("fg-default", "bg-default"),
+			"snippet.go", style.Of("fg-default", "bg-default"),
+		).noSpace(true).Invoke(Context{}).Prefix("elvish/"),
 		ActionFiles().Chdir("internal/shell").Invoke(Context{CallbackValue: "elvish/"}),
 	)
 
diff --git a/carapace_test.go b/carapace_test.go
index a77f7d5..ade01ff 100644
--- a/carapace_test.go
+++ b/carapace_test.go
@@ -231,7 +231,7 @@ func TestComplete(t *testing.T) {
 	cmd.Flags().BoolP("a", "1", false, "")
 	cmd.Flags().BoolP("b", "2", false, "")
 
-	if s, err := complete(cmd, []string{"elvish", "_", "test", "-1"}); err != nil || s != `[{"Value":"-12","Display":"2","Description":"","CodeSuffix":"","Style":"default"}]` {
+	if s, err := complete(cmd, []string{"elvish", "_", "test", "-1"}); err != nil || s != `[{"Value":"-12","Display":"2","Description":"","DescriptionStyle":"dim white","CodeSuffix":"","Style":"default"}]` {
 		t.Error(s)
 	}
 }
@@ -247,7 +247,7 @@ func TestCompleteOptarg(t *testing.T) {
 		"opt": ActionValuesDescribed("value", "description"),
 	})
 
-	if s, err := complete(cmd, []string{"elvish", "_", "test", "--opt="}); err != nil || s != `[{"Value":"--opt=value","Display":"value","Description":"description","CodeSuffix":" ","Style":"default"}]` {
+	if s, err := complete(cmd, []string{"elvish", "_", "test", "--opt="}); err != nil || s != `[{"Value":"--opt=value","Display":"value","Description":"description","DescriptionStyle":"dim white","CodeSuffix":" ","Style":"default"}]` {
 		t.Error(s)
 	}
 }
diff --git a/example/cmd/_test/elvish.elv b/example/cmd/_test/elvish.elv
index 96f3e1a..e40cc5a 100644
--- a/example/cmd/_test/elvish.elv
+++ b/example/cmd/_test/elvish.elv
@@ -3,7 +3,7 @@ set edit:completion:arg-completer[example] = {|@arg|
         if (eq $c[Description] "") {
             edit:complex-candidate $c[Value] &display=(styled $c[Display] $c[Style]) &code-suffix=$c[CodeSuffix]
         } else {
-            edit:complex-candidate $c[Value] &display=(styled $c[Display] $c[Style])" ("(styled $c[Description] magenta)")" &code-suffix=$c[CodeSuffix]
+            edit:complex-candidate $c[Value] &display=(styled $c[Display] $c[Style])" ("(styled $c[Description] $c[DescriptionStyle])")" &code-suffix=$c[CodeSuffix]
         }
     }
 }