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
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,11 @@ new VM, after a confirmed **Reset Omarchy**, or for an ephemeral launch.
Before Reset is enabled, the confirmation sheet requires typing `Try Omarchy`
exactly; cancelling the sheet returns to the start menu without changing the VM.

The start menu's **VM disk limit** setting chooses the capacity of a new or
reset VM in whole GB, with a 10 GB minimum and a 24 GB default. Changing it
does not resize an existing VM; the selected limit takes effect the next time
a VM is created or explicitly reset.

VMs created before paired boot files were introduced are preserved too. On the
first launch that needs them, Try Omarchy explains the transition in a
**Continue** / **Cancel** dialog before starting recovery. Continue performs a
Expand Down Expand Up @@ -296,8 +301,10 @@ you choose — it never creates a folder inside it on your behalf.
the moment it was created. Network volumes are refused because the VM's disk
lock is unreliable on them. Anything else is turned away when you pick it, with
the actual format named.
- You need roughly 7 GB free to create the VM, and up to 30 GB as it fills. The
disk is sparse, so it only ever occupies what the guest has actually written.
- You need roughly 7 GB free to create the default VM. With the default 24 GB
disk limit, the workspace can use up to roughly 30 GB including its factory
image. A larger selected limit raises that maximum. The disk is sparse, so
it only occupies what the guest has actually written.
- **Changing the location does not move your existing VM.** It stays where it
is, and switching back reaches it again.
- Do not disconnect the drive while Omarchy is running. macOS refuses a normal
Expand Down
2 changes: 1 addition & 1 deletion guest/Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ COPY --from=bootstrapper /rootfs/ /

RUN pacman-key --init && pacman-key --populate archlinuxarm && \
pacman -Syu --noconfirm && \
pacman -S --needed --noconfirm arch-install-scripts e2fsprogs git python rust=1:1.98.0-1 zstd && \
pacman -S --needed --noconfirm arch-install-scripts e2fsprogs git python rust=1:1.98.1-1 zstd && \
pacman -Scc --noconfirm

WORKDIR /workspace
Expand Down
4 changes: 2 additions & 2 deletions guest/scripts/finalize-rootfs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ systemctl enable omarchy-provision-owner.service
systemctl enable sddm.service
systemctl enable omarchy-native-mac-share.service

# The app expands only the writable APFS clone to 24 GiB. Grow ext4 online so
# Omarchy's update-safety check sees that working capacity.
# The app expands only the writable APFS clone to the configured capacity.
# Grow ext4 online so Omarchy's update-safety check sees that working capacity.
[[ -f /usr/lib/systemd/system/systemd-growfs-root.service ]] || { echo "Missing systemd root grow service" >&2; exit 1; }
mkdir -p /etc/systemd/system/local-fs.target.wants
ln -sfn /usr/lib/systemd/system/systemd-growfs-root.service \
Expand Down
2 changes: 1 addition & 1 deletion guest/scripts/register-pinned-ttfx.sh
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ source_date_epoch=${metadata[20]}

[[ $architecture == aarch64 ]] || fail "pinned ttfx component supports only aarch64"
[[ $target == aarch64-unknown-linux-gnu ]] || fail "unexpected ttfx build target: $target"
[[ $rust_package_version == "rust 1:1.98.0-1" ]] || fail "unexpected ttfx Rust package version"
[[ $rust_package_version == "rust 1:1.98.1-1" ]] || fail "unexpected ttfx Rust package version"
[[ $(pacman -Q rust) == "$rust_package_version" ]] || fail "ttfx Rust package identity mismatch"
[[ $(rustc --version) == "$rustc_version" ]] || fail "ttfx rustc identity mismatch"
[[ $(cargo --version) == "$cargo_version" ]] || fail "ttfx Cargo identity mismatch"
Expand Down
4 changes: 3 additions & 1 deletion guest/scripts/resolve-package-lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ def main() -> None:
check=False,
)
if result.returncode:
raise SystemExit(result.stderr or result.stdout)
# pacman reports the failure on stderr but names each unsatisfied
# dependency on stdout, so a diagnosis needs both streams.
raise SystemExit("".join(part for part in (result.stderr, result.stdout) if part).strip())

