Fix notification focus/read handling and add split surface control
This commit is contained in:
parent
bc074d20c1
commit
5acb4e47b1
6 changed files with 384 additions and 17 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import AppKit
|
||||
import CoreServices
|
||||
import UserNotifications
|
||||
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
|
|
@ -6,6 +7,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
|
||||
weak var tabManager: TabManager?
|
||||
weak var notificationStore: TerminalNotificationStore?
|
||||
private var workspaceObserver: NSObjectProtocol?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
|
@ -13,6 +15,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
registerLaunchServicesBundle()
|
||||
enforceSingleInstance()
|
||||
observeDuplicateLaunches()
|
||||
configureUserNotifications()
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +46,52 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
center.delegate = self
|
||||
}
|
||||
|
||||
private func registerLaunchServicesBundle() {
|
||||
let bundleURL = Bundle.main.bundleURL.standardizedFileURL
|
||||
let registerStatus = LSRegisterURL(bundleURL as CFURL, true)
|
||||
if registerStatus != noErr {
|
||||
NSLog("LaunchServices registration failed (status: \(registerStatus)) for \(bundleURL.path)")
|
||||
}
|
||||
}
|
||||
|
||||
private func enforceSingleInstance() {
|
||||
guard let bundleId = Bundle.main.bundleIdentifier else { return }
|
||||
let currentPid = ProcessInfo.processInfo.processIdentifier
|
||||
let currentURL = Bundle.main.bundleURL.standardizedFileURL
|
||||
|
||||
for app in NSRunningApplication.runningApplications(withBundleIdentifier: bundleId) {
|
||||
guard app.processIdentifier != currentPid else { continue }
|
||||
if let url = app.bundleURL?.standardizedFileURL, url == currentURL { continue }
|
||||
app.terminate()
|
||||
if !app.isTerminated {
|
||||
_ = app.forceTerminate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func observeDuplicateLaunches() {
|
||||
guard let bundleId = Bundle.main.bundleIdentifier else { return }
|
||||
let currentPid = ProcessInfo.processInfo.processIdentifier
|
||||
let currentURL = Bundle.main.bundleURL.standardizedFileURL
|
||||
|
||||
workspaceObserver = NSWorkspace.shared.notificationCenter.addObserver(
|
||||
forName: NSWorkspace.didLaunchApplicationNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] notification in
|
||||
guard self != nil else { return }
|
||||
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return }
|
||||
guard app.bundleIdentifier == bundleId, app.processIdentifier != currentPid else { return }
|
||||
if let url = app.bundleURL?.standardizedFileURL, url == currentURL { return }
|
||||
|
||||
app.terminate()
|
||||
if !app.isTerminated {
|
||||
_ = app.forceTerminate()
|
||||
}
|
||||
NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
|
||||
}
|
||||
}
|
||||
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
|
|
@ -55,7 +106,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
||||
) {
|
||||
completionHandler([.banner, .sound])
|
||||
completionHandler([.banner, .sound, .list])
|
||||
}
|
||||
|
||||
private func handleNotificationResponse(_ response: UNNotificationResponse) {
|
||||
|
|
@ -73,13 +124,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
switch response.actionIdentifier {
|
||||
case UNNotificationDefaultActionIdentifier, TerminalNotificationStore.actionShowIdentifier:
|
||||
DispatchQueue.main.async {
|
||||
if let notificationId = UUID(uuidString: response.notification.request.identifier) {
|
||||
self.notificationStore?.markRead(id: notificationId)
|
||||
} else if let notificationIdString = response.notification.request.content.userInfo["notificationId"] as? String,
|
||||
let notificationId = UUID(uuidString: notificationIdString) {
|
||||
self.notificationStore?.markRead(id: notificationId)
|
||||
}
|
||||
self.tabManager?.focusTab(tabId, surfaceId: surfaceId)
|
||||
self.markReadIfFocused(response: response, tabId: tabId, surfaceId: surfaceId)
|
||||
}
|
||||
case UNNotificationDismissActionIdentifier:
|
||||
DispatchQueue.main.async {
|
||||
|
|
@ -95,4 +141,28 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
}
|
||||
}
|
||||
|
||||
private func markReadIfFocused(response: UNNotificationResponse, tabId: UUID, surfaceId: UUID?) {
|
||||
let notificationId: UUID? = {
|
||||
if let id = UUID(uuidString: response.notification.request.identifier) {
|
||||
return id
|
||||
}
|
||||
if let idString = response.notification.request.content.userInfo["notificationId"] as? String,
|
||||
let id = UUID(uuidString: idString) {
|
||||
return id
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
guard let notificationId else { return }
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
guard let tabManager = self.tabManager else { return }
|
||||
guard tabManager.selectedTabId == tabId else { return }
|
||||
if let surfaceId {
|
||||
guard tabManager.focusedSurfaceId(for: tabId) == surfaceId else { return }
|
||||
}
|
||||
self.notificationStore?.markRead(id: notificationId)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,14 +64,6 @@ struct ContentView: View {
|
|||
}
|
||||
.onChange(of: tabManager.selectedTabId) { newValue in
|
||||
focusedTabId = newValue
|
||||
if let newValue {
|
||||
notificationStore.markRead(forTabId: newValue)
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
||||
if let selected = tabManager.selectedTabId {
|
||||
notificationStore.markRead(forTabId: selected)
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .ghosttyDidFocusTab)) { _ in
|
||||
sidebarSelection = .tabs
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ struct NotificationsPage: View {
|
|||
tabTitle: tabTitle(for: notification.tabId),
|
||||
onOpen: {
|
||||
tabManager.focusTab(notification.tabId, surfaceId: notification.surfaceId)
|
||||
notificationStore.markRead(id: notification.id)
|
||||
markReadIfFocused(notification)
|
||||
selection = .tabs
|
||||
},
|
||||
onClear: {
|
||||
|
|
@ -74,6 +74,16 @@ struct NotificationsPage: View {
|
|||
private func tabTitle(for tabId: UUID) -> String? {
|
||||
tabManager.tabs.first(where: { $0.id == tabId })?.title
|
||||
}
|
||||
|
||||
private func markReadIfFocused(_ notification: TerminalNotification) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
guard tabManager.selectedTabId == notification.tabId else { return }
|
||||
if let surfaceId = notification.surfaceId {
|
||||
guard tabManager.focusedSurfaceId(for: notification.tabId) == surfaceId else { return }
|
||||
}
|
||||
notificationStore.markRead(id: notification.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct NotificationRow: View {
|
||||
|
|
|
|||
|
|
@ -137,6 +137,15 @@ class TerminalController {
|
|||
case "new_tab":
|
||||
return newTab()
|
||||
|
||||
case "new_split":
|
||||
return newSplit(args)
|
||||
|
||||
case "list_surfaces":
|
||||
return listSurfaces(args)
|
||||
|
||||
case "focus_surface":
|
||||
return focusSurface(args)
|
||||
|
||||
case "close_tab":
|
||||
return closeTab(args)
|
||||
|
||||
|
|
@ -152,6 +161,12 @@ class TerminalController {
|
|||
case "send_key":
|
||||
return sendKey(args)
|
||||
|
||||
case "send_surface":
|
||||
return sendInputToSurface(args)
|
||||
|
||||
case "send_key_surface":
|
||||
return sendKeyToSurface(args)
|
||||
|
||||
case "help":
|
||||
return helpText()
|
||||
|
||||
|
|
@ -166,11 +181,16 @@ class TerminalController {
|
|||
ping - Check if server is running
|
||||
list_tabs - List all tabs with IDs
|
||||
new_tab - Create a new tab
|
||||
new_split <direction> - Split focused surface (left/right/up/down)
|
||||
list_surfaces [tab] - List surfaces for tab (current tab if omitted)
|
||||
focus_surface <id|idx> - Focus surface by ID or index (current tab)
|
||||
close_tab <id> - Close tab by ID
|
||||
select_tab <id|index> - Select tab by ID or index (0-based)
|
||||
current_tab - Get current tab ID
|
||||
send <text> - Send text to current tab
|
||||
send_key <key> - Send special key (ctrl-c, ctrl-d, enter, tab, escape)
|
||||
send_surface <id|idx> <text> - Send text to a surface in current tab
|
||||
send_key_surface <id|idx> <key> - Send special key to a surface in current tab
|
||||
help - Show this help
|
||||
"""
|
||||
}
|
||||
|
|
@ -200,6 +220,128 @@ class TerminalController {
|
|||
return "OK \(newTabId?.uuidString ?? "unknown")"
|
||||
}
|
||||
|
||||
private func newSplit(_ directionArg: String) -> String {
|
||||
guard let tabManager = tabManager else { return "ERROR: TabManager not available" }
|
||||
|
||||
let trimmed = directionArg.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard let direction = parseSplitDirection(trimmed) else {
|
||||
return "ERROR: Invalid direction. Use left, right, up, or down."
|
||||
}
|
||||
|
||||
var success = false
|
||||
DispatchQueue.main.sync {
|
||||
guard let tabId = tabManager.selectedTabId,
|
||||
let tab = tabManager.tabs.first(where: { $0.id == tabId }),
|
||||
let surfaceId = tab.focusedSurfaceId else {
|
||||
return
|
||||
}
|
||||
success = tabManager.newSplit(tabId: tabId, surfaceId: surfaceId, direction: direction)
|
||||
}
|
||||
return success ? "OK" : "ERROR: Failed to create split"
|
||||
}
|
||||
|
||||
private func listSurfaces(_ tabArg: String) -> String {
|
||||
guard let tabManager = tabManager else { return "ERROR: TabManager not available" }
|
||||
var result = ""
|
||||
DispatchQueue.main.sync {
|
||||
guard let tab = resolveTab(from: tabArg, tabManager: tabManager) else {
|
||||
result = "ERROR: Tab not found"
|
||||
return
|
||||
}
|
||||
let surfaces = tab.splitTree.root?.leaves() ?? []
|
||||
let focusedId = tab.focusedSurfaceId
|
||||
let lines = surfaces.enumerated().map { index, surface in
|
||||
let selected = surface.id == focusedId ? "*" : " "
|
||||
return "\(selected) \(index): \(surface.id.uuidString)"
|
||||
}
|
||||
result = lines.isEmpty ? "No surfaces" : lines.joined(separator: "\n")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func focusSurface(_ arg: String) -> String {
|
||||
guard let tabManager = tabManager else { return "ERROR: TabManager not available" }
|
||||
let trimmed = arg.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return "ERROR: Missing surface id or index" }
|
||||
|
||||
var success = false
|
||||
DispatchQueue.main.sync {
|
||||
guard let tabId = tabManager.selectedTabId,
|
||||
let tab = tabManager.tabs.first(where: { $0.id == tabId }) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let uuid = UUID(uuidString: trimmed),
|
||||
tab.surface(for: uuid) != nil {
|
||||
tabManager.focusSurface(tabId: tab.id, surfaceId: uuid)
|
||||
success = true
|
||||
return
|
||||
}
|
||||
|
||||
if let index = Int(trimmed), index >= 0 {
|
||||
let surfaces = tab.splitTree.root?.leaves() ?? []
|
||||
guard index < surfaces.count else { return }
|
||||
tabManager.focusSurface(tabId: tab.id, surfaceId: surfaces[index].id)
|
||||
success = true
|
||||
}
|
||||
}
|
||||
|
||||
return success ? "OK" : "ERROR: Surface not found"
|
||||
}
|
||||
|
||||
private func parseSplitDirection(_ value: String) -> SplitTree<TerminalSurface>.NewDirection? {
|
||||
switch value.lowercased() {
|
||||
case "left", "l":
|
||||
return .left
|
||||
case "right", "r":
|
||||
return .right
|
||||
case "up", "u":
|
||||
return .up
|
||||
case "down", "d":
|
||||
return .down
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveTab(from arg: String, tabManager: TabManager) -> Tab? {
|
||||
let trimmed = arg.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.isEmpty {
|
||||
guard let selected = tabManager.selectedTabId else { return nil }
|
||||
return tabManager.tabs.first(where: { $0.id == selected })
|
||||
}
|
||||
|
||||
if let uuid = UUID(uuidString: trimmed) {
|
||||
return tabManager.tabs.first(where: { $0.id == uuid })
|
||||
}
|
||||
|
||||
if let index = Int(trimmed), index >= 0, index < tabManager.tabs.count {
|
||||
return tabManager.tabs[index]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func resolveSurface(from arg: String, tabManager: TabManager) -> ghostty_surface_t? {
|
||||
guard let tabId = tabManager.selectedTabId,
|
||||
let tab = tabManager.tabs.first(where: { $0.id == tabId }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let uuid = UUID(uuidString: arg),
|
||||
let surface = tab.surface(for: uuid)?.surface {
|
||||
return surface
|
||||
}
|
||||
|
||||
if let index = Int(arg), index >= 0 {
|
||||
let surfaces = tab.splitTree.root?.leaves() ?? []
|
||||
guard index < surfaces.count else { return nil }
|
||||
return surfaces[index].surface
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func closeTab(_ tabId: String) -> String {
|
||||
guard let tabManager = tabManager else { return "ERROR: TabManager not available" }
|
||||
guard let uuid = UUID(uuidString: tabId) else { return "ERROR: Invalid tab ID" }
|
||||
|
|
@ -283,6 +425,41 @@ class TerminalController {
|
|||
return success ? "OK" : "ERROR: Failed to send input"
|
||||
}
|
||||
|
||||
private func sendInputToSurface(_ args: String) -> String {
|
||||
guard let tabManager = tabManager else { return "ERROR: TabManager not available" }
|
||||
let parts = args.split(separator: " ", maxSplits: 1).map(String.init)
|
||||
guard parts.count == 2 else { return "ERROR: Usage: send_surface <id|idx> <text>" }
|
||||
|
||||
let target = parts[0]
|
||||
let text = parts[1]
|
||||
|
||||
var success = false
|
||||
DispatchQueue.main.sync {
|
||||
guard let surface = resolveSurface(from: target, tabManager: tabManager) else { return }
|
||||
|
||||
let unescaped = text
|
||||
.replacingOccurrences(of: "\\n", with: "\r")
|
||||
.replacingOccurrences(of: "\\r", with: "\r")
|
||||
.replacingOccurrences(of: "\\t", with: "\t")
|
||||
|
||||
for char in unescaped {
|
||||
String(char).withCString { ptr in
|
||||
var keyEvent = ghostty_input_key_s()
|
||||
keyEvent.action = GHOSTTY_ACTION_PRESS
|
||||
keyEvent.keycode = 0
|
||||
keyEvent.mods = GHOSTTY_MODS_NONE
|
||||
keyEvent.consumed_mods = GHOSTTY_MODS_NONE
|
||||
keyEvent.text = ptr
|
||||
keyEvent.composing = false
|
||||
_ = ghostty_surface_key(surface, keyEvent)
|
||||
}
|
||||
}
|
||||
success = true
|
||||
}
|
||||
|
||||
return success ? "OK" : "ERROR: Failed to send input"
|
||||
}
|
||||
|
||||
private func sendKey(_ keyName: String) -> String {
|
||||
guard let tabManager = tabManager else { return "ERROR: TabManager not available" }
|
||||
|
||||
|
|
@ -364,6 +541,72 @@ class TerminalController {
|
|||
return success ? "OK" : "ERROR: Unknown key '\(keyName)'"
|
||||
}
|
||||
|
||||
private func sendKeyToSurface(_ args: String) -> String {
|
||||
guard let tabManager = tabManager else { return "ERROR: TabManager not available" }
|
||||
let parts = args.split(separator: " ", maxSplits: 1).map(String.init)
|
||||
guard parts.count == 2 else { return "ERROR: Usage: send_key_surface <id|idx> <key>" }
|
||||
|
||||
let target = parts[0]
|
||||
let keyName = parts[1]
|
||||
|
||||
var success = false
|
||||
DispatchQueue.main.sync {
|
||||
guard let surface = resolveSurface(from: target, tabManager: tabManager) else { return }
|
||||
|
||||
func sendKeyEvent(text: String, mods: ghostty_input_mods_e = GHOSTTY_MODS_NONE) {
|
||||
text.withCString { ptr in
|
||||
var keyEvent = ghostty_input_key_s()
|
||||
keyEvent.action = GHOSTTY_ACTION_PRESS
|
||||
keyEvent.keycode = 0
|
||||
keyEvent.mods = mods
|
||||
keyEvent.consumed_mods = GHOSTTY_MODS_NONE
|
||||
keyEvent.text = ptr
|
||||
keyEvent.composing = false
|
||||
_ = ghostty_surface_key(surface, keyEvent)
|
||||
}
|
||||
}
|
||||
|
||||
switch keyName.lowercased() {
|
||||
case "ctrl-c", "ctrl+c", "sigint":
|
||||
sendKeyEvent(text: "\u{03}")
|
||||
success = true
|
||||
case "ctrl-d", "ctrl+d", "eof":
|
||||
sendKeyEvent(text: "\u{04}")
|
||||
success = true
|
||||
case "ctrl-z", "ctrl+z", "sigtstp":
|
||||
sendKeyEvent(text: "\u{1A}")
|
||||
success = true
|
||||
case "ctrl-\\", "ctrl+\\", "sigquit":
|
||||
sendKeyEvent(text: "\u{1C}")
|
||||
success = true
|
||||
case "enter", "return":
|
||||
sendKeyEvent(text: "\r")
|
||||
success = true
|
||||
case "tab":
|
||||
sendKeyEvent(text: "\t")
|
||||
success = true
|
||||
case "escape", "esc":
|
||||
sendKeyEvent(text: "\u{1B}")
|
||||
success = true
|
||||
case "backspace":
|
||||
sendKeyEvent(text: "\u{7F}")
|
||||
success = true
|
||||
default:
|
||||
if keyName.lowercased().hasPrefix("ctrl-") || keyName.lowercased().hasPrefix("ctrl+") {
|
||||
let letter = keyName.dropFirst(5).lowercased()
|
||||
if letter.count == 1, let char = letter.first, char.isLetter {
|
||||
let ctrlCode = UInt8(char.asciiValue! - Character("a").asciiValue! + 1)
|
||||
let ctrlChar = String(UnicodeScalar(ctrlCode))
|
||||
sendKeyEvent(text: ctrlChar)
|
||||
success = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success ? "OK" : "ERROR: Failed to send key"
|
||||
}
|
||||
|
||||
deinit {
|
||||
stop()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ final class TerminalNotificationStore: ObservableObject {
|
|||
|
||||
func addNotification(tabId: UUID, surfaceId: UUID?, title: String, body: String) {
|
||||
let isActiveTab = AppDelegate.shared?.tabManager?.selectedTabId == tabId
|
||||
let shouldMarkRead = NSApp.isActive && (NSApp.keyWindow?.isKeyWindow ?? false) && isActiveTab
|
||||
let focusedSurfaceId = AppDelegate.shared?.tabManager?.focusedSurfaceId(for: tabId)
|
||||
let isFocusedSurface = surfaceId == nil || focusedSurfaceId == surfaceId
|
||||
let shouldMarkRead = NSApp.isActive && (NSApp.keyWindow?.isKeyWindow ?? false) && isActiveTab && isFocusedSurface
|
||||
let notification = TerminalNotification(
|
||||
id: UUID(),
|
||||
tabId: tabId,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ Usage:
|
|||
client.new_tab()
|
||||
client.list_tabs()
|
||||
client.select_tab(0)
|
||||
client.new_split("right")
|
||||
client.list_surfaces()
|
||||
client.focus_surface(0)
|
||||
|
||||
client.close()
|
||||
"""
|
||||
|
|
@ -125,6 +128,12 @@ class GhosttyTabs:
|
|||
return response[3:]
|
||||
raise GhosttyTabsError(response)
|
||||
|
||||
def new_split(self, direction: str) -> None:
|
||||
"""Create a split in the given direction (left/right/up/down)."""
|
||||
response = self._send_command(f"new_split {direction}")
|
||||
if not response.startswith("OK"):
|
||||
raise GhosttyTabsError(response)
|
||||
|
||||
def close_tab(self, tab_id: str) -> None:
|
||||
"""Close a tab by ID"""
|
||||
response = self._send_command(f"close_tab {tab_id}")
|
||||
|
|
@ -137,6 +146,34 @@ class GhosttyTabs:
|
|||
if not response.startswith("OK"):
|
||||
raise GhosttyTabsError(response)
|
||||
|
||||
def list_surfaces(self, tab: str | int | None = None) -> List[Tuple[int, str, bool]]:
|
||||
"""
|
||||
List surfaces for a tab. Returns list of (index, id, is_focused) tuples.
|
||||
If tab is None, uses the current tab.
|
||||
"""
|
||||
arg = "" if tab is None else str(tab)
|
||||
response = self._send_command(f"list_surfaces {arg}".rstrip())
|
||||
if response in ("No surfaces", "ERROR: Tab not found"):
|
||||
return []
|
||||
|
||||
surfaces = []
|
||||
for line in response.split("\n"):
|
||||
if not line.strip():
|
||||
continue
|
||||
selected = line.startswith("*")
|
||||
parts = line.lstrip("* ").split(" ", 1)
|
||||
if len(parts) >= 2:
|
||||
index = int(parts[0].rstrip(":"))
|
||||
surface_id = parts[1]
|
||||
surfaces.append((index, surface_id, selected))
|
||||
return surfaces
|
||||
|
||||
def focus_surface(self, surface: str | int) -> None:
|
||||
"""Focus a surface by ID or index in the current tab."""
|
||||
response = self._send_command(f"focus_surface {surface}")
|
||||
if not response.startswith("OK"):
|
||||
raise GhosttyTabsError(response)
|
||||
|
||||
def current_tab(self) -> str:
|
||||
"""Get the current tab's ID"""
|
||||
response = self._send_command("current_tab")
|
||||
|
|
@ -160,6 +197,13 @@ class GhosttyTabs:
|
|||
if not response.startswith("OK"):
|
||||
raise GhosttyTabsError(response)
|
||||
|
||||
def send_surface(self, surface: str | int, text: str) -> None:
|
||||
"""Send text to a specific surface by ID or index in the current tab."""
|
||||
escaped = text.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
|
||||
response = self._send_command(f"send_surface {surface} {escaped}")
|
||||
if not response.startswith("OK"):
|
||||
raise GhosttyTabsError(response)
|
||||
|
||||
def send_key(self, key: str) -> None:
|
||||
"""
|
||||
Send a special key to the current terminal.
|
||||
|
|
@ -173,6 +217,12 @@ class GhosttyTabs:
|
|||
if not response.startswith("OK"):
|
||||
raise GhosttyTabsError(response)
|
||||
|
||||
def send_key_surface(self, surface: str | int, key: str) -> None:
|
||||
"""Send a special key to a specific surface by ID or index in the current tab."""
|
||||
response = self._send_command(f"send_key_surface {surface} {key}")
|
||||
if not response.startswith("OK"):
|
||||
raise GhosttyTabsError(response)
|
||||
|
||||
def send_line(self, text: str) -> None:
|
||||
"""Send text followed by Enter"""
|
||||
self.send(text + "\\n")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue