Skip to content

Commit 29a3c1f

Browse files
committed
terminal: update SwiftTerm and adopt its new renderer and find bar
SwiftTerm 2 no longer exposes the underlying Terminal from TerminalView, so move the terminal views to the copied view APIs: terminalDimensions, resize(cols:rows:), setCursorStyle, softReset and resetToInitialState. The view's resize also soft resets the terminal now, so only call it when the saved size differs, otherwise every resume would reset the guest's modes. Track SwiftTerm from the osy/SwiftTerm fork's fix/visionos-build branch for now: upstream main reads UIWindow.screen, which does not exist on visionOS, and the fork reads the display scale from the trait collection instead so the visionOS build works again. SwiftTerm now ships a build tool plugin, which needs two changes to build_utm.sh for CI. Xcode refuses to run a package plugin the machine has not trusted, so pass -skipPackagePluginValidation. And -sdk also applies to host tools, so the plugin's generator was built for the target platform instead of the Mac and the build could not find it; pick the platform with a generic destination and the architectures with ARCHS instead. Also match the SDK name macosx when picking the default macOS scheme. Take advantage of what the update brings: - Render with Metal on macOS and iOS, falling back to Core Graphics if the GPU pipeline cannot be created. visionOS stays on Core Graphics because SwiftTerm's Metal path draws at the wrong scale there. - Release the renderer with updateUiClosed() when the terminal window closes or the iOS view is dismantled, as SwiftTerm now requires. - Feed guest output straight from the serial port thread; feed() is thread-safe now, so the hop to the main queue is no longer needed. - Add the standard Edit > Find menu on macOS, which the terminal's new find bar is driven from. - Open only http(s) links from the terminal. SwiftTerm now also turns host file paths printed by the guest into links, and its default handler on macOS opens any URL or existing host path, so clicking one could launch a host app. On iOS, let the bell use SwiftTerm's default haptic instead of an empty override. Assisted-by: Claude:claude-opus-5 Assisted-by: Claude:claude-opus-5-5
1 parent 8bc7a0b commit 29a3c1f

9 files changed

Lines changed: 156 additions & 33 deletions

File tree

‎Platform/Shared/VMCommands.swift‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ struct VMCommands: Commands {
3333
#endif
3434
SidebarCommands()
3535
ToolbarCommands()
36+
#if os(macOS)
37+
TextEditingCommands()
38+
#endif
3639
CommandGroup(replacing: .help) {
3740
Button(action: { NotificationCenter.default.post(name: NSNotification.ShowReleaseNotes, object: nil) }, label: {
3841
Text("What's New")

‎Platform/iOS/Display/VMDisplayTerminalViewController.swift‎

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import SwiftUI
2424
willSet {
2525
vmSerialPort.delegate = nil
2626
newValue.delegate = self
27-
terminalView.getTerminal().resetToInitialState()
28-
terminalView.getTerminal().softReset()
27+
terminalView.resetToInitialState()
28+
terminalView.softReset()
2929
}
3030
}
3131

@@ -55,10 +55,22 @@ import SwiftUI
5555
setupKeyboardMonitor()
5656
}
5757

58+
override func viewDidAppear(_ animated: Bool) {
59+
super.viewDidAppear(animated)
60+
#if !os(visionOS) // SwiftTerm's Metal renderer draws at the wrong scale on visionOS
61+
do {
62+
try terminalView.setUseMetal(true)
63+
} catch {
64+
logger.debug("Metal terminal renderer unavailable, using CoreGraphics: \(error)")
65+
}
66+
#endif
67+
}
68+
5869
override func enterLive() {
5970
super.enterLive()
6071
DispatchQueue.main.async {
61-
let terminalSize = CGSize(width: self.terminalView.getTerminal().cols, height: self.terminalView.getTerminal().rows)
72+
let dimensions = self.terminalView.terminalDimensions
73+
let terminalSize = CGSize(width: dimensions.cols, height: dimensions.rows)
6274
self.delegate.displayViewSize = terminalSize
6375
}
6476
}
@@ -72,6 +84,22 @@ import SwiftUI
7284
super.hideKeyboard()
7385
_ = terminalView.resignFirstResponder()
7486
}
87+
88+
func closeTerminal() {
89+
if let terminalView = terminalView {
90+
Self.closeWhenIdle(terminalView)
91+
}
92+
}
93+
94+
/// Releases the terminal's renderer, retrying while GPU work is still in flight
95+
private static func closeWhenIdle(_ terminalView: TerminalView) {
96+
guard !terminalView.updateUiClosed() else {
97+
return
98+
}
99+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
100+
closeWhenIdle(terminalView)
101+
}
102+
}
75103
}
76104

