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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ htmlcov/
*.db-wal
*.db-shm
client_secret.json
*.local.md
.DS_Store
xcuserdata/
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

### Added
- **Menu bar app** (`Ogmac.app`) — SwiftUI status item that surfaces sync state, settings, and run history without a terminal. Built unsigned; install via `bash packaging/build_app.sh` then `open dist/Ogmac.app`. Spec: `docs/superpowers/specs/2026-05-08-menu-bar-app-design.md`. Plan: `docs/superpowers/plans/2026-05-08-menu-bar-app-team-breakdown.md`.
- File-system event watcher on `state.db` (DispatchSource). The panel refreshes within ~200 ms of each daemon write — no periodic polling, no missed updates regardless of sync duration.
- "Last change" panel row that shows the most recent run with non-zero CRUD activity, or "Up to date · checked X min ago" when only no-op syncs exist in the last 50 runs.
- History view with a "meaningful runs only" filter. Hides the every-2-min network-change-triggered no-op syncs; keeps real changes, failures, and the scheduled `:00`/`:15`/`:30`/`:45` ticks.
- Diagnostic log file at `~/Library/Logs/ogmac/menubar.log` with structured per-event lines for refresh activity, file-watcher events, and reader errors.
- `ogmac pause` and `ogmac unpause` subcommands. `_run_sync` early-returns when paused. The `paused` flag persists in `state.db` and survives reboots, mirroring the existing `disabled` pattern.

### Changed
- `sync.interval_seconds` removed from the config schema. The field was never read; `StartCalendarInterval` in the launchd plist is the only schedule source. Existing configs that still contain the field are silently accepted with a deprecation warning.
- Minimum macOS for the menu bar app is **14.0** (the panel uses `@Environment(\.openSettings)`, which is macOS 14+). The Python daemon's minimum is unchanged.

## [0.1.0] - 2026-04-28

Initial release.
Expand Down
661 changes: 661 additions & 0 deletions Ogmac.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions Ogmac.xcodeproj/project.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

116 changes: 116 additions & 0 deletions Ogmac.xcodeproj/xcshareddata/xcschemes/Ogmac.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "66F56C885F925A1CFBC80DE8"
BuildableName = "Ogmac.app"
BlueprintName = "Ogmac"
ReferencedContainer = "container:Ogmac.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1B01FE72B690FF1D747CBA17"
BuildableName = "OgmacTests.xctest"
BlueprintName = "OgmacTests"
ReferencedContainer = "container:Ogmac.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
onlyGenerateCoverageForSpecifiedTargets = "NO">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "66F56C885F925A1CFBC80DE8"
BuildableName = "Ogmac.app"
BlueprintName = "Ogmac"
ReferencedContainer = "container:Ogmac.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1B01FE72B690FF1D747CBA17"
BuildableName = "OgmacTests.xctest"
BlueprintName = "OgmacTests"
ReferencedContainer = "container:Ogmac.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "66F56C885F925A1CFBC80DE8"
BuildableName = "Ogmac.app"
BlueprintName = "Ogmac"
ReferencedContainer = "container:Ogmac.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "66F56C885F925A1CFBC80DE8"
BuildableName = "Ogmac.app"
BlueprintName = "Ogmac"
ReferencedContainer = "container:Ogmac.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
70 changes: 70 additions & 0 deletions Ogmac/AppDeps.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Foundation

