Add ability to configure a default filter (for #76) (#161)

* Add ability to configure a default filter (for #76)

* Add test for color of default filter

* Add basic test for default filter

* Add goldens for tests

* Add more tests for default filters

* Update goldens

* Add another golden

* Update goldens

* Remove debug log

* Add golden to allowlist

* Update goldens
This commit is contained in:
David Dworken 2024-01-07 21:06:22 -08:00 committed by GitHub
parent 81ed634a4b
commit a3f1282368
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 251 additions and 15 deletions

View File

@ -113,6 +113,7 @@ func TestParam(t *testing.T) {
t.Run("testTui/color", testTui_color) t.Run("testTui/color", testTui_color)
t.Run("testTui/errors", testTui_errors) t.Run("testTui/errors", testTui_errors)
t.Run("testTui/ai", testTui_ai) t.Run("testTui/ai", testTui_ai)
t.Run("testTui/defaultFilter", testTui_defaultFilter)
// Assert there are no leaked connections // Assert there are no leaked connections
assertNoLeakedConnections(t) assertNoLeakedConnections(t)
@ -1752,6 +1753,53 @@ func testTui_scroll(t *testing.T) {
assertNoLeakedConnections(t) assertNoLeakedConnections(t)
} }
func testTui_defaultFilter(t *testing.T) {
// Setup
defer testutils.BackupAndRestore(t)()
tester, userSecret, _ := setupTestTui(t, Online)
db := hctx.GetDb(hctx.MakeContext())
e1 := testutils.MakeFakeHistoryEntry("exit 0")
e1.ExitCode = 0
require.NoError(t, db.Create(e1).Error)
manuallySubmitHistoryEntry(t, userSecret, e1)
e2 := testutils.MakeFakeHistoryEntry("exit 1")
e2.ExitCode = 1
require.NoError(t, db.Create(e2).Error)
manuallySubmitHistoryEntry(t, userSecret, e2)
// Configure a default filter
require.Equal(t, "\"\"", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get default-filter`)))
tester.RunInteractiveShell(t, `hishtory config-set default-filter "exit_code:0"`)
require.Equal(t, "\"exit_code:0\"", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get default-filter`)))
// Run a search query with no additional query
out := stripTuiCommandPrefix(t, captureTerminalOutput(t, tester, []string{
"hishtory SPACE tquery ENTER",
}))
testutils.CompareGoldens(t, out, "TestTui-DefaultFilter-Enabled")
// Run a search query with an additional query
out = stripTuiCommandPrefix(t, captureTerminalOutput(t, tester, []string{
"hishtory SPACE tquery ENTER",
"exit",
}))
testutils.CompareGoldens(t, out, "TestTui-DefaultFilter-EnabledAdditionalQuery")
// Run a search query and delete the default filter
out = stripTuiCommandPrefix(t, captureTerminalOutput(t, tester, []string{
"hishtory SPACE tquery ENTER",
"BSpace",
}))
testutils.CompareGoldens(t, out, "TestTui-DefaultFilter-Deleted")
// Run a search query, type something, and then delete the default filter
out = stripTuiCommandPrefix(t, captureTerminalOutput(t, tester, []string{
"hishtory SPACE tquery ENTER",
"exit Left Left Left Left Left BSpace BSpace",
}))
testutils.CompareGoldens(t, out, "TestTui-DefaultFilter-DeletedWithText")
}
func testTui_color(t *testing.T) { func testTui_color(t *testing.T) {
if runtime.GOOS == "linux" { if runtime.GOOS == "linux" {
// For some reason, this test fails on linux. Since this test isn't critical and is expected to be // For some reason, this test fails on linux. Since this test isn't critical and is expected to be
@ -1790,6 +1838,14 @@ 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-ColoredOutputWithCustomColorScheme") testutils.CompareGoldens(t, out, "TestTui-ColoredOutputWithCustomColorScheme")
// And one more time with a default filter
require.Equal(t, "\"\"", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get default-filter`)))
tester.RunInteractiveShell(t, `hishtory config-set default-filter "exit_code:0"`)
require.Equal(t, "\"exit_code:0\"", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get default-filter`)))
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-ColoredOutputWithDefaultFilter")
} }
func testTui_delete(t *testing.T) { func testTui_delete(t *testing.T) {

View File

@ -41,6 +41,15 @@ var getHighlightMatchesCmd = &cobra.Command{
fmt.Println(config.HighlightMatches) fmt.Println(config.HighlightMatches)
}, },
} }
var getDefaultFilterCmd = &cobra.Command{
Use: "default-filter",
Short: "The default filter that is applied to all search queries",
Run: func(cmd *cobra.Command, args []string) {
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
fmt.Printf("%#v", config.DefaultFilter)
},
}
var getFilterDuplicateCommandsCmd = &cobra.Command{ var getFilterDuplicateCommandsCmd = &cobra.Command{
Use: "filter-duplicate-commands", Use: "filter-duplicate-commands",
@ -149,4 +158,5 @@ func init() {
configGetCmd.AddCommand(getEnableAiCompletion) configGetCmd.AddCommand(getEnableAiCompletion)
configGetCmd.AddCommand(getPresavingCmd) configGetCmd.AddCommand(getPresavingCmd)
configGetCmd.AddCommand(getColorScheme) configGetCmd.AddCommand(getColorScheme)
configGetCmd.AddCommand(getDefaultFilterCmd)
} }

View File

@ -73,6 +73,18 @@ var setBetaModeCommand = &cobra.Command{
}, },
} }
var setDefaultFilterCommand = &cobra.Command{
Use: "default-filter",
Short: "Add a default filter that will be applied to all search queries (e.g. `exit_code:0` to filter to only commands that executed successfully)",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
config.DefaultFilter = args[0]
lib.CheckFatalError(hctx.SetConfig(config))
},
}
var setEnableAiCompletionCmd = &cobra.Command{ var setEnableAiCompletionCmd = &cobra.Command{
Use: "ai-completion", Use: "ai-completion",
Short: "Enable AI completion for searches starting with '?'", Short: "Enable AI completion for searches starting with '?'",
@ -216,6 +228,7 @@ func init() {
configSetCmd.AddCommand(setEnableAiCompletionCmd) configSetCmd.AddCommand(setEnableAiCompletionCmd)
configSetCmd.AddCommand(setPresavingCmd) configSetCmd.AddCommand(setPresavingCmd)
configSetCmd.AddCommand(setColorSchemeCmd) configSetCmd.AddCommand(setColorSchemeCmd)
configSetCmd.AddCommand(setDefaultFilterCommand)
setColorSchemeCmd.AddCommand(setColorSchemeSelectedText) setColorSchemeCmd.AddCommand(setColorSchemeSelectedText)
setColorSchemeCmd.AddCommand(setColorSchemeSelectedBackground) setColorSchemeCmd.AddCommand(setColorSchemeSelectedBackground)
setColorSchemeCmd.AddCommand(setColorSchemeBorderColor) setColorSchemeCmd.AddCommand(setColorSchemeBorderColor)

View File

@ -203,6 +203,8 @@ type ClientConfig struct {
EnablePresaving bool `json:"enable_presaving"` EnablePresaving bool `json:"enable_presaving"`
// The current color scheme for the TUI // The current color scheme for the TUI
ColorScheme ColorScheme `json:"color_scheme"` ColorScheme ColorScheme `json:"color_scheme"`
// A default filter that will be applied to all search queries
DefaultFilter string `json:"default_filter"`
} }
type ColorScheme struct { type ColorScheme struct {

View File

@ -26,7 +26,7 @@ var UNUSED_GOLDENS []string = []string{"TestTui-Exit", "testControlR-ControlC-ba
"testCustomColumns-tquery-zsh", "testUninstall-post-uninstall-bash", "testCustomColumns-tquery-zsh", "testUninstall-post-uninstall-bash",
"testUninstall-post-uninstall-zsh", "TestTui-ColoredOutput", "testUninstall-post-uninstall-zsh", "TestTui-ColoredOutput",
"TestTui-ColoredOutputWithCustomColorScheme", "TestTui-ColoredOutputWithSearch", "TestTui-ColoredOutputWithSearch-Highlight", "TestTui-ColoredOutputWithCustomColorScheme", "TestTui-ColoredOutputWithSearch", "TestTui-ColoredOutputWithSearch-Highlight",
"TestTui-DefaultColorScheme"} "TestTui-DefaultColorScheme", "TestTui-ColoredOutputWithDefaultFilter"}
func main() { func main() {
exportMetrics() exportMetrics()

View File

@ -1,4 +1,4 @@
Search Query: > ls Search Query: > ls
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
 Hostname CWD Timestamp Runtime Exit Code Command │  Hostname CWD Timestamp Runtime Exit Code Command │

View File

@ -1,4 +1,4 @@
Search Query: > ech  Search Query: > ech 
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
 Hostname CWD Timestamp Runtime Exit Code Command │  Hostname CWD Timestamp Runtime Exit Code Command │

View File

@ -0,0 +1,27 @@
Search Query: [exit_code:0] ech 
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
 Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
 │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘
hiSHtory: Search your shell history  • ctrl+h help

View File

@ -1,4 +1,4 @@
Search Query: > ech  Search Query: > ech 
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
 Hostname CWD Timestamp Runtime Exit Code Command │  Hostname CWD Timestamp Runtime Exit Code Command │

View File

@ -1,4 +1,4 @@
Search Query: > ech  Search Query: > ech 
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
 Hostname CWD Timestamp Runtime Exit Code Command │  Hostname CWD Timestamp Runtime Exit Code Command │

View File

@ -0,0 +1,27 @@
Search Query:
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:43:31 PDT 3s 1 exit 1 │
│ localhost /tmp/ Oct 17 2022 21:43:26 PDT 3s 0 exit 0 │
│ 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

@ -0,0 +1,27 @@
Search Query: [exit_code:0] exit
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:43:26 PDT 3s 0 exit 0 │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘
hiSHtory: Search your shell history • ctrl+h help

View File

@ -0,0 +1,27 @@
Search Query: [exit_code:0]
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:43:26 PDT 3s 0 exit 0 │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘
hiSHtory: Search your shell history • ctrl+h help

View File

@ -0,0 +1,27 @@
Search Query: [exit_code:0] exit
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:43:26 PDT 3s 0 exit 0 │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘
hiSHtory: Search your shell history • ctrl+h help

View File

@ -210,7 +210,14 @@ func initialModel(ctx context.Context, initialQuery string) model {
s.Spinner = spinner.Dot s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
queryInput := textinput.New() queryInput := textinput.New()
defaultFilter := hctx.GetConf(ctx).DefaultFilter
if defaultFilter != "" {
queryInput.Prompt = "[" + defaultFilter + "] "
}
queryInput.PromptStyle = queryInput.PlaceholderStyle
if defaultFilter == "" {
queryInput.Placeholder = "ls" queryInput.Placeholder = "ls"
}
queryInput.Focus() queryInput.Focus()
queryInput.CharLimit = 200 queryInput.CharLimit = 200
width, _, err := getTerminalSize() width, _, err := getTerminalSize()
@ -286,7 +293,13 @@ func runQueryAndUpdateTable(m model, forceUpdateTable, maintainCursor bool) tea.
queryId := LAST_DISPATCHED_QUERY_ID queryId := LAST_DISPATCHED_QUERY_ID
LAST_DISPATCHED_QUERY_TIMESTAMP = time.Now() LAST_DISPATCHED_QUERY_TIMESTAMP = time.Now()
return func() tea.Msg { return func() tea.Msg {
rows, entries, searchErr := getRows(m.ctx, hctx.GetConf(m.ctx).DisplayedColumns, query, PADDED_NUM_ENTRIES) conf := hctx.GetConf(m.ctx)
defaultFilter := conf.DefaultFilter
if m.queryInput.Prompt == "" {
// The default filter was cleared for this session, so don't apply it
defaultFilter = ""
}
rows, entries, searchErr := getRows(m.ctx, conf.DisplayedColumns, defaultFilter, query, PADDED_NUM_ENTRIES)
return asyncQueryFinishedMsg{queryId, rows, entries, searchErr, forceUpdateTable, maintainCursor, nil} return asyncQueryFinishedMsg{queryId, rows, entries, searchErr, forceUpdateTable, maintainCursor, nil}
} }
} }
@ -335,12 +348,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
pendingCommands = tea.Batch(pendingCommands, cmd1) pendingCommands = tea.Batch(pendingCommands, cmd1)
} }
forceUpdateTable := false
if msg.String() == "backspace" && (m.queryInput.Value() == "" || m.queryInput.Position() == 0) {
// Handle deleting the default filter just for this TUI instance
m.queryInput.Prompt = ""
forceUpdateTable = true
}
i, cmd2 := m.queryInput.Update(msg) i, cmd2 := m.queryInput.Update(msg)
m.queryInput = i m.queryInput = i
searchQuery := m.queryInput.Value() searchQuery := m.queryInput.Value()
m.runQuery = &searchQuery m.runQuery = &searchQuery
CURRENT_QUERY_FOR_HIGHLIGHTING = searchQuery CURRENT_QUERY_FOR_HIGHLIGHTING = searchQuery
cmd3 := runQueryAndUpdateTable(m, false, false) cmd3 := runQueryAndUpdateTable(m, forceUpdateTable, false)
preventTableOverscrolling(m) preventTableOverscrolling(m)
return m, tea.Batch(pendingCommands, cmd2, cmd3) return m, tea.Batch(pendingCommands, cmd2, cmd3)
} }
@ -505,13 +524,13 @@ func getRowsFromAiSuggestions(ctx context.Context, columnNames []string, query s
return rows, entries, nil return rows, entries, nil
} }
func getRows(ctx context.Context, columnNames []string, query string, numEntries int) ([]table.Row, []*data.HistoryEntry, error) { func getRows(ctx context.Context, columnNames []string, defaultFilter, query string, numEntries int) ([]table.Row, []*data.HistoryEntry, error) {
db := hctx.GetDb(ctx) db := hctx.GetDb(ctx)
config := hctx.GetConf(ctx) config := hctx.GetConf(ctx)
if config.AiCompletion && !config.IsOffline && strings.HasPrefix(query, "?") && len(query) > 1 { if config.AiCompletion && !config.IsOffline && strings.HasPrefix(query, "?") && len(query) > 1 {
return getRowsFromAiSuggestions(ctx, columnNames, query) return getRowsFromAiSuggestions(ctx, columnNames, query)
} }
searchResults, err := lib.Search(ctx, db, query, numEntries) searchResults, err := lib.Search(ctx, db, defaultFilter+" "+query, numEntries)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -564,7 +583,7 @@ var bigQueryResults []table.Row
func makeTableColumns(ctx context.Context, columnNames []string, rows []table.Row) ([]table.Column, error) { func makeTableColumns(ctx context.Context, columnNames []string, rows []table.Row) ([]table.Column, error) {
// Handle an initial query with no results // Handle an initial query with no results
if len(rows) == 0 || len(rows[0]) == 0 { if len(rows) == 0 || len(rows[0]) == 0 {
allRows, _, err := getRows(ctx, columnNames, "", 25) allRows, _, err := getRows(ctx, columnNames, hctx.GetConf(ctx).DefaultFilter, "", 25)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -590,7 +609,7 @@ func makeTableColumns(ctx context.Context, columnNames []string, rows []table.Ro
// Calculate the maximum column width that is useful for each column if we search for the empty string // Calculate the maximum column width that is useful for each column if we search for the empty string
if bigQueryResults == nil { if bigQueryResults == nil {
bigRows, _, err := getRows(ctx, columnNames, "", 1000) bigRows, _, err := getRows(ctx, columnNames, "", "", 1000)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -868,13 +887,14 @@ func TuiQuery(ctx context.Context, initialQuery string) error {
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()
rows, entries, err := getRows(ctx, hctx.GetConf(ctx).DisplayedColumns, initialQuery, PADDED_NUM_ENTRIES) conf := hctx.GetConf(ctx)
rows, entries, err := getRows(ctx, conf.DisplayedColumns, conf.DefaultFilter, initialQuery, PADDED_NUM_ENTRIES)
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, emptyQuery, PADDED_NUM_ENTRIES) rows, entries, err := getRows(ctx, hctx.GetConf(ctx).DisplayedColumns, conf.DefaultFilter, emptyQuery, PADDED_NUM_ENTRIES)
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})
} }
}() }()