77105
// MARK: - Layout terminal
@@ -127,7 +155,7 @@ extension VMDisplayTerminalViewController {
127155
terminalView.nativeForegroundColor = UIColor(textColor)
128156
terminalView.nativeBackgroundColor = UIColor(backgroundColor)
129157
}
130-
terminalView.getTerminal().setCursorStyle(style.hasCursorBlink ? .blinkBlock : .steadyBlock)
158+
terminalView.setCursorStyle(style.hasCursorBlink ? .blinkBlock : .steadyBlock)
131159
terminalView.optionAsMetaKey = boolForSetting("OptionAsMetaKey")
132160
}
133161
}
@@ -142,6 +170,10 @@ extension VMDisplayTerminalViewController: TerminalViewDelegate {
142170
}
143171

144172
func requestOpenLink(source: TerminalView, link: String, params: [String : String]) {
173+
guard let url = URL(string: link), let scheme = url.scheme?.lowercased(), ["http", "https"].contains(scheme) else {
174+
return
175+
}
176+
UIApplication.shared.open(url)
145177
}
146178

147179
func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {
@@ -156,9 +188,6 @@ extension VMDisplayTerminalViewController: TerminalViewDelegate {
156188
delegate?.displayDidAssertUserInteraction()
157189
}
158190

159-
func bell(source: TerminalView) {
160-
}
161-
162191
func rangeChanged(source: TerminalView, startY: Int, endY: Int) {
163192
}
164193

@@ -180,10 +209,7 @@ extension VMDisplayTerminalViewController: CSPortDelegate {
180209

181210
func port(_ port: CSPort, didRecieveData data: Data) {
182211
if let terminalView = terminalView {
183-
let arr = [UInt8](data)[...]
184-
DispatchQueue.main.async {
185-
terminalView.feed(byteArray: arr)
186-
}
212+
terminalView.feed(byteArray: [UInt8](data)[...])
187213
}
188214
}
189215
}

‎Platform/iOS/VMDisplayHostedView.swift‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,12 @@ struct VMDisplayHostedView: UIViewControllerRepresentable {
218218
}
219219
}
220220

221+
static func dismantleUIViewController(_ uiViewController: VMDisplayViewController, coordinator: Coordinator) {
222+
if let vc = uiViewController as? VMDisplayTerminalViewController {
223+
vc.closeTerminal()
224+
}
225+
}
226+
221227
func makeCoordinator() -> Coordinator {
222228
Coordinator(with: vm, device: device, state: $state)
223229
}

‎Platform/macOS/Display/VMDisplayAppleTerminalWindowController.swift‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ class VMDisplayAppleTerminalWindowController: VMDisplayAppleWindowController, VM
6363
terminalView!.terminalDelegate = self
6464
terminalView.allowMouseReporting = false
6565
super.windowDidLoad()
66+
enableMetalRenderer(for: terminalView)
67+
}
68+
69+
override func windowWillClose(_ notification: Notification) {
70+
closeTerminal(terminalView)
71+
super.windowWillClose(notification)
6672
}
6773

6874
override func updateWindowFrame() {
@@ -113,6 +119,10 @@ extension VMDisplayAppleTerminalWindowController: TerminalViewDelegate, UTMSeria
113119
func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {
114120
}
115121

122+
func requestOpenLink(source: TerminalView, link: String, params: [String : String]) {
123+
openLink(link)
124+
}
125+
116126
func send(source: TerminalView, data: ArraySlice<UInt8>) {
117127
serialPort.write(data: Data(data))
118128
}
@@ -122,10 +132,7 @@ extension VMDisplayAppleTerminalWindowController: TerminalViewDelegate, UTMSeria
122132

123133
func serialPort(_ serialPort: UTMSerialPort, didRecieveData data: Data) {
124134
if let terminalView = terminalView {
125-
let arr = [UInt8](data)[...]
126-
DispatchQueue.main.async {
127-
terminalView.feed(byteArray: arr)
128-
}
135+
terminalView.feed(byteArray: [UInt8](data)[...])
129136
}
130137
}
131138

‎Platform/macOS/Display/VMDisplayQemuTerminalWindowController.swift‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,16 @@ class VMDisplayQemuTerminalWindowController: VMDisplayQemuWindowController, VMDi
4646
terminalView.autoresizingMask = [.width, .height]
4747
terminalView.allowMouseReporting = false
4848
displayView.addSubview(terminalView)
49+
enableMetalRenderer(for: terminalView)
4950
vmSerialPort?.delegate = self // can be nil for primary window
5051
super.windowDidLoad()
5152
}
5253

