feat: 顶带 Shimo 化重设计 + 第一版功能减法

- 顶带:实心圆窗口按钮、无边框 tab 胶囊、右上 mosh/SSH 协议徽标
- 减法:删「新建 window/回首页」按钮、tab 组 +、pane 拖拽调宽,
  window/pane 管理交还 tmux(连带清理 resizePane/newWindow/ui-resize)
- 底部状态行去重:只留链路 + tmux 概览,协议由顶带徽标独占
- 徽标全局统一:淡染底+同色文字胶囊(线框废除),拼写走 TXTech 规范
- CLAUDE.md 记录无头截图横屏帧缓冲右侧裁切大坑

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kid
2026-07-26 16:52:19 +08:00
parent e59051a6de
commit 8da725114f
8 changed files with 97 additions and 305 deletions

View File

@@ -282,7 +282,7 @@ struct TerminalScreen: View {
// **** 2pt
// pane 线
.padding(.leading, TX.Layout.paneGap)
TerminalActionBar(session: session, actions: barActions, safeBottom: safeBottom)
TerminalActionBar(session: session, safeBottom: safeBottom)
}
.background(p.bgDeep)
}
@@ -348,18 +348,6 @@ struct TerminalScreen: View {
router.goHome()
}
/// **** window
/// `TerminalActionBar.statusLine`
private var barActions: [TerminalActionBar.Action] {
let tmux = session.tmuxController
return [
.init(id: "window", icon: "plus.rectangle.on.rectangle", title: "新建 window",
enabled: tmux != nil) {
tmux?.newWindow()
},
.init(id: "home", icon: "house", title: "回首页") { router.goHome() },
]
}
}
/// tmux
@@ -424,7 +412,9 @@ struct TmuxStage: View {
}
}
/// window tmux pane pane surface+ pane
/// window tmux pane pane surface
/// pane ** app UI** tmux prefix+
/// app `%layout-change`
struct TmuxWindowView: View {
@ObservedObject var window: TmuxWindow
let controller: TmuxController
@@ -455,10 +445,6 @@ struct TmuxWindowView: View {
y: (CGFloat(item.rect.y) + CGFloat(item.rect.height) / 2) * ch)
}
}
// pane pane 12pt
ForEach(splitters(layout.root), id: \.id) { s in
PaneSplitter(splitter: s, cellW: cw, cellH: ch, controller: controller)
}
}
} else {
Color.clear
@@ -476,126 +462,6 @@ struct TmuxWindowView: View {
}
}
/// tmux `resize-pane -x/-y` ** pane **
/// resize " pane"
private func splitters(_ node: TmuxLayout.Node) -> [SplitterSpec] {
var out: [SplitterSpec] = []
func walk(_ n: TmuxLayout.Node) {
switch n {
case .leaf:
break
case .horizontal(let children, _):
// ****
for (i, child) in children.enumerated() where i < children.count - 1 {
let r = child.rect
if let pane = firstPane(child) {
out.append(SplitterSpec(
id: "h-\(pane.raw)-\(i)", isVertical: true, pane: pane,
x: r.x + r.width, y: r.y, length: r.height, currentSize: r.width))
}
}
children.forEach(walk)
case .vertical(let children, _):
// ****
for (i, child) in children.enumerated() where i < children.count - 1 {
let r = child.rect
if let pane = firstPane(child) {
out.append(SplitterSpec(
id: "v-\(pane.raw)-\(i)", isVertical: false, pane: pane,
x: r.x, y: r.y + r.height, length: r.width, currentSize: r.height))
}
}
children.forEach(walk)
}
}
walk(node)
return out
}
private func firstPane(_ node: TmuxLayout.Node) -> TmuxPaneID? {
switch node {
case .leaf(let p, _): return p
case .horizontal(let cs, _), .vertical(let cs, _): return cs.compactMap(firstPane).first
}
}
}
///
struct SplitterSpec {
let id: String
/// true = false =
let isVertical: Bool
/// resize pane pane
let pane: TmuxPaneID
let x: Int, y: Int
///
let length: Int
/// cols / rows
let currentSize: Int
}
/// pane 12pt 44×3pt
///
/// **** + 线 pane frame** `resize-pane`**
/// tmux `%layout-change` frame "线 b"
struct PaneSplitter: View {
let splitter: SplitterSpec
let cellW: CGFloat
let cellH: CGFloat
let controller: TmuxController
@State private var drag: CGFloat = 0
@State private var dragging = false
@EnvironmentObject var theme: TXThemeManager
private var p: TXPalette { theme.palette }
private var hit: CGFloat { TX.Layout.paneDragHit }
var body: some View {
Group {
if splitter.isVertical {
bar(width: hit, height: CGFloat(splitter.length) * cellH)
.position(x: CGFloat(splitter.x) * cellW + drag,
y: (CGFloat(splitter.y) + CGFloat(splitter.length) / 2) * cellH)
} else {
bar(width: CGFloat(splitter.length) * cellW, height: hit)
.position(x: (CGFloat(splitter.x) + CGFloat(splitter.length) / 2) * cellW,
y: CGFloat(splitter.y) * cellH + drag)
}
}
.gesture(
DragGesture(minimumDistance: 2)
.onChanged { v in
dragging = true
drag = splitter.isVertical ? v.translation.width : v.translation.height
}
.onEnded { _ in
// tmux
let cell = splitter.isVertical ? cellW : cellH
let delta = cell > 0 ? Int((drag / cell).rounded()) : 0
let newSize = max(1, splitter.currentSize + delta)
if delta != 0 {
controller.resizePane(splitter.pane,
cols: splitter.isVertical ? newSize : nil,
rows: splitter.isVertical ? nil : newSize)
}
drag = 0
dragging = false
}
)
}
/// + 44×3pt
private func bar(width: CGFloat, height: CGFloat) -> some View {
ZStack {
Color.clear.frame(width: width, height: height)
RoundedRectangle(cornerRadius: 2)
.fill(dragging ? TXAccent.base : p.borderStrong)
.frame(width: splitter.isVertical ? 3 : 44,
height: splitter.isVertical ? 44 : 3)
.opacity(dragging ? 1 : 0.55)
}
.contentShape(Rectangle())
}
}
/// pane + + pane 2pt