feat: add tmux-based multi-terminal grid view
Launch Claude Code sessions inside embedded tmux panes instead of separate Terminal.app windows. Sessions are tiled in a grid layout with color-coded borders per project. Border flashing alerts when a session goes idle and needs input. Ctrl+` switches between picker and grid view. New modules: - src/tmux/session-manager.ts — tmux session lifecycle - src/tmux/ansi-parser.ts — ANSI escape code to cell grid parser - src/tmux/capture.ts — polls tmux capture-pane for rendering - src/tmux/input-bridge.ts — forwards keystrokes to tmux sessions - src/components/terminal-view.ts — FrameBuffer renderable for panes - src/components/session-grid.ts — tiled grid with flash effects Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
53
src/tmux/capture.ts
Normal file
53
src/tmux/capture.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export interface CaptureResult {
|
||||
lines: string[]
|
||||
cursorX: number
|
||||
cursorY: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export async function capturePane(sessionName: string): Promise<CaptureResult | null> {
|
||||
try {
|
||||
const [contentProc, infoProc] = [
|
||||
Bun.spawn(["tmux", "capture-pane", "-t", sessionName, "-p", "-e"], { stdout: "pipe", stderr: "ignore" }),
|
||||
Bun.spawn(["tmux", "display-message", "-t", sessionName, "-p", "#{cursor_x} #{cursor_y} #{pane_width} #{pane_height}"], { stdout: "pipe", stderr: "ignore" }),
|
||||
]
|
||||
|
||||
const [contentText, infoText] = await Promise.all([
|
||||
new Response(contentProc.stdout).text(),
|
||||
new Response(infoProc.stdout).text(),
|
||||
])
|
||||
|
||||
const [codeContent, codeInfo] = await Promise.all([contentProc.exited, infoProc.exited])
|
||||
if (codeContent !== 0 || codeInfo !== 0) return null
|
||||
|
||||
const parts = infoText.trim().split(" ")
|
||||
const cursorX = parseInt(parts[0]) || 0
|
||||
const cursorY = parseInt(parts[1]) || 0
|
||||
const width = parseInt(parts[2]) || 80
|
||||
const height = parseInt(parts[3]) || 24
|
||||
|
||||
const lines = contentText.split("\n")
|
||||
// Remove trailing empty line from split
|
||||
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop()
|
||||
|
||||
return { lines, cursorX, cursorY, width, height }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Hash for diffing - skip re-render if nothing changed
|
||||
let lastHash = ""
|
||||
|
||||
export function hasChanged(lines: string[]): boolean {
|
||||
// Simple fast hash: join first+last few lines + length
|
||||
const h = lines.length + ":" + (lines[0] || "") + (lines[lines.length - 1] || "")
|
||||
if (h === lastHash) return false
|
||||
lastHash = h
|
||||
return true
|
||||
}
|
||||
|
||||
export function resetHash() {
|
||||
lastHash = ""
|
||||
}
|
||||
Reference in New Issue
Block a user