Skip to content
Closed
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ port. The `dtc` mirror should be reverted once kernel.org returns.
2. Open the DMG and drag **Try Omarchy** to **Applications**.
3. Launch **Try Omarchy** from Applications.

Every launch begins at the start menu. While that menu is open, Try Omarchy behaves like a regular Mac app with standard Quit, Close Window, and Minimize commands; after the VM starts, that native app chrome steps aside for Omarchy. **Immersive** is on by default, so Omarchy opens Full Screen with the Mac menu bar and Dock hidden. Turn it off to open a resizable window; if you later enter Full Screen, the Mac menu bar and Dock remain available at the screen edges. Whenever the Omarchy window is focused, Command belongs to the guest as Super in either mode; Accessibility permission lets system shortcuts such as Command-Space reach it before macOS. Microphone and camera access are optional. The first launch takes longer while the app prepares Linux and starts Omarchy's account provisioning.
Every launch begins at the start menu. While that menu is open, Try Omarchy behaves like a regular Mac app with standard Quit, Close Window, and Minimize commands; after the VM starts, that native app chrome steps aside for Omarchy. **Immersive** is on by default, so Omarchy opens Full Screen with the Mac menu bar and Dock hidden. Turn it off to open a resizable window; if you later enter Full Screen, the Mac menu bar and Dock remain available at the screen edges. Whenever the Omarchy window is focused, Command belongs to the guest as Super in either mode; Accessibility permission lets system shortcuts such as Command-Space reach it before macOS. That focused Command capture is separate from relative mouse lock: desktop pointing stays absolute by default, and **Relative mouse** (or the floating Pointer control / menu-bar mouse icon while Omarchy is running) switches to a relative virtio mouse so games can capture the cursor. Immersive hides the Mac menu bar, so the floating Pointer HUD is the reliable mid-session control. Ctrl-Alt releases a grabbed cursor. Microphone and camera access are optional. The first launch takes longer while the app prepares Linux and starts Omarchy's account provisioning.

Restarting from inside Omarchy reboots the guest in the same Try Omarchy app.
Shutting down Omarchy closes the app and leaves it closed.
Expand Down
67 changes: 67 additions & 0 deletions macos/Sources/OmarchyVMHelper/PointerInputController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Foundation

/// Hot-swaps the guest pointer between absolute tablet and relative mouse over
/// QMP. Absolute remains the default for desktop trackpad use; relative is what
/// Cocoa needs before games can lock the host cursor.
final class QMPPointerInputController {
static let tabletDeviceID = "omarchy-tablet"
static let mouseDeviceID = "omarchy-mouse"

typealias ConnectionFactory = () throws -> QMPConnection

private let makeConnection: ConnectionFactory
private(set) var mode: PointerInputMode

init(socketPath: String, initialMode: PointerInputMode) {
makeConnection = {
try QMPConnection(
socketPath: socketPath,
identifierPrefix: "omarchy-pointer"
)
}
mode = initialMode
}

init(
connectionFactory: @escaping ConnectionFactory,
initialMode: PointerInputMode
) {
makeConnection = connectionFactory
mode = initialMode
}

func setMode(_ mode: PointerInputMode) throws {
guard mode != self.mode else { return }
let connection = try makeConnection()
defer { connection.close() }

// Add the new device first so the guest never loses its only pointer;
// the outgoing device stays until the replacement is attached.
let addDriver: String
let addID: String
let removeID: String
switch mode {
case .relative:
addDriver = "virtio-mouse-pci"
addID = Self.mouseDeviceID
removeID = Self.tabletDeviceID
case .absolute:
addDriver = "virtio-tablet-pci"
addID = Self.tabletDeviceID
removeID = Self.mouseDeviceID
}
_ = try connection.execute(
"device_add",
arguments: [
"driver": addDriver,
"id": addID,
"romfile": "",
]
)
_ = try connection.execute(
"device_del",
arguments: ["id": removeID]
)
self.mode = mode
}
}
174 changes: 174 additions & 0 deletions macos/Sources/OmarchyVMHelper/PointerModeStatusItem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import AppKit

/// Runtime pointer controls while the VM is running. The start menu dismisses
/// at launch, and Immersive hard-hides the Mac menu bar, so a status item alone
/// is invisible in the default presentation. Keep both: the menu-bar item for
/// windowed sessions, and a floating HUD that stays above Immersive Full Screen.
@MainActor
final class PointerModeStatusItem {
private var statusItem: NSStatusItem?
private var hud: NSPanel?
private var segmentedControl: NSSegmentedControl?
private var controller: QMPPointerInputController?
private var onModeChanged: ((PointerInputMode) -> Void)?

func show(
controller: QMPPointerInputController,
onModeChanged: @escaping (PointerInputMode) -> Void
) {
hide()
self.controller = controller
self.onModeChanged = onModeChanged

let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
if let button = item.button {
button.image = NSImage(
systemSymbolName: "computermouse",
accessibilityDescription: "Omarchy pointer mode"
)
button.image?.isTemplate = true
}
item.menu = makeMenu(mode: controller.mode)
statusItem = item
showHUD(mode: controller.mode)
}

func hide() {
if let statusItem {
NSStatusBar.system.removeStatusItem(statusItem)
}
statusItem = nil
hud?.orderOut(nil)
hud = nil
segmentedControl = nil
controller = nil
onModeChanged = nil
}

private func showHUD(mode: PointerInputMode) {
let panel = NSPanel(
contentRect: NSRect(x: 0, y: 0, width: 248, height: 44),
styleMask: [.titled, .nonactivatingPanel, .utilityWindow, .hudWindow],
backing: .buffered,
defer: false
)
panel.title = "Pointer"
panel.isFloatingPanel = true
panel.level = .statusBar
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
panel.isMovableByWindowBackground = true
panel.hidesOnDeactivate = false
panel.becomesKeyOnlyIfNeeded = true

let control = NSSegmentedControl(
labels: ["Desktop", "Game"],
trackingMode: .selectOne,
target: self,
action: #selector(changeSegment(_:))
)
control.segmentStyle = .rounded
control.setSelected(true, forSegment: mode == .relative ? 1 : 0)
control.setToolTip("Absolute tablet pointing for the Omarchy desktop.", forSegment: 0)
control.setToolTip("Relative mouse so games can lock and capture the cursor.", forSegment: 1)
control.setAccessibilityLabel("Pointer mode")
control.translatesAutoresizingMaskIntoConstraints = false
segmentedControl = control

let content = NSView(frame: NSRect(x: 0, y: 0, width: 248, height: 44))
content.addSubview(control)
NSLayoutConstraint.activate([
control.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 12),
control.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -12),
control.centerYAnchor.constraint(equalTo: content.centerYAnchor),
])
panel.contentView = content

