summaryrefslogtreecommitdiff
path: root/internal/tmuxedit/run.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tmuxedit/run.go')
-rw-r--r--internal/tmuxedit/run.go268
1 files changed, 0 insertions, 268 deletions
diff --git a/internal/tmuxedit/run.go b/internal/tmuxedit/run.go
deleted file mode 100644
index ff22b5b..0000000
--- a/internal/tmuxedit/run.go
+++ /dev/null
@@ -1,268 +0,0 @@
-package tmuxedit
-
-import (
- "context"
- "fmt"
- "log"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
-
- "codeberg.org/snonux/hexai/internal/appconfig"
- "codeberg.org/snonux/hexai/internal/editor"
- "codeberg.org/snonux/hexai/internal/tmux"
-)
-
-// Options holds the parsed command-line flags for hexai-tmux-edit.
-type Options struct {
- ConfigPath string // --config flag
- Agent string // --agent flag (explicit agent name, or auto-detect)
- Pane string // --pane flag (target pane ID)
-}
-
-func openEditorPopup(initial, popupW, popupH string) (string, error) {
- return tmuxEditDeps{}.openEditor(initial, popupW, popupH)
-}
-
-// openEditor creates a temp file, opens it in a tmux popup with the user's
-// editor, waits for completion, and returns the edited content.
-func (d tmuxEditDeps) openEditor(initial, popupW, popupH string) (string, error) {
- if d.openEditorPopup != nil {
- return d.openEditorPopup(initial, popupW, popupH)
- }
- ed, err := editor.Resolve()
- if err != nil {
- return "", err
- }
- // Create a temp file with the initial content
- f, err := os.CreateTemp("", "hexai-tmux-edit-*.md")
- if err != nil {
- return "", fmt.Errorf("create temp file: %w", err)
- }
- path := f.Name()
- defer func() { _ = os.Remove(path) }()
-
- if initial != "" {
- if _, err := f.WriteString(initial); err != nil {
- _ = f.Close()
- return "", fmt.Errorf("write initial content: %w", err)
- }
- }
- if err := f.Close(); err != nil {
- return "", fmt.Errorf("close temp file: %w", err)
- }
-
- // Build the tmux display-popup command to launch the editor
- if err := d.launch(ed, path, popupW, popupH); err != nil {
- return "", fmt.Errorf("popup editor: %w", err)
- }
-
- b, err := os.ReadFile(path)
- if err != nil {
- return "", fmt.Errorf("read edited file: %w", err)
- }
- return strings.TrimSpace(string(b)), nil
-}
-
-func launchPopup(ed, path, width, height string) error {
- return tmuxEditDeps{}.launch(ed, path, width, height)
-}
-
-// launch runs `tmux display-popup` with the editor. The -E flag makes the
-// popup close when the editor exits. The -d flag sets the working directory
-// for the popup. Uses .Run() so the popup blocks until the user closes it.
-func (d tmuxEditDeps) launch(ed, path, width, height string) error {
- if d.launchPopup != nil {
- return d.launchPopup(ed, path, width, height)
- }
- args := []string{"display-popup", "-E"}
-
- // Get current working directory to pass to the popup
- if cwd, err := os.Getwd(); err == nil && cwd != "" {
- args = append(args, "-d", cwd)
- }
-
- if width != "" {
- args = append(args, "-w", width)
- }
- if height != "" {
- args = append(args, "-h", height)
- }
- args = append(args, ed+" "+shellQuote(path))
- return exec.Command("tmux", args...).Run()
-}
-
-// shellQuote wraps a path in single quotes for safe shell use.
-func shellQuote(s string) string {
- return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
-}
-
-// Run is the main orchestrator for hexai-tmux-edit. It:
-// 1. Checks tmux availability
-// 2. Resolves the target pane
-// 3. Captures pane content
-// 4. Detects or selects the agent
-// 5. Extracts the current prompt
-// 6. Opens the editor in a popup
-// 7. Deduplicates and sends edited text back
-func Run(opts Options) error {
- if !tmux.Available() {
- return fmt.Errorf("tmux is not available (not in a tmux session)")
- }
- cfg := loadConfig(opts.ConfigPath)
- return runWithConfig(opts, cfg)
-}
-
-// loadConfig loads the application config, extracting tmux_edit settings.
-func loadConfig(configPath string) appconfig.App {
- logger := log.New(os.Stderr, "[hexai-tmux-edit] ", log.LstdFlags)
- lopts := appconfig.LoadOptions{ConfigPath: configPath}
- // This runner has no signal-aware context of its own; the load is a quick
- // local file read, so a background context is sufficient.
- return appconfig.LoadWithOptions(context.Background(), logger, lopts)
-}
-
-// debugLog is the debug logger. Set to a real logger via initDebugLog().
-var debugLog *log.Logger
-
-// initDebugLog creates a debug log file in the state directory
-// (~/.local/state/hexai/hexai-tmux-edit.log). Returns a closer for the
-// log file handle and an error if the state directory cannot be resolved.
-// Silently skips logging (returns a no-op closer) if the log file cannot
-// be opened.
-func initDebugLog() (func(), error) {
- stateDir, err := appconfig.StateDir()
- if err != nil {
- return nil, fmt.Errorf("cannot create state directory: %w", err)
- }
-
- logPath := filepath.Join(stateDir, "hexai-tmux-edit.log")
- f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
- if err != nil {
- return func() {}, nil
- }
- debugLog = log.New(f, "", log.LstdFlags|log.Lmicroseconds)
- return func() { _ = f.Close() }, nil
-}
-
-func dbg(format string, args ...any) {
- if debugLog != nil {
- debugLog.Printf(format, args...)
- }
-}
-
-// runWithConfig executes the edit workflow using the provided config.
-// It resolves the agent (by name or auto-detect), extracts the current
-// prompt, opens the editor popup, then clears and sends the result.
-func runWithConfig(opts Options, cfg appconfig.App) error {
- return tmuxEditDeps{}.runWithConfig(opts, cfg)
-}
-
-func (d tmuxEditDeps) runWithConfig(opts Options, cfg appconfig.App) error {
- closeLog, err := initDebugLog()
- if err != nil {
- return fmt.Errorf("init debug log: %w", err)
- }
- defer closeLog()
- dbg("=== hexai-tmux-edit start ===")
- dbg("opts: pane=%q agent=%q config=%q", opts.Pane, opts.Agent, opts.ConfigPath)
-
- paneID, err := d.resolveTargetPane(opts.Pane)
- if err != nil {
- dbg("resolveTargetPane error: %v", err)
- return err
- }
- dbg("resolved pane: %q", paneID)
-
- content, err := d.capture(paneID)
- if err != nil {
- dbg("capturePane error: %v", err)
- return err
- }
- dbg("captured %d bytes from pane", len(content))
- logPaneLines(content)
-
- agents := withAgentDeps(resolveAgents(cfg.TmuxEditAgents), d)
- agent := pickAgent(opts.Agent, content, agents)
- agent = withAgentDep(agent, d)
- dbg("agent: name=%q", agent.Name())
-
- original := agent.ExtractPrompt(content)
- dbg("extractPrompt result: %q", original)
-
- popupW, popupH := popupDimensions(cfg)
- dbg("opening editor popup: w=%s h=%s initial=%q", popupW, popupH, original)
-
- edited, err := d.openEditor(original, popupW, popupH)
- if err != nil {
- dbg("openEditorPopup error: %v", err)
- return err
- }
- dbg("editor returned: %q", edited)
-
- text := deduplicateText(original, edited)
- dbg("deduplicateText result: %q", text)
- if text == "" {
- dbg("nothing to send, exiting")
- return nil
- }
-
- dbg("clearing and sending to pane %q: %q", paneID, text)
- if err := agent.ClearInput(paneID); err != nil {
- dbg("ClearInput error: %v", err)
- return err
- }
- if err := agent.SendText(paneID, text); err != nil {
- dbg("SendText error: %v", err)
- return err
- }
-
- // Append to history (log errors but don't fail the operation)
- cwd, err := os.Getwd()
- if err != nil {
- cwd = "unknown"
- dbg("os.Getwd error (using 'unknown'): %v", err)
- }
- if err := AppendHistory(text, agent.Name(), cwd); err != nil {
- dbg("AppendHistory error: %v", err)
- // Non-fatal: log but continue
- } else {
- dbg("appended to history: agent=%q cwd=%q len=%d", agent.Name(), cwd, len(text))
- }
-
- dbg("=== done ===")
- return nil
-}
-
-// logPaneLines logs lines containing box-drawing or arrow characters for
-// debugging prompt detection.
-func logPaneLines(content string) {
- for i, line := range strings.Split(content, "\n") {
- if strings.Contains(line, "│") || strings.Contains(line, "→") {
- dbg(" pane line %d: %q", i, line)
- }
- }
-}
-
-// popupDimensions returns the popup width and height from config, defaulting
-// to "80%" for both if not set.
-func popupDimensions(cfg appconfig.App) (string, string) {
- w := cfg.TmuxEditPopupWidth
- if w == "" {
- w = "80%"
- }
- h := cfg.TmuxEditPopupHeight
- if h == "" {
- h = "80%"
- }
- return w, h
-}
-
-// pickAgent selects an agent by explicit name or auto-detection.
-func pickAgent(name, content string, agents []Agent) Agent {
- if name != "" {
- return findAgentByName(name, agents)
- }
- return detectAgent(content, agents)
-}