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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

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. Source builds include additional utilities in development toward the next release. The [product status guide](guide/product-status.md) distinguishes integrated features from work still under review.
The current download, v1.0.0, contains Sound. Source builds include additional utilities in development toward the next release. The [product status guide](guide/product-status.md) distinguishes released, integrated, and staged features.

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

Expand Down Expand Up @@ -60,6 +60,7 @@ not download a Semper DMG from an unofficial source.

- **Independent Utilities**: Home provides module summaries, attention items, up to four pinned actions, search, and recent action outcomes for the current session. Add, pause, or remove modules individually; adding a module starts no service and requests no permission. Detailed controls open in a native window.
- **Manual Window Layout**: Five optional actions arrange eligible windows into halves, maximize, center, or restore the preceding placement. Full-height windows and targets are refused, which can limit halves and maximize when both the Dock and menu bar auto-hide. Source integration and native acceptance are tracked in the [product status guide](guide/product-status.md#window-layout).
- **Local Image Copies**: File Shelf's staged Resize a Copy action saves one local JPEG or PNG at up to 1,024 or 2,048 pixels on its longest edge without enlargement or overwriting a file. It removes descriptive metadata and requires a destination that supports macOS file cloning. See the [image-copy guide](guide/shelf-image-copy.md) for format, size, recovery, and native acceptance limits.
- **Local Awake Sessions**: Public IOKit power assertions prevent idle system sleep, optionally keep the display on, and keep timed user sessions separate from Scene requests.
- **Authenticated Away Curtain**: One opaque panel covers each display, ordinary input is filtered, and local widgets can show time, battery, Away duration, and awake-request state.
- **Swift 6 & Core Audio TCC Taps**: Built using modern Swift 6 strict concurrency (`@MainActor`, `Sendable`) and low-latency CoreAudio process taps.
Expand Down Expand Up @@ -127,6 +128,7 @@ in your keychain so macOS can recognize later source updates as the same app.
- [Module Shell](guide/module-shell.md)
- [Direct Utilities: Workspace Restore, File Shelf, Safe Eject](guide/direct-utilities.md)
- [Choose Files in File Shelf](guide/shelf-file-selection.md)
- [Resize a Copy in File Shelf](guide/shelf-image-copy.md)
- [Presentation Controls](guide/presentation-controls.md)
- [Window Layout](guide/window-layout.md)
- [Awake Sessions](guide/awake-sessions.md)
Expand Down
45 changes: 23 additions & 22 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# Semper roadmap

Semper's source in this change set contains ten utility modules: Sound, Awake,
Displays, Workspace Restore, Window Layout, File Shelf, Safe Eject, Scenes,
Away, and Presentation. The baseline has nine integrated modules. Window Layout
becomes the tenth integrated module when this change set merges into `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).
Semper has ten utility modules integrated on `main`: Sound, Awake, Displays,
Workspace Restore, Window Layout, File Shelf, Safe Eject, Scenes, Away, and
Presentation. Resize a Copy extends File Shelf in this change set and awaits
integration. 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

Expand Down Expand Up @@ -35,9 +34,9 @@ state lives in the [product status guide](guide/product-status.md).

### 3. Window Layout acceptance and compatibility

The cleared implementation from [PR #106](https://github.com/niharnm/Semper/pull/106)
is included in this change set. Integration requires its merge to `main`;
native acceptance remains open. See the [Window Layout guide](guide/window-layout.md).
The implementation from [PR #106](https://github.com/niharnm/Semper/pull/106)
is integrated on `main` through [PR #110](https://github.com/niharnm/Semper/pull/110).
Native acceptance remains open. See the [Window Layout guide](guide/window-layout.md).

- Verify all five manual commands, optional shortcuts, Home/search/pinned
actions, intended-window selection, and later manual changes on real apps.
Expand Down Expand Up @@ -67,14 +66,17 @@ native acceptance remains open. See the [Window Layout guide](guide/window-layou
restarts.
- File Shelf and Safe Eject: behavior improvements from reproducible reports,
keeping original files and volumes safe.
- Finish review of File Shelf's **Resize a 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.
- Preserve pending cleanup through lifecycle changes, bound expiry retries,
verify file ownership before removal, and report the actual saved path.
Image-copy integration waits for the corrected implementation and tests.
- Complete integration and native acceptance of File Shelf's **Resize a Copy**,
implemented in this change set. It resizes one local JPEG or PNG to a longest
edge of 1024 or 2048 pixels without enlargement or overwriting a file.
Format, displayed orientation, color profile and PNG transparency are kept;
descriptive metadata is removed. JPEG re-encoding can lose detail.
- Verify Save-dialog focus, keyboard access, cancellation, and refusal of
destinations without macOS file-cloning support. Exercise cleanup recovery,
the verified saved path retained until Done, and Finish Without Verification
for changed or deleted output. Private cleanup remains required and public
copies remain untouched. See the [image-copy guide](guide/shelf-image-copy.md)
for input limits and remaining checks. No batch processing or uploads.
- 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
Expand Down Expand Up @@ -130,9 +132,8 @@ discussion and include a hardware test plan.
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.
- Implemented in this change set, awaiting integration: Window Layout.
- Under review, not integrated: File Shelf's Resize a Copy.
Restore, Window Layout, File Shelf, Safe Eject, Scenes, Away, and Presentation.
- Implemented in this change set, awaiting integration: File Shelf's Resize a Copy.
- Hardware-dependent: process taps, device routing, DDC, Bluetooth call mode,
media keys, Accessibility window operations, volume ejection, and permission
behavior.
Expand Down
11 changes: 6 additions & 5 deletions Semper/Modules/UtilityRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -949,12 +949,13 @@ final class UtilityRuntime {
},
stop: { [weak self] reason in
guard let self, let shelf = self.shelf else { return }
if reason == .pause {
await shelf.pause()
} else {
await shelf.shutdown()
self.shelf = nil
let result = reason == .pause ? await shelf.pause() : await shelf.shutdown()
if case .failure(let failure) = result {
throw UtilityCleanupDeferral(
reason: shelf.imageCopy.message ?? shelf.message ?? failure.localizedDescription,
retaining: [.shelf])
}
if reason != .pause { self.shelf = nil }
}))
try lifecycle.register(
.storage,
Expand Down
130 changes: 128 additions & 2 deletions Semper/Modules/UtilityShellView.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Foundation
import KeyboardShortcuts
import SwiftUI

Expand Down Expand Up @@ -104,7 +105,7 @@ struct UtilityShellView: View {
registry: runtime.registry, lifecycle: runtime.lifecycle,
pause: runtime.pause, remove: runtime.remove, mutationDisabledReason: runtime.mutationDisabledReason)
case .module(let id):
module(id).disabled(id != .away && runtime.mutationDisabledReason != nil)
module(id).disabled(moduleInteractionDisabled(for: id))
}
}

Expand All @@ -128,7 +129,10 @@ struct UtilityShellView: View {
UtilityActionList(commands: runtime.commands, actions: runtime.registry.favoriteActions)
}
ForEach(runtime.registry.addedModules) { module in
if compact, module.id == .shelf,
if compact, module.id == .shelf, shelfStopRecoveryRoute != nil {
ShelfStopRecoveryView(runtime: runtime)
.disabled(moduleInteractionDisabled(for: .shelf))
} else if compact, module.id == .shelf,
!runtime.registry.pausedModuleIDs.contains(.shelf),
!runtime.lifecycle.stopping.contains(.shelf), !runtime.lifecycle.isShuttingDown,
let shelf = runtime.shelf, shelf.isRunning
Expand Down Expand Up @@ -230,6 +234,15 @@ struct UtilityShellView: View {
return .module(.scenes)
}

var shelfStopRecoveryRoute: ShelfStopRecoveryRoute? {
ShelfStopRecoveryRoute.current(in: runtime)
}

func moduleInteractionDisabled(for id: UtilityModuleID) -> Bool {
if id == .shelf, shelfStopRecoveryRoute != nil { return false }
return id != .away && runtime.mutationDisabledReason != nil
}

@ViewBuilder
private func module(_ id: UtilityModuleID) -> some View {
if id == .away, runtime.awayCleanupResult != nil, runtime.awayCleanupResult != .complete {
Expand All @@ -241,6 +254,8 @@ struct UtilityShellView: View {
runtime.sceneShortcuts == nil || runtime.lifecycle.isShuttingDown
{
SceneRecoveryView(manager: scenes)
} else if id == .shelf, shelfStopRecoveryRoute != nil {
ShelfStopRecoveryView(runtime: runtime)
} else if runtime.registry.pausedModuleIDs.contains(id) {
ContentUnavailableView {
Label("Module paused", systemImage: "pause.circle")
Expand Down Expand Up @@ -540,6 +555,117 @@ struct UtilitySettingsView: View {
}
}

nonisolated enum ShelfStopRecoveryRoute: Equatable, Sendable {
case pause, shutdown

@MainActor
static func current(in runtime: UtilityRuntime) -> Self? {
guard runtime.shelf?.stopFailure != nil else { return nil }
return runtime.lifecycle.isShuttingDown ? .shutdown : .pause
}

@MainActor
static func retry(in runtime: UtilityRuntime) async throws {
switch current(in: runtime) {
case .pause: try await runtime.pause(.shelf)
case .shutdown: await runtime.shutdown()
case nil: return
}
}

@MainActor
static func acknowledgeRecoveredCopy(in runtime: UtilityRuntime, requestID: UUID) async throws {
guard current(in: runtime) != nil,
runtime.shelf?.acknowledgeImageCopyReceipt(requestID: requestID) == true
else { return }
try await retry(in: runtime)
}

@MainActor
static func acknowledgeUnverifiedCopy(in runtime: UtilityRuntime, requestID: UUID) async throws {
guard current(in: runtime) != nil, let service = runtime.shelf,
service.imageCopy.request?.id == requestID, service.imageCopy.hasUnverifiedPublishedCopy
else { return }
try await service.acknowledgeUnverifiedImageCopy(requestID: requestID).get()
guard runtime.shelf === service, service.imageCopy.request == nil else { return }
try await retry(in: runtime)
}
}

private struct ShelfStopRecoveryView: View {
@Bindable var runtime: UtilityRuntime
@State private var retrying = false

var body: some View {
let acknowledgementRequest =
runtime.shelf?.imageCopy.needsReceiptAcknowledgement == true
? runtime.shelf?.imageCopy.request : nil
VStack(alignment: .leading, spacing: 14) {
Label("File Shelf cleanup needs attention", systemImage: "exclamationmark.triangle")
.font(.headline)
Text(
runtime.shelf?.imageCopy.message ?? runtime.shelf?.stopFailure?.localizedDescription
?? "Retry cleanup to finish stopping File Shelf."
)
.foregroundStyle(.secondary)
if let session = runtime.shelf?.imageCopy {
if session.needsReceiptAcknowledgement, let receipt = session.receipt {
Label("Copy recovered", systemImage: "checkmark.circle").foregroundStyle(.green)
Text(receipt.url.path).font(.callout).textSelection(.enabled)
} else {
ForEach(session.recoveryLocations, id: \.self) { location in
Text(location.path).font(.callout).textSelection(.enabled)
}
}
}
Text("Shelf items are retained until cleanup finishes.").font(.callout)
Button(acknowledgementRequest == nil ? "Retry Cleanup" : "Done") {
guard !retrying else { return }
retrying = true
Task {
do {
if let acknowledgementRequest {
try await ShelfStopRecoveryRoute.acknowledgeRecoveredCopy(
in: runtime, requestID: acknowledgementRequest.id)
} else {
try await ShelfStopRecoveryRoute.retry(in: runtime)
}
} catch {
runtime.message = error.localizedDescription
}
retrying = false
}
}
.buttonStyle(.borderedProminent)
.disabled(retrying || runtime.shelf?.isStopping == true || runtime.lifecycle.stopping.contains(.shelf))
if let session = runtime.shelf?.imageCopy, session.hasUnverifiedPublishedCopy,
let request = session.request
{
ShelfUnverifiedCopyExplanation()
Button("Finish Without Verification") {
guard !retrying else { return }
retrying = true
Task {
do {
try await ShelfStopRecoveryRoute.acknowledgeUnverifiedCopy(
in: runtime, requestID: request.id)
} catch {
runtime.message = error.localizedDescription
}
retrying = false
}
}
.buttonStyle(.borderless)
.disabled(
retrying || session.isWorking || runtime.shelf?.isStopping == true
|| runtime.lifecycle.stopping.contains(.shelf))
}
if retrying { ProgressView().controlSize(.small) }
}
.padding(24)
}
}

private struct AwayCleanupView: View {
@Bindable var runtime: UtilityRuntime
@State private var retrying = false
Expand Down
Loading
Loading