@MainActor
final class AppDeps: ObservableObject {
let stateReader: any StateReading
let logReader: any LogReading
let configStore: any ConfigStoring
let runner: any OgmacCommanding
let settingsViewModel: SettingsViewModel
let statusController: StatusController
let launchAtLogin: LaunchAtLogin
@Published var firstLaunch: Bool

convenience init() {
let home = FileManager.default.homeDirectoryForCurrentUser
let dbPath = home
.appendingPathComponent("Library/Application Support/ogmac/state.db")
.path
let logPath = home
.appendingPathComponent("Library/Logs/ogmac/sync.log")
let configPath = home
.appendingPathComponent(".config/ogmac/config.yaml")

self.init(
runnerOverride: OgmacRunner(),
configStoreOverride: ConfigStore(configPath: configPath),
stateReaderOverride: StateReader(dbPath: dbPath),
logReaderOverride: LogReader(logPath: logPath)
)
}

init(
runnerOverride: any OgmacCommanding,
configStoreOverride: any ConfigStoring,
stateReaderOverride: (any StateReading)? = nil,
logReaderOverride: (any LogReading)? = nil
) {
let home = FileManager.default.homeDirectoryForCurrentUser
let dbPath = home
.appendingPathComponent("Library/Application Support/ogmac/state.db")
.path
let logPath = home
.appendingPathComponent("Library/Logs/ogmac/sync.log")

let stateReader = stateReaderOverride ?? StateReader(dbPath: dbPath)
let logReader = logReaderOverride ?? LogReader(
logPath: home.appendingPathComponent("Library/Logs/ogmac/sync.log")
)
_ = logPath

self.stateReader = stateReader
self.logReader = logReader
self.configStore = configStoreOverride
self.runner = runnerOverride
self.launchAtLogin = LaunchAtLogin()

self.settingsViewModel = SettingsViewModel(store: configStoreOverride)

let cliMissing = runnerOverride.binaryPath == nil
let configMissing = (try? configStoreOverride.load()) == nil
self.firstLaunch = cliMissing || configMissing

self.statusController = StatusController(
stateReader: stateReader,
logReader: logReader,
runner: runnerOverride,
stateDbPath: dbPath
)
}
}
18 changes: 18 additions & 0 deletions Ogmac/Assets.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"images" : [
{ "filename" : "icon-16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" },
{ "filename" : "icon-32.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" },
{ "filename" : "icon-32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" },
{ "filename" : "icon-64.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" },
{ "filename" : "icon-128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" },
{ "filename" : "icon-256.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" },
{ "filename" : "icon-256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" },
{ "filename" : "icon-512.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" },
{ "filename" : "icon-512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" },
{ "filename" : "icon-1024.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" }
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Ogmac/Assets.xcassets/AppIcon.appiconset/icon-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Ogmac/Assets.xcassets/AppIcon.appiconset/icon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions Ogmac/ConfigStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Foundation
import Yams

protocol ConfigStoring {
func load() throws -> ConfigDoc
func save(_ doc: ConfigDoc) throws
}

enum ConfigStoreError: Error {
case fileNotFound
case encodingFailed(String)
}

struct ConfigStore: ConfigStoring {
let configPath: URL

init(configPath: URL) {
self.configPath = configPath
}

func load() throws -> ConfigDoc {
guard FileManager.default.fileExists(atPath: configPath.path) else {
throw ConfigStoreError.fileNotFound
}
let data = try Data(contentsOf: configPath)
let decoder = YAMLDecoder()
return try decoder.decode(ConfigDoc.self, from: data)
}

func save(_ doc: ConfigDoc) throws {
let encoder = YAMLEncoder()
let yaml: String
do {
yaml = try encoder.encode(doc)
} catch {
throw ConfigStoreError.encodingFailed(error.localizedDescription)
}
if !FileManager.default.fileExists(atPath: configPath.path) {
let dir = configPath.deletingLastPathComponent()
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
try yaml.write(to: configPath, atomically: true, encoding: .utf8)
return
}
let tmp = configPath.appendingPathExtension("tmp")
try yaml.write(to: tmp, atomically: true, encoding: .utf8)
_ = try FileManager.default.replaceItemAt(configPath, withItemAt: tmp)
}
}
30 changes: 30 additions & 0 deletions Ogmac/DebugLog.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

enum DebugLog {
static let path: URL = {
let dir = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Logs/ogmac")
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir.appendingPathComponent("menubar.log")
}()

private static let formatter: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return f
}()

static func write(_ message: String) {
let line = "\(formatter.string(from: Date())) \(message)\n"
guard let data = line.data(using: .utf8) else { return }
if !FileManager.default.fileExists(atPath: path.path) {
try? data.write(to: path)
return
}
if let fh = try? FileHandle(forWritingTo: path) {
try? fh.seekToEnd()
try? fh.write(contentsOf: data)
try? fh.close()
}
}
}
38 changes: 38 additions & 0 deletions Ogmac/FirstLaunchView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import SwiftUI

struct FirstLaunchView: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Welcome to ogmac")
.font(.headline)

Text("Before this app can show sync status, you need to install and configure the ogmac CLI.")
.font(.caption)
.foregroundStyle(.secondary)

Text("Run in Terminal:")
.font(.caption)

HStack {
Text("ogmac login")
.font(.system(.body, design: .monospaced))
.padding(6)
.background(Color.secondary.opacity(0.1))
.cornerRadius(4)

Button("Copy") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString("ogmac login", forType: .string)
}
.buttonStyle(.borderless)
}

Divider()

Button("Quit ogmac") {
NSApp.terminate(nil)
}
}
.padding(16)
}
}
Loading
Loading