Replace server tabs with one window and a server picker

This commit is contained in:
Ian Gulliver
2026-10-18 23:10:31 -07:00
parent c64987f2eb
commit 74def5c9e6
5 changed files with 82 additions and 137 deletions
+8 -17
View File
@@ -24,7 +24,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
if flag {
return true
}
Servers.shared.openAll()
Servers.shared.openWindow?()
return false
}
@@ -64,8 +64,8 @@ struct ByreApp: App {
@NSApplicationDelegateAdaptor private var delegate: AppDelegate
var body: some Scene {
WindowGroup(id: "main", for: String.self) { $origin in
ServerWindow(origin: $origin)
Window("byre", id: "main") {
ServerWindow()
}
.defaultSize(width: 1280, height: 820)
.restorationBehavior(.disabled)
@@ -86,23 +86,16 @@ struct ByreCommands: Commands {
Updater.shared.check()
}
Button("Forget Server") {
focus?.forget?()
focus?.forget()
}
.disabled(focus?.forget == nil)
.disabled(focus == nil)
}
CommandGroup(replacing: .newItem) {
Button("New Server Window") {
Button("Add Server…") {
Servers.shared.current = nil
openWindow(id: "main")
}
.keyboardShortcut("n")
Menu("Open Server") {
ForEach(Servers.shared.origins, id: \.self) { origin in
Button(URL(string: origin)?.host() ?? origin) {
openWindow(id: "main", value: origin)
}
}
}
.disabled(Servers.shared.origins.isEmpty)
}
PasteboardCommands()
CommandMenu("Go") {
@@ -119,9 +112,7 @@ struct ByreCommands: Commands {
private func serverButton(_ label: String, _ key: Character, _ step: Int) -> some View {
Button(label) {
if let origin = Servers.shared.step(from: focus?.origin, by: step) {
openWindow(id: "main", value: origin)
}
Servers.shared.current = Servers.shared.step(from: Servers.shared.current, by: step)
}
.keyboardShortcut(KeyEquivalent(key))
.disabled(Servers.shared.origins.isEmpty)
+1 -1
View File
@@ -12,7 +12,7 @@ final class Notifier: NSObject, UNUserNotificationCenterDelegate {
private func open(_ origin: String, _ taskId: String) {
NSApp.activate()
Servers.shared.openWindow?(origin)
Servers.shared.show(origin)
pending = (origin, taskId)
deliver()
}
+62 -108
View File
@@ -11,13 +11,12 @@ final class Servers {
private(set) var entries: [(origin: String, record: ServerRecord)] = []
private(set) var connections: [String: Connection] = [:]
@ObservationIgnored var shown: Set<String> = []
@ObservationIgnored var windows: [String: WindowSlot] = [:]
@ObservationIgnored var launched = false
@ObservationIgnored var openWindow: ((String?) -> Void)?
var current: String?
@ObservationIgnored var openWindow: (() -> Void)?
private init() {
load()
current = origins.first
Task {
for await note in NotificationCenter.default.notifications(named: NSPersistentCloudKitContainer.eventChangedNotification) {
if let event = note.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey] as? NSPersistentCloudKitContainer.Event, event.type == .import, event.endDate != nil {
@@ -50,6 +49,9 @@ final class Servers {
}
}
entries = loaded
if let current, record(current) == nil {
self.current = origins.first
}
for (origin, _) in loaded where connections[origin] == nil {
connections[origin] = Connection(origin: parseOrigin(origin)!)
}
@@ -93,31 +95,17 @@ final class Servers {
return origins[(i + step + origins.count) % origins.count]
}
func arrange(_ group: NSWindowTabGroup?) {
guard let group else {
return
}
let selected = group.selectedWindow
let ordered = origins.compactMap { windows[$0]?.window }.filter { group.windows.contains($0) }
for (i, window) in ordered.enumerated() where group.windows[i] !== window {
group.insertWindow(window, at: i)
}
group.selectedWindow = selected
}
func openAll() {
if origins.isEmpty {
openWindow?(nil)
}
for origin in origins {
openWindow?(origin)
func show(_ origin: String) {
if record(origin) != nil {
current = origin
}
openWindow?()
}
}
@MainActor
final class WindowSlot {
weak var window: NSWindow?
func serverName(_ origin: String) -> String {
let host = URL(string: origin)?.host() ?? origin
return host.split(separator: ".").first.map(String.init) ?? host
}
@MainActor
@@ -130,6 +118,7 @@ final class Connection {
}
let origin: URL
let overlays = Overlays()
var phase: Phase = .starting
@ObservationIgnored private var stopped = false
@@ -368,10 +357,9 @@ struct FatalView: View {
}
struct ServerFocus {
let origin: String?
let store: Store?
let overlays: Overlays
let forget: (() -> Void)?
let forget: () -> Void
}
extension FocusedValues {
@@ -379,23 +367,15 @@ extension FocusedValues {
}
struct ServerWindow: View {
@Binding var origin: String?
@ViewState private var overlays = Overlays()
@ViewState private var form = ConnectForm()
@ViewState private var slot = WindowSlot()
@Environment(\.openWindow) private var openWindow
@Environment(\.dismiss) private var dismissWindow
private var connection: Connection? {
origin.flatMap { Servers.shared.connections[$0] }
Servers.shared.current.flatMap { Servers.shared.connections[$0] }
}
private var focus: ServerFocus {
var forgetting: (() -> Void)?
if connection != nil {
forgetting = forget
}
return ServerFocus(origin: connection?.origin.absoluteString, store: connection?.store, overlays: overlays, forget: forgetting)
private var focus: ServerFocus? {
connection.map { ServerFocus(store: $0.store, overlays: $0.overlays, forget: forget) }
}
private var frameColor: Color {
@@ -413,16 +393,13 @@ struct ServerWindow: View {
case .running(let store):
MainView()
.environment(store)
.environment(connection.overlays)
.id(connection.origin)
}
} else {
ConnectView(form: form) { connected in
let key = connected.absoluteString
if Servers.shared.shown.contains(key) {
openWindow(id: "main", value: key)
dismissWindow()
return
}
origin = key
form.serverText = ""
Servers.shared.current = connected.absoluteString
}
}
}
@@ -434,98 +411,75 @@ struct ServerWindow: View {
.background(Palette.bg)
.padding(connection == nil ? 0 : 3)
.background(frameColor)
.environment(overlays)
.font(Fonts.font(13))
.frame(minWidth: 640, minHeight: 400)
}
var body: some View {
framed
.navigationTitle(connection?.origin.host() ?? "byre")
.background {
WindowReader(found: joinTabs)
.toolbar {
ToolbarItem(placement: .principal) {
ServerPicker()
}
}
.toolbar(removing: .title)
.focusedSceneValue(\.serverWindow, focus)
.onChange(of: origin, initial: true) { old, new in
moved(old, new)
}
.onChange(of: Servers.shared.origins) { old, new in
adopt(new.filter { !old.contains($0) })
}
.onAppear(perform: appeared)
.onDisappear {
if let origin {
Servers.shared.shown.remove(origin)
release(origin)
}
}
}
private func moved(_ old: String?, _ new: String?) {
let servers = Servers.shared
if let old {
servers.shown.remove(old)
release(old)
}
if let new {
servers.shown.insert(new)
servers.windows[new] = slot
}
servers.arrange(slot.window?.tabGroup)
}
private func release(_ origin: String) {
if Servers.shared.windows[origin] === slot {
Servers.shared.windows[origin] = nil
}
}
private func appeared() {
let servers = Servers.shared
let openWindow = openWindow
servers.openWindow = { origin in
if let origin {
openWindow(id: "main", value: origin)
} else {
openWindow(id: "main")
}
}
if !servers.launched {
servers.launched = true
adopt(servers.origins)
for o in servers.origins where !servers.shown.contains(o) {
openWindow(id: "main", value: o)
}
Servers.shared.openWindow = {
openWindow(id: "main")
}
Notifier.shared.deliver()
}
private func joinTabs(_ window: NSWindow) {
window.tabbingMode = .preferred
slot.window = window
DispatchQueue.main.async {
let host = NSApp.windows.first { $0 !== window && $0.isVisible && $0.tabbingIdentifier == window.tabbingIdentifier }
if let host, !(host.tabbedWindows ?? []).contains(window) {
host.addTabbedWindow(window, ordered: .above)
}
Servers.shared.arrange(window.tabGroup)
}
}
private func adopt(_ candidates: [String]) {
guard origin == nil, form.serverText.isEmpty, !form.connecting, let next = candidates.first(where: { !Servers.shared.shown.contains($0) }) else {
guard Servers.shared.current == nil, form.serverText.isEmpty, !form.connecting, let next = candidates.first else {
return
}
origin = next
Servers.shared.shown.insert(next)
Servers.shared.current = next
}
private func forget() {
guard let current = origin else {
guard let current = Servers.shared.current else {
return
}
form.serverText = current
origin = nil
Servers.shared.current = nil
Servers.shared.remove(current)
}
}
struct ServerPicker: View {
var body: some View {
let servers = Servers.shared
Menu {
ForEach(servers.origins, id: \.self) { origin in
Button(serverName(origin)) {
servers.current = origin
}
}
Divider()
Button("Add Server…") {
servers.current = nil
}
} label: {
HStack(spacing: 6) {
if let current = servers.current {
Circle()
.fill(servers.color(current))
.frame(width: 8, height: 8)
Text(serverName(current))
} else {
Text("Add Server")
}
}
.font(Fonts.font(13))
}
}
}