// SPDX-FileCopyrightText: 2026 Kaede Akino // SPDX-License-Identifier: GPL-3.0-only import Foundation import TermindApplication import TermindDomain nonisolated struct RemoteTmuxRuntimeDialect: RemoteMultiplexerRuntime { private let execute: RemoteSessionCommandExecutor private let charset: RemoteCharset init(execute: @escaping RemoteSessionCommandExecutor, charset: RemoteCharset = .utf8) { self.execute = execute self.charset = charset } func inspect(terminal: String) async throws -> TerminalRuntimeUpdate { let result = try await execute(Self.command(terminal: terminal)) guard result.exitCode == 0, !result.truncated else { throw TerminalRuntimeError.unsupported } return try Self.parse(result.stdout, terminal: terminal, charset: charset) } func draftTarget( context: TerminalShellContext, terminal: String, replacing: Bool ) async throws -> RemoteMultiplexerDraftTarget { let update = try await inspect(terminal: terminal) guard update.active == context.identity, update.contexts.first(where: { $0.identity == context.identity }) == context else { throw TerminalRuntimeError.staleContext } return try Self.draftTarget( context: context, terminal: terminal, clientIdentity: update.clientIdentity, replacing: replacing) } func refreshSize(of client: RemoteTmuxClientIdentity) async throws { let result = try await execute(Self.resizeCommand(client: client)) guard result.exitCode == 0, !result.truncated, result.stdout == "TERMIND_RESIZED\n" else { throw TerminalRuntimeError.staleContext } } static func resizeCommand(client: RemoteTmuxClientIdentity) throws -> String { let identity = [ client.terminal, client.process, client.created, client.server, client.session, client.sessionCreated, ].joined(separator: "|") // Revalidate on the server immediately before signalling. A detached client's TTY // may already contain a shell, so this operation must never write terminal input. return """ \(RemoteTmuxCommands.executionPreamble) client=$(tmux list-clients -F '#{client_tty}|#{client_pid}|#{client_created}|#{pid}|#{session_id}|#{session_created}|#{client_flags}' 2>/dev/null | \ while IFS='|' read -r tty cp cc sp sid sc flags; do if [ "$tty" = \(try RemoteTmuxCommands.quote(client.terminal)) ]; then case ",$flags," in (*,read-only,*|*,ignore-size,*|*,active-pane,*|*,control-mode,*) continue ;; esac printf '%s|%s|%s|%s|%s|%s\\n' "$tty" "$cp" "$cc" "$sp" "$sid" "$sc" fi done) [ "$client" = \(try RemoteTmuxCommands.quote(identity)) ] || exit 79 kill -WINCH \(client.process) || exit 79 printf 'TERMIND_RESIZED\\n' """ } static func command(terminal: String) throws -> String { guard RemoteTmuxClientIdentity.validTerminal(terminal) else { throw TerminalRuntimeError.invalidResponse } return """ \(RemoteTmuxCommands.executionPreamble) tmux -V || exit 77 client=$(tmux list-clients -F 'C|#{client_tty}|#{client_pid}|#{client_created}|#{pid}|#{session_id}|#{session_created}|#{pane_id}|#{client_flags}|#{client_key_table}' 2>/dev/null | \ while IFS='|' read -r tag tty cp cc sp sid sc pane flags table; do if [ "$tty" = \(try RemoteTmuxCommands.quote(terminal)) ]; then printf 'C|%s|%s|%s|%s|%s|%s|%s|%s|%s\\n' "$tty" "$cp" "$cc" "$sp" "$sid" "$sc" "$pane" "$flags" "$table" fi done) if [ -z "$client" ]; then printf 'NONE\\n'; exit 0; fi printf '%s\\n' "$client" sid=$(printf '%s' "$client" | cut -d '|' -f6) panes=$(tmux list-panes -s -t "$sid" -F 'P|#{pane_id}|#{pane_pid}|#{pane_in_mode}|#{synchronize-panes}|#{?#{==:#{pane_current_command},bash},bash,#{?#{==:#{pane_current_command},zsh},zsh,#{?#{==:#{pane_current_command},fish},fish,unknown}}}') || exit 78 printf '%s\\n' "$panes" pane=$(printf '%s' "$client" | cut -d '|' -f8) cwd=$(tmux display-message -p -t "$pane" '#{pane_current_path}') || exit 78 foreground=$(tmux display-message -p -t "$pane" '#{pane_current_command}') || exit 78 printf 'D|%s|%s|%s\\n' "$pane" \ "$(printf '%s' "$cwd" | od -An -v -tx1 | tr -d ' \\n')" \ "$(printf '%s' "$foreground" | od -An -v -tx1 | tr -d ' \\n')" helper=$(tmux show-environment -t "$sid" TERMIND_TMUX_HELPERS 2>/dev/null) helper=${helper#TERMIND_TMUX_HELPERS=} case "$helper" in /tmp/termind-shell.*) ;; *) printf 'DONE\\n'; exit 0 ;; esac suffix=${helper#/tmp/termind-shell.} case "$suffix" in ''|*[!a-zA-Z0-9]*) printf 'DONE\\n'; exit 0 ;; esac count=0 for file in "$helper"/ctx.*/journal; do [ -f "$file" ] && [ ! -L "$file" ] || continue IFS='|' read -r tag token pane instance shell home pid history backend < "$file" || continue case "$tag:$backend" in M1:|M2:tmux) ;; *) continue ;; esac [ "$token" = "$(cat "$helper/event-token")" ] || continue dir=${file%/journal} [ ! -L "$dir" ] && [ "${dir##*/}" = "$instance" ] || continue [ "$(printf '%s\\n' "$panes" | awk -F '|' -v pane="$pane" '$2 == pane {print $3}')" = "$pid" ] || continue count=$((count + 1)); [ "$count" -le 128 ] || exit 78 printf 'BEGIN\\n' cat "$file" || exit 78 printf 'END\\n' done printf 'DONE\\n' """ } static func parse(_ text: String, terminal: String, charset: RemoteCharset = .utf8) throws -> TerminalRuntimeUpdate { var lines = text.components(separatedBy: "\n") guard !lines.isEmpty else { throw TerminalRuntimeError.invalidResponse } try RemoteTmuxCommands.validateVersion(lines.removeFirst()) if lines == ["NONE", ""] { return .init(status: .unknown) } guard let first = lines.first else { throw TerminalRuntimeError.invalidResponse } let client = first.components(separatedBy: "|") guard client.count == 10, client[0] == "C", client[1] == terminal, [2, 3, 4, 6].allSatisfy({ UInt64(client[$0]) != nil }), client[5].hasPrefix("$"), UInt64(client[5].dropFirst()) != nil, RemoteShellContextRecord.validPane(client[7]) else { throw TerminalRuntimeError.invalidResponse } let flags = Set(client[8].split(separator: ",").map(String.init)) guard !flags.contains("active-pane"), !flags.contains("read-only") else { return .init(status: .unavailable, backend: .tmux, issue: .paneAmbiguous) } let server = client[4] let session = client[5] + ":" + client[6] let clientIdentity = terminal + ":" + client[2] + ":" + client[3] var panes: [String: (process: String, interactive: Bool, foregroundShell: String)] = [:] var records: [RemoteShellContextRecord] = [] var nativeDirectory: String? var nativeCommand: String? var hasNativeDirectory = false var index = 1 while index < lines.count { let line = lines[index] index += 1 if line == "DONE" { guard index == lines.count - 1, lines.last == "" else { throw TerminalRuntimeError.invalidResponse } break } if line.hasPrefix("D|") { let fields = line.components(separatedBy: "|") guard !hasNativeDirectory, fields.count == 4, fields[1] == client[7] else { throw TerminalRuntimeError.invalidResponse } hasNativeDirectory = true let directory = try RemoteMultiplexerCommands.unhex(fields[2]) nativeDirectory = RemoteMultiplexerCommands.validPath(directory) ? directory : nil nativeCommand = try RemoteMultiplexerCommands.unhex(fields[3]) } else if line == "BEGIN" { let start = index while index < lines.count, lines[index] != "END" { index += 1 } guard index < lines.count else { throw TerminalRuntimeError.invalidResponse } records.append( try RemoteShellContextRecord(lines[start.. RemoteMultiplexerDraftTarget { let identity = context.identity let key = replacing ? "\u{1B}[777;2~" : "\u{1B}[777;1~" let session = identity.session.split(separator: ":").map(String.init) let client = clientIdentity?.split(separator: ":").map(String.init) ?? [] guard session.count == 2, session[0].hasPrefix("$"), UInt64(session[0].dropFirst()) != nil, UInt64(session[1]) != nil, UInt64(identity.server) != nil, RemoteShellContextRecord.validPane(identity.pane), client.count == 3, client[0] == terminal, UInt64(client[1]) != nil, UInt64(client[2]) != nil else { throw TerminalRuntimeError.staleContext } let target = try RemoteTmuxCommands.quote(session[0]) let root = """ \(RemoteTmuxCommands.executionPreamble) [ "$(tmux display-message -p -t \(target) '#{pid}:#{session_created}')" = '\(identity.server):\(session[1])' ] || exit 79 [ "$(tmux list-clients -F '#{client_tty}|#{client_pid}|#{client_created}|#{pane_id}|#{client_flags}|#{client_key_table}|#{session_id}|#{session_created}' | \ awk -F '|' -v tty=\(try RemoteTmuxCommands.quote(terminal)) '$1 == tty {print $2 ":" $3 ":" $4 ":" $6 ":" $7 ":" $8; if ($5 ~ /active-pane|read-only/) print "UNSUPPORTED"}')" \ = '\(client[1]):\(client[2]):\(identity.pane):root:\(identity.session)' ] || exit 79 helper=$(tmux show-environment -t \(target) TERMIND_TMUX_HELPERS) || exit 79 helper=${helper#TERMIND_TMUX_HELPERS=} """ let guardFormat = "#{&&:#{==:#{pane_in_mode},0},#{&&:#{==:#{synchronize-panes},0},#{&&:#{==:#{pane_dead},0},#{==:#{pane_current_command},\(context.shell)}}}}" let trigger = """ tmux if-shell -t \(try RemoteTmuxCommands.quote(identity.pane)) -F \(try RemoteTmuxCommands.quote(guardFormat)) \ \(try RemoteTmuxCommands.quote("send-keys -t \(identity.pane) -l \(try RemoteTmuxCommands.quote(key))")) || exit 79 """ return .init(root: root, trigger: trigger) } }