@@ -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
22802517struct ExportWorkstreamsView : View {
0 commit comments