Skip to content

Commit

Permalink
display(macOS): use SwiftTerm for SPICE QEMU
Browse files Browse the repository at this point in the history
Resolves #3473
  • Loading branch information
osy committed May 22, 2022
1 parent e063384 commit d9ecb1e
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 115 deletions.
6 changes: 3 additions & 3 deletions Managers/UTMQemuSystem.m
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,9 @@ - (void)argsRequired {
error:nil]];
[self pushArgv:self.resourceURL.path];
[self pushArgv:@"-S"]; // startup stopped
NSURL *spiceSocketURL = self.configuration.spiceSocketURL;
[self pushArgv:@"-spice"];
[self pushArgv:[NSString stringWithFormat:@"unix=on,addr=%@,disable-ticketing=on,image-compression=off,playback-compression=off,streaming-video=off,gl=%@", spiceSocketURL.path, self.isGLOn ? @"on" : @"off"]];
[self pushArgv:@"-chardev"];
[self pushArgv:@"spiceport,id=org.qemu.monitor.qmp,name=org.qemu.monitor.qmp.0"];
[self pushArgv:@"-mon"];
Expand All @@ -669,9 +672,6 @@ - (void)argsRequired {
[self pushArgv: @"-serial"];
[self pushArgv: @"chardev:term0"];
} else {
NSURL *spiceSocketURL = self.configuration.spiceSocketURL;
[self pushArgv:@"-spice"];
[self pushArgv:[NSString stringWithFormat:@"unix=on,addr=%@,disable-ticketing=on,image-compression=off,playback-compression=off,streaming-video=off,gl=%@", spiceSocketURL.path, self.isGLOn ? @"on" : @"off"]];
if (!self.isSparc) { // SPARC uses -vga (above)
[self pushArgv:@"-device"];
[self pushArgv:self.configuration.displayCard];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ class VMDisplayMetalWindowController: VMDisplayQemuWindowController {
self.syncCapsLock(with: event.modifierFlags)
}
}
qemuVM.ioDelegate = self
super.enterLive()
resizeConsoleToolbarItem.isEnabled = false // disable item
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class VMDisplayQemuWindowController: VMDisplayWindowController {
}

override func enterLive() {
qemuVM.ioDelegate = self
startPauseToolbarItem.isEnabled = true
#if arch(x86_64)
if vmQemuConfig.useHypervisor {
Expand Down
163 changes: 52 additions & 111 deletions Platform/macOS/Display/VMDisplayTerminalWindowController.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright © 2020 osy. All rights reserved.
// Copyright © 2022 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -14,44 +14,19 @@
// limitations under the License.
//

import SwiftUI
import WebKit
import SwiftTerm

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

private enum JSCommand: String {
case sendInput = "UTMSendInput"
case debug = "UTMDebug"
case sendGesture = "UTMSendGesture"
case sendTerminalSize = "UTMSendTerminalSize"
}

class VMDisplayTerminalWindowController: VMDisplayQemuWindowController {
var webView: WKWebView!
private var columns: Int?
private var rows: Int?
private var terminalView: TerminalView!
private weak var vmSerialPort: CSPort?

override func windowDidLoad() {
let webConfig = WKWebViewConfiguration()
webConfig.userContentController.add(self, name: JSCommand.sendInput.rawValue)
webConfig.userContentController.add(self, name: JSCommand.debug.rawValue)
webConfig.userContentController.add(self, name: JSCommand.sendGesture.rawValue)
webConfig.userContentController.add(self, name: JSCommand.sendTerminalSize.rawValue)
webView = WKWebView(frame: displayView.bounds, configuration: webConfig)
webView.autoresizingMask = [.width, .height]
webView.setValue(false, forKey: "drawsBackground")
displayView.addSubview(webView)

// load terminal.html
guard let resourceURL = Bundle.main.resourceURL else {
showErrorAlert(NSLocalizedString("Cannot find bundle resources.", comment: "VMDisplayTerminalWindowController"))
logger.critical("Cannot find system default Metal device.")
return
}
let indexFile = resourceURL.appendingPathComponent("terminal.html")
webView.navigationDelegate = self
self.webView.loadFileURL(indexFile, allowingReadAccessTo: resourceURL)

terminalView = TerminalView(frame: displayView.bounds)
terminalView.terminalDelegate = self
terminalView.autoresizingMask = [.width, .height]
displayView.addSubview(terminalView)
super.windowDidLoad()
}

Expand All @@ -60,100 +35,66 @@ class VMDisplayTerminalWindowController: VMDisplayQemuWindowController {
captureMouseToolbarItem.isEnabled = false
}

// MARK: - Resizing console
override func resizeConsoleButtonPressed(_ sender: Any) {
guard let columns = self.columns else {
logger.error("Did not get columns from page")
return
}
guard let rows = self.rows else {
logger.error("Did not get rows from page")
return
}
let cols = terminalView.getTerminal().cols
let rows = terminalView.getTerminal().rows
let template = vmQemuConfig?.consoleResizeCommand ?? kVMDefaultResizeCmd
let cmd = template
.replacingOccurrences(of: "$COLS", with: String(columns))
.replacingOccurrences(of: "$COLS", with: String(cols))
.replacingOccurrences(of: "$ROWS", with: String(rows))
.replacingOccurrences(of: "\\n", with: "\n")
qemuVM.sendInput(cmd)
}
}

extension VMDisplayTerminalWindowController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
updateSettings()
vmSerialPort?.write(cmd.data(using: .nonLossyASCII)!)
}

func updateSettings() {
if let consoleTextColor = vmQemuConfig?.consoleTextColor,
let srgbConsoleTextColor = Color(hexString: consoleTextColor)?.cgColor?.sRGBhexString,
let consoleBackgroundColor = vmQemuConfig?.consoleBackgroundColor,
let srgbConsoleBackgroundColor = Color(hexString: consoleBackgroundColor)?.cgColor?.sRGBhexString {
webView.evaluateJavaScript("changeColor('\(srgbConsoleTextColor)', '\(srgbConsoleBackgroundColor)');") { (_, err) in
if let error = err {
logger.error("changeColor error: \(error)")
}
}
}
if let consoleFont = vmQemuConfig?.consoleFont {
let consoleFontSize = vmQemuConfig?.consoleFontSize?.intValue ?? 12
webView.evaluateJavaScript("changeFont('\(consoleFont)', \(consoleFontSize));") { (_, err) in
if let error = err {
logger.error("changeFont error: \(error)")
}
}
override func spiceDidCreateSerial(_ serial: CSPort) {
if vmSerialPort == nil {
vmSerialPort = serial
serial.delegate = self
}
if let cursorBlink = vmQemuConfig?.consoleCursorBlink {
webView.evaluateJavaScript("setCursorBlink(\(cursorBlink ? "true" : "false"));") { (_, err) in
if let error = err {
logger.error("setCursorBlink error: \(error)")
}
}
}

override func spiceDidDestroySerial(_ serial: CSPort) {
if vmSerialPort == serial {
serial.delegate = nil
vmSerialPort = nil
}
}
}

extension VMDisplayTerminalWindowController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let command = JSCommand.init(rawValue: message.name) else {
logger.error("Unknown command: \(message.name)")
return
}
switch command {
case .sendInput:
guard let body = message.body as? String else {
logger.error("Body is not of string type")
return
}
qemuVM.sendInput(body)
case .debug:
logger.debug("JS debug: \(message.body)")
case .sendGesture:
logger.debug("Ignoring gesture")
case .sendTerminalSize:
guard let body = message.body as? [NSNumber], body.count == 2 else {
logger.error("Body is not an array of two numbers")
return
}
columns = body[0].intValue
rows = body[1].intValue
extension VMDisplayTerminalWindowController: TerminalViewDelegate {
func sizeChanged(source: TerminalView, newCols: Int, newRows: Int) {
}

func setTerminalTitle(source: TerminalView, title: String) {
window!.subtitle = title
}

func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {
}

func send(source: TerminalView, data: ArraySlice<UInt8>) {
if let vmSerialPort = vmSerialPort {
vmSerialPort.write(Data(data))
}
}

func scrolled(source: TerminalView, position: Double) {
}
}

extension VMDisplayTerminalWindowController: UTMTerminalDelegate {
func terminal(_ terminal: UTMTerminal, didReceive data: Data) {
var dataString = "["
for i in data.indices {
dataString = dataString.appendingFormat("%u,", data[i])
}
dataString = dataString + "]"
let jsString = "writeData(new Uint8Array(\(dataString)));"
DispatchQueue.main.async {
self.webView.evaluateJavaScript(jsString) { (_, err) in
if let error = err {
logger.error("JS evaluation failed: \(error)")
}
extension VMDisplayTerminalWindowController: CSPortDelegate {
func portDidDisconect(_ port: CSPort) {
}

func port(_ port: CSPort, didError error: String) {
showErrorAlert(error)
}

func port(_ port: CSPort, didRecieveData data: Data) {
if let terminalView = terminalView {
let arr = [UInt8](data)[...]
DispatchQueue.main.async {
terminalView.feed(byteArray: arr)
}
}
}
Expand Down

0 comments on commit d9ecb1e

Please sign in to comment.