Initial working version of control-r search

This commit is contained in:
David Dworken 2022-10-16 12:43:16 -07:00
parent 3fb55eb192
commit 5a943c20f1
4 changed files with 90 additions and 21 deletions

View File

@ -144,6 +144,7 @@ func TestParameterized(t *testing.T) {
t.Run("testLocalRedaction/"+tester.ShellName(), func(t *testing.T) { testLocalRedaction(t, tester) }) t.Run("testLocalRedaction/"+tester.ShellName(), func(t *testing.T) { testLocalRedaction(t, tester) })
t.Run("testRemoteRedaction/"+tester.ShellName(), func(t *testing.T) { testRemoteRedaction(t, tester) }) t.Run("testRemoteRedaction/"+tester.ShellName(), func(t *testing.T) { testRemoteRedaction(t, tester) })
t.Run("testMultipleUsers/"+tester.ShellName(), func(t *testing.T) { testMultipleUsers(t, tester) }) t.Run("testMultipleUsers/"+tester.ShellName(), func(t *testing.T) { testMultipleUsers(t, tester) })
t.Run("testConfigGetSet/"+tester.ShellName(), func(t *testing.T) { testConfigGetSet(t, tester) })
// TODO: Add a test for multi-line history entries // TODO: Add a test for multi-line history entries
} }
} }
@ -1545,6 +1546,32 @@ ls /tmp`, randomCmdUuid, randomCmdUuid)
} }
} }
func testConfigGetSet(t *testing.T, tester shellTester) {
// Setup
defer shared.BackupAndRestore(t)()
installHishtory(t, tester, "")
// Initially is false
out := tester.RunInteractiveShell(t, `hishtory config-get enable-control-r`)
if out != "false" {
t.Fatalf("unexpected config-get output: %#v", out)
}
// Set to true and check
tester.RunInteractiveShell(t, `hishtory config-set enable-control-r true`)
out = tester.RunInteractiveShell(t, `hishtory config-get enable-control-r`)
if out != "true" {
t.Fatalf("unexpected config-get output: %#v", out)
}
// Set to false and check
tester.RunInteractiveShell(t, `hishtory config-set enable-control-r false`)
out = tester.RunInteractiveShell(t, `hishtory config-get enable-control-r`)
if out != "false" {
t.Fatalf("unexpected config-get output: %#v", out)
}
}
type deviceSet struct { type deviceSet struct {
deviceMap *map[device]deviceOp deviceMap *map[device]deviceOp
currentDevice *device currentDevice *device

View File

@ -21,3 +21,16 @@ function _hishtory_precmd() {
fi fi
(hishtory saveHistoryEntry zsh $_hishtory_exit_code "$_hishtory_command" $_hishtory_start_time &) (hishtory saveHistoryEntry zsh $_hishtory_exit_code "$_hishtory_command" $_hishtory_start_time &)
} }
_hishtory_widget() {
BUFFER=$(hishtory tquery $BUFFER | tr -d '\n')
CURSOR=${#BUFFER}
zle reset-prompt
}
_hishtory_bind_control_r() {
zle -N _hishtory_widget
bindkey '^R' _hishtory_widget
}
[ "$(hishtory config-get enable-control-r)" = true ] && _hishtory_bind_control_r

View File

@ -3,6 +3,8 @@ package lib
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"strings"
_ "embed" // for embedding config.sh _ "embed" // for embedding config.sh
@ -13,8 +15,14 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/ddworken/hishtory/client/data" "github.com/ddworken/hishtory/client/data"
"github.com/ddworken/hishtory/client/hctx" "github.com/ddworken/hishtory/client/hctx"
"github.com/muesli/termenv"
) )
const TABLE_HEIGHT = 20
const PADDED_NUM_ENTRIES = TABLE_HEIGHT * 3
var selectedRow string = ""
var baseStyle = lipgloss.NewStyle(). var baseStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()). BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")) BorderForeground(lipgloss.Color("240"))
@ -29,6 +37,7 @@ type model struct {
selected bool selected bool
table table.Model table table.Model
runQuery string runQuery string
lastQuery string
err error err error
queryInput textinput.Model queryInput textinput.Model
banner string banner string
@ -41,7 +50,7 @@ type bannerMsg struct {
banner string banner string
} }
func initialModel(ctx *context.Context, t table.Model) model { func initialModel(ctx *context.Context, t table.Model, initialQuery string) model {
s := spinner.New() s := spinner.New()
s.Spinner = spinner.Dot s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
@ -50,25 +59,32 @@ func initialModel(ctx *context.Context, t table.Model) model {
queryInput.Focus() queryInput.Focus()
queryInput.CharLimit = 156 queryInput.CharLimit = 156
queryInput.Width = 50 queryInput.Width = 50
return model{ctx: ctx, spinner: s, isLoading: true, table: t, runQuery: "", queryInput: queryInput} if initialQuery != "" {
queryInput.SetValue(initialQuery)
}
return model{ctx: ctx, spinner: s, isLoading: true, table: t, runQuery: initialQuery, queryInput: queryInput}
} }
func (m model) Init() tea.Cmd { func (m model) Init() tea.Cmd {
return m.spinner.Tick return m.spinner.Tick
} }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func runQueryAndUpdateTable(m model) model {
if m.runQuery != "" { if m.runQuery != "" && m.runQuery != m.lastQuery {
rows, err := getRows(m.ctx, m.runQuery) rows, err := getRows(m.ctx, m.runQuery)
if err != nil { if err != nil {
m.err = err m.err = err
return m, nil return m
} }
m.table.SetRows(rows) m.table.SetRows(rows)
m.table.SetCursor(0) m.table.SetCursor(0)
m.lastQuery = m.runQuery
m.runQuery = "" m.runQuery = ""
} }
return m
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
@ -84,6 +100,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
i, cmd2 := m.queryInput.Update(msg) i, cmd2 := m.queryInput.Update(msg)
m.queryInput = i m.queryInput = i
m.runQuery = m.queryInput.Value() m.runQuery = m.queryInput.Value()
m = runQueryAndUpdateTable(m)
return m, tea.Batch(cmd1, cmd2) return m, tea.Batch(cmd1, cmd2)
} }
case errMsg: case errMsg:
@ -114,34 +131,43 @@ func (m model) View() string {
if m.err != nil { if m.err != nil {
return fmt.Sprintf("An unrecoverable error occured: %v\n", m.err) return fmt.Sprintf("An unrecoverable error occured: %v\n", m.err)
} }
if m.isLoading {
return fmt.Sprintf("\n\n %s Loading hishtory entries from other devices... press q to quit\n\n", m.spinner.View())
}
if m.selected { if m.selected {
return fmt.Sprintf("\n%s\n", m.table.SelectedRow()[4]) selectedRow = m.table.SelectedRow()[4]
return ""
}
loadingMessage := ""
if m.isLoading {
loadingMessage = fmt.Sprintf("%s Loading hishtory entries from other devices...", m.spinner.View())
} }
offlineWarning := "" offlineWarning := ""
if m.isOffline { if m.isOffline {
offlineWarning = "Warning: failed to contact the hishtory backend (are you offline?), so some results may be stale\n\n" offlineWarning = "Warning: failed to contact the hishtory backend (are you offline?), so some results may be stale\n\n"
} }
return fmt.Sprintf("\n%s%s\nSearch Query: %s\n\n%s\n", offlineWarning, m.banner, m.queryInput.View(), baseStyle.Render(m.table.View())) return fmt.Sprintf("\n%s\n%s%s\nSearch Query: %s\n\n%s\n", loadingMessage, offlineWarning, m.banner, m.queryInput.View(), baseStyle.Render(m.table.View()))
} }
func getRows(ctx *context.Context, query string) ([]table.Row, error) { func getRows(ctx *context.Context, query string) ([]table.Row, error) {
db := hctx.GetDb(ctx) db := hctx.GetDb(ctx)
data, err := data.Search(db, query, 25) data, err := data.Search(db, query, PADDED_NUM_ENTRIES)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var rows []table.Row var rows []table.Row
for _, entry := range data { for i := 0; i < PADDED_NUM_ENTRIES; i++ {
if i < len(data) {
entry := data[i]
entry.Command = strings.ReplaceAll(entry.Command, "\n", " ") // TODO: handle multi-line commands better here
row := table.Row{entry.Hostname, entry.CurrentWorkingDirectory, entry.StartTime.Format("Jan 2 2006 15:04:05 MST"), fmt.Sprintf("%d", entry.ExitCode), entry.Command} row := table.Row{entry.Hostname, entry.CurrentWorkingDirectory, entry.StartTime.Format("Jan 2 2006 15:04:05 MST"), fmt.Sprintf("%d", entry.ExitCode), entry.Command}
rows = append(rows, row) rows = append(rows, row)
} else {
rows = append(rows, table.Row{})
}
} }
return rows, nil return rows, nil
} }
func TuiQuery(ctx *context.Context) error { func TuiQuery(ctx *context.Context, initialQuery string) error {
lipgloss.SetColorProfile(termenv.ANSI)
columns := []table.Column{ columns := []table.Column{
{Title: "Hostname", Width: 25}, {Title: "Hostname", Width: 25},
{Title: "CWD", Width: 40}, {Title: "CWD", Width: 40},
@ -149,15 +175,15 @@ func TuiQuery(ctx *context.Context) error {
{Title: "Exit Code", Width: 9}, {Title: "Exit Code", Width: 9},
{Title: "Command", Width: 70}, {Title: "Command", Width: 70},
} }
rows, err := getRows(ctx, "") rows, err := getRows(ctx, initialQuery)
if err != nil { if err != nil {
return err return err
} }
t := table.New( t := table.New(
table.WithColumns(columns), table.WithColumns(columns),
table.WithRows(rows), table.WithRows(rows), // TODO: need to pad this to always have at least length items
table.WithFocused(true), table.WithFocused(true),
table.WithHeight(20), table.WithHeight(TABLE_HEIGHT),
) )
s := table.DefaultStyles() s := table.DefaultStyles()
@ -173,7 +199,7 @@ func TuiQuery(ctx *context.Context) error {
t.SetStyles(s) t.SetStyles(s)
t.Focus() t.Focus()
p := tea.NewProgram(initialModel(ctx, t)) p := tea.NewProgram(initialModel(ctx, t, initialQuery), tea.WithOutput(os.Stderr))
go func() { go func() {
err := RetrieveAdditionalEntriesFromRemote(ctx) err := RetrieveAdditionalEntriesFromRemote(ctx)
if err != nil { if err != nil {
@ -196,5 +222,8 @@ func TuiQuery(ctx *context.Context) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("%s\n", selectedRow)
return nil return nil
} }
// TODO: handling if someone hits enter when there are no results

View File

@ -36,7 +36,7 @@ func main() {
case "tquery": case "tquery":
ctx := hctx.MakeContext() ctx := hctx.MakeContext()
lib.CheckFatalError(lib.ProcessDeletionRequests(ctx)) lib.CheckFatalError(lib.ProcessDeletionRequests(ctx))
lib.CheckFatalError(lib.TuiQuery(ctx)) lib.CheckFatalError(lib.TuiQuery(ctx, strings.Join(os.Args[2:], " ")))
case "export": case "export":
ctx := hctx.MakeContext() ctx := hctx.MakeContext()
lib.CheckFatalError(lib.ProcessDeletionRequests(ctx)) lib.CheckFatalError(lib.ProcessDeletionRequests(ctx))
@ -113,7 +113,6 @@ func main() {
log.Fatalf("Failed to update hishtory: %v", err) log.Fatalf("Failed to update hishtory: %v", err)
} }
case "config-get": case "config-get":
// TODO: tests for config-get and config-set
ctx := hctx.MakeContext() ctx := hctx.MakeContext()
config := hctx.GetConf(ctx) config := hctx.GetConf(ctx)
key := os.Args[2] key := os.Args[2]
@ -343,3 +342,4 @@ func export(ctx *context.Context, query string) {
} }
// TODO(feature): Add a session_id column that corresponds to the shell session the command was run in // TODO(feature): Add a session_id column that corresponds to the shell session the command was run in
// TODO: Skip recording of empty commands