import ExpoModulesCoreimport UIKit public final class T3KeyboardCommandsModule: Module { public func definition() -> ModuleDefinition { Name("T3KeyboardCommands") View(T3KeyboardCommandsView.self) { Prop("enabledCommands") { (view: T3KeyboardCommandsView, commands: [String]) in view.setEnabledCommands(commands) } Events("onCommand") } }} public final class T3KeyboardCommandsView: ExpoView { let onCommand = EventDispatcher() private var enabledCommands = Set<String>() public override var canBecomeFirstResponder: Bool { true } public override var keyCommands: [UIKeyCommand]? { [ enabledCommand("newTask", input: "n", modifiers: .command, action: #selector(newTask), title: "New Task"), enabledCommand("focusSearch", input: "f", modifiers: .command, action: #selector(focusSearch), title: "Find"), enabledCommand("focusSearch", input: "k", modifiers: .command, action: #selector(focusSearch), title: "Focus Search"), enabledCommand("back", input: "[", modifiers: .command, action: #selector(goBack), title: "Back"), enabledCommand("files", input: "f", modifiers: [.command, .shift], action: #selector(openFiles), title: "Open Files"), enabledCommand("terminal", input: "t", modifiers: [.command, .shift], action: #selector(openTerminal), title: "Open Terminal"), enabledCommand("review", input: "r", modifiers: [.command, .shift], action: #selector(openReview), title: "Open Review"), enabledCommand( "copyThreadReference", input: "c", modifiers: [.command, .shift], action: #selector(copyThreadReference), title: "Copy PR Link or Thread ID" ), enabledCommand("toggleSidebar", input: "\\", modifiers: .command, action: #selector(handleToggleSidebar), title: "Toggle Sidebar"), ].compactMap { $0 } } func setEnabledCommands(_ commands: [String]) { enabledCommands = Set(commands) if isFirstResponder { resignFirstResponder() } reclaimFirstResponderIfAvailable() } private func enabledCommand( _ identifier: String, input: String, modifiers: UIKeyModifierFlags, action: Selector, title: String ) -> UIKeyCommand? { guard enabledCommands.contains(identifier) else { return nil } return command(input, modifiers: modifiers, action: action, title: title) } public required init(appContext: AppContext? = nil) { super.init(appContext: appContext) NotificationCenter.default.addObserver( self, selector: #selector(reclaimFirstResponderIfAvailable), name: UITextView.textDidEndEditingNotification, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(reclaimFirstResponderIfAvailable), name: UITextField.textDidEndEditingNotification, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(reclaimFirstResponderIfAvailable), name: UIApplication.didBecomeActiveNotification, object: nil ) } deinit { NotificationCenter.default.removeObserver(self) } public override func didMoveToWindow() { super.didMoveToWindow() reclaimFirstResponderIfAvailable() } public override func layoutSubviews() { super.layoutSubviews() reclaimFirstResponderIfAvailable() } private func command( _ input: String, modifiers: UIKeyModifierFlags, action: Selector, title: String ) -> UIKeyCommand { let command = UIKeyCommand(input: input, modifierFlags: modifiers, action: action) command.discoverabilityTitle = title command.wantsPriorityOverSystemBehavior = true return command } @objc private func newTask() { emit("newTask") } @objc private func focusSearch() { emit("focusSearch") } @objc private func goBack() { emit("back") } @objc private func openFiles() { emit("files") } @objc private func openTerminal() { emit("terminal") } @objc private func openReview() { emit("review") } @objc private func copyThreadReference() { emit("copyThreadReference") } @objc private func handleToggleSidebar() { emit("toggleSidebar") } private func emit(_ command: String) { onCommand(["command": command]) } @objc private func reclaimFirstResponderIfAvailable() { DispatchQueue.main.async { [weak self] in guard let self, self.window?.t3FirstResponder == nil else { return } self.becomeFirstResponder() } }} private extension UIView { var t3FirstResponder: UIResponder? { if isFirstResponder { return self } for subview in subviews { if let responder = subview.t3FirstResponder { return responder } } return nil }}
import ExpoModulesCore
import UIKit
public final class T3KeyboardCommandsModule: Module {
public func definition() -> ModuleDefinition {
Name("T3KeyboardCommands")
View(T3KeyboardCommandsView.self) {
Prop("enabledCommands") { (view: T3KeyboardCommandsView, commands: [String]) in
view.setEnabledCommands(commands)
}
Events("onCommand")
}
}
}
public final class T3KeyboardCommandsView: ExpoView {
let onCommand = EventDispatcher()
private var enabledCommands = Set<String>()
public override var canBecomeFirstResponder: Bool { true }
public override var keyCommands: [UIKeyCommand]? {
[
enabledCommand("newTask", input: "n", modifiers: .command, action: #selector(newTask), title: "New Task"),
enabledCommand("focusSearch", input: "f", modifiers: .command, action: #selector(focusSearch), title: "Find"),
enabledCommand("focusSearch", input: "k", modifiers: .command, action: #selector(focusSearch), title: "Focus Search"),
enabledCommand("back", input: "[", modifiers: .command, action: #selector(goBack), title: "Back"),
enabledCommand("files", input: "f", modifiers: [.command, .shift], action: #selector(openFiles), title: "Open Files"),
enabledCommand("terminal", input: "t", modifiers: [.command, .shift], action: #selector(openTerminal), title: "Open Terminal"),
enabledCommand("review", input: "r", modifiers: [.command, .shift], action: #selector(openReview), title: "Open Review"),
enabledCommand(
"copyThreadReference",
input: "c",
modifiers: [.command, .shift],
action: #selector(copyThreadReference),
title: "Copy PR Link or Thread ID"
),
enabledCommand("toggleSidebar", input: "\\", modifiers: .command, action: #selector(handleToggleSidebar), title: "Toggle Sidebar"),
].compactMap { $0 }
}
func setEnabledCommands(_ commands: [String]) {
enabledCommands = Set(commands)
if isFirstResponder {
resignFirstResponder()
}
reclaimFirstResponderIfAvailable()
}
private func enabledCommand(
_ identifier: String,
input: String,
modifiers: UIKeyModifierFlags,
action: Selector,
title: String
) -> UIKeyCommand? {
guard enabledCommands.contains(identifier) else { return nil }
return command(input, modifiers: modifiers, action: action, title: title)
}
public required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
NotificationCenter.default.addObserver(
self,
selector: #selector(reclaimFirstResponderIfAvailable),
name: UITextView.textDidEndEditingNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(reclaimFirstResponderIfAvailable),
name: UITextField.textDidEndEditingNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(reclaimFirstResponderIfAvailable),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
public override func didMoveToWindow() {
super.didMoveToWindow()
reclaimFirstResponderIfAvailable()
}
public override func layoutSubviews() {
super.layoutSubviews()
reclaimFirstResponderIfAvailable()
}
private func command(
_ input: String,
modifiers: UIKeyModifierFlags,
action: Selector,
title: String
) -> UIKeyCommand {
let command = UIKeyCommand(input: input, modifierFlags: modifiers, action: action)
command.discoverabilityTitle = title
command.wantsPriorityOverSystemBehavior = true
return command
}
@objc private func newTask() { emit("newTask") }
@objc private func focusSearch() { emit("focusSearch") }
@objc private func goBack() { emit("back") }
@objc private func openFiles() { emit("files") }
@objc private func openTerminal() { emit("terminal") }
@objc private func openReview() { emit("review") }
@objc private func copyThreadReference() { emit("copyThreadReference") }
@objc private func handleToggleSidebar() { emit("toggleSidebar") }
private func emit(_ command: String) {
onCommand(["command": command])
}
@objc private func reclaimFirstResponderIfAvailable() {
DispatchQueue.main.async { [weak self] in
guard let self, self.window?.t3FirstResponder == nil else { return }
self.becomeFirstResponder()
}
}
}
private extension UIView {
var t3FirstResponder: UIResponder? {
if isFirstResponder { return self }
for subview in subviews {
if let responder = subview.t3FirstResponder { return responder }
}
return nil
}
}