feat: let users pick the default server opened on launch (#26)

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
This commit is contained in:
Claude
2026-04-22 11:00:32 +00:00
parent 8773254d11
commit 41635955b0
3 changed files with 72 additions and 4 deletions
@@ -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
@@ -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)@" }
+5 -1
View File
@@ -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 {