resolved: dict[str, str] = {}
for line in result.stdout.splitlines():
Expand Down
8 changes: 4 additions & 4 deletions guest/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@
"url": "https://github.com/omacom-io/ttfx/archive/refs/tags/v0.3.2.tar.gz",
"sha256": "d0c0df4867e7f03142fb7f77c66670d0e8da15534239c1a7abfd89f19dfc00f6",
"cargoLockSha256": "49e2091962fc4d425b4cf3bde1a105719b5b50eed0583ec90e85922adb45e2ce",
"binarySha256": "9171a07c752b202a21f80a4ad336a9d093be06a6c96b062e8b5e0c158d2a86d2",
"binarySha256": "d034cc5b9a8d410ce93113ef0a5d27b5ee2327948562bf2b0e756eebd326fa8f",
"target": "aarch64-unknown-linux-gnu",
"rustPackageVersion": "rust 1:1.98.0-1",
"rustcVersion": "rustc 1.98.0 (88d9e12ae 2026-08-18) (Arch Linux rust 1:1.98.0-1)",
"cargoVersion": "cargo 1.98.0 (797e8a9bc 2026-08-05) (Arch Linux rust 1:1.98.0-1)",
"rustPackageVersion": "rust 1:1.98.1-1",
"rustcVersion": "rustc 1.98.1 (48a229cea 2026-09-01) (Arch Linux rust 1:1.98.1-1)",
"cargoVersion": "cargo 1.98.1 (797e8a9bc 2026-08-05) (Arch Linux rust 1:1.98.1-1)",
"reportedVersion": "ttfx 0.3.2",
"license": "MIT",
"licenseSha256": "175441de2eb9a0d3f0627c404ad71929336fd98d75926cc27b9e364d35cc7977",
Expand Down
10 changes: 5 additions & 5 deletions guest/tests/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,11 +342,11 @@ def main() -> None:
"url": "https://github.com/omacom-io/ttfx/archive/refs/tags/v0.3.2.tar.gz",
"sha256": "d0c0df4867e7f03142fb7f77c66670d0e8da15534239c1a7abfd89f19dfc00f6",
"cargoLockSha256": "49e2091962fc4d425b4cf3bde1a105719b5b50eed0583ec90e85922adb45e2ce",
"binarySha256": "9171a07c752b202a21f80a4ad336a9d093be06a6c96b062e8b5e0c158d2a86d2",
"binarySha256": "d034cc5b9a8d410ce93113ef0a5d27b5ee2327948562bf2b0e756eebd326fa8f",
"target": "aarch64-unknown-linux-gnu",
"rustPackageVersion": "rust 1:1.98.0-1",
"rustcVersion": "rustc 1.98.0 (88d9e12ae 2026-08-18) (Arch Linux rust 1:1.98.0-1)",
"cargoVersion": "cargo 1.98.0 (797e8a9bc 2026-08-05) (Arch Linux rust 1:1.98.0-1)",
"rustPackageVersion": "rust 1:1.98.1-1",
"rustcVersion": "rustc 1.98.1 (48a229cea 2026-09-01) (Arch Linux rust 1:1.98.1-1)",
"cargoVersion": "cargo 1.98.1 (797e8a9bc 2026-08-05) (Arch Linux rust 1:1.98.1-1)",
"reportedVersion": "ttfx 0.3.2",
"license": "MIT",
"licenseSha256": "175441de2eb9a0d3f0627c404ad71929336fd98d75926cc27b9e364d35cc7977",
Expand Down Expand Up @@ -451,7 +451,7 @@ def main() -> None:
check("try-omarchy-guest-work" in container, "guest cache has a project-scoped Docker volume")
containerfile = read(GUEST / "Containerfile")
check(
"arch-install-scripts e2fsprogs git python rust=1:1.98.0-1 zstd" in containerfile,
"arch-install-scripts e2fsprogs git python rust=1:1.98.1-1 zstd" in containerfile,
"guest builder pins Rust for source-built components",
)

