Ghostty-per-Projekt-Builder: Template + make/rebuild-Skripte
- ghostty-template/Ghostty.app: lokal gebautes Ghostty v1.3.1 mit DockTilePlugin-Patch (eigenes Dock-Icon ueber Contents/Resources/DockIcon.png) - make-ghostty-app.sh: erzeugt pro Projekt eine App; Slots fuer icon/workdir/command/theme/bg/fg/font/font-size - rebuild-template.sh: baut Template lokal neu (nur Xcode, kein Zig/VM), gepinnt auf vendor/libghostty v1.3.1 - ghostty-template/dock-icon.patch: der DockTilePlugin-Patch - icons/robotunits.png - enthaelt zudem die vorausgehende MiniTerm/libghostty-Integration
This commit is contained in:
+115
-12
@@ -41,15 +41,46 @@ final class GhosttyApp {
|
||||
// libghostty signalisiert Arbeit -> Tick auf dem Main-Thread.
|
||||
DispatchQueue.main.async { me.tick() }
|
||||
},
|
||||
action_cb: { _, _, _ in
|
||||
// Minimal: keine App-Aktionen (neue Fenster, Tabs, …) behandeln.
|
||||
return false
|
||||
action_cb: { _, _, action in
|
||||
// Minimal: die meisten App-Aktionen (neue Fenster, Tabs, …) nicht
|
||||
// behandeln. Endet der Befehl (closeOnExit) bzw. die Shell, meldet
|
||||
// libghostty das über diese Aktionen – dann die App beenden, sonst
|
||||
// bliebe das Fenster mit toter Surface offen.
|
||||
switch action.tag {
|
||||
case GHOSTTY_ACTION_CLOSE_WINDOW,
|
||||
GHOSTTY_ACTION_QUIT,
|
||||
GHOSTTY_ACTION_SHOW_CHILD_EXITED:
|
||||
DispatchQueue.main.async { NSApp.terminate(nil) }
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
},
|
||||
read_clipboard_cb: { _, _, _ in
|
||||
return false
|
||||
read_clipboard_cb: { userdata, _, state in
|
||||
// libghostty will den Zwischenablage-Inhalt (Paste). Das userdata
|
||||
// ist die SURFACE-userdata (= GhosttySurfaceView, in createSurface
|
||||
// gesetzt). Aus dem System-Pasteboard lesen und die Anfrage
|
||||
// abschließen. confirmed: true = direkt einfügen (kein
|
||||
// Unsafe-Paste-Bestätigungsdialog).
|
||||
guard let userdata = userdata else { return false }
|
||||
let view = Unmanaged<GhosttySurfaceView>.fromOpaque(userdata).takeUnretainedValue()
|
||||
guard let surface = view.surface else { return false }
|
||||
let s = NSPasteboard.general.string(forType: .string) ?? ""
|
||||
s.withCString {
|
||||
ghostty_surface_complete_clipboard_request(surface, $0, state, true)
|
||||
}
|
||||
return true
|
||||
},
|
||||
confirm_read_clipboard_cb: { _, _, _, _ in },
|
||||
write_clipboard_cb: { _, _, _, _, _ in },
|
||||
write_clipboard_cb: { _, _, content, len, _ in
|
||||
// libghostty schreibt in die Zwischenablage (⌘C-Keybind, OSC 52).
|
||||
// Ersten Eintrag mit Textinhalt ins System-Pasteboard übernehmen.
|
||||
guard let content = content, len > 0,
|
||||
let data = content.pointee.data else { return }
|
||||
let s = String(cString: data)
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(s, forType: .string)
|
||||
},
|
||||
close_surface_cb: { _, _ in
|
||||
DispatchQueue.main.async { NSApp.terminate(nil) }
|
||||
}
|
||||
@@ -87,7 +118,9 @@ final class GhosttyApp {
|
||||
final class GhosttySurfaceView: NSView {
|
||||
private let ghostty: GhosttyApp
|
||||
private let appConfig: AppConfig
|
||||
private var surface: ghostty_surface_t!
|
||||
// fileprivate, damit die Clipboard-Callbacks in GhosttyApp (gleiche Datei)
|
||||
// die Surface dieser View erreichen.
|
||||
fileprivate var surface: ghostty_surface_t!
|
||||
|
||||
init(ghostty: GhosttyApp, appConfig: AppConfig, frame: NSRect) {
|
||||
self.ghostty = ghostty
|
||||
@@ -129,12 +162,16 @@ final class GhosttySurfaceView: NSView {
|
||||
/// Login-Shell-Verhalten wie in der SwiftTerm-Variante: leeres MTCommand =>
|
||||
/// nil (libghostty startet selbst eine Login-Shell). Sonst Befehl ausführen
|
||||
/// und danach in eine interaktive Login-Shell übergehen, damit das Fenster
|
||||
/// offen bleibt.
|
||||
/// offen bleibt. Bei closeOnExit endet der Prozess mit dem Befehl; libghostty
|
||||
/// schließt die Surface (close_surface_cb -> NSApp.terminate).
|
||||
private func surfaceCommand() -> String? {
|
||||
let cmd = appConfig.command.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if cmd.isEmpty { return nil }
|
||||
let shell = ProcessInfo.processInfo.environment["SHELL"].flatMap { $0.isEmpty ? nil : $0 }
|
||||
?? "/bin/zsh"
|
||||
if appConfig.closeOnExit {
|
||||
return "\(shell) -lc '\(cmd)'"
|
||||
}
|
||||
return "\(shell) -lc '\(cmd); exec \(shell) -l'"
|
||||
}
|
||||
|
||||
@@ -218,9 +255,22 @@ final class GhosttySurfaceView: NSView {
|
||||
k.action = action
|
||||
k.mods = Self.translateMods(event.modifierFlags)
|
||||
k.keycode = UInt32(event.keyCode)
|
||||
k.unshifted_codepoint = (event.charactersIgnoringModifiers?.unicodeScalars.first?.value) ?? 0
|
||||
|
||||
let text = event.characters ?? ""
|
||||
// Funktionstasten (Pfeile, F-Tasten, Home/End/Page, …) liefern unter
|
||||
// AppKit Codepoints aus der Private Use Area (0xF700–0xF8FF). Diese NICHT
|
||||
// als Text/Codepoint übergeben – sonst sendet libghostty diese Zeichen
|
||||
// wörtlich statt der passenden Escape-Sequenz aus dem keycode.
|
||||
func isPUAFunctionKey(_ s: String?) -> Bool {
|
||||
guard let v = s?.unicodeScalars.first?.value else { return false }
|
||||
return (0xF700...0xF8FF).contains(v)
|
||||
}
|
||||
|
||||
let unshifted = event.charactersIgnoringModifiers
|
||||
k.unshifted_codepoint = isPUAFunctionKey(unshifted)
|
||||
? 0 : (unshifted?.unicodeScalars.first?.value ?? 0)
|
||||
|
||||
let chars = event.characters ?? ""
|
||||
let text = isPUAFunctionKey(chars) ? "" : chars
|
||||
text.withCString { ptr in
|
||||
k.text = ptr
|
||||
_ = ghostty_surface_key(surface, k)
|
||||
@@ -246,11 +296,24 @@ final class GhosttySurfaceView: NSView {
|
||||
|
||||
override func mouseDown(with event: NSEvent) { mouseButton(event, .press, GHOSTTY_MOUSE_LEFT) }
|
||||
override func mouseUp(with event: NSEvent) { mouseButton(event, .release, GHOSTTY_MOUSE_LEFT) }
|
||||
override func rightMouseDown(with event: NSEvent) { mouseButton(event, .press, GHOSTTY_MOUSE_RIGHT) }
|
||||
override func rightMouseUp(with event: NSEvent) { mouseButton(event, .release, GHOSTTY_MOUSE_RIGHT) }
|
||||
override func otherMouseDown(with event: NSEvent) { mouseButton(event, .press, GHOSTTY_MOUSE_MIDDLE) }
|
||||
override func otherMouseUp(with event: NSEvent) { mouseButton(event, .release, GHOSTTY_MOUSE_MIDDLE) }
|
||||
|
||||
// Rechte Maustaste: Shift -> Kontextmenü; Selektion vorhanden -> kopieren;
|
||||
// sonst -> einfügen (PuTTY/urxvt-Verhalten). Der Klick geht nicht an die
|
||||
// Surface, das Maus-Reporting per Rechtsklick entfällt damit bewusst.
|
||||
override func rightMouseDown(with event: NSEvent) {
|
||||
let mods = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
|
||||
if mods.contains(.shift) {
|
||||
showContextMenu(event)
|
||||
} else if let surface = surface, ghostty_surface_has_selection(surface) {
|
||||
copy(nil)
|
||||
} else {
|
||||
paste(nil)
|
||||
}
|
||||
}
|
||||
override func rightMouseUp(with event: NSEvent) {}
|
||||
|
||||
private enum MouseAction { case press, release }
|
||||
private func mouseButton(_ event: NSEvent, _ a: MouseAction, _ button: ghostty_input_mouse_button_e) {
|
||||
guard let surface = surface else { return }
|
||||
@@ -289,6 +352,46 @@ final class GhosttySurfaceView: NSView {
|
||||
addTrackingArea(area)
|
||||
}
|
||||
|
||||
// MARK: Zwischenablage
|
||||
|
||||
// Markierten Text der Surface in die System-Zwischenablage kopieren. Der
|
||||
// Selektor heißt `copy(_:)`, damit der Edit-Menü-Eintrag (⌘C) greift.
|
||||
@objc func copy(_ sender: Any?) {
|
||||
guard let surface = surface, ghostty_surface_has_selection(surface) else { return }
|
||||
var t = ghostty_text_s()
|
||||
guard ghostty_surface_read_selection(surface, &t), let cstr = t.text else { return }
|
||||
let s = String(cString: cstr)
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(s, forType: .string)
|
||||
ghostty_surface_free_text(surface, &t)
|
||||
}
|
||||
|
||||
// Zwischenablage einfügen. Über die Binding-Aktion, damit Bracketed-Paste
|
||||
// erhalten bleibt (mehrzeilige Eingaben). Selektor `paste(_:)` für ⌘V.
|
||||
@objc func paste(_ sender: Any?) {
|
||||
guard let surface = surface else { return }
|
||||
let action = "paste_from_clipboard"
|
||||
_ = action.withCString {
|
||||
ghostty_surface_binding_action(surface, $0, UInt(strlen($0)))
|
||||
}
|
||||
}
|
||||
|
||||
private func showContextMenu(_ event: NSEvent) {
|
||||
let menu = NSMenu()
|
||||
let hasSelection = surface.map { ghostty_surface_has_selection($0) } ?? false
|
||||
|
||||
let copyItem = NSMenuItem(title: "Kopieren", action: #selector(copy(_:)), keyEquivalent: "")
|
||||
copyItem.target = self
|
||||
copyItem.isEnabled = hasSelection
|
||||
menu.addItem(copyItem)
|
||||
|
||||
let pasteItem = NSMenuItem(title: "Einfügen", action: #selector(paste(_:)), keyEquivalent: "")
|
||||
pasteItem.target = self
|
||||
menu.addItem(pasteItem)
|
||||
|
||||
NSMenu.popUpContextMenu(menu, with: event, for: self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let surface = surface { ghostty_surface_free(surface) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user