Add mascot demo. Update assets for readme

This commit is contained in:
2026-03-14 02:45:15 +11:00
parent cf3dba8fe4
commit 3d68f08e1d
13 changed files with 568 additions and 21 deletions

View File

@@ -12,14 +12,23 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
private var keyEventMonitor: Any?
private let backgroundColor = NSColor.black
private let configuredShellPath: String
private let launchDirectory: String
@Published var title: String = "shell"
@Published var isRunning: Bool = true
@Published var currentDirectory: String?
init(fontSize: CGFloat, theme: TerminalTheme, shellPath: String) {
init(
fontSize: CGFloat,
theme: TerminalTheme,
shellPath: String,
initialDirectory: String? = nil,
startImmediately: Bool = true
) {
terminalView = TerminalView(frame: NSRect(x: 0, y: 0, width: 600, height: 300))
configuredShellPath = shellPath
launchDirectory = Self.resolveInitialDirectory(initialDirectory)
currentDirectory = launchDirectory
super.init()
terminalView.terminalDelegate = self
@@ -29,7 +38,11 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
applyTheme(theme)
installCommandArrowMonitor()
startShell()
if startImmediately {
startShell()
} else {
isRunning = false
}
}
deinit {
@@ -52,12 +65,29 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
args: ["-l"],
environment: nil,
execName: loginExecName,
currentDirectory: NSHomeDirectory()
currentDirectory: launchDirectory
)
process = proc
title = shellName
}
private static func resolveInitialDirectory(_ directory: String?) -> String {
normalizedDirectory(directory) ?? NSHomeDirectory()
}
private static func normalizedDirectory(_ directory: String?) -> String? {
let trimmed = directory?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !trimmed.isEmpty else {
return nil
}
if let url = URL(string: trimmed), url.isFileURL {
return url.path(percentEncoded: false)
}
return (trimmed as NSString).expandingTildeInPath
}
private func resolveShell() -> String {
let custom = configuredShellPath.trimmingCharacters(in: .whitespacesAndNewlines)
if !custom.isEmpty && FileManager.default.isExecutableFile(atPath: custom) {
@@ -145,7 +175,8 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
}
func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {
currentDirectory = directory
guard let normalizedDirectory = Self.normalizedDirectory(directory) else { return }
currentDirectory = normalizedDirectory
}
func scrolled(source: TerminalView, position: Double) {}

View File

@@ -3,12 +3,27 @@ import Combine
@MainActor
protocol TerminalSessionFactoryType {
func makeSession(fontSize: CGFloat, theme: TerminalTheme, shellPath: String) -> TerminalSession
func makeSession(
fontSize: CGFloat,
theme: TerminalTheme,
shellPath: String,
initialDirectory: String?
) -> TerminalSession
}
struct LiveTerminalSessionFactory: TerminalSessionFactoryType {
func makeSession(fontSize: CGFloat, theme: TerminalTheme, shellPath: String) -> TerminalSession {
TerminalSession(fontSize: fontSize, theme: theme, shellPath: shellPath)
func makeSession(
fontSize: CGFloat,
theme: TerminalTheme,
shellPath: String,
initialDirectory: String?
) -> TerminalSession {
TerminalSession(
fontSize: fontSize,
theme: theme,
shellPath: shellPath,
initialDirectory: initialDirectory
)
}
}
@@ -90,7 +105,8 @@ final class WorkspaceController: ObservableObject {
let session = sessionFactory.makeSession(
fontSize: config.fontSize,
theme: config.theme,
shellPath: config.shellPath
shellPath: config.shellPath,
initialDirectory: activeTab?.currentDirectory
)
titleObservers[session.id] = session.$title

View File

@@ -313,7 +313,12 @@ private final class ScreenRegistryTestSettingsProvider: TerminalSessionConfigura
private struct ScreenRegistryUnusedTerminalSessionFactory: TerminalSessionFactoryType {
@MainActor
func makeSession(fontSize: CGFloat, theme: TerminalTheme, shellPath: String) -> TerminalSession {
func makeSession(
fontSize: CGFloat,
theme: TerminalTheme,
shellPath: String,
initialDirectory: String?
) -> TerminalSession {
fatalError("ScreenRegistryTests should not create live terminal sessions.")
}
}

View File

@@ -120,6 +120,64 @@ final class WorkspaceRegistryTests: XCTestCase {
}
}
@MainActor
final class WorkspaceControllerTests: XCTestCase {
func testNewTabUsesActiveTabCurrentDirectory() {
let factory = RecordingTerminalSessionFactory()
let controller = WorkspaceController(
summary: WorkspaceSummary(name: "Main"),
sessionFactory: factory,
settingsProvider: TestSettingsProvider(),
bootstrapDefaultTab: false
)
controller.newTab()
controller.activeTab?.currentDirectory = "/tmp/Raycast"
controller.newTab()
XCTAssertEqual(factory.requestedDirectories, [nil, "/tmp/Raycast"])
XCTAssertEqual(controller.activeTab?.currentDirectory, "/tmp/Raycast")
XCTAssertEqual(controller.tabs.count, 2)
XCTAssertEqual(controller.activeTabIndex, 1)
}
func testNewTabNormalizesCurrentDirectoryFileURL() {
let factory = RecordingTerminalSessionFactory()
let controller = WorkspaceController(
summary: WorkspaceSummary(name: "Main"),
sessionFactory: factory,
settingsProvider: TestSettingsProvider(),
bootstrapDefaultTab: false
)
let expectedPath = "/tmp/Raycast Folder"
controller.newTab()
controller.activeTab?.currentDirectory = URL(fileURLWithPath: expectedPath).absoluteString
controller.newTab()
XCTAssertEqual(controller.activeTab?.currentDirectory, expectedPath)
}
func testNewTabFallsBackToHomeDirectoryWhenWorkspaceHasNoTabs() {
let factory = RecordingTerminalSessionFactory()
let controller = WorkspaceController(
summary: WorkspaceSummary(name: "Main"),
sessionFactory: factory,
settingsProvider: TestSettingsProvider(),
bootstrapDefaultTab: false
)
controller.newTab()
XCTAssertEqual(factory.requestedDirectories, [nil])
XCTAssertEqual(controller.activeTab?.currentDirectory, NSHomeDirectory())
XCTAssertEqual(controller.tabs.count, 1)
XCTAssertEqual(controller.activeTabIndex, 0)
}
}
private final class InMemoryWorkspaceStore: WorkspaceStoreType {
var savedSummaries: [WorkspaceSummary] = []
@@ -138,9 +196,35 @@ private final class TestSettingsProvider: TerminalSessionConfigurationProviding
let terminalSizePresets = TerminalSizePresetStore.loadDefaults()
}
private final class RecordingTerminalSessionFactory: TerminalSessionFactoryType {
private(set) var requestedDirectories: [String?] = []
@MainActor
func makeSession(
fontSize: CGFloat,
theme: TerminalTheme,
shellPath: String,
initialDirectory: String?
) -> TerminalSession {
requestedDirectories.append(initialDirectory)
return TerminalSession(
fontSize: fontSize,
theme: theme,
shellPath: shellPath,
initialDirectory: initialDirectory,
startImmediately: false
)
}
}
private struct UnusedTerminalSessionFactory: TerminalSessionFactoryType {
@MainActor
func makeSession(fontSize: CGFloat, theme: TerminalTheme, shellPath: String) -> TerminalSession {
func makeSession(
fontSize: CGFloat,
theme: TerminalTheme,
shellPath: String,
initialDirectory: String?
) -> TerminalSession {
fatalError("WorkspaceRegistryTests should not create live terminal sessions.")
}
}