Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion macos/CodeWith/Sources/CodeWith/App/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ final class AppModel {
FileManager.default.isExecutableFile(atPath: $0) && !AppServerClient.isSelfExecutable($0)
}
guard !candidates.isEmpty else {
publishCurrentMenuBarPreference()
connection = .unavailable("codewith CLI not found"); return
}
var lastError = "could not start the codewith app-server"
Expand All @@ -376,6 +377,7 @@ final class AppModel {
}
}
guard connection == .connected else {
publishCurrentMenuBarPreference()
connection = .unavailable(lastError); return
}
startNotificationConsumer()
Expand Down Expand Up @@ -952,12 +954,13 @@ final class AppModel {
configSandbox = cfg.sandbox
customInstructions = cfg.developerInstructions ?? ""
desktopSettings = cfg.desktop
publishMenuBarPreference(cfg.desktop.showMenuBar)
publishCurrentMenuBarPreference()
if let sandbox = cfg.sandbox, sandbox != "danger-full-access" {
previousNonFullSandbox = sandbox
}
} catch {
configError = error.localizedDescription
publishCurrentMenuBarPreference()
}
}

Expand Down Expand Up @@ -2782,6 +2785,10 @@ final class AppModel {
NotificationCenter.default.post(name: .codeWithMenuBarPreferenceChanged, object: enabled)
}

func publishCurrentMenuBarPreference() {
publishMenuBarPreference(desktopSettings.showMenuBar)
}

private func writeConfigValue(keyPath: String, value: JSONValue, reloadUserConfig: Bool = false) async {
await writeConfigValues([(keyPath: keyPath, value: value)], reloadUserConfig: reloadUserConfig)
}
Expand Down
124 changes: 122 additions & 2 deletions macos/CodeWith/Sources/CodeWith/App/CodeWithApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,131 @@ enum CodeWithMain {
let delegate = AppDelegate()
app.delegate = delegate
app.setActivationPolicy(.regular)
AppMenuBuilder.install(on: app)
app.run()
}
}

enum AppMenuBuilder {
static func install(on application: NSApplication) {
let mainMenu = NSMenu()
mainMenu.addItem(applicationMenu(for: application))
mainMenu.addItem(editMenu())
let window = windowMenu(for: application)
mainMenu.addItem(window.item)

application.mainMenu = mainMenu
application.windowsMenu = window.menu
}

private static func applicationMenu(for application: NSApplication) -> NSMenuItem {
let submenu = NSMenu(title: "CodeWith")
submenu.addItem(item(
"About CodeWith",
action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)),
target: application
))
submenu.addItem(.separator())
submenu.addItem(item(
"Hide CodeWith",
action: #selector(NSApplication.hide(_:)),
keyEquivalent: "h",
target: application
))
submenu.addItem(item(
"Hide Others",
action: #selector(NSApplication.hideOtherApplications(_:)),
keyEquivalent: "h",
modifiers: [.command, .option],
target: application
))
submenu.addItem(item(
"Show All",
action: #selector(NSApplication.unhideAllApplications(_:)),
target: application
))
submenu.addItem(.separator())
submenu.addItem(item(
"Quit CodeWith",
action: #selector(NSApplication.terminate(_:)),
keyEquivalent: "q",
target: application
))
return topLevelItem(title: "CodeWith", submenu: submenu)
}

private static func editMenu() -> NSMenuItem {
let submenu = NSMenu(title: "Edit")
submenu.addItem(item("Undo", action: Selector(("undo:")), keyEquivalent: "z"))
submenu.addItem(item(
"Redo",
action: Selector(("redo:")),
keyEquivalent: "z",
modifiers: [.command, .shift]
))
submenu.addItem(.separator())
submenu.addItem(item("Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x"))
submenu.addItem(item("Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"))
submenu.addItem(item("Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"))
submenu.addItem(item("Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"))
return topLevelItem(title: "Edit", submenu: submenu)
}

