diff options
Diffstat (limited to 'cmd/internal/hexai-action/main.go')
| -rw-r--r-- | cmd/internal/hexai-action/main.go | 196 |
1 files changed, 189 insertions, 7 deletions
diff --git a/cmd/internal/hexai-action/main.go b/cmd/internal/hexai-action/main.go index 8bcc3cd..b8ba524 100644 --- a/cmd/internal/hexai-action/main.go +++ b/cmd/internal/hexai-action/main.go @@ -6,24 +6,62 @@ import ( "fmt" "io" "os" + "path/filepath" + "time" "codeberg.org/snonux/hexai/internal/hexaiaction" + "codeberg.org/snonux/hexai/internal/tmux" + "golang.org/x/term" ) func main() { infile := flag.String("infile", "", "Read input from this file instead of stdin") outfile := flag.String("outfile", "", "Write output to this file instead of stdout") + // Tmux/UI flags + forceTmux := flag.Bool("tmux", false, "Force running the UI in a tmux split-pane (auto if not set)") + noTmux := flag.Bool("no-tmux", false, "Disable tmux mode even if available") + uiChild := flag.Bool("ui-child", false, "INTERNAL: run interactive UI and write to -outfile atomically") + tmuxTarget := flag.String("tmux-target", "", "tmux split target (advanced)") + tmuxSplit := flag.String("tmux-split", "v", "tmux split orientation: v or h") + tmuxPercent := flag.Int("tmux-percent", 33, "tmux split size percentage (1-100)") flag.Parse() - in, out, closeIn, closeOut, err := openIO(*infile, *outfile) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) + // Child mode: run TUI and write atomically to -outfile + if *uiChild { + if err := runChild(*infile, *outfile); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + return + } + + // Parent mode: decide inline vs tmux + if shouldRunInTmux(*forceTmux, *noTmux) { + if err := runInTmuxParent(*tmuxTarget, *tmuxSplit, *tmuxPercent); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + return } - defer closeIn() - defer closeOut() - if err := hexaiaction.Run(context.Background(), in, out, os.Stderr); err != nil { + // Inline path: only if we have a TTY for UI; otherwise echo input + if isTTY(os.Stdout.Fd()) && isTTY(os.Stdin.Fd()) { + in, out, closeIn, closeOut, err := openIO(*infile, *outfile) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + defer closeIn() + defer closeOut() + if err := hexaiactionRun(context.Background(), in, out, os.Stderr); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + return + } + + // Fallback: no TTY and tmux not available; echo input to output + if err := echoThrough(*infile, *outfile); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } @@ -54,3 +92,147 @@ func openIO(infile, outfile string) (io.Reader, io.Writer, func(), func(), error } return in, out, closeIn, closeOut, nil } + +// runChild runs the interactive flow and writes the final output atomically to outfile. +var hexaiactionRun = hexaiaction.Run + +func runChild(infile, outfile string) error { + if outfile == "" { + // No atomic handoff needed; just run normally to stdout + in, out, closeIn, closeOut, err := openIO(infile, "") + if err != nil { + return err + } + defer closeIn() + defer closeOut() + return hexaiactionRun(context.Background(), in, out, os.Stderr) + } + tmp := outfile + ".tmp" + in, out, closeIn, closeOut, err := openIO(infile, tmp) + if err != nil { + return err + } + defer closeIn() + if err := hexaiactionRun(context.Background(), in, out, os.Stderr); err != nil { + // On error, try to echo input to tmp to avoid blocking + closeOut() + if copyErr := echoThrough(infile, tmp); copyErr != nil { + return fmt.Errorf("hexai-action child: %v; echo failed: %v", err, copyErr) + } + } else { + closeOut() + } + return os.Rename(tmp, outfile) +} + +var isTTYFn = isTTY +var tmuxAvailableFn = tmux.Available +var splitRunFn = tmux.SplitRun +var osExecutableFn = os.Executable + +func shouldRunInTmux(forceTmux, noTmux bool) bool { + if noTmux { + return false + } + if forceTmux { + return true + } + // Auto: prefer tmux when stdio are not TTYs (Helix :pipe scenario) + if !(isTTYFn(os.Stdin.Fd()) && isTTYFn(os.Stdout.Fd())) && tmuxAvailableFn() { + return true + } + return false +} + +func isTTY(fd uintptr) bool { return term.IsTerminal(int(fd)) } + +func runInTmuxParent(target, split string, percent int) error { + // Prepare temp files + dir, err := os.MkdirTemp("", "hexai-action-") + if err != nil { + return err + } + defer func() { _ = os.RemoveAll(dir) }() + inPath := filepath.Join(dir, "input.txt") + outPath := filepath.Join(dir, "reply.txt") + // Read stdin and persist to inPath + if err := persistStdin(inPath); err != nil { + return err + } + // Build child argv + exe, err := osExecutableFn() + if err != nil { + return err + } + argv := []string{exe, "-ui-child", "-infile", inPath, "-outfile", outPath} + // Spawn tmux split + opts := tmux.SplitOpts{Target: target, Vertical: split != "h", Percent: percent} + if err := splitRunFn(opts, argv); err != nil { + return err + } + // Wait for outfile to appear + if err := waitForFile(outPath, 60*time.Second); err != nil { + return err + } + // Print to stdout + return catFileToStdout(outPath) +} + +func persistStdin(path string) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer func() { _ = f.Close() }() + if _, err := io.Copy(f, os.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-action: timeout waiting for reply file") + } + time.Sleep(200 * time.Millisecond) + } +} + +func catFileToStdout(path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer func() { _ = f.Close() }() + _, err = io.Copy(os.Stdout, f) + return err +} + +func echoThrough(infile, outfile string) error { + // Read from infile or stdin and write to outfile or stdout + var in io.Reader = os.Stdin + var out io.Writer = os.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 +} |
