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
+80
View File
@@ -0,0 +1,80 @@
import AppKit
import Foundation
/// Headless conversion, used for verification and scripting:
///
/// Konverter --convert a.png b.pdf --to jpg
/// Konverter --convert clip.mov --tool trimVideo --param start=0:02 --param end=0:05
enum CLI {
static func run() -> Int32 {
let arguments = Array(CommandLine.arguments.dropFirst())
guard let convertIndex = arguments.firstIndex(of: "--convert") else {
FileHandle.standardError.write(Data("usage: Konverter --convert <files…> --to <ext>\n".utf8))
return 2
}
var files: [URL] = []
var target: Format?
var tool: ToolID?
var params = ToolParams()
var index = convertIndex + 1
while index < arguments.count {
let argument = arguments[index]
switch argument {
case "--to":
index += 1
if index < arguments.count { target = Format(rawValue: arguments[index]) }
case "--tool":
index += 1
if index < arguments.count { tool = ToolID(rawValue: arguments[index]) }
case "--param":
index += 1
if index < arguments.count {
let pair = arguments[index].split(separator: "=", maxSplits: 1).map(String.init)
if pair.count == 2 { params[pair[0]] = pair[1] }
}
default:
if !argument.hasPrefix("--") {
files.append(URL(fileURLWithPath: argument))
}
}
index += 1
}
guard !files.isEmpty else {
FileHandle.standardError.write(Data("no input files\n".utf8))
return 2
}
do {
if let tool {
let outcome = try ToolRunner.run(tool: tool, inputs: files, params: params)
if let message = outcome.message {
print(message)
}
for output in outcome.outputs { print(output.path) }
return outcome.outputs.isEmpty && outcome.message == nil ? 1 : 0
}
guard let target else {
FileHandle.standardError.write(Data("--to is required\n".utf8))
return 2
}
var written: [URL] = []
for file in files {
let destination = OutputNaming.destination(for: file, format: target)
let outputs = try Converter.convert(source: file, to: target, at: destination)
written.append(contentsOf: outputs)
}
for output in written { print(output.path) }
return written.isEmpty ? 1 : 0
} catch {
let message = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
FileHandle.standardError.write(Data((message + "\n").utf8))
return 1
}
}
}