54+
override func windowWillClose(_ notification: Notification) {
55+
closeTerminal(terminalView)
56+
super.windowWillClose(notification)
57+
}
58+
5359
override func enterLive() {
5460
super.enterLive()
5561
isSizeChangeIgnored = true
@@ -113,6 +119,10 @@ extension VMDisplayQemuTerminalWindowController: TerminalViewDelegate {
113119
func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {
114120
}
115121

122+
func requestOpenLink(source: TerminalView, link: String, params: [String : String]) {
123+
openLink(link)
124+
}
125+
116126
func send(source: TerminalView, data: ArraySlice<UInt8>) {
117127
if let vmSerialPort = vmSerialPort {
118128
vmSerialPort.write(Data(data))
@@ -154,10 +164,7 @@ extension VMDisplayQemuTerminalWindowController: CSPortDelegate {
154164

155165
func port(_ port: CSPort, didRecieveData data: Data) {
156166
if let terminalView = terminalView {
157-
let arr = [UInt8](data)[...]
158-
DispatchQueue.main.async {
159-
terminalView.feed(byteArray: arr)
160-
}
167+
terminalView.feed(byteArray: [UInt8](data)[...])
161168
}
162169
}
163170
}

‎Platform/macOS/Display/VMDisplayTerminal.swift‎

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,26 @@ import SwiftUI
2020

2121
private let kVMDefaultResizeCmd = "stty cols $COLS rows $ROWS\\n"
2222

23+
/// Releases the terminal's renderer, retrying while GPU work is still in flight
24+
@MainActor private func closeWhenIdle(_ terminalView: TerminalView) {
25+
guard !terminalView.updateUiClosed() else {
26+
return
27+
}
28+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
29+
closeWhenIdle(terminalView)
30+
}
31+
}
32+
2333
protocol VMDisplayTerminal {
2434
var vm: (any UTMVirtualMachine)! { get }
2535
var isOptionAsMetaKey: Bool { get }
2636
@MainActor func setupTerminal(_ terminalView: TerminalView, using config: UTMConfigurationTerminal, id: Int, for window: NSWindow)
2737
func resizeCommand(for terminal: TerminalView, using config: UTMConfigurationTerminal) -> String
2838
func sizeChanged(id: Int, newCols: Int, newRows: Int)
2939
func sendString(_ string: String)
40+
@MainActor func enableMetalRenderer(for terminalView: TerminalView)
41+
@MainActor func closeTerminal(_ terminalView: TerminalView)
42+
@MainActor func openLink(_ link: String)
3043
}
3144

3245
extension VMDisplayTerminal {
@@ -50,17 +63,19 @@ extension VMDisplayTerminal {
5063
terminalView.nativeForegroundColor = NSColor(textColor)
5164
terminalView.nativeBackgroundColor = NSColor(backgroundColor)
5265
}
53-
terminalView.getTerminal().resize(cols: windowConfig.columns, rows: windowConfig.rows)
54-
terminalView.getTerminal().setCursorStyle(config.hasCursorBlink ? .blinkBlock : .steadyBlock)
66+
if terminalView.terminalDimensions != TerminalDimensions(cols: windowConfig.columns, rows: windowConfig.rows) {
67+
terminalView.resize(cols: windowConfig.columns, rows: windowConfig.rows)
68+
}
69+
terminalView.setCursorStyle(config.hasCursorBlink ? .blinkBlock : .steadyBlock)
5570
let size = window.frameRect(forContentRect: terminalView.getOptimalFrameSize()).size
5671
let frame = CGRect(origin: window.frame.origin, size: size)
5772
window.setFrame(frame, display: false, animate: true)
5873
terminalView.optionAsMetaKey = isOptionAsMetaKey
5974
}
6075

6176
func resizeCommand(for terminalView: TerminalView, using config: UTMConfigurationTerminal) -> String {
62-
let cols = terminalView.getTerminal().cols
63-
let rows = terminalView.getTerminal().rows
77+
let cols = terminalView.terminalDimensions.cols
78+
let rows = terminalView.terminalDimensions.rows
6479
let template = config.resizeCommand ?? kVMDefaultResizeCmd
6580
let cmd = template
6681
.replacingOccurrences(of: "$COLS", with: String(cols))
@@ -69,6 +84,26 @@ extension VMDisplayTerminal {
6984
return cmd
7085
}
7186

87+
@MainActor func enableMetalRenderer(for terminalView: TerminalView) {
88+
do {
89+
try terminalView.setUseMetal(true)
90+
} catch {
91+
logger.debug("Metal terminal renderer unavailable, using CoreGraphics: \(error)")
92+
}
93+
}
94+
95+
@MainActor func closeTerminal(_ terminalView: TerminalView) {
96+
closeWhenIdle(terminalView)
97+
}
98+
99+
/// Only web links are opened so that guest output cannot open host files or apps
100+
@MainActor func openLink(_ link: String) {
101+
guard let url = URL(string: link), let scheme = url.scheme?.lowercased(), ["http", "https"].contains(scheme) else {
102+
return
103+
}
104+
NSWorkspace.shared.open(url)
105+
}
106+
72107
func sizeChanged(id: Int, newCols: Int, newRows: Int) {
73108
Task { @MainActor in
74109
let windowConfig = UTMRegistryEntry.Terminal(columns: newCols, rows: newRows)

‎UTM.xcodeproj/project.pbxproj‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5601,9 +5601,9 @@
56015601
};
56025602
848F71E4277A2466006A0240 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = {
56035603
isa = XCRemoteSwiftPackageReference;
5604-
repositoryURL = "https://github.com/migueldeicaza/SwiftTerm.git";
5604+
repositoryURL = "https://github.com/osy/SwiftTerm.git";
56055605
requirement = {
5606-
branch = main;
5606+
branch = "fix/visionos-build";
56075607
kind = branch;
56085608
};
56095609
};
@@ -5737,9 +5737,9 @@
57375737
};
57385738
CEF7F58F2AEEDCC400E34952 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = {
57395739
isa = XCRemoteSwiftPackageReference;
5740-
repositoryURL = "https://github.com/migueldeicaza/SwiftTerm.git";
5740+
repositoryURL = "https://github.com/osy/SwiftTerm.git";
57415741
requirement = {
5742-
branch = main;
5742+
branch = "fix/visionos-build";
57435743
kind = branch;
57445744
};
57455745
};

‎UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved‎

Lines changed: 21 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎scripts/build_utm.sh‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ while [ "x$1" != "x" ]; do
5555
done
5656

5757
case $SDK in
58-
macos )
58+
macosx )
5959
SCHEME="macOS"
6060
;;
6161
* )
@@ -65,12 +65,33 @@ macos )
6565
;;
6666
esac
6767

