Add support for configuring the TUI color scheme, for #134 (#146)

* Add support for configuring the TUI color scheme, for #134

* Add tests for getting and setting the custom color scheme, and support full colors where terminals support them

* Add comments to document termenv.ANSI setting, and fix tests so they work uniformly
This commit is contained in:
David Dworken 2023-12-18 20:32:11 -08:00 committed by GitHub
parent 49fd540014
commit 8b7e54eab4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 167 additions and 8 deletions

View File

@ -1761,6 +1761,15 @@ func testTui_color(t *testing.T) {
out = captureTerminalOutputComplex(t, TmuxCaptureConfig{tester: tester, complexCommands: []TmuxCommand{{Keys: "hishtory SPACE tquery ENTER"}, {Keys: "ech"}}, includeEscapeSequences: true}) out = captureTerminalOutputComplex(t, TmuxCaptureConfig{tester: tester, complexCommands: []TmuxCommand{{Keys: "hishtory SPACE tquery ENTER"}, {Keys: "ech"}}, includeEscapeSequences: true})
out = stripTuiCommandPrefix(t, out) out = stripTuiCommandPrefix(t, out)
testutils.CompareGoldens(t, out, "TestTui-ColoredOutputWithSearch-BetaMode") testutils.CompareGoldens(t, out, "TestTui-ColoredOutputWithSearch-BetaMode")
// And one more time with customized colors
testutils.CompareGoldens(t, tester.RunInteractiveShell(t, ` hishtory config-get color-scheme`), "TestTui-DefaultColorScheme")
tester.RunInteractiveShell(t, ` hishtory config-set color-scheme selected-text #45f542`)
tester.RunInteractiveShell(t, ` hishtory config-set color-scheme selected-background #4842f5`)
tester.RunInteractiveShell(t, ` hishtory config-set color-scheme border-color #f54272`)
out = captureTerminalOutputComplex(t, TmuxCaptureConfig{tester: tester, complexCommands: []TmuxCommand{{Keys: "hishtory SPACE tquery ENTER"}, {Keys: "ech"}}, includeEscapeSequences: true})
out = stripTuiCommandPrefix(t, out)
testutils.CompareGoldens(t, out, "TestTui-CustomColorScheme")
} }
func testTui_delete(t *testing.T) { func testTui_delete(t *testing.T) {

View File

@ -2,6 +2,7 @@ package cmd
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/ddworken/hishtory/client/hctx" "github.com/ddworken/hishtory/client/hctx"
@ -15,6 +16,7 @@ var configAddCmd = &cobra.Command{
GroupID: GROUP_ID_CONFIG, GroupID: GROUP_ID_CONFIG,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
lib.CheckFatalError(cmd.Help()) lib.CheckFatalError(cmd.Help())
os.Exit(1)
}, },
} }

View File

@ -2,6 +2,7 @@ package cmd
import ( import (
"log" "log"
"os"
"github.com/ddworken/hishtory/client/hctx" "github.com/ddworken/hishtory/client/hctx"
"github.com/ddworken/hishtory/client/lib" "github.com/ddworken/hishtory/client/lib"
@ -14,6 +15,7 @@ var configDeleteCmd = &cobra.Command{
GroupID: GROUP_ID_CONFIG, GroupID: GROUP_ID_CONFIG,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
lib.CheckFatalError(cmd.Help()) lib.CheckFatalError(cmd.Help())
os.Exit(1)
}, },
} }

View File

