@lattices/cli 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -0
- package/app/Lattices.app/Contents/Info.plist +24 -0
- package/app/Package.swift +13 -0
- package/app/Sources/AccessibilityTextExtractor.swift +111 -0
- package/app/Sources/ActionRow.swift +61 -0
- package/app/Sources/App.swift +10 -0
- package/app/Sources/AppDelegate.swift +242 -0
- package/app/Sources/AppShellView.swift +62 -0
- package/app/Sources/AppTypeClassifier.swift +70 -0
- package/app/Sources/AppWindowShell.swift +63 -0
- package/app/Sources/CheatSheetHUD.swift +332 -0
- package/app/Sources/CommandModeState.swift +1362 -0
- package/app/Sources/CommandModeView.swift +1405 -0
- package/app/Sources/CommandModeWindow.swift +192 -0
- package/app/Sources/CommandPaletteView.swift +307 -0
- package/app/Sources/CommandPaletteWindow.swift +134 -0
- package/app/Sources/DaemonProtocol.swift +101 -0
- package/app/Sources/DaemonServer.swift +414 -0
- package/app/Sources/DesktopModel.swift +149 -0
- package/app/Sources/DesktopModelTypes.swift +71 -0
- package/app/Sources/DiagnosticLog.swift +271 -0
- package/app/Sources/EventBus.swift +30 -0
- package/app/Sources/HotkeyManager.swift +254 -0
- package/app/Sources/HotkeyStore.swift +338 -0
- package/app/Sources/InventoryManager.swift +35 -0
- package/app/Sources/InventoryPath.swift +43 -0
- package/app/Sources/KeyRecorderView.swift +210 -0
- package/app/Sources/LatticesApi.swift +1234 -0
- package/app/Sources/LayerBezel.swift +203 -0
- package/app/Sources/MainView.swift +479 -0
- package/app/Sources/MainWindow.swift +83 -0
- package/app/Sources/OcrModel.swift +430 -0
- package/app/Sources/OcrStore.swift +329 -0
- package/app/Sources/OmniSearchState.swift +283 -0
- package/app/Sources/OmniSearchView.swift +288 -0
- package/app/Sources/OmniSearchWindow.swift +105 -0
- package/app/Sources/OrphanRow.swift +129 -0
- package/app/Sources/PaletteCommand.swift +419 -0
- package/app/Sources/PermissionChecker.swift +125 -0
- package/app/Sources/Preferences.swift +99 -0
- package/app/Sources/ProcessModel.swift +199 -0
- package/app/Sources/ProcessQuery.swift +151 -0
- package/app/Sources/Project.swift +28 -0
- package/app/Sources/ProjectRow.swift +368 -0
- package/app/Sources/ProjectScanner.swift +128 -0
- package/app/Sources/ScreenMapState.swift +2387 -0
- package/app/Sources/ScreenMapView.swift +2820 -0
- package/app/Sources/ScreenMapWindowController.swift +89 -0
- package/app/Sources/SessionManager.swift +72 -0
- package/app/Sources/SettingsView.swift +1064 -0
- package/app/Sources/SettingsWindow.swift +20 -0
- package/app/Sources/TabGroupRow.swift +178 -0
- package/app/Sources/Terminal.swift +259 -0
- package/app/Sources/TerminalQuery.swift +156 -0
- package/app/Sources/TerminalSynthesizer.swift +200 -0
- package/app/Sources/Theme.swift +163 -0
- package/app/Sources/TilePickerView.swift +209 -0
- package/app/Sources/TmuxModel.swift +53 -0
- package/app/Sources/TmuxQuery.swift +81 -0
- package/app/Sources/WindowTiler.swift +1778 -0
- package/app/Sources/WorkspaceManager.swift +575 -0
- package/bin/client.js +4 -0
- package/bin/daemon-client.js +187 -0
- package/bin/lattices-app.js +221 -0
- package/bin/lattices.js +1551 -0
- package/docs/api.md +924 -0
- package/docs/app.md +297 -0
- package/docs/concepts.md +135 -0
- package/docs/config.md +245 -0
- package/docs/layers.md +410 -0
- package/docs/ocr.md +185 -0
- package/docs/overview.md +94 -0
- package/docs/quickstart.md +75 -0
- package/package.json +42 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum AppType: String, CaseIterable {
|
|
4
|
+
case terminal
|
|
5
|
+
case editor
|
|
6
|
+
case browser
|
|
7
|
+
case chat
|
|
8
|
+
case media
|
|
9
|
+
case design
|
|
10
|
+
case system
|
|
11
|
+
case other
|
|
12
|
+
|
|
13
|
+
var label: String { rawValue }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
enum AppTypeClassifier {
|
|
17
|
+
private static let nameMap: [String: AppType] = [
|
|
18
|
+
// Terminals
|
|
19
|
+
"iTerm2": .terminal, "Terminal": .terminal, "Alacritty": .terminal,
|
|
20
|
+
"kitty": .terminal, "Warp": .terminal, "Hyper": .terminal,
|
|
21
|
+
"WezTerm": .terminal, "Rio": .terminal, "Ghostty": .terminal,
|
|
22
|
+
|
|
23
|
+
// Editors / IDEs
|
|
24
|
+
"Xcode": .editor, "Code": .editor, "Visual Studio Code": .editor,
|
|
25
|
+
"Cursor": .editor, "Sublime Text": .editor, "TextEdit": .editor,
|
|
26
|
+
"Nova": .editor, "BBEdit": .editor, "Zed": .editor,
|
|
27
|
+
"IntelliJ IDEA": .editor, "WebStorm": .editor, "PyCharm": .editor,
|
|
28
|
+
"CLion": .editor, "GoLand": .editor, "RustRover": .editor,
|
|
29
|
+
"Android Studio": .editor, "Fleet": .editor, "Neovide": .editor,
|
|
30
|
+
|
|
31
|
+
// Browsers
|
|
32
|
+
"Safari": .browser, "Google Chrome": .browser, "Firefox": .browser,
|
|
33
|
+
"Arc": .browser, "Brave Browser": .browser, "Microsoft Edge": .browser,
|
|
34
|
+
"Orion": .browser, "Vivaldi": .browser, "Opera": .browser,
|
|
35
|
+
"Chrome": .browser, "Zen Browser": .browser,
|
|
36
|
+
|
|
37
|
+
// Chat / Communication
|
|
38
|
+
"Slack": .chat, "Discord": .chat, "Messages": .chat,
|
|
39
|
+
"Telegram": .chat, "WhatsApp": .chat, "Signal": .chat,
|
|
40
|
+
"Teams": .chat, "Microsoft Teams": .chat, "Zoom": .chat,
|
|
41
|
+
"FaceTime": .chat, "Skype": .chat,
|
|
42
|
+
|
|
43
|
+
// Media
|
|
44
|
+
"Spotify": .media, "Music": .media, "QuickTime Player": .media,
|
|
45
|
+
"VLC": .media, "IINA": .media, "Podcasts": .media,
|
|
46
|
+
"Photos": .media, "Preview": .media, "mpv": .media,
|
|
47
|
+
|
|
48
|
+
// Design
|
|
49
|
+
"Figma": .design, "Sketch": .design, "Pixelmator Pro": .design,
|
|
50
|
+
"Affinity Designer 2": .design, "Affinity Photo 2": .design,
|
|
51
|
+
"Adobe Photoshop": .design, "Adobe Illustrator": .design,
|
|
52
|
+
"Blender": .design, "OmniGraffle": .design,
|
|
53
|
+
|
|
54
|
+
// System
|
|
55
|
+
"Finder": .system, "System Preferences": .system, "System Settings": .system,
|
|
56
|
+
"Activity Monitor": .system, "Console": .system, "Disk Utility": .system,
|
|
57
|
+
"Keychain Access": .system,
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
static func classify(_ appName: String) -> AppType {
|
|
61
|
+
if let exact = nameMap[appName] { return exact }
|
|
62
|
+
// Substring fallback
|
|
63
|
+
let lower = appName.lowercased()
|
|
64
|
+
if lower.contains("terminal") || lower.contains("term") { return .terminal }
|
|
65
|
+
if lower.contains("code") || lower.contains("studio") || lower.contains("edit") { return .editor }
|
|
66
|
+
if lower.contains("chrome") || lower.contains("firefox") || lower.contains("safari") || lower.contains("browser") { return .browser }
|
|
67
|
+
if lower.contains("slack") || lower.contains("discord") || lower.contains("chat") || lower.contains("teams") { return .chat }
|
|
68
|
+
return .other
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
/// Shared factory for standalone NSWindow chrome.
|
|
5
|
+
/// Every managed window (Screen Map, Settings, Diagnostics, etc.) uses this
|
|
6
|
+
/// to get consistent title bar styling, dark appearance, and positioning.
|
|
7
|
+
struct AppWindowShell {
|
|
8
|
+
|
|
9
|
+
struct Config {
|
|
10
|
+
var title: String
|
|
11
|
+
var titleVisible: Bool = true
|
|
12
|
+
var initialSize: NSSize
|
|
13
|
+
var minSize: NSSize
|
|
14
|
+
var maxSize: NSSize
|
|
15
|
+
var miniaturizable: Bool = true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Create a styled NSWindow hosting a SwiftUI root view.
|
|
19
|
+
static func makeWindow<V: View>(config: Config, rootView: V) -> NSWindow {
|
|
20
|
+
let hosting = NSHostingView(rootView: rootView.preferredColorScheme(.dark))
|
|
21
|
+
hosting.frame = NSRect(origin: .zero, size: config.initialSize)
|
|
22
|
+
|
|
23
|
+
var styleMask: NSWindow.StyleMask = [.titled, .closable, .resizable]
|
|
24
|
+
if config.miniaturizable { styleMask.insert(.miniaturizable) }
|
|
25
|
+
|
|
26
|
+
let w = NSWindow(
|
|
27
|
+
contentRect: NSRect(origin: .zero, size: config.initialSize),
|
|
28
|
+
styleMask: styleMask,
|
|
29
|
+
backing: .buffered,
|
|
30
|
+
defer: false
|
|
31
|
+
)
|
|
32
|
+
w.contentView = hosting
|
|
33
|
+
w.title = config.title
|
|
34
|
+
w.titlebarAppearsTransparent = true
|
|
35
|
+
w.titleVisibility = config.titleVisible ? .visible : .hidden
|
|
36
|
+
w.isReleasedWhenClosed = false
|
|
37
|
+
w.backgroundColor = NSColor(red: 0.11, green: 0.11, blue: 0.12, alpha: 1.0)
|
|
38
|
+
w.appearance = NSAppearance(named: .darkAqua)
|
|
39
|
+
w.minSize = config.minSize
|
|
40
|
+
w.maxSize = config.maxSize
|
|
41
|
+
return w
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Center the window on screen, nudged 8% above vertical center.
|
|
45
|
+
/// Clamps to 92% screen width / 85% screen height.
|
|
46
|
+
static func positionCentered(_ window: NSWindow) {
|
|
47
|
+
guard let screen = NSScreen.main else { return }
|
|
48
|
+
let frame = screen.visibleFrame
|
|
49
|
+
let size = window.frame.size
|
|
50
|
+
let w = min(size.width, frame.width * 0.92)
|
|
51
|
+
let h = min(size.height, frame.height * 0.85)
|
|
52
|
+
let x = frame.midX - w / 2
|
|
53
|
+
let y = frame.midY - h / 2 + (frame.height * 0.08)
|
|
54
|
+
window.setFrame(NSRect(x: x, y: y, width: w, height: h), display: true)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Bring the window to front and update activation policy.
|
|
58
|
+
static func present(_ window: NSWindow) {
|
|
59
|
+
window.makeKeyAndOrderFront(nil)
|
|
60
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
61
|
+
AppDelegate.updateActivationPolicy()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
// MARK: - CheatSheetHUD (singleton window controller)
|
|
5
|
+
|
|
6
|
+
final class CheatSheetHUD {
|
|
7
|
+
static let shared = CheatSheetHUD()
|
|
8
|
+
|
|
9
|
+
private var panel: NSPanel?
|
|
10
|
+
private var localMonitor: Any?
|
|
11
|
+
private var globalMonitor: Any?
|
|
12
|
+
|
|
13
|
+
var isVisible: Bool { panel?.isVisible ?? false }
|
|
14
|
+
|
|
15
|
+
func toggle() {
|
|
16
|
+
if isVisible {
|
|
17
|
+
dismiss()
|
|
18
|
+
} else {
|
|
19
|
+
show()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func show() {
|
|
24
|
+
guard panel == nil else { return }
|
|
25
|
+
|
|
26
|
+
let view = CheatSheetView()
|
|
27
|
+
.preferredColorScheme(.dark)
|
|
28
|
+
|
|
29
|
+
let hosting = NSHostingView(rootView: view)
|
|
30
|
+
|
|
31
|
+
let p = NSPanel(
|
|
32
|
+
contentRect: NSRect(x: 0, y: 0, width: 520, height: 420),
|
|
33
|
+
styleMask: [.borderless, .nonactivatingPanel],
|
|
34
|
+
backing: .buffered,
|
|
35
|
+
defer: false
|
|
36
|
+
)
|
|
37
|
+
p.isOpaque = false
|
|
38
|
+
p.backgroundColor = .clear
|
|
39
|
+
p.level = .floating
|
|
40
|
+
p.hasShadow = true
|
|
41
|
+
p.hidesOnDeactivate = false
|
|
42
|
+
p.isReleasedWhenClosed = false
|
|
43
|
+
p.isMovableByWindowBackground = false
|
|
44
|
+
p.contentView = hosting
|
|
45
|
+
|
|
46
|
+
// Center on the screen containing the mouse cursor
|
|
47
|
+
let mouseLocation = NSEvent.mouseLocation
|
|
48
|
+
let screen = NSScreen.screens.first(where: { $0.frame.contains(mouseLocation) }) ?? NSScreen.main ?? NSScreen.screens.first!
|
|
49
|
+
let screenFrame = screen.visibleFrame
|
|
50
|
+
let x = screenFrame.midX - 260
|
|
51
|
+
let y = screenFrame.midY - 210
|
|
52
|
+
p.setFrameOrigin(NSPoint(x: x, y: y))
|
|
53
|
+
|
|
54
|
+
p.alphaValue = 0
|
|
55
|
+
p.orderFrontRegardless()
|
|
56
|
+
|
|
57
|
+
NSAnimationContext.runAnimationGroup { ctx in
|
|
58
|
+
ctx.duration = 0.15
|
|
59
|
+
p.animator().alphaValue = 1.0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
self.panel = p
|
|
63
|
+
installMonitors()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func dismiss() {
|
|
67
|
+
guard let p = panel else { return }
|
|
68
|
+
removeMonitors()
|
|
69
|
+
|
|
70
|
+
NSAnimationContext.runAnimationGroup({ ctx in
|
|
71
|
+
ctx.duration = 0.2
|
|
72
|
+
p.animator().alphaValue = 0
|
|
73
|
+
}) { [weak self] in
|
|
74
|
+
p.orderOut(nil)
|
|
75
|
+
self?.panel = nil
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// MARK: - Event monitors
|
|
80
|
+
|
|
81
|
+
private func installMonitors() {
|
|
82
|
+
// Escape key dismisses (global — panel is non-activating so keys go to frontmost app)
|
|
83
|
+
localMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { [weak self] event in
|
|
84
|
+
if event.keyCode == 53 { // Escape
|
|
85
|
+
self?.dismiss()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Click outside dismisses
|
|
90
|
+
globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] _ in
|
|
91
|
+
self?.dismiss()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private func removeMonitors() {
|
|
96
|
+
if let m = localMonitor { NSEvent.removeMonitor(m); localMonitor = nil }
|
|
97
|
+
if let m = globalMonitor { NSEvent.removeMonitor(m); globalMonitor = nil }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// MARK: - CheatSheetView
|
|
103
|
+
|
|
104
|
+
struct CheatSheetView: View {
|
|
105
|
+
@ObservedObject private var hotkeyStore = HotkeyStore.shared
|
|
106
|
+
|
|
107
|
+
var body: some View {
|
|
108
|
+
VStack(spacing: 0) {
|
|
109
|
+
// Title
|
|
110
|
+
HStack {
|
|
111
|
+
Text("KEYBOARD SHORTCUTS")
|
|
112
|
+
.font(Typo.pixel(14))
|
|
113
|
+
.foregroundColor(Palette.textDim)
|
|
114
|
+
.tracking(1)
|
|
115
|
+
Spacer()
|
|
116
|
+
}
|
|
117
|
+
.padding(.horizontal, 20)
|
|
118
|
+
.padding(.top, 16)
|
|
119
|
+
.padding(.bottom, 10)
|
|
120
|
+
|
|
121
|
+
Rectangle().fill(Palette.border).frame(height: 0.5)
|
|
122
|
+
|
|
123
|
+
// Two-column body
|
|
124
|
+
HStack(alignment: .top, spacing: 20) {
|
|
125
|
+
// Left column: Tiling
|
|
126
|
+
tilingColumn
|
|
127
|
+
|
|
128
|
+
Rectangle().fill(Palette.border).frame(width: 0.5)
|
|
129
|
+
|
|
130
|
+
// Right column: App + tmux
|
|
131
|
+
VStack(alignment: .leading, spacing: 16) {
|
|
132
|
+
appColumn
|
|
133
|
+
Rectangle().fill(Palette.border).frame(height: 0.5)
|
|
134
|
+
tmuxColumn
|
|
135
|
+
}
|
|
136
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
137
|
+
}
|
|
138
|
+
.padding(.horizontal, 20)
|
|
139
|
+
.padding(.vertical, 14)
|
|
140
|
+
|
|
141
|
+
Spacer(minLength: 0)
|
|
142
|
+
|
|
143
|
+
Rectangle().fill(Palette.border).frame(height: 0.5)
|
|
144
|
+
|
|
145
|
+
// Footer
|
|
146
|
+
HStack {
|
|
147
|
+
Spacer()
|
|
148
|
+
Text("Press ESC to dismiss")
|
|
149
|
+
.font(Typo.caption(10))
|
|
150
|
+
.foregroundColor(Palette.textMuted)
|
|
151
|
+
Spacer()
|
|
152
|
+
}
|
|
153
|
+
.padding(.vertical, 8)
|
|
154
|
+
}
|
|
155
|
+
.frame(width: 520, height: 420)
|
|
156
|
+
.background(
|
|
157
|
+
RoundedRectangle(cornerRadius: 12)
|
|
158
|
+
.fill(Palette.bg)
|
|
159
|
+
.overlay(
|
|
160
|
+
RoundedRectangle(cornerRadius: 12)
|
|
161
|
+
.strokeBorder(Palette.borderLit, lineWidth: 0.5)
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// MARK: - Tiling Column
|
|
168
|
+
|
|
169
|
+
private var tilingColumn: some View {
|
|
170
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
171
|
+
columnHeader("Tiling")
|
|
172
|
+
|
|
173
|
+
// 3x3 grid
|
|
174
|
+
VStack(spacing: 2) {
|
|
175
|
+
HStack(spacing: 2) {
|
|
176
|
+
tileCell(action: .tileTopLeft, label: "TL")
|
|
177
|
+
tileCell(action: .tileTop, label: "Top")
|
|
178
|
+
tileCell(action: .tileTopRight, label: "TR")
|
|
179
|
+
}
|
|
180
|
+
HStack(spacing: 2) {
|
|
181
|
+
tileCell(action: .tileLeft, label: "Left")
|
|
182
|
+
tileCell(action: .tileMaximize, label: "Max")
|
|
183
|
+
tileCell(action: .tileRight, label: "Right")
|
|
184
|
+
}
|
|
185
|
+
HStack(spacing: 2) {
|
|
186
|
+
tileCell(action: .tileBottomLeft, label: "BL")
|
|
187
|
+
tileCell(action: .tileBottom, label: "Bot")
|
|
188
|
+
tileCell(action: .tileBottomRight, label: "BR")
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
.padding(6)
|
|
192
|
+
.background(
|
|
193
|
+
RoundedRectangle(cornerRadius: 6)
|
|
194
|
+
.fill(Color.black.opacity(0.25))
|
|
195
|
+
.overlay(
|
|
196
|
+
RoundedRectangle(cornerRadius: 6)
|
|
197
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
// Thirds row
|
|
202
|
+
HStack(spacing: 2) {
|
|
203
|
+
tileCell(action: .tileLeftThird, label: "\u{2153}L")
|
|
204
|
+
tileCell(action: .tileCenterThird, label: "\u{2153}C")
|
|
205
|
+
tileCell(action: .tileRightThird, label: "\u{2153}R")
|
|
206
|
+
}
|
|
207
|
+
.padding(6)
|
|
208
|
+
.background(
|
|
209
|
+
RoundedRectangle(cornerRadius: 6)
|
|
210
|
+
.fill(Color.black.opacity(0.25))
|
|
211
|
+
.overlay(
|
|
212
|
+
RoundedRectangle(cornerRadius: 6)
|
|
213
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
// Center + Distribute
|
|
218
|
+
shortcutRow(action: .tileCenter)
|
|
219
|
+
shortcutRow(action: .tileDistribute)
|
|
220
|
+
}
|
|
221
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// MARK: - App Column
|
|
225
|
+
|
|
226
|
+
private var appColumn: some View {
|
|
227
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
228
|
+
columnHeader("App")
|
|
229
|
+
|
|
230
|
+
shortcutRow(action: .palette)
|
|
231
|
+
shortcutRow(action: .screenMap)
|
|
232
|
+
shortcutRow(action: .bezel)
|
|
233
|
+
shortcutRow(action: .cheatSheet)
|
|
234
|
+
shortcutRow(action: .desktopInventory)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// MARK: - tmux Column
|
|
239
|
+
|
|
240
|
+
private var tmuxColumn: some View {
|
|
241
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
242
|
+
columnHeader("Inside tmux")
|
|
243
|
+
|
|
244
|
+
tmuxRow("Detach", keys: ["Ctrl+B", "D"])
|
|
245
|
+
tmuxRow("Kill pane", keys: ["Ctrl+B", "X"])
|
|
246
|
+
tmuxRow("Pane left", keys: ["Ctrl+B", "\u{2190}"])
|
|
247
|
+
tmuxRow("Pane right", keys: ["Ctrl+B", "\u{2192}"])
|
|
248
|
+
tmuxRow("Zoom toggle", keys: ["Ctrl+B", "Z"])
|
|
249
|
+
tmuxRow("Scroll mode", keys: ["Ctrl+B", "["])
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// MARK: - Shared components
|
|
254
|
+
|
|
255
|
+
private func columnHeader(_ title: String) -> some View {
|
|
256
|
+
Text(title.uppercased())
|
|
257
|
+
.font(Typo.pixel(12))
|
|
258
|
+
.foregroundColor(Palette.textDim)
|
|
259
|
+
.tracking(1)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private func tileCell(action: HotkeyAction, label: String) -> some View {
|
|
263
|
+
let binding = hotkeyStore.bindings[action]
|
|
264
|
+
let badgeText = binding?.displayParts.last ?? ""
|
|
265
|
+
|
|
266
|
+
return VStack(spacing: 3) {
|
|
267
|
+
Text(label)
|
|
268
|
+
.font(Typo.caption(9))
|
|
269
|
+
.foregroundColor(Palette.textDim)
|
|
270
|
+
Text(badgeText)
|
|
271
|
+
.font(Typo.geistMonoBold(9))
|
|
272
|
+
.foregroundColor(Palette.text)
|
|
273
|
+
}
|
|
274
|
+
.frame(maxWidth: .infinity)
|
|
275
|
+
.frame(height: 38)
|
|
276
|
+
.background(
|
|
277
|
+
RoundedRectangle(cornerRadius: 4)
|
|
278
|
+
.fill(Palette.surface)
|
|
279
|
+
.overlay(
|
|
280
|
+
RoundedRectangle(cornerRadius: 4)
|
|
281
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private func shortcutRow(action: HotkeyAction) -> some View {
|
|
287
|
+
let binding = hotkeyStore.bindings[action]
|
|
288
|
+
return HStack(spacing: 8) {
|
|
289
|
+
if let parts = binding?.displayParts {
|
|
290
|
+
HStack(spacing: 3) {
|
|
291
|
+
ForEach(parts, id: \.self) { part in
|
|
292
|
+
keyBadge(part)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
Text(action.label)
|
|
297
|
+
.font(Typo.caption(11))
|
|
298
|
+
.foregroundColor(Palette.textDim)
|
|
299
|
+
Spacer()
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private func tmuxRow(_ label: String, keys: [String]) -> some View {
|
|
304
|
+
HStack(spacing: 8) {
|
|
305
|
+
HStack(spacing: 3) {
|
|
306
|
+
ForEach(keys, id: \.self) { key in
|
|
307
|
+
keyBadge(key)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
Text(label)
|
|
311
|
+
.font(Typo.caption(11))
|
|
312
|
+
.foregroundColor(Palette.textDim)
|
|
313
|
+
Spacer()
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private func keyBadge(_ key: String) -> some View {
|
|
318
|
+
Text(key)
|
|
319
|
+
.font(Typo.geistMonoBold(10))
|
|
320
|
+
.foregroundColor(Palette.text)
|
|
321
|
+
.padding(.horizontal, 6)
|
|
322
|
+
.padding(.vertical, 3)
|
|
323
|
+
.background(
|
|
324
|
+
RoundedRectangle(cornerRadius: 3)
|
|
325
|
+
.fill(Palette.surface)
|
|
326
|
+
.overlay(
|
|
327
|
+
RoundedRectangle(cornerRadius: 3)
|
|
328
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
}
|