Expand Down
90 changes: 90 additions & 0 deletions macos/Sources/OmarchyVMHelper/DiskCapacity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import Foundation

struct DiskCapacityPreference: Codable, Equatable {
static let minimumGigabytes = 10
static let defaultGigabytes = 24
static let maximumGigabytes = 8_192
static let bytesPerGigabyte: Int64 = 1_073_741_824

var gigabytes: Int

static let `default` = Self(gigabytes: defaultGigabytes)

var bytes: Int64 {
Int64(gigabytes) * Self.bytesPerGigabyte
}

static func validationError(for gigabytes: Int) -> String? {
guard gigabytes >= minimumGigabytes else {
return "The VM disk limit must be at least \(minimumGigabytes) GB."
}
guard gigabytes <= maximumGigabytes else {
return "The VM disk limit cannot exceed \(maximumGigabytes) GB."
}
return nil
}
}

struct DiskCapacityPreferenceStore {
static let key = "diskCapacityPreferences"
static let schemaVersion = 1

private let defaults: UserDefaults

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

func load() -> DiskCapacityPreference {
guard let data = defaults.data(forKey: Self.key),
let payload = try? JSONDecoder().decode(Payload.self, from: data),
payload.schemaVersion == Self.schemaVersion,
DiskCapacityPreference.validationError(for: payload.gigabytes) == nil
else {
return .default
}
return DiskCapacityPreference(gigabytes: payload.gigabytes)
}

func save(_ preference: DiskCapacityPreference) throws {
if let problem = DiskCapacityPreference.validationError(for: preference.gigabytes) {
throw DiskCapacityError.invalid(problem)
}
let payload = Payload(
schemaVersion: Self.schemaVersion,
gigabytes: preference.gigabytes
)
defaults.set(try JSONEncoder().encode(payload), forKey: Self.key)
}

private struct Payload: Codable {
let schemaVersion: Int
let gigabytes: Int
}
}

enum DiskCapacityError: LocalizedError, Equatable {
case invalid(String)

var errorDescription: String? {
switch self {
case .invalid(let message):
message
}
}
}

