From 5e889251458e44e41870ea98cd10bc87566d28c1 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Sun, 13 Oct 2024 15:35:30 +0200 Subject: [PATCH 01/27] [deps] update tview-command to latest --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ba98d09..31b0b52 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( ) require ( - github.com/spezifisch/tview-command v0.0.0-20240828122932-69552353924a + github.com/spezifisch/tview-command v0.0.0-20241013011314-5b01dcbdc948 github.com/supersonic-app/go-mpv v0.1.0 ) diff --git a/go.sum b/go.sum index 2a1840a..7b29c74 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spezifisch/tview-command v0.0.0-20240828122932-69552353924a h1:xgFDwjebiXdBQwBd9mBvAgw6OHC/FPLwXouYrDCgDMc= -github.com/spezifisch/tview-command v0.0.0-20240828122932-69552353924a/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013011314-5b01dcbdc948 h1:0e9YqpET7Zks2lk6IigZIE+fYcjz4cCT6jThcmC84No= +github.com/spezifisch/tview-command v0.0.0-20241013011314-5b01dcbdc948/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= From 85d3e0ae23ca9adacf30c12ad8b837b638cef670 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Sun, 13 Oct 2024 15:58:34 +0200 Subject: [PATCH 02/27] [tvcom] update, builds --- go.mod | 2 +- go.sum | 2 ++ stmps.go | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 31b0b52..7bce8b2 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( ) require ( - github.com/spezifisch/tview-command v0.0.0-20241013011314-5b01dcbdc948 + github.com/spezifisch/tview-command v0.0.0-20241013135507-37aefd938546 github.com/supersonic-app/go-mpv v0.1.0 ) diff --git a/go.sum b/go.sum index 7b29c74..bafb967 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9yS github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spezifisch/tview-command v0.0.0-20241013011314-5b01dcbdc948 h1:0e9YqpET7Zks2lk6IigZIE+fYcjz4cCT6jThcmC84No= github.com/spezifisch/tview-command v0.0.0-20241013011314-5b01dcbdc948/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013135507-37aefd938546 h1:2EpfyiBkDaPoU+klGhnrjT7v07OxeIqAfNpZkj80E00= +github.com/spezifisch/tview-command v0.0.0-20241013135507-37aefd938546/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= diff --git a/stmps.go b/stmps.go index 667c0f4..05d6c68 100644 --- a/stmps.go +++ b/stmps.go @@ -16,7 +16,7 @@ import ( "github.com/spezifisch/stmps/mpvplayer" "github.com/spezifisch/stmps/remote" "github.com/spezifisch/stmps/subsonic" - "github.com/spezifisch/tview-command/keybinding" + tviewcommand "github.com/spezifisch/tview-command" "github.com/spf13/viper" ) @@ -66,14 +66,14 @@ func parseConfig() { // initCommandHandler sets up tview-command as main input handler func initCommandHandler(logger *logger.Logger) { - keybinding.SetLogHandler(func(msg string) { + tviewcommand.SetLogHandler(func(msg string) { logger.Print(msg) }) configPath := "HACK.commands.toml" // Load the configuration file - config, err := keybinding.LoadConfig(configPath) + config, err := tviewcommand.LoadConfig(configPath) if err != nil || config == nil { logger.PrintError("Failed to load command-shortcut config", err) } From 162c4a99b780cef40b73fdadda5adef367e939a4 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Sun, 13 Oct 2024 17:26:51 +0200 Subject: [PATCH 03/27] [main] make it more testable, add some simple tests --- go.mod | 5 ++++- go.sum | 6 ++---- stmps.go | 31 +++++++++++++++++++------------ stmps_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 17 deletions(-) create mode 100644 stmps_test.go diff --git a/go.mod b/go.mod index 7bce8b2..90abf2a 100644 --- a/go.mod +++ b/go.mod @@ -12,12 +12,14 @@ require ( ) require ( - github.com/spezifisch/tview-command v0.0.0-20241013135507-37aefd938546 + github.com/spezifisch/tview-command v0.0.0-20241013143719-94366d6323e2 + github.com/stretchr/testify v1.9.0 github.com/supersonic-app/go-mpv v0.1.0 ) require ( github.com/BurntSushi/toml v1.4.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gdamore/encoding v1.0.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -26,6 +28,7 @@ require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect diff --git a/go.sum b/go.sum index bafb967..7fbbea9 100644 --- a/go.sum +++ b/go.sum @@ -51,10 +51,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spezifisch/tview-command v0.0.0-20241013011314-5b01dcbdc948 h1:0e9YqpET7Zks2lk6IigZIE+fYcjz4cCT6jThcmC84No= -github.com/spezifisch/tview-command v0.0.0-20241013011314-5b01dcbdc948/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= -github.com/spezifisch/tview-command v0.0.0-20241013135507-37aefd938546 h1:2EpfyiBkDaPoU+klGhnrjT7v07OxeIqAfNpZkj80E00= -github.com/spezifisch/tview-command v0.0.0-20241013135507-37aefd938546/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013143719-94366d6323e2 h1:rhNWDM0v9HbwuF5I8wvOW3bsCdiZ1KRnp7uvhp3Jw+Y= +github.com/spezifisch/tview-command v0.0.0-20241013143719-94366d6323e2/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= diff --git a/stmps.go b/stmps.go index 05d6c68..e1399fd 100644 --- a/stmps.go +++ b/stmps.go @@ -20,6 +20,9 @@ import ( "github.com/spf13/viper" ) +var osExit = os.Exit // A variable to allow mocking os.Exit in tests +var headlessMode bool // This can be set to true during tests + func readConfig() { required_properties := []string{"auth.username", "auth.password", "server.host"} @@ -31,7 +34,7 @@ func readConfig() { if err != nil { fmt.Printf("Config file error: %s \n", err) - os.Exit(1) + osExit(1) } for _, prop := range required_properties { @@ -57,10 +60,9 @@ func parseConfig() { u.User = nil viper.Set("server.host", u.String()) } else { - fmt.Printf("Invalid server format; must be a valid URL: http[s]://[user:pass@]server:port") - fmt.Printf("USAGE: %s [http[s]://[user:pass@]server:port]\n", os.Args[0]) - flag.Usage() - os.Exit(1) + fmt.Printf("Invalid server format; must be a valid URL!") + fmt.Printf("Usage: %s [http[s]://[user:pass@]server:port]\n", os.Args[0]) + osExit(1) } } @@ -93,7 +95,7 @@ func main() { if *help { fmt.Printf("USAGE: %s [[user:pass@]server:port]\n", os.Args[0]) flag.Usage() - os.Exit(0) + osExit(0) } // cpu/memprofile code straight from https://pkg.go.dev/runtime/pprof @@ -130,7 +132,7 @@ func main() { indexResponse, err := connection.GetIndexes() if err != nil { fmt.Printf("Error fetching playlists from server: %s\n", err) - os.Exit(1) + osExit(1) } if *list { @@ -151,7 +153,7 @@ func main() { playlistResponse, err := connection.GetPlaylists() if err != nil { fmt.Printf("Error fetching indexes from server: %s\n", err) - os.Exit(1) + osExit(1) } fmt.Printf(" Directory: %s\n", playlistResponse.Directory.Name) fmt.Printf(" Status: %s\n", playlistResponse.Status) @@ -166,14 +168,14 @@ func main() { fmt.Printf(" %s\n", pl.Name) } - os.Exit(0) + osExit(0) } // init mpv engine player, err := mpvplayer.NewPlayer(logger) if err != nil { fmt.Println("Unable to initialize mpv. Is mpv installed?") - os.Exit(1) + osExit(1) } var mprisPlayer *remote.MprisPlayer @@ -183,7 +185,7 @@ func main() { if err != nil { fmt.Printf("Unable to register MPRIS with DBUS: %s\n", err) fmt.Println("Try running without MPRIS") - os.Exit(1) + osExit(1) } defer mprisPlayer.Close() } @@ -192,12 +194,17 @@ func main() { if runtime.GOOS == "darwin" { if err = remote.RegisterMPMediaHandler(player, logger); err != nil { fmt.Printf("Unable to initialize MediaPlayer bindings: %s\n", err) - os.Exit(1) + osExit(1) } else { logger.Print("MacOS MediaPlayer registered") } } + if headlessMode { + fmt.Println("Running in headless mode for testing.") + return + } + ui := InitGui(&indexResponse.Indexes.Index, connection, player, diff --git a/stmps_test.go b/stmps_test.go new file mode 100644 index 0000000..a7fa512 --- /dev/null +++ b/stmps_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "os" + "testing" + + "github.com/spezifisch/stmps/logger" + "github.com/spezifisch/stmps/mpvplayer" + "github.com/stretchr/testify/assert" +) + +// Test initialization of the player +func TestPlayerInitialization(t *testing.T) { + logger := logger.Init() + player, err := mpvplayer.NewPlayer(logger) + assert.NoError(t, err, "Player initialization should not return an error") + assert.NotNil(t, player, "Player should be initialized") +} + +func TestMainWithoutTUI(t *testing.T) { + // Mock osExit to prevent actual exit during test + exitCalled := false + osExit = func(code int) { + exitCalled = true + if code != 0 { + t.Fatalf("Unexpected exit with code: %d", code) + } + // Since we don't abort execution here, we will run main() until the end or a panic. + } + headlessMode = true + + // Restore osExit after the test + defer func() { + osExit = os.Exit + headlessMode = false + }() + + // Set command-line arguments to trigger the help flag + os.Args = []string{"cmd", "--help"} + + main() + + if !exitCalled { + t.Fatalf("osExit was not called") + } +} From 73e4452b7caca9d0fe67e9faea4954a104deb066 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Sun, 13 Oct 2024 18:04:27 +0200 Subject: [PATCH 04/27] [main/test] fix tests, reorder main so that it's more testable --- stmps.go | 107 +++++++++++++++++++++++++++++++------------------- stmps_test.go | 16 ++++++-- 2 files changed, 80 insertions(+), 43 deletions(-) diff --git a/stmps.go b/stmps.go index e1399fd..7586604 100644 --- a/stmps.go +++ b/stmps.go @@ -22,26 +22,36 @@ import ( var osExit = os.Exit // A variable to allow mocking os.Exit in tests var headlessMode bool // This can be set to true during tests +var testMode bool // This can be set to true during tests, too -func readConfig() { +func readConfig(configFile *string) error { required_properties := []string{"auth.username", "auth.password", "server.host"} - viper.SetConfigName("stmp") - viper.SetConfigType("toml") - viper.AddConfigPath("$HOME/.config/stmp") - viper.AddConfigPath(".") - err := viper.ReadInConfig() + if configFile != nil { + // use custom config file + viper.SetConfigFile(*configFile) + } else { + // lookup default dirs + viper.SetConfigName("stmp") // TODO this should be stmps + viper.SetConfigType("toml") + viper.AddConfigPath("$HOME/.config/stmp") // TODO this should be stmps + viper.AddConfigPath(".") + } + // read it + err := viper.ReadInConfig() if err != nil { - fmt.Printf("Config file error: %s \n", err) - osExit(1) + return fmt.Errorf("Config file error: %s\n", err) } + // validate for _, prop := range required_properties { if !viper.IsSet(prop) { - fmt.Printf("Config property %s is required\n", prop) + return fmt.Errorf("Config property %s is required\n", prop) } } + + return nil } // parseConfig takes the first non-flag arguments from flags and parses it @@ -85,11 +95,13 @@ func initCommandHandler(logger *logger.Logger) { } func main() { + // parse flags and config help := flag.Bool("help", false, "Print usage") enableMpris := flag.Bool("mpris", false, "Enable MPRIS2") list := flag.Bool("list", false, "list server data") cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file`") memprofile := flag.String("memprofile", "", "write memory profile to `file`") + configFile := flag.String("config", "c", "use config `file`") flag.Parse() if *help { @@ -111,15 +123,58 @@ func main() { defer pprof.StopCPUProfile() } + // config gathering if len(flag.Args()) > 0 { parseConfig() - } else { - readConfig() + } + + if err := readConfig(configFile); err != nil { + if configFile == nil { + fmt.Fprintf(os.Stderr, "Failed to read configuration: configuration file is nil\n") + } else { + fmt.Fprintf(os.Stderr, "Failed to read configuration from file '%s': %v\n", *configFile, err) + } + osExit(1) } logger := logger.Init() initCommandHandler(logger) + // init mpv engine + player, err := mpvplayer.NewPlayer(logger) + if err != nil { + fmt.Println("Unable to initialize mpv. Is mpv installed?") + osExit(1) + } + + var mprisPlayer *remote.MprisPlayer + // init mpris2 player control (linux only but fails gracefully on other systems) + if *enableMpris { + mprisPlayer, err = remote.RegisterMprisPlayer(player, logger) + if err != nil { + fmt.Printf("Unable to register MPRIS with DBUS: %s\n", err) + fmt.Println("Try running without MPRIS") + osExit(1) + } + defer mprisPlayer.Close() + } + + // init macos mediaplayer control + if runtime.GOOS == "darwin" { + if err = remote.RegisterMPMediaHandler(player, logger); err != nil { + fmt.Printf("Unable to initialize MediaPlayer bindings: %s\n", err) + osExit(1) + } else { + logger.Print("MacOS MediaPlayer registered") + } + } + + if testMode { + fmt.Println("Running in test mode for testing.") + osExit(0) + return + } + connection := subsonic.Init(logger) connection.SetClientInfo(clientName, clientVersion) connection.Username = viper.GetString("auth.username") @@ -171,37 +226,9 @@ func main() { osExit(0) } - // init mpv engine - player, err := mpvplayer.NewPlayer(logger) - if err != nil { - fmt.Println("Unable to initialize mpv. Is mpv installed?") - osExit(1) - } - - var mprisPlayer *remote.MprisPlayer - // init mpris2 player control (linux only but fails gracefully on other systems) - if *enableMpris { - mprisPlayer, err = remote.RegisterMprisPlayer(player, logger) - if err != nil { - fmt.Printf("Unable to register MPRIS with DBUS: %s\n", err) - fmt.Println("Try running without MPRIS") - osExit(1) - } - defer mprisPlayer.Close() - } - - // init macos mediaplayer control - if runtime.GOOS == "darwin" { - if err = remote.RegisterMPMediaHandler(player, logger); err != nil { - fmt.Printf("Unable to initialize MediaPlayer bindings: %s\n", err) - osExit(1) - } else { - logger.Print("MacOS MediaPlayer registered") - } - } - if headlessMode { fmt.Println("Running in headless mode for testing.") + osExit(0) return } diff --git a/stmps_test.go b/stmps_test.go index a7fa512..a889e63 100644 --- a/stmps_test.go +++ b/stmps_test.go @@ -2,6 +2,7 @@ package main import ( "os" + "runtime" "testing" "github.com/spezifisch/stmps/logger" @@ -22,21 +23,30 @@ func TestMainWithoutTUI(t *testing.T) { exitCalled := false osExit = func(code int) { exitCalled = true + if code != 0 { - t.Fatalf("Unexpected exit with code: %d", code) + // Capture and print the stack trace + stackBuf := make([]byte, 1024) + stackSize := runtime.Stack(stackBuf, false) + stackTrace := string(stackBuf[:stackSize]) + + // Print the stack trace with new lines only + t.Fatalf("Unexpected exit with code: %d\nStack trace:\n%s\n", code, stackTrace) } // Since we don't abort execution here, we will run main() until the end or a panic. } headlessMode = true + testMode = true - // Restore osExit after the test + // Restore patches after the test defer func() { osExit = os.Exit headlessMode = false + testMode = false }() // Set command-line arguments to trigger the help flag - os.Args = []string{"cmd", "--help"} + os.Args = []string{"cmd", "--config=stmp-example.toml", "--help"} main() From 185d23ebd3a14f373b87d4e2649279b34aca7197 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Sun, 13 Oct 2024 18:23:34 +0200 Subject: [PATCH 05/27] [test] ensure proper exit was taken --- stmps.go | 4 ++-- stmps_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/stmps.go b/stmps.go index 7586604..820bd7b 100644 --- a/stmps.go +++ b/stmps.go @@ -171,7 +171,7 @@ func main() { if testMode { fmt.Println("Running in test mode for testing.") - osExit(0) + osExit(0x23420001) return } @@ -228,7 +228,7 @@ func main() { if headlessMode { fmt.Println("Running in headless mode for testing.") - osExit(0) + osExit(0x23420002) return } diff --git a/stmps_test.go b/stmps_test.go index a889e63..05f7e27 100644 --- a/stmps_test.go +++ b/stmps_test.go @@ -24,7 +24,7 @@ func TestMainWithoutTUI(t *testing.T) { osExit = func(code int) { exitCalled = true - if code != 0 { + if code != 0x23420001 { // Capture and print the stack trace stackBuf := make([]byte, 1024) stackSize := runtime.Stack(stackBuf, false) @@ -46,7 +46,7 @@ func TestMainWithoutTUI(t *testing.T) { }() // Set command-line arguments to trigger the help flag - os.Args = []string{"cmd", "--config=stmp-example.toml", "--help"} + os.Args = []string{"cmd", "--config=stmp-example.toml"} main() From de674c9378ecbfc6cddacc1fbf14b39f8b955cef Mon Sep 17 00:00:00 2001 From: spezifisch Date: Sun, 13 Oct 2024 21:39:54 +0200 Subject: [PATCH 06/27] [main/gui] add tvcom setup and a simple test keybinding config --- HACK.commands.toml | 16 +++++++++++++--- go.mod | 2 +- go.sum | 10 ++++++++++ gui.go | 16 +++++++++++++++- stmps.go | 22 ++++++++++++++++++---- 5 files changed, 57 insertions(+), 9 deletions(-) diff --git a/HACK.commands.toml b/HACK.commands.toml index 8702606..4b9dd46 100644 --- a/HACK.commands.toml +++ b/HACK.commands.toml @@ -1,10 +1,20 @@ +# This tvcom test config has different levels of inheritance to try out. + [Global.settings] silent = true [Global.bindings] +CTRL-Z = "tvcomToggleDebugWidget" [Default.bindings] -d = "deleteSelectedTrack" +CTRL-L = "log '^L pressed!'" + +[Init] +# clear inherited Default bindings +context_override = ["Empty"] +# Init: add key bindings only valid *during* program startup +[Init.bindings] +q = "force-quit" -[Empty.bindings] -# context with no bindings +[QueuePage.bindings] +# inherits Default diff --git a/go.mod b/go.mod index 90abf2a..f290175 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( ) require ( - github.com/spezifisch/tview-command v0.0.0-20241013143719-94366d6323e2 + github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289 github.com/stretchr/testify v1.9.0 github.com/supersonic-app/go-mpv v0.1.0 ) diff --git a/go.sum b/go.sum index 7fbbea9..db3a647 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,16 @@ github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9yS github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spezifisch/tview-command v0.0.0-20241013143719-94366d6323e2 h1:rhNWDM0v9HbwuF5I8wvOW3bsCdiZ1KRnp7uvhp3Jw+Y= github.com/spezifisch/tview-command v0.0.0-20241013143719-94366d6323e2/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013164823-5fc036e6cbb9 h1:pnSN+If3/EaRVpd75ukcpGOrnHkxvszI0wV6wIXQsnc= +github.com/spezifisch/tview-command v0.0.0-20241013164823-5fc036e6cbb9/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013170116-60d3122b9f50 h1:OUdI2lBFJqZ4lBYfFrMh8mg2JqEwEOJaLfpaxPFsmRg= +github.com/spezifisch/tview-command v0.0.0-20241013170116-60d3122b9f50/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013173643-6f725a86244f h1:eg0T1W7EDG8UG3R4qy23QRDuFL8/PrISE0xh0Z9zFeo= +github.com/spezifisch/tview-command v0.0.0-20241013173643-6f725a86244f/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013173854-23b02704223f h1:6KzEn8whFBcnVLuZU7sftvS4o8hkdh8520TU5ymD8Ls= +github.com/spezifisch/tview-command v0.0.0-20241013173854-23b02704223f/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289 h1:tYxRaxvhmXvJpOqxAt3AImUEqHtMFUfrztOSbTqn7ZI= +github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= diff --git a/gui.go b/gui.go index 0e3930d..a0879a9 100644 --- a/gui.go +++ b/gui.go @@ -12,6 +12,7 @@ import ( "github.com/spezifisch/stmps/mpvplayer" "github.com/spezifisch/stmps/remote" "github.com/spezifisch/stmps/subsonic" + tviewcommand "github.com/spezifisch/tview-command" ) // struct contains all the updatable elements of the Ui @@ -19,6 +20,10 @@ type Ui struct { app *tview.Application pages *tview.Pages + // keybindings, managed by tview-command + keyConfig *tviewcommand.Config + keyContextStack *tviewcommand.ContextStack + // top bar startStopStatus *tview.TextView playerStatus *tview.TextView @@ -77,12 +82,18 @@ const ( PageSelectPlaylist = "selectPlaylist" ) -func InitGui(indexes *[]subsonic.SubsonicIndex, +func InitGui( + tvcomConfig *tviewcommand.Config, + tvcomContextStack *tviewcommand.ContextStack, + indexes *[]subsonic.SubsonicIndex, connection *subsonic.SubsonicConnection, player *mpvplayer.Player, logger *logger.Logger, mprisPlayer *remote.MprisPlayer) (ui *Ui) { ui = &Ui{ + keyConfig: tvcomConfig, + keyContextStack: tvcomContextStack, + starIdList: map[string]struct{}{}, eventLoop: nil, // initialized by initEventLoops() @@ -95,6 +106,9 @@ func InitGui(indexes *[]subsonic.SubsonicIndex, mprisPlayer: mprisPlayer, } + ui.keyContextStack.Push("Init") + logger.Print("Context: QueuePage pushed to the stack") + ui.initEventLoops() ui.app = tview.NewApplication() diff --git a/stmps.go b/stmps.go index 820bd7b..c49c3ed 100644 --- a/stmps.go +++ b/stmps.go @@ -77,7 +77,7 @@ func parseConfig() { } // initCommandHandler sets up tview-command as main input handler -func initCommandHandler(logger *logger.Logger) { +func initCommandHandler(logger *logger.Logger) *tviewcommand.Config { tviewcommand.SetLogHandler(func(msg string) { logger.Print(msg) }) @@ -88,10 +88,13 @@ func initCommandHandler(logger *logger.Logger) { config, err := tviewcommand.LoadConfig(configPath) if err != nil || config == nil { logger.PrintError("Failed to load command-shortcut config", err) + return nil } - //env := keybinding.SetupEnvironment() + // Register commands //keybinding.RegisterCommands(env) + + return config } func main() { @@ -138,7 +141,15 @@ func main() { } logger := logger.Init() - initCommandHandler(logger) + + // init tview-command + tvcomConfig := initCommandHandler(logger) + if tvcomConfig == nil { + osExit(1) + } + + // init the context stack + tvcomContextStack := tviewcommand.NewContextStack() // init mpv engine player, err := mpvplayer.NewPlayer(logger) @@ -232,7 +243,10 @@ func main() { return } - ui := InitGui(&indexResponse.Indexes.Index, + ui := InitGui( + tvcomConfig, + tvcomContextStack, + &indexResponse.Indexes.Index, connection, player, logger, From c80f6c57023506a30c57c0e0d446885f7d0d9448 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Sun, 13 Oct 2024 22:02:00 +0200 Subject: [PATCH 07/27] [gui] add some first easy key-contexts --- go.mod | 2 +- go.sum | 2 ++ gui.go | 9 ++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index f290175..986ae71 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( ) require ( - github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289 + github.com/spezifisch/tview-command v0.0.0-20241013195600-86c4ba9c4c88 github.com/stretchr/testify v1.9.0 github.com/supersonic-app/go-mpv v0.1.0 ) diff --git a/go.sum b/go.sum index db3a647..60cb776 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,8 @@ github.com/spezifisch/tview-command v0.0.0-20241013173854-23b02704223f h1:6KzEn8 github.com/spezifisch/tview-command v0.0.0-20241013173854-23b02704223f/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289 h1:tYxRaxvhmXvJpOqxAt3AImUEqHtMFUfrztOSbTqn7ZI= github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013195600-86c4ba9c4c88 h1:Ck4VeJ+nT0iSc708iwqjTwhaTNTTFxmNilEXVmze6n8= +github.com/spezifisch/tview-command v0.0.0-20241013195600-86c4ba9c4c88/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= diff --git a/gui.go b/gui.go index a0879a9..d96886a 100644 --- a/gui.go +++ b/gui.go @@ -107,7 +107,7 @@ func InitGui( } ui.keyContextStack.Push("Init") - logger.Print("Context: QueuePage pushed to the stack") + logger.Print("Context: Init pushed to the stack") ui.initEventLoops() @@ -213,6 +213,9 @@ func (ui *Ui) Run() error { // receive events from mpv wrapper ui.player.RegisterEventConsumer(ui) + // leave init key context + ui.keyContextStack.PopExpect("Init") + // run gui/background event handler ui.runEventLoops() @@ -224,6 +227,7 @@ func (ui *Ui) Run() error { } func (ui *Ui) ShowHelp() { + ui.keyContextStack.Push("Help") activePage := ui.menuWidget.GetActivePage() ui.helpWidget.RenderHelp(activePage) @@ -234,11 +238,13 @@ func (ui *Ui) ShowHelp() { } func (ui *Ui) CloseHelp() { + ui.keyContextStack.PopExpect("Help") ui.helpWidget.visible = false ui.pages.HidePage(PageHelpBox) } func (ui *Ui) ShowSelectPlaylist() { + ui.keyContextStack.Push("SelectPlaylist") ui.pages.ShowPage(PageSelectPlaylist) ui.pages.SendToFront(PageSelectPlaylist) ui.app.SetFocus(ui.selectPlaylistModal) @@ -246,6 +252,7 @@ func (ui *Ui) ShowSelectPlaylist() { } func (ui *Ui) CloseSelectPlaylist() { + ui.keyContextStack.PopExpect("SelectPlaylist") ui.pages.HidePage(PageSelectPlaylist) ui.selectPlaylistWidget.visible = false } From fd3efb20c712ec0d0c44cc90717c6ac6ae404b9b Mon Sep 17 00:00:00 2001 From: spezifisch Date: Mon, 14 Oct 2024 19:23:26 +0200 Subject: [PATCH 08/27] [main] fix #70 --- stmps.go | 7 ++++--- stmps_test.go | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/stmps.go b/stmps.go index c49c3ed..e967a6e 100644 --- a/stmps.go +++ b/stmps.go @@ -27,14 +27,15 @@ var testMode bool // This can be set to true during tests, too func readConfig(configFile *string) error { required_properties := []string{"auth.username", "auth.password", "server.host"} - if configFile != nil { + if configFile != nil && *configFile != "" { // use custom config file viper.SetConfigFile(*configFile) } else { // lookup default dirs viper.SetConfigName("stmp") // TODO this should be stmps viper.SetConfigType("toml") - viper.AddConfigPath("$HOME/.config/stmp") // TODO this should be stmps + viper.AddConfigPath("$HOME/.config/stmp") // TODO this should be stmps only + viper.AddConfigPath("$HOME/.config/stmps") viper.AddConfigPath(".") } @@ -104,7 +105,7 @@ func main() { list := flag.Bool("list", false, "list server data") cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file`") memprofile := flag.String("memprofile", "", "write memory profile to `file`") - configFile := flag.String("config", "c", "use config `file`") + configFile := flag.String("config", "", "use config `file`") flag.Parse() if *help { diff --git a/stmps_test.go b/stmps_test.go index 05f7e27..5420264 100644 --- a/stmps_test.go +++ b/stmps_test.go @@ -46,7 +46,45 @@ func TestMainWithoutTUI(t *testing.T) { }() // Set command-line arguments to trigger the help flag - os.Args = []string{"cmd", "--config=stmp-example.toml"} + os.Args = []string{"doesntmatter", "--config=stmp-example.toml"} + + main() + + if !exitCalled { + t.Fatalf("osExit was not called") + } +} + +// Regression test for https://github.com/spezifisch/stmps/issues/70 +func TestMainWithConfigFileEmptyString(t *testing.T) { + // Mock osExit to prevent actual exit during test + exitCalled := false + osExit = func(code int) { + exitCalled = true + + if code != 0x23420001 { + // Capture and print the stack trace + stackBuf := make([]byte, 1024) + stackSize := runtime.Stack(stackBuf, false) + stackTrace := string(stackBuf[:stackSize]) + + // Print the stack trace with new lines only + t.Fatalf("Unexpected exit with code: %d\nStack trace:\n%s\n", code, stackTrace) + } + // Since we don't abort execution here, we will run main() until the end or a panic. + } + headlessMode = true + testMode = true + + // Restore patches after the test + defer func() { + osExit = os.Exit + headlessMode = false + testMode = false + }() + + // Set command-line arguments to trigger the help flag + os.Args = []string{"stmps"} main() From a720e6188862e3b9f284829c2b5408e76f7e074a Mon Sep 17 00:00:00 2001 From: spezifisch Date: Mon, 14 Oct 2024 19:31:23 +0200 Subject: [PATCH 09/27] [test] fix flag state not reset --- stmps_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/stmps_test.go b/stmps_test.go index 5420264..a08da3f 100644 --- a/stmps_test.go +++ b/stmps_test.go @@ -1,6 +1,7 @@ package main import ( + "flag" "os" "runtime" "testing" @@ -19,6 +20,9 @@ func TestPlayerInitialization(t *testing.T) { } func TestMainWithoutTUI(t *testing.T) { + // Reset flags before each test, needed for flag usage in main() + flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) + // Mock osExit to prevent actual exit during test exitCalled := false osExit = func(code int) { @@ -57,6 +61,9 @@ func TestMainWithoutTUI(t *testing.T) { // Regression test for https://github.com/spezifisch/stmps/issues/70 func TestMainWithConfigFileEmptyString(t *testing.T) { + // Reset flags before each test + flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) + // Mock osExit to prevent actual exit during test exitCalled := false osExit = func(code int) { From 9505aba1ec6beaa4e25caf933dd96a0f3f007b70 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Mon, 14 Oct 2024 19:49:36 +0200 Subject: [PATCH 10/27] [test] check result for either nominal error "tried config not found" or success (when found) --- stmps_test.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/stmps_test.go b/stmps_test.go index a08da3f..14186c4 100644 --- a/stmps_test.go +++ b/stmps_test.go @@ -1,7 +1,9 @@ package main import ( + "bytes" "flag" + "log" "os" "runtime" "testing" @@ -93,9 +95,27 @@ func TestMainWithConfigFileEmptyString(t *testing.T) { // Set command-line arguments to trigger the help flag os.Args = []string{"stmps"} - main() + // Capture output of the main function + output := captureOutput(func() { + main() + }) + // Check for the expected conditions if !exitCalled { t.Fatalf("osExit was not called") } + + // Either no error or a specific error message should pass the test + expectedErrorPrefix := "Config file error: Config File \"stmp\" Not Found" + if output != "" && !assert.Contains(t, output, expectedErrorPrefix) { + t.Fatalf("Unexpected error output: %s", output) + } +} + +func captureOutput(f func()) string { + var buf bytes.Buffer + log.SetOutput(&buf) + f() + log.SetOutput(os.Stderr) + return buf.String() } From 7ce1e7bde910c8c275ef609c4b33ff79a4b78081 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Mon, 14 Oct 2024 20:05:49 +0200 Subject: [PATCH 11/27] [main] specify exit codes, fix tests --- stmps.go | 9 +++++++-- stmps_test.go | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/stmps.go b/stmps.go index e967a6e..30d53ee 100644 --- a/stmps.go +++ b/stmps.go @@ -98,6 +98,11 @@ func initCommandHandler(logger *logger.Logger) *tviewcommand.Config { return config } +// return codes: +// 0 - OK +// 1 - generic errors +// 2 - main config errors +// 2 - keybinding config errors func main() { // parse flags and config help := flag.Bool("help", false, "Print usage") @@ -138,7 +143,7 @@ func main() { } else { fmt.Fprintf(os.Stderr, "Failed to read configuration from file '%s': %v\n", *configFile, err) } - osExit(1) + osExit(2) } logger := logger.Init() @@ -146,7 +151,7 @@ func main() { // init tview-command tvcomConfig := initCommandHandler(logger) if tvcomConfig == nil { - osExit(1) + osExit(3) } // init the context stack diff --git a/stmps_test.go b/stmps_test.go index 14186c4..8c21c3c 100644 --- a/stmps_test.go +++ b/stmps_test.go @@ -71,7 +71,7 @@ func TestMainWithConfigFileEmptyString(t *testing.T) { osExit = func(code int) { exitCalled = true - if code != 0x23420001 { + if code != 0x23420001 && code != 2 { // Capture and print the stack trace stackBuf := make([]byte, 1024) stackSize := runtime.Stack(stackBuf, false) From 668e02917270a815101e0f921df291f52ad10580 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Mon, 14 Oct 2024 19:23:26 +0200 Subject: [PATCH 12/27] [main] fix #70 --- stmps.go | 7 ++++--- stmps_test.go | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/stmps.go b/stmps.go index 7586604..70042f0 100644 --- a/stmps.go +++ b/stmps.go @@ -27,14 +27,15 @@ var testMode bool // This can be set to true during tests, too func readConfig(configFile *string) error { required_properties := []string{"auth.username", "auth.password", "server.host"} - if configFile != nil { + if configFile != nil && *configFile != "" { // use custom config file viper.SetConfigFile(*configFile) } else { // lookup default dirs viper.SetConfigName("stmp") // TODO this should be stmps viper.SetConfigType("toml") - viper.AddConfigPath("$HOME/.config/stmp") // TODO this should be stmps + viper.AddConfigPath("$HOME/.config/stmp") // TODO this should be stmps only + viper.AddConfigPath("$HOME/.config/stmps") viper.AddConfigPath(".") } @@ -101,7 +102,7 @@ func main() { list := flag.Bool("list", false, "list server data") cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file`") memprofile := flag.String("memprofile", "", "write memory profile to `file`") - configFile := flag.String("config", "c", "use config `file`") + configFile := flag.String("config", "", "use config `file`") flag.Parse() if *help { diff --git a/stmps_test.go b/stmps_test.go index a889e63..823e8f6 100644 --- a/stmps_test.go +++ b/stmps_test.go @@ -46,7 +46,45 @@ func TestMainWithoutTUI(t *testing.T) { }() // Set command-line arguments to trigger the help flag - os.Args = []string{"cmd", "--config=stmp-example.toml", "--help"} + os.Args = []string{"doesntmatter", "--config=stmp-example.toml"} + + main() + + if !exitCalled { + t.Fatalf("osExit was not called") + } +} + +// Regression test for https://github.com/spezifisch/stmps/issues/70 +func TestMainWithConfigFileEmptyString(t *testing.T) { + // Mock osExit to prevent actual exit during test + exitCalled := false + osExit = func(code int) { + exitCalled = true + + if code != 0x23420001 { + // Capture and print the stack trace + stackBuf := make([]byte, 1024) + stackSize := runtime.Stack(stackBuf, false) + stackTrace := string(stackBuf[:stackSize]) + + // Print the stack trace with new lines only + t.Fatalf("Unexpected exit with code: %d\nStack trace:\n%s\n", code, stackTrace) + } + // Since we don't abort execution here, we will run main() until the end or a panic. + } + headlessMode = true + testMode = true + + // Restore patches after the test + defer func() { + osExit = os.Exit + headlessMode = false + testMode = false + }() + + // Set command-line arguments to trigger the help flag + os.Args = []string{"stmps"} main() From 0232a30f1dcdb70ef35ead5edb5ea89313b5cce6 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Mon, 14 Oct 2024 20:05:49 +0200 Subject: [PATCH 13/27] [main] specify exit codes, fix tests --- stmps.go | 7 ++++++- stmps_test.go | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/stmps.go b/stmps.go index 70042f0..a10fdba 100644 --- a/stmps.go +++ b/stmps.go @@ -95,6 +95,11 @@ func initCommandHandler(logger *logger.Logger) { //keybinding.RegisterCommands(env) } +// return codes: +// 0 - OK +// 1 - generic errors +// 2 - main config errors +// 2 - keybinding config errors func main() { // parse flags and config help := flag.Bool("help", false, "Print usage") @@ -135,7 +140,7 @@ func main() { } else { fmt.Fprintf(os.Stderr, "Failed to read configuration from file '%s': %v\n", *configFile, err) } - osExit(1) + osExit(2) } logger := logger.Init() diff --git a/stmps_test.go b/stmps_test.go index 823e8f6..2f279c7 100644 --- a/stmps_test.go +++ b/stmps_test.go @@ -62,7 +62,7 @@ func TestMainWithConfigFileEmptyString(t *testing.T) { osExit = func(code int) { exitCalled = true - if code != 0x23420001 { + if code != 0x23420001 && code != 2 { // Capture and print the stack trace stackBuf := make([]byte, 1024) stackSize := runtime.Stack(stackBuf, false) From 617c588e5d474314e5f64ac315f82bbd10f7d3fe Mon Sep 17 00:00:00 2001 From: spezifisch Date: Mon, 14 Oct 2024 20:28:45 +0200 Subject: [PATCH 14/27] [tests] further painful cherry-picks to backport this --- stmps.go | 2 +- stmps_test.go | 31 +++++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/stmps.go b/stmps.go index a10fdba..ebf7bda 100644 --- a/stmps.go +++ b/stmps.go @@ -177,7 +177,7 @@ func main() { if testMode { fmt.Println("Running in test mode for testing.") - osExit(0) + osExit(0x23420001) return } diff --git a/stmps_test.go b/stmps_test.go index 2f279c7..8c21c3c 100644 --- a/stmps_test.go +++ b/stmps_test.go @@ -1,6 +1,9 @@ package main import ( + "bytes" + "flag" + "log" "os" "runtime" "testing" @@ -19,12 +22,15 @@ func TestPlayerInitialization(t *testing.T) { } func TestMainWithoutTUI(t *testing.T) { + // Reset flags before each test, needed for flag usage in main() + flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) + // Mock osExit to prevent actual exit during test exitCalled := false osExit = func(code int) { exitCalled = true - if code != 0 { + if code != 0x23420001 { // Capture and print the stack trace stackBuf := make([]byte, 1024) stackSize := runtime.Stack(stackBuf, false) @@ -57,6 +63,9 @@ func TestMainWithoutTUI(t *testing.T) { // Regression test for https://github.com/spezifisch/stmps/issues/70 func TestMainWithConfigFileEmptyString(t *testing.T) { + // Reset flags before each test + flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) + // Mock osExit to prevent actual exit during test exitCalled := false osExit = func(code int) { @@ -86,9 +95,27 @@ func TestMainWithConfigFileEmptyString(t *testing.T) { // Set command-line arguments to trigger the help flag os.Args = []string{"stmps"} - main() + // Capture output of the main function + output := captureOutput(func() { + main() + }) + // Check for the expected conditions if !exitCalled { t.Fatalf("osExit was not called") } + + // Either no error or a specific error message should pass the test + expectedErrorPrefix := "Config file error: Config File \"stmp\" Not Found" + if output != "" && !assert.Contains(t, output, expectedErrorPrefix) { + t.Fatalf("Unexpected error output: %s", output) + } +} + +func captureOutput(f func()) string { + var buf bytes.Buffer + log.SetOutput(&buf) + f() + log.SetOutput(os.Stderr) + return buf.String() } From 3af0a335eb532de8fe11903cc06282962a11f2a7 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Sun, 13 Oct 2024 17:26:51 +0200 Subject: [PATCH 15/27] [main] make it more testable, add some simple tests --- stmps.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stmps.go b/stmps.go index ebf7bda..05e4cca 100644 --- a/stmps.go +++ b/stmps.go @@ -238,6 +238,11 @@ func main() { return } + if headlessMode { + fmt.Println("Running in headless mode for testing.") + return + } + ui := InitGui(&indexResponse.Indexes.Index, connection, player, From a514a3d086b2ccb0d9a3def29e343b7e899c510b Mon Sep 17 00:00:00 2001 From: spezifisch Date: Sun, 13 Oct 2024 18:04:27 +0200 Subject: [PATCH 16/27] [main/test] fix tests, reorder main so that it's more testable --- stmps.go | 1 + 1 file changed, 1 insertion(+) diff --git a/stmps.go b/stmps.go index 05e4cca..fa8b695 100644 --- a/stmps.go +++ b/stmps.go @@ -240,6 +240,7 @@ func main() { if headlessMode { fmt.Println("Running in headless mode for testing.") + osExit(0) return } From efb42d05e8524e7c199812e6fcbda83a3338f5bc Mon Sep 17 00:00:00 2001 From: spezifisch Date: Sun, 13 Oct 2024 18:23:34 +0200 Subject: [PATCH 17/27] [test] ensure proper exit was taken --- stmps.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stmps.go b/stmps.go index fa8b695..a5afc48 100644 --- a/stmps.go +++ b/stmps.go @@ -234,7 +234,7 @@ func main() { if headlessMode { fmt.Println("Running in headless mode for testing.") - osExit(0) + osExit(0x23420002) return } From cbb11cb702c388a8c3d8a9e7071e75883f531663 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Sun, 13 Oct 2024 21:39:54 +0200 Subject: [PATCH 18/27] [main/gui] add tvcom setup and a simple test keybinding config --- HACK.commands.toml | 16 +++++++++++++--- go.mod | 2 +- go.sum | 10 ++++++++++ gui.go | 16 +++++++++++++++- stmps.go | 17 ++++++++++++++--- 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/HACK.commands.toml b/HACK.commands.toml index 8702606..4b9dd46 100644 --- a/HACK.commands.toml +++ b/HACK.commands.toml @@ -1,10 +1,20 @@ +# This tvcom test config has different levels of inheritance to try out. + [Global.settings] silent = true [Global.bindings] +CTRL-Z = "tvcomToggleDebugWidget" [Default.bindings] -d = "deleteSelectedTrack" +CTRL-L = "log '^L pressed!'" + +[Init] +# clear inherited Default bindings +context_override = ["Empty"] +# Init: add key bindings only valid *during* program startup +[Init.bindings] +q = "force-quit" -[Empty.bindings] -# context with no bindings +[QueuePage.bindings] +# inherits Default diff --git a/go.mod b/go.mod index 90abf2a..f290175 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( ) require ( - github.com/spezifisch/tview-command v0.0.0-20241013143719-94366d6323e2 + github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289 github.com/stretchr/testify v1.9.0 github.com/supersonic-app/go-mpv v0.1.0 ) diff --git a/go.sum b/go.sum index 7fbbea9..db3a647 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,16 @@ github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9yS github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spezifisch/tview-command v0.0.0-20241013143719-94366d6323e2 h1:rhNWDM0v9HbwuF5I8wvOW3bsCdiZ1KRnp7uvhp3Jw+Y= github.com/spezifisch/tview-command v0.0.0-20241013143719-94366d6323e2/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013164823-5fc036e6cbb9 h1:pnSN+If3/EaRVpd75ukcpGOrnHkxvszI0wV6wIXQsnc= +github.com/spezifisch/tview-command v0.0.0-20241013164823-5fc036e6cbb9/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013170116-60d3122b9f50 h1:OUdI2lBFJqZ4lBYfFrMh8mg2JqEwEOJaLfpaxPFsmRg= +github.com/spezifisch/tview-command v0.0.0-20241013170116-60d3122b9f50/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013173643-6f725a86244f h1:eg0T1W7EDG8UG3R4qy23QRDuFL8/PrISE0xh0Z9zFeo= +github.com/spezifisch/tview-command v0.0.0-20241013173643-6f725a86244f/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013173854-23b02704223f h1:6KzEn8whFBcnVLuZU7sftvS4o8hkdh8520TU5ymD8Ls= +github.com/spezifisch/tview-command v0.0.0-20241013173854-23b02704223f/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289 h1:tYxRaxvhmXvJpOqxAt3AImUEqHtMFUfrztOSbTqn7ZI= +github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= diff --git a/gui.go b/gui.go index 0e3930d..a0879a9 100644 --- a/gui.go +++ b/gui.go @@ -12,6 +12,7 @@ import ( "github.com/spezifisch/stmps/mpvplayer" "github.com/spezifisch/stmps/remote" "github.com/spezifisch/stmps/subsonic" + tviewcommand "github.com/spezifisch/tview-command" ) // struct contains all the updatable elements of the Ui @@ -19,6 +20,10 @@ type Ui struct { app *tview.Application pages *tview.Pages + // keybindings, managed by tview-command + keyConfig *tviewcommand.Config + keyContextStack *tviewcommand.ContextStack + // top bar startStopStatus *tview.TextView playerStatus *tview.TextView @@ -77,12 +82,18 @@ const ( PageSelectPlaylist = "selectPlaylist" ) -func InitGui(indexes *[]subsonic.SubsonicIndex, +func InitGui( + tvcomConfig *tviewcommand.Config, + tvcomContextStack *tviewcommand.ContextStack, + indexes *[]subsonic.SubsonicIndex, connection *subsonic.SubsonicConnection, player *mpvplayer.Player, logger *logger.Logger, mprisPlayer *remote.MprisPlayer) (ui *Ui) { ui = &Ui{ + keyConfig: tvcomConfig, + keyContextStack: tvcomContextStack, + starIdList: map[string]struct{}{}, eventLoop: nil, // initialized by initEventLoops() @@ -95,6 +106,9 @@ func InitGui(indexes *[]subsonic.SubsonicIndex, mprisPlayer: mprisPlayer, } + ui.keyContextStack.Push("Init") + logger.Print("Context: QueuePage pushed to the stack") + ui.initEventLoops() ui.app = tview.NewApplication() diff --git a/stmps.go b/stmps.go index a5afc48..7e8e252 100644 --- a/stmps.go +++ b/stmps.go @@ -78,7 +78,7 @@ func parseConfig() { } // initCommandHandler sets up tview-command as main input handler -func initCommandHandler(logger *logger.Logger) { +func initCommandHandler(logger *logger.Logger) *tviewcommand.Config { tviewcommand.SetLogHandler(func(msg string) { logger.Print(msg) }) @@ -89,10 +89,13 @@ func initCommandHandler(logger *logger.Logger) { config, err := tviewcommand.LoadConfig(configPath) if err != nil || config == nil { logger.PrintError("Failed to load command-shortcut config", err) + return nil } - //env := keybinding.SetupEnvironment() + // Register commands //keybinding.RegisterCommands(env) + + return config } // return codes: @@ -144,7 +147,15 @@ func main() { } logger := logger.Init() - initCommandHandler(logger) + + // init tview-command + tvcomConfig := initCommandHandler(logger) + if tvcomConfig == nil { + osExit(1) + } + + // init the context stack + tvcomContextStack := tviewcommand.NewContextStack() // init mpv engine player, err := mpvplayer.NewPlayer(logger) From 9f1f1e8a75553e2e2994eb81a7e713ac189111bb Mon Sep 17 00:00:00 2001 From: spezifisch Date: Sun, 13 Oct 2024 22:02:00 +0200 Subject: [PATCH 19/27] [gui] add some first easy key-contexts --- go.mod | 2 +- go.sum | 2 ++ gui.go | 9 ++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index f290175..986ae71 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( ) require ( - github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289 + github.com/spezifisch/tview-command v0.0.0-20241013195600-86c4ba9c4c88 github.com/stretchr/testify v1.9.0 github.com/supersonic-app/go-mpv v0.1.0 ) diff --git a/go.sum b/go.sum index db3a647..60cb776 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,8 @@ github.com/spezifisch/tview-command v0.0.0-20241013173854-23b02704223f h1:6KzEn8 github.com/spezifisch/tview-command v0.0.0-20241013173854-23b02704223f/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289 h1:tYxRaxvhmXvJpOqxAt3AImUEqHtMFUfrztOSbTqn7ZI= github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241013195600-86c4ba9c4c88 h1:Ck4VeJ+nT0iSc708iwqjTwhaTNTTFxmNilEXVmze6n8= +github.com/spezifisch/tview-command v0.0.0-20241013195600-86c4ba9c4c88/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= diff --git a/gui.go b/gui.go index a0879a9..d96886a 100644 --- a/gui.go +++ b/gui.go @@ -107,7 +107,7 @@ func InitGui( } ui.keyContextStack.Push("Init") - logger.Print("Context: QueuePage pushed to the stack") + logger.Print("Context: Init pushed to the stack") ui.initEventLoops() @@ -213,6 +213,9 @@ func (ui *Ui) Run() error { // receive events from mpv wrapper ui.player.RegisterEventConsumer(ui) + // leave init key context + ui.keyContextStack.PopExpect("Init") + // run gui/background event handler ui.runEventLoops() @@ -224,6 +227,7 @@ func (ui *Ui) Run() error { } func (ui *Ui) ShowHelp() { + ui.keyContextStack.Push("Help") activePage := ui.menuWidget.GetActivePage() ui.helpWidget.RenderHelp(activePage) @@ -234,11 +238,13 @@ func (ui *Ui) ShowHelp() { } func (ui *Ui) CloseHelp() { + ui.keyContextStack.PopExpect("Help") ui.helpWidget.visible = false ui.pages.HidePage(PageHelpBox) } func (ui *Ui) ShowSelectPlaylist() { + ui.keyContextStack.Push("SelectPlaylist") ui.pages.ShowPage(PageSelectPlaylist) ui.pages.SendToFront(PageSelectPlaylist) ui.app.SetFocus(ui.selectPlaylistModal) @@ -246,6 +252,7 @@ func (ui *Ui) ShowSelectPlaylist() { } func (ui *Ui) CloseSelectPlaylist() { + ui.keyContextStack.PopExpect("SelectPlaylist") ui.pages.HidePage(PageSelectPlaylist) ui.selectPlaylistWidget.visible = false } From 8dd60e3ce616cef73925217559887fbaae3858c2 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Mon, 14 Oct 2024 20:05:49 +0200 Subject: [PATCH 20/27] [main] specify exit codes, fix tests --- stmps.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stmps.go b/stmps.go index 7e8e252..61921dc 100644 --- a/stmps.go +++ b/stmps.go @@ -151,7 +151,7 @@ func main() { // init tview-command tvcomConfig := initCommandHandler(logger) if tvcomConfig == nil { - osExit(1) + osExit(3) } // init the context stack From 9e484243b1c9bb34c1f9eca7f8bd01509665085f Mon Sep 17 00:00:00 2001 From: spezifisch Date: Mon, 14 Oct 2024 20:39:10 +0200 Subject: [PATCH 21/27] [git] rebase complete --- stmps.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/stmps.go b/stmps.go index 61921dc..30d53ee 100644 --- a/stmps.go +++ b/stmps.go @@ -249,13 +249,10 @@ func main() { return } - if headlessMode { - fmt.Println("Running in headless mode for testing.") - osExit(0) - return - } - - ui := InitGui(&indexResponse.Indexes.Index, + ui := InitGui( + tvcomConfig, + tvcomContextStack, + &indexResponse.Indexes.Index, connection, player, logger, From 4934d0a7c9963f30dc14186ffdb3d6af0fdf8d7e Mon Sep 17 00:00:00 2001 From: spezifisch Date: Tue, 15 Oct 2024 00:50:38 +0200 Subject: [PATCH 22/27] [gui] tview-command is running along now, parsing keys to commands from its config and logging them --- go.mod | 2 +- go.sum | 2 ++ gui.go | 5 ++++- gui_handlers.go | 23 +++++++++++++++++++++++ stmps.go | 2 +- 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 986ae71..db19b04 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( ) require ( - github.com/spezifisch/tview-command v0.0.0-20241013195600-86c4ba9c4c88 + github.com/spezifisch/tview-command v0.0.0-20241014224340-cf19ddd0dd54 github.com/stretchr/testify v1.9.0 github.com/supersonic-app/go-mpv v0.1.0 ) diff --git a/go.sum b/go.sum index 60cb776..ddba79a 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,8 @@ github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289 h1:tYxRax github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= github.com/spezifisch/tview-command v0.0.0-20241013195600-86c4ba9c4c88 h1:Ck4VeJ+nT0iSc708iwqjTwhaTNTTFxmNilEXVmze6n8= github.com/spezifisch/tview-command v0.0.0-20241013195600-86c4ba9c4c88/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= +github.com/spezifisch/tview-command v0.0.0-20241014224340-cf19ddd0dd54 h1:d0haUdxW02V9MTO+8njIOSlEKjA+/JeXrqPLyx5J2sQ= +github.com/spezifisch/tview-command v0.0.0-20241014224340-cf19ddd0dd54/go.mod h1:ctJ/3e6RYpK+O9M1kFxkUFInR13DOeQmik5wZiJ5WNk= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= diff --git a/gui.go b/gui.go index d96886a..6579516 100644 --- a/gui.go +++ b/gui.go @@ -204,7 +204,10 @@ func InitGui( SetFocus(rootFlex). EnableMouse(true) - ui.playlistPage.UpdatePlaylists() + if !testMode { + // this connects to the subsonic server, so exclude it for tests + ui.playlistPage.UpdatePlaylists() + } return ui } diff --git a/gui_handlers.go b/gui_handlers.go index 14ff0cb..5de26a6 100644 --- a/gui_handlers.go +++ b/gui_handlers.go @@ -4,9 +4,12 @@ package main import ( + "fmt" + "github.com/gdamore/tcell/v2" "github.com/spezifisch/stmps/mpvplayer" "github.com/spezifisch/stmps/subsonic" + tviewcommand "github.com/spezifisch/tview-command" ) func (ui *Ui) handlePageInput(event *tcell.EventKey) *tcell.EventKey { @@ -16,6 +19,26 @@ func (ui *Ui) handlePageInput(event *tcell.EventKey) *tcell.EventKey { return event } + // Create a tview-command event + tcEvent := tviewcommand.FromEventKey(event, ui.keyConfig) + + // Look up the command based on the active context + activeContext := ui.keyContextStack.Current() + if err := tcEvent.LookupCommand(activeContext); err != nil { + ui.logger.Printf("[t-c] error: %v, context: %s", err, activeContext) + //return event // HACK + } else { + // Add some logs for now + right := "" + if tcEvent.IsBound { + right = fmt.Sprintf("bound to command: %s", tcEvent.Command) + } else { + right = "unbound" + } + ui.logger.Printf("[t-c] saw an event: key '%s' in context '%v' -> %s", tcEvent.KeyName, activeContext, right) + } + + // Old stuff switch event.Rune() { case '1': ui.ShowPage(PageBrowser) diff --git a/stmps.go b/stmps.go index 30d53ee..97f3dc6 100644 --- a/stmps.go +++ b/stmps.go @@ -201,7 +201,7 @@ func main() { connection.Scrobble = viper.GetBool("server.scrobble") connection.RandomSongNumber = viper.GetUint("client.random-songs") - indexResponse, err := connection.GetIndexes() + indexResponse, err := connection.GetIndexes() // TODO how long does this take? if err != nil { fmt.Printf("Error fetching playlists from server: %s\n", err) osExit(1) From 65492a6664e86909626f9cecff2ffb18056c7455 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Tue, 15 Oct 2024 01:21:03 +0200 Subject: [PATCH 23/27] [deps] pull in new t-c (working lookups are logged in log page, try Ctrl-L or -Z) --- go.mod | 2 +- go.sum | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index db19b04..aa128af 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( ) require ( - github.com/spezifisch/tview-command v0.0.0-20241014224340-cf19ddd0dd54 + github.com/spezifisch/tview-command v0.0.0-20241014231909-8cf60c7b94e5 github.com/stretchr/testify v1.9.0 github.com/supersonic-app/go-mpv v0.1.0 ) diff --git a/go.sum b/go.sum index ddba79a..df9c302 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,10 @@ github.com/spezifisch/tview-command v0.0.0-20241013195600-86c4ba9c4c88 h1:Ck4VeJ github.com/spezifisch/tview-command v0.0.0-20241013195600-86c4ba9c4c88/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= github.com/spezifisch/tview-command v0.0.0-20241014224340-cf19ddd0dd54 h1:d0haUdxW02V9MTO+8njIOSlEKjA+/JeXrqPLyx5J2sQ= github.com/spezifisch/tview-command v0.0.0-20241014224340-cf19ddd0dd54/go.mod h1:ctJ/3e6RYpK+O9M1kFxkUFInR13DOeQmik5wZiJ5WNk= +github.com/spezifisch/tview-command v0.0.0-20241014230331-c898ad9c68e5 h1:fO6M7R3MhatDGNuTzMLENoP2mxO5GJVXUlHOXGs1C/Y= +github.com/spezifisch/tview-command v0.0.0-20241014230331-c898ad9c68e5/go.mod h1:ctJ/3e6RYpK+O9M1kFxkUFInR13DOeQmik5wZiJ5WNk= +github.com/spezifisch/tview-command v0.0.0-20241014231909-8cf60c7b94e5 h1:pMUWJp+61LbdF6B9yb0acoescbPval2WxQ9gfFHPqJk= +github.com/spezifisch/tview-command v0.0.0-20241014231909-8cf60c7b94e5/go.mod h1:ctJ/3e6RYpK+O9M1kFxkUFInR13DOeQmik5wZiJ5WNk= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= From 0f0be604282b6404070f18ab14f07c44fbdc2c7b Mon Sep 17 00:00:00 2001 From: spezifisch Date: Tue, 15 Oct 2024 15:29:33 +0200 Subject: [PATCH 24/27] [commands] implement command registry --- commands/registry.go | 121 +++++++++++++++++++++++++++++++ commands/registry_test.go | 147 ++++++++++++++++++++++++++++++++++++++ gui_handlers.go | 18 +++++ 3 files changed, 286 insertions(+) create mode 100644 commands/registry.go create mode 100644 commands/registry_test.go diff --git a/commands/registry.go b/commands/registry.go new file mode 100644 index 0000000..3006116 --- /dev/null +++ b/commands/registry.go @@ -0,0 +1,121 @@ +package commands + +import ( + "fmt" + "strings" +) + +// CommandFunc defines the signature of a command function with arguments. +type CommandFunc func(args []string) error + +// CommandRegistry holds the list of available commands. +type CommandRegistry struct { + commands map[string]CommandFunc +} + +// NewRegistry creates a new CommandRegistry. +func NewRegistry() *CommandRegistry { + return &CommandRegistry{ + commands: make(map[string]CommandFunc), + } +} + +// Register adds a command with arguments support to the registry. +func (r *CommandRegistry) Register(name string, fn CommandFunc) { + r.commands[name] = fn +} + +// Execute parses and runs a command chain, supporting arguments and chaining. +func (r *CommandRegistry) Execute(commandStr string) error { + // Split the input into chains of commands + commandChains := parseCommandChain(commandStr) + + // Iterate over each command in the chain + for _, chain := range commandChains { + // Ensure the chain has at least one command + if len(chain) == 0 { + continue + } + + // The first element is the command name, the rest are arguments + commandName := chain[0] + args := chain[1:] + + if cmd, exists := r.commands[commandName]; exists { + // Execute the command with arguments + err := cmd(args) + if err != nil { + return fmt.Errorf("Error executing command '%s': %v", commandName, err) + } + } else { + return fmt.Errorf("Command '%s' not found", commandName) + } + } + + return nil +} + +// ExecuteChain allows executing multiple commands separated by ';' +func (r *CommandRegistry) ExecuteChain(commandChain string) error { + commands := strings.Split(commandChain, ";") + for _, cmd := range commands { + cmd = strings.TrimSpace(cmd) + if err := r.Execute(cmd); err != nil { + return err + } + } + return nil +} + +// parseCommandChain splits a command string into parts. +func parseCommandChain(input string) [][]string { + var commands [][]string + var currentCommand []string + var current strings.Builder + var inQuotes, escapeNext bool + + for _, char := range input { + switch { + case escapeNext: + current.WriteRune(char) + escapeNext = false + case char == '\\': + escapeNext = true + case char == '\'': + inQuotes = !inQuotes + case char == ';' && !inQuotes: + if current.Len() > 0 { + currentCommand = append(currentCommand, current.String()) + current.Reset() + } + if len(currentCommand) > 0 { + commands = append(commands, currentCommand) + currentCommand = nil + } + case char == ' ' && !inQuotes: + if current.Len() > 0 { + currentCommand = append(currentCommand, current.String()) + current.Reset() + } + default: + current.WriteRune(char) + } + } + if current.Len() > 0 { + currentCommand = append(currentCommand, current.String()) + } + if len(currentCommand) > 0 { + commands = append(commands, currentCommand) + } + + return commands +} + +// List returns a slice of all registered commands. +func (r *CommandRegistry) List() []string { + keys := make([]string, 0, len(r.commands)) + for k := range r.commands { + keys = append(keys, k) + } + return keys +} diff --git a/commands/registry_test.go b/commands/registry_test.go new file mode 100644 index 0000000..7a637d0 --- /dev/null +++ b/commands/registry_test.go @@ -0,0 +1,147 @@ +package commands + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRegisterAndExecuteCommand(t *testing.T) { + registry := NewRegistry() + + // Track if the command was called + wasCalledA := false + wasCalledB := false + + // Register a simple command that logs the first argument + registry.Register("log", func(args []string) error { + if len(args) > 0 { + wasCalledA = true + return nil + } + wasCalledB = true + return fmt.Errorf("missing argument") + }) + + // Test executing a valid command + err := registry.Execute("log 'test message'") + assert.NoError(t, err, "Command 'log' with argument should execute without error") + assert.True(t, wasCalledA, "Command 'log' success case should have been called") + assert.False(t, wasCalledB, "Command 'log' failure case should not have been called") + + wasCalledA = false + wasCalledB = false + err = registry.Execute("log") + assert.Error(t, err, "Command 'log' without argument should execute with error") + assert.False(t, wasCalledA, "Command 'log' success case should not have been called") + assert.True(t, wasCalledB, "Command 'log' failure case should have been called") +} + +func TestExecuteNonExistentCommand(t *testing.T) { + registry := NewRegistry() + + // Test executing a command that does not exist + err := registry.Execute("nonexistent") + assert.Error(t, err, "Should return error when executing a non-existent command") + assert.Contains(t, err.Error(), "Command 'nonexistent' not found", "Error message should indicate missing command") +} + +func TestCommandWithArguments(t *testing.T) { + registry := NewRegistry() + + // Register a command that expects an argument + registry.Register("log", func(args []string) error { + if len(args) > 0 && args[0] == "hello" { + return nil + } + return fmt.Errorf("wrong argument") + }) + + // Test command with correct argument + err := registry.Execute("log 'hello'") + assert.NoError(t, err, "Command with correct argument should execute without error") + + // Test command with wrong argument + err = registry.Execute("log 'wrong'") + assert.Error(t, err, "Command with wrong argument should return an error") +} + +func TestCommandChaining(t *testing.T) { + registry := NewRegistry() + + // Register a couple of commands + registry.Register("first", func(args []string) error { + return nil + }) + registry.Register("second", func(args []string) error { + return nil + }) + + // Test valid command chaining + err := registry.ExecuteChain("first; second") + assert.NoError(t, err, "Command chain should execute all commands without error") + + // Test chaining with an invalid command + err = registry.ExecuteChain("first; nonexistent; second") + assert.Error(t, err, "Command chain should return error if one command is invalid") + + // Test valid command with arguments in chaining + registry.Register("log", func(args []string) error { + if len(args) > 0 && args[0] == "message" { + return nil + } + return fmt.Errorf("unexpected argument") + }) + + err = registry.ExecuteChain("log 'message'; first") + assert.NoError(t, err, "Command chain with arguments should execute without error") + + // Test chaining commands with mixed valid and invalid arguments + err = registry.ExecuteChain("log 'message'; log 'wrong'; first") + assert.Error(t, err, "Command chain with one invalid argument should return error") +} +func TestParseCommandLine(t *testing.T) { + // Test parsing command with no arguments + result := parseCommandChain("log") + assert.Equal(t, [][]string{{"log"}}, result, "Command with no arguments should return single element slice") + + // Test parsing command with a quoted argument + result = parseCommandChain("log 'hello world'") + assert.Equal(t, [][]string{{"log", "hello world"}}, result, "Command with quoted argument should return correctly split parts") + + // Test parsing command with multiple arguments + result = parseCommandChain("add 'file.txt' 'destination'") + assert.Equal(t, [][]string{{"add", "file.txt", "destination"}}, result, "Command with multiple quoted arguments should return correctly split parts") + + // Test command chain separated by semicolons + result = parseCommandChain("log 'message'; first; second") + assert.Equal(t, [][]string{{"log", "message"}, {"first"}, {"second"}}, result, "Command chain should return correctly split commands and arguments") +} + +func TestParseCommandChain(t *testing.T) { + // Test parsing a chain of commands + result := parseCommandChain("log 'message'; first; second") + expected := [][]string{ + {"log", "message"}, + {"first"}, + {"second"}, + } + assert.Equal(t, expected, result, "Command chain should return correctly split commands and arguments") + + // Test parsing a chain with no arguments + result = parseCommandChain("first; second") + expected = [][]string{ + {"first"}, + {"second"}, + } + assert.Equal(t, expected, result, "Command chain without arguments should return correctly split commands") + + // Test parsing with multiple quoted arguments + result = parseCommandChain("add 'file.txt' 'destination'; move 'file.txt'") + expected = [][]string{ + {"add", "file.txt", "destination"}, + {"move", "file.txt"}, + } + assert.Equal(t, expected, result, "Command chain with multiple arguments should return correctly parsed commands") +} diff --git a/gui_handlers.go b/gui_handlers.go index 5de26a6..9ba5b99 100644 --- a/gui_handlers.go +++ b/gui_handlers.go @@ -12,6 +12,17 @@ import ( tviewcommand "github.com/spezifisch/tview-command" ) +// >>> Stuff that will be moved to t-c +type MyEvent struct { + tviewcommand.Event +} + +func (e *MyEvent) IsCommand(name string) bool { + return e.Command == name +} + +//<<< End of Stuff + func (ui *Ui) handlePageInput(event *tcell.EventKey) *tcell.EventKey { // we don't want any of these firing if we're trying to add a new playlist focused := ui.app.GetFocus() @@ -38,6 +49,13 @@ func (ui *Ui) handlePageInput(event *tcell.EventKey) *tcell.EventKey { ui.logger.Printf("[t-c] saw an event: key '%s' in context '%v' -> %s", tcEvent.KeyName, activeContext, right) } + // Ensure "force-quit" command quits + myEvent := MyEvent{*tcEvent} + if myEvent.IsCommand("force-quit") { + ui.Quit() + return nil + } + // Old stuff switch event.Rune() { case '1': From 0e29de44697dc2dd1de1088917499ab8ba762cdf Mon Sep 17 00:00:00 2001 From: spezifisch Date: Tue, 15 Oct 2024 16:18:18 +0200 Subject: [PATCH 25/27] [commands] add CommandContext --- commands/context.go | 9 +++++++++ commands/registry.go | 12 ++++++------ commands/registry_test.go | 33 +++++++++++++++++++-------------- 3 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 commands/context.go diff --git a/commands/context.go b/commands/context.go new file mode 100644 index 0000000..3936561 --- /dev/null +++ b/commands/context.go @@ -0,0 +1,9 @@ +package commands + +import "github.com/spezifisch/stmps/logger" + +type CommandContext struct { + Logger *logger.Logger + CurrentPage string + // Other UI or state fields +} diff --git a/commands/registry.go b/commands/registry.go index 3006116..d44a55a 100644 --- a/commands/registry.go +++ b/commands/registry.go @@ -5,8 +5,8 @@ import ( "strings" ) -// CommandFunc defines the signature of a command function with arguments. -type CommandFunc func(args []string) error +// CommandFunc defines the signature of a callback function implementing a command. +type CommandFunc func(ctx *CommandContext, args []string) error // CommandRegistry holds the list of available commands. type CommandRegistry struct { @@ -26,7 +26,7 @@ func (r *CommandRegistry) Register(name string, fn CommandFunc) { } // Execute parses and runs a command chain, supporting arguments and chaining. -func (r *CommandRegistry) Execute(commandStr string) error { +func (r *CommandRegistry) Execute(ctx *CommandContext, commandStr string) error { // Split the input into chains of commands commandChains := parseCommandChain(commandStr) @@ -43,7 +43,7 @@ func (r *CommandRegistry) Execute(commandStr string) error { if cmd, exists := r.commands[commandName]; exists { // Execute the command with arguments - err := cmd(args) + err := cmd(ctx, args) if err != nil { return fmt.Errorf("Error executing command '%s': %v", commandName, err) } @@ -56,11 +56,11 @@ func (r *CommandRegistry) Execute(commandStr string) error { } // ExecuteChain allows executing multiple commands separated by ';' -func (r *CommandRegistry) ExecuteChain(commandChain string) error { +func (r *CommandRegistry) ExecuteChain(ctx *CommandContext, commandChain string) error { commands := strings.Split(commandChain, ";") for _, cmd := range commands { cmd = strings.TrimSpace(cmd) - if err := r.Execute(cmd); err != nil { + if err := r.Execute(ctx, cmd); err != nil { return err } } diff --git a/commands/registry_test.go b/commands/registry_test.go index 7a637d0..0598b23 100644 --- a/commands/registry_test.go +++ b/commands/registry_test.go @@ -9,13 +9,14 @@ import ( func TestRegisterAndExecuteCommand(t *testing.T) { registry := NewRegistry() + ctx := &CommandContext{} // Track if the command was called wasCalledA := false wasCalledB := false // Register a simple command that logs the first argument - registry.Register("log", func(args []string) error { + registry.Register("log", func(ctx *CommandContext, args []string) error { if len(args) > 0 { wasCalledA = true return nil @@ -25,14 +26,14 @@ func TestRegisterAndExecuteCommand(t *testing.T) { }) // Test executing a valid command - err := registry.Execute("log 'test message'") + err := registry.Execute(ctx, "log 'test message'") assert.NoError(t, err, "Command 'log' with argument should execute without error") assert.True(t, wasCalledA, "Command 'log' success case should have been called") assert.False(t, wasCalledB, "Command 'log' failure case should not have been called") wasCalledA = false wasCalledB = false - err = registry.Execute("log") + err = registry.Execute(ctx, "log") assert.Error(t, err, "Command 'log' without argument should execute with error") assert.False(t, wasCalledA, "Command 'log' success case should not have been called") assert.True(t, wasCalledB, "Command 'log' failure case should have been called") @@ -40,18 +41,20 @@ func TestRegisterAndExecuteCommand(t *testing.T) { func TestExecuteNonExistentCommand(t *testing.T) { registry := NewRegistry() + ctx := &CommandContext{} // Test executing a command that does not exist - err := registry.Execute("nonexistent") + err := registry.Execute(ctx, "nonexistent") assert.Error(t, err, "Should return error when executing a non-existent command") assert.Contains(t, err.Error(), "Command 'nonexistent' not found", "Error message should indicate missing command") } func TestCommandWithArguments(t *testing.T) { registry := NewRegistry() + ctx := &CommandContext{} // Register a command that expects an argument - registry.Register("log", func(args []string) error { + registry.Register("log", func(ctx *CommandContext, args []string) error { if len(args) > 0 && args[0] == "hello" { return nil } @@ -59,48 +62,50 @@ func TestCommandWithArguments(t *testing.T) { }) // Test command with correct argument - err := registry.Execute("log 'hello'") + err := registry.Execute(ctx, "log 'hello'") assert.NoError(t, err, "Command with correct argument should execute without error") // Test command with wrong argument - err = registry.Execute("log 'wrong'") + err = registry.Execute(ctx, "log 'wrong'") assert.Error(t, err, "Command with wrong argument should return an error") } func TestCommandChaining(t *testing.T) { registry := NewRegistry() + ctx := &CommandContext{} // Register a couple of commands - registry.Register("first", func(args []string) error { + registry.Register("first", func(ctx *CommandContext, args []string) error { return nil }) - registry.Register("second", func(args []string) error { + registry.Register("second", func(ctx *CommandContext, args []string) error { return nil }) // Test valid command chaining - err := registry.ExecuteChain("first; second") + err := registry.ExecuteChain(ctx, "first; second") assert.NoError(t, err, "Command chain should execute all commands without error") // Test chaining with an invalid command - err = registry.ExecuteChain("first; nonexistent; second") + err = registry.ExecuteChain(ctx, "first; nonexistent; second") assert.Error(t, err, "Command chain should return error if one command is invalid") // Test valid command with arguments in chaining - registry.Register("log", func(args []string) error { + registry.Register("log", func(ctx *CommandContext, args []string) error { if len(args) > 0 && args[0] == "message" { return nil } return fmt.Errorf("unexpected argument") }) - err = registry.ExecuteChain("log 'message'; first") + err = registry.ExecuteChain(ctx, "log 'message'; first") assert.NoError(t, err, "Command chain with arguments should execute without error") // Test chaining commands with mixed valid and invalid arguments - err = registry.ExecuteChain("log 'message'; log 'wrong'; first") + err = registry.ExecuteChain(ctx, "log 'message'; log 'wrong'; first") assert.Error(t, err, "Command chain with one invalid argument should return error") } + func TestParseCommandLine(t *testing.T) { // Test parsing command with no arguments result := parseCommandChain("log") From 2eb8397a6ca6ec12ac9ffe595cfc6a17256e94a8 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Tue, 15 Oct 2024 17:59:34 +0200 Subject: [PATCH 26/27] [gui/commands] t-c is in charge of inputs now, implement a bunch of previous code as commands --- commands/context.go | 2 +- commands/registry.go | 12 ++++ go.sum | 18 ------ gui.go | 9 ++- gui_commands.go | 121 +++++++++++++++++++++++++++++++++++++++ gui_commands_test.go | 54 ++++++++++++++++++ gui_handlers.go | 131 +++++-------------------------------------- stmps.go | 7 ++- 8 files changed, 216 insertions(+), 138 deletions(-) create mode 100644 gui_commands.go create mode 100644 gui_commands_test.go diff --git a/commands/context.go b/commands/context.go index 3936561..a49982d 100644 --- a/commands/context.go +++ b/commands/context.go @@ -3,7 +3,7 @@ package commands import "github.com/spezifisch/stmps/logger" type CommandContext struct { - Logger *logger.Logger + Logger logger.LoggerInterface CurrentPage string // Other UI or state fields } diff --git a/commands/registry.go b/commands/registry.go index d44a55a..065682a 100644 --- a/commands/registry.go +++ b/commands/registry.go @@ -25,6 +25,18 @@ func (r *CommandRegistry) Register(name string, fn CommandFunc) { r.commands[name] = fn } +// Get returns the command function and a boolean indicating if the command exists. +func (r *CommandRegistry) Get(commandName string) (CommandFunc, bool) { + cmd, exists := r.commands[commandName] + return cmd, exists +} + +// CommandExists is a small wrapper function to extract the "exists" boolean. +func (r *CommandRegistry) CommandExists(commandName string) bool { + _, exists := r.Get(commandName) + return exists +} + // Execute parses and runs a command chain, supporting arguments and chaining. func (r *CommandRegistry) Execute(ctx *CommandContext, commandStr string) error { // Split the input into chains of commands diff --git a/go.sum b/go.sum index df9c302..ce1e6ec 100644 --- a/go.sum +++ b/go.sum @@ -51,24 +51,6 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spezifisch/tview-command v0.0.0-20241013143719-94366d6323e2 h1:rhNWDM0v9HbwuF5I8wvOW3bsCdiZ1KRnp7uvhp3Jw+Y= -github.com/spezifisch/tview-command v0.0.0-20241013143719-94366d6323e2/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= -github.com/spezifisch/tview-command v0.0.0-20241013164823-5fc036e6cbb9 h1:pnSN+If3/EaRVpd75ukcpGOrnHkxvszI0wV6wIXQsnc= -github.com/spezifisch/tview-command v0.0.0-20241013164823-5fc036e6cbb9/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= -github.com/spezifisch/tview-command v0.0.0-20241013170116-60d3122b9f50 h1:OUdI2lBFJqZ4lBYfFrMh8mg2JqEwEOJaLfpaxPFsmRg= -github.com/spezifisch/tview-command v0.0.0-20241013170116-60d3122b9f50/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= -github.com/spezifisch/tview-command v0.0.0-20241013173643-6f725a86244f h1:eg0T1W7EDG8UG3R4qy23QRDuFL8/PrISE0xh0Z9zFeo= -github.com/spezifisch/tview-command v0.0.0-20241013173643-6f725a86244f/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= -github.com/spezifisch/tview-command v0.0.0-20241013173854-23b02704223f h1:6KzEn8whFBcnVLuZU7sftvS4o8hkdh8520TU5ymD8Ls= -github.com/spezifisch/tview-command v0.0.0-20241013173854-23b02704223f/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= -github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289 h1:tYxRaxvhmXvJpOqxAt3AImUEqHtMFUfrztOSbTqn7ZI= -github.com/spezifisch/tview-command v0.0.0-20241013193408-213554cd0289/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= -github.com/spezifisch/tview-command v0.0.0-20241013195600-86c4ba9c4c88 h1:Ck4VeJ+nT0iSc708iwqjTwhaTNTTFxmNilEXVmze6n8= -github.com/spezifisch/tview-command v0.0.0-20241013195600-86c4ba9c4c88/go.mod h1:BmHPVRuS00KaY6eP3VAoPJVlfN0Fulajx3Dw9CwKfFw= -github.com/spezifisch/tview-command v0.0.0-20241014224340-cf19ddd0dd54 h1:d0haUdxW02V9MTO+8njIOSlEKjA+/JeXrqPLyx5J2sQ= -github.com/spezifisch/tview-command v0.0.0-20241014224340-cf19ddd0dd54/go.mod h1:ctJ/3e6RYpK+O9M1kFxkUFInR13DOeQmik5wZiJ5WNk= -github.com/spezifisch/tview-command v0.0.0-20241014230331-c898ad9c68e5 h1:fO6M7R3MhatDGNuTzMLENoP2mxO5GJVXUlHOXGs1C/Y= -github.com/spezifisch/tview-command v0.0.0-20241014230331-c898ad9c68e5/go.mod h1:ctJ/3e6RYpK+O9M1kFxkUFInR13DOeQmik5wZiJ5WNk= github.com/spezifisch/tview-command v0.0.0-20241014231909-8cf60c7b94e5 h1:pMUWJp+61LbdF6B9yb0acoescbPval2WxQ9gfFHPqJk= github.com/spezifisch/tview-command v0.0.0-20241014231909-8cf60c7b94e5/go.mod h1:ctJ/3e6RYpK+O9M1kFxkUFInR13DOeQmik5wZiJ5WNk= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= diff --git a/gui.go b/gui.go index 6579516..bc3f22d 100644 --- a/gui.go +++ b/gui.go @@ -8,6 +8,7 @@ import ( "github.com/gdamore/tcell/v2" "github.com/rivo/tview" + "github.com/spezifisch/stmps/commands" "github.com/spezifisch/stmps/logger" "github.com/spezifisch/stmps/mpvplayer" "github.com/spezifisch/stmps/remote" @@ -24,6 +25,9 @@ type Ui struct { keyConfig *tviewcommand.Config keyContextStack *tviewcommand.ContextStack + // command registry, managed by stmps (for now) + commandRegistry *commands.CommandRegistry + // top bar startStopStatus *tview.TextView playerStatus *tview.TextView @@ -85,6 +89,7 @@ const ( func InitGui( tvcomConfig *tviewcommand.Config, tvcomContextStack *tviewcommand.ContextStack, + commandRegistry *commands.CommandRegistry, indexes *[]subsonic.SubsonicIndex, connection *subsonic.SubsonicConnection, player *mpvplayer.Player, @@ -106,8 +111,9 @@ func InitGui( mprisPlayer: mprisPlayer, } + logger.Print("tc: Init") ui.keyContextStack.Push("Init") - logger.Print("Context: Init pushed to the stack") + ui.commandRegistry = commandRegistry ui.initEventLoops() @@ -198,6 +204,7 @@ func InitGui( AddItem(ui.menuWidget.Root, 1, 0, false) // add main input handler + logger.Print("tc: Adding input handler") rootFlex.SetInputCapture(ui.handlePageInput) ui.app.SetRoot(rootFlex, true). diff --git a/gui_commands.go b/gui_commands.go new file mode 100644 index 0000000..b26c3f8 --- /dev/null +++ b/gui_commands.go @@ -0,0 +1,121 @@ +package main + +import ( + "fmt" + "strconv" + "strings" + + "github.com/spezifisch/stmps/commands" +) + +// Register all commands to the registry and include the context handling +func (ui *Ui) registerCommands(registry *commands.CommandRegistry) { + // NOP + registry.Register("nop", func(ctx *commands.CommandContext, args []string) error { + return nil + }) + + // ECHO + registry.Register("echo", func(ctx *commands.CommandContext, args []string) error { + if len(args) == 0 { + return fmt.Errorf("no arguments provided") + } + + // Join the arguments and output the result + output := strings.Join(args, " ") + ctx.Logger.Print(output) + return nil + }) + + // ... + registry.Register("show-page", func(ctx *commands.CommandContext, args []string) error { + if len(args) < 1 { + return fmt.Errorf("missing page argument") + } + ui.ShowPage(args[0]) + return nil + }) + + registry.Register("quit", func(ctx *commands.CommandContext, args []string) error { + ui.Quit() + return nil + }) + + registry.Register("add-random-songs", func(ctx *commands.CommandContext, args []string) error { + randomType := "random" + if len(args) > 0 { + randomType = args[0] + } + ui.handleAddRandomSongs("", randomType) + return nil + }) + + registry.Register("clear-queue", func(ctx *commands.CommandContext, args []string) error { + ui.player.ClearQueue() + ui.queuePage.UpdateQueue() + return nil + }) + + registry.Register("pause-playback", func(ctx *commands.CommandContext, args []string) error { + if err := ui.player.Pause(); err != nil { + return err + } + return nil + }) + + registry.Register("stop-playback", func(ctx *commands.CommandContext, args []string) error { + if err := ui.player.Stop(); err != nil { + return err + } + return nil + }) + + registry.Register("adjust-volume", func(ctx *commands.CommandContext, args []string) error { + if len(args) < 1 { + return fmt.Errorf("missing volume argument") + } + volume, err := strconv.Atoi(args[0]) + if err != nil { + return err + } + if err := ui.player.AdjustVolume(volume); err != nil { + return err + } + return nil + }) + + registry.Register("seek", func(ctx *commands.CommandContext, args []string) error { + if len(args) < 1 { + return fmt.Errorf("missing seek time argument") + } + seekTime, err := strconv.Atoi(args[0]) + if err != nil { + return err + } + if err := ui.player.Seek(seekTime); err != nil { + return err + } + return nil + }) + + registry.Register("next-track", func(ctx *commands.CommandContext, args []string) error { + if err := ui.player.PlayNextTrack(); err != nil { + return err + } + ui.queuePage.UpdateQueue() + return nil + }) + + registry.Register("start-scan", func(ctx *commands.CommandContext, args []string) error { + if err := ui.connection.StartScan(); err != nil { + return err + } + return nil + }) + + registry.Register("debug-message", func(ctx *commands.CommandContext, args []string) error { + ui.logger.Print("test debug message") + ui.showMessageBox("foo bar") + return nil + }) +} diff --git a/gui_commands_test.go b/gui_commands_test.go new file mode 100644 index 0000000..065bc9f --- /dev/null +++ b/gui_commands_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "bytes" + "fmt" + "testing" + + "github.com/spezifisch/stmps/commands" + "github.com/spezifisch/stmps/logger" + "github.com/stretchr/testify/assert" +) + +func TestRegisterCommands(t *testing.T) { + ui := &Ui{} + + // Register the commands + registry := commands.NewRegistry() + ui.registerCommands(registry) + + // Command context for testing + ctx := &commands.CommandContext{} + + // Test 'nop' command + err := registry.Execute(ctx, "nop") + assert.NoError(t, err, "Command 'nop' should execute without error") + + // Capture output of the echo command using a TestLogger + var buf bytes.Buffer + ctx.Logger = &TestLogger{&buf} + + // Test 'echo' command + err = registry.Execute(ctx, "echo Hello World") + assert.NoError(t, err, "Command 'echo' should execute without error") + assert.Equal(t, "Hello World\n", buf.String(), "Command 'echo' should output the correct string") +} + +// TestLogger is a simple implementation of LoggerInterface to capture output for testing +type TestLogger struct { + buf *bytes.Buffer +} + +func (l *TestLogger) Print(s string) { + l.buf.WriteString(s + "\n") +} + +func (l *TestLogger) Printf(s string, as ...interface{}) { + l.buf.WriteString(fmt.Sprintf(s, as...)) +} + +func (l *TestLogger) PrintError(source string, err error) { + l.buf.WriteString(fmt.Sprintf("Error in %s: %v\n", source, err)) +} + +var _ logger.LoggerInterface = (*TestLogger)(nil) diff --git a/gui_handlers.go b/gui_handlers.go index 9ba5b99..4dfc9b8 100644 --- a/gui_handlers.go +++ b/gui_handlers.go @@ -4,9 +4,8 @@ package main import ( - "fmt" - "github.com/gdamore/tcell/v2" + "github.com/spezifisch/stmps/commands" "github.com/spezifisch/stmps/mpvplayer" "github.com/spezifisch/stmps/subsonic" tviewcommand "github.com/spezifisch/tview-command" @@ -21,135 +20,33 @@ func (e *MyEvent) IsCommand(name string) bool { return e.Command == name } -//<<< End of Stuff - +// <<< End of Stuff func (ui *Ui) handlePageInput(event *tcell.EventKey) *tcell.EventKey { - // we don't want any of these firing if we're trying to add a new playlist focused := ui.app.GetFocus() - if ui.playlistPage.IsNewPlaylistInputFocused(focused) || ui.browserPage.IsSearchFocused(focused) || focused == ui.searchPage.searchField || ui.selectPlaylistWidget.visible { + if ui.playlistPage.IsNewPlaylistInputFocused(focused) || + ui.browserPage.IsSearchFocused(focused) || + focused == ui.searchPage.searchField || + ui.selectPlaylistWidget.visible { return event } - // Create a tview-command event tcEvent := tviewcommand.FromEventKey(event, ui.keyConfig) - - // Look up the command based on the active context activeContext := ui.keyContextStack.Current() - if err := tcEvent.LookupCommand(activeContext); err != nil { - ui.logger.Printf("[t-c] error: %v, context: %s", err, activeContext) - //return event // HACK - } else { - // Add some logs for now - right := "" - if tcEvent.IsBound { - right = fmt.Sprintf("bound to command: %s", tcEvent.Command) - } else { - right = "unbound" - } - ui.logger.Printf("[t-c] saw an event: key '%s' in context '%v' -> %s", tcEvent.KeyName, activeContext, right) - } - - // Ensure "force-quit" command quits - myEvent := MyEvent{*tcEvent} - if myEvent.IsCommand("force-quit") { - ui.Quit() - return nil - } - - // Old stuff - switch event.Rune() { - case '1': - ui.ShowPage(PageBrowser) - - case '2': - ui.ShowPage(PageQueue) - - case '3': - ui.ShowPage(PagePlaylists) - - case '4': - ui.ShowPage(PageSearch) - - case '5': - ui.ShowPage(PageLog) - - case '?': - ui.ShowHelp() - - case 'Q': - ui.Quit() - - case 'r': - // add random songs to queue - ui.handleAddRandomSongs("", "random") - - case 'D': - // clear queue and stop playing - ui.player.ClearQueue() - ui.queuePage.UpdateQueue() - case 'p': - // toggle playing/pause - err := ui.player.Pause() - if err != nil { - ui.logger.PrintError("handlePageInput: Pause", err) + if err := tcEvent.LookupCommand(activeContext); err == nil && tcEvent.IsBound { + ctx := &commands.CommandContext{ + Logger: ui.logger, + CurrentPage: activeContext, } - case 'P': - // stop playing without changes to queue - ui.logger.Print("key stop") - err := ui.player.Stop() + err := ui.commandRegistry.Execute(ctx, tcEvent.Command) if err != nil { - ui.logger.PrintError("handlePageInput: Stop", err) - } - - case 'X': - // debug stuff - ui.logger.Print("test") - //ui.player.Test() - ui.showMessageBox("foo bar") - - case '-': - // volume- - if err := ui.player.AdjustVolume(-5); err != nil { - ui.logger.PrintError("handlePageInput: AdjustVolume-", err) - } - - case '+', '=': - // volume+ - if err := ui.player.AdjustVolume(5); err != nil { - ui.logger.PrintError("handlePageInput: AdjustVolume+", err) - } - - case '.': - // << - if err := ui.player.Seek(10); err != nil { - ui.logger.PrintError("handlePageInput: Seek+", err) - } - - case ',': - // >> - if err := ui.player.Seek(-10); err != nil { - ui.logger.PrintError("handlePageInput: Seek-", err) - } - - case '>': - // skip to next track - if err := ui.player.PlayNextTrack(); err != nil { - ui.logger.PrintError("handlePageInput: Next", err) + ui.logger.PrintError("t-c command execution", err) } - ui.queuePage.UpdateQueue() - - case 's': - if err := ui.connection.StartScan(); err != nil { - ui.logger.PrintError("startScan:", err) - } - - default: - return event + return nil } - return nil + return event // Pass event back if no command was handled } func (ui *Ui) ShowPage(name string) { diff --git a/stmps.go b/stmps.go index 97f3dc6..660513e 100644 --- a/stmps.go +++ b/stmps.go @@ -12,6 +12,7 @@ import ( "runtime" "runtime/pprof" + "github.com/spezifisch/stmps/commands" "github.com/spezifisch/stmps/logger" "github.com/spezifisch/stmps/mpvplayer" "github.com/spezifisch/stmps/remote" @@ -154,9 +155,12 @@ func main() { osExit(3) } - // init the context stack + // init the context stack (context-sensitive keybindings) tvcomContextStack := tviewcommand.NewContextStack() + // init command registry (command parser and executor) + commandRegistry := commands.NewRegistry() + // init mpv engine player, err := mpvplayer.NewPlayer(logger) if err != nil { @@ -252,6 +256,7 @@ func main() { ui := InitGui( tvcomConfig, tvcomContextStack, + commandRegistry, &indexResponse.Indexes.Index, connection, player, From eea809a5ed551bab70d069e7178ac21c3a790544 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Tue, 15 Oct 2024 18:09:34 +0200 Subject: [PATCH 27/27] [config] start with some commands --- HACK.commands.toml | 27 +++++++++++++++++++++------ gui.go | 4 ++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/HACK.commands.toml b/HACK.commands.toml index 4b9dd46..f3b7b44 100644 --- a/HACK.commands.toml +++ b/HACK.commands.toml @@ -1,20 +1,35 @@ -# This tvcom test config has different levels of inheritance to try out. - [Global.settings] silent = true [Global.bindings] -CTRL-Z = "tvcomToggleDebugWidget" +context_add = "default" +CTRL-Z = "tvcomToggleDebugWidget" # Binds to debug toggle +Q = "force-quit" # Quit command [Default.bindings] +1 = "show-page browser" +2 = "show-page queue" +3 = "show-page playlists" +4 = "show-page search" +5 = "show-page log" +r = "add-random-songs random" +D = "clear-queue" +p = "pause-playback" +P = "stop-playback" +- = "adjust-volume -5" +"+" = "adjust-volume 5" +"." = "seek 10" +"," = "seek -10" +">" = "next-track" +s = "start-scan" CTRL-L = "log '^L pressed!'" +[QueuePage.bindings] +# inherits Default + [Init] # clear inherited Default bindings context_override = ["Empty"] # Init: add key bindings only valid *during* program startup [Init.bindings] q = "force-quit" - -[QueuePage.bindings] -# inherits Default diff --git a/gui.go b/gui.go index bc3f22d..f18932a 100644 --- a/gui.go +++ b/gui.go @@ -203,6 +203,10 @@ func InitGui( AddItem(ui.pages, 0, 1, true). AddItem(ui.menuWidget.Root, 1, 0, false) + // add commands + logger.Print("tc: Adding Ui commands") + ui.registerCommands(ui.commandRegistry) + // add main input handler logger.Print("tc: Adding input handler") rootFlex.SetInputCapture(ui.handlePageInput)