Add support for full-screen rendering (#258)

This commit is contained in:
David Dworken 2024-10-19 13:11:34 -07:00 committed by GitHub
parent 5996b832a3
commit 389515ae28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 246 additions and 11 deletions

View File

@ -199,6 +199,19 @@ You can configure a custom timestamp format for hiSHtory via `hishtory config-se
</blockquote></details> </blockquote></details>
<details>
<summary>Custom rendering</summary><blockquote>
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
```
</blockquote></details>
<details> <details>
<summary>Web UI for sharing</summary><blockquote> <summary>Web UI for sharing</summary><blockquote>

View File

@ -119,6 +119,7 @@ func TestParam(t *testing.T) {
t.Run("testTui/ai", wrapTestForSharding(testTui_ai)) t.Run("testTui/ai", wrapTestForSharding(testTui_ai))
t.Run("testTui/defaultFilter", wrapTestForSharding(testTui_defaultFilter)) t.Run("testTui/defaultFilter", wrapTestForSharding(testTui_defaultFilter))
t.Run("testTui/escaping", wrapTestForSharding(testTui_escaping)) t.Run("testTui/escaping", wrapTestForSharding(testTui_escaping))
t.Run("testTui/fullscreen", wrapTestForSharding(testTui_fullscreen))
// Assert there are no leaked connections // Assert there are no leaked connections
assertNoLeakedConnections(t) assertNoLeakedConnections(t)
@ -1871,6 +1872,54 @@ func testTui_escaping(t *testing.T) {
testutils.CompareGoldens(t, out, "TestTui-Escaping") 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) { func testTui_defaultFilter(t *testing.T) {
// Setup // Setup
defer testutils.BackupAndRestore(t)() defer testutils.BackupAndRestore(t)()

View File

@ -184,6 +184,7 @@ func init() {
configGetCmd.AddCommand(getAiCompletionEndpoint) configGetCmd.AddCommand(getAiCompletionEndpoint)
configGetCmd.AddCommand(getCompactMode) configGetCmd.AddCommand(getCompactMode)
configGetCmd.AddCommand(getLogLevelCmd) configGetCmd.AddCommand(getLogLevelCmd)
configGetCmd.AddCommand(getFullScreenCmd)
} }
var getLogLevelCmd = &cobra.Command{ var getLogLevelCmd = &cobra.Command{
@ -195,3 +196,13 @@ var getLogLevelCmd = &cobra.Command{
fmt.Println(config.LogLevel.String()) 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)
},
}

View File

@ -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() { func init() {
rootCmd.AddCommand(configSetCmd) rootCmd.AddCommand(configSetCmd)
configSetCmd.AddCommand(setEnableControlRCmd) configSetCmd.AddCommand(setEnableControlRCmd)
@ -277,6 +290,7 @@ func init() {
configSetCmd.AddCommand(setAiCompletionEndpoint) configSetCmd.AddCommand(setAiCompletionEndpoint)
configSetCmd.AddCommand(compactMode) configSetCmd.AddCommand(compactMode)
configSetCmd.AddCommand(setLogLevelCmd) configSetCmd.AddCommand(setLogLevelCmd)
configSetCmd.AddCommand(setFullScreenCmd)
setColorSchemeCmd.AddCommand(setColorSchemeSelectedText) setColorSchemeCmd.AddCommand(setColorSchemeSelectedText)
setColorSchemeCmd.AddCommand(setColorSchemeSelectedBackground) setColorSchemeCmd.AddCommand(setColorSchemeSelectedBackground)
setColorSchemeCmd.AddCommand(setColorSchemeBorderColor) setColorSchemeCmd.AddCommand(setColorSchemeBorderColor)

View File

@ -221,6 +221,8 @@ type ClientConfig struct {
KeyBindings keybindings.SerializableKeyMap `json:"key_bindings"` KeyBindings keybindings.SerializableKeyMap `json:"key_bindings"`
// The log level for hishtory (e.g., "debug", "info", "warn", "error") // The log level for hishtory (e.g., "debug", "info", "warn", "error")
LogLevel logrus.Level `json:"log_level"` LogLevel logrus.Level `json:"log_level"`
// Whether the TUI should render in full-screen mode
FullScreenRendering bool `json:"full_screen_rendering"`
} }
type ColorScheme struct { type ColorScheme struct {

View File

@ -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 ~/ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘

46
client/testdata/TestTui-FullScreenHelp vendored Normal file
View File

@ -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

View File

@ -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

View File

@ -29,11 +29,6 @@ import (
"golang.org/x/term" "golang.org/x/term"
) )
const (
TABLE_HEIGHT = 20
PADDED_NUM_ENTRIES = TABLE_HEIGHT * 5
)
var ( var (
CURRENT_QUERY_FOR_HIGHLIGHTING string = "" CURRENT_QUERY_FOR_HIGHLIGHTING string = ""
SELECTED_COMMAND 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 // The default filter was cleared for this session, so don't apply it
defaultFilter = "" 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} 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 { 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", getTableHeight(m.ctx)+3)
} }
helpTextLen := strings.Count(helpText, "\n") helpTextLen := strings.Count(helpText, "\n")
baseStyle := getBaseStyle(*hctx.GetConf(m.ctx)) baseStyle := getBaseStyle(*hctx.GetConf(m.ctx))
@ -656,6 +651,25 @@ func min(a, b int) int {
return b 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) { func makeTable(ctx context.Context, shellName string, rows []table.Row) (table.Model, error) {
config := hctx.GetConf(ctx) config := hctx.GetConf(ctx)
columns, err := makeTableColumns(ctx, shellName, config.DisplayedColumns, rows) 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) { if isExtraCompactHeightMode(ctx) {
tuiSize -= 3 tuiSize -= 3
} }
tableHeight := min(TABLE_HEIGHT, terminalHeight-tuiSize) tableHeight := min(getTableHeight(ctx), terminalHeight-tuiSize)
t := table.New( t := table.New(
table.WithColumns(columns), table.WithColumns(columns),
table.WithRows(rows), table.WithRows(rows),
@ -868,20 +882,24 @@ func configureColorProfile(ctx context.Context) {
func TuiQuery(ctx context.Context, shellName, initialQuery string) error { func TuiQuery(ctx context.Context, shellName, initialQuery string) error {
loadedKeyBindings = hctx.GetConf(ctx).KeyBindings.ToKeyMap() loadedKeyBindings = hctx.GetConf(ctx).KeyBindings.ToKeyMap()
configureColorProfile(ctx) 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 // Async: Get the initial set of rows
go func() { go func() {
LAST_DISPATCHED_QUERY_ID++ LAST_DISPATCHED_QUERY_ID++
queryId := LAST_DISPATCHED_QUERY_ID queryId := LAST_DISPATCHED_QUERY_ID
LAST_DISPATCHED_QUERY_TIMESTAMP = time.Now() LAST_DISPATCHED_QUERY_TIMESTAMP = time.Now()
conf := hctx.GetConf(ctx) 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 == "" { if err == nil || initialQuery == "" {
p.Send(asyncQueryFinishedMsg{queryId: queryId, rows: rows, entries: entries, searchErr: err, forceUpdateTable: true, maintainCursor: false, overriddenSearchQuery: nil}) p.Send(asyncQueryFinishedMsg{queryId: queryId, rows: rows, entries: entries, searchErr: err, forceUpdateTable: true, maintainCursor: false, overriddenSearchQuery: nil})
} else { } else {
// initialQuery is likely invalid in some way, let's just drop it // initialQuery is likely invalid in some way, let's just drop it
emptyQuery := "" 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}) p.Send(asyncQueryFinishedMsg{queryId: queryId, rows: rows, entries: entries, searchErr: err, forceUpdateTable: true, maintainCursor: false, overriddenSearchQuery: &emptyQuery})
} }
}() }()