initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.build/
|
||||||
|
.direnv/
|
||||||
|
.swiftpm/
|
||||||
27
Makefile
Normal file
27
Makefile
Normal file
@@ -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
|
||||||
13
Package.swift
Normal file
13
Package.swift
Normal file
@@ -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"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
152
Sources/Nunc/main.swift
Normal file
152
Sources/Nunc/main.swift
Normal file
@@ -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()
|
||||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -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
|
||||||
|
}
|
||||||
28
flake.nix
Normal file
28
flake.nix
Normal file
@@ -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
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user