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:
@@ -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)) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user