Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] tview-command integration (fixes #31) #64

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5e88925
[deps] update tview-command to latest
spezifisch Oct 13, 2024
85d3e0a
[tvcom] update, builds
spezifisch Oct 13, 2024
162c4a9
[main] make it more testable, add some simple tests
spezifisch Oct 13, 2024
73e4452
[main/test] fix tests, reorder main so that it's more testable
spezifisch Oct 13, 2024
185d23e
[test] ensure proper exit was taken
spezifisch Oct 13, 2024
de674c9
[main/gui] add tvcom setup and a simple test keybinding config
spezifisch Oct 13, 2024
c80f6c5
[gui] add some first easy key-contexts
spezifisch Oct 13, 2024
fd3efb2
[main] fix #70
spezifisch Oct 14, 2024
a720e61
[test] fix flag state not reset
spezifisch Oct 14, 2024
9505aba
[test] check result for either nominal error "tried config not found"…
spezifisch Oct 14, 2024
7ce1e7b
[main] specify exit codes, fix tests
spezifisch Oct 14, 2024
668e029
[main] fix #70
spezifisch Oct 14, 2024
0232a30
[main] specify exit codes, fix tests
spezifisch Oct 14, 2024
617c588
[tests] further painful cherry-picks to backport this
spezifisch Oct 14, 2024
3af0a33
[main] make it more testable, add some simple tests
spezifisch Oct 13, 2024
a514a3d
[main/test] fix tests, reorder main so that it's more testable
spezifisch Oct 13, 2024
efb42d0
[test] ensure proper exit was taken
spezifisch Oct 13, 2024
cbb11cb
[main/gui] add tvcom setup and a simple test keybinding config
spezifisch Oct 13, 2024
9f1f1e8
[gui] add some first easy key-contexts
spezifisch Oct 13, 2024
8dd60e3
[main] specify exit codes, fix tests
spezifisch Oct 14, 2024
9e48424
[git] rebase complete
spezifisch Oct 14, 2024
36b14a0
Merge branch 'dev-spezifisch' of github.com:spezifisch/stmps into dev…
spezifisch Oct 14, 2024
4934d0a
[gui] tview-command is running along now, parsing keys to commands fr…
spezifisch Oct 14, 2024
65492a6
[deps] pull in new t-c (working lookups are logged in log page, try C…
spezifisch Oct 14, 2024
0f0be60
[commands] implement command registry
spezifisch Oct 15, 2024
0e29de4
[commands] add CommandContext
spezifisch Oct 15, 2024
2eb8397
[gui/commands] t-c is in charge of inputs now, implement a bunch of p…
spezifisch Oct 15, 2024
eea809a
[config] start with some commands
spezifisch Oct 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions HACK.commands.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,34 @@
silent = true

[Global.bindings]
context_add = "default"
CTRL-Z = "tvcomToggleDebugWidget" # Binds to debug toggle
Q = "force-quit" # Quit command

[Default.bindings]
d = "deleteSelectedTrack"
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!'"

[Empty.bindings]
# context with no bindings
[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"
9 changes: 9 additions & 0 deletions commands/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package commands

import "github.com/spezifisch/stmps/logger"

type CommandContext struct {
Logger logger.LoggerInterface
CurrentPage string
// Other UI or state fields
}
133 changes: 133 additions & 0 deletions commands/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package commands

import (
"fmt"
"strings"
)

// 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 {
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
}

// 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
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(ctx, 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(ctx *CommandContext, commandChain string) error {
commands := strings.Split(commandChain, ";")
for _, cmd := range commands {
cmd = strings.TrimSpace(cmd)
if err := r.Execute(ctx, 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
}
152 changes: 152 additions & 0 deletions commands/registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package commands

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

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(ctx *CommandContext, 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(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(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")
}

func TestExecuteNonExistentCommand(t *testing.T) {
registry := NewRegistry()
ctx := &CommandContext{}

// Test executing a command that does not exist
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(ctx *CommandContext, 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(ctx, "log 'hello'")
assert.NoError(t, err, "Command with correct argument should execute without error")

// Test command with wrong argument
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(ctx *CommandContext, args []string) error {
return nil
})
registry.Register("second", func(ctx *CommandContext, args []string) error {
return nil
})

// Test valid command chaining
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(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(ctx *CommandContext, args []string) error {
if len(args) > 0 && args[0] == "message" {
return nil
}
return fmt.Errorf("unexpected argument")
})

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(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")
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")
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
)

require (
github.com/spezifisch/tview-command v0.0.0-20241013143719-94366d6323e2
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
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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-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-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=
Expand Down
Loading