if let screen = NSScreen.main {
let frame = screen.visibleFrame
let origin = NSPoint(
x: frame.maxX - panel.frame.width - 16,
y: frame.maxY - panel.frame.height - 16
)
panel.setFrameOrigin(origin)
}
panel.orderFrontRegardless()
hud = panel
}

private func makeMenu(mode: PointerInputMode) -> NSMenu {
let menu = NSMenu(title: "Pointer")
menu.autoenablesItems = false
menu.addItem(modeItem(
title: "Desktop pointer",
toolTip: "Absolute tablet pointing for the Omarchy desktop.",
action: #selector(selectAbsolute),
selected: mode == .absolute
))
menu.addItem(modeItem(
title: "Game pointer",
toolTip: "Relative mouse so games can lock and capture the cursor.",
action: #selector(selectRelative),
selected: mode == .relative
))
menu.addItem(.separator())
let help = NSMenuItem(
title: "Ctrl-Alt releases a grabbed cursor",
action: nil,
keyEquivalent: ""
)
help.isEnabled = false
menu.addItem(help)
return menu
}

private func modeItem(
title: String,
toolTip: String,
action: Selector,
selected: Bool
) -> NSMenuItem {
let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
item.target = self
item.state = selected ? .on : .off
item.toolTip = toolTip
return item
}

@objc private func selectAbsolute() {
apply(.absolute)
}

@objc private func selectRelative() {
apply(.relative)
}

@objc private func changeSegment(_ sender: NSSegmentedControl) {
apply(sender.selectedSegment == 1 ? .relative : .absolute)
}

private func apply(_ mode: PointerInputMode) {
guard let controller else { return }
do {
try controller.setMode(mode)
onModeChanged?(mode)
refresh(mode: mode)
} catch {
fputs(
"omarchy-vm-helper: could not switch pointer mode: \(error.localizedDescription)\n",
stderr
)
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = "Couldn’t change the pointer"
alert.informativeText = error.localizedDescription
alert.addButton(withTitle: "OK")
alert.runModal()
refresh(mode: controller.mode)
}
}

private func refresh(mode: PointerInputMode) {
statusItem?.menu = makeMenu(mode: mode)
segmentedControl?.setSelected(true, forSegment: mode == .relative ? 1 : 0)
}
}
63 changes: 63 additions & 0 deletions macos/Sources/OmarchyVMHelper/PointerPreferences.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Foundation

enum PointerInputMode: String, Equatable {
case absolute
case relative
}

struct PointerPreferences: Equatable {
var mode: PointerInputMode

static let defaults = Self(mode: .absolute)
}

struct PointerPreferenceStore {
static let key = "pointerPreferences"
static let schemaVersion = 1

private let defaults: UserDefaults

init(defaults: UserDefaults = .standard) {
self.defaults = defaults
}

func load() -> PointerPreferences {
guard let data = defaults.data(forKey: Self.key),
let payload = try? JSONDecoder().decode(Payload.self, from: data),
payload.schemaVersion == Self.schemaVersion,
let mode = PointerInputMode(rawValue: payload.mode) else {
return .defaults
}
return PointerPreferences(mode: mode)
}

func save(_ preferences: PointerPreferences) {
let payload = Payload(
schemaVersion: Self.schemaVersion,
mode: preferences.mode.rawValue
)
guard let data = try? JSONEncoder().encode(payload) else { return }
defaults.set(data, forKey: Self.key)
}

private struct Payload: Codable {
let schemaVersion: Int
let mode: String
}
}

struct PointerLaunchConfiguration: Equatable {
static let environmentKey = "OMARCHY_QEMU_POINTER_MODE"

let environment: [String: String]

static func make(
baseEnvironment: [String: String],
preferences: PointerPreferences
) -> Self {
var environment = baseEnvironment
environment.removeValue(forKey: environmentKey)
environment[environmentKey] = preferences.mode.rawValue
return Self(environment: environment)
}
}
6 changes: 6 additions & 0 deletions macos/Sources/OmarchyVMHelper/StartMenuPresentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,10 @@ enum StartMenuPresentation {
? "Omarchy opens Full Screen with the Mac menu bar and Dock hidden."
: "Omarchy opens in a window with the Mac menu bar and Dock available."
}

static func relativePointerDetail(isEnabled: Bool) -> String {
isEnabled
? "Starts with relative mouse for games. Switch anytime from the Pointer HUD."
: "Desktop uses absolute pointing. Turn on for games that need pointer lock."
}
}
Loading
Loading