Merge pull request #7901 from utmapp/feature/swiftterm-2

terminal: update SwiftTerm and adopt its new renderer and find bar
This commit is contained in:
osy
2026-09-22 18:50:56 -05:00
committed by GitHub
9 changed files with 156 additions and 33 deletions
+3
View File
@@ -33,6 +33,9 @@ struct VMCommands: Commands {
#endif
SidebarCommands()
ToolbarCommands()
#if os(macOS)
TextEditingCommands()
#endif
CommandGroup(replacing: .help) {
Button(action: { NotificationCenter.default.post(name: NSNotification.ShowReleaseNotes, object: nil) }, label: {
Text("What's New")
@@ -24,8 +24,8 @@ import SwiftUI
willSet {
vmSerialPort.delegate = nil
newValue.delegate = self
terminalView.getTerminal().resetToInitialState()
terminalView.getTerminal().softReset()
terminalView.resetToInitialState()
terminalView.softReset()
}
}
@@ -55,10 +55,22 @@ import SwiftUI
setupKeyboardMonitor()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
#if !os(visionOS) // SwiftTerm's Metal renderer draws at the wrong scale on visionOS
do {
try terminalView.setUseMetal(true)
} catch {
logger.debug("Metal terminal renderer unavailable, using CoreGraphics: \(error)")
}
#endif
}
override func enterLive() {
super.enterLive()
DispatchQueue.main.async {
let terminalSize = CGSize(width: self.terminalView.getTerminal().cols, height: self.terminalView.getTerminal().rows)
let dimensions = self.terminalView.terminalDimensions
let terminalSize = CGSize(width: dimensions.cols, height: dimensions.rows)
self.delegate.displayViewSize = terminalSize
}
}
@@ -72,6 +84,22 @@ import SwiftUI
super.hideKeyboard()
_ = terminalView.resignFirstResponder()
}
func closeTerminal() {
if let terminalView = terminalView {
Self.closeWhenIdle(terminalView)
}
}
/// Releases the terminal's renderer, retrying while GPU work is still in flight
private static func closeWhenIdle(_ terminalView: TerminalView) {
guard !terminalView.updateUiClosed() else {
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
closeWhenIdle(terminalView)
}
}
}
// MARK: - Layout terminal
@@ -127,7 +155,7 @@ extension VMDisplayTerminalViewController {
terminalView.nativeForegroundColor = UIColor(textColor)
terminalView.nativeBackgroundColor = UIColor(backgroundColor)
}
terminalView.getTerminal().setCursorStyle(style.hasCursorBlink ? .blinkBlock : .steadyBlock)
terminalView.setCursorStyle(style.hasCursorBlink ? .blinkBlock : .steadyBlock)
terminalView.optionAsMetaKey = boolForSetting("OptionAsMetaKey")
}
}
@@ -142,6 +170,10 @@ extension VMDisplayTerminalViewController: TerminalViewDelegate {
}
func requestOpenLink(source: TerminalView, link: String, params: [String : String]) {
guard let url = URL(string: link), let scheme = url.scheme?.lowercased(), ["http", "https"].contains(scheme) else {
return
}
UIApplication.shared.open(url)
}
func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {
@@ -156,9 +188,6 @@ extension VMDisplayTerminalViewController: TerminalViewDelegate {
delegate?.displayDidAssertUserInteraction()
}
func bell(source: TerminalView) {
}
func rangeChanged(source: TerminalView, startY: Int, endY: Int) {
}
@@ -180,10 +209,7 @@ extension VMDisplayTerminalViewController: CSPortDelegate {
func port(_ port: CSPort, didRecieveData data: Data) {
if let terminalView = terminalView {
let arr = [UInt8](data)[...]
DispatchQueue.main.async {
terminalView.feed(byteArray: arr)
}
terminalView.feed(byteArray: [UInt8](data)[...])
}
}
}
+6
View File
@@ -218,6 +218,12 @@ struct VMDisplayHostedView: UIViewControllerRepresentable {
}
}
static func dismantleUIViewController(_ uiViewController: VMDisplayViewController, coordinator: Coordinator) {
if let vc = uiViewController as? VMDisplayTerminalViewController {
vc.closeTerminal()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(with: vm, device: device, state: $state)
}
@@ -63,6 +63,12 @@ class VMDisplayAppleTerminalWindowController: VMDisplayAppleWindowController, VM
terminalView!.terminalDelegate = self
terminalView.allowMouseReporting = false
super.windowDidLoad()
enableMetalRenderer(for: terminalView)
}
override func windowWillClose(_ notification: Notification) {
closeTerminal(terminalView)
super.windowWillClose(notification)
}
override func updateWindowFrame() {
@@ -113,6 +119,10 @@ extension VMDisplayAppleTerminalWindowController: TerminalViewDelegate, UTMSeria
func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {
}
func requestOpenLink(source: TerminalView, link: String, params: [String : String]) {
openLink(link)
}
func send(source: TerminalView, data: ArraySlice<UInt8>) {
serialPort.write(data: Data(data))
}
@@ -122,10 +132,7 @@ extension VMDisplayAppleTerminalWindowController: TerminalViewDelegate, UTMSeria
func serialPort(_ serialPort: UTMSerialPort, didRecieveData data: Data) {
if let terminalView = terminalView {
let arr = [UInt8](data)[...]
DispatchQueue.main.async {
terminalView.feed(byteArray: arr)
}
terminalView.feed(byteArray: [UInt8](data)[...])
}
}
@@ -46,10 +46,16 @@ class VMDisplayQemuTerminalWindowController: VMDisplayQemuWindowController, VMDi
terminalView.autoresizingMask = [.width, .height]
terminalView.allowMouseReporting = false
displayView.addSubview(terminalView)
enableMetalRenderer(for: terminalView)
vmSerialPort?.delegate = self // can be nil for primary window
super.windowDidLoad()
}
override func windowWillClose(_ notification: Notification) {
closeTerminal(terminalView)
super.windowWillClose(notification)
}
override func enterLive() {
super.enterLive()
isSizeChangeIgnored = true
@@ -113,6 +119,10 @@ extension VMDisplayQemuTerminalWindowController: TerminalViewDelegate {
func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {
}
func requestOpenLink(source: TerminalView, link: String, params: [String : String]) {
openLink(link)
}
func send(source: TerminalView, data: ArraySlice<UInt8>) {
if let vmSerialPort = vmSerialPort {
vmSerialPort.write(Data(data))
@@ -154,10 +164,7 @@ extension VMDisplayQemuTerminalWindowController: CSPortDelegate {
func port(_ port: CSPort, didRecieveData data: Data) {
if let terminalView = terminalView {
let arr = [UInt8](data)[...]
DispatchQueue.main.async {
terminalView.feed(byteArray: arr)
}
terminalView.feed(byteArray: [UInt8](data)[...])
}
}
}
+39 -4
View File
@@ -20,6 +20,16 @@ import SwiftUI
private let kVMDefaultResizeCmd = "stty cols $COLS rows $ROWS\\n"
/// Releases the terminal's renderer, retrying while GPU work is still in flight
@MainActor private func closeWhenIdle(_ terminalView: TerminalView) {
guard !terminalView.updateUiClosed() else {
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
closeWhenIdle(terminalView)
}
}
protocol VMDisplayTerminal {
var vm: (any UTMVirtualMachine)! { get }
var isOptionAsMetaKey: Bool { get }
@@ -27,6 +37,9 @@ protocol VMDisplayTerminal {
func resizeCommand(for terminal: TerminalView, using config: UTMConfigurationTerminal) -> String
func sizeChanged(id: Int, newCols: Int, newRows: Int)
func sendString(_ string: String)
@MainActor func enableMetalRenderer(for terminalView: TerminalView)
@MainActor func closeTerminal(_ terminalView: TerminalView)
@MainActor func openLink(_ link: String)
}
extension VMDisplayTerminal {
@@ -50,8 +63,10 @@ extension VMDisplayTerminal {
terminalView.nativeForegroundColor = NSColor(textColor)
terminalView.nativeBackgroundColor = NSColor(backgroundColor)
}
terminalView.getTerminal().resize(cols: windowConfig.columns, rows: windowConfig.rows)
terminalView.getTerminal().setCursorStyle(config.hasCursorBlink ? .blinkBlock : .steadyBlock)
if terminalView.terminalDimensions != TerminalDimensions(cols: windowConfig.columns, rows: windowConfig.rows) {
terminalView.resize(cols: windowConfig.columns, rows: windowConfig.rows)
}
terminalView.setCursorStyle(config.hasCursorBlink ? .blinkBlock : .steadyBlock)
let size = window.frameRect(forContentRect: terminalView.getOptimalFrameSize()).size
let frame = CGRect(origin: window.frame.origin, size: size)
window.setFrame(frame, display: false, animate: true)
@@ -59,8 +74,8 @@ extension VMDisplayTerminal {
}
func resizeCommand(for terminalView: TerminalView, using config: UTMConfigurationTerminal) -> String {
let cols = terminalView.getTerminal().cols
let rows = terminalView.getTerminal().rows
let cols = terminalView.terminalDimensions.cols
let rows = terminalView.terminalDimensions.rows
let template = config.resizeCommand ?? kVMDefaultResizeCmd
let cmd = template
.replacingOccurrences(of: "$COLS", with: String(cols))
@@ -69,6 +84,26 @@ extension VMDisplayTerminal {
return cmd
}
@MainActor func enableMetalRenderer(for terminalView: TerminalView) {
do {
try terminalView.setUseMetal(true)
} catch {
logger.debug("Metal terminal renderer unavailable, using CoreGraphics: \(error)")
}
}
@MainActor func closeTerminal(_ terminalView: TerminalView) {
closeWhenIdle(terminalView)
}
/// Only web links are opened so that guest output cannot open host files or apps
@MainActor func openLink(_ link: String) {
guard let url = URL(string: link), let scheme = url.scheme?.lowercased(), ["http", "https"].contains(scheme) else {
return
}
NSWorkspace.shared.open(url)
}
func sizeChanged(id: Int, newCols: Int, newRows: Int) {
Task { @MainActor in
let windowConfig = UTMRegistryEntry.Terminal(columns: newCols, rows: newRows)
+4 -4
View File
@@ -5601,9 +5601,9 @@
};
848F71E4277A2466006A0240 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/migueldeicaza/SwiftTerm.git";
repositoryURL = "https://github.com/osy/SwiftTerm.git";
requirement = {
branch = main;
branch = "fix/visionos-build";
kind = branch;
};
};
@@ -5737,9 +5737,9 @@
};
CEF7F58F2AEEDCC400E34952 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/migueldeicaza/SwiftTerm.git";
repositoryURL = "https://github.com/osy/SwiftTerm.git";
requirement = {
branch = main;
branch = "fix/visionos-build";
kind = branch;
};
};
@@ -28,6 +28,15 @@
"revision" : "c359a08accfb49662a17cdfc5e333c7b4e5c2c56"
}
},
{
"identity" : "h",
"kind" : "remoteSourceControl",
"location" : "https://github.com/rarestype/h",
"state" : {
"revision" : "aa3626829160917d4378330617971977cbd78f5b",
"version" : "1.0.1"
}
},
{
"identity" : "inappsettingskit",
"kind" : "remoteSourceControl",
@@ -73,6 +82,15 @@
"version" : "1.5.3"
}
},
{
"identity" : "swift-png",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tayloraswift/swift-png",
"state" : {
"revision" : "8a0bcd4df5e4b307c804937776a56dd6ecdf6396",
"version" : "4.5.1"
}
},
{
"identity" : "swiftconnect",
"kind" : "remoteSourceControl",
@@ -103,10 +121,10 @@
{
"identity" : "swiftterm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/migueldeicaza/SwiftTerm.git",
"location" : "https://github.com/osy/SwiftTerm.git",
"state" : {
"branch" : "main",
"revision" : "ea0f681b25c8385b4a5a48d435e61d11392216e0"
"branch" : "fix/visionos-build",
"revision" : "f2f5349b2a1c629ec6c87286d814147136e44eca"
}
},
{
+24 -3
View File
@@ -55,7 +55,7 @@ while [ "x$1" != "x" ]; do
done
case $SDK in
macos )
macosx )
SCHEME="macOS"
;;
* )
@@ -65,12 +65,33 @@ macos )
;;
esac
ARCH_ARGS=$(echo $ARCH | xargs printf -- "-arch %s ")
# -sdk would also apply to host tools such as package build plugins, so select
# the platform with a destination instead
case $SDK in
iphoneos )
PLATFORM="iOS"
;;
iphonesimulator )
PLATFORM="iOS Simulator"
;;
xros )
PLATFORM="visionOS"
;;
xrsimulator )
PLATFORM="visionOS Simulator"
;;
macosx )
PLATFORM="macOS"
;;
* )
usage
;;
esac
if [ ! -z "$TEAM_IDENTIFIER" ]; then
TEAM_IDENTIFIER_PREFIX="TeamIdentifierPrefix=${TEAM_IDENTIFIER}."
fi
xcodebuild archive -archivePath "$OUTPUT" -scheme "$SCHEME" -sdk "$SDK" $ARCH_ARGS -configuration Release CODE_SIGNING_ALLOWED=NO $TEAM_IDENTIFIER_PREFIX
xcodebuild archive -archivePath "$OUTPUT" -scheme "$SCHEME" -destination "generic/platform=$PLATFORM" ARCHS="$ARCH" -configuration Release -skipPackagePluginValidation CODE_SIGNING_ALLOWED=NO $TEAM_IDENTIFIER_PREFIX
BUILT_PATH=$(find $OUTPUT.xcarchive -name '*.app' -type d | head -1)
# Only retain the target architecture to address < iOS 15 crash & save disk space
if [ "$SDK" == "iphoneos" ]; then