diff --git a/.gitignore b/.gitignore index 7eb1730ae..66494288c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ DerivedData .DS_Store *~ esctest/ +Package.resolved diff --git a/Sources/SwiftTerm/Apple/AppleTerminalView.swift b/Sources/SwiftTerm/Apple/AppleTerminalView.swift index f88ed92e3..750058eef 100644 --- a/Sources/SwiftTerm/Apple/AppleTerminalView.swift +++ b/Sources/SwiftTerm/Apple/AppleTerminalView.swift @@ -1934,4 +1934,4 @@ extension TerminalView { } #endif -#endif +#endif \ No newline at end of file diff --git a/Sources/SwiftTerm/Colors.swift b/Sources/SwiftTerm/Colors.swift index 7411bb524..cc84cd547 100644 --- a/Sources/SwiftTerm/Colors.swift +++ b/Sources/SwiftTerm/Colors.swift @@ -33,7 +33,7 @@ public class Color: Hashable { hasher.combine(blue) } - static let paleColors: [Color] = [ + public static let paleColors: [Color] = [ // dark colors Color (red8: 0x2e, green8: 0x34, blue8: 0x36), Color (red8: 0xcc, green8: 0x00, blue8: 0x00), @@ -55,7 +55,7 @@ public class Color: Hashable { Color (red8: 0xee, green8: 0xee, blue8: 0xec) ] - static let vgaColors: [Color] = [ + public static let vgaColors: [Color] = [ // dark colors Color (red8: 0, green8: 0, blue8: 0), Color (red8: 170, green8: 0, blue8: 0), @@ -75,7 +75,7 @@ public class Color: Hashable { Color (red8: 255, green8: 255, blue8: 255), ] - static let terminalAppColors: [Color] = [ + public static let terminalAppColors: [Color] = [ Color (red8: 0, green8: 0, blue8: 0), Color (red8: 194, green8: 54, blue8: 33), Color (red8: 37, green8: 188, blue8: 36), @@ -94,7 +94,7 @@ public class Color: Hashable { Color (red8: 233, green8: 235, blue8: 235), ] - static let xtermColors: [Color] = [ + public static let xtermColors: [Color] = [ Color (red8: 0, green8: 0, blue8: 0), Color (red8: 205, green8: 0, blue8: 0), Color (red8: 0, green8: 205, blue8: 0), @@ -113,7 +113,7 @@ public class Color: Hashable { Color (red8: 255, green8: 255, blue8: 255), ] - static let defaultInstalledColors: [Color] = [ + public static let defaultInstalledColors: [Color] = [ Color (red8: 0, green8: 0, blue8: 0), Color (red8: 153, green8: 0, blue8: 1), Color (red8: 0, green8: 166, blue8: 3), @@ -131,6 +131,135 @@ public class Color: Hashable { Color (red8: 0, green8: 229, blue8: 229), Color (red8: 229, green8: 229, blue8: 229), ] + + /// Dracula - popular vibrant dark theme + public static let draculaColors: [Color] = [ + // normal colors + Color (red8: 0x21, green8: 0x22, blue8: 0x2c), + Color (red8: 0xff, green8: 0x55, blue8: 0x55), + Color (red8: 0x50, green8: 0xfa, blue8: 0x7b), + Color (red8: 0xf1, green8: 0xfa, blue8: 0x8c), + Color (red8: 0xbd, green8: 0x93, blue8: 0xf9), + Color (red8: 0xff, green8: 0x79, blue8: 0xc6), + Color (red8: 0x8b, green8: 0xe9, blue8: 0xfd), + Color (red8: 0xf8, green8: 0xf8, blue8: 0xf2), + // bright colors + Color (red8: 0x62, green8: 0x72, blue8: 0xa4), + Color (red8: 0xff, green8: 0x6e, blue8: 0x6e), + Color (red8: 0x69, green8: 0xff, blue8: 0x94), + Color (red8: 0xff, green8: 0xff, blue8: 0xa5), + Color (red8: 0xd6, green8: 0xac, blue8: 0xff), + Color (red8: 0xff, green8: 0x92, blue8: 0xdf), + Color (red8: 0xa4, green8: 0xff, blue8: 0xff), + Color (red8: 0xff, green8: 0xff, blue8: 0xff), + ] + + /// Catppuccin Mocha - soothing pastel dark theme + public static let catppuccinMochaColors: [Color] = [ + // normal colors + Color (red8: 0x45, green8: 0x47, blue8: 0x5a), + Color (red8: 0xf3, green8: 0x8b, blue8: 0xa8), + Color (red8: 0xa6, green8: 0xe3, blue8: 0xa1), + Color (red8: 0xf9, green8: 0xe2, blue8: 0xaf), + Color (red8: 0x89, green8: 0xb4, blue8: 0xfa), + Color (red8: 0xf5, green8: 0xc2, blue8: 0xe7), + Color (red8: 0x94, green8: 0xe2, blue8: 0xd5), + Color (red8: 0xba, green8: 0xc2, blue8: 0xde), + // bright colors + Color (red8: 0x58, green8: 0x5b, blue8: 0x70), + Color (red8: 0xf3, green8: 0x8b, blue8: 0xa8), + Color (red8: 0xa6, green8: 0xe3, blue8: 0xa1), + Color (red8: 0xf9, green8: 0xe2, blue8: 0xaf), + Color (red8: 0x89, green8: 0xb4, blue8: 0xfa), + Color (red8: 0xf5, green8: 0xc2, blue8: 0xe7), + Color (red8: 0x94, green8: 0xe2, blue8: 0xd5), + Color (red8: 0xa6, green8: 0xad, blue8: 0xc8), + ] + + /// Gruvbox Dark - retro, warm, earthy tones + public static let gruvboxDarkColors: [Color] = [ + // normal colors + Color (red8: 0x28, green8: 0x28, blue8: 0x28), + Color (red8: 0xcc, green8: 0x24, blue8: 0x1d), + Color (red8: 0x98, green8: 0x97, blue8: 0x1a), + Color (red8: 0xd7, green8: 0x99, blue8: 0x21), + Color (red8: 0x45, green8: 0x85, blue8: 0x88), + Color (red8: 0xb1, green8: 0x62, blue8: 0x86), + Color (red8: 0x68, green8: 0x9d, blue8: 0x6a), + Color (red8: 0xa8, green8: 0x99, blue8: 0x84), + // bright colors + Color (red8: 0x92, green8: 0x83, blue8: 0x74), + Color (red8: 0xfb, green8: 0x49, blue8: 0x34), + Color (red8: 0xb8, green8: 0xbb, blue8: 0x26), + Color (red8: 0xfa, green8: 0xbd, blue8: 0x2f), + Color (red8: 0x83, green8: 0xa5, blue8: 0x98), + Color (red8: 0xd3, green8: 0x86, blue8: 0x9b), + Color (red8: 0x8e, green8: 0xc0, blue8: 0x7c), + Color (red8: 0xeb, green8: 0xdb, blue8: 0xb2), + ] + + /// Solarized Dark - Ethan Schoonover's CIELAB-designed palette + public static let solarizedDarkColors: [Color] = [ + // normal colors + Color (red8: 0x00, green8: 0x2b, blue8: 0x36), + Color (red8: 0xdc, green8: 0x32, blue8: 0x2f), + Color (red8: 0x85, green8: 0x99, blue8: 0x00), + Color (red8: 0xb5, green8: 0x89, blue8: 0x00), + Color (red8: 0x26, green8: 0x8b, blue8: 0xd2), + Color (red8: 0xd3, green8: 0x36, blue8: 0x82), + Color (red8: 0x2a, green8: 0xa1, blue8: 0x98), + Color (red8: 0xee, green8: 0xe8, blue8: 0xd5), + // bright colors + Color (red8: 0x07, green8: 0x36, blue8: 0x42), + Color (red8: 0xcb, green8: 0x4b, blue8: 0x16), + Color (red8: 0x58, green8: 0x6e, blue8: 0x75), + Color (red8: 0x65, green8: 0x7b, blue8: 0x83), + Color (red8: 0x83, green8: 0x94, blue8: 0x96), + Color (red8: 0x6c, green8: 0x71, blue8: 0xc4), + Color (red8: 0x93, green8: 0xa1, blue8: 0xa1), + Color (red8: 0xfd, green8: 0xf6, blue8: 0xe3), + ] + + /// All available color scheme names, suitable for populating a menu + public static let colorSchemeNames: [String] = [ + "Dracula", + "Catppuccin Mocha", + "Gruvbox Dark", + "Solarized Dark", + "Tango", + "VGA", + "Xterm", + "macOS Terminal", + "Default", + ] + + /// Returns the 16 ANSI base colors for the given color scheme name. + /// The name must be one of the values from ``colorSchemeNames``. + /// Returns `nil` if the name is not recognized. + public static func colorScheme(forName name: String) -> [Color] { + switch name { + case "Dracula": + return draculaColors + case "Catppuccin Mocha": + return catppuccinMochaColors + case "Gruvbox Dark": + return gruvboxDarkColors + case "Solarized Dark": + return solarizedDarkColors + case "Tango": + return paleColors + case "VGA": + return vgaColors + case "Xterm": + return xtermColors + case "macOS Terminal": + return terminalAppColors + case "Default": + return defaultInstalledColors + default: + return defaultInstalledColors + } + } static func setupDefaultAnsiColors (initialColors: [Color]) -> [Color] { diff --git a/Sources/SwiftTerm/Mac/MacExtensions.swift b/Sources/SwiftTerm/Mac/MacExtensions.swift index 90402c064..72d2d9220 100644 --- a/Sources/SwiftTerm/Mac/MacExtensions.swift +++ b/Sources/SwiftTerm/Mac/MacExtensions.swift @@ -26,6 +26,14 @@ extension NSColor { var red: CGFloat = 0.0, green: CGFloat = 0.0, blue: CGFloat = 0.0, alpha: CGFloat = 1.0 color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + // For transparent colors (like .clear used as background), the mathematical + // inverse would also be invisible. Return a visible color based on appearance. + if alpha < 0.1 { + let isDark = NSAppearance.currentDrawing().bestMatch(from: [.darkAqua, .aqua]) == .darkAqua + return isDark ? NSColor.white : NSColor.black + } + return NSColor(calibratedRed: 1.0 - red, green: 1.0 - green, blue: 1.0 - blue, alpha: alpha) } diff --git a/Sources/SwiftTerm/Mac/MacTerminalView.swift b/Sources/SwiftTerm/Mac/MacTerminalView.swift index ed4aac911..e0aa06152 100644 --- a/Sources/SwiftTerm/Mac/MacTerminalView.swift +++ b/Sources/SwiftTerm/Mac/MacTerminalView.swift @@ -307,10 +307,18 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations, if settingBg { return } settingBg = true _nativeBg = newValue + layer?.backgroundColor = newValue.cgColor terminal.backgroundColor = nativeBackgroundColor.getTerminalColor () settingBg = false } } + + override public var isOpaque: Bool { + if let bg = _nativeBg { + return bg.alphaComponent >= 1.0 + } + return true + } /// Controls weather to use high ansi colors, if false terminal will use bold text instead of high ansi colors public var useBrightColors: Bool = true @@ -393,13 +401,18 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations, func setupScroller() { - let scrollerFrame = getScrollerFrame() + let scrollerWidth = NSScroller.scrollerWidth(for: .regular, scrollerStyle: scrollerStyle) + // Use a tall-enough frame so NSScroller initializes as vertical (orientation is determined by frame aspect) + let initialFrame = bounds.height > scrollerWidth + ? getScrollerFrame() + : NSRect(x: 0, y: 0, width: scrollerWidth, height: scrollerWidth + 1) + if scroller == nil { - scroller = NSScroller(frame: scrollerFrame) + scroller = NSScroller(frame: initialFrame) } else { - scroller?.frame = scrollerFrame + scroller?.frame = initialFrame } - scroller.autoresizingMask = [.minXMargin, .height] + scroller.autoresizingMask = [] scroller.scrollerStyle = scrollerStyle scroller.knobProportion = 0.1 scroller.isEnabled = false @@ -1117,7 +1130,7 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations, { // find the selected range of text in the buffer and put in the clipboard let str = selection.getSelectedText() - + let clipboard = NSPasteboard.general clipboard.clearContents() clipboard.setString(str, forType: .string) @@ -1591,6 +1604,24 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations, public func iTermContent (source: Terminal, content: ArraySlice) { terminalDelegate?.iTermContent(source: self, content: content) } + + // added by SNG for contextual menu + open override func menu(for event: NSEvent) -> NSMenu? { + let menu = NSMenu() + + let copyItem = menu.addItem(withTitle: "Copy", action: #selector(copy(_:)), keyEquivalent: "") + copyItem.target = self + + let pasteItem = menu.addItem(withTitle: "Paste", action: #selector(paste(_:)), keyEquivalent: "") + pasteItem.target = self + + menu.addItem(.separator()) + + let selectAllItem = menu.addItem(withTitle: "Select All", action: #selector(selectAll(_:)), keyEquivalent: "") + selectAllItem.target = self + + return menu + } } diff --git a/Sources/SwiftTerm/Pty.swift b/Sources/SwiftTerm/Pty.swift index 0e18402f5..c73bf092d 100644 --- a/Sources/SwiftTerm/Pty.swift +++ b/Sources/SwiftTerm/Pty.swift @@ -56,6 +56,16 @@ public class PseudoTerminalHelpers { } } + /* Like String.withCString, but tolerates a nil string by passing a nil pointer to the body. */ + static func withOptionalCString( + _ string: String?, _ body: (UnsafePointer?) -> R + ) -> R { + if let string { + return string.withCString { body($0) } + } + return body(nil) + } + /** * This method both forks and executes the provided command under a Pseudo Terminal (pty) * - Parameter andExec: the name of the executable to run @@ -68,25 +78,37 @@ public class PseudoTerminalHelpers { public static func fork (andExec: String, args: [String], env: [String], currentDirectory: String? = nil, desiredWindowSize: inout winsize) -> (pid: pid_t, masterFd: Int32)? { var master: Int32 = 0 - - let pid = forkpty(&master, nil, nil, &desiredWindowSize) - if pid < 0 { - return nil - } - if pid == 0 { - if let currentDirectory { - _ = currentDirectory.withCString { p in - chdir(p) + + // After fork() in a multi-threaded process only the calling thread survives in the + // child; any lock another thread held at fork time (notably the Swift runtime's + // os_unfair_lock conformance cache) is left locked forever and aborts the child. + // Between fork and execve we may therefore only call async-signal-safe functions. + // Materialize every C string here, BEFORE forkpty(), so the child path touches no + // Swift runtime machinery (no String/Array bridging, allocation, or conformance + // lookups) and does nothing but chdir + execve. The buffers stay alive for the + // duration of these closures and the child inherits them at the same addresses. + return andExec.withCString { execPath in + withArrayOfCStrings(args) { pargs in + withArrayOfCStrings(env) { penv in + withOptionalCString(currentDirectory) { cwd in + let pid = forkpty(&master, nil, nil, &desiredWindowSize) + if pid < 0 { + return nil + } + if pid == 0 { + if let cwd { + _ = chdir(cwd) + } + _ = execve(execPath, pargs, penv) + // execve only returns on failure; exit without unwinding back + // into the (fork-unsafe) Swift runtime. + _exit(127) + } + return (pid, master) + } } } - - withArrayOfCStrings(args, { pargs in - withArrayOfCStrings(env, { penv in - let _ = execve(andExec, pargs, penv) - }) - }) } - return (pid, master) } /** diff --git a/TerminalApp/MacTerminal/ViewController.swift b/TerminalApp/MacTerminal/ViewController.swift index 77bcb94f7..d655b5376 100644 --- a/TerminalApp/MacTerminal/ViewController.swift +++ b/TerminalApp/MacTerminal/ViewController.swift @@ -117,6 +117,8 @@ class ViewController: NSViewController, LocalProcessTerminalViewDelegate, NSUser super.viewDidLoad() terminal = LocalProcessTerminalView(frame: view.frame) terminal.caretColor = .systemGreen + terminal.nativeBackgroundColor = .clear + terminal.nativeForegroundColor = .labelColor terminal.getTerminal().setCursorStyle(.steadyBlock) zoomGesture = NSMagnificationGestureRecognizer(target: self, action: #selector(zoomGestureHandler)) terminal.addGestureRecognizer(zoomGesture!)