@arach/lattices 0.1.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 +157 -0
- package/app/Lattices.app/Contents/Info.plist +24 -0
- package/app/Package.swift +13 -0
- package/app/Sources/App.swift +49 -0
- package/app/Sources/AppDelegate.swift +104 -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 +331 -0
- package/app/Sources/CommandModeState.swift +1341 -0
- package/app/Sources/CommandModeView.swift +1380 -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 +406 -0
- package/app/Sources/DesktopModel.swift +121 -0
- package/app/Sources/DesktopModelTypes.swift +71 -0
- package/app/Sources/DiagnosticLog.swift +253 -0
- package/app/Sources/EventBus.swift +29 -0
- package/app/Sources/HotkeyManager.swift +249 -0
- package/app/Sources/HotkeyStore.swift +330 -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 +915 -0
- package/app/Sources/MainView.swift +507 -0
- package/app/Sources/MainWindow.swift +70 -0
- package/app/Sources/OrphanRow.swift +129 -0
- package/app/Sources/PaletteCommand.swift +409 -0
- package/app/Sources/PermissionChecker.swift +115 -0
- package/app/Sources/Preferences.swift +48 -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 +121 -0
- package/app/Sources/ScreenMapState.swift +2397 -0
- package/app/Sources/ScreenMapView.swift +2817 -0
- package/app/Sources/ScreenMapWindowController.swift +89 -0
- package/app/Sources/SessionManager.swift +72 -0
- package/app/Sources/SettingsView.swift +641 -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 +124 -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 +1752 -0
- package/app/Sources/WorkspaceManager.swift +434 -0
- package/bin/daemon-client.js +187 -0
- package/bin/lattices-app.js +205 -0
- package/bin/lattices.js +1295 -0
- package/docs/api.md +707 -0
- package/docs/app.md +250 -0
- package/docs/concepts.md +225 -0
- package/docs/config.md +234 -0
- package/docs/layers.md +317 -0
- package/docs/overview.md +74 -0
- package/docs/quickstart.md +82 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<picture>
|
|
2
|
+
<img alt="lattice" src="www/public/og.png" />
|
|
3
|
+
</picture>
|
|
4
|
+
|
|
5
|
+
# lattice
|
|
6
|
+
|
|
7
|
+
Declarative tmux sessions for developers.
|
|
8
|
+
|
|
9
|
+
One command to create a named tmux session with your tools running. Auto-detects your stack, fully configurable with `.lattice.json`, and a native macOS menu bar app.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install -g lattice
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
cd my-project
|
|
21
|
+
lattice
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That's it. lattice creates a tmux session named after your project with Claude Code on the left and your dev server on the right. It detects your package manager and dev command automatically.
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
1. **Run `lattice`** in any project directory
|
|
29
|
+
2. A named tmux session is created with configured panes
|
|
30
|
+
3. Commands start running in each pane immediately
|
|
31
|
+
4. Detach with `Ctrl+b d`, reattach by running `lattice` again
|
|
32
|
+
5. Sessions persist in the background until you kill them
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
Drop a `.lattice.json` in your project root:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"ensure": true,
|
|
41
|
+
"panes": [
|
|
42
|
+
{ "name": "claude", "cmd": "claude", "size": 60 },
|
|
43
|
+
{ "name": "server", "cmd": "pnpm dev" },
|
|
44
|
+
{ "name": "tests", "cmd": "pnpm test --watch" }
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Pane options
|
|
50
|
+
|
|
51
|
+
| Field | Description |
|
|
52
|
+
|--------|------------------------------------------|
|
|
53
|
+
| `name` | Label for the pane (for your reference) |
|
|
54
|
+
| `cmd` | Command to run in the pane |
|
|
55
|
+
| `size` | Width % for the first pane (default: 60) |
|
|
56
|
+
|
|
57
|
+
### Session options
|
|
58
|
+
|
|
59
|
+
| Field | Description |
|
|
60
|
+
|-----------|-----------------------------------------------------------------------------|
|
|
61
|
+
| `ensure` | Auto-restart exited commands on reattach |
|
|
62
|
+
| `prefill` | Type exited commands into panes on reattach without running (you hit Enter) |
|
|
63
|
+
|
|
64
|
+
### Layouts
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
2 panes — side-by-side 3+ panes — main-vertical
|
|
68
|
+
|
|
69
|
+
┌──────────┬─────────┐ ┌──────────┬─────────┐
|
|
70
|
+
│ claude │ server │ │ claude │ server │
|
|
71
|
+
│ (60%) │ (40%) │ │ (60%) ├─────────┤
|
|
72
|
+
└──────────┴─────────┘ │ │ tests │
|
|
73
|
+
└──────────┴─────────┘
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Auto-detection
|
|
77
|
+
|
|
78
|
+
Without a config file, lattice reads your `package.json` and picks the right command:
|
|
79
|
+
|
|
80
|
+
- Checks `scripts.dev`, `scripts.start`, `scripts.serve`, `scripts.watch`
|
|
81
|
+
- Detects package manager from lock files (pnpm, bun, yarn, npm)
|
|
82
|
+
- Falls back to a shell if no dev command is found
|
|
83
|
+
|
|
84
|
+
## Menu bar app
|
|
85
|
+
|
|
86
|
+
A macOS companion app for managing sessions without touching the terminal.
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
lattice app # Launch (builds from source or downloads binary)
|
|
90
|
+
lattice app build # Force rebuild from source
|
|
91
|
+
lattice app quit # Stop the menu bar app
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Features:
|
|
95
|
+
- See all projects and their session status at a glance
|
|
96
|
+
- Launch, attach, or detach sessions with a click
|
|
97
|
+
- **Command palette** (`Cmd+Shift+M`): Raycast-style launcher for all actions — fuzzy search, keyboard navigation, instant access to projects, window tiling, and settings
|
|
98
|
+
- Auto-scans your project directories
|
|
99
|
+
- Built with SwiftUI, runs natively on macOS
|
|
100
|
+
|
|
101
|
+
The app tries to compile from source first (requires Xcode CLI tools), falling back to a pre-built arm64 binary from GitHub releases.
|
|
102
|
+
|
|
103
|
+
## Tab groups
|
|
104
|
+
|
|
105
|
+
Bundle related projects as tabs within a single terminal window.
|
|
106
|
+
Configure in `~/.lattice/workspace.json`:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"name": "my-setup",
|
|
111
|
+
"groups": [
|
|
112
|
+
{
|
|
113
|
+
"id": "talkie",
|
|
114
|
+
"label": "Talkie",
|
|
115
|
+
"tabs": [
|
|
116
|
+
{ "path": "/Users/you/dev/talkie-ios", "label": "iOS" },
|
|
117
|
+
{ "path": "/Users/you/dev/talkie-web", "label": "Website" },
|
|
118
|
+
{ "path": "/Users/you/dev/talkie-api", "label": "API" }
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Each tab gets its own tmux window with pane layout from its `.lattice.json`.
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
lattice groups # List groups with status
|
|
129
|
+
lattice group talkie # Launch or attach
|
|
130
|
+
lattice tab talkie iOS # Switch to a tab
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Groups can also be referenced in [workspace layers](https://lattice.dev/docs/layers) to tile a whole group into a screen position.
|
|
134
|
+
|
|
135
|
+
## CLI reference
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
lattice Create or reattach to a session for the current project
|
|
139
|
+
lattice init Generate a .lattice.json config
|
|
140
|
+
lattice ls List active tmux sessions
|
|
141
|
+
lattice kill [name] Kill a session (defaults to current project)
|
|
142
|
+
lattice group [id] List tab groups or launch/attach a group
|
|
143
|
+
lattice groups List all tab groups with status
|
|
144
|
+
lattice tab <group> [tab] Switch tab within a group (by label or index)
|
|
145
|
+
lattice app Launch the menu bar companion app
|
|
146
|
+
lattice help Show help
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Requirements
|
|
150
|
+
|
|
151
|
+
- **tmux** — `brew install tmux`
|
|
152
|
+
- **Node.js** 18+
|
|
153
|
+
- **macOS** (the CLI is macOS-only, the menu bar app requires arm64)
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleIdentifier</key>
|
|
6
|
+
<string>com.arach.lattices</string>
|
|
7
|
+
<key>CFBundleName</key>
|
|
8
|
+
<string>Lattices</string>
|
|
9
|
+
<key>CFBundleExecutable</key>
|
|
10
|
+
<string>Lattices</string>
|
|
11
|
+
<key>CFBundleIconFile</key>
|
|
12
|
+
<string>AppIcon</string>
|
|
13
|
+
<key>CFBundlePackageType</key>
|
|
14
|
+
<string>APPL</string>
|
|
15
|
+
<key>CFBundleVersion</key>
|
|
16
|
+
<string>1</string>
|
|
17
|
+
<key>CFBundleShortVersionString</key>
|
|
18
|
+
<string>0.1.0</string>
|
|
19
|
+
<key>LSUIElement</key>
|
|
20
|
+
<true/>
|
|
21
|
+
<key>NSSupportsAutomaticTermination</key>
|
|
22
|
+
<true/>
|
|
23
|
+
</dict>
|
|
24
|
+
</plist>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
@main
|
|
4
|
+
struct LatticesApp: App {
|
|
5
|
+
@NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
|
|
6
|
+
@StateObject private var scanner = ProjectScanner.shared
|
|
7
|
+
|
|
8
|
+
/// 3×3 grid icon for the menu bar — L-shape bright, rest dim (template for auto light/dark)
|
|
9
|
+
private static let menuBarIcon: NSImage = {
|
|
10
|
+
let size: CGFloat = 18
|
|
11
|
+
let img = NSImage(size: NSSize(width: size, height: size), flipped: false) { _ in
|
|
12
|
+
let pad: CGFloat = 2
|
|
13
|
+
let gap: CGFloat = 1.5
|
|
14
|
+
let cellSize = (size - 2 * pad - 2 * gap) / 3
|
|
15
|
+
|
|
16
|
+
// L-shape: left column + bottom row are solid, rest are dim
|
|
17
|
+
let solidCells: Set<Int> = [0, 3, 6, 7, 8] // (0,0),(1,0),(2,0),(2,1),(2,2)
|
|
18
|
+
|
|
19
|
+
for row in 0..<3 {
|
|
20
|
+
for col in 0..<3 {
|
|
21
|
+
let idx = row * 3 + col
|
|
22
|
+
let x = pad + CGFloat(col) * (cellSize + gap)
|
|
23
|
+
let y = pad + CGFloat(row) * (cellSize + gap)
|
|
24
|
+
let rect = NSRect(x: x, y: y, width: cellSize, height: cellSize)
|
|
25
|
+
|
|
26
|
+
if solidCells.contains(idx) {
|
|
27
|
+
NSColor.black.setFill()
|
|
28
|
+
} else {
|
|
29
|
+
NSColor.black.withAlphaComponent(0.25).setFill()
|
|
30
|
+
}
|
|
31
|
+
let path = NSBezierPath(roundedRect: rect, xRadius: 0.8, yRadius: 0.8)
|
|
32
|
+
path.fill()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
img.isTemplate = true
|
|
38
|
+
return img
|
|
39
|
+
}()
|
|
40
|
+
|
|
41
|
+
var body: some Scene {
|
|
42
|
+
MenuBarExtra {
|
|
43
|
+
MainView(scanner: scanner)
|
|
44
|
+
} label: {
|
|
45
|
+
Image(nsImage: Self.menuBarIcon)
|
|
46
|
+
}
|
|
47
|
+
.menuBarExtraStyle(.window)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
|
|
3
|
+
/// Registers the global hotkey (Cmd+Shift+D) on launch.
|
|
4
|
+
/// The menu bar itself is handled by SwiftUI's MenuBarExtra.
|
|
5
|
+
class AppDelegate: NSObject, NSApplicationDelegate {
|
|
6
|
+
|
|
7
|
+
/// Toggle between .accessory (hidden from Dock/Cmd+Tab) and .regular (visible)
|
|
8
|
+
/// based on whether any managed windows are open.
|
|
9
|
+
/// Call this after showing or dismissing a window.
|
|
10
|
+
static func updateActivationPolicy() {
|
|
11
|
+
let hasVisibleWindow =
|
|
12
|
+
CommandModeWindow.shared.isVisible ||
|
|
13
|
+
CommandPaletteWindow.shared.isVisible ||
|
|
14
|
+
MainWindow.shared.isVisible ||
|
|
15
|
+
ScreenMapWindowController.shared.isVisible
|
|
16
|
+
let desired: NSApplication.ActivationPolicy = hasVisibleWindow ? .regular : .accessory
|
|
17
|
+
if NSApp.activationPolicy() != desired {
|
|
18
|
+
NSApp.setActivationPolicy(desired)
|
|
19
|
+
if desired == .regular {
|
|
20
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
26
|
+
NSApp.setActivationPolicy(.accessory)
|
|
27
|
+
NSApp.appearance = NSAppearance(named: .darkAqua)
|
|
28
|
+
|
|
29
|
+
CommandPaletteWindow.shared.configure(scanner: ProjectScanner.shared)
|
|
30
|
+
|
|
31
|
+
// Register all hotkeys via HotkeyStore (user-configurable bindings)
|
|
32
|
+
let store = HotkeyStore.shared
|
|
33
|
+
store.register(action: .palette) { CommandPaletteWindow.shared.toggle() }
|
|
34
|
+
store.register(action: .screenMap) { ScreenMapWindowController.shared.toggle() }
|
|
35
|
+
store.register(action: .bezel) { WindowBezel.showBezelForFrontmostWindow() }
|
|
36
|
+
store.register(action: .cheatSheet) { CheatSheetHUD.shared.toggle() }
|
|
37
|
+
|
|
38
|
+
// Layer-switching hotkeys
|
|
39
|
+
let workspace = WorkspaceManager.shared
|
|
40
|
+
let layerCount = (workspace.config?.layers ?? []).count
|
|
41
|
+
for (i, action) in HotkeyAction.layerActions.prefix(layerCount).enumerated() {
|
|
42
|
+
let index = i
|
|
43
|
+
store.register(action: action) { workspace.tileLayer(index: index) }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Tiling hotkeys
|
|
47
|
+
let tileMap: [(HotkeyAction, TilePosition)] = [
|
|
48
|
+
(.tileLeft, .left), (.tileRight, .right),
|
|
49
|
+
(.tileMaximize, .maximize), (.tileCenter, .center),
|
|
50
|
+
(.tileTopLeft, .topLeft), (.tileTopRight, .topRight),
|
|
51
|
+
(.tileBottomLeft, .bottomLeft), (.tileBottomRight, .bottomRight),
|
|
52
|
+
(.tileTop, .top), (.tileBottom, .bottom),
|
|
53
|
+
(.tileLeftThird, .leftThird), (.tileCenterThird, .centerThird),
|
|
54
|
+
(.tileRightThird, .rightThird),
|
|
55
|
+
]
|
|
56
|
+
for (action, position) in tileMap {
|
|
57
|
+
store.register(action: action) { WindowTiler.tileFrontmostViaAX(to: position) }
|
|
58
|
+
}
|
|
59
|
+
store.register(action: .tileDistribute) { WindowTiler.distributeVisible() }
|
|
60
|
+
|
|
61
|
+
// Style the MenuBarExtra panel when it appears
|
|
62
|
+
NotificationCenter.default.addObserver(
|
|
63
|
+
forName: NSWindow.didBecomeKeyNotification,
|
|
64
|
+
object: nil,
|
|
65
|
+
queue: .main
|
|
66
|
+
) { note in
|
|
67
|
+
guard let panel = note.object as? NSPanel else { return }
|
|
68
|
+
Self.stylePanel(panel)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check macOS permissions (Accessibility, Screen Recording)
|
|
72
|
+
PermissionChecker.shared.check()
|
|
73
|
+
|
|
74
|
+
// Start daemon services
|
|
75
|
+
DesktopModel.shared.start()
|
|
76
|
+
TmuxModel.shared.start()
|
|
77
|
+
ProcessModel.shared.start()
|
|
78
|
+
LatticesApi.setup()
|
|
79
|
+
DaemonServer.shared.start()
|
|
80
|
+
|
|
81
|
+
// --diagnostics flag: auto-open diagnostics panel on launch
|
|
82
|
+
if CommandLine.arguments.contains("--diagnostics") {
|
|
83
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
84
|
+
DiagnosticWindow.shared.show()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --screen-map flag: auto-open screen map on launch
|
|
89
|
+
if CommandLine.arguments.contains("--screen-map") {
|
|
90
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
91
|
+
ScreenMapWindowController.shared.show()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private static func stylePanel(_ panel: NSPanel) {
|
|
97
|
+
let bg = NSColor(red: 0.11, green: 0.11, blue: 0.12, alpha: 1.0)
|
|
98
|
+
panel.backgroundColor = bg
|
|
99
|
+
panel.isOpaque = false
|
|
100
|
+
panel.hasShadow = true
|
|
101
|
+
panel.becomesKeyOnlyIfNeeded = false
|
|
102
|
+
panel.invalidateShadow()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
// MARK: - Navigation Pages
|
|
4
|
+
|
|
5
|
+
enum AppPage: String, CaseIterable {
|
|
6
|
+
case screenMap
|
|
7
|
+
case settings
|
|
8
|
+
case docs
|
|
9
|
+
|
|
10
|
+
var label: String {
|
|
11
|
+
switch self {
|
|
12
|
+
case .screenMap: return "Screen Map"
|
|
13
|
+
case .settings: return "Settings"
|
|
14
|
+
case .docs: return "Docs"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var icon: String {
|
|
19
|
+
switch self {
|
|
20
|
+
case .screenMap: return "rectangle.3.group"
|
|
21
|
+
case .settings: return "gearshape"
|
|
22
|
+
case .docs: return "book"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// MARK: - App Shell View
|
|
28
|
+
|
|
29
|
+
struct AppShellView: View {
|
|
30
|
+
@ObservedObject var controller: ScreenMapController
|
|
31
|
+
@ObservedObject var windowController = ScreenMapWindowController.shared
|
|
32
|
+
|
|
33
|
+
var body: some View {
|
|
34
|
+
contentArea
|
|
35
|
+
.background(Palette.bg)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// MARK: - Content Area
|
|
39
|
+
|
|
40
|
+
@ViewBuilder
|
|
41
|
+
private var contentArea: some View {
|
|
42
|
+
switch windowController.activePage {
|
|
43
|
+
case .screenMap:
|
|
44
|
+
ScreenMapView(controller: controller, onNavigate: { page in
|
|
45
|
+
windowController.activePage = page
|
|
46
|
+
})
|
|
47
|
+
case .settings:
|
|
48
|
+
SettingsContentView(
|
|
49
|
+
prefs: Preferences.shared,
|
|
50
|
+
scanner: ProjectScanner.shared,
|
|
51
|
+
onBack: { windowController.activePage = .screenMap; controller.enter() }
|
|
52
|
+
)
|
|
53
|
+
case .docs:
|
|
54
|
+
SettingsContentView(
|
|
55
|
+
page: .docs,
|
|
56
|
+
prefs: Preferences.shared,
|
|
57
|
+
scanner: ProjectScanner.shared,
|
|
58
|
+
onBack: { windowController.activePage = .screenMap; controller.enter() }
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -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
|
+
}
|