commit 51cb14a782542f0a98c5bcbd8e76e4733373cbc9 Author: marcus.hinz Date: Sun Jun 21 19:41:16 2026 +0200 Initial: MiniTerm Mac-App-Builder (umgezogen aus claude-projects) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..163a629 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.build/ +*.app/ diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..5f89513 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382", + "version" : "1.8.2" + } + }, + { + "identity" : "swiftterm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/migueldeicaza/SwiftTerm.git", + "state" : { + "branch" : "main", + "revision" : "24a68bcadc479d945c7ca32f21ac0a8ab895c690" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..7cd16a9 --- /dev/null +++ b/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "MiniTerm", + platforms: [ + .macOS(.v13) + ], + dependencies: [ + // Pin to a tag instead of `branch` if you prefer reproducible builds, + // e.g. .package(url: "...", from: "1.2.0") + .package(url: "https://github.com/migueldeicaza/SwiftTerm.git", branch: "main") + ], + targets: [ + .executableTarget( + name: "MiniTerm", + dependencies: ["SwiftTerm"] + ) + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..962adff --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# MiniTerm — winzige eigenständige Terminal-Apps für macOS + +Ein minimaler, auf **SwiftTerm** basierender Terminal-Emulator. Sinn der Sache: +jede erzeugte `.app` ist eine **eigene macOS-App mit eigener Bundle-ID** — also +**eigenes Dock-Icon und eigener ⌘-Tab-Eintrag**. Genau das, was sich unter macOS +mit `--class` (wie bei Alacritty/Ghostty unter Linux) *nicht* lösen lässt. + +Jede App startet in einem konfigurierbaren **Arbeitsverzeichnis** und führt einen +konfigurierbaren **Befehl** aus (z. B. `claude`). Für Claude Code reicht das +locker — SwiftTerm ist ein vollwertiger xterm-256color-kompatibler Emulator. + +## Voraussetzungen + +- macOS 13+ und **Xcode** bzw. die Command Line Tools (`xcode-select --install`) +- Swift (kommt mit Xcode) + +## Schnellstart + +```bash +cd MiniTerm +chmod +x make-app.sh + +./make-app.sh \ + --name "Claude · Projekt A" \ + --bundle-id com.deinname.claude.projekta \ + --workdir ~/code/projekt-a \ + --command "claude" +``` + +Beim ersten Lauf wird das Binary einmal gebaut (`swift build -c release`, +lädt SwiftTerm). Danach liegt die App in `~/Applications/Claude · Projekt A.app` +— per Doppelklick starten, Dock-Icon erscheint als eigene App. + +## Mehrere Apps (eigene Icons/Configs) + +Einfach pro App einmal aufrufen. Das Binary wird nur einmal kompiliert und +wiederverwendet: + +```bash +./make-app.sh --name "Claude · Webshop" --bundle-id com.deinname.claude.webshop \ + --workdir ~/code/webshop --command "claude" --icon ./icons/shop.icns + +./make-app.sh --name "Claude · Infra" --bundle-id com.deinname.claude.infra \ + --workdir ~/code/infra --command "claude" --icon ./icons/infra.icns + +./make-app.sh --name "Scratch Shell" --bundle-id com.deinname.scratch \ + --workdir ~ --command "" # leer = einfache Shell +``` + +## Optionen + +| Flag | Pflicht | Bedeutung | +|----------------|---------|--------------------------------------------------------| +| `--name` | ja | Anzeigename + Dateiname der App | +| `--bundle-id` | ja | Eindeutige Bundle-ID (gibt die eigene App-Identität) | +| `--workdir` | nein | Startverzeichnis (`~` wird expandiert; leer = Home) | +| `--command` | nein | Befehl beim Start (leer = nur eine Login-Shell) | +| `--font-size` | nein | Schriftgröße (Default 13) | +| `--font` | nein | Schriftfamilie (Default Menlo; Fallback System-Mono) | +| `--icon` | nein | PNG/JPG (wird automatisch konvertiert) oder `.icns` | +| `--out` | nein | Zielordner (Default `~/Applications`) | + +## Icon aus einem PNG + +`--icon` nimmt direkt ein **PNG** (oder JPG) — das Skript erzeugt daraus +automatisch alle nötigen Größen und das `.icns` (via `sips`/`iconutil`): + +```bash +./make-app.sh --name "Claude · Infra" --bundle-id com.deinname.claude.infra \ + --workdir ~/code/infra --command "claude" --icon ~/Downloads/infra.png +``` + +Am besten ein **quadratisches** Bild nehmen, sonst verzerrt `sips` es. Eine +fertige `.icns`-Datei kannst du weiterhin auch direkt angeben. + +## Wie die Konfiguration funktioniert + +Das kompilierte Binary ist für alle Apps identisch. Die Unterschiede stecken in +der `Info.plist` jedes Bundles (Schlüssel `MTCommand`, `MTWorkingDirectory`, +`MTWindowTitle`, `MTFontSize`, `MTFontName`). Das Programm liest sie beim Start aus +`Bundle.main`. So genügt ein Build für beliebig viele Apps. + +## Hinweise + +- Der Befehl läuft immer über eine **Login-Shell** (`zsh -l`), damit PATH stimmt + (Homebrew, `~/.local/bin` usw.) und `claude` gefunden wird. +- Nach Beenden des Befehls fällt das Fenster in eine normale Shell zurück + (`exec zsh -l`), schließt sich also nicht sofort. +- Die App ist **ad-hoc signiert**. Beim ersten Start ggf. Rechtsklick → „Öffnen", + falls Gatekeeper meckert. +- SwiftTerm-Version: im `Package.swift` auf `branch: "main"`. Für reproduzierbare + Builds dort auf einen festen Tag (`from: "..."`) umstellen. + +## Tastatur + +- **Enter** sendet wie gewohnt einen Wagenrücklauf (`\r`) → Eingabe absenden. +- **Shift+Enter** sendet einen Zeilenumbruch (`\n`) → für mehrzeilige Eingaben, + z. B. in Claude Code. Gilt auch für Enter auf dem Ziffernblock. +- ⌘C / ⌘V funktionieren über das Edit-Menü. diff --git a/Sources/MiniTerm/main.swift b/Sources/MiniTerm/main.swift new file mode 100644 index 0000000..b13759e --- /dev/null +++ b/Sources/MiniTerm/main.swift @@ -0,0 +1,213 @@ +import AppKit +import SwiftTerm + +// MARK: - Configuration +// +// Every value is read from the app bundle's Info.plist, so a single compiled +// binary can power many different .app bundles (each with its own command, +// working directory, title and icon). See make-app.sh. +// +// MTCommand String Command to run, e.g. "claude". Empty = plain shell. +// MTWorkingDirectory String Directory to start in. "~" is expanded. Empty = home. +// MTFontSize Number Font point size (default 13). +// MTFontName String Font family name (default "Menlo"). Falls back to +// the system monospaced font if the name is missing. +// MTWindowTitle String Window title (default = CFBundleName). +// +struct AppConfig { + let command: String + let workingDirectory: String? + let fontSize: CGFloat + let fontName: String + let windowTitle: String + + static func load() -> AppConfig { + let info = Bundle.main.infoDictionary ?? [:] + + let cmd = ((info["MTCommand"] as? String) ?? "") + .trimmingCharacters(in: .whitespacesAndNewlines) + + var workdir: String? = nil + if let raw = info["MTWorkingDirectory"] as? String, + !raw.trimmingCharacters(in: .whitespaces).isEmpty { + workdir = (raw as NSString).expandingTildeInPath + } + + let size = (info["MTFontSize"] as? NSNumber)?.doubleValue ?? 13.0 + + let fontName = ((info["MTFontName"] as? String) ?? "") + .trimmingCharacters(in: .whitespacesAndNewlines) + + let title = (info["MTWindowTitle"] as? String) + ?? (info["CFBundleName"] as? String) + ?? "Terminal" + + return AppConfig(command: cmd, + workingDirectory: workdir, + fontSize: CGFloat(size), + fontName: fontName.isEmpty ? "Menlo" : fontName, + windowTitle: title) + } +} + +// MARK: - App delegate +final class AppDelegate: NSObject, NSApplicationDelegate, LocalProcessTerminalViewDelegate { + private var window: NSWindow! + private var terminalView: LocalProcessTerminalView! + private var keyMonitor: Any? + private let config = AppConfig.load() + + func applicationDidFinishLaunching(_ notification: Notification) { + let frame = NSRect(x: 0, y: 0, width: 900, height: 560) + + terminalView = LocalProcessTerminalView(frame: frame) + terminalView.processDelegate = self + terminalView.font = resolvedFont() + + installKeyMonitor() + + window = NSWindow( + contentRect: frame, + styleMask: [.titled, .closable, .miniaturizable, .resizable], + backing: .buffered, + defer: false) + window.title = config.windowTitle + window.contentView = terminalView + window.setFrameAutosaveName("MiniTermMain") + window.center() + window.makeKeyAndOrderFront(nil) + window.makeFirstResponder(terminalView) + + setupMenu() + launchProcess() + } + + private func launchProcess() { + let shell = userShell() + + // We always go through a LOGIN shell (-l) so PATH is set up exactly like + // in a normal terminal (Homebrew, ~/.local/bin, etc.). Without this, + // tools like `claude` are often "command not found". + let args: [String] + if config.command.isEmpty { + args = ["-l"] + } else { + // Run the requested command, then hand control to an interactive + // login shell so the window stays open after the command exits. + args = ["-l", "-c", "\(config.command); exec \(shell) -l"] + } + + let env = Terminal.getEnvironmentVariables(termName: "xterm-256color", trueColor: true) + + terminalView.startProcess( + executable: shell, + args: args, + environment: env, + execName: nil, + currentDirectory: config.workingDirectory) + } + + // Intercept Shift+Return (and Shift+Enter on the numpad) before it reaches + // the terminal and send a line feed (\n) instead of the normal carriage + // return (\r). This lets CLIs like Claude Code insert a newline in + // multi-line input while plain Enter still submits. We use a local event + // monitor because SwiftTerm's keyDown(with:) is `public`, not `open`, so it + // can't be overridden from outside the module. + private func installKeyMonitor() { + keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in + guard let self = self else { return event } + // Only act when our window is focused and the keyboard focus is in + // the terminal (the view itself or one of its subviews). + guard self.window.isKeyWindow else { return event } + if let responder = self.window.firstResponder as? NSView, + !(responder === self.terminalView || responder.isDescendant(of: self.terminalView)) { + return event + } + + let isReturn = (event.keyCode == 36 || event.keyCode == 76) + let mods = event.modifierFlags.intersection(.deviceIndependentFlagsMask) + if isReturn && mods == .shift { + self.terminalView.send(txt: "\n") + return nil // consume the event + } + return event + } + } + + // Resolve the configured font, falling back gracefully so the app never + // launches with a broken/empty font if the name doesn't exist on the system. + private func resolvedFont() -> NSFont { + if let f = NSFont(name: config.fontName, size: config.fontSize) { + return f + } + if let menlo = NSFont(name: "Menlo", size: config.fontSize) { + return menlo + } + return NSFont.monospacedSystemFont(ofSize: config.fontSize, weight: .regular) + } + + private func userShell() -> String { + if let shell = ProcessInfo.processInfo.environment["SHELL"], !shell.isEmpty { + return shell + } + return "/bin/zsh" + } + + private func setupMenu() { + let mainMenu = NSMenu() + + // Application menu + let appItem = NSMenuItem() + mainMenu.addItem(appItem) + let appMenu = NSMenu() + let name = config.windowTitle + appMenu.addItem(withTitle: "Hide \(name)", + action: #selector(NSApplication.hide(_:)), + keyEquivalent: "h") + appMenu.addItem(NSMenuItem.separator()) + appMenu.addItem(withTitle: "Quit \(name)", + action: #selector(NSApplication.terminate(_:)), + keyEquivalent: "q") + appItem.submenu = appMenu + + // Edit menu (enables ⌘C / ⌘V in the terminal) + let editItem = NSMenuItem() + mainMenu.addItem(editItem) + let editMenu = NSMenu(title: "Edit") + editMenu.addItem(withTitle: "Copy", + action: #selector(NSText.copy(_:)), + keyEquivalent: "c") + editMenu.addItem(withTitle: "Paste", + action: #selector(NSText.paste(_:)), + keyEquivalent: "v") + editItem.submenu = editMenu + + NSApp.mainMenu = mainMenu + } + + // MARK: LocalProcessTerminalViewDelegate + func sizeChanged(source: LocalProcessTerminalView, newCols: Int, newRows: Int) {} + + func setTerminalTitle(source: LocalProcessTerminalView, title: String) { + // Keep a stable window title. Uncomment to follow the shell's title: + // window.title = title.isEmpty ? config.windowTitle : title + } + + func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {} + + func processTerminated(source: TerminalView, exitCode: Int32?) { + NSApp.terminate(nil) + } + + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} + +// MARK: - Entry point +let app = NSApplication.shared +app.setActivationPolicy(.regular) // a real Dock app, not a background process +let delegate = AppDelegate() +app.delegate = delegate +app.activate(ignoringOtherApps: true) +app.run() diff --git a/icons/download.jpeg b/icons/download.jpeg new file mode 100755 index 0000000..c8bb397 Binary files /dev/null and b/icons/download.jpeg differ diff --git a/icons/hicolor/128x128/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png b/icons/hicolor/128x128/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png new file mode 100755 index 0000000..1ccce91 Binary files /dev/null and b/icons/hicolor/128x128/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png differ diff --git a/icons/hicolor/128x128/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png b/icons/hicolor/128x128/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png new file mode 100755 index 0000000..6ee0178 Binary files /dev/null and b/icons/hicolor/128x128/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png differ diff --git a/icons/hicolor/128x128/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png b/icons/hicolor/128x128/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png new file mode 100755 index 0000000..214c81e Binary files /dev/null and b/icons/hicolor/128x128/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png differ diff --git a/icons/hicolor/128x128/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png b/icons/hicolor/128x128/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png new file mode 100755 index 0000000..c660e75 Binary files /dev/null and b/icons/hicolor/128x128/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png differ diff --git a/icons/hicolor/128x128/apps/chrome-pfjjjhloipcienfopbicmcjmlkilooii-Default.png b/icons/hicolor/128x128/apps/chrome-pfjjjhloipcienfopbicmcjmlkilooii-Default.png new file mode 100755 index 0000000..8ffcfc0 Binary files /dev/null and b/icons/hicolor/128x128/apps/chrome-pfjjjhloipcienfopbicmcjmlkilooii-Default.png differ diff --git a/icons/hicolor/128x128/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png b/icons/hicolor/128x128/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png new file mode 100755 index 0000000..4f8c0ea Binary files /dev/null and b/icons/hicolor/128x128/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png differ diff --git a/icons/hicolor/16x16/apps/C06E_winhlp32.0.png b/icons/hicolor/16x16/apps/C06E_winhlp32.0.png new file mode 100755 index 0000000..f8ab918 Binary files /dev/null and b/icons/hicolor/16x16/apps/C06E_winhlp32.0.png differ diff --git a/icons/hicolor/16x16/apps/D15F_hh.0.png b/icons/hicolor/16x16/apps/D15F_hh.0.png new file mode 100755 index 0000000..f8ab918 Binary files /dev/null and b/icons/hicolor/16x16/apps/D15F_hh.0.png differ diff --git a/icons/hicolor/16x16/apps/D23E_msiexec.0.png b/icons/hicolor/16x16/apps/D23E_msiexec.0.png new file mode 100755 index 0000000..49c5579 Binary files /dev/null and b/icons/hicolor/16x16/apps/D23E_msiexec.0.png differ diff --git a/icons/hicolor/16x16/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png b/icons/hicolor/16x16/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png new file mode 100755 index 0000000..2ca1d2b Binary files /dev/null and b/icons/hicolor/16x16/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png differ diff --git a/icons/hicolor/256x256/apps/C06E_winhlp32.0.png b/icons/hicolor/256x256/apps/C06E_winhlp32.0.png new file mode 100755 index 0000000..6e72db2 Binary files /dev/null and b/icons/hicolor/256x256/apps/C06E_winhlp32.0.png differ diff --git a/icons/hicolor/256x256/apps/D15F_hh.0.png b/icons/hicolor/256x256/apps/D15F_hh.0.png new file mode 100755 index 0000000..c5206d6 Binary files /dev/null and b/icons/hicolor/256x256/apps/D15F_hh.0.png differ diff --git a/icons/hicolor/256x256/apps/D23E_msiexec.0.png b/icons/hicolor/256x256/apps/D23E_msiexec.0.png new file mode 100755 index 0000000..7af8169 Binary files /dev/null and b/icons/hicolor/256x256/apps/D23E_msiexec.0.png differ diff --git a/icons/hicolor/256x256/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png b/icons/hicolor/256x256/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png new file mode 100755 index 0000000..8d7fd7f Binary files /dev/null and b/icons/hicolor/256x256/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png differ diff --git a/icons/hicolor/256x256/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png b/icons/hicolor/256x256/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png new file mode 100755 index 0000000..dcdd240 Binary files /dev/null and b/icons/hicolor/256x256/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png differ diff --git a/icons/hicolor/256x256/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png b/icons/hicolor/256x256/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png new file mode 100755 index 0000000..c017683 Binary files /dev/null and b/icons/hicolor/256x256/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png differ diff --git a/icons/hicolor/256x256/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png b/icons/hicolor/256x256/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png new file mode 100755 index 0000000..5a82bc0 Binary files /dev/null and b/icons/hicolor/256x256/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png differ diff --git a/icons/hicolor/256x256/apps/chrome-pfjjjhloipcienfopbicmcjmlkilooii-Default.png b/icons/hicolor/256x256/apps/chrome-pfjjjhloipcienfopbicmcjmlkilooii-Default.png new file mode 100755 index 0000000..307244e Binary files /dev/null and b/icons/hicolor/256x256/apps/chrome-pfjjjhloipcienfopbicmcjmlkilooii-Default.png differ diff --git a/icons/hicolor/256x256/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png b/icons/hicolor/256x256/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png new file mode 100755 index 0000000..660836b Binary files /dev/null and b/icons/hicolor/256x256/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png differ diff --git a/icons/hicolor/32x32/apps/C06E_winhlp32.0.png b/icons/hicolor/32x32/apps/C06E_winhlp32.0.png new file mode 100755 index 0000000..5b64f1e Binary files /dev/null and b/icons/hicolor/32x32/apps/C06E_winhlp32.0.png differ diff --git a/icons/hicolor/32x32/apps/D15F_hh.0.png b/icons/hicolor/32x32/apps/D15F_hh.0.png new file mode 100755 index 0000000..d5ee64a Binary files /dev/null and b/icons/hicolor/32x32/apps/D15F_hh.0.png differ diff --git a/icons/hicolor/32x32/apps/D23E_msiexec.0.png b/icons/hicolor/32x32/apps/D23E_msiexec.0.png new file mode 100755 index 0000000..4348519 Binary files /dev/null and b/icons/hicolor/32x32/apps/D23E_msiexec.0.png differ diff --git a/icons/hicolor/32x32/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png b/icons/hicolor/32x32/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png new file mode 100755 index 0000000..a1773b3 Binary files /dev/null and b/icons/hicolor/32x32/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png differ diff --git a/icons/hicolor/32x32/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png b/icons/hicolor/32x32/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png new file mode 100755 index 0000000..f067b9c Binary files /dev/null and b/icons/hicolor/32x32/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png differ diff --git a/icons/hicolor/32x32/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png b/icons/hicolor/32x32/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png new file mode 100755 index 0000000..9f95337 Binary files /dev/null and b/icons/hicolor/32x32/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png differ diff --git a/icons/hicolor/32x32/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png b/icons/hicolor/32x32/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png new file mode 100755 index 0000000..3031a4e Binary files /dev/null and b/icons/hicolor/32x32/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png differ diff --git a/icons/hicolor/32x32/apps/chrome-pfjjjhloipcienfopbicmcjmlkilooii-Default.png b/icons/hicolor/32x32/apps/chrome-pfjjjhloipcienfopbicmcjmlkilooii-Default.png new file mode 100755 index 0000000..aa13741 Binary files /dev/null and b/icons/hicolor/32x32/apps/chrome-pfjjjhloipcienfopbicmcjmlkilooii-Default.png differ diff --git a/icons/hicolor/32x32/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png b/icons/hicolor/32x32/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png new file mode 100755 index 0000000..a1773b3 Binary files /dev/null and b/icons/hicolor/32x32/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png differ diff --git a/icons/hicolor/48x48/apps/C06E_winhlp32.0.png b/icons/hicolor/48x48/apps/C06E_winhlp32.0.png new file mode 100755 index 0000000..489d60a Binary files /dev/null and b/icons/hicolor/48x48/apps/C06E_winhlp32.0.png differ diff --git a/icons/hicolor/48x48/apps/D15F_hh.0.png b/icons/hicolor/48x48/apps/D15F_hh.0.png new file mode 100755 index 0000000..3e6ef35 Binary files /dev/null and b/icons/hicolor/48x48/apps/D15F_hh.0.png differ diff --git a/icons/hicolor/48x48/apps/D23E_msiexec.0.png b/icons/hicolor/48x48/apps/D23E_msiexec.0.png new file mode 100755 index 0000000..536f361 Binary files /dev/null and b/icons/hicolor/48x48/apps/D23E_msiexec.0.png differ diff --git a/icons/hicolor/48x48/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png b/icons/hicolor/48x48/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png new file mode 100755 index 0000000..9c8b281 Binary files /dev/null and b/icons/hicolor/48x48/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png differ diff --git a/icons/hicolor/48x48/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png b/icons/hicolor/48x48/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png new file mode 100755 index 0000000..1d94c31 Binary files /dev/null and b/icons/hicolor/48x48/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png differ diff --git a/icons/hicolor/48x48/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png b/icons/hicolor/48x48/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png new file mode 100755 index 0000000..ea57e05 Binary files /dev/null and b/icons/hicolor/48x48/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png differ diff --git a/icons/hicolor/48x48/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png b/icons/hicolor/48x48/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png new file mode 100755 index 0000000..190d84d Binary files /dev/null and b/icons/hicolor/48x48/apps/chrome-oakokndjledcjeafhnccifkkikobnidc-Default.png differ diff --git a/icons/hicolor/48x48/apps/chrome-pfjjjhloipcienfopbicmcjmlkilooii-Default.png b/icons/hicolor/48x48/apps/chrome-pfjjjhloipcienfopbicmcjmlkilooii-Default.png new file mode 100755 index 0000000..ae4b7d4 Binary files /dev/null and b/icons/hicolor/48x48/apps/chrome-pfjjjhloipcienfopbicmcjmlkilooii-Default.png differ diff --git a/icons/hicolor/48x48/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png b/icons/hicolor/48x48/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png new file mode 100755 index 0000000..9c8b281 Binary files /dev/null and b/icons/hicolor/48x48/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png differ diff --git a/icons/hicolor/512x512/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png b/icons/hicolor/512x512/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png new file mode 100755 index 0000000..5db10bd Binary files /dev/null and b/icons/hicolor/512x512/apps/chrome-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png differ diff --git a/icons/hicolor/512x512/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png b/icons/hicolor/512x512/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png new file mode 100755 index 0000000..d5ffeeb Binary files /dev/null and b/icons/hicolor/512x512/apps/chrome-eoficlgicibekocmfdomjbfnjmehnhcd-Default.png differ diff --git a/icons/hicolor/512x512/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png b/icons/hicolor/512x512/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png new file mode 100755 index 0000000..a059e2f Binary files /dev/null and b/icons/hicolor/512x512/apps/chrome-lneibfepblpbmpahmjfemmjggkkmdjnj-Default.png differ diff --git a/icons/hicolor/512x512/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png b/icons/hicolor/512x512/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png new file mode 100755 index 0000000..6cdb7dc Binary files /dev/null and b/icons/hicolor/512x512/apps/msedge-cifhbcnohmdccbgoicgdjpfamggdegmo-Default.png differ diff --git a/icons/limbach.png b/icons/limbach.png new file mode 100755 index 0000000..bba815d Binary files /dev/null and b/icons/limbach.png differ diff --git a/icons/robot.png b/icons/robot.png new file mode 100755 index 0000000..c2d864b Binary files /dev/null and b/icons/robot.png differ diff --git a/icons/wittgenstein.png b/icons/wittgenstein.png new file mode 100755 index 0000000..fc810bf Binary files /dev/null and b/icons/wittgenstein.png differ diff --git a/icons/wuerth-ft.png b/icons/wuerth-ft.png new file mode 100755 index 0000000..60d48e6 Binary files /dev/null and b/icons/wuerth-ft.png differ diff --git a/icons/wuerth.png b/icons/wuerth.png new file mode 100755 index 0000000..95ae414 Binary files /dev/null and b/icons/wuerth.png differ diff --git a/make-app.sh b/make-app.sh new file mode 100755 index 0000000..29d5d3f --- /dev/null +++ b/make-app.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# +# make-app.sh — build a standalone macOS .app from the MiniTerm binary. +# +# Each generated .app has its OWN bundle id, so macOS treats it as a separate +# application: separate Dock icon, separate ⌘-Tab entry. Run this script once +# per app you want. +# +# Usage: +# ./make-app.sh \ +# --name "Claude · Projekt A" \ +# --bundle-id com.yourname.claude.projekta \ +# --workdir ~/code/project-a \ +# --command "claude" \ +# [--font-size 13] \ +# [--font "JetBrains Mono"] \ +# [--icon ./icons/projekta.png] \ # .png/.jpg (auto-converted) or .icns +# [--out ~/Applications] +# +# Build the binary fresh? The script runs `swift build -c release` automatically +# the first time and reuses the result for further apps. + +set -euo pipefail + +# ---- defaults ------------------------------------------------------------- +NAME="" +BUNDLE_ID="" +WORKDIR="" +COMMAND="" +FONT_SIZE="13" +FONT_NAME="Menlo" +ICON="" +OUTDIR="$HOME/Applications" + +# ---- parse args ----------------------------------------------------------- +while [[ $# -gt 0 ]]; do + case "$1" in + --name) NAME="$2"; shift 2 ;; + --bundle-id) BUNDLE_ID="$2"; shift 2 ;; + --workdir) WORKDIR="$2"; shift 2 ;; + --command) COMMAND="$2"; shift 2 ;; + --font-size) FONT_SIZE="$2"; shift 2 ;; + --font) FONT_NAME="$2"; shift 2 ;; + --icon) ICON="$2"; shift 2 ;; + --out) OUTDIR="$2"; shift 2 ;; + -h|--help) + grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +if [[ -z "$NAME" || -z "$BUNDLE_ID" ]]; then + echo "Error: --name and --bundle-id are required." >&2 + echo "Run './make-app.sh --help' for usage." >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# ---- build binary once ---------------------------------------------------- +BIN=".build/release/MiniTerm" +if [[ ! -f "$BIN" ]]; then + echo "==> Building MiniTerm (release)…" + swift build -c release +fi + +# Keep workdir portable across machines/users: store paths under the current +# home directory as a literal "~", so each app expands it per-user at runtime +# (Swift's expandingTildeInPath). Note the shell may have already expanded a "~" +# you typed on the command line into an absolute $HOME path, so we normalize that +# back to "~" too. Paths outside $HOME stay absolute (genuinely machine-specific). +case "$WORKDIR" in + "$HOME") WORKDIR="~" ;; + "$HOME"/*) WORKDIR="~/${WORKDIR#"$HOME"/}" ;; +esac + +# ---- assemble the .app bundle --------------------------------------------- +APP="$OUTDIR/$NAME.app" +echo "==> Creating $APP" +rm -rf "$APP" +mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" + +cp "$BIN" "$APP/Contents/MacOS/MiniTerm" +chmod +x "$APP/Contents/MacOS/MiniTerm" + +# Build an .icns from a square source image (PNG/JPG/…) using macOS tools. +make_icns_from_image() { + local src="$1" dest="$2" + local iconset + iconset="$(mktemp -d)/icon.iconset" + mkdir -p "$iconset" + # All sizes Apple expects in an .iconset + local sizes=(16 32 128 256 512) + for s in "${sizes[@]}"; do + sips -z "$s" "$s" "$src" --out "$iconset/icon_${s}x${s}.png" >/dev/null + sips -z "$((s*2))" "$((s*2))" "$src" --out "$iconset/icon_${s}x${s}@2x.png" >/dev/null + done + iconutil -c icns "$iconset" -o "$dest" + rm -rf "$(dirname "$iconset")" +} + +ICON_KEY="" +if [[ -n "$ICON" ]]; then + if [[ ! -f "$ICON" ]]; then + echo " (warning: --icon file not found: $ICON; skipping icon)" >&2 + elif [[ "$ICON" == *.icns ]]; then + cp "$ICON" "$APP/Contents/Resources/AppIcon.icns" + ICON_KEY="CFBundleIconFileAppIcon" + elif command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1; then + echo "==> Converting $ICON to .icns…" + if make_icns_from_image "$ICON" "$APP/Contents/Resources/AppIcon.icns"; then + ICON_KEY="CFBundleIconFileAppIcon" + else + echo " (warning: icon conversion failed; building without icon)" >&2 + fi + else + echo " (warning: sips/iconutil not available; pass a .icns file instead)" >&2 + fi +fi + +cat > "$APP/Contents/Info.plist" < + + + + CFBundleName${NAME} + CFBundleDisplayName${NAME} + CFBundleExecutableMiniTerm + CFBundleIdentifier${BUNDLE_ID} + CFBundlePackageTypeAPPL + CFBundleVersion1 + CFBundleShortVersionString1.0 + LSMinimumSystemVersion13.0 + NSHighResolutionCapable + ${ICON_KEY} + MTCommand${COMMAND} + MTWorkingDirectory${WORKDIR} + MTWindowTitle${NAME} + MTFontSize${FONT_SIZE} + MTFontName${FONT_NAME} + + +PLIST + +# ---- ad-hoc codesign (helps on Apple Silicon / Gatekeeper) ---------------- +if command -v codesign >/dev/null 2>&1; then + codesign --force --deep --sign - "$APP" >/dev/null 2>&1 \ + && echo "==> Ad-hoc signed." \ + || echo " (codesign skipped/failed — app still runs)" +fi + +echo "==> Done: $APP" +echo " Command : ${COMMAND:-}" +echo " Workdir : ${WORKDIR:-}" +echo " Font : ${FONT_NAME} ${FONT_SIZE}pt" diff --git a/wittgenstein.png b/wittgenstein.png new file mode 100755 index 0000000..fc810bf Binary files /dev/null and b/wittgenstein.png differ