mirror of
https://github.com/ddworken/hishtory.git
synced 2025-01-12 17:28:14 +01:00
Merge pull request #72 from ddworken/hscroll
Implement horizontal scrolling
This commit is contained in:
commit
031bb5446c
10
.github/workflows/go-test.yml
vendored
10
.github/workflows/go-test.yml
vendored
@ -32,8 +32,8 @@ jobs:
|
||||
sudo hostname ghaction-runner-hostname || true # Set a consistent hostname so we can run tests that depend on it
|
||||
sudo scutil --set HostName ghaction-runner-hostname || true
|
||||
make test
|
||||
# - name: Setup tmate session
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# with:
|
||||
# limit-access-to-actor: true
|
||||
- name: Setup tmate session
|
||||
if: ${{ failure() }}
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
with:
|
||||
limit-access-to-actor: true
|
@ -493,6 +493,11 @@ func OpenDB() (*gorm.DB, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to the DB: %v", err)
|
||||
}
|
||||
underlyingDb, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to access underlying DB: %v", err)
|
||||
}
|
||||
underlyingDb.SetMaxOpenConns(1)
|
||||
db.Exec("PRAGMA journal_mode = WAL")
|
||||
AddDatabaseTables(db)
|
||||
return db, nil
|
||||
|
@ -30,8 +30,6 @@ func skipSlowTests() bool {
|
||||
return os.Getenv("FAST") != ""
|
||||
}
|
||||
|
||||
var initialWd string
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
defer testutils.BackupAndRestoreEnv("HISHTORY_TEST")()
|
||||
os.Setenv("HISHTORY_TEST", "1")
|
||||
@ -45,11 +43,6 @@ func TestMain(m *testing.M) {
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to build client: %v", err))
|
||||
}
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to os.Getwd(): %v", err))
|
||||
}
|
||||
initialWd = cwd
|
||||
m.Run()
|
||||
}
|
||||
|
||||
@ -296,12 +289,12 @@ yes | hishtory init `+userSecret)
|
||||
|
||||
// And test the export for each shell without anything filtered out
|
||||
out = tester.RunInteractiveShell(t, `hishtory export -pipefail | grep -v 'hishtory init '`)
|
||||
compareGoldens(t, out, "testIntegrationWithNewDevice-"+tester.ShellName())
|
||||
testutils.CompareGoldens(t, out, "testIntegrationWithNewDevice-"+tester.ShellName())
|
||||
|
||||
// And test the table but with a subset of columns that is static
|
||||
tester.RunInteractiveShell(t, `hishtory config-set displayed-columns Hostname 'Exit Code' Command`)
|
||||
out = tester.RunInteractiveShell(t, `hishtory query -pipefail | grep -v 'hishtory init ' | grep -v 'ls /'`)
|
||||
compareGoldens(t, out, "testIntegrationWithNewDevice-table"+tester.ShellName())
|
||||
testutils.CompareGoldens(t, out, "testIntegrationWithNewDevice-table"+tester.ShellName())
|
||||
|
||||
// Assert there are no leaked connections
|
||||
assertNoLeakedConnections(t)
|
||||
@ -1062,30 +1055,30 @@ func testDisplayTable(t *testing.T, tester shellTester) {
|
||||
// Query and check the table
|
||||
tester.RunInteractiveShell(t, ` hishtory disable`)
|
||||
out := hishtoryQuery(t, tester, "table")
|
||||
compareGoldens(t, out, "testDisplayTable-defaultColumns")
|
||||
testutils.CompareGoldens(t, out, "testDisplayTable-defaultColumns")
|
||||
|
||||
// Adjust the columns that should be displayed
|
||||
tester.RunInteractiveShell(t, `hishtory config-set displayed-columns Hostname Command`)
|
||||
|
||||
// And check the table again
|
||||
out = hishtoryQuery(t, tester, "table")
|
||||
compareGoldens(t, out, "testDisplayTable-customColumns")
|
||||
testutils.CompareGoldens(t, out, "testDisplayTable-customColumns")
|
||||
|
||||
// And again
|
||||
tester.RunInteractiveShell(t, `hishtory config-set displayed-columns Hostname 'Exit Code' Command`)
|
||||
out = hishtoryQuery(t, tester, "table")
|
||||
compareGoldens(t, out, "testDisplayTable-customColumns-2")
|
||||
testutils.CompareGoldens(t, out, "testDisplayTable-customColumns-2")
|
||||
|
||||
// And again
|
||||
tester.RunInteractiveShell(t, `hishtory config-add displayed-columns CWD`)
|
||||
out = hishtoryQuery(t, tester, "table")
|
||||
compareGoldens(t, out, "testDisplayTable-customColumns-3")
|
||||
testutils.CompareGoldens(t, out, "testDisplayTable-customColumns-3")
|
||||
|
||||
// Test displaying a command with multiple lines
|
||||
entry3 := testutils.MakeFakeHistoryEntry("while :\ndo\nls /table/\ndone")
|
||||
manuallySubmitHistoryEntry(t, userSecret, entry3)
|
||||
out = hishtoryQuery(t, tester, "table")
|
||||
compareGoldens(t, out, "testDisplayTable-customColumns-multiLineCommand")
|
||||
testutils.CompareGoldens(t, out, "testDisplayTable-customColumns-multiLineCommand")
|
||||
|
||||
// Add a custom column
|
||||
tester.RunInteractiveShell(t, `hishtory config-add custom-columns foo "echo aaaaaaaaaaaaa"`)
|
||||
@ -1099,7 +1092,7 @@ func testDisplayTable(t *testing.T, tester shellTester) {
|
||||
|
||||
// And run a query and confirm it is displayed
|
||||
out = hishtoryQuery(t, tester, "table")
|
||||
compareGoldens(t, out, "testDisplayTable-customColumns-trulyCustom")
|
||||
testutils.CompareGoldens(t, out, "testDisplayTable-customColumns-trulyCustom")
|
||||
}
|
||||
|
||||
func testRequestAndReceiveDbDump(t *testing.T, tester shellTester) {
|
||||
@ -1779,36 +1772,7 @@ func TestFish(t *testing.T) {
|
||||
// Check a table to see some other metadata
|
||||
tester.RunInteractiveShell(t, `hishtory config-set displayed-columns CWD Hostname 'Exit Code' Command`)
|
||||
out = hishtoryQuery(t, tester, "-pipefail")
|
||||
compareGoldens(t, out, "TestFish-table")
|
||||
}
|
||||
|
||||
func normalizeHostnames(data string) string {
|
||||
hostnames := []string{"Davids-MacBook-Air.local", "ghaction-runner-hostname"}
|
||||
for _, hostname := range hostnames {
|
||||
data = strings.ReplaceAll(data, hostname, "ghaction-runner-hostname")
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func compareGoldens(t *testing.T, out, goldenName string) {
|
||||
out = normalizeHostnames(out)
|
||||
goldenPath := path.Join(initialWd, "client/lib/goldens/", goldenName)
|
||||
expected, err := os.ReadFile(goldenPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
expected = []byte("ERR_FILE_NOT_FOUND")
|
||||
} else {
|
||||
testutils.Check(t, err)
|
||||
}
|
||||
}
|
||||
if diff := cmp.Diff(string(expected), out); diff != "" {
|
||||
if os.Getenv("HISHTORY_UPDATE_GOLDENS") == "" {
|
||||
_, filename, line, _ := runtime.Caller(1)
|
||||
t.Fatalf("hishtory golden mismatch for %s at %s:%d (-expected +got):\n%s\nactual=\n%s", goldenName, filename, line, diff, out)
|
||||
} else {
|
||||
testutils.Check(t, os.WriteFile(goldenPath, []byte(out), 0644))
|
||||
}
|
||||
}
|
||||
testutils.CompareGoldens(t, out, "TestFish-table")
|
||||
}
|
||||
|
||||
func TestTui(t *testing.T) {
|
||||
@ -1831,7 +1795,7 @@ func TestTui(t *testing.T) {
|
||||
t.Fatalf("failed to split out=%#v", out)
|
||||
}
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
compareGoldens(t, out, "TestTui-Initial")
|
||||
testutils.CompareGoldens(t, out, "TestTui-Initial")
|
||||
|
||||
// Check the output when there is a search
|
||||
out = captureTerminalOutput(t, tester, []string{
|
||||
@ -1839,7 +1803,7 @@ func TestTui(t *testing.T) {
|
||||
"ls",
|
||||
})
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
compareGoldens(t, out, "TestTui-Search")
|
||||
testutils.CompareGoldens(t, out, "TestTui-Search")
|
||||
|
||||
// Check the output when there is a selected result
|
||||
out = captureTerminalOutput(t, tester, []string{
|
||||
@ -1858,7 +1822,7 @@ func TestTui(t *testing.T) {
|
||||
"ls",
|
||||
})
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
compareGoldens(t, out, "TestTui-InitialInvalidSearch")
|
||||
testutils.CompareGoldens(t, out, "TestTui-InitialInvalidSearch")
|
||||
|
||||
// Check the output when the initial search is invalid
|
||||
out = captureTerminalOutput(t, tester, []string{
|
||||
@ -1866,14 +1830,14 @@ func TestTui(t *testing.T) {
|
||||
"ls:",
|
||||
})
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
compareGoldens(t, out, "TestTui-InvalidSearch")
|
||||
testutils.CompareGoldens(t, out, "TestTui-InvalidSearch")
|
||||
|
||||
// Check the output when the size is smaller
|
||||
out = captureTerminalOutputWithShellNameAndDimensions(t, tester, tester.ShellName(), 100, 20, []TmuxCommand{
|
||||
{Keys: "hishtory SPACE tquery ENTER"},
|
||||
})
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
compareGoldens(t, out, "TestTui-SmallTerminal")
|
||||
testutils.CompareGoldens(t, out, "TestTui-SmallTerminal")
|
||||
|
||||
// Check that we can use left arrow keys to scroll
|
||||
out = captureTerminalOutput(t, tester, []string{
|
||||
@ -1883,7 +1847,7 @@ func TestTui(t *testing.T) {
|
||||
"l",
|
||||
})
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
compareGoldens(t, out, "TestTui-LeftScroll")
|
||||
testutils.CompareGoldens(t, out, "TestTui-LeftScroll")
|
||||
|
||||
// Check that we can exit the TUI via pressing esc
|
||||
out = captureTerminalOutput(t, tester, []string{
|
||||
@ -1894,7 +1858,7 @@ func TestTui(t *testing.T) {
|
||||
t.Fatalf("unexpected out=\n%s", out)
|
||||
}
|
||||
if !testutils.IsGithubAction() {
|
||||
compareGoldens(t, out, "TestTui-Exit")
|
||||
testutils.CompareGoldens(t, out, "TestTui-Exit")
|
||||
}
|
||||
|
||||
// Check that it resizes after the terminal size is adjusted
|
||||
@ -1904,7 +1868,7 @@ func TestTui(t *testing.T) {
|
||||
{ResizeX: 300, ResizeY: 100},
|
||||
})
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
compareGoldens(t, out, "TestTui-Resize")
|
||||
testutils.CompareGoldens(t, out, "TestTui-Resize")
|
||||
|
||||
// Check that we can delete an entry
|
||||
out = captureTerminalOutput(t, tester, []string{
|
||||
@ -1913,14 +1877,14 @@ func TestTui(t *testing.T) {
|
||||
"C-K",
|
||||
})
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
compareGoldens(t, out, "TestTui-Delete")
|
||||
testutils.CompareGoldens(t, out, "TestTui-Delete")
|
||||
|
||||
// And that it stays deleted
|
||||
out = captureTerminalOutput(t, tester, []string{
|
||||
"hishtory SPACE tquery ENTER",
|
||||
})
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
compareGoldens(t, out, "TestTui-DeleteStill")
|
||||
testutils.CompareGoldens(t, out, "TestTui-DeleteStill")
|
||||
|
||||
// And that we can then delete another entry
|
||||
out = captureTerminalOutput(t, tester, []string{
|
||||
@ -1928,14 +1892,31 @@ func TestTui(t *testing.T) {
|
||||
"C-K",
|
||||
})
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
compareGoldens(t, out, "TestTui-DeleteAgain")
|
||||
testutils.CompareGoldens(t, out, "TestTui-DeleteAgain")
|
||||
|
||||
// And that it stays deleted
|
||||
out = captureTerminalOutput(t, tester, []string{
|
||||
"hishtory SPACE tquery ENTER",
|
||||
})
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
compareGoldens(t, out, "TestTui-DeleteAgainStill")
|
||||
testutils.CompareGoldens(t, out, "TestTui-DeleteAgainStill")
|
||||
|
||||
// Test horizontal scrolling by one to the right
|
||||
testutils.Check(t, db.Create(testutils.MakeFakeHistoryEntry("echo '1234567890qwertyuiopasdfghjklzxxcvbnm0987654321_0_1234567890qwertyuiopasdfghjklzxxcvbnm0987654321_1_1234567890qwertyuiopasdfghjklzxxcvbnm0987654321_2_1234567890qwertyuiopasdfghjklzxxcvbnm0987654321'")).Error)
|
||||
out = captureTerminalOutput(t, tester, []string{
|
||||
"hishtory SPACE tquery ENTER",
|
||||
"S-Left S-Right S-Right S-Left",
|
||||
})
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
testutils.CompareGoldens(t, out, "TestTui-RightScroll")
|
||||
|
||||
// Test horizontal scrolling by two
|
||||
out = captureTerminalOutput(t, tester, []string{
|
||||
"hishtory SPACE tquery ENTER",
|
||||
"S-Right S-Right",
|
||||
})
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
testutils.CompareGoldens(t, out, "TestTui-RightScrollTwo")
|
||||
|
||||
// Assert there are no leaked connections
|
||||
assertNoLeakedConnections(t)
|
||||
@ -2035,7 +2016,7 @@ func testControlR(t *testing.T, tester shellTester, shellName string, onlineStat
|
||||
t.Fatalf("failed to find separator in %#v", out)
|
||||
}
|
||||
out = strings.TrimSpace(strings.Split(out, "\n\n\n")[1])
|
||||
compareGoldens(t, out, "testControlR-Initial")
|
||||
testutils.CompareGoldens(t, out, "testControlR-Initial")
|
||||
|
||||
// And check that we can scroll down and select an option
|
||||
out = captureTerminalOutputWithShellName(t, tester, shellName, []string{"C-R", "Down Down", "Enter"})
|
||||
@ -2072,14 +2053,14 @@ func testControlR(t *testing.T, tester shellTester, shellName string, onlineStat
|
||||
if strings.Contains(out, "\n\n\n") {
|
||||
out = strings.TrimSpace(strings.Split(out, "\n\n\n")[1])
|
||||
}
|
||||
compareGoldens(t, out, "testControlR-Search")
|
||||
testutils.CompareGoldens(t, out, "testControlR-Search")
|
||||
|
||||
// An advanced search and check that the table is updated
|
||||
out = captureTerminalOutputWithShellName(t, tester, shellName, []string{"C-R", "cwd:/tmp/ SPACE ls"})
|
||||
if strings.Contains(out, "\n\n\n") {
|
||||
out = strings.TrimSpace(strings.Split(out, "\n\n\n")[1])
|
||||
}
|
||||
compareGoldens(t, out, "testControlR-AdvancedSearch")
|
||||
testutils.CompareGoldens(t, out, "testControlR-AdvancedSearch")
|
||||
|
||||
// Set some different columns to be displayed and check that the table displays those
|
||||
tester.RunInteractiveShell(t, `hishtory config-set displayed-columns Hostname 'Exit Code' Command`)
|
||||
@ -2087,7 +2068,7 @@ func testControlR(t *testing.T, tester shellTester, shellName string, onlineStat
|
||||
if strings.Contains(out, "\n\n\n") {
|
||||
out = strings.TrimSpace(strings.Split(out, "\n\n\n")[1])
|
||||
}
|
||||
compareGoldens(t, out, "testControlR-displayedColumns")
|
||||
testutils.CompareGoldens(t, out, "testControlR-displayedColumns")
|
||||
|
||||
// Add a custom column
|
||||
tester.RunInteractiveShell(t, `hishtory config-add custom-columns foo "echo foo"`)
|
||||
@ -2102,35 +2083,35 @@ func testControlR(t *testing.T, tester shellTester, shellName string, onlineStat
|
||||
if tester.ShellName() == "bash" {
|
||||
out = strings.TrimSpace(strings.Split(out, "\n\n\n")[1])
|
||||
}
|
||||
compareGoldens(t, out, "testControlR-customColumn")
|
||||
testutils.CompareGoldens(t, out, "testControlR-customColumn")
|
||||
|
||||
// Start with a search query, and then press control-r and it shows results for that query
|
||||
out = captureTerminalOutputWithShellName(t, tester, shellName, []string{"ls", "C-R"})
|
||||
if strings.Contains(out, "\n\n\n") {
|
||||
out = strings.TrimSpace(strings.Split(out, "\n\n\n")[1])
|
||||
}
|
||||
compareGoldens(t, out, "testControlR-InitialSearch")
|
||||
testutils.CompareGoldens(t, out, "testControlR-InitialSearch")
|
||||
|
||||
// Start with a search query, and then press control-r, then make the query more specific
|
||||
out = captureTerminalOutputWithShellName(t, tester, shellName, []string{"e", "C-R", "cho"})
|
||||
if strings.Contains(out, "\n\n\n") {
|
||||
out = strings.TrimSpace(strings.Split(out, "\n\n\n")[1])
|
||||
}
|
||||
compareGoldens(t, out, "testControlR-InitialSearchExpanded")
|
||||
testutils.CompareGoldens(t, out, "testControlR-InitialSearchExpanded")
|
||||
|
||||
// Start with a search query for which there are no results
|
||||
out = captureTerminalOutputWithShellName(t, tester, shellName, []string{"asdf", "C-R"})
|
||||
if strings.Contains(out, "\n\n\n") {
|
||||
out = strings.TrimSpace(strings.Split(out, "\n\n\n")[1])
|
||||
}
|
||||
compareGoldens(t, out, "testControlR-InitialSearchNoResults")
|
||||
testutils.CompareGoldens(t, out, "testControlR-InitialSearchNoResults")
|
||||
|
||||
// Start with a search query for which there are no results
|
||||
out = captureTerminalOutputWithShellName(t, tester, shellName, []string{"asdf", "C-R", "BSpace BSpace BSpace BSpace echo"})
|
||||
if strings.Contains(out, "\n\n\n") {
|
||||
out = strings.TrimSpace(strings.Split(out, "\n\n\n")[1])
|
||||
}
|
||||
compareGoldens(t, out, "testControlR-InitialSearchNoResultsThenFoundResults")
|
||||
testutils.CompareGoldens(t, out, "testControlR-InitialSearchNoResultsThenFoundResults")
|
||||
|
||||
// Search, hit control-c, and the table should be cleared
|
||||
out = captureTerminalOutputWithShellName(t, tester, shellName, []string{"echo", "C-R", "c", "C-C"})
|
||||
@ -2142,7 +2123,7 @@ func testControlR(t *testing.T, tester shellTester, shellName string, onlineStat
|
||||
}
|
||||
if !testutils.IsGithubAction() {
|
||||
// This bit is broken on actions since actions run as a different user
|
||||
compareGoldens(t, out, "testControlR-ControlC-"+shellName)
|
||||
testutils.CompareGoldens(t, out, "testControlR-ControlC-"+shellName)
|
||||
}
|
||||
|
||||
// Disable control-r
|
||||
@ -2154,7 +2135,7 @@ func testControlR(t *testing.T, tester shellTester, shellName string, onlineStat
|
||||
}
|
||||
if !testutils.IsGithubAction() {
|
||||
// This bit is broken on actions since actions run as a different user
|
||||
compareGoldens(t, out, "testControlR-"+shellName+"-Disabled")
|
||||
testutils.CompareGoldens(t, out, "testControlR-"+shellName+"-Disabled")
|
||||
}
|
||||
|
||||
// Re-enable control-r
|
||||
@ -2166,7 +2147,7 @@ func testControlR(t *testing.T, tester shellTester, shellName string, onlineStat
|
||||
if strings.Contains(out, "\n\n\n") {
|
||||
out = strings.TrimSpace(strings.Split(out, "\n\n\n")[1])
|
||||
}
|
||||
compareGoldens(t, out, "testControlR-Final")
|
||||
testutils.CompareGoldens(t, out, "testControlR-Final")
|
||||
|
||||
// Record a multi-line command
|
||||
tester.RunInteractiveShell(t, ` hishtory enable`)
|
||||
@ -2180,7 +2161,7 @@ func testControlR(t *testing.T, tester shellTester, shellName string, onlineStat
|
||||
if strings.Contains(out, "\n\n\n") {
|
||||
out = strings.TrimSpace(strings.Split(out, "\n\n\n")[1])
|
||||
}
|
||||
compareGoldens(t, out, "testControlR-DisplayMultiline-"+shellName)
|
||||
testutils.CompareGoldens(t, out, "testControlR-DisplayMultiline-"+shellName)
|
||||
|
||||
// Check that we can select it correctly
|
||||
out = captureTerminalOutputWithShellName(t, tester, shellName, []string{"C-R", "Slah", "Enter"})
|
||||
@ -2191,7 +2172,7 @@ func testControlR(t *testing.T, tester shellTester, shellName string, onlineStat
|
||||
t.Fatalf("out has unexpected output missing the selected row: \n%s", out)
|
||||
}
|
||||
if !testutils.IsGithubAction() {
|
||||
compareGoldens(t, out, "testControlR-SelectMultiline-"+shellName)
|
||||
testutils.CompareGoldens(t, out, "testControlR-SelectMultiline-"+shellName)
|
||||
}
|
||||
|
||||
// Assert there are no leaked connections
|
||||
@ -2214,7 +2195,7 @@ echo baz`)
|
||||
|
||||
// Check that the hishtory is saved correctly
|
||||
out = tester.RunInteractiveShell(t, `hishtory export | grep -v pipefail`)
|
||||
compareGoldens(t, out, "testCustomColumns-initHistory")
|
||||
testutils.CompareGoldens(t, out, "testCustomColumns-initHistory")
|
||||
|
||||
// Configure a custom column
|
||||
tester.RunInteractiveShell(t, `hishtory config-add custom-columns git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || true'`)
|
||||
@ -2230,14 +2211,14 @@ echo bar`)
|
||||
// And check that it is all recorded correctly
|
||||
tester.RunInteractiveShell(t, `hishtory config-set displayed-columns 'Exit Code' git_remote Command `)
|
||||
out = tester.RunInteractiveShell(t, `hishtory query -pipefail`)
|
||||
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"})
|
||||
testName := "testCustomColumns-tquery-" + tester.ShellName()
|
||||
if testutils.IsGithubAction() {
|
||||
testName += "-isAction"
|
||||
testName += "-" + runtime.GOOS
|
||||
}
|
||||
compareGoldens(t, out, testName)
|
||||
testutils.CompareGoldens(t, out, testName)
|
||||
}
|
||||
|
||||
func testUninstall(t *testing.T, tester shellTester) {
|
||||
@ -2249,19 +2230,19 @@ func testUninstall(t *testing.T, tester shellTester) {
|
||||
tester.RunInteractiveShell(t, `echo foo
|
||||
echo baz`)
|
||||
out := tester.RunInteractiveShell(t, `hishtory export -pipefail`)
|
||||
compareGoldens(t, out, "testUninstall-recorded")
|
||||
testutils.CompareGoldens(t, out, "testUninstall-recorded")
|
||||
|
||||
// And then uninstall
|
||||
out, err := tester.RunInteractiveShellRelaxed(t, `yes | hishtory uninstall`)
|
||||
testutils.Check(t, err)
|
||||
compareGoldens(t, out, "testUninstall-uninstall")
|
||||
testutils.CompareGoldens(t, out, "testUninstall-uninstall")
|
||||
|
||||
// And check that hishtory has been uninstalled
|
||||
out, err = tester.RunInteractiveShellRelaxed(t, `echo foo
|
||||
hishtory
|
||||
echo bar`)
|
||||
testutils.Check(t, err)
|
||||
compareGoldens(t, out, "testUninstall-post-uninstall")
|
||||
testutils.CompareGoldens(t, out, "testUninstall-post-uninstall")
|
||||
|
||||
// And check again, but in a way that shows the full terminal output
|
||||
if !testutils.IsGithubAction() {
|
||||
@ -2270,7 +2251,7 @@ echo bar`)
|
||||
"hishtory ENTER",
|
||||
"echo SPACE bar ENTER",
|
||||
})
|
||||
compareGoldens(t, out, "testUninstall-post-uninstall-"+tester.ShellName())
|
||||
testutils.CompareGoldens(t, out, "testUninstall-post-uninstall-"+tester.ShellName())
|
||||
}
|
||||
}
|
||||
|
||||
@ -2304,14 +2285,14 @@ func TestTimestampFormat(t *testing.T) {
|
||||
|
||||
// And check that it is displayed in both the tui and the classic view
|
||||
out := hishtoryQuery(t, tester, "-pipefail -tablesizing")
|
||||
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 = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
goldenName := "TestTimestampFormat-tquery"
|
||||
if testutils.IsGithubAction() {
|
||||
goldenName += "-isAction"
|
||||
}
|
||||
compareGoldens(t, out, goldenName)
|
||||
testutils.CompareGoldens(t, out, goldenName)
|
||||
}
|
||||
|
||||
func TestZDotDir(t *testing.T) {
|
||||
@ -2363,23 +2344,23 @@ echo baz
|
||||
echo baz
|
||||
echo foo`)
|
||||
out := tester.RunInteractiveShell(t, `hishtory export -pipefail`)
|
||||
compareGoldens(t, out, "testRemoveDuplicateRows-export")
|
||||
testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-export")
|
||||
tester.RunInteractiveShell(t, `hishtory config-set displayed-columns 'Exit Code' Command`)
|
||||
out = tester.RunInteractiveShell(t, `hishtory query -pipefail`)
|
||||
compareGoldens(t, out, "testRemoveDuplicateRows-query")
|
||||
testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-query")
|
||||
out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery SPACE -pipefail ENTER"})
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
compareGoldens(t, out, "testRemoveDuplicateRows-tquery")
|
||||
testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-tquery")
|
||||
|
||||
// And change the config to filter out duplicate rows
|
||||
tester.RunInteractiveShell(t, `hishtory config-set filter-duplicate-commands true`)
|
||||
out = tester.RunInteractiveShell(t, `hishtory export -pipefail`)
|
||||
compareGoldens(t, out, "testRemoveDuplicateRows-enabled-export")
|
||||
testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-enabled-export")
|
||||
out = tester.RunInteractiveShell(t, `hishtory query -pipefail`)
|
||||
compareGoldens(t, out, "testRemoveDuplicateRows-enabled-query")
|
||||
testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-enabled-query")
|
||||
out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery SPACE -pipefail ENTER"})
|
||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||
compareGoldens(t, out, "testRemoveDuplicateRows-enabled-tquery")
|
||||
testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-enabled-tquery")
|
||||
}
|
||||
|
||||
func TestSetConfigNoCorruption(t *testing.T) {
|
||||
|
26
client/lib/goldens/TestTui-RightScroll
Normal file
26
client/lib/goldens/TestTui-RightScroll
Normal file
@ -0,0 +1,26 @@
|
||||
Search Query: > ls
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Hostname CWD Timestamp Runtime Exit Code Command │
|
||||
│────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||
│ localhost /tmp/ Oct 17 2022 21:43:31 PDT 3s 2 …567890qwertyuiopasdfghjklzxxcvbnm0987654321_0_1234567890qwertyuiopasdfghjklzxxcvbnm0987654321_1_1234567890qwertyuiopasdfghjk… │
|
||||
│ localhost /tmp/ Oct 17 2022 21:43:16 PDT 3s 2 … │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
26
client/lib/goldens/TestTui-RightScrollTwo
Normal file
26
client/lib/goldens/TestTui-RightScrollTwo
Normal file
@ -0,0 +1,26 @@
|
||||
Search Query: > ls
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Hostname CWD Timestamp Runtime Exit Code Command │
|
||||
│────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||
│ localhost /tmp/ Oct 17 2022 21:43:31 PDT 3s 2 …tyuiopasdfghjklzxxcvbnm0987654321_0_1234567890qwertyuiopasdfghjklzxxcvbnm0987654321_1_1234567890qwertyuiopasdfghjklzxxcvbnm0… │
|
||||
│ localhost /tmp/ Oct 17 2022 21:43:16 PDT 3s 2 … │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
21
client/lib/goldens/unittestTable-truncatedTable
Normal file
21
client/lib/goldens/unittestTable-truncatedTable
Normal file
@ -0,0 +1,21 @@
|
||||
[1mColumn1 [0m [1mColumn2 [0m
|
||||
[1;m a1 a2345 [0m
|
||||
b1 b23
|
||||
c1 c1234567890abcdefgh…
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
21
client/lib/goldens/unittestTable-truncatedTable-right1
Normal file
21
client/lib/goldens/unittestTable-truncatedTable-right1
Normal file
@ -0,0 +1,21 @@
|
||||
[1mColumn1 [0m [1mColumn2 [0m
|
||||
[1;m a1 …2345 [0m
|
||||
b1 …23
|
||||
c1 …1234567890abcdefgh…
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
21
client/lib/goldens/unittestTable-truncatedTable-right2
Normal file
21
client/lib/goldens/unittestTable-truncatedTable-right2
Normal file
@ -0,0 +1,21 @@
|
||||
[1mColumn1 [0m [1mColumn2 [0m
|
||||
[1;m a1 …345 [0m
|
||||
b1 …3
|
||||
c1 …234567890abcdefghi…
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
21
client/lib/goldens/unittestTable-truncatedTable-right3
Normal file
21
client/lib/goldens/unittestTable-truncatedTable-right3
Normal file
@ -0,0 +1,21 @@
|
||||
[1mColumn1 [0m [1mColumn2 [0m
|
||||
[1;m a1 …45 [0m
|
||||
b1 …
|
||||
c1 …34567890abcdefghij…
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
"github.com/ddworken/hishtory/client/table"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
@ -372,6 +372,14 @@ func makeTable(ctx *context.Context, rows []table.Row) (table.Model, error) {
|
||||
key.WithKeys("end"),
|
||||
key.WithHelp("end", "go to end"),
|
||||
),
|
||||
MoveLeft: key.NewBinding(
|
||||
key.WithKeys("shift+left"),
|
||||
key.WithHelp("Shift+←", "move left"),
|
||||
),
|
||||
MoveRight: key.NewBinding(
|
||||
key.WithKeys("shift+right"),
|
||||
key.WithHelp("Shift+→", "move right"),
|
||||
),
|
||||
}
|
||||
_, terminalHeight, err := getTerminalSize()
|
||||
if err != nil {
|
||||
|
487
client/table/table.go
Normal file
487
client/table/table.go
Normal file
@ -0,0 +1,487 @@
|
||||
// Forked from https://github.com/charmbracelet/bubbles/blob/master/table/table.go to add horizontal scrolling
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
// Model defines a state for the table widget.
|
||||
type Model struct {
|
||||
KeyMap KeyMap
|
||||
|
||||
cols []Column
|
||||
rows []Row
|
||||
cursor int
|
||||
focus bool
|
||||
styles Styles
|
||||
|
||||
viewport viewport.Model
|
||||
start int
|
||||
end int
|
||||
|
||||
hcol int
|
||||
hstep int
|
||||
hcursor int
|
||||
}
|
||||
|
||||
// Row represents one line in the table.
|
||||
type Row []string
|
||||
|
||||
// Column defines the table structure.
|
||||
type Column struct {
|
||||
Title string
|
||||
Width int
|
||||
}
|
||||
|
||||
// KeyMap defines keybindings. It satisfies to the help.KeyMap interface, which
|
||||
// is used to render the menu menu.
|
||||
type KeyMap struct {
|
||||
LineUp key.Binding
|
||||
LineDown key.Binding
|
||||
PageUp key.Binding
|
||||
PageDown key.Binding
|
||||
HalfPageUp key.Binding
|
||||
HalfPageDown key.Binding
|
||||
GotoTop key.Binding
|
||||
GotoBottom key.Binding
|
||||
MoveLeft key.Binding
|
||||
MoveRight key.Binding
|
||||
}
|
||||
|
||||
// DefaultKeyMap returns a default set of keybindings.
|
||||
func DefaultKeyMap() KeyMap {
|
||||
const spacebar = " "
|
||||
return KeyMap{
|
||||
LineUp: key.NewBinding(
|
||||
key.WithKeys("up", "k"),
|
||||
key.WithHelp("↑/k", "up"),
|
||||
),
|
||||
LineDown: key.NewBinding(
|
||||
key.WithKeys("down", "j"),
|
||||
key.WithHelp("↓/j", "down"),
|
||||
),
|
||||
PageUp: key.NewBinding(
|
||||
key.WithKeys("b", "pgup"),
|
||||
key.WithHelp("b/pgup", "page up"),
|
||||
),
|
||||
PageDown: key.NewBinding(
|
||||
key.WithKeys("f", "pgdown", spacebar),
|
||||
key.WithHelp("f/pgdn", "page down"),
|
||||
),
|
||||
HalfPageUp: key.NewBinding(
|
||||
key.WithKeys("u", "ctrl+u"),
|
||||
key.WithHelp("u", "½ page up"),
|
||||
),
|
||||
HalfPageDown: key.NewBinding(
|
||||
key.WithKeys("d", "ctrl+d"),
|
||||
key.WithHelp("d", "½ page down"),
|
||||
),
|
||||
GotoTop: key.NewBinding(
|
||||
key.WithKeys("home", "g"),
|
||||
key.WithHelp("g/home", "go to start"),
|
||||
),
|
||||
GotoBottom: key.NewBinding(
|
||||
key.WithKeys("end", "G"),
|
||||
key.WithHelp("G/end", "go to end"),
|
||||
),
|
||||
MoveLeft: key.NewBinding(
|
||||
key.WithKeys("shift+left"),
|
||||
key.WithHelp("Shift+←", "move left"),
|
||||
),
|
||||
MoveRight: key.NewBinding(
|
||||
key.WithKeys("shift+right"),
|
||||
key.WithHelp("Shift+→", "move right"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Styles contains style definitions for this list component. By default, these
|
||||
// values are generated by DefaultStyles.
|
||||
type Styles struct {
|
||||
Header lipgloss.Style
|
||||
Cell lipgloss.Style
|
||||
Selected lipgloss.Style
|
||||
}
|
||||
|
||||
// DefaultStyles returns a set of default style definitions for this table.
|
||||
func DefaultStyles() Styles {
|
||||
return Styles{
|
||||
Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")),
|
||||
Header: lipgloss.NewStyle().Bold(true).Padding(0, 1),
|
||||
Cell: lipgloss.NewStyle().Padding(0, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// SetStyles sets the table styles.
|
||||
func (m *Model) SetStyles(s Styles) {
|
||||
m.styles = s
|
||||
m.UpdateViewport()
|
||||
}
|
||||
|
||||
// Option is used to set options in New. For example:
|
||||
//
|
||||
// table := New(WithColumns([]Column{{Title: "ID", Width: 10}}))
|
||||
type Option func(*Model)
|
||||
|
||||
// New creates a new model for the table widget.
|
||||
func New(opts ...Option) Model {
|
||||
m := Model{
|
||||
cursor: 0,
|
||||
viewport: viewport.New(0, 20),
|
||||
|
||||
KeyMap: DefaultKeyMap(),
|
||||
styles: DefaultStyles(),
|
||||
|
||||
hcol: -1,
|
||||
hstep: 10,
|
||||
hcursor: 0,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&m)
|
||||
}
|
||||
|
||||
m.UpdateViewport()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// WithColumns sets the table columns (headers).
|
||||
func WithColumns(cols []Column) Option {
|
||||
return func(m *Model) {
|
||||
m.cols = cols
|
||||
}
|
||||
}
|
||||
|
||||
// WithRows sets the table rows (data).
|
||||
func WithRows(rows []Row) Option {
|
||||
return func(m *Model) {
|
||||
m.rows = rows
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeight sets the height of the table.
|
||||
func WithHeight(h int) Option {
|
||||
return func(m *Model) {
|
||||
m.viewport.Height = h
|
||||
}
|
||||
}
|
||||
|
||||
// WithWidth sets the width of the table.
|
||||
func WithWidth(w int) Option {
|
||||
return func(m *Model) {
|
||||
m.viewport.Width = w
|
||||
}
|
||||
}
|
||||
|
||||
// WithFocused sets the focus state of the table.
|
||||
func WithFocused(f bool) Option {
|
||||
return func(m *Model) {
|
||||
m.focus = f
|
||||
}
|
||||
}
|
||||
|
||||
// WithStyles sets the table styles.
|
||||
func WithStyles(s Styles) Option {
|
||||
return func(m *Model) {
|
||||
m.styles = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithKeyMap sets the key map.
|
||||
func WithKeyMap(km KeyMap) Option {
|
||||
return func(m *Model) {
|
||||
m.KeyMap = km
|
||||
}
|
||||
}
|
||||
|
||||
// Update is the Bubble Tea update loop.
|
||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
if !m.focus {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.KeyMap.LineUp):
|
||||
m.MoveUp(1)
|
||||
case key.Matches(msg, m.KeyMap.LineDown):
|
||||
m.MoveDown(1)
|
||||
case key.Matches(msg, m.KeyMap.PageUp):
|
||||
m.MoveUp(m.viewport.Height)
|
||||
case key.Matches(msg, m.KeyMap.PageDown):
|
||||
m.MoveDown(m.viewport.Height)
|
||||
case key.Matches(msg, m.KeyMap.HalfPageUp):
|
||||
m.MoveUp(m.viewport.Height / 2)
|
||||
case key.Matches(msg, m.KeyMap.HalfPageDown):
|
||||
m.MoveDown(m.viewport.Height / 2)
|
||||
case key.Matches(msg, m.KeyMap.LineDown):
|
||||
m.MoveDown(1)
|
||||
case key.Matches(msg, m.KeyMap.GotoTop):
|
||||
m.GotoTop()
|
||||
case key.Matches(msg, m.KeyMap.GotoBottom):
|
||||
m.GotoBottom()
|
||||
case key.Matches(msg, m.KeyMap.MoveLeft):
|
||||
m.MoveLeft(m.hstep)
|
||||
case key.Matches(msg, m.KeyMap.MoveRight):
|
||||
m.MoveRight(m.hstep)
|
||||
}
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
// Focused returns the focus state of the table.
|
||||
func (m Model) Focused() bool {
|
||||
return m.focus
|
||||
}
|
||||
|
||||
// Focus focusses the table, allowing the user to move around the rows and
|
||||
// interact.
|
||||
func (m *Model) Focus() {
|
||||
m.focus = true
|
||||
m.UpdateViewport()
|
||||
}
|
||||
|
||||
// Blur blurs the table, preventing selection or movement.
|
||||
func (m *Model) Blur() {
|
||||
m.focus = false
|
||||
m.UpdateViewport()
|
||||
}
|
||||
|
||||
// View renders the component.
|
||||
func (m Model) View() string {
|
||||
return m.headersView() + "\n" + m.viewport.View()
|
||||
}
|
||||
|
||||
// UpdateViewport updates the list content based on the previously defined
|
||||
// columns and rows.
|
||||
func (m *Model) UpdateViewport() {
|
||||
renderedRows := make([]string, 0, len(m.rows))
|
||||
|
||||
// Render only rows from: m.cursor-m.viewport.Height to: m.cursor+m.viewport.Height
|
||||
// Constant runtime, independent of number of rows in a table.
|
||||
// Limits the number of renderedRows to a maximum of 2*m.viewport.Height
|
||||
if m.cursor >= 0 {
|
||||
m.start = clamp(m.cursor-m.viewport.Height, 0, m.cursor)
|
||||
} else {
|
||||
m.start = 0
|
||||
}
|
||||
m.end = clamp(m.cursor+m.viewport.Height, m.cursor, len(m.rows))
|
||||
for i := m.start; i < m.end; i++ {
|
||||
renderedRows = append(renderedRows, m.renderRow(i))
|
||||
}
|
||||
|
||||
m.viewport.SetContent(
|
||||
lipgloss.JoinVertical(lipgloss.Left, renderedRows...),
|
||||
)
|
||||
}
|
||||
|
||||
// SelectedRow returns the selected row.
|
||||
// You can cast it to your own implementation.
|
||||
func (m Model) SelectedRow() Row {
|
||||
return m.rows[m.cursor]
|
||||
}
|
||||
|
||||
// Rows returns the current rows.
|
||||
func (m Model) Rows() []Row {
|
||||
return m.rows
|
||||
}
|
||||
|
||||
// SetRows set a new rows state.
|
||||
func (m *Model) SetRows(r []Row) {
|
||||
m.rows = r
|
||||
m.UpdateViewport()
|
||||
}
|
||||
|
||||
// SetColumns set a new columns state.
|
||||
func (m *Model) SetColumns(c []Column) {
|
||||
m.cols = c
|
||||
m.UpdateViewport()
|
||||
}
|
||||
|
||||
// ColIndex gets the index of a column n, where if n is positive it returns n clamped, and if n is negative it reutrns the column index counting from the right
|
||||
func (m *Model) ColIndex(n int) int {
|
||||
if n < 0 {
|
||||
return clamp(len(m.cols)-n, 0, len(m.cols)-1)
|
||||
} else {
|
||||
return clamp(n, 0, len(m.cols)-1)
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the maximum useful horizontal scroll
|
||||
func (m *Model) MaxHScroll() int {
|
||||
maxWidth := 0
|
||||
index := m.ColIndex(m.hcol)
|
||||
for _, row := range m.rows {
|
||||
if len(row) > index {
|
||||
maxWidth = max(len(row[index]), maxWidth)
|
||||
}
|
||||
}
|
||||
return max(maxWidth-m.cols[index].Width+1, 0)
|
||||
}
|
||||
|
||||
// SetWidth sets the width of the viewport of the table.
|
||||
func (m *Model) SetWidth(w int) {
|
||||
m.viewport.Width = w
|
||||
m.UpdateViewport()
|
||||
}
|
||||
|
||||
// SetHeight sets the height of the viewport of the table.
|
||||
func (m *Model) SetHeight(h int) {
|
||||
m.viewport.Height = h
|
||||
m.UpdateViewport()
|
||||
}
|
||||
|
||||
// Height returns the viewport height of the table.
|
||||
func (m Model) Height() int {
|
||||
return m.viewport.Height
|
||||
}
|
||||
|
||||
// Width returns the viewport width of the table.
|
||||
func (m Model) Width() int {
|
||||
return m.viewport.Width
|
||||
}
|
||||
|
||||
// Cursor returns the index of the selected row.
|
||||
func (m Model) Cursor() int {
|
||||
return m.cursor
|
||||
}
|
||||
|
||||
// SetCursor sets the cursor position in the table.
|
||||
func (m *Model) SetCursor(n int) {
|
||||
m.cursor = clamp(n, 0, len(m.rows)-1)
|
||||
m.UpdateViewport()
|
||||
}
|
||||
|
||||
// MoveUp moves the selection up by any number of row.
|
||||
// It can not go above the first row.
|
||||
func (m *Model) MoveUp(n int) {
|
||||
m.cursor = clamp(m.cursor-n, 0, len(m.rows)-1)
|
||||
switch {
|
||||
case m.start == 0:
|
||||
m.viewport.SetYOffset(clamp(m.viewport.YOffset, 0, m.cursor))
|
||||
case m.start < m.viewport.Height:
|
||||
m.viewport.SetYOffset(clamp(m.viewport.YOffset+n, 0, m.cursor))
|
||||
case m.viewport.YOffset >= 1:
|
||||
m.viewport.YOffset = clamp(m.viewport.YOffset+n, 1, m.viewport.Height)
|
||||
}
|
||||
m.UpdateViewport()
|
||||
}
|
||||
|
||||
// MoveDown moves the selection down by any number of row.
|
||||
// It can not go below the last row.
|
||||
func (m *Model) MoveDown(n int) {
|
||||
m.cursor = clamp(m.cursor+n, 0, len(m.rows)-1)
|
||||
m.UpdateViewport()
|
||||
|
||||
switch {
|
||||
case m.end == len(m.rows):
|
||||
m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.viewport.Height))
|
||||
case m.cursor > (m.end-m.start)/2:
|
||||
m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.cursor))
|
||||
case m.viewport.YOffset > 1:
|
||||
case m.cursor > m.viewport.YOffset+m.viewport.Height-1:
|
||||
m.viewport.SetYOffset(clamp(m.viewport.YOffset+1, 0, 1))
|
||||
}
|
||||
}
|
||||
|
||||
// GotoTop moves the selection to the first row.
|
||||
func (m *Model) GotoTop() {
|
||||
m.MoveUp(m.cursor)
|
||||
}
|
||||
|
||||
// GotoBottom moves the selection to the last row.
|
||||
func (m *Model) GotoBottom() {
|
||||
m.MoveDown(len(m.rows))
|
||||
}
|
||||
|
||||
// MoveLeft scrolls left
|
||||
func (m *Model) MoveLeft(n int) {
|
||||
m.hcursor = clamp(m.hcursor-n, 0, m.MaxHScroll())
|
||||
m.UpdateViewport()
|
||||
}
|
||||
|
||||
// MoveRight scrolls right
|
||||
func (m *Model) MoveRight(n int) {
|
||||
m.hcursor = clamp(m.hcursor+n, 0, m.MaxHScroll())
|
||||
m.UpdateViewport()
|
||||
}
|
||||
|
||||
// FromValues create the table rows from a simple string. It uses `\n` by
|
||||
// default for getting all the rows and the given separator for the fields on
|
||||
// each row.
|
||||
func (m *Model) FromValues(value, separator string) {
|
||||
rows := []Row{}
|
||||
for _, line := range strings.Split(value, "\n") {
|
||||
r := Row{}
|
||||
for _, field := range strings.Split(line, separator) {
|
||||
r = append(r, field)
|
||||
}
|
||||
rows = append(rows, r)
|
||||
}
|
||||
|
||||
m.SetRows(rows)
|
||||
}
|
||||
|
||||
func (m Model) headersView() string {
|
||||
var s = make([]string, 0, len(m.cols))
|
||||
for _, col := range m.cols {
|
||||
style := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true)
|
||||
renderedCell := style.Render(runewidth.Truncate(col.Title, col.Width, "…"))
|
||||
s = append(s, m.styles.Header.Render(renderedCell))
|
||||
}
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, s...)
|
||||
}
|
||||
|
||||
func (m *Model) renderRow(rowID int) string {
|
||||
var s = make([]string, 0, len(m.cols))
|
||||
for i, value := range m.rows[rowID] {
|
||||
style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true)
|
||||
var renderedCell string
|
||||
if i == m.ColIndex(m.hcol) && m.hcursor > 0 {
|
||||
renderedCell = m.styles.Cell.Render(style.Render(runewidth.Truncate(runewidth.TruncateLeft(value, m.hcursor, "…"), m.cols[i].Width, "…")))
|
||||
} else {
|
||||
renderedCell = m.styles.Cell.Render(style.Render(runewidth.Truncate(value, m.cols[i].Width, "…")))
|
||||
}
|
||||
s = append(s, renderedCell)
|
||||
}
|
||||
|
||||
row := lipgloss.JoinHorizontal(lipgloss.Left, s...)
|
||||
|
||||
if rowID == m.cursor {
|
||||
return m.styles.Selected.Render(row)
|
||||
}
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func clamp(v, low, high int) int {
|
||||
return min(max(v, low), high)
|
||||
}
|
80
client/table/table_test.go
Normal file
80
client/table/table_test.go
Normal file
@ -0,0 +1,80 @@
|
||||
// Forked from https://github.com/charmbracelet/bubbles/blob/master/table/table_test.go to add horizontal scrolling
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ddworken/hishtory/shared/testutils"
|
||||
)
|
||||
|
||||
func TestFromValues(t *testing.T) {
|
||||
input := "foo1,bar1\nfoo2,bar2\nfoo3,bar3"
|
||||
table := New(WithColumns([]Column{{Title: "Foo"}, {Title: "Bar"}}))
|
||||
table.FromValues(input, ",")
|
||||
|
||||
if len(table.rows) != 3 {
|
||||
t.Fatalf("expect table to have 3 rows but it has %d", len(table.rows))
|
||||
}
|
||||
|
||||
expect := []Row{
|
||||
{"foo1", "bar1"},
|
||||
{"foo2", "bar2"},
|
||||
{"foo3", "bar3"},
|
||||
}
|
||||
if !deepEqual(table.rows, expect) {
|
||||
t.Fatal("table rows is not equals to the input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromValuesWithTabSeparator(t *testing.T) {
|
||||
input := "foo1.\tbar1\nfoo,bar,baz\tbar,2"
|
||||
table := New(WithColumns([]Column{{Title: "Foo"}, {Title: "Bar"}}))
|
||||
table.FromValues(input, "\t")
|
||||
|
||||
if len(table.rows) != 2 {
|
||||
t.Fatalf("expect table to have 2 rows but it has %d", len(table.rows))
|
||||
}
|
||||
|
||||
expect := []Row{
|
||||
{"foo1.", "bar1"},
|
||||
{"foo,bar,baz", "bar,2"},
|
||||
}
|
||||
if !deepEqual(table.rows, expect) {
|
||||
t.Fatal("table rows is not equals to the input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHScoll(t *testing.T) {
|
||||
table := New(
|
||||
WithColumns([]Column{{Title: "Column1", Width: 10}, {Title: "Column2", Width: 20}}),
|
||||
WithRows([]Row{
|
||||
{"a1", "a2345"},
|
||||
{"b1", "b23"},
|
||||
{"c1", "c1234567890abcdefghijklmnopqrstuvwxyz"},
|
||||
}),
|
||||
)
|
||||
testutils.CompareGoldens(t, table.View(), "unittestTable-truncatedTable")
|
||||
table.MoveRight(1)
|
||||
testutils.CompareGoldens(t, table.View(), "unittestTable-truncatedTable-right1")
|
||||
table.MoveRight(1)
|
||||
testutils.CompareGoldens(t, table.View(), "unittestTable-truncatedTable-right2")
|
||||
table.MoveRight(1)
|
||||
testutils.CompareGoldens(t, table.View(), "unittestTable-truncatedTable-right3")
|
||||
table.MoveLeft(1)
|
||||
testutils.CompareGoldens(t, table.View(), "unittestTable-truncatedTable-right2")
|
||||
}
|
||||
|
||||
func deepEqual(a, b []Row) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, r := range a {
|
||||
for j, f := range r {
|
||||
if f != b[i][j] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
24
go.mod
24
go.mod
@ -3,20 +3,27 @@ module github.com/ddworken/hishtory
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/DataDog/datadog-go v4.8.3+incompatible
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||
github.com/charmbracelet/bubbles v0.14.1-0.20221006154229-d1775121146a
|
||||
github.com/charmbracelet/bubbletea v0.23.0
|
||||
github.com/charmbracelet/bubbles v0.15.0
|
||||
github.com/charmbracelet/bubbletea v0.23.1
|
||||
github.com/charmbracelet/lipgloss v0.6.0
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/glebarez/sqlite v1.4.7
|
||||
github.com/go-test/deep v1.0.8
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/jackc/pgx/v4 v4.14.1
|
||||
github.com/lib/pq v1.10.4
|
||||
github.com/mattn/go-runewidth v0.0.14
|
||||
github.com/muesli/termenv v0.13.0
|
||||
github.com/rodaine/table v1.0.1
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/slsa-framework/slsa-verifier v1.3.2
|
||||
github.com/spf13/cobra v1.6.1
|
||||
golang.org/x/term v0.2.0
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.43.1
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gorm.io/driver/postgres v1.3.1
|
||||
gorm.io/driver/sqlite v1.3.6
|
||||
gorm.io/gorm v1.23.8
|
||||
@ -36,15 +43,11 @@ require (
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/DataDog/datadog-agent/pkg/obfuscate v0.40.1 // indirect
|
||||
github.com/DataDog/datadog-go v4.8.3+incompatible // indirect
|
||||
github.com/DataDog/datadog-go/v5 v5.1.1 // indirect
|
||||
github.com/DataDog/gostackparse v0.5.0 // indirect
|
||||
github.com/DataDog/sketches-go v1.4.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/PaesslerAG/gval v1.0.0 // indirect
|
||||
github.com/PaesslerAG/jsonpath v0.1.1 // indirect
|
||||
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
|
||||
github.com/alecthomas/kong v0.7.1 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
|
||||
github.com/alibabacloud-go/cr-20160607 v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/cr-20181201 v1.0.10 // indirect
|
||||
@ -104,7 +107,6 @@ require (
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
|
||||
github.com/felixge/fgprof v0.9.3 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/fullstorydev/grpcurl v1.8.7 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
@ -149,7 +151,6 @@ require (
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add // indirect
|
||||
@ -161,7 +162,6 @@ require (
|
||||
github.com/jackc/pgproto3/v2 v2.2.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgtype v1.9.1 // indirect
|
||||
github.com/jackc/pgx/v4 v4.14.1 // indirect
|
||||
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect
|
||||
github.com/jhump/protoreflect v1.13.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
@ -179,7 +179,6 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
@ -200,7 +199,6 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||
github.com/philhofer/fwd v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkg/profile v1.7.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.13.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
@ -217,13 +215,11 @@ require (
|
||||
github.com/sigstore/fulcio v0.6.0 // indirect
|
||||
github.com/sigstore/rekor v1.0.0 // indirect
|
||||
github.com/sigstore/sigstore v1.4.5 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
||||
github.com/slsa-framework/slsa-github-generator v1.2.0 // indirect
|
||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||
github.com/spf13/afero v1.8.2 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cobra v1.6.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.13.0 // indirect
|
||||
@ -283,11 +279,9 @@ require (
|
||||
google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a // indirect
|
||||
google.golang.org/grpc v1.50.1 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.43.1 // indirect
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ddworken/hishtory/client/data"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -23,6 +24,28 @@ const (
|
||||
DB_SHM_PATH = data.DB_PATH + "-shm"
|
||||
)
|
||||
|
||||
var initialWd string
|
||||
|
||||
func init() {
|
||||
initialWd = getInitialWd()
|
||||
}
|
||||
|
||||
func getInitialWd() string {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if !strings.Contains(cwd, "/hishtory/") {
|
||||
return cwd
|
||||
}
|
||||
components := strings.Split(cwd, "/hishtory/")
|
||||
dir := components[0] + "/hishtory"
|
||||
if IsGithubAction() {
|
||||
dir += "/hishtory"
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func ResetLocalState(t *testing.T) {
|
||||
homedir, err := os.UserHomeDir()
|
||||
Check(t, err)
|
||||
@ -321,3 +344,32 @@ func persistLog() {
|
||||
_, err = f.WriteString("\n")
|
||||
checkError(err)
|
||||
}
|
||||
|
||||
func CompareGoldens(t *testing.T, out, goldenName string) {
|
||||
out = normalizeHostnames(out)
|
||||
goldenPath := path.Join(initialWd, "client/lib/goldens/", goldenName)
|
||||
expected, err := os.ReadFile(goldenPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
expected = []byte("ERR_FILE_NOT_FOUND:" + goldenPath)
|
||||
} else {
|
||||
Check(t, err)
|
||||
}
|
||||
}
|
||||
if diff := cmp.Diff(string(expected), out); diff != "" {
|
||||
if os.Getenv("HISHTORY_UPDATE_GOLDENS") == "" {
|
||||
_, filename, line, _ := runtime.Caller(1)
|
||||
t.Fatalf("hishtory golden mismatch for %s at %s:%d (-expected +got):\n%s\nactual=\n%s", goldenName, filename, line, diff, out)
|
||||
} else {
|
||||
Check(t, os.WriteFile(goldenPath, []byte(out), 0644))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeHostnames(data string) string {
|
||||
hostnames := []string{"Davids-MacBook-Air.local", "ghaction-runner-hostname"}
|
||||
for _, hostname := range hostnames {
|
||||
data = strings.ReplaceAll(data, hostname, "ghaction-runner-hostname")
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user