Merge branch 'master' into quote-init

This commit is contained in:
David Dworken 2024-10-20 09:26:57 -07:00 committed by GitHub
commit 15bc95c560
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 270 additions and 25 deletions

View File

@ -159,8 +159,8 @@ hishtory config-set filter-duplicate-commands true
If you don't need the ability to sync your shell history, you can install hiSHtory in offline mode: If you don't need the ability to sync your shell history, you can install hiSHtory in offline mode:
``` ```sh
curl https://hishtory.dev/install.py | HISHTORY_OFFLINE=true python3 - curl https://hishtory.dev/install.py | python3 - --offline
``` ```
This disables syncing completely so that the client will not rely on the hiSHtory backend at all. You can also change the syncing status via `hishtory syncing enable` or `hishtory syncing disable`. This disables syncing completely so that the client will not rely on the hiSHtory backend at all. You can also change the syncing status via `hishtory syncing enable` or `hishtory syncing disable`.
@ -199,6 +199,19 @@ You can configure a custom timestamp format for hiSHtory via `hishtory config-se
</blockquote></details> </blockquote></details>
<details>
<summary>Custom rendering</summary><blockquote>
By default, hiHStory tries to render the TUI in a reasonable way that balances terminal space consumption and TUI usability. If you find that you wish to customize this behavior, there are two config options that you can experiment with enabling:
```
hishtory config-set compact-mode true # Renders the TUI in "compact mode" with less whitespace
hishtory config-set full-screen true # Renders the TUI in "full-screen mode" so that it uses the entire terminal
```
</blockquote></details>
<details> <details>
<summary>Web UI for sharing</summary><blockquote> <summary>Web UI for sharing</summary><blockquote>

View File

@ -1 +1 @@
312 314

View File

@ -53,6 +53,9 @@ os.system('chmod +x ' + tmpFilePath)
cmd = tmpFilePath + ' install' cmd = tmpFilePath + ' install'
if os.environ.get('HISHTORY_OFFLINE'): if os.environ.get('HISHTORY_OFFLINE'):
cmd += " --offline" cmd += " --offline"
additional_flags = [flag for flag in sys.argv[1:] if flag.startswith("-") and flag != "-" and flag != "--"]
if additional_flags:
cmd += " " + " ".join(additional_flags)
exitCode = os.system(cmd) exitCode = os.system(cmd)
os.remove(tmpFilePath) os.remove(tmpFilePath)
if exitCode != 0: if exitCode != 0:

View File

@ -119,6 +119,7 @@ func TestParam(t *testing.T) {
t.Run("testTui/ai", wrapTestForSharding(testTui_ai)) t.Run("testTui/ai", wrapTestForSharding(testTui_ai))
t.Run("testTui/defaultFilter", wrapTestForSharding(testTui_defaultFilter)) t.Run("testTui/defaultFilter", wrapTestForSharding(testTui_defaultFilter))
t.Run("testTui/escaping", wrapTestForSharding(testTui_escaping)) t.Run("testTui/escaping", wrapTestForSharding(testTui_escaping))
t.Run("testTui/fullscreen", wrapTestForSharding(testTui_fullscreen))
// Assert there are no leaked connections // Assert there are no leaked connections
assertNoLeakedConnections(t) assertNoLeakedConnections(t)
@ -838,13 +839,13 @@ func testHishtoryBackgroundSaving(t *testing.T, tester shellTester) {
// Setup // Setup
defer testutils.BackupAndRestore(t)() defer testutils.BackupAndRestore(t)()
// Check that we can find the go binary // Check that we can find the go binary and use that path to it for consistency
_, err := exec.LookPath("go") goBinPath, err := exec.LookPath("go")
require.NoError(t, err) require.NoError(t, err)
// Test install with an unset HISHTORY_TEST var so that we save in the background (this is likely to be flakey!) // Test install with an unset HISHTORY_TEST var so that we save in the background (this is likely to be flakey!)
out := tester.RunInteractiveShell(t, `unset HISHTORY_TEST out := tester.RunInteractiveShell(t, `unset HISHTORY_TEST
CGO_ENABLED=0 go build -o /tmp/client CGO_ENABLED=0 `+goBinPath+` build -o /tmp/client
/tmp/client install`) /tmp/client install`)
r := regexp.MustCompile(`Setting secret hishtory key to (.*)`) r := regexp.MustCompile(`Setting secret hishtory key to (.*)`)
matches := r.FindStringSubmatch(out) matches := r.FindStringSubmatch(out)
@ -1063,18 +1064,16 @@ func TestInstallViaPythonScriptWithCustomHishtoryPath(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, os.RemoveAll(path.Join(homedir, altHishtoryPath))) require.NoError(t, os.RemoveAll(path.Join(homedir, altHishtoryPath)))
testInstallViaPythonScriptChild(t, zshTester{}) testInstallViaPythonScriptChild(t, zshTester{}, Online)
} }
func TestInstallViaPythonScriptInOfflineMode(t *testing.T) { func TestInstallViaPythonScriptInOfflineMode(t *testing.T) {
markTestForSharding(t, 1) markTestForSharding(t, 1)
defer testutils.BackupAndRestore(t)() defer testutils.BackupAndRestore(t)()
defer testutils.BackupAndRestoreEnv("HISHTORY_OFFLINE")()
os.Setenv("HISHTORY_OFFLINE", "1")
tester := zshTester{} tester := zshTester{}
// Check that installing works // Check that installing works
testInstallViaPythonScriptChild(t, tester) testInstallViaPythonScriptChild(t, tester, Offline)
// And check that it installed in offline mode // And check that it installed in offline mode
out := tester.RunInteractiveShell(t, `hishtory status -v`) out := tester.RunInteractiveShell(t, `hishtory status -v`)
@ -1083,14 +1082,14 @@ func TestInstallViaPythonScriptInOfflineMode(t *testing.T) {
func testInstallViaPythonScript(t *testing.T, tester shellTester) { func testInstallViaPythonScript(t *testing.T, tester shellTester) {
defer testutils.BackupAndRestore(t)() defer testutils.BackupAndRestore(t)()
testInstallViaPythonScriptChild(t, tester) testInstallViaPythonScriptChild(t, tester, Online)
// And check that it installed in online mode // And check that it installed in online mode
out := tester.RunInteractiveShell(t, `hishtory status -v`) out := tester.RunInteractiveShell(t, `hishtory status -v`)
require.Contains(t, out, "\nSync Mode: Enabled\n") require.Contains(t, out, "\nSync Mode: Enabled\n")
} }
func testInstallViaPythonScriptChild(t *testing.T, tester shellTester) { func testInstallViaPythonScriptChild(t *testing.T, tester shellTester, onlineStatus OnlineStatus) {
if !testutils.IsOnline() { if !testutils.IsOnline() {
t.Skip("skipping because we're currently offline") t.Skip("skipping because we're currently offline")
} }
@ -1099,7 +1098,11 @@ func testInstallViaPythonScriptChild(t *testing.T, tester shellTester) {
defer testutils.BackupAndRestoreEnv("HISHTORY_TEST")() defer testutils.BackupAndRestoreEnv("HISHTORY_TEST")()
// Install via the python script // Install via the python script
out := tester.RunInteractiveShell(t, `curl https://hishtory.dev/install.py | python3 -`) additionalFlags := " "
if onlineStatus == Offline {
additionalFlags = "--offline"
}
out := tester.RunInteractiveShell(t, `curl https://hishtory.dev/install.py | python3 - `+additionalFlags)
require.Contains(t, out, "Succesfully installed hishtory") require.Contains(t, out, "Succesfully installed hishtory")
r := regexp.MustCompile(`Setting secret hishtory key to (.*)`) r := regexp.MustCompile(`Setting secret hishtory key to (.*)`)
matches := r.FindStringSubmatch(out) matches := r.FindStringSubmatch(out)
@ -1869,6 +1872,54 @@ func testTui_escaping(t *testing.T) {
testutils.CompareGoldens(t, out, "TestTui-Escaping") testutils.CompareGoldens(t, out, "TestTui-Escaping")
} }
func testTui_fullscreen(t *testing.T) {
// Setup
defer testutils.BackupAndRestore(t)()
tester, _, _ := setupTestTui(t, Online)
// By default full-screen mode is disabled
require.Equal(t, "false", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get full-screen`)))
require.Equal(t, "false", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get compact-mode`)))
// Test that we can enable it
tester.RunInteractiveShell(t, `hishtory config-set full-screen true`)
require.Equal(t, "true", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get full-screen`)))
// Test that it renders in full-screen mode taking up the entire terminal
out := captureTerminalOutput(t, tester, []string{
"echo foo ENTER",
"hishtory SPACE tquery ENTER",
})
testutils.CompareGoldens(t, out, "TestTui-FullScreenRender")
// Test that it clears full-screen mode and restores the original terminal state
out = captureTerminalOutput(t, tester, []string{
"echo SPACE foo ENTER",
"hishtory SPACE tquery ENTER",
"Escape",
})
require.Contains(t, out, "echo foo\n")
require.Contains(t, out, "hishtory tquery\n")
require.NotContains(t, out, "Search Query")
require.True(t, len(strings.Split(out, "\n")) <= 7)
// Test that it renders the help page fine
out = captureTerminalOutput(t, tester, []string{
"echo SPACE foo ENTER",
"hishtory SPACE tquery ENTER",
"C-h",
})
testutils.CompareGoldens(t, out, "TestTui-FullScreenHelp")
// Test that it renders fine in full-screen mode and compact-mode
tester.RunInteractiveShell(t, `hishtory config-set compact-mode true`)
out = captureTerminalOutput(t, tester, []string{
"echo foo ENTER",
"hishtory SPACE tquery ENTER",
})
testutils.CompareGoldens(t, out, "TestTui-FullScreenCompactRender")
}
func testTui_defaultFilter(t *testing.T) { func testTui_defaultFilter(t *testing.T) {
// Setup // Setup
defer testutils.BackupAndRestore(t)() defer testutils.BackupAndRestore(t)()

View File

@ -184,6 +184,7 @@ func init() {
configGetCmd.AddCommand(getAiCompletionEndpoint) configGetCmd.AddCommand(getAiCompletionEndpoint)
configGetCmd.AddCommand(getCompactMode) configGetCmd.AddCommand(getCompactMode)
configGetCmd.AddCommand(getLogLevelCmd) configGetCmd.AddCommand(getLogLevelCmd)
configGetCmd.AddCommand(getFullScreenCmd)
} }
var getLogLevelCmd = &cobra.Command{ var getLogLevelCmd = &cobra.Command{
@ -195,3 +196,13 @@ var getLogLevelCmd = &cobra.Command{
fmt.Println(config.LogLevel.String()) fmt.Println(config.LogLevel.String())
}, },
} }
var getFullScreenCmd = &cobra.Command{
Use: "full-screen",
Short: "Get whether or not hishtory is configured to run in full-screen mode",
Run: func(cmd *cobra.Command, args []string) {
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
fmt.Println(config.FullScreenRendering)
},
}

View File

@ -262,6 +262,19 @@ var setLogLevelCmd = &cobra.Command{
}, },
} }
var setFullScreenCmd = &cobra.Command{
Use: "full-screen",
Short: "Configure whether or not hishtory is configured to run in full-screen mode",
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.FullScreenRendering = args[0] == "true"
lib.CheckFatalError(hctx.SetConfig(config))
},
}
func init() { func init() {
rootCmd.AddCommand(configSetCmd) rootCmd.AddCommand(configSetCmd)
configSetCmd.AddCommand(setEnableControlRCmd) configSetCmd.AddCommand(setEnableControlRCmd)
@ -277,6 +290,7 @@ func init() {
configSetCmd.AddCommand(setAiCompletionEndpoint) configSetCmd.AddCommand(setAiCompletionEndpoint)
configSetCmd.AddCommand(compactMode) configSetCmd.AddCommand(compactMode)
configSetCmd.AddCommand(setLogLevelCmd) configSetCmd.AddCommand(setLogLevelCmd)
configSetCmd.AddCommand(setFullScreenCmd)
setColorSchemeCmd.AddCommand(setColorSchemeSelectedText) setColorSchemeCmd.AddCommand(setColorSchemeSelectedText)
setColorSchemeCmd.AddCommand(setColorSchemeSelectedBackground) setColorSchemeCmd.AddCommand(setColorSchemeSelectedBackground)
setColorSchemeCmd.AddCommand(setColorSchemeBorderColor) setColorSchemeCmd.AddCommand(setColorSchemeBorderColor)

View File

@ -42,6 +42,9 @@ var installCmd = &cobra.Command{
if len(args) > 0 { if len(args) > 0 {
secretKey = args[0] secretKey = args[0]
} }
if strings.HasPrefix(secretKey, "-") {
lib.CheckFatalError(fmt.Errorf("secret key %#v looks like a CLI flag, please use a secret key that does not start with a -", secretKey))
}
lib.CheckFatalError(install(secretKey, *offlineInstall, *skipConfigModification)) lib.CheckFatalError(install(secretKey, *offlineInstall, *skipConfigModification))
if os.Getenv("HISHTORY_SKIP_INIT_IMPORT") == "" { if os.Getenv("HISHTORY_SKIP_INIT_IMPORT") == "" {
db, err := hctx.OpenLocalSqliteDb() db, err := hctx.OpenLocalSqliteDb()

View File

@ -221,6 +221,8 @@ type ClientConfig struct {
KeyBindings keybindings.SerializableKeyMap `json:"key_bindings"` KeyBindings keybindings.SerializableKeyMap `json:"key_bindings"`
// The log level for hishtory (e.g., "debug", "info", "warn", "error") // The log level for hishtory (e.g., "debug", "info", "warn", "error")
LogLevel logrus.Level `json:"log_level"` LogLevel logrus.Level `json:"log_level"`
// Whether the TUI should render in full-screen mode
FullScreenRendering bool `json:"full_screen_rendering"`
} }
type ColorScheme struct { type ColorScheme struct {

View File

@ -46,7 +46,7 @@ function __hishtory_postcommand() {
if [ -n "${HISHTORY_FIRST_PROMPT:-}" ]; then if [ -n "${HISHTORY_FIRST_PROMPT:-}" ]; then
unset HISHTORY_FIRST_PROMPT unset HISHTORY_FIRST_PROMPT
return return $EXIT_CODE
fi fi
# Run after every prompt # Run after every prompt
@ -57,6 +57,8 @@ function __hishtory_postcommand() {
LAST_SAVED_COMMAND=$CMD LAST_SAVED_COMMAND=$CMD
(hishtory updateLocalDbFromRemote &) (hishtory updateLocalDbFromRemote &)
return $EXIT_CODE
} }
PROMPT_COMMAND="__hishtory_postcommand; $PROMPT_COMMAND" PROMPT_COMMAND="__hishtory_postcommand; $PROMPT_COMMAND"
export HISTTIMEFORMAT=$HISTTIMEFORMAT export HISTTIMEFORMAT=$HISTTIMEFORMAT

View File

@ -0,0 +1,40 @@
Search Query: > ls
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ 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 ~/ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘

46
client/testdata/TestTui-FullScreenHelp vendored Normal file
View File

@ -0,0 +1,46 @@
Search Query: > ls
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ 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
↑ 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

View File

@ -0,0 +1,42 @@
Search Query: > ls
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ 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

@ -30,11 +30,6 @@ import (
"golang.org/x/term" "golang.org/x/term"
) )
const (
TABLE_HEIGHT = 20
PADDED_NUM_ENTRIES = TABLE_HEIGHT * 5
)
var ( var (
CURRENT_QUERY_FOR_HIGHLIGHTING string = "" CURRENT_QUERY_FOR_HIGHLIGHTING string = ""
SELECTED_COMMAND string = "" SELECTED_COMMAND string = ""
@ -225,7 +220,7 @@ func runQueryAndUpdateTable(m model, forceUpdateTable, maintainCursor bool) tea.
// The default filter was cleared for this session, so don't apply it // The default filter was cleared for this session, so don't apply it
defaultFilter = "" defaultFilter = ""
} }
rows, entries, searchErr := getRows(m.ctx, conf.DisplayedColumns, m.shellName, defaultFilter, query, PADDED_NUM_ENTRIES) rows, entries, searchErr := getRows(m.ctx, conf.DisplayedColumns, m.shellName, defaultFilter, query, getNumEntriesNeeded(m.ctx))
return asyncQueryFinishedMsg{queryId, rows, entries, searchErr, forceUpdateTable, maintainCursor, nil, false} return asyncQueryFinishedMsg{queryId, rows, entries, searchErr, forceUpdateTable, maintainCursor, nil, false}
} }
} }
@ -467,7 +462,7 @@ func getBaseStyle(config hctx.ClientConfig) lipgloss.Style {
func renderNullableTable(m model, helpText string) string { func renderNullableTable(m model, helpText string) string {
if m.table == nil { if m.table == nil {
return strings.Repeat("\n", TABLE_HEIGHT+3) return strings.Repeat("\n", getTableHeight(m.ctx)+3)
} }
helpTextLen := strings.Count(helpText, "\n") helpTextLen := strings.Count(helpText, "\n")
baseStyle := getBaseStyle(*hctx.GetConf(m.ctx)) baseStyle := getBaseStyle(*hctx.GetConf(m.ctx))
@ -666,6 +661,25 @@ func min(a, b int) int {
return b return b
} }
func getTableHeight(ctx context.Context) int {
config := hctx.GetConf(ctx)
if config.FullScreenRendering {
_, terminalHeight, err := getTerminalSize()
if err != nil {
// A reasonable guess at a default if for some reason we fail to retrieve the terminal size
return 30
}
return max(terminalHeight-15, 20)
}
// Default to 20 when not full-screen since we want to balance showing a large table with not using the entire screen
return 20
}
func getNumEntriesNeeded(ctx context.Context) int {
// Get more than table height since the TUI filters some out (e.g. duplicate entries)
return getTableHeight(ctx) * 5
}
func makeTable(ctx context.Context, shellName string, rows []table.Row) (table.Model, error) { func makeTable(ctx context.Context, shellName string, rows []table.Row) (table.Model, error) {
config := hctx.GetConf(ctx) config := hctx.GetConf(ctx)
columns, err := makeTableColumns(ctx, shellName, config.DisplayedColumns, rows) columns, err := makeTableColumns(ctx, shellName, config.DisplayedColumns, rows)
@ -699,7 +713,7 @@ func makeTable(ctx context.Context, shellName string, rows []table.Row) (table.M
if isExtraCompactHeightMode(ctx) { if isExtraCompactHeightMode(ctx) {
tuiSize -= 3 tuiSize -= 3
} }
tableHeight := min(TABLE_HEIGHT, terminalHeight-tuiSize) tableHeight := min(getTableHeight(ctx), terminalHeight-tuiSize)
t := table.New( t := table.New(
table.WithColumns(columns), table.WithColumns(columns),
table.WithRows(rows), table.WithRows(rows),
@ -918,12 +932,16 @@ func TuiQuery(ctx context.Context, shellName string, initialQueryArray []string)
} }
loadedKeyBindings = hctx.GetConf(ctx).KeyBindings.ToKeyMap() loadedKeyBindings = hctx.GetConf(ctx).KeyBindings.ToKeyMap()
configureColorProfile(ctx) configureColorProfile(ctx)
p := tea.NewProgram(initialModel(ctx, shellName, initialQueryWithEscaping), tea.WithOutput(os.Stderr)) additionalOptions := []tea.ProgramOption{tea.WithOutput(os.Stderr)}
if hctx.GetConf(ctx).FullScreenRendering {
additionalOptions = append(additionalOptions, tea.WithAltScreen())
}
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() {
queryId := allocateQueryId() queryId := allocateQueryId()
conf := hctx.GetConf(ctx) conf := hctx.GetConf(ctx)
rows, entries, err := getRows(ctx, conf.DisplayedColumns, shellName, conf.DefaultFilter, initialQueryWithEscaping, PADDED_NUM_ENTRIES) rows, entries, err := getRows(ctx, conf.DisplayedColumns, shellName, conf.DefaultFilter, initialQueryWithEscaping, getNumEntriesNeeded(ctx))
if err == nil || initialQueryWithEscaping == "" { if err == nil || initialQueryWithEscaping == "" {
if err != nil { if err != nil {
panic(err) panic(err)
@ -932,7 +950,7 @@ func TuiQuery(ctx context.Context, shellName string, initialQueryArray []string)
} else { } else {
// The initial query 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, PADDED_NUM_ENTRIES) rows, entries, err := getRows(ctx, hctx.GetConf(ctx).DisplayedColumns, shellName, conf.DefaultFilter, emptyQuery, getNumEntriesNeeded(ctx))
if err != nil { if err != nil {
panic(err) panic(err)
} }