@ -2,6 +2,7 @@ package cmd
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/ddworken/hishtory/client/hctx" "github.com/ddworken/hishtory/client/hctx"
@ -17,6 +18,7 @@ var configGetCmd = &cobra.Command{
GroupID: GROUP_ID_CONFIG, GroupID: GROUP_ID_CONFIG,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
lib.CheckFatalError(cmd.Help()) lib.CheckFatalError(cmd.Help())
os.Exit(1)
}, },
} }
@ -121,6 +123,18 @@ var getCustomColumnsCmd = &cobra.Command{
}, },
} }
var getColorScheme = &cobra.Command{
Use: "color-scheme",
Short: "Get the currently configured color scheme for selected text in the TUI",
Run: func(cmd *cobra.Command, args []string) {
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
fmt.Println("selected-text: " + config.ColorScheme.SelectedText)
fmt.Println("selected-background: " + config.ColorScheme.SelectedBackground)
fmt.Println("border-color: " + config.ColorScheme.BorderColor)
},
}
func init() { func init() {
rootCmd.AddCommand(configGetCmd) rootCmd.AddCommand(configGetCmd)
configGetCmd.AddCommand(getEnableControlRCmd) configGetCmd.AddCommand(getEnableControlRCmd)
@ -132,4 +146,5 @@ func init() {
configGetCmd.AddCommand(getHighlightMatchesCmd) configGetCmd.AddCommand(getHighlightMatchesCmd)
configGetCmd.AddCommand(getEnableAiCompletion) configGetCmd.AddCommand(getEnableAiCompletion)
configGetCmd.AddCommand(getPresavingCmd) configGetCmd.AddCommand(getPresavingCmd)
configGetCmd.AddCommand(getColorScheme)
} }

View File

@ -3,6 +3,8 @@ package cmd
import ( import (
"fmt" "fmt"
"log" "log"
"os"
"strings"
"github.com/ddworken/hishtory/client/hctx" "github.com/ddworken/hishtory/client/hctx"
"github.com/ddworken/hishtory/client/lib" "github.com/ddworken/hishtory/client/lib"
@ -15,6 +17,7 @@ var configSetCmd = &cobra.Command{
GroupID: GROUP_ID_CONFIG, GroupID: GROUP_ID_CONFIG,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
lib.CheckFatalError(cmd.Help()) lib.CheckFatalError(cmd.Help())
os.Exit(1)
}, },
} }
@ -146,6 +149,61 @@ var setTimestampFormatCmd = &cobra.Command{
}, },
} }
var setColorSchemeCmd = &cobra.Command{
Use: "color-scheme",
Short: "Set a custom color scheme",
Run: func(cmd *cobra.Command, args []string) {
lib.CheckFatalError(cmd.Help())
os.Exit(1)
},
}
var setColorSchemeSelectedText = &cobra.Command{
Use: "selected-text",
Short: "Set the color of the selected text to the given hexadecimal color",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
lib.CheckFatalError(validateColor(args[0]))
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
config.ColorScheme.SelectedText = args[0]
lib.CheckFatalError(hctx.SetConfig(config))
},
}
var setColorSchemeSelectedBackground = &cobra.Command{
Use: "selected-background",
Short: "Set the background color of the selected row to the given hexadecimal color",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
lib.CheckFatalError(validateColor(args[0]))
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
config.ColorScheme.SelectedBackground = args[0]
lib.CheckFatalError(hctx.SetConfig(config))
},
}
var setColorSchemeBorderColor = &cobra.Command{
Use: "border-color",
Short: "Set the color of the table borders",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
lib.CheckFatalError(validateColor(args[0]))
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
config.ColorScheme.BorderColor = args[0]
lib.CheckFatalError(hctx.SetConfig(config))
},
}
func validateColor(color string) error {
if !strings.HasPrefix(color, "#") || len(color) != 7 {
return fmt.Errorf("color %q is invalid, it should be a hexadecimal color like #663399", color)
}
return nil
}
func init() { func init() {
rootCmd.AddCommand(configSetCmd) rootCmd.AddCommand(configSetCmd)
configSetCmd.AddCommand(setEnableControlRCmd) configSetCmd.AddCommand(setEnableControlRCmd)
@ -156,4 +214,8 @@ func init() {
configSetCmd.AddCommand(setHighlightMatchesCmd) configSetCmd.AddCommand(setHighlightMatchesCmd)
configSetCmd.AddCommand(setEnableAiCompletionCmd) configSetCmd.AddCommand(setEnableAiCompletionCmd)
configSetCmd.AddCommand(setPresavingCmd) configSetCmd.AddCommand(setPresavingCmd)
configSetCmd.AddCommand(setColorSchemeCmd)
setColorSchemeCmd.AddCommand(setColorSchemeSelectedText)
setColorSchemeCmd.AddCommand(setColorSchemeSelectedBackground)
setColorSchemeCmd.AddCommand(setColorSchemeBorderColor)
} }

View File

@ -201,6 +201,14 @@ type ClientConfig struct {
AiCompletion bool `json:"ai_completion"` AiCompletion bool `json:"ai_completion"`
// Whether to enable presaving // Whether to enable presaving
EnablePresaving bool `json:"enable_presaving"` EnablePresaving bool `json:"enable_presaving"`
// The current color scheme for the TUI
ColorScheme ColorScheme `json:"color_scheme"`
}
type ColorScheme struct {
SelectedText string
SelectedBackground string
BorderColor string
} }
type CustomColumnDefinition struct { type CustomColumnDefinition struct {
@ -229,6 +237,14 @@ func GetConfigContents() ([]byte, error) {
return dat, nil return dat, nil
} }
func GetDefaultColorScheme() ColorScheme {
return ColorScheme{
SelectedBackground: "#3300ff",
SelectedText: "#ffff99",
BorderColor: "#585858",
}
}
func GetConfig() (ClientConfig, error) { func GetConfig() (ClientConfig, error) {
data, err := GetConfigContents() data, err := GetConfigContents()
if err != nil { if err != nil {
@ -245,6 +261,15 @@ func GetConfig() (ClientConfig, error) {
if config.TimestampFormat == "" { if config.TimestampFormat == "" {
config.TimestampFormat = "Jan 2 2006 15:04:05 MST" config.TimestampFormat = "Jan 2 2006 15:04:05 MST"
} }
if config.ColorScheme.SelectedBackground == "" {
config.ColorScheme.SelectedBackground = GetDefaultColorScheme().SelectedBackground
}
if config.ColorScheme.SelectedText == "" {
config.ColorScheme.SelectedText = GetDefaultColorScheme().SelectedText
}
if config.ColorScheme.BorderColor == "" {
config.ColorScheme.BorderColor = GetDefaultColorScheme().BorderColor
}
return config, nil return config, nil
} }

View File

@ -0,0 +1,27 @@
Search Query: > ech 
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
 Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
 localhost  /tmp/  Oct 17 2022 21:43:21 PDT  3s  2  echo 'aaaaaa bbbb'  │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘
hiSHtory: Search your shell history  • ctrl+h help

View File

@ -0,0 +1,3 @@
selected-text: #ffff99
selected-background: #3300ff
border-color: #585858

View File

@ -41,10 +41,6 @@ var LAST_DISPATCHED_QUERY_ID = 0
var LAST_DISPATCHED_QUERY_TIMESTAMP time.Time var LAST_DISPATCHED_QUERY_TIMESTAMP time.Time
var LAST_PROCESSED_QUERY_ID = -1 var LAST_PROCESSED_QUERY_ID = -1
var baseStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240"))
type keyMap struct { type keyMap struct {
Up key.Binding Up key.Binding
Down key.Binding Down key.Binding
@ -456,11 +452,18 @@ func isCompactHeightMode() bool {
return height < 25 return height < 25
} }
func getBaseStyle(config hctx.ClientConfig) lipgloss.Style {
return lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color(config.ColorScheme.BorderColor))
}
func renderNullableTable(m model, helpText string) string { func renderNullableTable(m model, helpText string) string {
if m.table == nil { if m.table == nil {
return strings.Repeat("\n", TABLE_HEIGHT+3) return strings.Repeat("\n", TABLE_HEIGHT+3)
} }
helpTextLen := strings.Count(helpText, "\n") helpTextLen := strings.Count(helpText, "\n")
baseStyle := getBaseStyle(*hctx.GetConf(m.ctx))
if isCompactHeightMode() && helpTextLen > 1 { if isCompactHeightMode() && helpTextLen > 1 {
// If the help text is expanded, and this is a small window, then we truncate the table so that the help text displays on top of it // If the help text is expanded, and this is a small window, then we truncate the table so that the help text displays on top of it
lines := strings.Split(baseStyle.Render(m.table.View()), "\n") lines := strings.Split(baseStyle.Render(m.table.View()), "\n")
@ -692,12 +695,12 @@ func makeTable(ctx context.Context, rows []table.Row) (table.Model, error) {
s := table.DefaultStyles() s := table.DefaultStyles()
s.Header = s.Header. s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()). BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")). BorderForeground(lipgloss.Color(config.ColorScheme.BorderColor)).
BorderBottom(true). BorderBottom(true).
Bold(false) Bold(false)
s.Selected = s.Selected. s.Selected = s.Selected.
Foreground(lipgloss.Color("229")). Foreground(lipgloss.Color(config.ColorScheme.SelectedText)).
Background(lipgloss.Color("57")). Background(lipgloss.Color(config.ColorScheme.SelectedBackground)).
Bold(false) Bold(false)
if config.HighlightMatches { if config.HighlightMatches {
MATCH_NOTHING_REGEXP := regexp.MustCompile("a^") MATCH_NOTHING_REGEXP := regexp.MustCompile("a^")
@ -795,7 +798,18 @@ func deleteHistoryEntry(ctx context.Context, entry data.HistoryEntry) error {
} }
func TuiQuery(ctx context.Context, initialQuery string) error { func TuiQuery(ctx context.Context, initialQuery string) error {
lipgloss.SetColorProfile(termenv.ANSI) if hctx.GetConf(ctx).ColorScheme == hctx.GetDefaultColorScheme() {
// Set termenv.ANSI for the default color scheme, so that we preserve
// the true default color scheme of hishtory which was initially
// configured with termenv.ANSI (even though we want to support
// full colors) for custom color schemes.
lipgloss.SetColorProfile(termenv.ANSI)
} else if os.Getenv("HISHTORY_TEST") != "" {
// We also set termenv.ANSI for tests so as to ensure that all our
// test environments behave the same (by default, github actions
// ubuntu and macos have different termenv support).
lipgloss.SetColorProfile(termenv.ANSI)
}
p := tea.NewProgram(initialModel(ctx, initialQuery), tea.WithOutput(os.Stderr)) p := tea.NewProgram(initialModel(ctx, initialQuery), tea.WithOutput(os.Stderr))
// Async: Get the initial set of rows // Async: Get the initial set of rows
go func() { go func() {