summaryrefslogtreecommitdiff
path: root/internal/hexaiaction/cmdentry.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-08 12:02:40 +0300
committerPaul Buetow <paul@buetow.org>2025-09-08 12:02:40 +0300
commit75cf6abd55bfb60324fc47cf91eac08dbb8b87b4 (patch)
tree6ef90d8014fe4d9a757d3f7e95bf736b70e4c685 /internal/hexaiaction/cmdentry.go
parent0dcf347c3fbc6e4ffb7e46294f5dd92dbbcd98ef (diff)
docs: move tmux documentation to its own file
Diffstat (limited to 'internal/hexaiaction/cmdentry.go')
-rw-r--r--internal/hexaiaction/cmdentry.go252
1 files changed, 143 insertions, 109 deletions
diff --git a/internal/hexaiaction/cmdentry.go b/internal/hexaiaction/cmdentry.go
index cf72495..ca33443 100644
--- a/internal/hexaiaction/cmdentry.go
+++ b/internal/hexaiaction/cmdentry.go
@@ -1,149 +1,183 @@
package hexaiaction
import (
- "context"
- "fmt"
- "io"
- "os"
- "path/filepath"
- "time"
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "time"
- "codeberg.org/snonux/hexai/internal/tmux"
- "golang.org/x/term"
+ "codeberg.org/snonux/hexai/internal/tmux"
+ "golang.org/x/term"
)
// Options configures the command-line orchestration for hexai-tmux-action.
type Options struct {
- Infile string
- Outfile string
- UIChild bool
- TmuxTarget string
- TmuxSplit string // "v" or "h"
- TmuxPercent int // 1-100
+ Infile string
+ Outfile string
+ UIChild bool
+ TmuxTarget string
+ TmuxSplit string // "v" or "h"
+ TmuxPercent int // 1-100
}
// 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 {
- if opts.UIChild {
- return runChild(ctx, opts.Infile, opts.Outfile, stdout, stderr)
- }
- // Always use tmux path
- return runInTmuxParent(stdin, stdout, opts.TmuxTarget, opts.TmuxSplit, opts.TmuxPercent)
+ if opts.UIChild {
+ return runChild(ctx, opts.Infile, opts.Outfile, stdout, stderr)
+ }
+ // Always use tmux path
+ return runInTmuxParent(stdin, stdout, opts.TmuxTarget, opts.TmuxSplit, opts.TmuxPercent)
}
// seams for unit tests
-var isTTYFn = func(fd uintptr) bool { return term.IsTerminal(int(fd)) }
-var splitRunFn = tmux.SplitRun
-var osExecutableFn = os.Executable
-var runFn = Run
+var (
+ isTTYFn = func(fd uintptr) bool { return term.IsTerminal(int(fd)) }
+ splitRunFn = tmux.SplitRun
+ 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)
- out := io.Writer(os.Stdout)
- closeIn := func() {}
- closeOut := func() {}
- if path := infile; path != "" {
- f, err := os.Open(path)
- if err != nil { return nil, nil, func(){}, func(){}, fmt.Errorf("hexai-tmux-action: cannot open infile: %w", err) }
- in = f
- closeIn = func() { _ = f.Close() }
- }
- if path := outfile; path != "" {
- f, err := os.Create(path)
- if err != nil { return nil, nil, func(){}, func(){}, fmt.Errorf("hexai-tmux-action: cannot open outfile: %w", err) }
- out = f
- closeOut = func() { _ = f.Close() }
- }
- return in, out, closeIn, closeOut, nil
+ in := io.Reader(os.Stdin)
+ out := io.Writer(os.Stdout)
+ closeIn := func() {}
+ closeOut := func() {}
+ if path := infile; path != "" {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, nil, func() {}, func() {}, fmt.Errorf("hexai-tmux-action: cannot open infile: %w", err)
+ }
+ in = f
+ closeIn = func() { _ = f.Close() }
+ }
+ if path := outfile; path != "" {
+ f, err := os.Create(path)
+ if err != nil {
+ return nil, nil, func() {}, func() {}, fmt.Errorf("hexai-tmux-action: cannot open outfile: %w", err)
+ }
+ out = f
+ closeOut = func() { _ = f.Close() }
+ }
+ return in, out, closeIn, closeOut, nil
}
// 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 {
- if outfile == "" {
- // No atomic handoff needed; just run normally to provided stdout
- var in io.Reader = os.Stdin
- if infile != "" {
- f, err := os.Open(infile)
- if err != nil { return err }
- defer func(){ _ = f.Close() }()
- in = f
- }
- return runFn(ctx, in, stdout, stderr)
- }
- tmp := outfile + ".tmp"
- in, out, closeIn, closeOut, err := openIO(infile, tmp)
- if err != nil { return err }
- defer closeIn()
- if err := runFn(ctx, in, out, stderr); err != nil {
- closeOut()
- if copyErr := echoThrough(infile, tmp, os.Stdin, stdout); copyErr != nil {
- return fmt.Errorf("hexai-tmux-action child: %v; echo failed: %v", err, copyErr)
- }
- } else {
- closeOut()
- }
- return os.Rename(tmp, outfile)
+ if outfile == "" {
+ // No atomic handoff needed; just run normally to provided stdout
+ var in io.Reader = os.Stdin
+ if infile != "" {
+ f, err := os.Open(infile)
+ if err != nil {
+ return err
+ }
+ defer func() { _ = f.Close() }()
+ in = f
+ }
+ return runFn(ctx, in, stdout, stderr)
+ }
+ tmp := outfile + ".tmp"
+ in, out, closeIn, closeOut, err := openIO(infile, tmp)
+ if err != nil {
+ return err
+ }
+ defer closeIn()
+ if err := runFn(ctx, in, out, stderr); err != nil {
+ closeOut()
+ if copyErr := echoThrough(infile, tmp, os.Stdin, stdout); copyErr != nil {
+ return fmt.Errorf("hexai-tmux-action child: %v; echo failed: %v", err, copyErr)
+ }
+ } else {
+ closeOut()
+ }
+ return os.Rename(tmp, outfile)
}
func runInTmuxParent(stdin io.Reader, stdout io.Writer, target, split string, percent int) error {
- dir, err := os.MkdirTemp("", "hexai-tmux-action-")
- if err != nil { return err }
- defer func() { _ = os.RemoveAll(dir) }()
- inPath := filepath.Join(dir, "input.txt")
- outPath := filepath.Join(dir, "reply.txt")
- if err := persistStdin(inPath, stdin); err != nil { return err }
- exe, err := osExecutableFn()
- if err != nil { return err }
- argv := []string{exe, "-ui-child", "-infile", inPath, "-outfile", outPath}
- opts := tmux.SplitOpts{Target: target, Vertical: split != "h", Percent: percent}
- if err := splitRunFn(opts, argv); err != nil { return err }
- if err := waitForFile(outPath, 60*time.Second); err != nil { return err }
- return catFileTo(stdout, outPath)
+ dir, err := os.MkdirTemp("", "hexai-tmux-action-")
+ if err != nil {
+ return err
+ }
+ defer func() { _ = os.RemoveAll(dir) }()
+ inPath := filepath.Join(dir, "input.txt")
+ outPath := filepath.Join(dir, "reply.txt")
+ if err := persistStdin(inPath, stdin); err != nil {
+ return err
+ }
+ exe, err := osExecutableFn()
+ if err != nil {
+ return err
+ }
+ argv := []string{exe, "-ui-child", "-infile", inPath, "-outfile", outPath}
+ opts := tmux.SplitOpts{Target: target, Vertical: split != "h", Percent: percent}
+ if err := splitRunFn(opts, argv); err != nil {
+ return err
+ }
+ if err := waitForFile(outPath, 60*time.Second); err != nil {
+ return err
+ }
+ return catFileTo(stdout, outPath)
}
func persistStdin(path string, stdin io.Reader) error {
- f, err := os.Create(path)
- if err != nil { return err }
- defer func() { _ = f.Close() }()
- if _, err := io.Copy(f, stdin); err != nil { return err }
- return f.Sync()
+ f, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+ defer func() { _ = f.Close() }()
+ if _, err := io.Copy(f, stdin); err != nil {
+ return err
+ }
+ return f.Sync()
}
func waitForFile(path string, timeout time.Duration) error {
- deadline := time.Now().Add(timeout)
- for {
- if _, err := os.Stat(path); err == nil { return nil }
- if time.Now().After(deadline) { return fmt.Errorf("hexai-tmux-action: timeout waiting for reply file") }
- time.Sleep(200 * time.Millisecond)
- }
+ deadline := time.Now().Add(timeout)
+ for {
+ if _, err := os.Stat(path); err == nil {
+ return nil
+ }
+ if time.Now().After(deadline) {
+ return fmt.Errorf("hexai-tmux-action: timeout waiting for reply file")
+ }
+ time.Sleep(200 * time.Millisecond)
+ }
}
func catFileTo(w io.Writer, path string) error {
- f, err := os.Open(path)
- if err != nil { return err }
- defer func() { _ = f.Close() }()
- _, err = io.Copy(w, f)
- return err
+ f, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer func() { _ = f.Close() }()
+ _, err = io.Copy(w, f)
+ return err
}
// echoThrough no longer used in tmux-only flow, but kept for potential reuse.
func echoThrough(infile, outfile string, stdin io.Reader, stdout io.Writer) error {
- var in io.Reader = stdin
- var out io.Writer = stdout
- if infile != "" {
- f, err := os.Open(infile)
- if err != nil { return err }
- defer func() { _ = f.Close() }()
- in = f
- }
- if outfile != "" {
- f, err := os.Create(outfile)
- if err != nil { return err }
- defer func() { _ = f.Close() }()
- out = f
- }
- _, err := io.Copy(out, in)
- return err
+ var in io.Reader = stdin
+ var out io.Writer = stdout
+ if infile != "" {
+ f, err := os.Open(infile)
+ if err != nil {
+ return err
+ }
+ defer func() { _ = f.Close() }()
+ in = f
+ }
+ if outfile != "" {
+ f, err := os.Create(outfile)
+ if err != nil {
+ return err
+ }
+ defer func() { _ = f.Close() }()
+ out = f
+ }
+ _, err := io.Copy(out, in)
+ return err
}