Skip to content
Merged
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
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
<a href="https://github.com/niharnm/Semper/graphs/contributors"><img src="https://img.shields.io/github/contributors/niharnm/Semper" alt="Contributors"/></a>
</p>

Native macOS utilities in one menu bar app. Sound provides independent app volume, output routing, ISO 226 equal-loudness compensation, and AutoEQ headphone correction. Awake requests that macOS prevent automatic idle sleep for a chosen duration. Away covers each display with a customizable privacy curtain and requires Mac authentication or a Semper PIN to exit. External Displays provides supported brightness, contrast, volume, and input controls, while Scenes group reversible settings.
Control windows, files, displays, power, and sound from one native macOS menu bar app. Add the utilities you need, use them independently, or combine settings with Scenes and Presentation. Away provides an authenticated privacy curtain.

The current download, v1.0.0, contains Sound. The other modules are integrated in this repository and are in development toward the next release. The [product status guide](guide/product-status.md) records each module's state.

[semper.systems](https://www.semper.systems/)

Expand All @@ -34,6 +36,10 @@ Semper is an open-source project founded and led by [**Nihar Manchikakapudi**](h
Open the disk image and drag **Semper** into **Applications**. Requires
macOS 15.4 or later.

The current release is v1.0.0, published 2026-08-26, and contains the Sound
utility. The other modules described above are not in this download; building
from source runs current development code.

With [Homebrew](https://brew.sh):

```bash
Expand Down Expand Up @@ -115,6 +121,10 @@ in your keychain so macOS can recognize later source updates as the same app.

## Documentation & Guides

- [Product Status](guide/product-status.md)
- [Module Shell](guide/module-shell.md)
- [Direct Utilities: Workspace Restore, File Shelf, Safe Eject](guide/direct-utilities.md)
- [Awake Sessions](guide/awake-sessions.md)
- [URL Schemes](guide/url-schemes.md)
- [App Shortcuts](guide/app-shortcuts.md)
- [Experiments](guide/experiments.md)
Expand Down
160 changes: 106 additions & 54 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,111 @@
# Semper roadmap

Semper publishes signed macOS releases. This roadmap describes the work needed
to keep distribution dependable and grow the project without hiding
experimental behavior.
Semper is one menu bar app with nine utility modules: Sound, Awake, Displays,
Workspace Restore, File Shelf, Safe Eject, Scenes, Away, and Presentation. All
nine are integrated on `main`. The downloadable release is v1.0.0, which
contains Sound only. This roadmap orders the work to deliver the whole suite as
dependable signed releases without hiding experimental behavior. Per-module
state lives in the [product status guide](guide/product-status.md).

## Current priorities

### 1. Release maintenance

- Test signed and notarized builds on a clean Mac before publication.
- Verify install, permission, relaunch, update, and uninstall behavior for each
release.
- Publish accurate release notes and known device limits.
- Keep the website, README, Homebrew cask, and update feed tied to the same
release artifact.

### 2. Modules and utility foundation

- Keep Sound and Awake independent while they share the same menu bar shell.
- Add new utilities only when they have a clear local use case and no hidden account requirement.
- Keep module actions testable without live audio or power-management side effects.

### 3. Audio reliability

- Add focused tests around tap lifecycle, crossfades, output gating, and device
reconnect behavior.
- Collect reproducible reports for apps with custom audio engines.
- Preserve real-time callback safety and resource teardown order.

### 4. Device compatibility

- Record verified behavior for built-in, Bluetooth, USB, HDMI, DisplayPort,
DDC, aggregate, and virtual devices.
- Improve handling for devices that report controls they do not actually
support.
- Document where software volume or ignored-app behavior is the correct
fallback.

### 5. Interface and accessibility

- Audit keyboard navigation, focus order, VoiceOver labels, contrast, reduced
motion, and visible capability states.
- Keep the menu-bar popup and settings behavior consistent.
- Add tests for state transitions that do not require live audio hardware.

### 6. Contributor documentation

- Keep starter issues small, testable, and unclaimed until someone begins.
- Add architecture notes for high-risk audio paths.
- Turn verified device reports and recurring support answers into guides.
### 1. Ship the integrated multi-utility suite

- Complete native acceptance of the integrated build: signed clean-Mac install,
real permission prompts, hardware behavior, accessibility, update, and
uninstall. The [status guide](guide/product-status.md) records the gates.
- Publish the next release only when the website, README, release notes,
Homebrew cask, and update feed describe the same artifact.
- Keep the released-versus-development boundary explicit everywhere.

### 2. Shell, navigation, permissions, and lifecycle

- Keep one predictable shell: Home summaries, action search, favorites, and
module add, pause, resume, and remove.
- Adding a module must continue to start no service and request no permission.
First explicit use creates the runtime and states its permission reason.
- Denied or revoked permissions, limited runtimes, and failed cleanup stay
visible with a recovery path. Quit drains composed sessions before the
services they use. See the [module shell guide](guide/module-shell.md).
- Finish the current interaction gaps first: keyboard-accessible file selection
in File Shelf and cancellation while Presentation prepares or starts.

### 3. Window Layout

The next planned increment. It is not implemented today.

- Manual placement commands: left half, right half, maximize to the usable
screen area, center, and restore the last placement.
- Built in its own branch on the existing Workspace Restore window helpers.
- Manual actions only: no automatic tiling and no window watching.
- Preserve the intended window when the menu bar takes focus. Verify each
placement and keep later manual adjustments intact when restoring.
- Test half and maximized windows through later center and restore actions
without weakening Workspace Restore's fullscreen protections.

### 4. Cross-module workflows

- Scenes and Presentation compose the other modules: preview before apply,
verified writes, reverse-order recovery, and visible partial failures.
- Composition stays a feature, not a requirement. Every module must remain
useful on its own.

### 5. Module depth guided by user jobs and reuse

- Sound and Displays: grow verified device compatibility and document correct
fallbacks for devices that misreport controls.
- Workspace Restore: arrangement reliability across displays, Spaces, and
restarts.
- File Shelf and Safe Eject: behavior improvements from reproducible reports,
keeping original files and volumes safe.
- After File Shelf's file picker, add **Resize Image Copy** for one selected
local JPEG or PNG. Offer 1024 or 2048 pixels on the longest edge without
enlargement, show output dimensions, and save a separate copy. Preserve
orientation, color and transparency, explain metadata handling, and support
cancellation. No batch processing, uploads, or original-file replacement.
- Awake and Away: keep power assertions and the curtain testable and honest
about what they do not block.
- A new utility needs a clear local user job, no account requirement, the
shared lifecycle and disclosure rules, and reuse of existing services where
reasonable.

### 6. Quality, accessibility, and contributor documentation

- Preserve real-time audio callback safety. Add focused tests around tap
lifecycle, crossfades, output gating, and device reconnect behavior.
- Audit keyboard navigation, focus order, VoiceOver labels, contrast, and
reduced motion across all module surfaces.
- Turn verified device reports and recurring support answers into guides, and
keep starter issues small, testable, and unclaimed until someone begins.

## Reference utilities

Focused specialist tools set the expectations each Semper module must meet:

- [Rectangle](https://github.com/rxhanson/Rectangle) for window placement
- [MonitorControl](https://github.com/MonitorControl/MonitorControl) for
external display control
- [Amphetamine](https://apps.apple.com/us/app/amphetamine/id937984704) for
awake sessions
- [Dropover](https://dropoverapp.com/) for a file shelf
- [Vorssaint utilities](https://github.com/vorssaint/vorssaint-utils) for a
broad free modular suite
- [FineTune](https://github.com/ronitsingh10/FineTune) for per-app audio with
AutoEQ and ISO 226 loudness compensation

Semper has not benchmarked against these tools and claims no superiority. The
case for Semper is one shell with shared lifecycle, disclosure, and recovery
rules, and modules that can work together.

## Contribution levels

- **Starter:** documentation, pure-function tests, accessibility labels, and
isolated UI state.
- **Intermediate:** parser behavior, settings state, device classification,
and failure recovery with tests.
- **Advanced:** process taps, aggregate devices, HAL callback code, crossfades,
DSP lifecycle, and output safety.
module lifecycle transitions, and failure recovery with tests.
- **Advanced:** process taps, aggregate devices, HAL callback code, DSP
lifecycle, DDC transport, Accessibility window operations, Disk Arbitration,
power assertions, and curtain input filtering.

Use [good first issues](https://github.com/niharnm/Semper/contribute) for a
first pull request. Advanced work should begin with a GitHub issue or
Expand All @@ -66,11 +114,15 @@ discussion and include a hardware test plan.
## Current boundaries

- Supported platform: macOS 15.4 or later.
- Available today: Sound controls, timed or indefinite Awake sessions, source
builds, unit tests, the static website, and signed macOS releases through
GitHub and Homebrew.
- Downloadable today: v1.0.0, published 2026-08-26, with Sound. Source builds,
unit tests, the static website, and signed releases through GitHub and
Homebrew are current.
- Integrated on `main` and in no download yet: Awake, Displays, Workspace
Restore, File Shelf, Safe Eject, Scenes, Away, and Presentation.
- Planned additions: Window Layout and File Shelf's Resize Image Copy action.
- Hardware-dependent: process taps, device routing, DDC, Bluetooth call mode,
media keys, and permission behavior.
media keys, Accessibility window operations, volume ejection, and permission
behavior.
- Release-dependent: automatic updates require a current signed feed, and broad
device compatibility claims require verified hardware reports.
compatibility claims require verified hardware reports.
- Out of scope today: Windows, Linux, iOS, cloud accounts, and audio recording.
9 changes: 7 additions & 2 deletions Semper/Presentation/PresentationController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ final class PresentationController {
phase == .preview && !isBusy && (scenePreview?.canApply ?? true)
}

var canCancelOperation: Bool {
isBusy && (phase == .preparing || phase == .starting)
}

var retainedModules: Set<UtilityModuleID> {
guard reservation != nil else { return [] }
var result: Set<UtilityModuleID> = [.scenes]
Expand Down Expand Up @@ -143,7 +147,7 @@ final class PresentationController {
try Task.checkCancellation()
self.phase = .preview
} catch {
self.message = error.localizedDescription
self.message = error is CancellationError ? "Presentation cancelled." : error.localizedDescription
await self.recoverAfterFailure()
throw error
}
Expand Down Expand Up @@ -177,14 +181,15 @@ final class PresentationController {
if let plan = draft.workspacePlan, let workspace = self.workspace {
let result = await workspace.apply(plan, ownerToken: token)
self.workspaceReceipt = result
try Task.checkCancellation()
guard result.outcome == .completed else { throw PresentationError.workspacePartial }
}
try Task.checkCancellation()
guard self.now() < deadline else { throw PresentationError.deadlineReached }
self.phase = .active
self.message = "Presentation is active. Later manual changes will be preserved during restore."
} catch {
self.message = error.localizedDescription
self.message = error is CancellationError ? "Presentation cancelled." : error.localizedDescription
await self.recoverAfterFailure()
throw error
}
Expand Down
11 changes: 10 additions & 1 deletion Semper/Presentation/PresentationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ struct PresentationView: View {
.textSelection(.enabled)
}
if controller.isBusy {
HStack { ProgressView().controlSize(.small); Text("Finishing the current operation…") }
HStack {
ProgressView().controlSize(.small)
Text("Finishing the current operation…")
if controller.canCancelOperation {
Button("Cancel Presentation") { run { try await controller.stop() } }
.keyboardShortcut(.cancelAction)
.help("Cancel preparation or startup and restore any applied changes.")
}
}
}
if controller.reservation == nil {
if runtime.scenes?.hasPendingRestore == true {
Expand Down Expand Up @@ -304,6 +312,7 @@ struct PresentationView: View {
private func run(_ action: @escaping @MainActor () async throws -> Void) {
Task { @MainActor in
do { errorMessage = nil; try await action() }
catch is CancellationError { errorMessage = nil }
catch { errorMessage = error.localizedDescription }
}
}
Expand Down
1 change: 1 addition & 0 deletions Semper/SemperApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ struct SemperApp: App {
FluidMenuBarExtra("Semper", image: launchIconImage, isInserted: $showMenuBarExtra) {
if let runtime { UtilityShellView(runtime: runtime, compact: true) }
}
.commandsRemoved() // The library supplies its own empty Settings scene.
}

init() {
Expand Down
35 changes: 35 additions & 0 deletions Semper/Shelf/ShelfFileSelection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import AppKit

@MainActor
protocol ShelfFileChoosing: AnyObject {
func chooseFiles() async -> [URL]?
func cancel()
}

@MainActor
final class NativeShelfFileChooser: ShelfFileChoosing {
private var panel: NSOpenPanel?

func chooseFiles() async -> [URL]? {
let panel = NSOpenPanel()
panel.canChooseFiles = true
panel.canChooseDirectories = true
panel.allowsMultipleSelection = true
panel.resolvesAliases = true
panel.prompt = "Add to Shelf"
panel.message = "Choose files or folders. Original items stay in place."
self.panel = panel
return await withCheckedContinuation { continuation in
panel.begin { response in
Task { @MainActor in
self.panel = nil
continuation.resume(returning: response == .OK ? panel.urls : nil)
}
}
}
}

func cancel() {
panel?.cancel(nil)
}
}
6 changes: 3 additions & 3 deletions Semper/Shelf/ShelfModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ nonisolated enum ShelfFileState: Equatable, Sendable {
var message: String {
switch self {
case .available(let directory): directory ? "Folder reference" : "File reference"
case .missing: "Original file is missing. Locate it in Finder and drop it again."
case .missing: "Original file is missing. Locate it in Finder and add it again."
case .cloudOnly: "Download this item in Finder, then refresh the shelf."
case .inaccessible: "File access is unavailable. Drop the item again to grant access."
case .inaccessible: "File access is unavailable. Choose or drop the item again to grant access."
}
}
var isAvailable: Bool {
Expand All @@ -79,7 +79,7 @@ nonisolated enum ShelfFailure: Error, Equatable, LocalizedError, Sendable {
case .unsupported: "This drop has no supported file, image, link, or plain-text representation."
case .missing: "The original file is missing."
case .cloudOnly: "Download this item in Finder before using it."
case .inaccessible: "The item cannot be read. Check access in Finder and drop it again."
case .inaccessible: "The item cannot be read. Check access in Finder, then choose or drop it again."
case .invalidStore: "Saved shelf data could not be read. It has been left untouched."
case .storeVersion: "This saved shelf uses a newer format. It has been left untouched."
case .storeWrite: "The shelf could not save its local data."
Expand Down
Loading
Loading