PTY Resize: Buffer Switch and Reinit Triggers in SwiftTerm
When the iPhone’s terminal view changes size — orientation rotation, keyboard appearance, split view changes — the new dimensions must be communicated to the Mac so the PTY’s winsize matches what the user sees. TermOnMac drives resize through SwiftTerm’s onBufferSwitch and onNeedsReinit callbacks.
The Resize Send Path
sendResize sends an AppMessage.ptyResize to the Mac:
// ios_remote_dev_ios_app/RemoteDevApp/Network/iOSRelayConnection.swift
func sendResize(cols: Int, rows: Int, sessionId: String = "") {
let msg = AppMessage.ptyResize(cols: cols, rows: rows, sessionId: sessionId)
try? sendEncrypted(msg)
}
On the Mac, PTYManager.resize calls ioctl(masterFD, TIOCSWINSZ, &ws):
// mac_agent/Sources/MacAgentLib/PTYSession.swift
func resize(cols: UInt16, rows: UInt16) {
guard masterFD >= 0 else { return }
var ws = winsize(ws_row: rows, ws_col: cols, ws_xpixel: 0, ws_ypixel: 0)
_ = ioctl(masterFD, TIOCSWINSZ, &ws)
}
TIOCSWINSZ updates the kernel’s window size for the PTY. The shell receives SIGWINCH and re-renders if needed.
SwiftTerm’s onBufferSwitch
SwiftTerm calls onBufferSwitch when the terminal switches between the normal and alternate screen buffer. This happens when programs like vim, less, or htop start (entering alt buffer) or exit (returning to normal buffer):
// ios_remote_dev_ios_app/RemoteDevApp/Views/TerminalView.swift
tv.onBufferSwitch = { [weak tv, weak connection] in
guard let tv, let connection else { return }
let cols = tv.getTerminal().cols
let rows = tv.getTerminal().rows
guard cols >= 20, rows >= 5 else { return }
DebugLog.shared.log("[bufferSwitch] cols=\(cols) rows=\(rows)")
connection.sendResize(cols: cols, rows: rows)
}
The minimum size guard (cols >= 20, rows >= 5) skips resize events with implausibly small dimensions, which can occur during transient layout calculations before the terminal view is fully laid out.
onNeedsReinit and Ctrl-L
onNeedsReinit fires when SwiftTerm detects that a terminal reinitialization is needed — for example, after certain layout changes that invalidate cached state:
tv.onNeedsReinit = { [weak tv, weak connection] in
guard let tv, let connection else { return }
let cols = tv.getTerminal().cols
let rows = tv.getTerminal().rows
DebugLog.shared.log("[reinit] fixing terminal: cols=\(cols) rows=\(rows)")
connection.sendResize(cols: cols, rows: rows)
// Ctrl-L tells the shell to clear and redraw the prompt.
// DECAWM has already been re-enabled by scheduleReinit's feed.
connection.sendInput(Data([0x0c]))
}
After sending the resize, 0x0c (Ctrl-L, form feed) is sent as input. Most shells interpret Ctrl-L as “clear screen and redraw the prompt.” This ensures the prompt is re-rendered at the new terminal width — without it, the prompt might still be at the old position with stale wrap state.
The comment notes that DECAWM (auto-wrap mode) has already been re-enabled by SwiftTerm’s scheduleReinit before this callback runs, so we don’t need to handle that here.
Pending Initial Redraw
SessionStore tracks sessions that need a Ctrl-L after their first resize:
// ios_remote_dev_ios_app/RemoteDevApp/Models/SessionStore.swift
/// Sessions that were just created and need a Ctrl-L clear after their first
/// resize, so the prompt redraws at the correct terminal width.
var pendingInitialRedraw: Set<String> = []
When a session is first created, the Mac starts the PTY at default 80×24. The iPhone’s actual terminal size depends on the device and orientation — usually different from 80×24. After the first resize message, the prompt is at the wrong position; sending Ctrl-L cleans it up.
ResizeDebouncer
A separate ResizeDebouncer exists to coalesce rapid resize events:
ios_remote_dev_ios_app/RemoteDevApp/Views/ResizeDebouncer.swift
Without debouncing, rotating the device or showing/hiding the keyboard can fire multiple resize events in quick succession. Debouncing limits these to a single resize message after the size has stabilized.