@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.
Files changed (64) hide show
  1. package/README.md +157 -0
  2. package/app/Lattices.app/Contents/Info.plist +24 -0
  3. package/app/Package.swift +13 -0
  4. package/app/Sources/App.swift +49 -0
  5. package/app/Sources/AppDelegate.swift +104 -0
  6. package/app/Sources/AppShellView.swift +62 -0
  7. package/app/Sources/AppTypeClassifier.swift +70 -0
  8. package/app/Sources/AppWindowShell.swift +63 -0
  9. package/app/Sources/CheatSheetHUD.swift +331 -0
  10. package/app/Sources/CommandModeState.swift +1341 -0
  11. package/app/Sources/CommandModeView.swift +1380 -0
  12. package/app/Sources/CommandModeWindow.swift +192 -0
  13. package/app/Sources/CommandPaletteView.swift +307 -0
  14. package/app/Sources/CommandPaletteWindow.swift +134 -0
  15. package/app/Sources/DaemonProtocol.swift +101 -0
  16. package/app/Sources/DaemonServer.swift +406 -0
  17. package/app/Sources/DesktopModel.swift +121 -0
  18. package/app/Sources/DesktopModelTypes.swift +71 -0
  19. package/app/Sources/DiagnosticLog.swift +253 -0
  20. package/app/Sources/EventBus.swift +29 -0
  21. package/app/Sources/HotkeyManager.swift +249 -0
  22. package/app/Sources/HotkeyStore.swift +330 -0
  23. package/app/Sources/InventoryManager.swift +35 -0
  24. package/app/Sources/InventoryPath.swift +43 -0
  25. package/app/Sources/KeyRecorderView.swift +210 -0
  26. package/app/Sources/LatticesApi.swift +915 -0
  27. package/app/Sources/MainView.swift +507 -0
  28. package/app/Sources/MainWindow.swift +70 -0
  29. package/app/Sources/OrphanRow.swift +129 -0
  30. package/app/Sources/PaletteCommand.swift +409 -0
  31. package/app/Sources/PermissionChecker.swift +115 -0
  32. package/app/Sources/Preferences.swift +48 -0
  33. package/app/Sources/ProcessModel.swift +199 -0
  34. package/app/Sources/ProcessQuery.swift +151 -0
  35. package/app/Sources/Project.swift +28 -0
  36. package/app/Sources/ProjectRow.swift +368 -0
  37. package/app/Sources/ProjectScanner.swift +121 -0
  38. package/app/Sources/ScreenMapState.swift +2397 -0
  39. package/app/Sources/ScreenMapView.swift +2817 -0
  40. package/app/Sources/ScreenMapWindowController.swift +89 -0
  41. package/app/Sources/SessionManager.swift +72 -0
  42. package/app/Sources/SettingsView.swift +641 -0
  43. package/app/Sources/SettingsWindow.swift +20 -0
  44. package/app/Sources/TabGroupRow.swift +178 -0
  45. package/app/Sources/Terminal.swift +259 -0
  46. package/app/Sources/TerminalQuery.swift +156 -0
  47. package/app/Sources/TerminalSynthesizer.swift +200 -0
  48. package/app/Sources/Theme.swift +124 -0
  49. package/app/Sources/TilePickerView.swift +209 -0
  50. package/app/Sources/TmuxModel.swift +53 -0
  51. package/app/Sources/TmuxQuery.swift +81 -0
  52. package/app/Sources/WindowTiler.swift +1752 -0
  53. package/app/Sources/WorkspaceManager.swift +434 -0
  54. package/bin/daemon-client.js +187 -0
  55. package/bin/lattices-app.js +205 -0
  56. package/bin/lattices.js +1295 -0
  57. package/docs/api.md +707 -0
  58. package/docs/app.md +250 -0
  59. package/docs/concepts.md +225 -0
  60. package/docs/config.md +234 -0
  61. package/docs/layers.md +317 -0
  62. package/docs/overview.md +74 -0
  63. package/docs/quickstart.md +82 -0
  64. 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,13 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "Lattices",
6
+ platforms: [.macOS(.v13)],
7
+ targets: [
8
+ .executableTarget(
9
+ name: "Lattices",
10
+ path: "Sources"
11
+ )
12
+ ]
13
+ )
@@ -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
+ }