Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions client/cmd/configGet.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ func init() {
configGetCmd.AddCommand(getCompactMode)
configGetCmd.AddCommand(getLogLevelCmd)
configGetCmd.AddCommand(getFullScreenCmd)
configGetCmd.AddCommand(getShowPreviewPaneCmd)
configGetCmd.AddCommand(getDefaultSearchColumns)
}

Expand All @@ -231,3 +232,13 @@ var getFullScreenCmd = &cobra.Command{
fmt.Println(config.FullScreenRendering)
},
}

var getShowPreviewPaneCmd = &cobra.Command{
Use: "show-preview-pane",
Short: "Get whether the preview pane is enabled for displaying full commands",
Run: func(cmd *cobra.Command, args []string) {
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
fmt.Println(config.ShowPreviewPane)
},
}
14 changes: 14 additions & 0 deletions client/cmd/configSet.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,19 @@ var setFullScreenCmd = &cobra.Command{
},
}

var setShowPreviewPaneCmd = &cobra.Command{
Use: "show-preview-pane",
Short: "Configure whether to show a preview pane displaying the full currently selected command",
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.ShowPreviewPane = args[0] == "true"
lib.CheckFatalError(hctx.SetConfig(config))
},
}

func validateDefaultSearchColumns(ctx context.Context, columns []string) error {
customColNames, err := lib.GetAllCustomColumnNames(ctx)
if err != nil {
Expand Down Expand Up @@ -341,6 +354,7 @@ func init() {
configSetCmd.AddCommand(compactMode)
configSetCmd.AddCommand(setLogLevelCmd)
configSetCmd.AddCommand(setFullScreenCmd)
configSetCmd.AddCommand(setShowPreviewPaneCmd)
configSetCmd.AddCommand(setDefaultSearchColumns)
setColorSchemeCmd.AddCommand(setColorSchemeSelectedText)
setColorSchemeCmd.AddCommand(setColorSchemeSelectedBackground)
Expand Down
2 changes: 2 additions & 0 deletions client/hctx/hctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ type ClientConfig struct {
// Columns that are used for default searches.
// See https://github.com/ddworken/hishtory/issues/268 for context on this.
DefaultSearchColumns []string `json:"default_search_columns"`
// Whether to show a preview pane for the currently selected command
ShowPreviewPane bool `json:"show_preview_pane"`
}

type ColorScheme struct {
Expand Down
40 changes: 40 additions & 0 deletions client/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ func TestParam(t *testing.T) {
t.Run("testTui/defaultFilter", wrapTestForSharding(testTui_defaultFilter))
t.Run("testTui/escaping", wrapTestForSharding(testTui_escaping))
t.Run("testTui/fullscreen", wrapTestForSharding(testTui_fullscreen))
t.Run("testTui/previewPane", wrapTestForSharding(testTui_previewPane))

// Assert there are no leaked connections
assertNoLeakedConnections(t)
Expand Down Expand Up @@ -2617,6 +2618,45 @@ func testTui_fullscreen(t *testing.T) {
testutils.CompareGoldens(t, out, "TestTui-FullScreenCompactRender")
}

func testTui_previewPane(t *testing.T) {
// Setup
defer testutils.BackupAndRestore(t)()
tester, _, db := setupTestTui(t, Online)

// Add a long command to test preview pane wrapping
longCmd := testutils.MakeFakeHistoryEntry("echo 'this is a very long command that will be truncated in the table but should be fully visible in the preview pane when we enable it'")
require.NoError(t, db.Create(longCmd).Error)

// By default preview pane is disabled
require.Equal(t, "false", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get show-preview-pane`)))

// Test that we can enable it
tester.RunInteractiveShell(t, `hishtory config-set show-preview-pane true`)
require.Equal(t, "true", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get show-preview-pane`)))

// Test that the preview pane displays the full command
out := captureTerminalOutput(t, tester, []string{
"hishtory SPACE tquery ENTER",
})
out = stripTuiCommandPrefix(t, out)
// The preview pane should show the long command
require.Contains(t, out, "this is a very long command")
require.Contains(t, out, "fully visible in the preview pane")

// Test that we can toggle it off with Ctrl+O
out = captureTerminalOutput(t, tester, []string{
"hishtory SPACE tquery ENTER",
"C-o",
})
out = stripTuiCommandPrefix(t, out)
// After toggling off, the preview should not be visible
// (We can't easily test this without golden files, but the command should run without error)

// Test that we can disable it via config
tester.RunInteractiveShell(t, `hishtory config-set show-preview-pane false`)
require.Equal(t, "false", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get show-preview-pane`)))
}

func testTui_defaultFilter(t *testing.T) {
// Setup
defer testutils.BackupAndRestore(t)()
Expand Down
3 changes: 3 additions & 0 deletions client/testdata/TestStatusFullConfig
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,13 @@ Full Config:
- ctrl+left
wordright:
- ctrl+right
togglepreviewpane:
- ctrl+o
loglevel: info
fullscreenrendering: false
defaultsearchcolumns:
- command
- hostname
- current_working_directory
showpreviewpane: false

2 changes: 1 addition & 1 deletion client/testdata/TestTui-FullScreenHelp
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ 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
ctrl+x select an entry and cd into that directory ctrl+o toggle preview pane
2 changes: 1 addition & 1 deletion client/testdata/TestTui-HelpPage
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ 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
ctrl+x select an entry and cd into that directory ctrl+o toggle preview pane
2 changes: 1 addition & 1 deletion client/testdata/TestTui-KeyBindings-Help
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ 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+j help
ctrl+x select an entry and cd into that directory
ctrl+x select an entry and cd into that directory ctrl+o toggle preview pane
16 changes: 15 additions & 1 deletion client/tui/keybindings/keybindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type SerializableKeyMap struct {
JumpEndOfInput []string
WordLeft []string
WordRight []string
TogglePreviewPane []string
}

func prettifyKeyBinding(kb string) string {
Expand Down Expand Up @@ -126,6 +127,10 @@ func (s SerializableKeyMap) ToKeyMap() KeyMap {
key.WithKeys(s.WordRight...),
key.WithHelp(prettifyKeyBinding(s.WordRight[0]), "jump right one word "),
),
TogglePreviewPane: key.NewBinding(
key.WithKeys(s.TogglePreviewPane...),
key.WithHelp(prettifyKeyBinding(s.TogglePreviewPane[0]), "toggle preview pane "),
),
}
}

Expand Down Expand Up @@ -181,6 +186,9 @@ func (s SerializableKeyMap) WithDefaults() SerializableKeyMap {
if len(s.WordRight) == 0 {
s.WordRight = DefaultKeyMap.WordRight.Keys()
}
if len(s.TogglePreviewPane) == 0 {
s.TogglePreviewPane = DefaultKeyMap.TogglePreviewPane.Keys()
}
return s
}

Expand All @@ -202,6 +210,7 @@ type KeyMap struct {
JumpEndOfInput key.Binding
WordLeft key.Binding
WordRight key.Binding
TogglePreviewPane key.Binding
}

func (k KeyMap) ToSerializable() SerializableKeyMap {
Expand All @@ -223,6 +232,7 @@ func (k KeyMap) ToSerializable() SerializableKeyMap {
JumpEndOfInput: k.JumpEndOfInput.Keys(),
WordLeft: k.WordLeft.Keys(),
WordRight: k.WordRight.Keys(),
TogglePreviewPane: k.TogglePreviewPane.Keys(),
}
}

Expand All @@ -243,7 +253,7 @@ func (k KeyMap) ShortHelp() []key.Binding {
func (k KeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{fakeTitleKeyBinding, k.Up, k.Left, k.SelectEntry, k.SelectEntryAndChangeDir},
{fakeEmptyKeyBinding, k.Down, k.Right, k.DeleteEntry},
{fakeEmptyKeyBinding, k.Down, k.Right, k.DeleteEntry, k.TogglePreviewPane},
{fakeEmptyKeyBinding, k.PageUp, k.TableLeft, k.Quit},
{fakeEmptyKeyBinding, k.PageDown, k.TableRight, k.Help},
}
Expand Down Expand Up @@ -323,4 +333,8 @@ var DefaultKeyMap = KeyMap{
key.WithKeys("ctrl+right"),
key.WithHelp("ctrl+right", "jump right one word "),
),
TogglePreviewPane: key.NewBinding(
key.WithKeys("ctrl+o"),
key.WithHelp("ctrl+o", "toggle preview pane "),
),
}
68 changes: 66 additions & 2 deletions client/tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ type model struct {

// Whether we've finished the first load of results. If we haven't, we refuse to run additional queries to avoid race conditions with how we handle invalid initial queries.
hasFinishedFirstLoad bool

// Whether the preview pane is visible
showPreviewPane bool
}

type (
Expand Down Expand Up @@ -154,7 +157,7 @@ func initialModel(ctx context.Context, shellName, initialQuery string) model {
queryInput.SetValue(initialQuery)
}
CURRENT_QUERY_FOR_HIGHLIGHTING = initialQuery
return model{ctx: ctx, spinner: s, isLoading: true, table: nil, tableEntries: []*data.HistoryEntry{}, runQuery: &initialQuery, queryInput: queryInput, help: help.New(), shellName: shellName, hasFinishedFirstLoad: false}
return model{ctx: ctx, spinner: s, isLoading: true, table: nil, tableEntries: []*data.HistoryEntry{}, runQuery: &initialQuery, queryInput: queryInput, help: help.New(), shellName: shellName, hasFinishedFirstLoad: false, showPreviewPane: cfg.ShowPreviewPane}
}

func (m model) Init() tea.Cmd {
Expand Down Expand Up @@ -270,6 +273,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, loadedKeyBindings.Help):
m.help.ShowAll = !m.help.ShowAll
return m, nil
case key.Matches(msg, loadedKeyBindings.TogglePreviewPane):
m.showPreviewPane = !m.showPreviewPane
cmd := runQueryAndUpdateTable(m, true, true)
return m, cmd
case key.Matches(msg, loadedKeyBindings.JumpStartOfInput):
m.queryInput.SetCursor(0)
return m, nil
Expand Down Expand Up @@ -383,6 +390,59 @@ func calculateWordBoundaries(input string) []int {
return ret
}

func renderPreviewPane(m model) string {
if !m.showPreviewPane || m.table == nil || len(m.tableEntries) == 0 {
return ""
}

cursor := m.table.Cursor()
if cursor >= len(m.tableEntries) {
return ""
}

command := m.tableEntries[cursor].Command

terminalWidth, _, err := getTerminalSize()
if err != nil {
terminalWidth = 80
}

wrappedCommand := wrapText(command, terminalWidth-4)

return "\n" + wrappedCommand
}

func wrapText(text string, width int) string {
if width <= 0 {
width = 80
}

var result strings.Builder
lines := strings.Split(text, "\n")

for lineIdx, line := range lines {
if lineIdx > 0 {
result.WriteString("\n")
}

if len(line) <= width {
result.WriteString(line)
continue
}

for len(line) > width {
result.WriteString(line[:width])
result.WriteString("\n")
line = line[width:]
}
if len(line) > 0 {
result.WriteString(line)
}
}

return result.String()
}

func (m model) View() string {
if m.fatalErr != nil {
return fmt.Sprintf("An unrecoverable error occured: %v\n", m.fatalErr)
Expand Down Expand Up @@ -432,7 +492,8 @@ func (m model) View() string {
if isCompactHeightMode(m.ctx) {
additionalSpacing = ""
}
return fmt.Sprintf("%s%s%s%sSearch Query: %s\n%s%s\n", additionalSpacing, additionalMessagesStr, m.banner, additionalSpacing, m.queryInput.View(), additionalSpacing, renderNullableTable(m, helpView)) + helpView
previewPane := renderPreviewPane(m)
return fmt.Sprintf("%s%s%s%sSearch Query: %s\n%s%s%s\n", additionalSpacing, additionalMessagesStr, m.banner, additionalSpacing, m.queryInput.View(), additionalSpacing, renderNullableTable(m, helpView), previewPane) + helpView
}

func isExtraCompactHeightMode(ctx context.Context) bool {
Expand Down Expand Up @@ -722,6 +783,9 @@ func makeTable(ctx context.Context, shellName string, rows []table.Row) (table.M
if isExtraCompactHeightMode(ctx) {
tuiSize -= 3
}
if config.ShowPreviewPane {
tuiSize += 6
}
tableHeight := min(getTableHeight(ctx), terminalHeight-tuiSize)
t := table.New(
table.WithColumns(columns),
Expand Down
Loading