diff --git a/client/client_test.go b/client/client_test.go index c058adb..577dee7 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -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 = stripTuiCommandPrefix(t, out) 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) { diff --git a/client/cmd/configAdd.go b/client/cmd/configAdd.go index 4d8df7b..72af921 100644 --- a/client/cmd/configAdd.go +++ b/client/cmd/configAdd.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "os" "strings" "github.com/ddworken/hishtory/client/hctx" @@ -15,6 +16,7 @@ var configAddCmd = &cobra.Command{ GroupID: GROUP_ID_CONFIG, Run: func(cmd *cobra.Command, args []string) { lib.CheckFatalError(cmd.Help()) + os.Exit(1) }, } diff --git a/client/cmd/configDelete.go b/client/cmd/configDelete.go index da67a38..2e94c70 100644 --- a/client/cmd/configDelete.go +++ b/client/cmd/configDelete.go @@ -2,6 +2,7 @@ package cmd import ( "log" + "os" "github.com/ddworken/hishtory/client/hctx" "github.com/ddworken/hishtory/client/lib" @@ -14,6 +15,7 @@ var configDeleteCmd = &cobra.Command{ GroupID: GROUP_ID_CONFIG, Run: func(cmd *cobra.Command, args []string) { lib.CheckFatalError(cmd.Help()) + os.Exit(1) }, } diff --git a/client/cmd/configGet.go b/client/cmd/configGet.go index 4da07e4..2086dc6 100644 --- a/client/cmd/configGet.go +++ b/client/cmd/configGet.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "os" "strings" "github.com/ddworken/hishtory/client/hctx" @@ -17,6 +18,7 @@ var configGetCmd = &cobra.Command{ GroupID: GROUP_ID_CONFIG, Run: func(cmd *cobra.Command, args []string) { 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() { rootCmd.AddCommand(configGetCmd) configGetCmd.AddCommand(getEnableControlRCmd) @@ -132,4 +146,5 @@ func init() { configGetCmd.AddCommand(getHighlightMatchesCmd) configGetCmd.AddCommand(getEnableAiCompletion) configGetCmd.AddCommand(getPresavingCmd) + configGetCmd.AddCommand(getColorScheme) } diff --git a/client/cmd/configSet.go b/client/cmd/configSet.go index cf1fedc..2977e33 100644 --- a/client/cmd/configSet.go +++ b/client/cmd/configSet.go @@ -3,6 +3,8 @@ package cmd import ( "fmt" "log" + "os" + "strings" "github.com/ddworken/hishtory/client/hctx" "github.com/ddworken/hishtory/client/lib" @@ -15,6 +17,7 @@ var configSetCmd = &cobra.Command{ GroupID: GROUP_ID_CONFIG, Run: func(cmd *cobra.Command, args []string) { 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() { rootCmd.AddCommand(configSetCmd) configSetCmd.AddCommand(setEnableControlRCmd) @@ -156,4 +214,8 @@ func init() { configSetCmd.AddCommand(setHighlightMatchesCmd) configSetCmd.AddCommand(setEnableAiCompletionCmd) configSetCmd.AddCommand(setPresavingCmd) + configSetCmd.AddCommand(setColorSchemeCmd) + setColorSchemeCmd.AddCommand(setColorSchemeSelectedText) + setColorSchemeCmd.AddCommand(setColorSchemeSelectedBackground) + setColorSchemeCmd.AddCommand(setColorSchemeBorderColor) } diff --git a/client/hctx/hctx.go b/client/hctx/hctx.go index c5ed605..75d020e 100644 --- a/client/hctx/hctx.go +++ b/client/hctx/hctx.go @@ -201,6 +201,14 @@ type ClientConfig struct { AiCompletion bool `json:"ai_completion"` // Whether to 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 { @@ -229,6 +237,14 @@ func GetConfigContents() ([]byte, error) { return dat, nil } +func GetDefaultColorScheme() ColorScheme { + return ColorScheme{ + SelectedBackground: "#3300ff", + SelectedText: "#ffff99", + BorderColor: "#585858", + } +} + func GetConfig() (ClientConfig, error) { data, err := GetConfigContents() if err != nil { @@ -245,6 +261,15 @@ func GetConfig() (ClientConfig, error) { if config.TimestampFormat == "" { 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 } diff --git a/client/lib/goldens/TestTui-CustomColorScheme b/client/lib/goldens/TestTui-CustomColorScheme new file mode 100644 index 0000000..300d8ec --- /dev/null +++ b/client/lib/goldens/TestTui-CustomColorScheme @@ -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 \ No newline at end of file diff --git a/client/lib/goldens/TestTui-DefaultColorScheme b/client/lib/goldens/TestTui-DefaultColorScheme new file mode 100644 index 0000000..0df54e4 --- /dev/null +++ b/client/lib/goldens/TestTui-DefaultColorScheme @@ -0,0 +1,3 @@ +selected-text: #ffff99 +selected-background: #3300ff +border-color: #585858 diff --git a/client/tui/tui.go b/client/tui/tui.go index 0410c36..2167636 100644 --- a/client/tui/tui.go +++ b/client/tui/tui.go @@ -41,10 +41,6 @@ var LAST_DISPATCHED_QUERY_ID = 0 var LAST_DISPATCHED_QUERY_TIMESTAMP time.Time var LAST_PROCESSED_QUERY_ID = -1 -var baseStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")) - type keyMap struct { Up key.Binding Down key.Binding @@ -456,11 +452,18 @@ func isCompactHeightMode() bool { 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 { if m.table == nil { return strings.Repeat("\n", TABLE_HEIGHT+3) } helpTextLen := strings.Count(helpText, "\n") + baseStyle := getBaseStyle(*hctx.GetConf(m.ctx)) 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 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.Header = s.Header. BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")). + BorderForeground(lipgloss.Color(config.ColorScheme.BorderColor)). BorderBottom(true). Bold(false) s.Selected = s.Selected. - Foreground(lipgloss.Color("229")). - Background(lipgloss.Color("57")). + Foreground(lipgloss.Color(config.ColorScheme.SelectedText)). + Background(lipgloss.Color(config.ColorScheme.SelectedBackground)). Bold(false) if config.HighlightMatches { 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 { - 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)) // Async: Get the initial set of rows go func() {