Add Konverter: a local-only menu bar file converter

Drag files in Finder, hold a modifier, and drop on a wheel of outputs.
Conversions run entirely on-device through ImageIO, PDFKit, AVFoundation,
the bundled CLI tools, and optional ffmpeg/LibreOffice.

Includes a --self-test suite (41 checks) and a --convert CLI mode so the
conversion matrix can be verified without the UI.
This commit is contained in:
2026-10-20 00:36:40 +08:00
commit 2d031e58a4
47 changed files with 9149 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
import Foundation
/// Optional external helpers. Konverter works without them — they only widen
/// the conversion matrix (formats Apple's frameworks cannot write) and are
/// detected once at launch.
enum Toolchain {
private static let homebrewPaths = ["/opt/homebrew/bin", "/usr/local/bin"]
/// ffmpeg/ffprobe: MKV, WebM, AVI, WMV, MP3, FLAC, OGG, Opus, WMA.
static let ffmpeg: String? = Shell.find("ffmpeg", extraPaths: homebrewPaths)
static let ffprobe: String? = Shell.find("ffprobe", extraPaths: homebrewPaths)
/// LibreOffice: PPT/PPTX rendering, XLS/XLSX/ODS, PAGES, Keynote.
static let soffice: String? = {
let candidates = [
"/Applications/LibreOffice.app/Contents/MacOS/soffice",
"/Applications/LibreOffice.app/Contents/MacOS/soffice.bin"
]
for candidate in candidates where FileManager.default.isExecutableFile(atPath: candidate) {
return candidate
}
return Shell.find("soffice", extraPaths: homebrewPaths)
}()
static var hasFFmpeg: Bool { ffmpeg != nil }
static var hasLibreOffice: Bool { soffice != nil }
/// `curl`-free, sandbox-free best-effort image rasteriser for SVG and other
/// formats ImageIO refuses to decode but Quick Look understands.
static let qlmanage: String? = Shell.find("qlmanage", extraPaths: ["/usr/bin"])
static let ditto = "/usr/bin/ditto"
static let tar = "/usr/bin/tar"
static let gzip = "/usr/bin/gzip"
static let gunzip = "/usr/bin/gunzip"
static let textutil = "/usr/bin/textutil"
static let afconvert = "/usr/bin/afconvert"
static let zip = "/usr/bin/zip"
static let unzip = "/usr/bin/unzip"
/// Human readable summary for the About/Settings pane.
static var summary: String {
var parts: [String] = ["Core (ImageIO, PDFKit, AVFoundation)"]
if hasFFmpeg { parts.append("ffmpeg") }
if hasLibreOffice { parts.append("LibreOffice") }
return parts.joined(separator: " · ")
}
static var missingToolAdvice: String? {
var advice: [String] = []
if !hasFFmpeg { advice.append("brew install ffmpeg") }
if !hasLibreOffice { advice.append("brew install --cask libreoffice") }
return advice.isEmpty ? nil : advice.joined(separator: " · ")
}
}