diff --git a/README.md b/README.md index f251a07..c2cb8d4 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,19 @@ You can configure a custom timestamp format for hiSHtory via `hishtory config-se +
+Custom rendering
+ +By default, hiHStory tries to render the TUI in a reasonable way that balances terminal space consumption and TUI usability. If you find that you wish to customize this behavior, there are two config options that you can experiment with enabling: + +``` +hishtory config-set compact-mode true # Renders the TUI in "compact mode" with less whitespace +hishtory config-set full-screen true # Renders the TUI in "full-screen mode" so that it uses the entire terminal +``` + +
+ +
Web UI for sharing
diff --git a/client/client_test.go b/client/client_test.go index 9f338bc..d5aeea9 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -119,6 +119,7 @@ func TestParam(t *testing.T) { t.Run("testTui/ai", wrapTestForSharding(testTui_ai)) t.Run("testTui/defaultFilter", wrapTestForSharding(testTui_defaultFilter)) t.Run("testTui/escaping", wrapTestForSharding(testTui_escaping)) + t.Run("testTui/fullscreen", wrapTestForSharding(testTui_fullscreen)) // Assert there are no leaked connections assertNoLeakedConnections(t) @@ -1871,6 +1872,54 @@ func testTui_escaping(t *testing.T) { testutils.CompareGoldens(t, out, "TestTui-Escaping") } +func testTui_fullscreen(t *testing.T) { + // Setup + defer testutils.BackupAndRestore(t)() + tester, _, _ := setupTestTui(t, Online) + + // By default full-screen mode is disabled + require.Equal(t, "false", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get full-screen`))) + require.Equal(t, "false", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get compact-mode`))) + + // Test that we can enable it + tester.RunInteractiveShell(t, `hishtory config-set full-screen true`) + require.Equal(t, "true", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get full-screen`))) + + // Test that it renders in full-screen mode taking up the entire terminal + out := captureTerminalOutput(t, tester, []string{ + "echo foo ENTER", + "hishtory SPACE tquery ENTER", + }) + testutils.CompareGoldens(t, out, "TestTui-FullScreenRender") + + // Test that it clears full-screen mode and restores the original terminal state + out = captureTerminalOutput(t, tester, []string{ + "echo SPACE foo ENTER", + "hishtory SPACE tquery ENTER", + "Escape", + }) + require.Contains(t, out, "echo foo\n") + require.Contains(t, out, "hishtory tquery\n") + require.NotContains(t, out, "Search Query") + require.True(t, len(strings.Split(out, "\n")) <= 7) + + // Test that it renders the help page fine + out = captureTerminalOutput(t, tester, []string{ + "echo SPACE foo ENTER", + "hishtory SPACE tquery ENTER", + "C-h", + }) + testutils.CompareGoldens(t, out, "TestTui-FullScreenHelp") + + // Test that it renders fine in full-screen mode and compact-mode + tester.RunInteractiveShell(t, `hishtory config-set compact-mode true`) + out = captureTerminalOutput(t, tester, []string{ + "echo foo ENTER", + "hishtory SPACE tquery ENTER", + }) + testutils.CompareGoldens(t, out, "TestTui-FullScreenCompactRender") +} + func testTui_defaultFilter(t *testing.T) { // Setup defer testutils.BackupAndRestore(t)() diff --git a/client/cmd/configGet.go b/client/cmd/configGet.go index b777993..acd21ca 100644 --- a/client/cmd/configGet.go +++ b/client/cmd/configGet.go @@ -184,6 +184,7 @@ func init() { configGetCmd.AddCommand(getAiCompletionEndpoint) configGetCmd.AddCommand(getCompactMode) configGetCmd.AddCommand(getLogLevelCmd) + configGetCmd.AddCommand(getFullScreenCmd) } var getLogLevelCmd = &cobra.Command{ @@ -195,3 +196,13 @@ var getLogLevelCmd = &cobra.Command{ fmt.Println(config.LogLevel.String()) }, } + +var getFullScreenCmd = &cobra.Command{ + Use: "full-screen", + Short: "Get whether or not hishtory is configured to run in full-screen mode", + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + fmt.Println(config.FullScreenRendering) + }, +} diff --git a/client/cmd/configSet.go b/client/cmd/configSet.go index 3b7d586..d9e2b31 100644 --- a/client/cmd/configSet.go +++ b/client/cmd/configSet.go @@ -262,6 +262,19 @@ var setLogLevelCmd = &cobra.Command{ }, } +var setFullScreenCmd = &cobra.Command{ + Use: "full-screen", + Short: "Configure whether or not hishtory is configured to run in full-screen mode", + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + ValidArgs: []string{"true", "false"}, + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + config.FullScreenRendering = args[0] == "true" + lib.CheckFatalError(hctx.SetConfig(config)) + }, +} + func init() { rootCmd.AddCommand(configSetCmd) configSetCmd.AddCommand(setEnableControlRCmd) @@ -277,6 +290,7 @@ func init() { configSetCmd.AddCommand(setAiCompletionEndpoint) configSetCmd.AddCommand(compactMode) configSetCmd.AddCommand(setLogLevelCmd) + configSetCmd.AddCommand(setFullScreenCmd) setColorSchemeCmd.AddCommand(setColorSchemeSelectedText) setColorSchemeCmd.AddCommand(setColorSchemeSelectedBackground) setColorSchemeCmd.AddCommand(setColorSchemeBorderColor) diff --git a/client/hctx/hctx.go b/client/hctx/hctx.go index 9d0e36e..8764ed7 100644 --- a/client/hctx/hctx.go +++ b/client/hctx/hctx.go @@ -221,6 +221,8 @@ type ClientConfig struct { KeyBindings keybindings.SerializableKeyMap `json:"key_bindings"` // The log level for hishtory (e.g., "debug", "info", "warn", "error") LogLevel logrus.Level `json:"log_level"` + // Whether the TUI should render in full-screen mode + FullScreenRendering bool `json:"full_screen_rendering"` } type ColorScheme struct { diff --git a/client/testdata/TestTui-FullScreenCompactRender b/client/testdata/TestTui-FullScreenCompactRender new file mode 100644 index 0000000..eb01d54 --- /dev/null +++ b/client/testdata/TestTui-FullScreenCompactRender @@ -0,0 +1,40 @@ +Search Query: > ls +┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Hostname CWD Timestamp Runtime Exit Code Command │ +│────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ localhost /tmp/ Oct 17 2022 21:43:21 PDT 3s 2 echo 'aaaaaa bbbb' │ +│ localhost /tmp/ Oct 17 2022 21:43:16 PDT 3s 2 ls ~/ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└────────────────────────────────────────────────────────────────────────────────────────────────────────┘ \ No newline at end of file diff --git a/client/testdata/TestTui-FullScreenHelp b/client/testdata/TestTui-FullScreenHelp new file mode 100644 index 0000000..07dc589 --- /dev/null +++ b/client/testdata/TestTui-FullScreenHelp @@ -0,0 +1,46 @@ +Search Query: > ls + +┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Hostname CWD Timestamp Runtime Exit Code Command │ +│────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ localhost /tmp/ Oct 17 2022 21:43:21 PDT 3s 2 echo 'aaaaaa bbbb' │ +│ localhost /tmp/ Oct 17 2022 21:43:16 PDT 3s 2 ls ~/ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +hiSHtory: Search your shell history +↑ scroll up ↓ scroll down pgup page up pgdn page down +← move left → move right shift+← scroll the table left shift+→ scroll the table right +enter select an entry ctrl+k delete the highlighted entry esc exit hiSHtory ctrl+h help +ctrl+x select an entry and cd into that directory \ No newline at end of file diff --git a/client/testdata/TestTui-FullScreenRender b/client/testdata/TestTui-FullScreenRender new file mode 100644 index 0000000..ca00974 --- /dev/null +++ b/client/testdata/TestTui-FullScreenRender @@ -0,0 +1,42 @@ +Search Query: > ls + +┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Hostname CWD Timestamp Runtime Exit Code Command │ +│────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ localhost /tmp/ Oct 17 2022 21:43:21 PDT 3s 2 echo 'aaaaaa bbbb' │ +│ localhost /tmp/ Oct 17 2022 21:43:16 PDT 3s 2 ls ~/ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +hiSHtory: Search your shell history • ctrl+h help \ No newline at end of file diff --git a/client/tui/tui.go b/client/tui/tui.go index 9e8440d..9a755e5 100644 --- a/client/tui/tui.go +++ b/client/tui/tui.go @@ -29,11 +29,6 @@ import ( "golang.org/x/term" ) -const ( - TABLE_HEIGHT = 20 - PADDED_NUM_ENTRIES = TABLE_HEIGHT * 5 -) - var ( CURRENT_QUERY_FOR_HIGHLIGHTING string = "" SELECTED_COMMAND string = "" @@ -218,7 +213,7 @@ func runQueryAndUpdateTable(m model, forceUpdateTable, maintainCursor bool) tea. // The default filter was cleared for this session, so don't apply it defaultFilter = "" } - rows, entries, searchErr := getRows(m.ctx, conf.DisplayedColumns, m.shellName, defaultFilter, query, PADDED_NUM_ENTRIES) + rows, entries, searchErr := getRows(m.ctx, conf.DisplayedColumns, m.shellName, defaultFilter, query, getNumEntriesNeeded(m.ctx)) return asyncQueryFinishedMsg{queryId, rows, entries, searchErr, forceUpdateTable, maintainCursor, nil} } } @@ -457,7 +452,7 @@ func getBaseStyle(config hctx.ClientConfig) lipgloss.Style { func renderNullableTable(m model, helpText string) string { if m.table == nil { - return strings.Repeat("\n", TABLE_HEIGHT+3) + return strings.Repeat("\n", getTableHeight(m.ctx)+3) } helpTextLen := strings.Count(helpText, "\n") baseStyle := getBaseStyle(*hctx.GetConf(m.ctx)) @@ -656,6 +651,25 @@ func min(a, b int) int { return b } +func getTableHeight(ctx context.Context) int { + config := hctx.GetConf(ctx) + if config.FullScreenRendering { + _, terminalHeight, err := getTerminalSize() + if err != nil { + // A reasonable guess at a default if for some reason we fail to retrieve the terminal size + return 30 + } + return max(terminalHeight-15, 20) + } + // Default to 20 when not full-screen since we want to balance showing a large table with not using the entire screen + return 20 +} + +func getNumEntriesNeeded(ctx context.Context) int { + // Get more than table height since the TUI filters some out (e.g. duplicate entries) + return getTableHeight(ctx) * 5 +} + func makeTable(ctx context.Context, shellName string, rows []table.Row) (table.Model, error) { config := hctx.GetConf(ctx) columns, err := makeTableColumns(ctx, shellName, config.DisplayedColumns, rows) @@ -689,7 +703,7 @@ func makeTable(ctx context.Context, shellName string, rows []table.Row) (table.M if isExtraCompactHeightMode(ctx) { tuiSize -= 3 } - tableHeight := min(TABLE_HEIGHT, terminalHeight-tuiSize) + tableHeight := min(getTableHeight(ctx), terminalHeight-tuiSize) t := table.New( table.WithColumns(columns), table.WithRows(rows), @@ -868,20 +882,24 @@ func configureColorProfile(ctx context.Context) { func TuiQuery(ctx context.Context, shellName, initialQuery string) error { loadedKeyBindings = hctx.GetConf(ctx).KeyBindings.ToKeyMap() configureColorProfile(ctx) - p := tea.NewProgram(initialModel(ctx, shellName, initialQuery), tea.WithOutput(os.Stderr)) + additionalOptions := []tea.ProgramOption{tea.WithOutput(os.Stderr)} + if hctx.GetConf(ctx).FullScreenRendering { + additionalOptions = append(additionalOptions, tea.WithAltScreen()) + } + p := tea.NewProgram(initialModel(ctx, shellName, initialQuery), additionalOptions...) // Async: Get the initial set of rows go func() { LAST_DISPATCHED_QUERY_ID++ queryId := LAST_DISPATCHED_QUERY_ID LAST_DISPATCHED_QUERY_TIMESTAMP = time.Now() conf := hctx.GetConf(ctx) - rows, entries, err := getRows(ctx, conf.DisplayedColumns, shellName, conf.DefaultFilter, initialQuery, PADDED_NUM_ENTRIES) + rows, entries, err := getRows(ctx, conf.DisplayedColumns, shellName, conf.DefaultFilter, initialQuery, getNumEntriesNeeded(ctx)) if err == nil || initialQuery == "" { p.Send(asyncQueryFinishedMsg{queryId: queryId, rows: rows, entries: entries, searchErr: err, forceUpdateTable: true, maintainCursor: false, overriddenSearchQuery: nil}) } else { // initialQuery is likely invalid in some way, let's just drop it emptyQuery := "" - rows, entries, err := getRows(ctx, hctx.GetConf(ctx).DisplayedColumns, shellName, conf.DefaultFilter, emptyQuery, PADDED_NUM_ENTRIES) + rows, entries, err := getRows(ctx, hctx.GetConf(ctx).DisplayedColumns, shellName, conf.DefaultFilter, emptyQuery, getNumEntriesNeeded(ctx)) p.Send(asyncQueryFinishedMsg{queryId: queryId, rows: rows, entries: entries, searchErr: err, forceUpdateTable: true, maintainCursor: false, overriddenSearchQuery: &emptyQuery}) } }()