mirror of
https://github.com/ddworken/hishtory.git
synced 2024-12-25 16:28:53 +01:00
Initial working version of control-r search
This commit is contained in:
parent
3fb55eb192
commit
5a943c20f1
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user