@wangyaoshen/remux 0.3.8-dev.29e114b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +47 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +38 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
- package/.github/dependabot.yml +33 -0
- package/.github/workflows/ci.yml +65 -0
- package/.github/workflows/deploy.yml +65 -0
- package/.github/workflows/publish.yml +312 -0
- package/.github/workflows/release-please.yml +21 -0
- package/.gitmodules +3 -0
- package/.nvmrc +1 -0
- package/.release-please-manifest.json +3 -0
- package/CLAUDE.md +104 -0
- package/Dockerfile +23 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/apps/ios/Config/signing.xcconfig +4 -0
- package/apps/ios/Package.swift +26 -0
- package/apps/ios/Remux.xcodeproj/project.pbxproj +477 -0
- package/apps/ios/Remux.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/Contents.json +23 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_120x120.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_152x152.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_167x167.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_180x180.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_20x20.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_29x29.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_40x40.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_58x58.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_60x60.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_76x76.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_80x80.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_87x87.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/Contents.json +6 -0
- package/apps/ios/Sources/Remux/Extensions/FaceIDManager.swift +29 -0
- package/apps/ios/Sources/Remux/Extensions/InspectCache.swift +66 -0
- package/apps/ios/Sources/Remux/MainTabView.swift +32 -0
- package/apps/ios/Sources/Remux/Remux.entitlements +8 -0
- package/apps/ios/Sources/Remux/RemuxiOSApp.swift +14 -0
- package/apps/ios/Sources/Remux/RootView.swift +130 -0
- package/apps/ios/Sources/Remux/Views/Control/ControlView.swift +102 -0
- package/apps/ios/Sources/Remux/Views/Inspect/InspectView.swift +98 -0
- package/apps/ios/Sources/Remux/Views/Live/LiveTerminalView.swift +132 -0
- package/apps/ios/Sources/Remux/Views/Now/NowView.swift +173 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/ManualConnectView.swift +55 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/OnboardingView.swift +70 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/QRScannerView.swift +92 -0
- package/apps/ios/Sources/Remux/Views/Settings/MeView.swift +136 -0
- package/apps/macos/Package.swift +37 -0
- package/apps/macos/Resources/shell-integration/bash/bash-preexec.sh +382 -0
- package/apps/macos/Resources/shell-integration/bash/ghostty.bash +315 -0
- package/apps/macos/Resources/shell-integration/elvish/lib/ghostty-integration.elv +191 -0
- package/apps/macos/Resources/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +246 -0
- package/apps/macos/Resources/shell-integration/nushell/vendor/autoload/ghostty.nu +110 -0
- package/apps/macos/Resources/shell-integration/zsh/.zshenv +61 -0
- package/apps/macos/Resources/shell-integration/zsh/ghostty-integration +458 -0
- package/apps/macos/Resources/terminfo/67/ghostty +0 -0
- package/apps/macos/Resources/terminfo/78/xterm-ghostty +0 -0
- package/apps/macos/Sources/Remux/AppDelegate.swift +257 -0
- package/apps/macos/Sources/Remux/CrashReporter.swift +210 -0
- package/apps/macos/Sources/Remux/FinderIntegration.swift +117 -0
- package/apps/macos/Sources/Remux/GhosttyConfig.swift +311 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutAction.swift +115 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutSettingsView.swift +271 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/StoredShortcut.swift +149 -0
- package/apps/macos/Sources/Remux/MainContentView.swift +308 -0
- package/apps/macos/Sources/Remux/MenuBarManager.swift +275 -0
- package/apps/macos/Sources/Remux/NotificationManager.swift +145 -0
- package/apps/macos/Sources/Remux/PortScanner.swift +152 -0
- package/apps/macos/Sources/Remux/RemuxApp.swift +13 -0
- package/apps/macos/Sources/Remux/SSHDetector.swift +151 -0
- package/apps/macos/Sources/Remux/SessionPersistence.swift +226 -0
- package/apps/macos/Sources/Remux/SocketController.swift +258 -0
- package/apps/macos/Sources/Remux/UpdateChecker.swift +152 -0
- package/apps/macos/Sources/Remux/Views/CommandPalette.swift +198 -0
- package/apps/macos/Sources/Remux/Views/ConnectionView.swift +84 -0
- package/apps/macos/Sources/Remux/Views/InspectView.swift +127 -0
- package/apps/macos/Sources/Remux/Views/SettingsView.swift +77 -0
- package/apps/macos/Sources/Remux/Views/Sidebar/SidebarView.swift +410 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/BrowserPanel.swift +193 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/MarkdownPanel.swift +277 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/PanelProtocol.swift +14 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitNode.swift +149 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitView.swift +234 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/TerminalPanel.swift +26 -0
- package/apps/macos/Sources/Remux/Views/TabBarView.swift +94 -0
- package/apps/macos/Sources/Remux/Views/Terminal/ClipboardHelper.swift +101 -0
- package/apps/macos/Sources/Remux/Views/Terminal/CopyModeOverlay.swift +325 -0
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeTerminalView.swift +39 -0
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeView.swift +559 -0
- package/apps/macos/Sources/Remux/Views/Terminal/SurfaceSearchOverlay.swift +109 -0
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalContainerView.swift +95 -0
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalRelay.swift +117 -0
- package/build.mjs +33 -0
- package/native/android/DecodeGoldenPayloads.kt +487 -0
- package/native/android/ProtocolModels.kt +188 -0
- package/native/ios/DecodeGoldenPayloads.swift +711 -0
- package/native/ios/ProtocolModels.swift +200 -0
- package/package.json +45 -0
- package/packages/RemuxKit/Package.swift +27 -0
- package/packages/RemuxKit/Sources/RemuxKit/Device/DeviceManager.swift +27 -0
- package/packages/RemuxKit/Sources/RemuxKit/Models/ProtocolModels.swift +206 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/MessageRouter.swift +108 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/RemuxConnection.swift +395 -0
- package/packages/RemuxKit/Sources/RemuxKit/State/RemuxState.swift +188 -0
- package/packages/RemuxKit/Sources/RemuxKit/Storage/KeychainStore.swift +142 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyBridge.swift +145 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyTerminalView.swift +35 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/Resources/ghostty-terminal.html +91 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ConnectionIntegrationTest.swift +74 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/KeychainStoreTests.swift +81 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ProtocolModelsTests.swift +179 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/RemuxStateTests.swift +62 -0
- package/playwright.config.ts +17 -0
- package/pnpm-lock.yaml +1588 -0
- package/pty-daemon.js +303 -0
- package/release-please-config.json +14 -0
- package/scripts/auto-deploy.sh +46 -0
- package/scripts/build-dmg.sh +121 -0
- package/scripts/build-ghostty-kit.sh +43 -0
- package/scripts/check-active-terminology.mjs +132 -0
- package/scripts/setup-ci-secrets.sh +80 -0
- package/scripts/sync-ghostty-web.sh +28 -0
- package/scripts/upload-testflight.sh +100 -0
- package/server.js +7074 -0
- package/src/adapters/agent-events.ts +246 -0
- package/src/adapters/claude-code.ts +158 -0
- package/src/adapters/codex.ts +210 -0
- package/src/adapters/generic-shell.ts +58 -0
- package/src/adapters/index.ts +15 -0
- package/src/adapters/registry.ts +99 -0
- package/src/adapters/types.ts +41 -0
- package/src/auth.ts +174 -0
- package/src/e2ee.ts +236 -0
- package/src/git-service.ts +168 -0
- package/src/message-buffer.ts +137 -0
- package/src/pty-daemon.ts +357 -0
- package/src/push.ts +127 -0
- package/src/renderers.ts +455 -0
- package/src/server.ts +2407 -0
- package/src/service.ts +226 -0
- package/src/session.ts +978 -0
- package/src/store.ts +1422 -0
- package/src/team.ts +123 -0
- package/src/tunnel.ts +126 -0
- package/src/types.d.ts +50 -0
- package/src/vt-tracker.ts +188 -0
- package/src/workspace-head.ts +144 -0
- package/src/workspace.ts +153 -0
- package/src/ws-handler.ts +1526 -0
- package/start.ps1 +83 -0
- package/tests/adapters.test.js +171 -0
- package/tests/auth.test.js +243 -0
- package/tests/codex-adapter.test.js +535 -0
- package/tests/durable-stream.test.js +153 -0
- package/tests/e2e/app.spec.js +530 -0
- package/tests/e2ee.test.js +325 -0
- package/tests/message-buffer.test.js +245 -0
- package/tests/message-routing.test.js +305 -0
- package/tests/pty-daemon.test.js +346 -0
- package/tests/push.test.js +281 -0
- package/tests/renderers.test.js +391 -0
- package/tests/search-shell.test.js +499 -0
- package/tests/server.test.js +882 -0
- package/tests/service.test.js +267 -0
- package/tests/store.test.js +369 -0
- package/tests/tunnel.test.js +67 -0
- package/tests/workspace-head.test.js +116 -0
- package/tests/workspace.test.js +417 -0
- package/tsconfig.backend.json +11 -0
- package/tsconfig.json +15 -0
- package/tui/client/client_test.go +125 -0
- package/tui/client/connection.go +342 -0
- package/tui/client/host_manager.go +141 -0
- package/tui/config/cache.go +81 -0
- package/tui/config/config.go +53 -0
- package/tui/config/config_test.go +89 -0
- package/tui/go.mod +32 -0
- package/tui/go.sum +50 -0
- package/tui/main.go +261 -0
- package/tui/tests/integration_test.go +283 -0
- package/tui/ui/model.go +310 -0
- package/vitest.config.js +10 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import GhosttyKit
|
|
3
|
+
import UniformTypeIdentifiers
|
|
4
|
+
|
|
5
|
+
/// NSView subclass wrapping libghostty's native Metal terminal renderer.
|
|
6
|
+
/// Adapted for ghostty v1.3.1 API: uses `command` field to spawn a relay
|
|
7
|
+
/// process (`nc -U <socket>`) that bridges Unix socket <-> PTY stdio.
|
|
8
|
+
///
|
|
9
|
+
/// Data flow:
|
|
10
|
+
/// Remote PTY -> WebSocket -> TerminalRelay.writeToTerminal() -> socket -> nc stdout -> ghostty renders
|
|
11
|
+
/// User types -> ghostty -> nc stdin -> socket -> TerminalRelay.onDataFromClient -> WebSocket -> remote PTY
|
|
12
|
+
///
|
|
13
|
+
/// Supports NSTextInputClient for CJK IME and NSDraggingDestination for file drops.
|
|
14
|
+
///
|
|
15
|
+
/// Architecture ref: Calyx, Kytos (libghostty-based terminal apps)
|
|
16
|
+
/// IME ref: ghostty-org/ghostty macOS TerminalView NSTextInputClient
|
|
17
|
+
@MainActor
|
|
18
|
+
final class GhosttyNativeView: NSView, @preconcurrency NSTextInputClient {
|
|
19
|
+
|
|
20
|
+
// MARK: - Callbacks
|
|
21
|
+
|
|
22
|
+
var onResize: ((Int, Int) -> Void)?
|
|
23
|
+
var onBell: (() -> Void)?
|
|
24
|
+
var onTitle: ((String) -> Void)?
|
|
25
|
+
|
|
26
|
+
// Search callbacks
|
|
27
|
+
var onSearchStart: ((String?) -> Void)?
|
|
28
|
+
var onSearchEnd: (() -> Void)?
|
|
29
|
+
var onSearchTotal: ((Int) -> Void)?
|
|
30
|
+
var onSearchSelected: ((Int) -> Void)?
|
|
31
|
+
|
|
32
|
+
// MARK: - Ghostty state (nonisolated for deinit cleanup)
|
|
33
|
+
|
|
34
|
+
nonisolated(unsafe) private var ghosttyApp: ghostty_app_t?
|
|
35
|
+
nonisolated(unsafe) private var surface: ghostty_surface_t?
|
|
36
|
+
|
|
37
|
+
/// The relay command spawned by ghostty as its "shell" process.
|
|
38
|
+
private(set) var relayCommand: String?
|
|
39
|
+
|
|
40
|
+
// MARK: - IME composing state
|
|
41
|
+
|
|
42
|
+
/// Whether the input method is actively composing (marked text present).
|
|
43
|
+
private var isComposing: Bool = false
|
|
44
|
+
|
|
45
|
+
/// The current marked text from the input method.
|
|
46
|
+
private var imeMarkedText: NSMutableAttributedString = NSMutableAttributedString()
|
|
47
|
+
|
|
48
|
+
/// The selected range within the marked text.
|
|
49
|
+
private var imeSelectedRange: NSRange = NSRange(location: NSNotFound, length: 0)
|
|
50
|
+
|
|
51
|
+
// MARK: - File drop state
|
|
52
|
+
|
|
53
|
+
/// Whether a drag is currently hovering over the view.
|
|
54
|
+
private var isDragHighlighted: Bool = false {
|
|
55
|
+
didSet {
|
|
56
|
+
layer?.borderWidth = isDragHighlighted ? 2 : 0
|
|
57
|
+
layer?.borderColor = isDragHighlighted
|
|
58
|
+
? NSColor.controlAccentColor.cgColor
|
|
59
|
+
: nil
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// MARK: - Init
|
|
64
|
+
|
|
65
|
+
/// Create the view with a relay socket path. Ghostty will spawn `nc -U <socketPath>`.
|
|
66
|
+
init(frame frameRect: NSRect, socketPath: String) {
|
|
67
|
+
self.relayCommand = "nc -U \(socketPath)"
|
|
68
|
+
super.init(frame: frameRect)
|
|
69
|
+
setupView()
|
|
70
|
+
initGhostty()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override init(frame frameRect: NSRect) {
|
|
74
|
+
super.init(frame: frameRect)
|
|
75
|
+
setupView()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
required init?(coder: NSCoder) {
|
|
79
|
+
fatalError("init(coder:) not supported")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
deinit {
|
|
83
|
+
if let surface { ghostty_surface_free(surface) }
|
|
84
|
+
if let ghosttyApp { ghostty_app_free(ghosttyApp) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private func setupView() {
|
|
88
|
+
wantsLayer = true
|
|
89
|
+
layer?.isOpaque = true
|
|
90
|
+
layer?.backgroundColor = NSColor.black.cgColor
|
|
91
|
+
|
|
92
|
+
// Register for file drag-and-drop
|
|
93
|
+
registerForDraggedTypes([.fileURL])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// MARK: - Ghostty initialization
|
|
97
|
+
|
|
98
|
+
private func initGhostty() {
|
|
99
|
+
ghostty_init(0, nil)
|
|
100
|
+
|
|
101
|
+
let config = ghostty_config_new()!
|
|
102
|
+
ghostty_config_load_default_files(config)
|
|
103
|
+
ghostty_config_finalize(config)
|
|
104
|
+
|
|
105
|
+
// Runtime callbacks
|
|
106
|
+
var rtConfig = ghostty_runtime_config_s()
|
|
107
|
+
rtConfig.userdata = Unmanaged.passUnretained(self).toOpaque()
|
|
108
|
+
rtConfig.supports_selection_clipboard = true
|
|
109
|
+
|
|
110
|
+
rtConfig.wakeup_cb = { ud in
|
|
111
|
+
guard let ud else { return }
|
|
112
|
+
let view = Unmanaged<GhosttyNativeView>.fromOpaque(ud).takeUnretainedValue()
|
|
113
|
+
DispatchQueue.main.async {
|
|
114
|
+
view.needsDisplay = true
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
rtConfig.action_cb = { app, target, action in
|
|
119
|
+
guard let app else { return false }
|
|
120
|
+
guard let ud = ghostty_app_userdata(app) else { return false }
|
|
121
|
+
let view = Unmanaged<GhosttyNativeView>.fromOpaque(ud).takeUnretainedValue()
|
|
122
|
+
return GhosttyNativeView.handleAction(view: view, target: target, action: action)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
rtConfig.read_clipboard_cb = { ud, clipboard, state in
|
|
126
|
+
guard let ud else { return false }
|
|
127
|
+
let view = Unmanaged<GhosttyNativeView>.fromOpaque(ud).takeUnretainedValue()
|
|
128
|
+
guard let surface = view.surface else { return false }
|
|
129
|
+
let pb = NSPasteboard.general
|
|
130
|
+
if let str = pb.string(forType: .string) {
|
|
131
|
+
str.withCString { ptr in
|
|
132
|
+
ghostty_surface_complete_clipboard_request(surface, ptr, state, true)
|
|
133
|
+
}
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
rtConfig.confirm_read_clipboard_cb = { ud, content, state, req in
|
|
140
|
+
// Auto-confirm clipboard reads for simplicity
|
|
141
|
+
guard let ud else { return }
|
|
142
|
+
let view = Unmanaged<GhosttyNativeView>.fromOpaque(ud).takeUnretainedValue()
|
|
143
|
+
guard let surface = view.surface, let content else { return }
|
|
144
|
+
ghostty_surface_complete_clipboard_request(surface, content, state, true)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
rtConfig.write_clipboard_cb = { _, clipboard, content, count, _ in
|
|
148
|
+
guard let content, count > 0, let data = content.pointee.data else { return }
|
|
149
|
+
let str = String(cString: data)
|
|
150
|
+
let pb: NSPasteboard
|
|
151
|
+
if clipboard == GHOSTTY_CLIPBOARD_SELECTION {
|
|
152
|
+
// Selection clipboard: use a named pasteboard
|
|
153
|
+
pb = NSPasteboard(name: .init("org.remux.selection"))
|
|
154
|
+
} else {
|
|
155
|
+
pb = NSPasteboard.general
|
|
156
|
+
}
|
|
157
|
+
pb.clearContents()
|
|
158
|
+
pb.setString(str, forType: .string)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
rtConfig.close_surface_cb = { _, _ in }
|
|
162
|
+
|
|
163
|
+
ghosttyApp = ghostty_app_new(&rtConfig, config)
|
|
164
|
+
ghostty_config_free(config)
|
|
165
|
+
guard let ghosttyApp else { return }
|
|
166
|
+
|
|
167
|
+
// Surface config — relay command as the "shell"
|
|
168
|
+
var surfaceConfig = ghostty_surface_config_new()
|
|
169
|
+
surfaceConfig.platform_tag = GHOSTTY_PLATFORM_MACOS
|
|
170
|
+
surfaceConfig.platform = ghostty_platform_u(
|
|
171
|
+
macos: ghostty_platform_macos_s(
|
|
172
|
+
nsview: Unmanaged.passUnretained(self).toOpaque()
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
surfaceConfig.userdata = Unmanaged.passUnretained(self).toOpaque()
|
|
176
|
+
|
|
177
|
+
if let screen = NSScreen.main {
|
|
178
|
+
surfaceConfig.scale_factor = screen.backingScaleFactor
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Set relay command: nc connects to the Unix socket and pipes stdio
|
|
182
|
+
if let cmd = relayCommand {
|
|
183
|
+
cmd.withCString { ptr in
|
|
184
|
+
surfaceConfig.command = ptr
|
|
185
|
+
surface = ghostty_surface_new(ghosttyApp, &surfaceConfig)
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
surface = ghostty_surface_new(ghosttyApp, &surfaceConfig)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// MARK: - Action handler (called from C callback context)
|
|
193
|
+
|
|
194
|
+
/// Handle ghostty actions. This is a static method so the C function pointer
|
|
195
|
+
/// (action_cb) can dispatch to it via Unmanaged userdata.
|
|
196
|
+
nonisolated private static func handleAction(
|
|
197
|
+
view: GhosttyNativeView,
|
|
198
|
+
target: ghostty_target_s,
|
|
199
|
+
action: ghostty_action_s
|
|
200
|
+
) -> Bool {
|
|
201
|
+
// All UI work must happen on MainActor
|
|
202
|
+
DispatchQueue.main.async { @MainActor in
|
|
203
|
+
switch action.tag {
|
|
204
|
+
case GHOSTTY_ACTION_RING_BELL:
|
|
205
|
+
view.onBell?()
|
|
206
|
+
|
|
207
|
+
case GHOSTTY_ACTION_SET_TITLE:
|
|
208
|
+
if let titlePtr = action.action.set_title.title {
|
|
209
|
+
let title = String(cString: titlePtr)
|
|
210
|
+
view.onTitle?(title)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case GHOSTTY_ACTION_START_SEARCH:
|
|
214
|
+
var needle: String? = nil
|
|
215
|
+
if let needlePtr = action.action.start_search.needle {
|
|
216
|
+
needle = String(cString: needlePtr)
|
|
217
|
+
}
|
|
218
|
+
view.onSearchStart?(needle)
|
|
219
|
+
|
|
220
|
+
case GHOSTTY_ACTION_END_SEARCH:
|
|
221
|
+
view.onSearchEnd?()
|
|
222
|
+
|
|
223
|
+
case GHOSTTY_ACTION_SEARCH_TOTAL:
|
|
224
|
+
let total = Int(action.action.search_total.total)
|
|
225
|
+
view.onSearchTotal?(total)
|
|
226
|
+
|
|
227
|
+
case GHOSTTY_ACTION_SEARCH_SELECTED:
|
|
228
|
+
let selected = Int(action.action.search_selected.selected)
|
|
229
|
+
view.onSearchSelected?(selected)
|
|
230
|
+
|
|
231
|
+
case GHOSTTY_ACTION_DESKTOP_NOTIFICATION:
|
|
232
|
+
// Could forward to NotificationManager if desired
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
default:
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return true
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// MARK: - Search methods
|
|
243
|
+
|
|
244
|
+
/// Navigate to the next search match.
|
|
245
|
+
func searchForward() {
|
|
246
|
+
guard let surface else { return }
|
|
247
|
+
let action = "search_forward"
|
|
248
|
+
_ = ghostty_surface_binding_action(surface, action, UInt(action.utf8.count))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// Navigate to the previous search match.
|
|
252
|
+
func searchBackward() {
|
|
253
|
+
guard let surface else { return }
|
|
254
|
+
let action = "search_backward"
|
|
255
|
+
_ = ghostty_surface_binding_action(surface, action, UInt(action.utf8.count))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// Send text input directly to the ghostty surface (used for paste).
|
|
259
|
+
func sendText(_ text: String) {
|
|
260
|
+
guard let surface else { return }
|
|
261
|
+
ghostty_surface_text(surface, text, UInt(text.utf8.count))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// MARK: - View lifecycle
|
|
265
|
+
|
|
266
|
+
override var acceptsFirstResponder: Bool { true }
|
|
267
|
+
override var isFlipped: Bool { true }
|
|
268
|
+
|
|
269
|
+
override func viewDidMoveToWindow() {
|
|
270
|
+
super.viewDidMoveToWindow()
|
|
271
|
+
guard let surface else { return }
|
|
272
|
+
if let screen = window?.screen {
|
|
273
|
+
ghostty_surface_set_content_scale(surface, screen.backingScaleFactor, screen.backingScaleFactor)
|
|
274
|
+
}
|
|
275
|
+
ghostty_surface_set_focus(surface, window != nil)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
override func setFrameSize(_ newSize: NSSize) {
|
|
279
|
+
super.setFrameSize(newSize)
|
|
280
|
+
guard let surface, newSize.width > 0, newSize.height > 0 else { return }
|
|
281
|
+
ghostty_surface_set_size(surface, UInt32(newSize.width), UInt32(newSize.height))
|
|
282
|
+
let termSize = ghostty_surface_size(surface)
|
|
283
|
+
onResize?(Int(termSize.columns), Int(termSize.rows))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
override func draw(_ dirtyRect: NSRect) {
|
|
287
|
+
surface.flatMap { ghostty_surface_draw($0) }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// MARK: - Keyboard input with IME support
|
|
291
|
+
|
|
292
|
+
override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
|
293
|
+
// Do not intercept key equivalents while composing
|
|
294
|
+
if isComposing { return false }
|
|
295
|
+
|
|
296
|
+
// Intercept Cmd+V for enhanced paste
|
|
297
|
+
if event.modifierFlags.contains(.command),
|
|
298
|
+
event.charactersIgnoringModifiers == "v" {
|
|
299
|
+
handlePaste()
|
|
300
|
+
return true
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Intercept Cmd+F for search
|
|
304
|
+
if event.modifierFlags.contains(.command),
|
|
305
|
+
event.charactersIgnoringModifiers == "f" {
|
|
306
|
+
onSearchStart?(nil)
|
|
307
|
+
return true
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return super.performKeyEquivalent(with: event)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
override func keyDown(with event: NSEvent) {
|
|
314
|
+
// Let the input method system handle the event first (for CJK IME).
|
|
315
|
+
// inputContext?.handleEvent() will call back into NSTextInputClient methods:
|
|
316
|
+
// setMarkedText (composing), insertText (committed), etc.
|
|
317
|
+
if inputContext?.handleEvent(event) == true {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// If the input method did not handle it, send directly to ghostty
|
|
322
|
+
guard let surface else { return }
|
|
323
|
+
var key = ghostty_input_key_s()
|
|
324
|
+
key.action = GHOSTTY_ACTION_PRESS
|
|
325
|
+
key.keycode = UInt32(event.keyCode)
|
|
326
|
+
key.composing = false
|
|
327
|
+
if event.isARepeat { key.action = GHOSTTY_ACTION_REPEAT }
|
|
328
|
+
_ = ghostty_surface_key(surface, key)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
override func keyUp(with event: NSEvent) {
|
|
332
|
+
guard let surface else { return }
|
|
333
|
+
var key = ghostty_input_key_s()
|
|
334
|
+
key.action = GHOSTTY_ACTION_RELEASE
|
|
335
|
+
key.keycode = UInt32(event.keyCode)
|
|
336
|
+
key.composing = false
|
|
337
|
+
_ = ghostty_surface_key(surface, key)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
override func doCommand(by selector: Selector) {
|
|
341
|
+
// Called by the input method for special commands (e.g. moveLeft:, deleteBackward:).
|
|
342
|
+
// We intentionally do nothing here — ghostty handles these via keyDown.
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// MARK: - NSTextInputClient (CJK IME support)
|
|
346
|
+
|
|
347
|
+
/// Whether there is currently marked (composing) text.
|
|
348
|
+
func hasMarkedText() -> Bool {
|
|
349
|
+
return isComposing
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/// The range of the marked text within the total text storage.
|
|
353
|
+
func markedRange() -> NSRange {
|
|
354
|
+
if isComposing {
|
|
355
|
+
return NSRange(location: 0, length: imeMarkedText.length)
|
|
356
|
+
}
|
|
357
|
+
return NSRange(location: NSNotFound, length: 0)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/// The range of the current selection. Returns empty range at the end.
|
|
361
|
+
func selectedRange() -> NSRange {
|
|
362
|
+
return imeSelectedRange
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/// Called by the input method to set or update composing text.
|
|
366
|
+
func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
|
|
367
|
+
let attrStr: NSAttributedString
|
|
368
|
+
if let s = string as? NSAttributedString {
|
|
369
|
+
attrStr = s
|
|
370
|
+
} else if let s = string as? String {
|
|
371
|
+
attrStr = NSAttributedString(string: s)
|
|
372
|
+
} else {
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
imeMarkedText = NSMutableAttributedString(attributedString: attrStr)
|
|
377
|
+
imeSelectedRange = selectedRange
|
|
378
|
+
isComposing = imeMarkedText.length > 0
|
|
379
|
+
|
|
380
|
+
// Notify ghostty that we are in a composing state
|
|
381
|
+
if isComposing, let surface {
|
|
382
|
+
var key = ghostty_input_key_s()
|
|
383
|
+
key.action = GHOSTTY_ACTION_PRESS
|
|
384
|
+
key.keycode = 0
|
|
385
|
+
key.composing = true
|
|
386
|
+
_ = ghostty_surface_key(surface, key)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
needsDisplay = true
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/// Called by the input method when composition is canceled.
|
|
393
|
+
func unmarkText() {
|
|
394
|
+
imeMarkedText = NSMutableAttributedString()
|
|
395
|
+
imeSelectedRange = NSRange(location: NSNotFound, length: 0)
|
|
396
|
+
isComposing = false
|
|
397
|
+
needsDisplay = true
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/// Valid attributes for marked text display.
|
|
401
|
+
func validAttributesForMarkedText() -> [NSAttributedString.Key] {
|
|
402
|
+
return [.underlineStyle, .foregroundColor, .backgroundColor]
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/// Return attributed substring for the proposed range.
|
|
406
|
+
func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
|
|
407
|
+
// We don't maintain a text storage, so return nil
|
|
408
|
+
return nil
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/// Called when the input method commits text (final input after composing).
|
|
412
|
+
func insertText(_ string: Any, replacementRange: NSRange) {
|
|
413
|
+
// Unmark first if we were composing
|
|
414
|
+
let wasComposing = isComposing
|
|
415
|
+
if wasComposing {
|
|
416
|
+
unmarkText()
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Send the committed text to ghostty
|
|
420
|
+
guard let surface else { return }
|
|
421
|
+
if let str = string as? String {
|
|
422
|
+
ghostty_surface_text(surface, str, UInt(str.utf8.count))
|
|
423
|
+
} else if let attrStr = string as? NSAttributedString {
|
|
424
|
+
let str = attrStr.string
|
|
425
|
+
ghostty_surface_text(surface, str, UInt(str.utf8.count))
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/// Return the first rect for the character at the given range.
|
|
430
|
+
/// Used by the input method to position the candidates window.
|
|
431
|
+
func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect {
|
|
432
|
+
// Return a rect near the cursor position for the IME candidates window.
|
|
433
|
+
// We approximate using the bottom-left of the view + some offset.
|
|
434
|
+
guard let windowRef = window else {
|
|
435
|
+
return NSRect(x: 0, y: 0, width: 0, height: 20)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Use a position near the center-bottom of the view as a reasonable default
|
|
439
|
+
let viewRect = NSRect(x: 0, y: bounds.height - 20, width: bounds.width, height: 20)
|
|
440
|
+
let windowRect = convert(viewRect, to: nil)
|
|
441
|
+
let screenRect = windowRef.convertToScreen(windowRect)
|
|
442
|
+
return screenRect
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/// Return the character index for a given point (used by input method).
|
|
446
|
+
func characterIndex(for point: NSPoint) -> Int {
|
|
447
|
+
return NSNotFound
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// MARK: - Mouse input
|
|
451
|
+
|
|
452
|
+
override func mouseDown(with event: NSEvent) {
|
|
453
|
+
guard let surface else { return }
|
|
454
|
+
window?.makeFirstResponder(self)
|
|
455
|
+
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_LEFT, ghostty_input_mods_e(rawValue: 0))
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
override func mouseUp(with event: NSEvent) {
|
|
459
|
+
guard let surface else { return }
|
|
460
|
+
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_LEFT, ghostty_input_mods_e(rawValue: 0))
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
override func mouseMoved(with event: NSEvent) {
|
|
464
|
+
guard let surface else { return }
|
|
465
|
+
let pt = convert(event.locationInWindow, from: nil)
|
|
466
|
+
ghostty_surface_mouse_pos(surface, pt.x, pt.y, ghostty_input_mods_e(rawValue: 0))
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
override func mouseDragged(with event: NSEvent) {
|
|
470
|
+
guard let surface else { return }
|
|
471
|
+
let pt = convert(event.locationInWindow, from: nil)
|
|
472
|
+
ghostty_surface_mouse_pos(surface, pt.x, pt.y, ghostty_input_mods_e(rawValue: 0))
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
override func scrollWheel(with event: NSEvent) {
|
|
476
|
+
guard let surface else { return }
|
|
477
|
+
ghostty_surface_mouse_scroll(surface, event.scrollingDeltaX, event.scrollingDeltaY, 0)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// MARK: - Focus
|
|
481
|
+
|
|
482
|
+
override func becomeFirstResponder() -> Bool {
|
|
483
|
+
surface.flatMap { ghostty_surface_set_focus($0, true) }
|
|
484
|
+
return super.becomeFirstResponder()
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
override func resignFirstResponder() -> Bool {
|
|
488
|
+
surface.flatMap { ghostty_surface_set_focus($0, false) }
|
|
489
|
+
return super.resignFirstResponder()
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// MARK: - NSDraggingDestination (File Drop Support)
|
|
493
|
+
|
|
494
|
+
override func draggingEntered(_ sender: any NSDraggingInfo) -> NSDragOperation {
|
|
495
|
+
guard sender.draggingPasteboard.canReadObject(
|
|
496
|
+
forClasses: [NSURL.self],
|
|
497
|
+
options: [.urlReadingFileURLsOnly: true]
|
|
498
|
+
) else {
|
|
499
|
+
return []
|
|
500
|
+
}
|
|
501
|
+
isDragHighlighted = true
|
|
502
|
+
return .copy
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
override func draggingUpdated(_ sender: any NSDraggingInfo) -> NSDragOperation {
|
|
506
|
+
guard isDragHighlighted else { return [] }
|
|
507
|
+
return .copy
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
override func draggingExited(_ sender: (any NSDraggingInfo)?) {
|
|
511
|
+
isDragHighlighted = false
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
override func performDragOperation(_ sender: any NSDraggingInfo) -> Bool {
|
|
515
|
+
isDragHighlighted = false
|
|
516
|
+
|
|
517
|
+
let pb = sender.draggingPasteboard
|
|
518
|
+
guard let urls = pb.readObjects(
|
|
519
|
+
forClasses: [NSURL.self],
|
|
520
|
+
options: [.urlReadingFileURLsOnly: true]
|
|
521
|
+
) as? [URL], !urls.isEmpty else {
|
|
522
|
+
return false
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Shell-escape each path and join with spaces
|
|
526
|
+
let escapedPaths = urls.map { url in
|
|
527
|
+
ClipboardHelper.escapeForShell(url.path)
|
|
528
|
+
}
|
|
529
|
+
let text = escapedPaths.joined(separator: " ")
|
|
530
|
+
sendText(text)
|
|
531
|
+
return true
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
override func prepareForDragOperation(_ sender: any NSDraggingInfo) -> Bool {
|
|
535
|
+
return true
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
override func concludeDragOperation(_ sender: (any NSDraggingInfo)?) {
|
|
539
|
+
isDragHighlighted = false
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// MARK: - Private: Paste handling
|
|
543
|
+
|
|
544
|
+
private func handlePaste() {
|
|
545
|
+
let pb = NSPasteboard.general
|
|
546
|
+
|
|
547
|
+
// Check for image first — save to temp and paste path
|
|
548
|
+
if let imageURL = ClipboardHelper.saveImageToTemp(from: pb) {
|
|
549
|
+
let escapedPath = ClipboardHelper.escapeForShell(imageURL.path)
|
|
550
|
+
sendText(escapedPath)
|
|
551
|
+
return
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Use ClipboardHelper for enhanced paste (files, RTF, HTML, etc.)
|
|
555
|
+
if let content = ClipboardHelper.pasteContent(from: pb) {
|
|
556
|
+
sendText(content)
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Search overlay displayed on top of the terminal surface.
|
|
4
|
+
/// Adapted from ghostty-org/ghostty macOS SearchView.swift (design pattern)
|
|
5
|
+
struct SurfaceSearchOverlay: View {
|
|
6
|
+
@Binding var isVisible: Bool
|
|
7
|
+
@Binding var searchText: String
|
|
8
|
+
@Binding var totalMatches: Int
|
|
9
|
+
@Binding var selectedMatch: Int
|
|
10
|
+
|
|
11
|
+
var onSearch: (String) -> Void
|
|
12
|
+
var onNext: () -> Void
|
|
13
|
+
var onPrevious: () -> Void
|
|
14
|
+
var onClose: () -> Void
|
|
15
|
+
|
|
16
|
+
@FocusState private var isFocused: Bool
|
|
17
|
+
|
|
18
|
+
var body: some View {
|
|
19
|
+
if isVisible {
|
|
20
|
+
HStack(spacing: 6) {
|
|
21
|
+
// Search text field
|
|
22
|
+
TextField("Find...", text: $searchText)
|
|
23
|
+
.textFieldStyle(.roundedBorder)
|
|
24
|
+
.frame(width: 200)
|
|
25
|
+
.focused($isFocused)
|
|
26
|
+
.onSubmit {
|
|
27
|
+
if NSApp.currentEvent?.modifierFlags.contains(.shift) == true {
|
|
28
|
+
onPrevious()
|
|
29
|
+
} else {
|
|
30
|
+
onNext()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
.onChange(of: searchText) { _, newValue in
|
|
34
|
+
onSearch(newValue)
|
|
35
|
+
}
|
|
36
|
+
.onExitCommand {
|
|
37
|
+
closeSearch()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Match indicator
|
|
41
|
+
matchIndicator
|
|
42
|
+
.font(.system(size: 11, design: .monospaced))
|
|
43
|
+
.foregroundStyle(.secondary)
|
|
44
|
+
.frame(minWidth: 50)
|
|
45
|
+
|
|
46
|
+
// Navigation buttons
|
|
47
|
+
Button(action: onPrevious) {
|
|
48
|
+
Image(systemName: "chevron.up")
|
|
49
|
+
.font(.system(size: 11, weight: .medium))
|
|
50
|
+
}
|
|
51
|
+
.buttonStyle(.borderless)
|
|
52
|
+
.help("Previous Match (Shift+Enter)")
|
|
53
|
+
.disabled(totalMatches <= 0)
|
|
54
|
+
|
|
55
|
+
Button(action: onNext) {
|
|
56
|
+
Image(systemName: "chevron.down")
|
|
57
|
+
.font(.system(size: 11, weight: .medium))
|
|
58
|
+
}
|
|
59
|
+
.buttonStyle(.borderless)
|
|
60
|
+
.help("Next Match (Enter)")
|
|
61
|
+
.disabled(totalMatches <= 0)
|
|
62
|
+
|
|
63
|
+
// Close button
|
|
64
|
+
Button(action: closeSearch) {
|
|
65
|
+
Image(systemName: "xmark")
|
|
66
|
+
.font(.system(size: 10, weight: .bold))
|
|
67
|
+
.foregroundStyle(.secondary)
|
|
68
|
+
}
|
|
69
|
+
.buttonStyle(.borderless)
|
|
70
|
+
.help("Close (Esc)")
|
|
71
|
+
}
|
|
72
|
+
.padding(.horizontal, 10)
|
|
73
|
+
.padding(.vertical, 6)
|
|
74
|
+
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8))
|
|
75
|
+
.overlay(
|
|
76
|
+
RoundedRectangle(cornerRadius: 8)
|
|
77
|
+
.stroke(Color.primary.opacity(0.1), lineWidth: 1)
|
|
78
|
+
)
|
|
79
|
+
.shadow(color: .black.opacity(0.15), radius: 4, y: 2)
|
|
80
|
+
.padding(.trailing, 12)
|
|
81
|
+
.padding(.top, 8)
|
|
82
|
+
.onAppear {
|
|
83
|
+
isFocused = true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@ViewBuilder
|
|
89
|
+
private var matchIndicator: some View {
|
|
90
|
+
if searchText.isEmpty {
|
|
91
|
+
Text("")
|
|
92
|
+
} else if totalMatches < 0 {
|
|
93
|
+
// -1 means regex error
|
|
94
|
+
Text("error")
|
|
95
|
+
.foregroundStyle(.red)
|
|
96
|
+
} else if totalMatches == 0 {
|
|
97
|
+
Text("0 results")
|
|
98
|
+
} else {
|
|
99
|
+
let display = selectedMatch >= 0 ? "\(selectedMatch + 1) of \(totalMatches)" : "\(totalMatches) found"
|
|
100
|
+
Text(display)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private func closeSearch() {
|
|
105
|
+
searchText = ""
|
|
106
|
+
isVisible = false
|
|
107
|
+
onClose()
|
|
108
|
+
}
|
|
109
|
+
}
|