Quote initial commands to make it possible to easily use hishtory to find matching entries for already typed commands that contain flags (#251)

* Quote initial commands to make it possible to easily use hishtory to find matching entries for already typed commands that contain flags

* Add test for quoting dashes

* Fix test failures

* More test fixes

* Update goldens

* Update goldens

* Update goldens

* Fix race condition

* Fix test harness bug by swapping to splitn

* Update goldens

* Update golden

* Update test
This commit is contained in:
David Dworken 2024-10-20 12:22:29 -07:00 committed by GitHub
parent 905afd91c3
commit 0023c72636
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 148 additions and 44 deletions

View File

@ -2165,6 +2165,14 @@ func testTui_search(t *testing.T, onlineStatus OnlineStatus) {
"'\"'foo:bar'\"'", "'\"'foo:bar'\"'",
})) }))
testutils.CompareGoldens(t, out, "TestTui-SearchColonDoubleQuoted") testutils.CompareGoldens(t, out, "TestTui-SearchColonDoubleQuoted")
// And check that we can quote dashes
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("foo --bar")).Error)
out = stripTuiCommandPrefix(t, captureTerminalOutput(t, tester, []string{
"hishtory SPACE tquery ENTER",
"'\"'--bar'\"'",
}))
testutils.CompareGoldens(t, out, "TestTui-SearchQuoteDash")
} }
func testTui_general(t *testing.T, onlineStatus OnlineStatus) { func testTui_general(t *testing.T, onlineStatus OnlineStatus) {
@ -2520,8 +2528,8 @@ echo bar`)
tester.RunInteractiveShell(t, `hishtory config-set displayed-columns 'Exit Code' git_remote Command`) tester.RunInteractiveShell(t, `hishtory config-set displayed-columns 'Exit Code' git_remote Command`)
out = tester.RunInteractiveShell(t, `hishtory query -pipefail`) out = tester.RunInteractiveShell(t, `hishtory query -pipefail`)
testutils.CompareGoldens(t, out, fmt.Sprintf("testCustomColumns-query-isAction=%v", testutils.IsGithubAction())) testutils.CompareGoldens(t, out, fmt.Sprintf("testCustomColumns-query-isAction=%v", testutils.IsGithubAction()))
out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery SPACE -pipefail ENTER"}) out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery SPACE ENTER", "-pipefail"})
out = stripRequiredPrefix(t, out, "hishtory tquery -pipefail") out = stripRequiredPrefix(t, out, "hishtory tquery")
testName := "testCustomColumns-tquery-" + tester.ShellName() testName := "testCustomColumns-tquery-" + tester.ShellName()
if testutils.IsGithubAction() { if testutils.IsGithubAction() {
testName += "-isAction" testName += "-isAction"
@ -2777,8 +2785,8 @@ func TestTimestampFormat(t *testing.T) {
// And check that it is displayed in both the tui and the classic view // And check that it is displayed in both the tui and the classic view
out := hishtoryQuery(t, tester, "-pipefail -tablesizing") out := hishtoryQuery(t, tester, "-pipefail -tablesizing")
testutils.CompareGoldens(t, out, "TestTimestampFormat-query") testutils.CompareGoldens(t, out, "TestTimestampFormat-query")
out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery SPACE -pipefail SPACE -tablesizing ENTER"}) out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery ENTER", "table_cmd SPACE -tquery"})
out = stripRequiredPrefix(t, out, "hishtory tquery -pipefail -tablesizing") out = stripRequiredPrefix(t, out, "hishtory tquery")
testutils.CompareGoldens(t, out, "TestTimestampFormat-tquery") testutils.CompareGoldens(t, out, "TestTimestampFormat-tquery")
} }
@ -2817,7 +2825,7 @@ func TestSortByConsistentTimezone(t *testing.T) {
testutils.CompareGoldens(t, out, "TestSortByConsistentTimezone-query") testutils.CompareGoldens(t, out, "TestSortByConsistentTimezone-query")
out = tester.RunInteractiveShell(t, `hishtory export -pipefail -tablesizing`) out = tester.RunInteractiveShell(t, `hishtory export -pipefail -tablesizing`)
testutils.CompareGoldens(t, out, "TestSortByConsistentTimezone-export") testutils.CompareGoldens(t, out, "TestSortByConsistentTimezone-export")
out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery SPACE -pipefail SPACE -tablesizing ENTER"}) out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery ENTER", "-pipefail SPACE -tablesizing"})
out = stripTuiCommandPrefix(t, out) out = stripTuiCommandPrefix(t, out)
require.Regexp(t, regexp.MustCompile(`Timestamp[\s\S]*Command[\s\S]*Apr 16 2022 01:36:26 PDT[\s\S]*third_entry[\s\S]*Apr 16 2022 01:19:46 PDT[\s\S]*second_entry[\s\S]*Apr 16 2022 01:03:06 PDT[\s\S]*first_entry`), out) require.Regexp(t, regexp.MustCompile(`Timestamp[\s\S]*Command[\s\S]*Apr 16 2022 01:36:26 PDT[\s\S]*third_entry[\s\S]*Apr 16 2022 01:19:46 PDT[\s\S]*second_entry[\s\S]*Apr 16 2022 01:03:06 PDT[\s\S]*first_entry`), out)
} }
@ -2878,8 +2886,8 @@ echo foo`)
tester.RunInteractiveShell(t, `hishtory config-set displayed-columns 'Exit Code' Command`) tester.RunInteractiveShell(t, `hishtory config-set displayed-columns 'Exit Code' Command`)
out = tester.RunInteractiveShell(t, `hishtory query -pipefail`) out = tester.RunInteractiveShell(t, `hishtory query -pipefail`)
testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-query") testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-query")
out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery SPACE -pipefail ENTER"}) out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery ENTER", "-pipefail"})
out = stripRequiredPrefix(t, out, "hishtory tquery -pipefail") out = stripRequiredPrefix(t, out, "hishtory tquery")
testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-tquery") testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-tquery")
// And change the config to filter out duplicate rows // And change the config to filter out duplicate rows
@ -2894,18 +2902,20 @@ echo foo`)
testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-enabled-query") testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-enabled-query")
// Check tquery // Check tquery
out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery SPACE -pipefail ENTER"}) out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery ENTER", "-pipefail"})
out = stripRequiredPrefix(t, out, "hishtory tquery -pipefail") out = stripRequiredPrefix(t, out, "hishtory tquery")
testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-enabled-tquery") testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-enabled-tquery")
// Check actually selecting it with query // Check actually selecting it with query
out = captureTerminalOutputWithComplexCommands(t, tester, []TmuxCommand{ out = captureTerminalOutputWithComplexCommands(t, tester, []TmuxCommand{
{Keys: "hishtory SPACE tquery SPACE -pipefail ENTER", ExtraDelay: 1.0}, {Keys: "hishtory SPACE tquery ENTER", ExtraDelay: 1.0},
{Keys: "Down Down"}, {Keys: "-pipefail", ExtraDelay: 1.0},
{Keys: "Down Down Down"},
{Keys: "ENTER", ExtraDelay: 1.0}, {Keys: "ENTER", ExtraDelay: 1.0},
}) })
out = stripTuiCommandPrefix(t, out) out = stripTuiCommandPrefix(t, out)
require.Contains(t, out, "\necho foo\n") require.Contains(t, out, "echo foo\n")
require.NotContains(t, out, "hishtory tquery")
require.NotContains(t, out, "echo baz") require.NotContains(t, out, "echo baz")
require.NotContains(t, out, "config-set") require.NotContains(t, out, "config-set")
} }

View File

@ -55,7 +55,7 @@ var tqueryCmd = &cobra.Command{
if os.Getenv("HISHTORY_SHELL_NAME") != "" { if os.Getenv("HISHTORY_SHELL_NAME") != "" {
shellName = os.Getenv("HISHTORY_SHELL_NAME") shellName = os.Getenv("HISHTORY_SHELL_NAME")
} }
lib.CheckFatalError(tui.TuiQuery(ctx, shellName, strings.Join(args, " "))) lib.CheckFatalError(tui.TuiQuery(ctx, shellName, args))
}, },
} }

View File

@ -965,7 +965,9 @@ func splitEscaped(query string, separator rune, maxSplit int) []string {
isInSingleQuotedString := false isInSingleQuotedString := false
for i := 0; i < len(runeQuery); i++ { for i := 0; i < len(runeQuery); i++ {
if (maxSplit < 0 || splits < maxSplit) && runeQuery[i] == separator && !isInSingleQuotedString && !isInDoubleQuotedString { if (maxSplit < 0 || splits < maxSplit) && runeQuery[i] == separator && !isInSingleQuotedString && !isInDoubleQuotedString {
tokens = append(tokens, string(token)) if string(token) != "" {
tokens = append(tokens, string(token))
}
token = token[:0] token = token[:0]
splits++ splits++
} else if runeQuery[i] == '\\' && i+1 < len(runeQuery) { } else if runeQuery[i] == '\\' && i+1 < len(runeQuery) {
@ -982,8 +984,13 @@ func splitEscaped(query string, separator rune, maxSplit int) []string {
} else if runeQuery[i] == '\'' && !isInDoubleQuotedString && !heuristicIgnoreUnclosedQuote(isInSingleQuotedString, '\'', runeQuery, i) { } else if runeQuery[i] == '\'' && !isInDoubleQuotedString && !heuristicIgnoreUnclosedQuote(isInSingleQuotedString, '\'', runeQuery, i) {
isInSingleQuotedString = !isInSingleQuotedString isInSingleQuotedString = !isInSingleQuotedString
} else { } else {
if (isInSingleQuotedString || isInDoubleQuotedString) && separator == ' ' && runeQuery[i] == ':' { if (isInSingleQuotedString || isInDoubleQuotedString) && separator == ' ' {
token = append(token, '\\') if runeQuery[i] == ':' {
token = append(token, '\\')
}
if runeQuery[i] == '-' && len(token) == 0 {
token = append(token, '\\')
}
} }
token = append(token, runeQuery[i]) token = append(token, runeQuery[i])
} }

View File

@ -324,6 +324,11 @@ func TestSplitEscaped(t *testing.T) {
{"foo:bar", ' ', -1, []string{"foo:bar"}}, {"foo:bar", ' ', -1, []string{"foo:bar"}},
{"'foo:bar'", ' ', -1, []string{"foo\\:bar"}}, {"'foo:bar'", ' ', -1, []string{"foo\\:bar"}},
{"\"foo:bar\"", ' ', -1, []string{"foo\\:bar"}}, {"\"foo:bar\"", ' ', -1, []string{"foo\\:bar"}},
// Tests for quoting dashes
{"'-foo'", ' ', -1, []string{"\\-foo"}},
{"'--foo'", ' ', -1, []string{"\\--foo"}},
{"bar '--foo'", ' ', -1, []string{"bar", "\\--foo"}},
{"bar 'foo-baz'", ' ', -1, []string{"bar", "foo-baz"}},
} }
for _, tc := range testcases { for _, tc := range testcases {
actual := splitEscaped(tc.input, tc.char, tc.limit) actual := splitEscaped(tc.input, tc.char, tc.limit)

View File

@ -1,10 +1,10 @@
Search Query: > -pipefail -tablesizing Search Query: > table_cmd -tquery
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │ Hostname CWD Timestamp Runtime Exit Code Command │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost ~/foo/ 2022/Apr/16 01:03 24s 3 table_cmd2 │ localhost ~/foo/ 2022/Apr/16 01:03 24s 3 table_cmd2 │
│ localhost /tmp/ 2022/Apr/16 01:03 4s 2 table_cmd1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… │ │ localhost /tmp/ 2022/Apr/16 01:03 4s 2 table_cmd1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… │
│ │ │ │
│ │ │ │
│ │ │ │

27
client/testdata/TestTui-SearchQuoteDash vendored Normal file
View File

@ -0,0 +1,27 @@
Search Query: > "--bar"
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:43:46 PDT 3s 2 foo --bar │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘
hiSHtory: Search your shell history • ctrl+h help

View File

@ -3,6 +3,7 @@ Search Query: > -pipefail
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Exit Code git_remote Command │ │ Exit Code git_remote Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ 0 https://github.com/ddworken/hishtory hishtory tquery │
│ 0 https://github.com/ddworken/hishtory hishtory config-set displayed-columns 'Exit Code' git_remote Command │ │ 0 https://github.com/ddworken/hishtory hishtory config-set displayed-columns 'Exit Code' git_remote Command │
│ 0 echo bar │ │ 0 echo bar │
│ 0 cd / │ │ 0 cd / │
@ -22,6 +23,5 @@ Search Query: > -pipefail
│ │ │ │
│ │ │ │
│ │ │ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
hiSHtory: Search your shell history • ctrl+h help hiSHtory: Search your shell history • ctrl+h help

View File

@ -3,6 +3,7 @@ Search Query: > -pipefail
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Exit Code git_remote Command │ │ Exit Code git_remote Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ 0 https://github.com/ddworken/hishtory hishtory tquery │
│ 0 https://github.com/ddworken/hishtory hishtory config-set displayed-columns 'Exit Code' git_remote Command │ │ 0 https://github.com/ddworken/hishtory hishtory config-set displayed-columns 'Exit Code' git_remote Command │
│ 0 echo bar │ │ 0 echo bar │
│ 0 cd / │ │ 0 cd / │
@ -22,6 +23,5 @@ Search Query: > -pipefail
│ │ │ │
│ │ │ │
│ │ │ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
hiSHtory: Search your shell history • ctrl+h help hiSHtory: Search your shell history • ctrl+h help

View File

@ -4,4 +4,5 @@ echo baz
echo baz echo baz
echo foo echo foo
hishtory config-set displayed-columns 'Exit Code' Command hishtory config-set displayed-columns 'Exit Code' Command
hishtory tquery
hishtory config-set filter-duplicate-commands true hishtory config-set filter-duplicate-commands true

View File

@ -1,5 +1,6 @@
Exit Code Command Exit Code Command
0 hishtory config-set filter-duplicate-commands true 0 hishtory config-set filter-duplicate-commands true
0 hishtory tquery
0 hishtory config-set displayed-columns 'Exit Code' Command 0 hishtory config-set displayed-columns 'Exit Code' Command
0 echo foo 0 echo foo
0 echo baz 0 echo baz

View File

@ -3,6 +3,7 @@ Search Query: > -pipefail
┌───────────────────────────────────────────────────────────────────────────┐ ┌───────────────────────────────────────────────────────────────────────────┐
│ Exit Code Command │ │ Exit Code Command │
│───────────────────────────────────────────────────────────────────────────│ │───────────────────────────────────────────────────────────────────────────│
│ 0 hishtory tquery │
│ 0 hishtory config-set filter-duplicate-commands true │ │ 0 hishtory config-set filter-duplicate-commands true │
│ 0 hishtory config-set displayed-columns 'Exit Code' Command │ │ 0 hishtory config-set displayed-columns 'Exit Code' Command │
│ 0 echo foo │ │ 0 echo foo │
@ -22,6 +23,5 @@ Search Query: > -pipefail
│ │ │ │
│ │ │ │
│ │ │ │
│ │
└───────────────────────────────────────────────────────────────────────────┘ └───────────────────────────────────────────────────────────────────────────┘
hiSHtory: Search your shell history • ctrl+h help hiSHtory: Search your shell history • ctrl+h help

View File

@ -3,6 +3,7 @@ Search Query: > -pipefail
┌───────────────────────────────────────────────────────────────────────────┐ ┌───────────────────────────────────────────────────────────────────────────┐
│ Exit Code Command │ │ Exit Code Command │
│───────────────────────────────────────────────────────────────────────────│ │───────────────────────────────────────────────────────────────────────────│
│ 0 hishtory tquery │
│ 0 hishtory config-set displayed-columns 'Exit Code' Command │ │ 0 hishtory config-set displayed-columns 'Exit Code' Command │
│ 0 echo foo │ │ 0 echo foo │
│ 0 echo baz │ │ 0 echo baz │
@ -22,6 +23,5 @@ Search Query: > -pipefail
│ │ │ │
│ │ │ │
│ │ │ │
│ │
└───────────────────────────────────────────────────────────────────────────┘ └───────────────────────────────────────────────────────────────────────────┘
hiSHtory: Search your shell history • ctrl+h help hiSHtory: Search your shell history • ctrl+h help

View File

@ -361,7 +361,7 @@ func stripShellPrefix(out string) string {
func stripRequiredPrefix(t *testing.T, out, prefix string) string { func stripRequiredPrefix(t *testing.T, out, prefix string) string {
require.Contains(t, out, prefix) require.Contains(t, out, prefix)
return strings.TrimSpace(strings.Split(out, prefix)[1]) return strings.TrimSpace(strings.SplitN(out, prefix, 2)[1])
} }
func stripTuiCommandPrefix(t *testing.T, out string) string { func stripTuiCommandPrefix(t *testing.T, out string) string {

View File

@ -3,6 +3,7 @@ package tui
import ( import (
"context" "context"
_ "embed" // for embedding config.sh _ "embed" // for embedding config.sh
"encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -95,6 +96,9 @@ type model struct {
// The currently executing shell. Defaults to bash if not specified. Used for more precise AI suggestions. // The currently executing shell. Defaults to bash if not specified. Used for more precise AI suggestions.
shellName string shellName string
// 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
} }
type ( type (
@ -119,6 +123,8 @@ type asyncQueryFinishedMsg struct {
maintainCursor bool maintainCursor bool
// An updated search query. May be used for initial queries when they're invalid. // An updated search query. May be used for initial queries when they're invalid.
overriddenSearchQuery *string overriddenSearchQuery *string
isFirstQuery bool
} }
func initialModel(ctx context.Context, shellName, initialQuery string) model { func initialModel(ctx context.Context, shellName, initialQuery string) model {
@ -148,7 +154,7 @@ func initialModel(ctx context.Context, shellName, initialQuery string) model {
queryInput.SetValue(initialQuery) queryInput.SetValue(initialQuery)
} }
CURRENT_QUERY_FOR_HIGHLIGHTING = 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} return model{ctx: ctx, spinner: s, isLoading: true, table: nil, tableEntries: []*data.HistoryEntry{}, runQuery: &initialQuery, queryInput: queryInput, help: help.New(), shellName: shellName, hasFinishedFirstLoad: false}
} }
func (m model) Init() tea.Cmd { func (m model) Init() tea.Cmd {
@ -199,13 +205,14 @@ func preventTableOverscrolling(m model) {
func runQueryAndUpdateTable(m model, forceUpdateTable, maintainCursor bool) tea.Cmd { func runQueryAndUpdateTable(m model, forceUpdateTable, maintainCursor bool) tea.Cmd {
if (m.runQuery != nil && *m.runQuery != m.lastQuery) || forceUpdateTable || m.searchErr != nil { if (m.runQuery != nil && *m.runQuery != m.lastQuery) || forceUpdateTable || m.searchErr != nil {
// if !m.hasFinishedFirstLoad {
// return nil
// }
query := m.lastQuery query := m.lastQuery
if m.runQuery != nil { if m.runQuery != nil {
query = *m.runQuery query = *m.runQuery
} }
LAST_DISPATCHED_QUERY_ID++ queryId := allocateQueryId()
queryId := LAST_DISPATCHED_QUERY_ID
LAST_DISPATCHED_QUERY_TIMESTAMP = time.Now()
return func() tea.Msg { return func() tea.Msg {
conf := hctx.GetConf(m.ctx) conf := hctx.GetConf(m.ctx)
defaultFilter := conf.DefaultFilter defaultFilter := conf.DefaultFilter
@ -214,7 +221,7 @@ func runQueryAndUpdateTable(m model, forceUpdateTable, maintainCursor bool) tea.
defaultFilter = "" defaultFilter = ""
} }
rows, entries, searchErr := getRows(m.ctx, conf.DisplayedColumns, m.shellName, defaultFilter, query, getNumEntriesNeeded(m.ctx)) 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, false}
} }
} }
return nil return nil
@ -331,6 +338,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.queryInput.SetValue(*msg.overriddenSearchQuery) m.queryInput.SetValue(*msg.overriddenSearchQuery)
} }
} }
if msg.isFirstQuery {
m.hasFinishedFirstLoad = true
}
return m, nil return m, nil
default: default:
var cmd tea.Cmd var cmd tea.Cmd
@ -879,28 +889,72 @@ func configureColorProfile(ctx context.Context) {
} }
} }
func TuiQuery(ctx context.Context, shellName, initialQuery string) error { func buildInitialQueryWithSearchEscaping(initialQueryArray []string) (string, error) {
var initialQuery string
for i, queryChunk := range initialQueryArray {
if i != 0 {
initialQuery += " "
}
if strings.HasPrefix(queryChunk, "-") {
quoted, err := json.Marshal(queryChunk)
if err != nil {
return "", fmt.Errorf("failed to marshal query chunk for escaping: %w", err)
}
initialQuery += string(quoted)
} else {
initialQuery += queryChunk
}
}
return initialQuery, nil
}
func splitQueryArray(initialQueryArray []string) []string {
var splitQueryArray []string
for _, queryChunk := range initialQueryArray {
splitQueryArray = append(splitQueryArray, strings.Split(queryChunk, " ")...)
}
return splitQueryArray
}
func allocateQueryId() int {
LAST_DISPATCHED_QUERY_ID++
LAST_DISPATCHED_QUERY_TIMESTAMP = time.Now()
return LAST_DISPATCHED_QUERY_ID
}
func TuiQuery(ctx context.Context, shellName string, initialQueryArray []string) error {
initialQueryArray = splitQueryArray(initialQueryArray)
initialQueryWithEscaping, err := buildInitialQueryWithSearchEscaping(initialQueryArray)
if err != nil {
return err
}
loadedKeyBindings = hctx.GetConf(ctx).KeyBindings.ToKeyMap() loadedKeyBindings = hctx.GetConf(ctx).KeyBindings.ToKeyMap()
configureColorProfile(ctx) configureColorProfile(ctx)
additionalOptions := []tea.ProgramOption{tea.WithOutput(os.Stderr)} additionalOptions := []tea.ProgramOption{tea.WithOutput(os.Stderr)}
if hctx.GetConf(ctx).FullScreenRendering { if hctx.GetConf(ctx).FullScreenRendering {
additionalOptions = append(additionalOptions, tea.WithAltScreen()) additionalOptions = append(additionalOptions, tea.WithAltScreen())
} }
p := tea.NewProgram(initialModel(ctx, shellName, initialQuery), additionalOptions...) p := tea.NewProgram(initialModel(ctx, shellName, initialQueryWithEscaping), additionalOptions...)
// Async: Get the initial set of rows // Async: Get the initial set of rows
go func() { go func() {
LAST_DISPATCHED_QUERY_ID++ queryId := allocateQueryId()
queryId := LAST_DISPATCHED_QUERY_ID
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, getNumEntriesNeeded(ctx)) rows, entries, err := getRows(ctx, conf.DisplayedColumns, shellName, conf.DefaultFilter, initialQueryWithEscaping, getNumEntriesNeeded(ctx))
if err == nil || initialQuery == "" { if err == nil || initialQueryWithEscaping == "" {
p.Send(asyncQueryFinishedMsg{queryId: queryId, rows: rows, entries: entries, searchErr: err, forceUpdateTable: true, maintainCursor: false, overriddenSearchQuery: nil}) if err != nil {
panic(err)
}
p.Send(asyncQueryFinishedMsg{queryId: queryId, rows: rows, entries: entries, searchErr: err, forceUpdateTable: true, maintainCursor: false, overriddenSearchQuery: nil, isFirstQuery: true})
} else { } else {
// initialQuery is likely invalid in some way, let's just drop it // The initial query 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, getNumEntriesNeeded(ctx)) 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}) if err != nil {
panic(err)
}
p.Send(asyncQueryFinishedMsg{queryId: allocateQueryId(), rows: rows, entries: entries, searchErr: err, forceUpdateTable: true, maintainCursor: false, overriddenSearchQuery: &emptyQuery, isFirstQuery: true})
} }
}() }()
// Async: Retrieve additional entries from the backend // Async: Retrieve additional entries from the backend
@ -931,13 +985,13 @@ func TuiQuery(ctx context.Context, shellName, initialQuery string) error {
p.Send(bannerMsg{banner: string(banner)}) p.Send(bannerMsg{banner: string(banner)})
}() }()
// Blocking: Start the TUI // Blocking: Start the TUI
_, err := p.Run() _, err = p.Run()
if err != nil { if err != nil {
return err return err
} }
if SELECTED_COMMAND == "" && os.Getenv("HISHTORY_TERM_INTEGRATION") != "" { if SELECTED_COMMAND == "" && os.Getenv("HISHTORY_TERM_INTEGRATION") != "" {
// Print out the initialQuery instead so that we don't clear the terminal // Print out the initialQuery instead so that we don't clear the terminal (note that we don't use the escaped one here)
SELECTED_COMMAND = initialQuery SELECTED_COMMAND = strings.Join(initialQueryArray, " ")
} }
fmt.Printf("%s\n", SELECTED_COMMAND) fmt.Printf("%s\n", SELECTED_COMMAND)
return nil return nil
@ -945,4 +999,3 @@ func TuiQuery(ctx context.Context, shellName, initialQuery string) error {
// TODO: support custom key bindings // TODO: support custom key bindings
// TODO: make the help page wrap // TODO: make the help page wrap
// TODO: If the initial query contains dashes, maybe we should smartly escape them?