@arach/lattices 0.1.0 → 0.2.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 +28 -28
- package/app/Sources/ActionRow.swift +61 -0
- package/app/Sources/App.swift +1 -40
- package/app/Sources/AppDelegate.swift +154 -24
- package/app/Sources/CheatSheetHUD.swift +1 -0
- package/app/Sources/CommandModeState.swift +40 -19
- package/app/Sources/CommandModeView.swift +27 -2
- package/app/Sources/DaemonServer.swift +8 -0
- package/app/Sources/DiagnosticLog.swift +19 -1
- package/app/Sources/EventBus.swift +1 -0
- package/app/Sources/HotkeyManager.swift +1 -0
- package/app/Sources/HotkeyStore.swift +9 -1
- package/app/Sources/LatticesApi.swift +210 -0
- package/app/Sources/MainView.swift +46 -86
- package/app/Sources/MainWindow.swift +13 -0
- package/app/Sources/OcrModel.swift +309 -0
- package/app/Sources/OcrStore.swift +295 -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/PaletteCommand.swift +11 -1
- package/app/Sources/PermissionChecker.swift +12 -2
- package/app/Sources/Preferences.swift +44 -0
- package/app/Sources/ScreenMapState.swift +7 -17
- package/app/Sources/ScreenMapView.swift +3 -0
- package/app/Sources/SettingsView.swift +534 -122
- package/app/Sources/Theme.swift +39 -0
- package/app/Sources/WindowTiler.swift +59 -56
- package/bin/lattices-app.js +23 -7
- package/bin/lattices.js +123 -0
- package/docs/api.md +390 -249
- package/docs/app.md +75 -28
- package/docs/concepts.md +45 -136
- package/docs/config.md +8 -7
- package/docs/layers.md +16 -18
- package/docs/ocr.md +185 -0
- package/docs/overview.md +39 -34
- package/docs/quickstart.md +34 -35
- package/package.json +6 -2
package/app/Sources/Theme.swift
CHANGED
|
@@ -92,6 +92,41 @@ struct GlassCard: ViewModifier {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
struct LiquidGlassCard: ViewModifier {
|
|
96
|
+
func body(content: Content) -> some View {
|
|
97
|
+
content
|
|
98
|
+
.background(
|
|
99
|
+
ZStack {
|
|
100
|
+
// Base: translucent dark fill
|
|
101
|
+
RoundedRectangle(cornerRadius: 10)
|
|
102
|
+
.fill(Color.white.opacity(0.04))
|
|
103
|
+
|
|
104
|
+
// Subtle gradient: brighter at top edge for "glass reflection"
|
|
105
|
+
RoundedRectangle(cornerRadius: 10)
|
|
106
|
+
.fill(
|
|
107
|
+
LinearGradient(
|
|
108
|
+
colors: [Color.white.opacity(0.06), Color.clear],
|
|
109
|
+
startPoint: .top,
|
|
110
|
+
endPoint: .center
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// Border: top-bright, bottom-dark for depth
|
|
115
|
+
RoundedRectangle(cornerRadius: 10)
|
|
116
|
+
.strokeBorder(
|
|
117
|
+
LinearGradient(
|
|
118
|
+
colors: [Color.white.opacity(0.12), Color.white.opacity(0.04)],
|
|
119
|
+
startPoint: .top,
|
|
120
|
+
endPoint: .bottom
|
|
121
|
+
),
|
|
122
|
+
lineWidth: 0.5
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
.shadow(color: Color.black.opacity(0.2), radius: 8, y: 4)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
95
130
|
struct AngularButton: ViewModifier {
|
|
96
131
|
let color: Color
|
|
97
132
|
var filled: Bool = true
|
|
@@ -118,6 +153,10 @@ extension View {
|
|
|
118
153
|
modifier(GlassCard(isHovered: hovered))
|
|
119
154
|
}
|
|
120
155
|
|
|
156
|
+
func liquidGlass() -> some View {
|
|
157
|
+
modifier(LiquidGlassCard())
|
|
158
|
+
}
|
|
159
|
+
|
|
121
160
|
func angularButton(_ color: Color, filled: Bool = true) -> some View {
|
|
122
161
|
modifier(AngularButton(color: color, filled: filled))
|
|
123
162
|
}
|
|
@@ -5,6 +5,24 @@ import CoreGraphics
|
|
|
5
5
|
@_silgen_name("_AXUIElementGetWindow")
|
|
6
6
|
func _AXUIElementGetWindow(_ element: AXUIElement, _ windowID: UnsafeMutablePointer<CGWindowID>) -> AXError
|
|
7
7
|
|
|
8
|
+
// MARK: - SkyLight Private APIs (instant window moves, no animation)
|
|
9
|
+
// Loaded at runtime via dlsym — graceful fallback if unavailable.
|
|
10
|
+
|
|
11
|
+
private let skyLight: UnsafeMutableRawPointer? = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight", RTLD_NOW)
|
|
12
|
+
|
|
13
|
+
private typealias SLSMainConnectionIDFunc = @convention(c) () -> Int32
|
|
14
|
+
private typealias SLSMoveWindowFunc = @convention(c) (Int32, UInt32, UnsafeMutablePointer<CGPoint>) -> CGError
|
|
15
|
+
|
|
16
|
+
private let _SLSMainConnectionID: SLSMainConnectionIDFunc? = {
|
|
17
|
+
guard let sl = skyLight, let sym = dlsym(sl, "SLSMainConnectionID") else { return nil }
|
|
18
|
+
return unsafeBitCast(sym, to: SLSMainConnectionIDFunc.self)
|
|
19
|
+
}()
|
|
20
|
+
|
|
21
|
+
private let _SLSMoveWindow: SLSMoveWindowFunc? = {
|
|
22
|
+
guard let sl = skyLight, let sym = dlsym(sl, "SLSMoveWindow") else { return nil }
|
|
23
|
+
return unsafeBitCast(sym, to: SLSMoveWindowFunc.self)
|
|
24
|
+
}()
|
|
25
|
+
|
|
8
26
|
// MARK: - Window Highlight Overlay
|
|
9
27
|
|
|
10
28
|
final class WindowHighlight {
|
|
@@ -1626,47 +1644,30 @@ enum WindowTiler {
|
|
|
1626
1644
|
/// Tile the frontmost window of any app to a position using AX API.
|
|
1627
1645
|
/// Works for any application (Finder, Chrome, etc.), not just terminals.
|
|
1628
1646
|
static func tileFrontmostViaAX(to position: TilePosition) {
|
|
1629
|
-
let diag = DiagnosticLog.shared
|
|
1630
|
-
let t = diag.startTimed("tileFrontmostViaAX: \(position.rawValue)")
|
|
1631
|
-
|
|
1632
1647
|
// 1. Get frontmost application
|
|
1633
|
-
guard let frontApp = NSWorkspace.shared.frontmostApplication
|
|
1634
|
-
|
|
1635
|
-
diag.finish(t)
|
|
1636
|
-
return
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
// 2. Skip if Lattices
|
|
1640
|
-
if frontApp.bundleIdentifier == "com.arach.lattices" {
|
|
1641
|
-
diag.info("tileFrontmostViaAX: skipping Lattices")
|
|
1642
|
-
diag.finish(t)
|
|
1643
|
-
return
|
|
1644
|
-
}
|
|
1648
|
+
guard let frontApp = NSWorkspace.shared.frontmostApplication,
|
|
1649
|
+
frontApp.bundleIdentifier != "com.arach.lattices" else { return }
|
|
1645
1650
|
|
|
1646
|
-
let
|
|
1647
|
-
let appRef = AXUIElementCreateApplication(pid)
|
|
1651
|
+
let appRef = AXUIElementCreateApplication(frontApp.processIdentifier)
|
|
1648
1652
|
|
|
1649
|
-
//
|
|
1653
|
+
// 2. Get focused window
|
|
1650
1654
|
var focusedRef: CFTypeRef?
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
diag.warn("tileFrontmostViaAX: no focused window (AX error \(err.rawValue))")
|
|
1654
|
-
diag.finish(t)
|
|
1655
|
-
return
|
|
1656
|
-
}
|
|
1655
|
+
guard AXUIElementCopyAttributeValue(appRef, kAXFocusedWindowAttribute as CFString, &focusedRef) == .success,
|
|
1656
|
+
let axWindow = focusedRef else { return }
|
|
1657
1657
|
|
|
1658
|
-
|
|
1658
|
+
let win = axWindow as! AXUIElement
|
|
1659
|
+
|
|
1660
|
+
// 3. Read current position → find containing screen (AX coords: top-left origin)
|
|
1659
1661
|
var posRef: CFTypeRef?
|
|
1660
1662
|
var sizeRef: CFTypeRef?
|
|
1661
|
-
AXUIElementCopyAttributeValue(
|
|
1662
|
-
AXUIElementCopyAttributeValue(
|
|
1663
|
+
AXUIElementCopyAttributeValue(win, kAXPositionAttribute as CFString, &posRef)
|
|
1664
|
+
AXUIElementCopyAttributeValue(win, kAXSizeAttribute as CFString, &sizeRef)
|
|
1663
1665
|
|
|
1664
1666
|
var currentPos = CGPoint.zero
|
|
1665
1667
|
var currentSize = CGSize.zero
|
|
1666
1668
|
if let pv = posRef { AXValueGetValue(pv as! AXValue, .cgPoint, ¤tPos) }
|
|
1667
1669
|
if let sv = sizeRef { AXValueGetValue(sv as! AXValue, .cgSize, ¤tSize) }
|
|
1668
1670
|
|
|
1669
|
-
// Find screen containing window center (AX uses top-left origin)
|
|
1670
1671
|
let primaryHeight = NSScreen.screens.first?.frame.height ?? 1080
|
|
1671
1672
|
let nsCenterX = currentPos.x + currentSize.width / 2
|
|
1672
1673
|
let nsCenterY = primaryHeight - (currentPos.y + currentSize.height / 2)
|
|
@@ -1674,40 +1675,42 @@ enum WindowTiler {
|
|
|
1674
1675
|
$0.frame.contains(NSPoint(x: nsCenterX, y: nsCenterY))
|
|
1675
1676
|
}) ?? NSScreen.main ?? NSScreen.screens[0]
|
|
1676
1677
|
|
|
1677
|
-
//
|
|
1678
|
+
// 4. Compute target frame (AX/CGS coords: top-left origin)
|
|
1678
1679
|
let targetFrame = tileFrame(for: position, on: screen)
|
|
1679
|
-
|
|
1680
|
-
// 6. Double-set: size → pos → size → pos
|
|
1681
1680
|
var newPos = CGPoint(x: targetFrame.origin.x, y: targetFrame.origin.y)
|
|
1682
1681
|
var newSize = CGSize(width: targetFrame.width, height: targetFrame.height)
|
|
1683
1682
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
}
|
|
1691
|
-
if let sv = AXValueCreate(.cgSize, &newSize) {
|
|
1692
|
-
AXUIElementSetAttributeValue(win, kAXSizeAttribute as CFString, sv)
|
|
1693
|
-
}
|
|
1694
|
-
if let pv = AXValueCreate(.cgPoint, &newPos) {
|
|
1695
|
-
AXUIElementSetAttributeValue(win, kAXPositionAttribute as CFString, pv)
|
|
1696
|
-
}
|
|
1683
|
+
// 5. Try SkyLight fast path (instant, no animation)
|
|
1684
|
+
var wid: CGWindowID = 0
|
|
1685
|
+
let hasSkyLight = _SLSMainConnectionID != nil
|
|
1686
|
+
&& _SLSMoveWindow != nil
|
|
1687
|
+
&& _AXUIElementGetWindow(win, &wid) == .success
|
|
1688
|
+
&& wid != 0
|
|
1697
1689
|
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
x: targetFrame.origin.x,
|
|
1701
|
-
y: primaryHeight - targetFrame.origin.y - targetFrame.height,
|
|
1702
|
-
width: targetFrame.width,
|
|
1703
|
-
height: targetFrame.height
|
|
1704
|
-
)
|
|
1705
|
-
DispatchQueue.main.async {
|
|
1706
|
-
WindowHighlight.shared.flash(frame: nsFrame, duration: 0.6)
|
|
1707
|
-
}
|
|
1690
|
+
if hasSkyLight {
|
|
1691
|
+
let cid = _SLSMainConnectionID!()
|
|
1708
1692
|
|
|
1709
|
-
|
|
1710
|
-
|
|
1693
|
+
// Resize via AX (app must handle its own content layout)
|
|
1694
|
+
if let sv = AXValueCreate(.cgSize, &newSize) {
|
|
1695
|
+
AXUIElementSetAttributeValue(win, kAXSizeAttribute as CFString, sv)
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// Move via SkyLight (instant, no animation)
|
|
1699
|
+
if _SLSMoveWindow!(cid, UInt32(wid), &newPos) != .success {
|
|
1700
|
+
// SLS move failed — fall back to AX position
|
|
1701
|
+
if let pv = AXValueCreate(.cgPoint, &newPos) {
|
|
1702
|
+
AXUIElementSetAttributeValue(win, kAXPositionAttribute as CFString, pv)
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
} else {
|
|
1706
|
+
// 6. AX fallback: size then position
|
|
1707
|
+
if let sv = AXValueCreate(.cgSize, &newSize) {
|
|
1708
|
+
AXUIElementSetAttributeValue(win, kAXSizeAttribute as CFString, sv)
|
|
1709
|
+
}
|
|
1710
|
+
if let pv = AXValueCreate(.cgPoint, &newPos) {
|
|
1711
|
+
AXUIElementSetAttributeValue(win, kAXPositionAttribute as CFString, pv)
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1711
1714
|
}
|
|
1712
1715
|
|
|
1713
1716
|
// MARK: - Private
|
package/bin/lattices-app.js
CHANGED
|
@@ -19,7 +19,22 @@ const ASSET_NAME = "Lattices-macos-arm64";
|
|
|
19
19
|
|
|
20
20
|
function isRunning() {
|
|
21
21
|
try {
|
|
22
|
-
execSync("pgrep -
|
|
22
|
+
execSync("pgrep -x Lattices", { stdio: "pipe" });
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function quit() {
|
|
30
|
+
try {
|
|
31
|
+
execSync("pkill -x Lattices", { stdio: "pipe" });
|
|
32
|
+
// Wait briefly for process to exit
|
|
33
|
+
try { execSync("sleep 0.5", { stdio: "pipe" }); } catch {}
|
|
34
|
+
// Force kill if still running
|
|
35
|
+
if (isRunning()) {
|
|
36
|
+
execSync("pkill -9 -x Lattices", { stdio: "pipe" });
|
|
37
|
+
}
|
|
23
38
|
return true;
|
|
24
39
|
} catch {
|
|
25
40
|
return false;
|
|
@@ -78,8 +93,8 @@ function buildFromSource() {
|
|
|
78
93
|
try {
|
|
79
94
|
// Prefer a real signing identity for stable TCC grants; fall back to ad-hoc with fixed identifier
|
|
80
95
|
const identities = execSync("security find-identity -v -p codesigning", { stdio: "pipe" }).toString();
|
|
81
|
-
const devId = identities.match(/"(
|
|
82
|
-
|| identities.match(/"(
|
|
96
|
+
const devId = identities.match(/"(Developer ID Application:[^"]+)"/)?.[1]
|
|
97
|
+
|| identities.match(/"(Apple Development:[^"]+)"/)?.[1];
|
|
83
98
|
const signArg = devId ? `'${devId}'` : "-";
|
|
84
99
|
execSync(
|
|
85
100
|
`codesign --force --sign ${signArg} --identifier com.arach.lattices '${bundlePath}'`,
|
|
@@ -89,6 +104,8 @@ function buildFromSource() {
|
|
|
89
104
|
// Non-fatal — app still works, just permissions won't persist across rebuilds
|
|
90
105
|
console.log("Warning: code signing failed — permissions may not persist across rebuilds.");
|
|
91
106
|
}
|
|
107
|
+
// Update bundle timestamp so Finder shows the correct modified date
|
|
108
|
+
try { execSync(`touch '${bundlePath}'`, { stdio: "pipe" }); } catch {}
|
|
92
109
|
console.log("Build complete.");
|
|
93
110
|
return true;
|
|
94
111
|
}
|
|
@@ -181,15 +198,14 @@ if (cmd === "build") {
|
|
|
181
198
|
}
|
|
182
199
|
buildFromSource();
|
|
183
200
|
} else if (cmd === "quit") {
|
|
184
|
-
|
|
185
|
-
execSync("pkill -f Lattices.app", { stdio: "pipe" });
|
|
201
|
+
if (quit()) {
|
|
186
202
|
console.log("lattices app stopped.");
|
|
187
|
-
}
|
|
203
|
+
} else {
|
|
188
204
|
console.log("lattices app is not running.");
|
|
189
205
|
}
|
|
190
206
|
} else if (cmd === "restart") {
|
|
191
207
|
// Quit → rebuild → relaunch
|
|
192
|
-
|
|
208
|
+
quit();
|
|
193
209
|
if (!hasSwift()) {
|
|
194
210
|
console.error("Swift is required. Install with: xcode-select --install");
|
|
195
211
|
process.exit(1);
|
package/bin/lattices.js
CHANGED
|
@@ -867,6 +867,122 @@ async function daemonStatusInventory() {
|
|
|
867
867
|
}
|
|
868
868
|
}
|
|
869
869
|
|
|
870
|
+
// ── OCR commands ──────────────────────────────────────────────────────
|
|
871
|
+
|
|
872
|
+
async function ocrCommand(sub, ...rest) {
|
|
873
|
+
const { daemonCall } = await getDaemonClient();
|
|
874
|
+
|
|
875
|
+
if (!sub || sub === "snapshot" || sub === "ls") {
|
|
876
|
+
// Default: show latest OCR snapshot
|
|
877
|
+
try {
|
|
878
|
+
const results = await daemonCall("ocr.snapshot", null, 5000);
|
|
879
|
+
if (!results.length) {
|
|
880
|
+
console.log("No OCR results yet. The first scan runs ~60s after launch.");
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
console.log(`\x1b[1mOCR Snapshot\x1b[0m (${results.length} windows)\n`);
|
|
884
|
+
for (const r of results) {
|
|
885
|
+
const age = Math.round((Date.now() / 1000) - r.timestamp);
|
|
886
|
+
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.floor(age / 60)}m ago` : `${Math.floor(age / 3600)}h ago`;
|
|
887
|
+
const lines = (r.fullText || "").split("\n").filter(Boolean);
|
|
888
|
+
const preview = lines.slice(0, 3).map(l => l.length > 80 ? l.slice(0, 77) + "..." : l);
|
|
889
|
+
console.log(` \x1b[1m${r.app}\x1b[0m wid:${r.wid} \x1b[90m${ageStr}\x1b[0m`);
|
|
890
|
+
console.log(` \x1b[36m"${r.title || "(untitled)"}"\x1b[0m`);
|
|
891
|
+
if (preview.length) {
|
|
892
|
+
for (const line of preview) {
|
|
893
|
+
console.log(` \x1b[90m${line}\x1b[0m`);
|
|
894
|
+
}
|
|
895
|
+
if (lines.length > 3) {
|
|
896
|
+
console.log(` \x1b[90m… ${lines.length - 3} more lines\x1b[0m`);
|
|
897
|
+
}
|
|
898
|
+
} else {
|
|
899
|
+
console.log(` \x1b[90m(no text detected)\x1b[0m`);
|
|
900
|
+
}
|
|
901
|
+
console.log();
|
|
902
|
+
}
|
|
903
|
+
} catch {
|
|
904
|
+
console.log("Daemon not running. Start with: lattices app");
|
|
905
|
+
}
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (sub === "search") {
|
|
910
|
+
const query = rest.join(" ");
|
|
911
|
+
if (!query) {
|
|
912
|
+
console.log("Usage: lattices ocr search <query>");
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
try {
|
|
916
|
+
const results = await daemonCall("ocr.search", { query }, 5000);
|
|
917
|
+
if (!results.length) {
|
|
918
|
+
console.log(`No OCR matches for "${query}".`);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
console.log(`\x1b[1mOCR Search\x1b[0m "${query}" (${results.length} matches)\n`);
|
|
922
|
+
for (const r of results) {
|
|
923
|
+
const snippet = r.snippet || r.fullText?.slice(0, 120) || "";
|
|
924
|
+
console.log(` \x1b[1m${r.app}\x1b[0m wid:${r.wid}`);
|
|
925
|
+
console.log(` \x1b[36m"${r.title || "(untitled)"}"\x1b[0m`);
|
|
926
|
+
console.log(` ${snippet}`);
|
|
927
|
+
console.log();
|
|
928
|
+
}
|
|
929
|
+
} catch (e) {
|
|
930
|
+
console.log(`Error: ${e.message}`);
|
|
931
|
+
}
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (sub === "scan") {
|
|
936
|
+
try {
|
|
937
|
+
console.log("Triggering OCR scan...");
|
|
938
|
+
await daemonCall("ocr.scan", null, 30000);
|
|
939
|
+
console.log("Scan complete.");
|
|
940
|
+
} catch (e) {
|
|
941
|
+
console.log(`Error: ${e.message}`);
|
|
942
|
+
}
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (sub === "history") {
|
|
947
|
+
const wid = parseInt(rest[0], 10);
|
|
948
|
+
if (isNaN(wid)) {
|
|
949
|
+
console.log("Usage: lattices ocr history <wid>");
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
const results = await daemonCall("ocr.history", { wid }, 5000);
|
|
954
|
+
if (!results.length) {
|
|
955
|
+
console.log(`No OCR history for wid:${wid}.`);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
console.log(`\x1b[1mOCR History\x1b[0m wid:${wid} (${results.length} entries)\n`);
|
|
959
|
+
for (const r of results) {
|
|
960
|
+
const ts = new Date(r.timestamp * 1000).toLocaleTimeString();
|
|
961
|
+
const lines = (r.fullText || "").split("\n").filter(Boolean);
|
|
962
|
+
const preview = lines.slice(0, 2).map(l => l.length > 80 ? l.slice(0, 77) + "..." : l);
|
|
963
|
+
console.log(` \x1b[90m${ts}\x1b[0m \x1b[1m${r.app}\x1b[0m — "${r.title}"`);
|
|
964
|
+
for (const line of preview) {
|
|
965
|
+
console.log(` \x1b[90m${line}\x1b[0m`);
|
|
966
|
+
}
|
|
967
|
+
console.log();
|
|
968
|
+
}
|
|
969
|
+
} catch (e) {
|
|
970
|
+
console.log(`Error: ${e.message}`);
|
|
971
|
+
}
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Unknown subcommand
|
|
976
|
+
console.log(`lattices ocr — Screen text recognition
|
|
977
|
+
|
|
978
|
+
Usage:
|
|
979
|
+
lattices ocr Show latest OCR snapshot (all windows)
|
|
980
|
+
lattices ocr search <q> Full-text search across all scanned windows
|
|
981
|
+
lattices ocr scan Trigger an immediate scan
|
|
982
|
+
lattices ocr history <id> Show OCR timeline for a window (by wid)
|
|
983
|
+
`);
|
|
984
|
+
}
|
|
985
|
+
|
|
870
986
|
function printUsage() {
|
|
871
987
|
console.log(`lattices — Claude Code + dev server in tmux
|
|
872
988
|
|
|
@@ -886,6 +1002,10 @@ Usage:
|
|
|
886
1002
|
lattices tile <position> Tile the frontmost window (left, right, top, etc.)
|
|
887
1003
|
lattices distribute Smart-grid all visible windows (daemon required)
|
|
888
1004
|
lattices layer [index] List layers or switch to a layer (daemon required)
|
|
1005
|
+
lattices ocr Show latest OCR snapshot (all windows)
|
|
1006
|
+
lattices ocr search <q> Full-text search screen text
|
|
1007
|
+
lattices ocr scan Trigger an immediate scan
|
|
1008
|
+
lattices ocr history <wid> OCR timeline for a specific window
|
|
889
1009
|
lattices daemon status Show daemon status
|
|
890
1010
|
lattices app Launch the menu bar companion app
|
|
891
1011
|
lattices app build Rebuild the menu bar app
|
|
@@ -1268,6 +1388,9 @@ switch (command) {
|
|
|
1268
1388
|
case "layers":
|
|
1269
1389
|
await layerCommand(args[1]);
|
|
1270
1390
|
break;
|
|
1391
|
+
case "ocr":
|
|
1392
|
+
await ocrCommand(args[1], ...args.slice(2));
|
|
1393
|
+
break;
|
|
1271
1394
|
case "daemon":
|
|
1272
1395
|
if (args[1] === "status") {
|
|
1273
1396
|
await daemonStatusCommand();
|