struct DiskCapacityLaunchConfiguration: Equatable {
static let environmentKey = "OMARCHY_QEMU_GPU_DISK_SIZE_GB"

let environment: [String: String]

static func make(
baseEnvironment: [String: String],
preference: DiskCapacityPreference
) -> Self {
var environment = baseEnvironment
environment[environmentKey] = String(preference.gigabytes)
return Self(environment: environment)
}
}
83 changes: 83 additions & 0 deletions macos/Sources/OmarchyVMHelper/StartMenuWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ final class StartMenuWindow: NSObject, NSWindowDelegate {
private let validateStorageLocation: (String) -> String?
private let chooseStorageLocation: (String) -> String?
private let useDefaultStorageLocation: () -> Void
private let diskCapacity: () -> Int
private let saveDiskCapacity: (Int) -> String?

private var microphoneRequestInFlight = false
private var cameraRequestInFlight = false
Expand Down Expand Up @@ -275,6 +277,8 @@ final class StartMenuWindow: NSObject, NSWindowDelegate {
validateStorageLocation: @escaping (String) -> String?,
chooseStorageLocation: @escaping (String) -> String?,
useDefaultStorageLocation: @escaping () -> Void,
diskCapacity: @escaping () -> Int,
saveDiskCapacity: @escaping (Int) -> String?,
resetStorage: @escaping () -> Void,
sharedFolderStatus: @escaping () -> SharedFolderMenuState,
chooseSharedFolder: @escaping (String) -> String?,
Expand All @@ -299,6 +303,8 @@ final class StartMenuWindow: NSObject, NSWindowDelegate {
self.validateStorageLocation = validateStorageLocation
self.chooseStorageLocation = chooseStorageLocation
self.useDefaultStorageLocation = useDefaultStorageLocation
self.diskCapacity = diskCapacity
self.saveDiskCapacity = saveDiskCapacity
self.resetStorage = resetStorage
self.sharedFolderStatus = sharedFolderStatus
self.chooseSharedFolder = chooseSharedFolder
Expand Down Expand Up @@ -610,10 +616,27 @@ final class StartMenuWindow: NSObject, NSWindowDelegate {
)
}

let capacityRow: NSView? = canResetStorage
? permissionRow(
symbolName: "internaldrive",
title: "VM disk limit",
detail: "Maximum capacity for a new or reset VM. Existing VM disks are not resized.",
granted: true,
statusLabels: (
"\u{25cf} \(diskCapacity()) GB",
"\u{25cf} \(diskCapacity()) GB"
),
actions: [("Change\u{2026}", #selector(beginDiskCapacitySelection))]
)
: nil

var permissionRowViews = [accessibilityRow, microphoneRow, cameraRow, sharedFolderRow]
if let storageRow {
permissionRowViews.append(storageRow)
}
if let capacityRow {
permissionRowViews.append(capacityRow)
}
permissionRowViews.append(contentsOf: [portForwardingRow, immersiveRow])

var permissionRowsAndSeparators: [NSView] = []
Expand Down Expand Up @@ -1390,6 +1413,66 @@ final class StartMenuWindow: NSObject, NSWindowDelegate {
render()
launch()
}

@objc private func beginDiskCapacitySelection() {
guard canResetStorage,
!launchInProgress,
!resetInProgress
else { return }
presentDiskCapacityEditor(initialText: String(diskCapacity()))
}

private func presentDiskCapacityEditor(initialText: String) {
let alert = NSAlert()
alert.messageText = "Set VM disk limit"
alert.informativeText = "Enter the maximum virtual disk capacity in GB. The minimum is \(DiskCapacityPreference.minimumGigabytes) GB. This applies when a VM is first created or reset."
alert.addButton(withTitle: "Save")
alert.addButton(withTitle: "Cancel")

let field = NSTextField(string: initialText)
field.placeholderString = "\(DiskCapacityPreference.defaultGigabytes)"
field.alignment = .right
field.setAccessibilityLabel("VM disk limit in GB")

let unit = NSTextField(labelWithString: "GB")
let accessory = NSStackView(views: [field, unit])
accessory.orientation = .horizontal
accessory.alignment = .centerY
accessory.spacing = 8
accessory.frame = NSRect(x: 0, y: 0, width: 220, height: 26)
field.widthAnchor.constraint(equalToConstant: 170).isActive = true
alert.accessoryView = accessory

alert.beginSheetModal(for: window) { [weak self] response in
guard response == .alertFirstButtonReturn, let self else { return }
let entered = field.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
guard let gigabytes = Int(entered), String(gigabytes) == entered else {
self.presentDiskCapacityProblem(
"Enter a whole number of GB.",
enteredValue: entered
)
return
}
if let problem = self.saveDiskCapacity(gigabytes) {
self.presentDiskCapacityProblem(problem, enteredValue: entered)
return
}
self.render()
}
}

private func presentDiskCapacityProblem(_ message: String, enteredValue: String) {
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = "Invalid disk limit"
alert.informativeText = message
alert.addButton(withTitle: "Try Again")
alert.addButton(withTitle: "Cancel")
alert.beginSheetModal(for: window) { [weak self] response in
guard response == .alertFirstButtonReturn else { return }
self?.presentDiskCapacityEditor(initialText: enteredValue)
}
}
}

private final class StartMenuDocumentView: NSView {
Expand Down
Loading