From 41635955b058571a2f527556c187e7fe92d6fbe5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 11:00:32 +0000 Subject: [PATCH] feat: let users pick the default server opened on launch (#26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repurposes the previously-unused ServerEntry.openOnLaunch flag so users can nominate Local or any registered remote as the server Scarf opens into when a fresh window has no prior binding (first launch or File → New Window). - ServerRegistry gains `defaultServerID` (returns the flagged entry's ID or falls back to Local) and `setDefaultServer(_:)` (flips the flag on the named entry and clears it elsewhere, then persists). - ScarfApp's WindowGroup defaultValue closure now returns `registry.defaultServerID` instead of hardcoded `ServerContext.local.id`. - ManageServersView gains a Local row at the top of the list plus a star button per row: filled yellow on the current default, outline on the others. Click to promote. Backward compatible: the openOnLaunch field was already in the persisted schema (default false), so existing servers.json files load unchanged — Local remains the default until the user picks otherwise. Refs #26 --- .../Core/Persistence/ServerRegistry.swift | 32 +++++++++++++++- .../Servers/Views/ManageServersView.swift | 38 ++++++++++++++++++- scarf/scarf/scarfApp.swift | 6 ++- 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/scarf/scarf/Core/Persistence/ServerRegistry.swift b/scarf/scarf/Core/Persistence/ServerRegistry.swift index e699b19..666f2da 100644 --- a/scarf/scarf/Core/Persistence/ServerRegistry.swift +++ b/scarf/scarf/Core/Persistence/ServerRegistry.swift @@ -9,8 +9,10 @@ struct ServerEntry: Identifiable, Codable, Hashable, Sendable { var id: ServerID var displayName: String var kind: ServerKind - /// User preference: open this server in a window on launch. Phase 3 - /// multi-window uses this; Phase 2 ignores it. + /// User preference: this server is the one Scarf opens into when a + /// fresh window has no prior binding (first launch or File → New). + /// At most one entry should have this set — `ServerRegistry` enforces + /// mutual exclusivity. If none do, Local is the implicit default. var openOnLaunch: Bool = false var context: ServerContext { @@ -69,6 +71,32 @@ final class ServerRegistry { return nil } + /// The server a fresh window should open into. Returns the ID of the + /// remote entry flagged `openOnLaunch`, or Local's ID if none is + /// flagged (or if the flagged entry was removed out from under us). + /// Consumed by the `WindowGroup`'s `defaultValue` closure. + var defaultServerID: ServerID { + entries.first(where: { $0.openOnLaunch })?.id ?? ServerContext.local.id + } + + /// Flip the default server to `id`. Passing `ServerContext.local.id` + /// clears the flag on every remote entry, making Local the implicit + /// default. Passing an unknown ID is a no-op. Persisted on return. + func setDefaultServer(_ id: ServerID) { + var changed = false + for idx in entries.indices { + let shouldBeDefault = (entries[idx].id == id) + if entries[idx].openOnLaunch != shouldBeDefault { + entries[idx].openOnLaunch = shouldBeDefault + changed = true + } + } + if changed { + save() + onEntriesChanged?() + } + } + // MARK: - Mutations /// Optional callback fired whenever `entries` changes. The app wires diff --git a/scarf/scarf/Features/Servers/Views/ManageServersView.swift b/scarf/scarf/Features/Servers/Views/ManageServersView.swift index 015e24b..504c842 100644 --- a/scarf/scarf/Features/Servers/Views/ManageServersView.swift +++ b/scarf/scarf/Features/Servers/Views/ManageServersView.swift @@ -87,9 +87,28 @@ struct ManageServersView: View { } private var list: some View { - List { + let defaultID = registry.defaultServerID + return List { + // Local sits at the top so users can mark it as the open-on-launch + // default alongside remote servers. It's synthesized (not in + // `registry.entries`), so render it explicitly. + HStack(spacing: 10) { + defaultStar(for: ServerContext.local.id, currentDefault: defaultID) + Image(systemName: "laptopcomputer") + .foregroundStyle(.blue) + VStack(alignment: .leading, spacing: 2) { + Text("Local").font(.body) + Text("This Mac") + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + } + .padding(.vertical, 4) + ForEach(registry.entries) { entry in HStack(spacing: 10) { + defaultStar(for: entry.id, currentDefault: defaultID) Image(systemName: "server.rack") .foregroundStyle(.blue) VStack(alignment: .leading, spacing: 2) { @@ -123,6 +142,23 @@ struct ManageServersView: View { .listStyle(.inset) } + /// A star button that marks the open-on-launch default. Filled + yellow + /// on the current default row (and non-interactive — clicking it is a + /// no-op since the flag is already set); outline + secondary elsewhere, + /// clicking promotes that row to default. + @ViewBuilder + private func defaultStar(for id: ServerID, currentDefault: ServerID) -> some View { + let isDefault = id == currentDefault + Button { + if !isDefault { registry.setDefaultServer(id) } + } label: { + Image(systemName: isDefault ? "star.fill" : "star") + .foregroundStyle(isDefault ? .yellow : .secondary) + } + .buttonStyle(.borderless) + .help(isDefault ? "Opens on launch" : "Set as default — open this server when Scarf launches.") + } + private func summary(for config: SSHConfig) -> String { var s = "" if let user = config.user, !user.isEmpty { s += "\(user)@" } diff --git a/scarf/scarf/scarfApp.swift b/scarf/scarf/scarfApp.swift index 365818e..92cdfa5 100644 --- a/scarf/scarf/scarfApp.swift +++ b/scarf/scarf/scarfApp.swift @@ -63,7 +63,11 @@ struct ScarfApp: App { .environment(updater) } } defaultValue: { - ServerContext.local.id + // Honour the user's "open on launch" choice from the Manage + // Servers popover. Falls back to Local when no entry is flagged + // (the default behaviour for fresh installs) or when the + // flagged entry was removed while the app was closed. + registry.defaultServerID } .defaultSize(width: 1100, height: 700) .commands {