Skip to content

Commit 568cb66

Browse files
Chris Fieldsclaude
andcommitted
Add workstream and quick-launch API endpoints for Stream Deck
Add GET /api/workstreams, POST /api/workstreams/:id/launch, and POST /api/quick-launch endpoints to support long-press workstream mode on Stream Deck dice buttons. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6bc23fa commit 568cb66

2 files changed

Lines changed: 106 additions & 0 deletions

File tree

APIServer.swift

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,23 @@ struct ErrorResponse: Codable {
3232
let error: String
3333
}
3434

35+
struct WorkstreamResponse: Codable {
36+
let id: String
37+
let name: String
38+
let theme: String
39+
let directory: String?
40+
let hasCommand: Bool
41+
}
42+
43+
struct WorkstreamsResponse: Codable {
44+
let workstreams: [WorkstreamResponse]
45+
}
46+
47+
struct WorkstreamLaunchResponse: Codable {
48+
let name: String
49+
let theme: String
50+
}
51+
3552
// MARK: - API Server
3653

3754
class APIServer {
@@ -47,6 +64,9 @@ class APIServer {
4764
var windowDataProvider: (() -> [GhosttyWindow])?
4865
var focusWindowHandler: ((Int, pid_t) -> Void)?
4966
var launchRandomHandler: (() -> (theme: String, windowName: String)?)?
67+
var workstreamsProvider: (() -> [WorkstreamResponse])?
68+
var launchWorkstreamHandler: ((String) -> (name: String, theme: String)?)?
69+
var openQuickLaunchHandler: (() -> Void)?
5070

5171
private init() {}
5272

@@ -180,6 +200,16 @@ class APIServer {
180200
case ("POST", "/api/launch-random"):
181201
handleLaunchRandom(connection)
182202

203+
case ("GET", "/api/workstreams"):
204+
handleGetWorkstreams(connection)
205+
206+
case ("POST", "/api/quick-launch"):
207+
handleOpenQuickLaunch(connection)
208+
209+
case ("POST", _) where path.hasPrefix("/api/workstreams/") && path.hasSuffix("/launch"):
210+
let workstreamId = extractWorkstreamId(from: path)
211+
handleLaunchWorkstream(connection, workstreamId: workstreamId)
212+
183213
case ("POST", _) where path.hasPrefix("/api/windows/") && path.hasSuffix("/focus"):
184214
let windowId = extractWindowId(from: path)
185215
handleFocusWindow(connection, windowId: windowId)
@@ -202,6 +232,15 @@ class APIServer {
202232
return ""
203233
}
204234

235+
private func extractWorkstreamId(from path: String) -> String {
236+
// /api/workstreams/{id}/launch -> extract {id}
237+
let components = path.components(separatedBy: "/")
238+
if components.count >= 4 {
239+
return components[3]
240+
}
241+
return ""
242+
}
243+
205244
private func handleHealth(_ connection: NWConnection) {
206245
let response = HealthResponse(status: "ok", version: "1.0.0")
207246
sendResponse(connection, status: 200, body: response)
@@ -274,6 +313,42 @@ class APIServer {
274313
sendResponse(connection, status: 200, body: response)
275314
}
276315

316+
private func handleGetWorkstreams(_ connection: NWConnection) {
317+
guard let provider = workstreamsProvider else {
318+
sendResponse(connection, status: 503, body: ErrorResponse(error: "Workstreams not available"))
319+
return
320+
}
321+
322+
let workstreams = provider()
323+
let response = WorkstreamsResponse(workstreams: workstreams)
324+
sendResponse(connection, status: 200, body: response)
325+
}
326+
327+
private func handleLaunchWorkstream(_ connection: NWConnection, workstreamId: String) {
328+
guard let handler = launchWorkstreamHandler else {
329+
sendResponse(connection, status: 503, body: ErrorResponse(error: "Workstream launch handler not available"))
330+
return
331+
}
332+
333+
guard let result = handler(workstreamId) else {
334+
sendResponse(connection, status: 404, body: ErrorResponse(error: "Workstream not found"))
335+
return
336+
}
337+
338+
let response = WorkstreamLaunchResponse(name: result.name, theme: result.theme)
339+
sendResponse(connection, status: 200, body: response)
340+
}
341+
342+
private func handleOpenQuickLaunch(_ connection: NWConnection) {
343+
guard let handler = openQuickLaunchHandler else {
344+
sendResponse(connection, status: 503, body: ErrorResponse(error: "Quick launch handler not available"))
345+
return
346+
}
347+
348+
handler()
349+
sendResponse(connection, status: 200, body: ["success": true])
350+
}
351+
277352
private func sendResponse<T: Encodable>(_ connection: NWConnection, status: Int, body: T) {
278353
let encoder = JSONEncoder()
279354
encoder.outputFormatting = .prettyPrinted

GhosttyThemePickerApp.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,37 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4242
return (theme: theme, windowName: windowName)
4343
}
4444

45+
APIServer.shared.workstreamsProvider = {
46+
guard let themeManager = WindowTracker.shared.themeManager else {
47+
return []
48+
}
49+
return themeManager.workstreams.map { ws in
50+
WorkstreamResponse(
51+
id: ws.id.uuidString,
52+
name: ws.name,
53+
theme: ws.theme,
54+
directory: ws.directory,
55+
hasCommand: ws.command != nil && !(ws.command?.isEmpty ?? true)
56+
)
57+
}
58+
}
59+
60+
APIServer.shared.launchWorkstreamHandler = { idString in
61+
guard let themeManager = WindowTracker.shared.themeManager,
62+
let uuid = UUID(uuidString: idString),
63+
let workstream = themeManager.workstreams.first(where: { $0.id == uuid }) else {
64+
return nil
65+
}
66+
themeManager.launchWorkstream(workstream)
67+
return (name: workstream.name, theme: workstream.theme)
68+
}
69+
70+
APIServer.shared.openQuickLaunchHandler = {
71+
DispatchQueue.main.async {
72+
QuickLaunchPanel.shared.show(themeManager: WindowTracker.shared.themeManager)
73+
}
74+
}
75+
4576
// Start API server
4677
APIServer.shared.start()
4778
print("Background services started")

0 commit comments

Comments
 (0)