Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ DerivedData
.DS_Store
*~
esctest/
Package.resolved
2 changes: 1 addition & 1 deletion Sources/SwiftTerm/Apple/AppleTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1934,4 +1934,4 @@ extension TerminalView {
}
#endif

#endif
#endif
139 changes: 134 additions & 5 deletions Sources/SwiftTerm/Colors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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]
{
Expand Down
8 changes: 8 additions & 0 deletions Sources/SwiftTerm/Mac/MacExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
41 changes: 36 additions & 5 deletions Sources/SwiftTerm/Mac/MacTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1591,6 +1604,24 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,
public func iTermContent (source: Terminal, content: ArraySlice<UInt8>) {
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
}
}


Expand Down
54 changes: 38 additions & 16 deletions Sources/SwiftTerm/Pty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<R>(
_ string: String?, _ body: (UnsafePointer<CChar>?) -> 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
Expand All @@ -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)
}

/**
Expand Down
2 changes: 2 additions & 0 deletions TerminalApp/MacTerminal/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!)
Expand Down