Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions Sources/Terminal/TerminalSurface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,15 @@ class TerminalSurface: NSObject, LocalProcessTerminalViewDelegate {
name: .deckardScrollbackChanged, object: nil)
}

deinit {
// Backstop for surfaces released without terminate(): a still-installed
// monitor would outlive the surface (its closure retains the view) and
// permanently swallow the window's keystrokes.
if let monitor = keyEventMonitor {
NSEvent.removeMonitor(monitor)
}
}

/// Apply a color scheme to this terminal.
func applyColorScheme(_ scheme: TerminalColorScheme) {
scheme.apply(to: terminalView)
Expand Down Expand Up @@ -465,21 +474,39 @@ class TerminalSurface: NSObject, LocalProcessTerminalViewDelegate {
if let initialInput {
pendingInitialInput = initialInput
terminalView.handlesPasteShortcuts = false
// Overwriting keyEventMonitor would orphan the previous monitor
// inside AppKit — permanently swallowing every keystroke for the
// window — so remove any existing one first.
if let existing = keyEventMonitor {
NSEvent.removeMonitor(existing)
keyEventMonitor = nil
}
let view = terminalView
keyEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
let monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
// Swallow key events targeting our terminal view's window.
if event.window === view.window { return nil }
return event
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
keyEventMonitor = monitor
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self, monitor] in
// If the surface was deallocated, deinit removes the monitor.
guard let self else { return }
defer { self.terminalView.handlesPasteShortcuts = true }
// Always restore input handling and remove the monitor this
// call installed on every exit path — but only if it is still
// the active one (terminate() or a later startShell may have
// removed or replaced it already; removing a monitor twice is
// not allowed).
defer {
self.terminalView.handlesPasteShortcuts = true
if let current = self.keyEventMonitor,
let monitor,
(current as AnyObject) === (monitor as AnyObject) {
NSEvent.removeMonitor(current)
self.keyEventMonitor = nil
}
}
guard let input = self.pendingInitialInput else { return }
self.pendingInitialInput = nil
if let monitor = self.keyEventMonitor {
NSEvent.removeMonitor(monitor)
self.keyEventMonitor = nil
}
self.sendInput(input)
}
}
Expand All @@ -496,6 +523,7 @@ class TerminalSurface: NSObject, LocalProcessTerminalViewDelegate {
func terminate() {
guard !processExited else { return }
processExited = true
pendingInitialInput = nil
if let monitor = keyEventMonitor {
NSEvent.removeMonitor(monitor)
keyEventMonitor = nil
Expand Down
Loading