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