Skip to content

Commit 526988a

Browse files
Chris Fieldsclaude
andcommitted
Add Stream Deck plugin installer tab
- Add StreamDeckInstaller.swift for downloading/installing plugin from GitHub - Add Stream Deck tab to Settings with version checking and one-click install - Check for updates from GitHub releases - Include "Get a Stream Deck" link when app not detected Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5afd252 commit 526988a

3 files changed

Lines changed: 462 additions & 0 deletions

File tree

GhosttyThemePicker.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
A1000001234567890000005 /* WindowTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000002234567890000006 /* WindowTracker.swift */; };
1515
A1000001234567890000006 /* HookInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000002234567890000007 /* HookInstaller.swift */; };
1616
A1000001234567890000007 /* hooks in Resources */ = {isa = PBXBuildFile; fileRef = A1000002234567890000008 /* hooks */; };
17+
A1000001234567890000008 /* StreamDeckInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000002234567890000009 /* StreamDeckInstaller.swift */; };
1718
/* End PBXBuildFile section */
1819

1920
/* Begin PBXFileReference section */
@@ -25,6 +26,7 @@
2526
A1000002234567890000006 /* WindowTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTracker.swift; sourceTree = "<group>"; };
2627
A1000002234567890000007 /* HookInstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HookInstaller.swift; sourceTree = "<group>"; };
2728
A1000002234567890000008 /* hooks */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Resources/hooks; sourceTree = "<group>"; };
29+
A1000002234567890000009 /* StreamDeckInstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamDeckInstaller.swift; sourceTree = "<group>"; };
2830
A1000003234567890000001 /* GhosttyThemePicker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GhosttyThemePicker.app; sourceTree = BUILT_PRODUCTS_DIR; };
2931
/* End PBXFileReference section */
3032

@@ -47,6 +49,7 @@
4749
A1000002234567890000005 /* APIServer.swift */,
4850
A1000002234567890000006 /* WindowTracker.swift */,
4951
A1000002234567890000007 /* HookInstaller.swift */,
52+
A1000002234567890000009 /* StreamDeckInstaller.swift */,
5053
A1000002234567890000003 /* Info.plist */,
5154
A1000002234567890000004 /* Assets.xcassets */,
5255
A1000002234567890000008 /* hooks */,
@@ -137,6 +140,7 @@
137140
A1000001234567890000004 /* APIServer.swift in Sources */,
138141
A1000001234567890000005 /* WindowTracker.swift in Sources */,
139142
A1000001234567890000006 /* HookInstaller.swift in Sources */,
143+
A1000001234567890000008 /* StreamDeckInstaller.swift in Sources */,
140144
);
141145
runOnlyForDeploymentPostprocessing = 0;
142146
};

GhosttyThemePickerApp.swift

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,6 +1944,11 @@ struct SettingsView: View {
19441944
.tabItem {
19451945
Label("Claude Hooks", systemImage: "terminal.fill")
19461946
}
1947+
1948+
StreamDeckSettingsView()
1949+
.tabItem {
1950+
Label("Stream Deck", systemImage: "square.grid.3x3")
1951+
}
19471952
}
19481953
.frame(width: 500, height: 400)
19491954
}
@@ -2275,6 +2280,238 @@ struct ClaudeHooksSettingsView: View {
22752280
}
22762281
}
22772282