private static func windowMenu(for application: NSApplication) -> (item: NSMenuItem, menu: NSMenu) {
let submenu = NSMenu(title: "Window")
submenu.addItem(item(
"Minimize",
action: #selector(NSWindow.performMiniaturize(_:)),
keyEquivalent: "m"
))
submenu.addItem(item("Zoom", action: #selector(NSWindow.performZoom(_:))))
submenu.addItem(.separator())
submenu.addItem(item(
"Bring All to Front",
action: #selector(NSApplication.arrangeInFront(_:)),
target: application
))
return (topLevelItem(title: "Window", submenu: submenu), submenu)
}

private static func topLevelItem(title: String, submenu: NSMenu) -> NSMenuItem {
let menuItem = NSMenuItem()
menuItem.title = title
menuItem.submenu = submenu
return menuItem
}

private static func item(
_ title: String,
action: Selector,
keyEquivalent: String = "",
modifiers: NSEvent.ModifierFlags = [.command],
target: AnyObject? = nil
) -> NSMenuItem {
let menuItem = NSMenuItem(title: title, action: action, keyEquivalent: keyEquivalent)
menuItem.keyEquivalentModifierMask = modifiers
menuItem.target = target
return menuItem
}
}

protocol AppWindowPresenting: AnyObject {
var isMiniaturized: Bool { get }
func deminiaturize(_ sender: Any?)
func makeKeyAndOrderFront(_ sender: Any?)
}

extension NSWindow: AppWindowPresenting {}

enum ExistingWindowPresenter {
static func restore(_ window: AppWindowPresenting) {
if window.isMiniaturized {
window.deminiaturize(nil)
}
window.makeKeyAndOrderFront(nil)
}
}

final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
private var window: NSWindow?
private var statusItem: NSStatusItem?
Expand Down Expand Up @@ -48,13 +169,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
guard let enabled = note.object as? Bool else { return }
self?.setMenuBarEnabled(enabled)
}
setMenuBarEnabled(true)
showMainWindow()
}

private func showMainWindow() {
if let window {
window.makeKeyAndOrderFront(nil)
ExistingWindowPresenter.restore(window)
NSApp.activate(ignoringOtherApps: true)
return
}
Expand Down
64 changes: 64 additions & 0 deletions macos/CodeWith/Tests/CodeWithTests/AppDelegateTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import AppKit
import XCTest
@testable import CodeWith

@MainActor
final class AppDelegateTests: XCTestCase {
func testInstallMainMenuProvidesNativeApplicationEditAndWindowMenus() {
let application = NSApplication.shared
let previousMainMenu = application.mainMenu
let previousWindowsMenu = application.windowsMenu
defer {
application.mainMenu = previousMainMenu
application.windowsMenu = previousWindowsMenu
}

AppMenuBuilder.install(on: application)

XCTAssertEqual(application.mainMenu?.items.map(\.title), ["CodeWith", "Edit", "Window"])
XCTAssertEqual(application.mainMenu?.item(withTitle: "CodeWith")?.submenu?.item(withTitle: "Quit CodeWith")?.action,
#selector(NSApplication.terminate(_:)))
XCTAssertEqual(application.mainMenu?.item(withTitle: "Edit")?.submenu?.item(withTitle: "Copy")?.action,
#selector(NSText.copy(_:)))
XCTAssertTrue(application.windowsMenu === application.mainMenu?.item(withTitle: "Window")?.submenu)
}

func testExistingMiniaturizedWindowIsDeminiaturizedBeforeBeingOrderedFront() {
let window = WindowPresenterSpy(isMiniaturized: true)

ExistingWindowPresenter.restore(window)

XCTAssertEqual(window.calls, [.deminiaturize, .orderFront])
}

func testExistingVisibleWindowIsOnlyOrderedFront() {
let window = WindowPresenterSpy(isMiniaturized: false)

ExistingWindowPresenter.restore(window)

XCTAssertEqual(window.calls, [.orderFront])
}
}

@MainActor
private final class WindowPresenterSpy: AppWindowPresenting {
enum Call: Equatable {
case deminiaturize
case orderFront
}

let isMiniaturized: Bool
private(set) var calls: [Call] = []

init(isMiniaturized: Bool) {
self.isMiniaturized = isMiniaturized
}

func deminiaturize(_ sender: Any?) {
calls.append(.deminiaturize)
}

func makeKeyAndOrderFront(_ sender: Any?) {
calls.append(.orderFront)
}
}
17 changes: 17 additions & 0 deletions macos/CodeWith/Tests/CodeWithTests/AppModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@ final class AppModelTests: XCTestCase {
XCTAssertFalse(m.desktopSettings.showMenuBar)
}

func testPublishCurrentMenuBarPreferenceUsesDefaultWhenConfigIsUnavailable() {
var received: Bool?
let token = NotificationCenter.default.addObserver(
forName: .codeWithMenuBarPreferenceChanged,
object: nil,
queue: nil
) { note in
received = note.object as? Bool
}
defer { NotificationCenter.default.removeObserver(token) }

let m = AppModel()
m.publishCurrentMenuBarPreference()

XCTAssertEqual(received, true)
}

func testNewChatResets() {
let m = AppModel()
m.activeThreadId = "x"; m.activeMessages = [ChatMessage(role: .user, text: "hi")]
Expand Down
Loading