diff --git a/.gitignore b/.gitignore index 163a629..8a7925e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .build/ *.app/ +.ghostty-build/ +.DS_Store diff --git a/Sources/MiniTerm/Ghostty.swift b/Sources/MiniTerm/Ghostty.swift index f7573ff..ead12ac 100644 --- a/Sources/MiniTerm/Ghostty.swift +++ b/Sources/MiniTerm/Ghostty.swift @@ -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.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) } } diff --git a/Sources/MiniTerm/main.swift b/Sources/MiniTerm/main.swift index 0d20388..a9e0948 100644 --- a/Sources/MiniTerm/main.swift +++ b/Sources/MiniTerm/main.swift @@ -13,6 +13,8 @@ import SwiftTerm // 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). +// MTCloseOnExit Bool Close the window when the command exits (default +// false = drop back into an interactive login shell). // struct AppConfig { let command: String @@ -21,6 +23,7 @@ struct AppConfig { let fontName: String let windowTitle: String let engine: String // "ghostty" (Default) oder "swiftterm" + let closeOnExit: Bool // true = Fenster schließt, wenn der Befehl endet static func load() -> AppConfig { let info = Bundle.main.infoDictionary ?? [:] @@ -46,12 +49,15 @@ struct AppConfig { ?? (info["CFBundleName"] as? String) ?? "Terminal" + let closeOnExit = (info["MTCloseOnExit"] as? NSNumber)?.boolValue ?? false + return AppConfig(command: cmd, workingDirectory: workdir, fontSize: CGFloat(size), fontName: fontName.isEmpty ? "Menlo" : fontName, windowTitle: title, - engine: engine == "swiftterm" ? "swiftterm" : "ghostty") + engine: engine == "swiftterm" ? "swiftterm" : "ghostty", + closeOnExit: closeOnExit) } } @@ -120,6 +126,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, LocalProcessTerminalVi let args: [String] if config.command.isEmpty { args = ["-l"] + } else if config.closeOnExit { + // Run the command and let the process end with it; the window + // closes via processTerminated(). + args = ["-l", "-c", config.command] } else { // Run the requested command, then hand control to an interactive // login shell so the window stays open after the command exits. diff --git a/ghostty-template/dock-icon.patch b/ghostty-template/dock-icon.patch new file mode 100644 index 0000000..624825b --- /dev/null +++ b/ghostty-template/dock-icon.patch @@ -0,0 +1,20 @@ +diff --git a/macos/Sources/Features/Custom App Icon/DockTilePlugin.swift b/macos/Sources/Features/Custom App Icon/DockTilePlugin.swift +index 990cd8b..6cf7031 100644 +--- a/macos/Sources/Features/Custom App Icon/DockTilePlugin.swift ++++ b/macos/Sources/Features/Custom App Icon/DockTilePlugin.swift +@@ -51,6 +51,15 @@ class DockTilePlugin: NSObject, NSDockTilePlugIn { + return + } + ++ // Custom per-project dock icon: if the enclosing app bundle ships a ++ // Resources/DockIcon.png, use it verbatim and skip the ghost logic. ++ if let appURL = ghosttyAppURL, ++ let img = NSImage(contentsOf: appURL.appendingPathComponent("Contents/Resources/DockIcon.png")) { ++ dockTile.setIcon(img) ++ iconChangeObserver = nil ++ return ++ } ++ + // Try to restore the previous icon on launch. + iconDidChange(ghosttyUserDefaults.appIcon, dockTile: dockTile) + diff --git a/icons/robotunits.png b/icons/robotunits.png new file mode 100644 index 0000000..c2d864b Binary files /dev/null and b/icons/robotunits.png differ diff --git a/make-app.sh b/make-app.sh index 29d5d3f..c4e2a31 100755 --- a/make-app.sh +++ b/make-app.sh @@ -15,7 +15,8 @@ # [--font-size 13] \ # [--font "JetBrains Mono"] \ # [--icon ./icons/projekta.png] \ # .png/.jpg (auto-converted) or .icns -# [--out ~/Applications] +# [--out ~/Applications] \ +# [--close-on-exit] # close the window when the command exits # # Build the binary fresh? The script runs `swift build -c release` automatically # the first time and reuses the result for further apps. @@ -31,6 +32,7 @@ FONT_SIZE="13" FONT_NAME="Menlo" ICON="" OUTDIR="$HOME/Applications" +CLOSE_ON_EXIT="false" # ---- parse args ----------------------------------------------------------- while [[ $# -gt 0 ]]; do @@ -43,6 +45,7 @@ while [[ $# -gt 0 ]]; do --font) FONT_NAME="$2"; shift 2 ;; --icon) ICON="$2"; shift 2 ;; --out) OUTDIR="$2"; shift 2 ;; + --close-on-exit) CLOSE_ON_EXIT="true"; shift ;; -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "Unknown option: $1" >&2; exit 1 ;; @@ -139,6 +142,7 @@ cat > "$APP/Contents/Info.plist" <MTWindowTitle${NAME} MTFontSize${FONT_SIZE} MTFontName${FONT_NAME} + MTCloseOnExit<${CLOSE_ON_EXIT}/> PLIST diff --git a/make-ghostty-app.sh b/make-ghostty-app.sh new file mode 100755 index 0000000..6d27308 --- /dev/null +++ b/make-ghostty-app.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# Erzeugt aus dem gepatchten Ghostty-Template eine eigenständige, pro-Projekt +# konfigurierte macOS-App: eigene Bundle-ID (eigenes Dock-Icon + ⌘-Tab), +# eigenes Icon, eigene Ghostty-Config (working-directory, command, Farben, Font). +# +# Beispiel: +# ./make-ghostty-app.sh misc \ +# --icon ~/claude-projects/misc/misc-icon.png \ +# --workdir ~/claude-projects/misc \ +# --command ~/.local/bin/claude \ +# --bg 2b0a3d --fg ffd000 --font "Courier New" --font-size 18 +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEMPLATE="$SCRIPT_DIR/ghostty-template/Ghostty.app" +PB=/usr/libexec/PlistBuddy + +# --- Defaults --- +NAME="" +BUNDLE_ID="" +ICON="" +WORKDIR="" +COMMAND_LINE="" +THEME="" +BG="" +FG="" +FONT="" +FONT_SIZE="" +OUT_DIR="$HOME/Applications" +CFG_BASE="$HOME/.config-ghostty" + +usage() { grep '^#' "$0" | sed 's/^# \?//'; exit 1; } + +[[ $# -ge 1 ]] || usage +NAME="$1"; shift +while [[ $# -gt 0 ]]; do + case "$1" in + --id) BUNDLE_ID="$2"; shift 2;; + --icon) ICON="$2"; shift 2;; + --workdir) WORKDIR="$2"; shift 2;; + --command) COMMAND_LINE="$2"; shift 2;; + --theme) THEME="$2"; shift 2;; + --bg) BG="$2"; shift 2;; + --fg) FG="$2"; shift 2;; + --font) FONT="$2"; shift 2;; + --font-size) FONT_SIZE="$2"; shift 2;; + --out) OUT_DIR="$2"; shift 2;; + *) echo "Unbekannte Option: $1" >&2; usage;; + esac +done + +[[ -d "$TEMPLATE" ]] || { echo "Template fehlt: $TEMPLATE (erst rebuild-template.sh laufen lassen)" >&2; exit 1; } +: "${BUNDLE_ID:=com.mitchellh.ghostty.$NAME}" +APP="$OUT_DIR/Ghostty-$NAME.app" +CFG="$CFG_BASE/$NAME" + +# Pfade expandieren (~) +WORKDIR="${WORKDIR/#\~/$HOME}" +ICON="${ICON/#\~/$HOME}" +COMMAND_LINE="${COMMAND_LINE/#\~/$HOME}" + +echo ">> Baue $APP (id=$BUNDLE_ID)" + +# --- 1. Template kopieren --- +mkdir -p "$OUT_DIR" +pkill -9 -f "$APP/Contents/MacOS" 2>/dev/null || true +sleep 1 +rm -rf "$APP" +cp -R "$TEMPLATE" "$APP" + +# --- 2. Icon (PNG -> .icns für statisch + PNG fürs Dock-Tile) --- +if [[ -n "$ICON" ]]; then + [[ -f "$ICON" ]] || { echo "Icon nicht gefunden: $ICON" >&2; exit 1; } + TMP_ISET="$(mktemp -d)/icon.iconset"; mkdir -p "$TMP_ISET" + for s in 16 32 128 256 512; do + sips -z $s $s "$ICON" --out "$TMP_ISET/icon_${s}x${s}.png" >/dev/null + sips -z $((s*2)) $((s*2)) "$ICON" --out "$TMP_ISET/icon_${s}x${s}@2x.png" >/dev/null + done + iconutil -c icns "$TMP_ISET" -o "$APP/Contents/Resources/Ghostty.icns" + cp "$ICON" "$APP/Contents/Resources/DockIcon.png" # laufendes Dock-Tile (Plugin-Patch) + rm -rf "$(dirname "$TMP_ISET")" +fi + +# --- 3. Info.plist: Identität + Config-Pfad --- +PL="$APP/Contents/Info.plist" +$PB -c "Set :CFBundleIdentifier $BUNDLE_ID" "$PL" +$PB -c "Set :CFBundleName Ghostty-$NAME" "$PL" 2>/dev/null || $PB -c "Add :CFBundleName string Ghostty-$NAME" "$PL" +$PB -c "Delete :CFBundleIconName" "$PL" 2>/dev/null || true # sonst gewinnt Assets.car (Geist) +$PB -c "Delete :LSEnvironment" "$PL" 2>/dev/null || true +$PB -c "Add :LSEnvironment dict" "$PL" +$PB -c "Add :LSEnvironment:XDG_CONFIG_HOME string $CFG" "$PL" + +# --- 4. Ghostty-Config schreiben --- +mkdir -p "$CFG/ghostty" +{ + [[ -n "$WORKDIR" ]] && echo "working-directory = $WORKDIR" + [[ -n "$COMMAND_LINE" ]] && echo "command = $COMMAND_LINE" + [[ -n "$COMMAND_LINE" ]] && echo "wait-after-command = true" + [[ -n "$THEME" ]] && echo "theme = $THEME" + [[ -n "$BG" ]] && echo "background = $BG" + [[ -n "$FG" ]] && echo "foreground = $FG" + [[ -n "$FONT" ]] && echo "font-family = $FONT" + [[ -n "$FONT_SIZE" ]] && echo "font-size = $FONT_SIZE" +} > "$CFG/ghostty/config" + +# --- 5. ad-hoc signieren --- +codesign --force --deep --sign - "$APP" >/dev/null + +echo ">> Fertig: $APP" +echo " Config: $CFG/ghostty/config" +echo " Starten: open -n \"$APP\"" diff --git a/rebuild-template.sh b/rebuild-template.sh new file mode 100755 index 0000000..90d0b2e --- /dev/null +++ b/rebuild-template.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Baut das gepatchte Ghostty-Template (ghostty-template/Ghostty.app) lokal neu — +# OHNE VM, OHNE Zig. Der kritische Zig-Kern liegt bereits als +# vendor/libghostty/lib/libghostty.a vor; hier laeuft nur noch reines Xcode. +# +# Voraussetzungen: +# - Volles Xcode (CommandLineTools reichen NICHT). Per `xcodes` installierbar. +# - vendor/libghostty/lib/libghostty.a (arm64) + vendor/libghostty/include +# - Installierte /Applications/Ghostty.app (liefert die Shell-Ressourcen) +# +# WICHTIG: libghostty.a ist auf Ghostty v1.3.1 gepinnt. Ein Versionssprung +# erfordert einen NEUEN libghostty.a-Build (Zig, nur in macOS-15-VM moeglich — +# siehe vendor/libghostty/README.md). Dieses Skript baut nur das App-Bundle neu. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GHOSTTY_VERSION="${1:-v1.3.1}" +VENDOR="$SCRIPT_DIR/vendor/libghostty" +BUILD="$SCRIPT_DIR/.ghostty-build" +SRC="$BUILD/ghostty-src" +INSTALLED="/Applications/Ghostty.app" + +# --- Xcode (volles) finden --- +find_xcode() { + local p + p="$(xcode-select -p 2>/dev/null || true)" + [[ "$p" == *Xcode*/Contents/Developer ]] && { echo "$p"; return; } + for c in /Applications/Xcode*.app \ + "$HOME/Library/Application Support/com.robotsandpencils.xcodes/Xcode.app"; do + [[ -d "$c/Contents/Developer" ]] && { echo "$c/Contents/Developer"; return; } + done + return 1 +} +DEVELOPER_DIR="$(find_xcode)" || { echo "Volles Xcode nicht gefunden." >&2; exit 1; } +export DEVELOPER_DIR +echo ">> Xcode: $(xcodebuild -version | head -1)" + +# --- Vorbedingungen --- +[[ -f "$VENDOR/lib/libghostty.a" ]] || { echo "fehlt: $VENDOR/lib/libghostty.a" >&2; exit 1; } +[[ -d "$INSTALLED/Contents/Resources" ]] || { echo "fehlt: $INSTALLED (Shell-Ressourcen)" >&2; exit 1; } + +# --- 1. Source holen --- +mkdir -p "$BUILD" +if [[ ! -d "$SRC/.git" ]]; then + rm -rf "$SRC" + git clone --depth 1 --branch "$GHOSTTY_VERSION" https://github.com/ghostty-org/ghostty.git "$SRC" +else + git -C "$SRC" fetch --depth 1 origin "$GHOSTTY_VERSION" + git -C "$SRC" checkout -f FETCH_HEAD +fi + +# --- 2. Patch anwenden (idempotent) --- +cd "$SRC" +git checkout -- "macos/Sources/Features/Custom App Icon/DockTilePlugin.swift" 2>/dev/null || true +git apply "$SCRIPT_DIR/ghostty-template/dock-icon.patch" +echo ">> Patch angewandt" + +# --- 3. xcframework aus libghostty.a (Header MIT module.modulemap aus src/include) --- +OUT_XCF="$SRC/macos/GhosttyKit.xcframework" +rm -rf "$OUT_XCF" +xcodebuild -create-xcframework \ + -library "$VENDOR/lib/libghostty.a" \ + -headers "$SRC/include" \ + -output "$OUT_XCF" >/dev/null +echo ">> GhosttyKit.xcframework verpackt" + +# --- 4. Shell-Ressourcen einspeisen (sonst CpResource-Fehler) --- +SHARE="$SRC/zig-out/share" +mkdir -p "$SHARE" +for n in zsh vim terminfo man nvim locale fish ghostty bat bash-completion; do + rm -rf "$SHARE/$n" + cp -R "$INSTALLED/Contents/Resources/$n" "$SHARE/$n" +done +echo ">> Shell-Ressourcen eingespeist" + +# --- 5. App bauen (arm64, ReleaseLocal, ohne Signing) --- +cd "$SRC/macos" +xcodebuild -project Ghostty.xcodeproj -target Ghostty -configuration ReleaseLocal \ + SYMROOT="$BUILD/out" ARCHS=arm64 ONLY_ACTIVE_ARCH=YES \ + CLANG_MODULE_CACHE_PATH="$BUILD/modulecache" \ + CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" \ + build >/dev/null +echo ">> App gebaut" + +# --- 6. Template ersetzen --- +DST="$SCRIPT_DIR/ghostty-template/Ghostty.app" +rm -rf "$DST" +cp -R "$BUILD/out/ReleaseLocal/Ghostty.app" "$DST" +echo ">> Template aktualisiert: $DST"