2283+
// MARK: - Stream Deck Settings View
2284+
2285+
struct StreamDeckSettingsView: View {
2286+
@State private var isInstalled = false
2287+
@State private var installedVersion: String?
2288+
@State private var latestVersion: String?
2289+
@State private var downloadURL: String?
2290+
@State private var isLoading = false
2291+
@State private var isDownloading = false
2292+
@State private var errorMessage: String?
2293+
@State private var successMessage: String?
2294+
@State private var streamDeckInstalled = false
2295+
2296+
private var updateAvailable: Bool {
2297+
guard let installed = installedVersion, let latest = latestVersion else { return false }
2298+
return StreamDeckInstaller.shared.isUpdateAvailable(installed: installed, latest: latest)
2299+
}
2300+
2301+
var body: some View {
2302+
VStack(alignment: .leading, spacing: 16) {
2303+
Text("Stream Deck Plugin")
2304+
.font(.headline)
2305+
2306+
Text("Install the Ghostty Claude plugin for Elgato Stream Deck to display Claude state indicators and quickly switch between Ghostty windows.")
2307+
.font(.caption)
2308+
.foregroundColor(.secondary)
2309+
.fixedSize(horizontal: false, vertical: true)
2310+
2311+
// Stream Deck app status
2312+
if !streamDeckInstalled {
2313+
HStack {
2314+
Image(systemName: "exclamationmark.triangle.fill")
2315+
.foregroundColor(.orange)
2316+
Text("Stream Deck app not detected")
2317+
.font(.caption)
2318+
Spacer()
2319+
Link("Get a Stream Deck", destination: URL(string: "https://www.elgato.com/us/en/s/explore-stream-deck")!)
2320+
.font(.caption)
2321+
}
2322+
.padding(.vertical, 4)
2323+
}
2324+
2325+
// Plugin status
2326+
GroupBox {
2327+
VStack(alignment: .leading, spacing: 8) {
2328+
HStack {
2329+
Image(systemName: isInstalled ? "checkmark.circle.fill" : "xmark.circle.fill")
2330+
.foregroundColor(isInstalled ? .green : .secondary)
2331+
if isInstalled {
2332+
Text("Plugin installed")
2333+
if let version = installedVersion {
2334+
Text("v\(version)")
2335+
.font(.caption)
2336+
.foregroundColor(.secondary)
2337+
}
2338+
} else {
2339+
Text("Plugin not installed")
2340+
}
2341+
}
2342+
2343+
if let latest = latestVersion {
2344+
HStack {
2345+
Image(systemName: "arrow.down.circle")
2346+
.foregroundColor(.blue)
2347+
Text("Latest version: v\(latest)")
2348+
.font(.caption)
2349+
if updateAvailable {
2350+
Text("(Update available)")
2351+
.font(.caption)
2352+
.foregroundColor(.orange)
2353+
}
2354+
}
2355+
}
2356+
2357+
if isLoading {
2358+
HStack {
2359+
ProgressView()
2360+
.scaleEffect(0.6)
2361+
Text("Checking for updates...")
2362+
.font(.caption)
2363+
.foregroundColor(.secondary)
2364+
}
2365+
}
2366+
}
2367+
.frame(maxWidth: .infinity, alignment: .leading)
2368+
.padding(4)
2369+
}
2370+
2371+
// Description
2372+
GroupBox {
2373+
VStack(alignment: .leading, spacing: 8) {
2374+
Text("Features:")
2375+
.font(.caption)
2376+
.fontWeight(.medium)
2377+
2378+
VStack(alignment: .leading, spacing: 4) {
2379+
Label("Show Claude state for each Ghostty window", systemImage: "circle.fill")
2380+
.font(.caption)
2381+
Label("Click to focus window", systemImage: "arrow.right.circle")
2382+
.font(.caption)
2383+
Label("Windows sorted by priority (asking first)", systemImage: "list.number")
2384+
.font(.caption)
2385+
}
2386+
.foregroundColor(.secondary)
2387+
}
2388+
.frame(maxWidth: .infinity, alignment: .leading)
2389+
.padding(4)
2390+
}
2391+
2392+
// Error/Success messages
2393+
if let error = errorMessage {
2394+
HStack {
2395+
Image(systemName: "exclamationmark.triangle.fill")
2396+
.foregroundColor(.red)
2397+
Text(error)
2398+
.font(.caption)
2399+
.foregroundColor(.red)
2400+
}
2401+
}
2402+
2403+
if let success = successMessage {
2404+
HStack {
2405+
Image(systemName: "checkmark.circle.fill")
2406+
.foregroundColor(.green)
2407+
Text(success)
2408+
.font(.caption)
2409+
.foregroundColor(.green)
2410+
}
2411+
}
2412+
2413+
Spacer()
2414+
2415+
// Action buttons
2416+
HStack {
2417+
Button {
2418+
checkForUpdates()
2419+
} label: {
2420+
HStack {
2421+
Image(systemName: "arrow.clockwise")
2422+
Text("Check for Updates")
2423+
}
2424+
}
2425+
.disabled(isLoading || isDownloading)
2426+
2427+
Spacer()
2428+
2429+
if isInstalled && updateAvailable {
2430+
Button {
2431+
installOrUpdate()
2432+
} label: {
2433+
HStack {
2434+
Image(systemName: "arrow.down.circle")
2435+
Text("Update Plugin")
2436+
}
2437+
}
2438+
.buttonStyle(.borderedProminent)
2439+
.disabled(isDownloading || downloadURL == nil)
2440+
} else if !isInstalled {
2441+
Button {
2442+
installOrUpdate()
2443+
} label: {
2444+
HStack {
2445+
Image(systemName: "square.and.arrow.down")
2446+
Text("Install Plugin")
2447+
}
2448+
}
2449+
.buttonStyle(.borderedProminent)
2450+
.disabled(isDownloading || downloadURL == nil)
2451+
}
2452+
2453+
if isDownloading {
2454+
ProgressView()
2455+
.scaleEffect(0.6)
2456+
}
2457+
}
2458+
}
2459+
.padding()
2460+
.onAppear {
2461+
checkStatus()
2462+
checkForUpdates()
2463+
}
2464+
}
2465+
2466+
private func checkStatus() {
2467+
streamDeckInstalled = StreamDeckInstaller.shared.isStreamDeckInstalled()
2468+
isInstalled = StreamDeckInstaller.shared.isInstalled()
2469+
installedVersion = StreamDeckInstaller.shared.installedVersion()
2470+
}
2471+
2472+
private func checkForUpdates() {
2473+
isLoading = true
2474+
errorMessage = nil
2475+
2476+
StreamDeckInstaller.shared.fetchLatestRelease { result in
2477+
DispatchQueue.main.async {
2478+
isLoading = false
2479+
switch result {
2480+
case .success(let release):
2481+
latestVersion = release.version
2482+
downloadURL = release.downloadURL
2483+
case .failure(let error):
2484+
errorMessage = "Failed to check for updates: \(error.localizedDescription)"
2485+
}
2486+
}
2487+
}
2488+
}
2489+
2490+
private func installOrUpdate() {
2491+
guard let url = downloadURL else { return }
2492+
2493+
isDownloading = true
2494+
errorMessage = nil
2495+
successMessage = nil
2496+
2497+
StreamDeckInstaller.shared.installPlugin(from: url) { result in
2498+
DispatchQueue.main.async {
2499+
isDownloading = false
2500+
switch result {
2501+
case .success:
2502+
successMessage = "Plugin downloaded. Stream Deck installer should open automatically."
2503+
// Refresh status after a delay to allow installation
2504+
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
2505+
checkStatus()
2506+
}
2507+
case .failure(let error):
2508+
errorMessage = error.localizedDescription
2509+
}
2510+
}
2511+
}
2512+
}
2513+
}
2514+
22782515
// MARK: - Export Workstreams View
22792516

22802517
struct ExportWorkstreamsView: View {

0 commit comments

Comments
 (0)