68-
ARCH_ARGS=$(echo $ARCH | xargs printf -- "-arch %s ")
68+
# -sdk would also apply to host tools such as package build plugins, so select
69+
# the platform with a destination instead
70+
case $SDK in
71+
iphoneos )
72+
PLATFORM="iOS"
73+
;;
74+
iphonesimulator )
75+
PLATFORM="iOS Simulator"
76+
;;
77+
xros )
78+
PLATFORM="visionOS"
79+
;;
80+
xrsimulator )
81+
PLATFORM="visionOS Simulator"
82+
;;
83+
macosx )
84+
PLATFORM="macOS"
85+
;;
86+
* )
87+
usage
88+
;;
89+
esac
6990
if [ ! -z "$TEAM_IDENTIFIER" ]; then
7091
TEAM_IDENTIFIER_PREFIX="TeamIdentifierPrefix=${TEAM_IDENTIFIER}."
7192
fi
7293

73-
xcodebuild archive -archivePath "$OUTPUT" -scheme "$SCHEME" -sdk "$SDK" $ARCH_ARGS -configuration Release CODE_SIGNING_ALLOWED=NO $TEAM_IDENTIFIER_PREFIX
94+
xcodebuild archive -archivePath "$OUTPUT" -scheme "$SCHEME" -destination "generic/platform=$PLATFORM" ARCHS="$ARCH" -configuration Release -skipPackagePluginValidation CODE_SIGNING_ALLOWED=NO $TEAM_IDENTIFIER_PREFIX
7495
BUILT_PATH=$(find $OUTPUT.xcarchive -name '*.app' -type d | head -1)
7596
# Only retain the target architecture to address < iOS 15 crash & save disk space
7697
if [ "$SDK" == "iphoneos" ]; then

0 commit comments

Comments
 (0)