feat: tmux 动态 resize(iPadOS 拖拽窗口/旋转自适应)

- ContentView 观测 onChange(geo.size)/onAppear → controller.containerResized
- containerResized: 点尺寸 × displayScale ÷ cell 像素 = 目标 cols×rows,去重 + debounce(200ms)
  → refresh-client -C WxH → tmux 重排回 %layout-change → 更新 pane frame
- cell 像素来自 raw 终端/pane surface metrics(InMemoryTerminalViewport.cellWidthPixels);
  surface resize 回调只 noteCellPixels 不驱动 tmux(防反馈环,单向数据流)
- 验证:容器 1032x1280@2x cell16x35→129x73,tmux 根 rect 精确采纳,2 pane 复用不重建

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kid
2026-07-24 17:42:45 +08:00
parent 9a27338222
commit 7abe61a71f
4 changed files with 85 additions and 32 deletions

View File

@@ -124,12 +124,20 @@ final class TmuxByteChannel: @unchecked Sendable {
func bytes(_ b: [UInt8]) { cont.yield(.bytes(b)) }
}
/// 线tmux attach `refresh-client -C`
/// 线 + cell tmux attach/resize `refresh-client -C`
final class GridBox: @unchecked Sendable {
private let lock = NSLock()
private var v: (cols: Int, rows: Int) = (80, 24)
var value: (cols: Int, rows: Int) { lock.lock(); defer { lock.unlock() }; return v }
func set(cols: Int, rows: Int) { lock.lock(); v = (max(1, cols), max(1, rows)); lock.unlock() }
private var cols = 80, rows = 24, cellW = 0, cellH = 0
var value: (cols: Int, rows: Int, cellW: Int, cellH: Int) {
lock.lock(); defer { lock.unlock() }; return (cols, rows, cellW, cellH)
}
func set(cols: Int, rows: Int, cellW: Int, cellH: Int) {
lock.lock()
self.cols = max(1, cols); self.rows = max(1, rows)
if cellW > 0 { self.cellW = cellW } // cell
if cellH > 0 { self.cellH = cellH }
lock.unlock()
}
}
/// M0 SSH SSHSession(libssh2) GhosttyKit in-memory
@@ -188,7 +196,9 @@ final class SSHTerminalModel: ObservableObject {
else { holder.transport?.send(data) }
},
resize: { vp in
grid.set(cols: Int(vp.columns), rows: Int(vp.rows)) // tmux
// + cell tmux resize
grid.set(cols: Int(vp.columns), rows: Int(vp.rows),
cellW: Int(vp.cellWidthPixels), cellH: Int(vp.cellHeightPixels))
if let m = moshHolder.session { m.resize(cols: Int(vp.columns), rows: Int(vp.rows)) }
else { holder.transport?.resize(cols: vp.columns, rows: vp.rows) }
}
@@ -333,9 +343,10 @@ final class SSHTerminalModel: ObservableObject {
let holder = self.holder
let controller = TmuxController(sendRaw: { data in holder.transport?.send(data) })
controller.onExit = { [weak self] in Task { @MainActor in self?.exitTmux() } }
// attach refresh-client -C tmux
// + cell attach refresh-client -C resize
let g = screenGrid.value
controller.setClientSize(cols: g.cols, rows: g.rows)
controller.setCellPixels(w: g.cellW, h: g.cellH)
tmuxController = controller
if !initialBytes.isEmpty { controller.feed(Data(initialBytes)) }
}