Inline text editing implementation within the file browser preview pane using tmux PTY backend, cursor movement, text manipulation, and editor state management. Covers entry/exit lifecycle, dimension calculations, confirmation dialogs, click-away detection, mouse forwarding, and app-level key routing. Use when working on inline editing features, text input components, or debugging editor rendering/input issues in the file browser plugin.
The inline editor (tmux_inline_edit) lets users edit files directly within the file browser preview pane using their preferred terminal editor (vim, nvim, nano, etc.) without leaving the TUI. The file tree remains visible during editing.
Core Principle: This is NOT a terminal emulator. Tmux manages the PTY backend; Sidecar acts as an input/output relay, similar to the workspace plugin's interactive mode.
internal/plugins/filebrowser/inline_edit.go): Creates tmux sessions, manages editor lifecycleinternal/plugins/filebrowser/view.go, inline_edit.go): Renders editor content within preview paneinternal/plugins/filebrowser/plugin.go, ): Routes keys/clicks to editor or confirmation dialogmouse.gointernal/tty/tty.go): Handles tmux capture, cursor overlay, and input forwardingUser presses 'e' on file
-> enterInlineEditMode()
-> tmux new-session -d -s {sessionName} {editor} {path}
-> InlineEditStartedMsg
-> handleInlineEditStarted()
-> tty.Model.Enter()
-> Start polling tmux capture-pane
-> renderInlineEditorContent() in preview pane
-> User types -> tty.Model forwards to tmux
-> User exits -> SessionDeadMsg or exit keys
-> exitInlineEditMode()
-> Refresh preview
| File | Purpose |
|---|---|
internal/plugins/filebrowser/inline_edit.go | Editor lifecycle, confirmation dialog, dimension calculations |
internal/plugins/filebrowser/view.go | Preview pane rendering, gradient border |
internal/plugins/filebrowser/mouse.go | Click-away detection |
internal/plugins/filebrowser/plugin.go | State management, Update routing |
internal/tty/tty.go | TTY model for tmux interaction (shared with workspace) |
internal/app/update.go | App-level key routing for inline edit context |
The editor renders within renderPreviewPane(), NOT as a full-screen takeover. The file tree stays visible.
// view.go - renderPreviewPane()
func (p *Plugin) renderPreviewPane(visibleHeight int) string {
if p.inlineEditMode && p.inlineEditor != nil && p.inlineEditor.IsActive() {
return p.renderInlineEditorContent(visibleHeight)
}
// ... normal preview rendering
}
The tty.Model needs exact dimensions matching the preview pane content area:
func (p *Plugin) calculateInlineEditorWidth() int {
if !p.treeVisible {
return p.width - 4 // borders + padding
}
p.calculatePaneWidths()
return p.previewWidth - 4
}
func (p *Plugin) calculateInlineEditorHeight() int {
paneHeight := p.height
innerHeight := paneHeight - 2 // pane borders
contentHeight := innerHeight - 2 // header lines
if len(p.tabs) > 1 {
contentHeight-- // tab line
}
return contentHeight
}
These MUST stay in sync with renderInlineEditorContent() layout calculations.
Rule: session alive = show confirmation, session dead = exit immediately.
Always show confirmation when the session is alive, regardless of file modification status. Vim's modification status cannot be reliably detected externally.
func (p *Plugin) isInlineEditSessionAlive() bool {
if p.inlineEditSession == "" {
return false
}
err := exec.Command("tmux", "has-session", "-t", p.inlineEditSession).Run()
return err == nil
}
Check session alive status:
Update() when in inline edit mode - if dead, exit immediatelyState fields:
showExitConfirmation bool // Dialog visible
pendingClickRegion string // Where user clicked
pendingClickData interface{} // Click data (tree index, tab index)
exitConfirmSelection int // 0=Save&Exit, 1=Exit without saving, 2=Cancel
Options:
Mouse regions are registered during render. Clicks between items may miss regions, so always include position-based fallback:
if p.inlineEditMode && p.inlineEditor != nil && p.inlineEditor.IsActive() {
action := p.mouseHandler.HandleMouse(msg)
handleClickAway := func(regionID string, regionData interface{}) (*Plugin, tea.Cmd) {
if !p.isInlineEditSessionAlive() {
p.exitInlineEditMode()
p.pendingClickRegion = regionID
p.pendingClickData = regionData
return p.processPendingClickAction()
}
p.pendingClickRegion = regionID
p.pendingClickData = regionData
p.showExitConfirmation = true
p.exitConfirmSelection = 0
return p, nil
}
if action.Type == mouse.ActionClick {
if action.Region != nil {
switch action.Region.ID {
case regionTreePane, regionTreeItem, regionPreviewTab:
return handleClickAway(action.Region.ID, action.Region.Data)
}
}
// Fallback: position-based detection
if p.treeVisible && action.X < p.treeWidth {
return handleClickAway(regionTreePane, nil)
}
}
// Forward to tty model
return p, p.inlineEditor.Update(msg)
}
Visual indicator that edit mode is active:
if p.inlineEditMode && p.inlineEditor != nil && p.inlineEditor.IsActive() {
rightPane = styles.RenderPanelWithGradient(previewContent, p.previewWidth,
paneHeight, styles.GetInteractiveGradient())
}
Full mouse interaction including text selection via SGR (1006) protocol. Mouse events are forwarded to the tty model which translates them into SGR escape sequences: \x1b[<button;x;y;M/m where M = press/drag, m = release.
sendEditorSaveAndQuit() detects which editor is running and sends the appropriate sequence:
| Editor | Save & Quit Command |
|---|---|
| vim, nvim, vi | Escape :wq Enter |
| nano | Ctrl+O Enter Ctrl+X |
| emacs | Ctrl+X Ctrl+S Ctrl+X Ctrl+C |
| helix | Escape :wq Enter |
| micro | Ctrl+S Ctrl+Q |
| kakoune | Escape :write-quit Enter |
| joe | Ctrl+K X |
| ne | Escape :SaveQuit Enter |
| amp | Ctrl+S Ctrl+Q |
| Method | Confirmation | Description |
|---|---|---|
Ctrl+\ | No | Immediate exit (tty.Config.ExitKey) |
| Double-ESC | No | Exit with 150ms delay (vim ESC compatibility) |
:q, :wq in vim | No | Normal editor exit, session death detected |
| Click tree/tab | Yes (if alive) | Shows confirmation when session alive; exits immediately if dead |
inlineEditor *tty.Model // Embeddable tty model
inlineEditMode bool // Currently editing
inlineEditSession string // Tmux session name
inlineEditFile string // File being edited
showExitConfirmation bool
pendingClickRegion string
pendingClickData interface{}
exitConfirmSelection int
func (p *Plugin) Update(msg tea.Msg) (plugin.Plugin, tea.Cmd) {
// 1. Handle exit confirmation dialog FIRST
if p.showExitConfirmation { /* j/k navigation, Enter confirm, Esc cancel */ }
// 2. Handle inline edit mode
if p.inlineEditMode && p.inlineEditor.IsActive() { /* Delegate to tty.Model */ }
// 3. Normal plugin handling
}
The app intercepts global shortcuts (q, 1-5, `, ~, ?, !, @) before plugins. For inline edit to receive ALL keys, internal/app/update.go must recognize the context:
if m.activeContext == "workspace-interactive" || m.activeContext == "file-browser-inline-edit" {
// Forward ALL keys to plugin
}
The plugin returns "file-browser-inline-edit" from FocusContext().
IsActive() on every message - tty model can become inactive asynchronously; without this check, users get a blank screen after vim exitstty.Model.View()"file-browser-inline-edit" context - otherwise typing q in vim triggers quit instead of inserting characterGated behind tmux_inline_edit. Enable in ~/.config/sidecar/config.json:
{ "features": { "tmux_inline_edit": true } }
Fallback: features.IsEnabled(features.TmuxInlineEdit.Name) returns false -> opens external editor.
| Key | Command | Description |
|---|---|---|
e | edit | Edit file inline (within preview pane) |
E | edit-external | Edit in full terminal (suspends TUI) |
Registered in:
internal/plugins/filebrowser/plugin.go - Commands() methodinternal/plugins/filebrowser/handlers.go - Key handlinginternal/keymap/bindings.go - Key bindings for file-browser-tree and file-browser-preview contextsdocs/guides/interactive-shell-implementation.mdinternal/tty/tty.gointernal/features/features.go