summaryrefslogtreecommitdiff
path: root/internal/hexaiaction
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-18 07:45:37 +0300
committerPaul Buetow <paul@buetow.org>2026-06-18 07:45:37 +0300
commit4ffb22e7f69f1c9c79b095d4e60bad3d97aac55b (patch)
tree2dc708cbb95975d34084eb5afd376871caa13e27 /internal/hexaiaction
parentece7dcfd232b780f5650326c8ac2379ca70387d4 (diff)
ik0 replace test seams with dependency injection
Diffstat (limited to 'internal/hexaiaction')
-rw-r--r--internal/hexaiaction/cmdentry.go54
-rw-r--r--internal/hexaiaction/cmdentry_runcommand_test.go18
-rw-r--r--internal/hexaiaction/cmdentry_test.go36
-rw-r--r--internal/hexaiaction/custom_action_test.go9
-rw-r--r--internal/hexaiaction/run.go28
-rw-r--r--internal/hexaiaction/tui.go12
-rw-r--r--internal/hexaiaction/tui_config_test.go9
-rw-r--r--internal/hexaiaction/tui_custom.go22
-rw-r--r--internal/hexaiaction/tui_custom_test.go9
9 files changed, 121 insertions, 76 deletions
diff --git a/internal/hexaiaction/cmdentry.go b/internal/hexaiaction/cmdentry.go
index 85e9059..ec4970c 100644
--- a/internal/hexaiaction/cmdentry.go
+++ b/internal/hexaiaction/cmdentry.go
@@ -25,23 +25,47 @@ type Options struct {
// RunCommand is the CLI orchestrator used by cmd/hexai-tmux-action. It runs in tmux
// split-pane mode by default, or child mode when -ui-child is set.
func RunCommand(ctx context.Context, opts Options, stdin io.Reader, stdout, stderr io.Writer) error {
+ return commandRunner{}.RunCommand(ctx, opts, stdin, stdout, stderr)
+}
+
+type commandRunner struct {
+ popupRun func(tmux.PopupOpts, []string) error
+ osExecutable func() (string, error)
+ run func(context.Context, io.Reader, io.Writer, io.Writer) error
+}
+
+func (r commandRunner) popup(opts tmux.PopupOpts, argv []string) error {
+ if r.popupRun != nil {
+ return r.popupRun(opts, argv)
+ }
+ return tmux.PopupRun(opts, argv)
+}
+
+func (r commandRunner) executable() (string, error) {
+ if r.osExecutable != nil {
+ return r.osExecutable()
+ }
+ return os.Executable()
+}
+
+func (r commandRunner) runAction(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error {
+ if r.run != nil {
+ return r.run(ctx, stdin, stdout, stderr)
+ }
+ return Run(ctx, stdin, stdout, stderr)
+}
+
+func (r commandRunner) RunCommand(ctx context.Context, opts Options, stdin io.Reader, stdout, stderr io.Writer) error {
if err := llm.RegisterAllProviders(); err != nil {
return fmt.Errorf("failed to register LLM providers: %w", err)
}
if opts.UIChild {
- return runChild(ctx, opts.Infile, opts.Outfile, stdout, stderr)
+ return r.runChild(ctx, opts.Infile, opts.Outfile, stdout, stderr)
}
// Always use tmux popup path
- return runInTmuxParent(ctx, stdin, stdout, opts.TmuxTarget, opts.TmuxPopupWidth, opts.TmuxPopupHeight)
+ return r.runInTmuxParent(ctx, stdin, stdout, opts.TmuxTarget, opts.TmuxPopupWidth, opts.TmuxPopupHeight)
}
-// seams for unit tests
-var (
- popupRunFn = tmux.PopupRun
- osExecutableFn = os.Executable
- runFn = Run
-)
-
// openIO returns readers/writers for infile/outfile flags with deferred closers.
func openIO(infile, outfile string) (io.Reader, io.Writer, func(), func(), error) {
in := io.Reader(os.Stdin)
@@ -68,7 +92,7 @@ func openIO(infile, outfile string) (io.Reader, io.Writer, func(), func(), error
}
// runChild runs the interactive flow and writes the final output atomically when outfile is set.
-func runChild(ctx context.Context, infile, outfile string, stdout, stderr io.Writer) error {
+func (r commandRunner) runChild(ctx context.Context, infile, outfile string, stdout, stderr io.Writer) error {
if outfile == "" {
// No atomic handoff needed; just run normally to provided stdout
var in io.Reader = os.Stdin
@@ -80,7 +104,7 @@ func runChild(ctx context.Context, infile, outfile string, stdout, stderr io.Wri
defer func() { _ = f.Close() }()
in = f
}
- return runFn(ctx, in, stdout, stderr)
+ return r.runAction(ctx, in, stdout, stderr)
}
tmp := outfile + ".tmp"
in, out, closeIn, closeOut, err := openIO(infile, tmp)
@@ -88,7 +112,7 @@ func runChild(ctx context.Context, infile, outfile string, stdout, stderr io.Wri
return err
}
defer closeIn()
- if err := runFn(ctx, in, out, stderr); err != nil {
+ if err := r.runAction(ctx, in, out, stderr); err != nil {
closeOut()
if copyErr := echoThrough(infile, tmp, os.Stdin, stdout); copyErr != nil {
// Wrap the primary child error with %w so callers can inspect it
@@ -102,7 +126,7 @@ func runChild(ctx context.Context, infile, outfile string, stdout, stderr io.Wri
return os.Rename(tmp, outfile)
}
-func runInTmuxParent(ctx context.Context, stdin io.Reader, stdout io.Writer, target, popupWidth, popupHeight string) error {
+func (r commandRunner) runInTmuxParent(ctx context.Context, stdin io.Reader, stdout io.Writer, target, popupWidth, popupHeight string) error {
dir, err := os.MkdirTemp("", "hexai-tmux-action-")
if err != nil {
return err
@@ -113,13 +137,13 @@ func runInTmuxParent(ctx context.Context, stdin io.Reader, stdout io.Writer, tar
if err := persistStdin(inPath, stdin); err != nil {
return err
}
- exe, err := osExecutableFn()
+ exe, err := r.executable()
if err != nil {
return err
}
argv := []string{exe, "-ui-child", "-infile", inPath, "-outfile", outPath}
opts := tmux.PopupOpts{Target: target, Width: popupWidth, Height: popupHeight}
- if err := popupRunFn(opts, argv); err != nil {
+ if err := r.popup(opts, argv); err != nil {
return err
}
if err := waitForFile(ctx, outPath, 60*time.Second); err != nil {
diff --git a/internal/hexaiaction/cmdentry_runcommand_test.go b/internal/hexaiaction/cmdentry_runcommand_test.go
index ac6106d..b71a0f2 100644
--- a/internal/hexaiaction/cmdentry_runcommand_test.go
+++ b/internal/hexaiaction/cmdentry_runcommand_test.go
@@ -16,14 +16,12 @@ func TestRunCommand_UIChild(t *testing.T) {
in := filepath.Join(dir, "in.txt")
out := filepath.Join(dir, "out.txt")
_ = os.WriteFile(in, []byte("sel"), 0o600)
- old := runFn
- runFn = func(_ context.Context, _ io.Reader, w io.Writer, _ io.Writer) error {
+ r := commandRunner{run: func(_ context.Context, _ io.Reader, w io.Writer, _ io.Writer) error {
_, _ = io.WriteString(w, "OK")
return nil
- }
- t.Cleanup(func() { runFn = old })
+ }}
opts := Options{Infile: in, Outfile: out, UIChild: true}
- if err := RunCommand(context.Background(), opts, bytes.NewBuffer(nil), io.Discard, io.Discard); err != nil {
+ if err := r.RunCommand(context.Background(), opts, bytes.NewBuffer(nil), io.Discard, io.Discard); err != nil {
t.Fatalf("RunCommand UIChild: %v", err)
}
b, _ := os.ReadFile(out)
@@ -33,10 +31,9 @@ func TestRunCommand_UIChild(t *testing.T) {
}
func TestRunCommand_Tmux(t *testing.T) {
- oldExec := osExecutableFn
- oldPopup := popupRunFn
- osExecutableFn = func() (string, error) { return "/bin/hexai-tmux-action", nil }
- popupRunFn = func(_ tmux.PopupOpts, argv []string) error {
+ r := commandRunner{}
+ r.osExecutable = func() (string, error) { return "/bin/hexai-tmux-action", nil }
+ r.popupRun = func(_ tmux.PopupOpts, argv []string) error {
for i := 0; i < len(argv)-1; i++ {
if argv[i] == "-outfile" && i+1 < len(argv) {
_ = os.WriteFile(argv[i+1], []byte("OUT"), 0o600)
@@ -45,9 +42,8 @@ func TestRunCommand_Tmux(t *testing.T) {
}
return nil
}
- defer func() { osExecutableFn = oldExec; popupRunFn = oldPopup }()
var out bytes.Buffer
- if err := RunCommand(context.Background(), Options{}, bytes.NewBufferString("X"), &out, io.Discard); err != nil {
+ if err := r.RunCommand(context.Background(), Options{}, bytes.NewBufferString("X"), &out, io.Discard); err != nil {
t.Fatalf("RunCommand tmux: %v", err)
}
if out.String() != "OUT" {
diff --git a/internal/hexaiaction/cmdentry_test.go b/internal/hexaiaction/cmdentry_test.go
index b9d5e9b..99805ca 100644
--- a/internal/hexaiaction/cmdentry_test.go
+++ b/internal/hexaiaction/cmdentry_test.go
@@ -77,10 +77,8 @@ func TestRunInTmuxParent_Stubbed(t *testing.T) {
_ = w.Close()
// capture stdout
rout, wout, _ := os.Pipe()
- oldExec := osExecutableFn
- oldPopup := popupRunFn
- osExecutableFn = func() (string, error) { return "/bin/hexai-tmux-action", nil }
- popupRunFn = func(opts tmux.PopupOpts, argv []string) error {
+ runner := commandRunner{osExecutable: func() (string, error) { return "/bin/hexai-tmux-action", nil }}
+ runner.popupRun = func(opts tmux.PopupOpts, argv []string) error {
for i := 0; i < len(argv)-1; i++ {
if argv[i] == "-outfile" && i+1 < len(argv) {
_ = os.WriteFile(argv[i+1], []byte("OUT:"+strings.Join(argv, ",")), 0o600)
@@ -89,8 +87,7 @@ func TestRunInTmuxParent_Stubbed(t *testing.T) {
}
return nil
}
- t.Cleanup(func() { osExecutableFn = oldExec; popupRunFn = oldPopup })
- if err := runInTmuxParent(context.Background(), r, wout, "", "", ""); err != nil {
+ if err := runner.runInTmuxParent(context.Background(), r, wout, "", "", ""); err != nil {
t.Fatalf("runInTmuxParent: %v", err)
}
_ = wout.Close()
@@ -102,27 +99,24 @@ func TestRunInTmuxParent_Stubbed(t *testing.T) {
}
func TestRunInTmuxParent_ExecutableError(t *testing.T) {
- old := osExecutableFn
- osExecutableFn = func() (string, error) { return "", fmt.Errorf("no exe") }
- t.Cleanup(func() { osExecutableFn = old })
+ runner := commandRunner{osExecutable: func() (string, error) { return "", fmt.Errorf("no exe") }}
r, w, _ := os.Pipe()
_, _ = w.Write([]byte("x"))
_ = w.Close()
- if err := runInTmuxParent(context.Background(), r, io.Discard, "", "", ""); err == nil {
+ if err := runner.runInTmuxParent(context.Background(), r, io.Discard, "", "", ""); err == nil {
t.Fatal("expected error from missing executable")
}
}
func TestRunInTmuxParent_PopupError(t *testing.T) {
- oldExec := osExecutableFn
- osExecutableFn = func() (string, error) { return "/bin/hexai-tmux-action", nil }
- oldPopup := popupRunFn
- popupRunFn = func(_ tmux.PopupOpts, _ []string) error { return fmt.Errorf("popup failed") }
- t.Cleanup(func() { osExecutableFn = oldExec; popupRunFn = oldPopup })
+ runner := commandRunner{
+ osExecutable: func() (string, error) { return "/bin/hexai-tmux-action", nil },
+ popupRun: func(_ tmux.PopupOpts, _ []string) error { return fmt.Errorf("popup failed") },
+ }
r, w, _ := os.Pipe()
_, _ = w.Write([]byte("x"))
_ = w.Close()
- if err := runInTmuxParent(context.Background(), r, io.Discard, "", "", ""); err == nil {
+ if err := runner.runInTmuxParent(context.Background(), r, io.Discard, "", "", ""); err == nil {
t.Fatal("expected popup error")
}
}
@@ -133,13 +127,11 @@ func TestRunChild_StdoutAndOutfile(t *testing.T) {
in := filepath.Join(dir, "in.txt")
out := filepath.Join(dir, "out.txt")
_ = os.WriteFile(in, []byte("sel"), 0o600)
- oldRun := runFn
- runFn = func(_ context.Context, _ io.Reader, w io.Writer, _ io.Writer) error {
+ runner := commandRunner{run: func(_ context.Context, _ io.Reader, w io.Writer, _ io.Writer) error {
_, _ = io.WriteString(w, "RESULT")
return nil
- }
- t.Cleanup(func() { runFn = oldRun })
- if err := runChild(context.Background(), in, out, io.Discard, io.Discard); err != nil {
+ }}
+ if err := runner.runChild(context.Background(), in, out, io.Discard, io.Discard); err != nil {
t.Fatalf("runChild: %v", err)
}
b, _ := os.ReadFile(out)
@@ -148,7 +140,7 @@ func TestRunChild_StdoutAndOutfile(t *testing.T) {
}
// Stdout mode
r, w, _ := os.Pipe()
- if err := runChild(context.Background(), in, "", w, io.Discard); err != nil {
+ if err := runner.runChild(context.Background(), in, "", w, io.Discard); err != nil {
t.Fatalf("runChild: %v", err)
}
_ = w.Close()
diff --git a/internal/hexaiaction/custom_action_test.go b/internal/hexaiaction/custom_action_test.go
index e2f7902..7bda0ad 100644
--- a/internal/hexaiaction/custom_action_test.go
+++ b/internal/hexaiaction/custom_action_test.go
@@ -3,11 +3,9 @@ package hexaiaction
import (
"bytes"
"context"
- "os"
"testing"
"codeberg.org/snonux/hexai/internal/appconfig"
- "codeberg.org/snonux/hexai/internal/editor"
"codeberg.org/snonux/hexai/internal/llm"
)
@@ -29,12 +27,9 @@ func TestActionCustom_UsesEditorPrompt(t *testing.T) {
}
runner.newClient = func(_ appconfig.App) (actionClient, error) { return llmFake2{}, nil }
- oldRunEd := editor.RunEditor
- editor.RunEditor = func(_ context.Context, _ string, path string) error {
- return os.WriteFile(path, []byte("make it done"), 0o600)
+ runner.openEditor = func(context.Context, []byte) (string, error) {
+ return "make it done", nil
}
- t.Cleanup(func() { editor.RunEditor = oldRunEd })
- t.Setenv("HEXAI_EDITOR", "dummy")
in := bytes.NewBufferString("some code")
var out bytes.Buffer
diff --git a/internal/hexaiaction/run.go b/internal/hexaiaction/run.go
index f0ce7ee..8b78bd0 100644
--- a/internal/hexaiaction/run.go
+++ b/internal/hexaiaction/run.go
@@ -66,6 +66,8 @@ type actionClientFactory func(cfg appconfig.App) (actionClient, error)
type actionConfigLoader func(context.Context, *log.Logger) appconfig.App
+type actionEditorOpener func(context.Context, []byte) (string, error)
+
type actionStatusSink interface {
SetLLMStart(provider, model string) error
}
@@ -75,6 +77,7 @@ type Runner struct {
chooseAction actionChooser
newClient actionClientFactory
loadConfig actionConfigLoader
+ openEditor actionEditorOpener
statusSink actionStatusSink
}
@@ -84,6 +87,7 @@ func NewRunner() *Runner {
chooseAction: chooseActionFromConfig,
newClient: defaultActionClientFactory,
loadConfig: loadActionConfig,
+ openEditor: editor.OpenTempAndEdit,
statusSink: tmuxActionStatusSink{},
}
}
@@ -117,6 +121,22 @@ func loadActionConfig(ctx context.Context, logger *log.Logger) appconfig.App {
return appconfig.LoadWithOptions(ctx, logger, appconfig.LoadOptions{ConfigPath: configPathFromContext(ctx)})
}
+type actionEditorKey struct{}
+
+func withActionEditor(ctx context.Context, open actionEditorOpener) context.Context {
+ if open == nil {
+ open = editor.OpenTempAndEdit
+ }
+ return context.WithValue(ctx, actionEditorKey{}, open)
+}
+
+func actionEditorFromContext(ctx context.Context) actionEditorOpener {
+ if open, ok := ctx.Value(actionEditorKey{}).(actionEditorOpener); ok && open != nil {
+ return open
+ }
+ return editor.OpenTempAndEdit
+}
+
type actionPlan struct {
fallback string
run func(context.Context) (string, error)
@@ -154,6 +174,7 @@ func (r *Runner) Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Wri
chooser := chooseActionFromConfig
newClient := defaultActionClientFactory
loadConfig := loadActionConfig
+ openEditor := actionEditorOpener(editor.OpenTempAndEdit)
statusSink := actionStatusSink(tmuxActionStatusSink{})
if r != nil {
if r.chooseAction != nil {
@@ -165,6 +186,9 @@ func (r *Runner) Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Wri
if r.loadConfig != nil {
loadConfig = r.loadConfig
}
+ if r.openEditor != nil {
+ openEditor = r.openEditor
+ }
if r.statusSink != nil {
statusSink = r.statusSink
}
@@ -208,7 +232,7 @@ func (r *Runner) Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Wri
if err != nil {
return err
}
- out, err := executeAction(ctx, choice.kind, parts, &cfg, client, stderr, choice.custom)
+ out, err := executeAction(withActionEditor(ctx, openEditor), choice.kind, parts, &cfg, client, stderr, choice.custom)
if err != nil {
return err
}
@@ -381,7 +405,7 @@ func handleCustomAction(ctx context.Context, parts InputParts, cfg actionConfig,
}
func handleCustomPromptAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer, stderr io.Writer) (string, error) {
- prompt, err := editor.OpenTempAndEdit(ctx, nil)
+ prompt, err := actionEditorFromContext(ctx)(ctx, nil)
if err != nil || strings.TrimSpace(prompt) == "" {
_, _ = fmt.Fprintln(stderr, logging.AnsiBase+"hexai-tmux-action: custom prompt canceled or empty; echoing input"+logging.AnsiReset)
return parts.Selection, nil
diff --git a/internal/hexaiaction/tui.go b/internal/hexaiaction/tui.go
index 9155cfe..7506cdc 100644
--- a/internal/hexaiaction/tui.go
+++ b/internal/hexaiaction/tui.go
@@ -124,7 +124,11 @@ func (m model) View() string {
// RunTUI returns the chosen ActionKind from the default hardcoded menu.
func RunTUI() (ActionKind, error) {
- p := tea.NewProgram(newModel())
+ return tuiRunner{}.RunTUI()
+}
+
+func (r tuiRunner) RunTUI() (ActionKind, error) {
+ p := r.program(newModel())
md, err := p.Run()
if err != nil {
return ActionSkip, err
@@ -142,8 +146,12 @@ func RunTUI() (ActionKind, error) {
// Custom entries are resolved by ID against customs. Falls back to ActionSkip
// if the program returns an unexpected model type.
func RunTUIFromConfig(entries []appconfig.TmuxActionMenuEntry, customs []appconfig.CustomAction) (ActionKind, *appconfig.CustomAction, error) {
+ return tuiRunner{}.RunTUIFromConfig(entries, customs)
+}
+
+func (r tuiRunner) RunTUIFromConfig(entries []appconfig.TmuxActionMenuEntry, customs []appconfig.CustomAction) (ActionKind, *appconfig.CustomAction, error) {
m := newModelFromMenuEntries(entries, customs)
- p := teaNewProgram(m)
+ p := r.program(m)
md, err := p.Run()
if err != nil {
return ActionSkip, nil, err
diff --git a/internal/hexaiaction/tui_config_test.go b/internal/hexaiaction/tui_config_test.go
index e8e178f..e1723ab 100644
--- a/internal/hexaiaction/tui_config_test.go
+++ b/internal/hexaiaction/tui_config_test.go
@@ -154,19 +154,16 @@ func TestHandleKey_ChosenCustomIsSet(t *testing.T) {
}
func TestRunTUIFromConfig_ViaTmuxActionSeam(t *testing.T) {
- old := teaNewProgram
- t.Cleanup(func() { teaNewProgram = old })
-
- teaNewProgram = func(m model) teaProgram {
+ r := tuiRunner{newProgram: func(m model) teaProgram {
return fakeProg{m: m, onRun: func(mm *model) {
mm.chosen = ActionSkip
}}
- }
+ }}
entries := []appconfig.TmuxActionMenuEntry{
{Kind: "skip", Hotkey: "s"},
}
- kind, custom, err := RunTUIFromConfig(entries, nil)
+ kind, custom, err := r.RunTUIFromConfig(entries, nil)
if err != nil {
t.Fatalf("RunTUIFromConfig: %v", err)
}
diff --git a/internal/hexaiaction/tui_custom.go b/internal/hexaiaction/tui_custom.go
index 2e6561b..910242f 100644
--- a/internal/hexaiaction/tui_custom.go
+++ b/internal/hexaiaction/tui_custom.go
@@ -12,6 +12,21 @@ import (
// RunTUIWithCustom shows the main menu plus a configurable "Custom actions…" item.
// If the user selects that item, it shows a submenu listing user-defined custom actions.
func RunTUIWithCustom(customs []appconfig.CustomAction, menuHotkey string) (ActionKind, *appconfig.CustomAction, error) {
+ return tuiRunner{}.RunTUIWithCustom(customs, menuHotkey)
+}
+
+type tuiRunner struct {
+ newProgram func(model) teaProgram
+}
+
+func (r tuiRunner) program(m model) teaProgram {
+ if r.newProgram != nil {
+ return r.newProgram(m)
+ }
+ return tea.NewProgram(m)
+}
+
+func (r tuiRunner) RunTUIWithCustom(customs []appconfig.CustomAction, menuHotkey string) (ActionKind, *appconfig.CustomAction, error) {
// When no customs, fall back to default menu
if len(customs) == 0 {
kind, err := RunTUI()
@@ -28,7 +43,7 @@ func RunTUIWithCustom(customs []appconfig.CustomAction, menuHotkey string) (Acti
items = append(items, item{title: "Custom actions…", desc: "", kind: ActionCustom, hotkey: hk})
m.list.SetItems(items)
// Run main menu
- p := teaNewProgram(m)
+ p := r.program(m)
md, err := p.Run()
if err != nil {
return ActionSkip, nil, err
@@ -50,7 +65,7 @@ func RunTUIWithCustom(customs []appconfig.CustomAction, menuHotkey string) (Acti
subItems = append(subItems, item{title: ca.Title, desc: "", kind: ActionCustom, hotkey: r})
}
sub.list.SetItems(subItems)
- sp := teaNewProgram(sub)
+ sp := r.program(sub)
smd, err := sp.Run()
if err != nil {
return ActionSkip, nil, err
@@ -69,8 +84,5 @@ func RunTUIWithCustom(customs []appconfig.CustomAction, menuHotkey string) (Acti
return ActionSkip, nil, nil
}
-// teaNewProgram is a tiny seam for tests to stub bubbletea program creation.
-var teaNewProgram = func(m model) teaProgram { return tea.NewProgram(m) }
-
// teaProgram is the subset of bubbletea.Program we need; enables testing seam.
type teaProgram interface{ Run() (tea.Model, error) }
diff --git a/internal/hexaiaction/tui_custom_test.go b/internal/hexaiaction/tui_custom_test.go
index 5ded806..7f7e2d3 100644
--- a/internal/hexaiaction/tui_custom_test.go
+++ b/internal/hexaiaction/tui_custom_test.go
@@ -21,11 +21,8 @@ func (f fakeProg) Run() (tea.Model, error) {
}
func TestRunTUIWithCustom_SubmenuAndHotkeys(t *testing.T) {
- old := teaNewProgram
- t.Cleanup(func() { teaNewProgram = old })
-
calls := 0
- teaNewProgram = func(m model) teaProgram {
+ r := tuiRunner{newProgram: func(m model) teaProgram {
calls++
if calls == 1 {
// Main menu should have "Custom actions…" with configured hotkey
@@ -57,13 +54,13 @@ func TestRunTUIWithCustom_SubmenuAndHotkeys(t *testing.T) {
return fakeProg{m: m, onRun: func(mm *model) { mm.list.Select(0) }}
}
return fakeProg{m: m}
- }
+ }}
customs := []appconfig.CustomAction{
{ID: "a", Title: "A", Hotkey: "x", Instruction: "do"},
{ID: "b", Title: "B", Hotkey: "y", Instruction: "do2"},
}
- kind, selected, err := RunTUIWithCustom(customs, "z")
+ kind, selected, err := r.RunTUIWithCustom(customs, "z")
if err != nil {
t.Fatalf("RunTUIWithCustom error: %v", err)
}