Skip to content

Commit

Permalink
Fix Terminal State Loss (#1843)
Browse files Browse the repository at this point in the history
* Fix Terminal State Loss

* Use Transparent Terminal Caret

* Fix Resizing Bug, Refactor Changes From Main, Pin SwiftTerm
  • Loading branch information
thecoolwinter authored Aug 25, 2024
1 parent df1f3b2 commit 0bf177f
Show file tree
Hide file tree
Showing 12 changed files with 632 additions and 291 deletions.
96 changes: 63 additions & 33 deletions CodeEdit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "a33fcca819dee4c816b1474e19017510b1d62b170c921187042e0675d3f4b0b3",
"originHash" : "c1c6a3fce844bb0e9fb04272ffab26747869319dec6715e2d5d6ab10df59932a",
"pins" : [
{
"identity" : "anycodable",
Expand All @@ -13,7 +13,7 @@
{
"identity" : "codeeditkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/CodeEditApp/CodeEditKit",
"location" : "https://github.com/CodeEditApp/CodeEditKit.git",
"state" : {
"revision" : "ad28213a968586abb0cb21a8a56a3587227895f1",
"version" : "0.1.2"
Expand Down Expand Up @@ -220,10 +220,9 @@
{
"identity" : "swiftterm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/migueldeicaza/SwiftTerm.git",
"location" : "https://github.com/migueldeicaza/SwiftTerm",
"state" : {
"revision" : "55e7cdbeb3f41c80cce7b8a29ce9d17e214b2e77",
"version" : "1.2.0"
"revision" : "384776a4e24d08833ac7c6b8c6f6c7490323c845"
}
},
{
Expand Down
19 changes: 19 additions & 0 deletions CodeEdit/Features/TerminalEmulator/Model/Shell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,23 @@ enum Shell: String, CaseIterable {
// Run the process
try process.run()
}

var defaultPath: String {
switch self {
case .bash:
"/bin/bash"
case .zsh:
"/bin/zsh"
}
}

/// Gets the default shell from the current user and returns the string of the shell path.
///
/// If getting the user's shell does not work, defaults to `zsh`,
static func autoDetectDefaultShell() -> String {
guard let currentUser = CurrentUser.getCurrentUser() else {
return Self.zsh.rawValue // macOS defaults to zsh
}
return currentUser.shell
}
}
43 changes: 43 additions & 0 deletions CodeEdit/Features/TerminalEmulator/Model/TerminalCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// TerminalCache.swift
// CodeEdit
//
// Created by Khan Winter on 7/27/24.
//

import Foundation
import SwiftTerm

/// Stores a mapping of ID -> terminal view for reusing terminal views.
/// This allows terminal views to continue to receive data even when not in the view hierarchy.
final class TerminalCache {
static let shared: TerminalCache = TerminalCache()

/// The cache of terminal views.
private var terminals: [UUID: CELocalProcessTerminalView]

private init() {
terminals = [:]
}

/// Get a cached terminal view.
/// - Parameter id: The ID of the terminal.
/// - Returns: The existing terminal, if it exists.
func getTerminalView(_ id: UUID) -> CELocalProcessTerminalView? {
terminals[id]
}

/// Store a terminal view for reuse.
/// - Parameters:
/// - id: The ID of the terminal.
/// - view: The view representing the terminal's contents.
func cacheTerminalView(for id: UUID, view: CELocalProcessTerminalView) {
terminals[id] = view
}

/// Remove any view associated with the terminal id.
/// - Parameter id: The ID of the terminal.
func removeCachedView(_ id: UUID) {
terminals[id] = nil
}
}
160 changes: 160 additions & 0 deletions CodeEdit/Features/TerminalEmulator/Views/CETerminalView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
//
// CETerminalView.swift
// CodeEdit
//
// Created by Khan Winter on 8/7/24.
//

import AppKit
import SwiftTerm
import Foundation

/// # Dev Note (please read)
///
/// This entire file is a nearly 1:1 copy of SwiftTerm's `LocalProcessTerminalView`. The exception being the use of
/// `CETerminalView` over `TerminalView`. This change was made to fix the terminal clearing when the view was given a
/// frame of `0`. This enables terminals to keep running in the background, and allows them to be removed and added
/// back into the hierarchy for use in the utility area.
///
/// If there is a bug here: **there probably isn't**. Look instead in ``TerminalEmulatorView``.

class CETerminalView: TerminalView {
override var frame: NSRect {
get {
return super.frame
}
set(newValue) {
if newValue != .zero {
super.frame = newValue
}
}
}
}

protocol CELocalProcessTerminalViewDelegate: AnyObject {
/// This method is invoked to notify that the terminal has been resized to the specified number of columns and rows
/// the user interface code might try to adjust the containing scroll view, or if it is a top level window, the
/// window itself
/// - Parameter source: the sending instance
/// - Parameter newCols: the new number of columns that should be shown
/// - Parameter newRow: the new number of rows that should be shown
func sizeChanged(source: CETerminalView, newCols: Int, newRows: Int)

/// This method is invoked when the title of the terminal window should be updated to the provided title
/// - Parameter source: the sending instance
/// - Parameter title: the desired title
func setTerminalTitle(source: CETerminalView, title: String)

/// Invoked when the OSC command 7 for "current directory has changed" command is sent
/// - Parameter source: the sending instance
/// - Parameter directory: the new working directory
func hostCurrentDirectoryUpdate (source: TerminalView, directory: String?)

/// This method will be invoked when the child process started by `startProcess` has terminated.
/// - Parameter source: the local process that terminated
/// - Parameter exitCode: the exit code returned by the process, or nil if this was an error caused during
/// the IO reading/writing
func processTerminated (source: TerminalView, exitCode: Int32?)
}

class CELocalProcessTerminalView: CETerminalView, TerminalViewDelegate, LocalProcessDelegate {
var process: LocalProcess!

override public init (frame: CGRect) {
super.init(frame: frame)
setup()
}

public required init? (coder: NSCoder) {
super.init(coder: coder)
setup()
}

func setup () {
terminalDelegate = self
process = LocalProcess(delegate: self)
}

/// The `processDelegate` is used to deliver messages and information relevant to the execution of the terminal.
public weak var processDelegate: CELocalProcessTerminalViewDelegate?

/// This method is invoked to notify the client of the new columsn and rows that have been set by the UI
public func sizeChanged(source: TerminalView, newCols: Int, newRows: Int) {
guard process.running else {
return
}
var size = getWindowSize()
_ = PseudoTerminalHelpers.setWinSize(masterPtyDescriptor: process.childfd, windowSize: &size)

processDelegate?.sizeChanged(source: self, newCols: newCols, newRows: newRows)
}

public func clipboardCopy(source: TerminalView, content: Data) {
if let str = String(bytes: content, encoding: .utf8) {
let pasteBoard = NSPasteboard.general
pasteBoard.clearContents()
pasteBoard.writeObjects([str as NSString])
}
}

public func rangeChanged(source: TerminalView, startY: Int, endY: Int) { }

/// Invoke this method to notify the processDelegate of the new title for the terminal window
public func setTerminalTitle(source: TerminalView, title: String) {
processDelegate?.setTerminalTitle(source: self, title: title)
}

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

/// This method is invoked when input from the user needs to be sent to the client
public func send(source: TerminalView, data: ArraySlice<UInt8>) {
process.send(data: data)
}

/// Use this method to toggle the logging of data coming from the host, or pass nil to stop
public func setHostLogging (directory: String?) {
process.setHostLogging(directory: directory)
}

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

/// Launches a child process inside a pseudo-terminal.
/// - Parameter executable: The executable to launch inside the pseudo terminal, defaults to /bin/bash
/// - Parameter args: an array of strings that is passed as the arguments to the underlying process
/// - Parameter environment: an array of environment variables to pass to the child process, if this is null,
/// this picks a good set of defaults from `Terminal.getEnvironmentVariables`.
/// - Parameter execName: If provided, this is used as the Unix argv[0] parameter,
/// otherwise, the executable is used as the args [0], this is used when
/// the intent is to set a different process name than the file that backs it.
public func startProcess(
executable: String = "/bin/bash",
args: [String] = [],
environment: [String]? = nil,
execName: String? = nil
) {
process.startProcess(executable: executable, args: args, environment: environment, execName: execName)
}

/// Implements the LocalProcessDelegate method.
public func processTerminated(_ source: LocalProcess, exitCode: Int32?) {
processDelegate?.processTerminated(source: self, exitCode: exitCode)
}

/// Implements the LocalProcessDelegate.dataReceived method
public func dataReceived(slice: ArraySlice<UInt8>) {
feed(byteArray: slice)
}

/// Implements the LocalProcessDelegate.getWindowSize method
public func getWindowSize() -> winsize {
let frame: CGRect = self.frame
return winsize(
ws_row: UInt16(getTerminal().rows),
ws_col: UInt16(getTerminal().cols),
ws_xpixel: UInt16(frame.width),
ws_ypixel: UInt16(frame.height)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,21 @@ import SwiftUI
import SwiftTerm

extension TerminalEmulatorView {
final class Coordinator: NSObject, LocalProcessTerminalViewDelegate {

@State private var url: URL

final class Coordinator: NSObject, CELocalProcessTerminalViewDelegate {
private let terminalID: UUID
public var onTitleChange: (_ title: String) -> Void

init(url: URL, onTitleChange: @escaping (_ title: String) -> Void) {
self._url = .init(wrappedValue: url)
init(terminalID: UUID, onTitleChange: @escaping (_ title: String) -> Void) {
self.terminalID = terminalID
self.onTitleChange = onTitleChange
super.init()
}

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

func sizeChanged(source: LocalProcessTerminalView, newCols: Int, newRows: Int) {}
func sizeChanged(source: CETerminalView, newCols: Int, newRows: Int) {}

func setTerminalTitle(source: LocalProcessTerminalView, title: String) {
func setTerminalTitle(source: CETerminalView, title: String) {
onTitleChange(title)
}

Expand All @@ -35,7 +33,7 @@ extension TerminalEmulatorView {
}
source.feed(text: "Exit code: \(exitCode)\n\r\n")
source.feed(text: "To open a new session, create a new terminal tab.")
TerminalEmulatorView.lastTerminal[url.path] = nil
TerminalCache.shared.removeCachedView(terminalID)
}
}
}
Loading

0 comments on commit 0bf177f

Please sign in to comment.