diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-02 09:38:04 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-02 09:38:04 +0300 |
| commit | a68228bfa12f4d8a51fe53e244fcd2e66c1ef692 (patch) | |
| tree | 94e257b21b93419fef655c9813f68190204c3d0d /internal/tmuxedit/run.go | |
| parent | 9f0e96ce62339ddefa8771891e0864ede9af5064 (diff) | |
Remove hexai-tmux-edit popup editor featurev0.42.0
The tmux popup editor and its per-agent detection (Cursor/Amp/Aider)
added maintenance surface without enough use to justify it; Codex and
Claude Code already support external-editor mode natively via Ctrl+G.
Drops internal/tmuxedit, cmd/hexai-tmux-edit, the [tmux_edit] config
schema, the Mage build target, and all related docs/README mentions.
Bump version to 0.42.0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'internal/tmuxedit/run.go')
| -rw-r--r-- | internal/tmuxedit/run.go | 268 |
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) -} |
