commit 451bf3a9fced914f95ab141b35601397f4be8c14 Author: Evan Reichard Date: Wed Apr 15 18:03:52 2026 -0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7447dba --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.build/ +.direnv/ +.swiftpm/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6e0e11a --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +DEVELOPER_DIR := /Library/Developer/CommandLineTools +SWIFT := /usr/bin/swift +INSTALL_DIR := $(HOME)/.local/bin +BINARY_NAME := nunc + +export DEVELOPER_DIR +unexport SDKROOT + +.PHONY: build release install clean run + +build: + $(SWIFT) build + +release: + $(SWIFT) build -c release + +install: release + mkdir -p $(INSTALL_DIR) + cp .build/release/Nunc $(INSTALL_DIR)/$(BINARY_NAME) + @echo "Installed to $(INSTALL_DIR)/$(BINARY_NAME)" + +clean: + $(SWIFT) package clean + rm -rf .build + +run: build + .build/debug/Nunc diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..feb4435 --- /dev/null +++ b/Package.swift @@ -0,0 +1,13 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "Nunc", + platforms: [.macOS(.v13)], + targets: [ + .executableTarget( + name: "Nunc", + path: "Sources/Nunc" + ) + ] +) diff --git a/Sources/Nunc/main.swift b/Sources/Nunc/main.swift new file mode 100644 index 0000000..5a61a3c --- /dev/null +++ b/Sources/Nunc/main.swift @@ -0,0 +1,152 @@ +import AppKit + +enum Position: String, Codable { + case top_left, top_center, top_right + case bottom_left, bottom_center, bottom_right +} + +struct Config: Codable { + var fontSize: Double = 14.0 + var verticalPadding: Double = 8.0 + var horizontalPadding: Double = 12.0 + var position: Position = .top_right + var offsetX: Double = 0.0 + var offsetY: Double = 0.0 +} + +func loadConfig() -> Config { + let configDir = FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent(".config/nunc") + let configFile = configDir.appendingPathComponent("config.json") + + guard FileManager.default.fileExists(atPath: configFile.path) else { + // Write default config if none exists + try? FileManager.default.createDirectory(at: configDir, withIntermediateDirectories: true) + let defaultConfig = Config() + if let data = try? JSONEncoder.pretty.encode(defaultConfig) { + try? data.write(to: configFile) + } + return Config() + } + + guard let data = try? Data(contentsOf: configFile), + let config = try? JSONDecoder().decode(Config.self, from: data) else { + fputs("nunc: failed to parse \(configFile.path), using defaults\n", stderr) + return Config() + } + return config +} + +extension JSONEncoder { + static var pretty: JSONEncoder { + let e = JSONEncoder() + e.outputFormatting = [.prettyPrinted, .sortedKeys] + return e + } +} + +let config = loadConfig() + +class UnconstrainedPanel: NSPanel { + override func constrainFrameRect(_ frameRect: NSRect, to screen: NSScreen?) -> NSRect { + return frameRect + } +} + +let app = NSApplication.shared +app.setActivationPolicy(.prohibited) + +let font = NSFont.monospacedDigitSystemFont(ofSize: config.fontSize, weight: .regular) + +let label = NSTextField(labelWithString: "") +label.font = font +label.textColor = .white +label.alignment = .center +label.translatesAutoresizingMaskIntoConstraints = false + +// Sample Dimensions +let sampleText = "2026-04-15T23:59:59+00:00" +let textSize = (sampleText as NSString).size(withAttributes: [.font: font]) +let windowSize = NSSize( + width: ceil(textSize.width + config.horizontalPadding * 2), + height: ceil(textSize.height + config.verticalPadding * 2) +) + +let screenFrame = NSScreen.main?.frame ?? NSRect(x: 0, y: 0, width: 1440, height: 900) + +func computeOrigin() -> NSPoint { + let x: Double + let y: Double + + switch config.position { + case .top_left: + x = screenFrame.minX + y = screenFrame.maxY - windowSize.height + case .top_center: + x = screenFrame.midX - windowSize.width / 2 + y = screenFrame.maxY - windowSize.height + case .top_right: + x = screenFrame.maxX - windowSize.width + y = screenFrame.maxY - windowSize.height + case .bottom_left: + x = screenFrame.minX + y = screenFrame.minY + case .bottom_center: + x = screenFrame.midX - windowSize.width / 2 + y = screenFrame.minY + case .bottom_right: + x = screenFrame.maxX - windowSize.width + y = screenFrame.minY + } + + return NSPoint(x: x + config.offsetX, y: y - config.offsetY) +} + +let windowFrame = NSRect(origin: computeOrigin(), size: windowSize) + +let window = UnconstrainedPanel( + contentRect: windowFrame, + styleMask: [.nonactivatingPanel, .fullSizeContentView, .borderless], + backing: .buffered, + defer: false +) +window.level = .floating +window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] +window.isMovableByWindowBackground = true +window.backgroundColor = .clear +window.hasShadow = true +window.isReleasedWhenClosed = false + +let vfx = NSVisualEffectView(frame: NSRect(origin: .zero, size: windowSize)) +vfx.material = .hudWindow +vfx.blendingMode = .behindWindow +vfx.state = .active +vfx.wantsLayer = true +vfx.layer?.cornerRadius = 10 +vfx.layer?.masksToBounds = true + +vfx.addSubview(label) +NSLayoutConstraint.activate([ + label.centerXAnchor.constraint(equalTo: vfx.centerXAnchor), + label.centerYAnchor.constraint(equalTo: vfx.centerYAnchor), +]) + +window.contentView = vfx +window.orderFrontRegardless() + +let formatter = DateFormatter() +formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX" +formatter.locale = Locale(identifier: "en_US_POSIX") + +func updateClock() { + label.stringValue = formatter.string(from: Date()) +} + +updateClock() + +let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in + updateClock() +} +RunLoop.main.add(timer, forMode: .common) + +app.run() diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..b547349 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1771208521, + "narHash": "sha256-X01Q3DgSpjeBpapoGA4rzKOn25qdKxbPnxHeMLNoHTU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "fa56d7d6de78f5a7f997b0ea2bc6efd5868ad9e8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..1dd0d5a --- /dev/null +++ b/flake.nix @@ -0,0 +1,28 @@ +{ + description = "Dev Shell"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { self + , nixpkgs + , flake-utils + , + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + gnumake + ]; + }; + } + ); +}