Files
Termind/TFM/App/ScreenshotDemo.swift
T

280 lines
11 KiB
Swift

import TermindApplication
#if DEBUG
import Foundation
/// An explicit, isolated launch configuration used to capture product
/// images. The scene and language travel as one value so a capture cannot
/// accidentally combine demo content in one language with UI in another.
struct ScreenshotDemoConfiguration: Equatable {
enum Scene: String, CaseIterable {
case hosts
case workspace
case agent
case command
case metrics
case metricsDetail = "metrics-detail"
case appSettings = "app-settings"
}
enum ParseError: Error, Equatable, CustomStringConvertible {
case duplicateArguments
case invalidFormat(String)
case unknownScene(String)
case unknownLanguage(String)
var description: String {
switch self {
case .duplicateArguments:
return "Pass --termind-demo only once."
case .invalidFormat(let value):
return "Invalid Demo argument \(value). Expected "
+ "--termind-demo=<hosts|workspace|agent|command|metrics|metrics-detail|app-settings>:<en|zh-Hans|zh-Hant>."
case .unknownScene(let value):
return
"Unknown Demo scene \(value). Expected hosts, workspace, agent, command, metrics, metrics-detail, or app-settings."
case .unknownLanguage(let value):
return "Unknown Demo language \(value). Expected en or zh."
}
}
}
private static let option = "--termind-demo"
let scene: Scene
let language: AppLanguage
static var active: Self? {
do {
return try parse(arguments: ProcessInfo.processInfo.arguments)
} catch {
fatalError("Unable to launch Termind Demo: \(error)")
}
}
static func parse(arguments: [String]) throws -> Self? {
let candidates = arguments.filter {
$0 == option || $0.hasPrefix("\(option)=")
}
guard candidates.count <= 1 else { throw ParseError.duplicateArguments }
guard let argument = candidates.first else { return nil }
let prefix = "\(option)="
guard argument.hasPrefix(prefix) else {
throw ParseError.invalidFormat(argument)
}
let value = String(argument.dropFirst(prefix.count))
let components = value.split(separator: ":", omittingEmptySubsequences: false)
guard components.count == 2 else {
throw ParseError.invalidFormat(argument)
}
let sceneValue = String(components[0])
guard let scene = Scene(rawValue: sceneValue) else {
throw ParseError.unknownScene(sceneValue)
}
let languageValue = String(components[1])
let language: AppLanguage
switch languageValue {
case "en": language = .english
case "zh-Hans": language = .simplifiedChinese
case "zh-Hant": language = .traditionalChinese
default: throw ParseError.unknownLanguage(languageValue)
}
return Self(scene: scene, language: language)
}
var windowSize: CGSize {
switch scene {
case .command:
CGSize(width: 1_022, height: 640)
case .workspace, .agent, .hosts, .metrics, .metricsDetail, .appSettings:
CGSize(
width: Metrics.Window.defaultWidth,
height: Metrics.Window.defaultHeight
)
}
}
func makeWindow(hosts: HostLibraryModel) -> WindowModel {
switch scene {
case .workspace:
return workspaceWindow(hosts: hosts, showsCommandResult: false)
case .command:
return workspaceWindow(hosts: hosts, showsCommandResult: true)
case .agent:
return agentWindow(hosts: hosts)
case .hosts:
return hostsWindow()
case .appSettings:
let window = WindowModel()
window.selectedDestination = .settings
window.settingsRoute = .category(.app)
return window
case .metrics, .metricsDetail:
let window = WindowModel()
window.selectedDestination = .metrics
window.showsSidebar = scene == .metrics
window.sidebarMode = .navigation
window.metricsHostID = scene == .metricsDetail ? hosts.hosts.first?.id : nil
return window
}
}
private func hostsWindow() -> WindowModel {
let window = WindowModel(vaultDestination: .hosts)
window.showsSidebar = true
window.sidebarMode = .navigation
return window
}
private func agentWindow(hosts: HostLibraryModel) -> WindowModel {
guard let host = hosts.hosts.first else {
preconditionFailure("Screenshot demo requires its bundled host")
}
let session = SSHSessionModel.demo(host: host)
let pane = Pane(.sshSession(session))
pane.customTitle = text(en: "nginx diagnostics", zhHans: "Nginx 诊断", zhHant: "Nginx 診斷")
let workspace = Workspace(pane: pane)
workspace.customTitle = text(
en: "Production diagnostics", zhHans: "生产环境诊断", zhHant: "正式環境診斷")
workspace.focusedPaneID = pane.id
let conversation = ConversationModel.demo(language: language)
let library = AssistantLibraryModel()
library.insertAndSelect(conversation)
let window = WindowModel(workspaces: [workspace], assistantLibrary: library)
window.showsSidebar = true
window.sidebarMode = .assistant
window.openAssistantInSidebar(conversation.id)
return window
}
private func workspaceWindow(
hosts: HostLibraryModel, showsCommandResult: Bool
) -> WindowModel {
let saved = hosts.hosts
precondition(saved.count >= 2, "Screenshot demo requires its bundled hosts")
let firstSession: SSHSessionModel
if showsCommandResult {
firstSession = SSHSessionModel.demo(
host: saved[0],
transcript: [
"\u{001B}]7;file://web-01/var/log/nginx\u{0007}",
"\u{001B}[2mLast login: Thu Aug 27 16:42:18 from 10.0.0.12\u{001B}[0m",
"\u{001B}[36mdeploy@web-01\u{001B}[0m "
+ "\u{001B}[34m/var/log/nginx\u{001B}[0m % "
+ "find . -type f -newermt today -printf '%s %p\\n' | sort -nr | head -10 ",
].joined(separator: "\r\n")
)
} else {
firstSession = .demo(
host: saved[0],
transcript: broadcastTranscript(for: saved[0].name, variant: .primary)
)
}
let first = Pane(.sshSession(firstSession))
first.customTitle =
showsCommandResult
? text(en: "nginx logs", zhHans: "Nginx 日志", zhHant: "Nginx 日誌")
: "api-01"
let sftpChannel = SFTPChannelModel.demo(hostID: saved[0].id)
sftpChannel.selection = ["/etc/nginx/nginx.conf"]
let sftp = Pane(.sftpChannel(sftpChannel))
sftp.customTitle = text(en: "nginx config", zhHans: "Nginx 配置", zhHant: "Nginx 設定")
let secondSession =
showsCommandResult
? SSHSessionModel.demo(host: saved[1])
: SSHSessionModel.demo(
host: saved[1],
transcript: broadcastTranscript(for: saved[1].name, variant: .secondary)
)
let second = Pane(.sshSession(secondSession))
second.customTitle = "worker-02"
let workspace = Workspace(pane: first)
if showsCommandResult {
workspace.layout = .leaf(first)
} else {
workspace.layout = .split(
.init(
axis: .horizontal,
fraction: 0.5,
first: .leaf(first),
second: .split(
.init(
axis: .vertical,
fraction: 0.5,
first: .leaf(sftp),
second: .leaf(second)
)
)
)
)
}
workspace.focusedPaneID = first.id
workspace.customTitle =
showsCommandResult
? text(en: "Log investigation", zhHans: "日志调查", zhHant: "日誌調查")
: text(en: "Production diagnostics", zhHans: "生产环境诊断", zhHant: "正式環境診斷")
workspace.broadcastsInput = !showsCommandResult
if showsCommandResult {
workspace.commandGeneratorState = .inserted(targetCount: 1)
}
let window = WindowModel(workspaces: [workspace])
window.showsSidebar = showsCommandResult
if showsCommandResult {
window.sidebarMode = .navigation
}
return window
}
private enum BroadcastTranscriptVariant { case primary, secondary }
private func broadcastTranscript(
for host: String, variant: BroadcastTranscriptVariant
) -> String {
let prompt =
"\u{001B}[36mdeploy@\(host)\u{001B}[0m "
+ "\u{001B}[34m/var/log/nginx\u{001B}[0m %"
let history: [String]
switch variant {
case .primary:
history = [
prompt + " systemctl status nginx --no-pager",
"● nginx.service - A high performance web server",
" Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled)",
" Active: \u{001B}[32mactive (running)\u{001B}[0m since Thu 2026-08-27 09:14:03 UTC",
]
case .secondary:
history = [
prompt + " journalctl -u nginx -n 2 --no-pager",
"Aug 27 09:13:58 web-02 nginx[1842]: configuration test is successful",
"Aug 27 09:14:03 web-02 systemd[1]: Started nginx.service",
]
}
return
([
"\u{001B}]7;file://\(host)/var/log/nginx\u{0007}",
"\u{001B}[2mLast login: Thu Aug 27 16:42:18 from 10.0.0.12\u{001B}[0m",
] + history + [
prompt + " tail -f /var/log/nginx/access.log"
]).joined(separator: "\r\n")
}
private func text(en: String, zhHans: String, zhHant: String) -> String {
switch language {
case .simplifiedChinese: zhHans
case .traditionalChinese: zhHant
case .system, .english: en
}
}
}
#endif