mirror of
https://github.com/Termix-SSH/Mobile.git
synced 2026-07-28 05:51:43 +02:00
6c40d2f0cc
* fix: register physical Tab key on iPadOS/iOS hardware keyboards (#14) The native module only registered Shift+Tab but not bare Tab, so iOS intercepted it for UI focus navigation. Register an unmodified Tab UIKeyCommand with wantsPriorityOverSystemBehavior and handle both Tab and Shift+Tab in the same JS branch. Fixes Termix-SSH/Support#557 * fix: pass jumpHosts, forceKeyboardInteractive, and overrideCredentialUsername to terminal connection (#15) These fields were defined on SSHHost but omitted from the terminal hostConfig passthrough. Without jumpHosts the backend cannot route through jump servers; without forceKeyboardInteractive hosts that require keyboard-interactive auth fail silently; without overrideCredentialUsername shared credentials use the wrong username. Fixes Termix-SSH/Support#422 Fixes Termix-SSH/Support#638 * fix: improve mobile terminal scroll recovery (#17) * Add mobile remote desktop sessions (#18) * feat: add mobile remote desktop sessions * feat: add remote desktop input controls * feat: polish remote desktop controls * feat: add mobile terminal font selection (#19) * feat: add mobile shift tab hotkey (#20) * feat: initial UI redesign * feat: revamp server login system * chore: update app icon/background color * fix: remove unused fields in the settings tab * feat: improve the user pin system and update all settings modals to use new ui * fix: active server not updating unless app restarts * feat: add version in settings * feat: improve host page UI (updated host form and added credential management) * fix: preserve composed iOS terminal input * ci: restore mobile checks * feat: rewrite of connection system for all types * fix: preserve mobile jump hosts (#30) * fix: guard android user ca trust (#29) * fix: capture ios hardware tab key (#27) * fix: reset terminal input after special keys (#28) * fix: open oidc in system browser (#26) * fix: preserve websocket auth context (#25) * fix: keep none-auth terminal prompts alive (#23) * fix: allow local network SSL in Android app (#21) * feat: allow oidc passkeys * feat: bundeled xterm into app and added docker loading screen * feat: add 2fa, api key, and active sessions to settings and addressed multiple bug fixes and inconsistencies with termix web * chore: update repo images and readme * chore: update readme to split table for screenshots * chore: rename ios to apple in build workflow * fix: android build failure * feat: add snippet management and fix several bugs * fix: web login and oidc incorrect logic * chore: run linter --------- Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
248 lines
9.7 KiB
Swift
248 lines
9.7 KiB
Swift
import ExpoModulesCore
|
|
import UIKit
|
|
|
|
public class HardwareKeyboardModule: Module {
|
|
private static var swizzled = false
|
|
private static var instances: [ObjectIdentifier: HardwareKeyboardModule] = [:]
|
|
private static var lastEmit: (input: String, shift: Bool, ctrl: Bool, alt: Bool, time: TimeInterval)?
|
|
|
|
public func definition() -> ModuleDefinition {
|
|
Name("HardwareKeyboard")
|
|
|
|
Events("onKeyCommand")
|
|
|
|
OnCreate {
|
|
// Register immediately on creation, not waiting for a listener
|
|
HardwareKeyboardModule.instances[ObjectIdentifier(self)] = self
|
|
HardwareKeyboardModule.swizzleIfNeeded()
|
|
}
|
|
|
|
OnDestroy {
|
|
HardwareKeyboardModule.instances.removeValue(forKey: ObjectIdentifier(self))
|
|
}
|
|
}
|
|
|
|
static func swizzleIfNeeded() {
|
|
guard !swizzled else { return }
|
|
swizzled = true
|
|
|
|
if
|
|
let original = class_getInstanceMethod(
|
|
UIViewController.self,
|
|
#selector(getter: UIViewController.keyCommands)
|
|
),
|
|
let replacement = class_getInstanceMethod(
|
|
UIViewController.self,
|
|
#selector(UIViewController.hk_keyCommands)
|
|
)
|
|
{
|
|
method_exchangeImplementations(original, replacement)
|
|
}
|
|
|
|
if
|
|
let original = class_getInstanceMethod(
|
|
UIResponder.self,
|
|
#selector(UIResponder.pressesBegan(_:with:))
|
|
),
|
|
let replacement = class_getInstanceMethod(
|
|
UIResponder.self,
|
|
#selector(UIResponder.hk_pressesBegan(_:with:))
|
|
)
|
|
{
|
|
method_exchangeImplementations(original, replacement)
|
|
}
|
|
}
|
|
|
|
static func emitToAll(_ input: String, shift: Bool, ctrl: Bool = false, alt: Bool = false) {
|
|
let now = Date().timeIntervalSince1970
|
|
if let last = lastEmit,
|
|
last.input == input,
|
|
last.shift == shift,
|
|
last.ctrl == ctrl,
|
|
last.alt == alt,
|
|
now - last.time < 0.03
|
|
{
|
|
return
|
|
}
|
|
lastEmit = (input, shift, ctrl, alt, now)
|
|
|
|
let payload: [String: Any] = ["input": input, "shift": shift, "ctrl": ctrl, "alt": alt]
|
|
for instance in instances.values {
|
|
instance.sendEvent("onKeyCommand", payload)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension UIResponder {
|
|
@objc func hk_pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
|
|
let handled = presses.contains { press in
|
|
guard press.type == .keyboard, let key = press.key else { return false }
|
|
|
|
let modifiers = key.modifierFlags
|
|
let shift = modifiers.contains(.shift)
|
|
let ctrl = modifiers.contains(.control)
|
|
let alt = modifiers.contains(.alternate)
|
|
|
|
switch key.keyCode {
|
|
case .keyboardTab:
|
|
HardwareKeyboardModule.emitToAll("\t", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardUpArrow:
|
|
HardwareKeyboardModule.emitToAll("ArrowUp", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardDownArrow:
|
|
HardwareKeyboardModule.emitToAll("ArrowDown", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardLeftArrow:
|
|
HardwareKeyboardModule.emitToAll("ArrowLeft", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardRightArrow:
|
|
HardwareKeyboardModule.emitToAll("ArrowRight", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardEscape:
|
|
HardwareKeyboardModule.emitToAll("Escape", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardDeleteOrBackspace:
|
|
HardwareKeyboardModule.emitToAll("Backspace", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardDeleteForward:
|
|
HardwareKeyboardModule.emitToAll("Delete", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardHome:
|
|
HardwareKeyboardModule.emitToAll("Home", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardEnd:
|
|
HardwareKeyboardModule.emitToAll("End", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardPageUp:
|
|
HardwareKeyboardModule.emitToAll("PageUp", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardPageDown:
|
|
HardwareKeyboardModule.emitToAll("PageDown", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardF1:
|
|
HardwareKeyboardModule.emitToAll("F1", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardF2:
|
|
HardwareKeyboardModule.emitToAll("F2", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardF3:
|
|
HardwareKeyboardModule.emitToAll("F3", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardF4:
|
|
HardwareKeyboardModule.emitToAll("F4", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardF5:
|
|
HardwareKeyboardModule.emitToAll("F5", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardF6:
|
|
HardwareKeyboardModule.emitToAll("F6", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardF7:
|
|
HardwareKeyboardModule.emitToAll("F7", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardF8:
|
|
HardwareKeyboardModule.emitToAll("F8", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardF9:
|
|
HardwareKeyboardModule.emitToAll("F9", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardF10:
|
|
HardwareKeyboardModule.emitToAll("F10", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardF11:
|
|
HardwareKeyboardModule.emitToAll("F11", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
case .keyboardF12:
|
|
HardwareKeyboardModule.emitToAll("F12", shift: shift, ctrl: ctrl, alt: alt)
|
|
return true
|
|
default:
|
|
if let input = key.charactersIgnoringModifiers, input.count == 1 {
|
|
if ctrl {
|
|
HardwareKeyboardModule.emitToAll(input, shift: shift, ctrl: true, alt: alt)
|
|
return true
|
|
}
|
|
if alt && !ctrl {
|
|
HardwareKeyboardModule.emitToAll(input, shift: shift, ctrl: false, alt: true)
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
if !handled {
|
|
self.hk_pressesBegan(presses, with: event)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension UIViewController {
|
|
@objc func hk_keyCommands() -> [UIKeyCommand]? {
|
|
var commands = self.hk_keyCommands() ?? []
|
|
|
|
// Shift+Tab
|
|
let shiftTab = UIKeyCommand(input: "\t", modifierFlags: .shift, action: #selector(hk_handleShiftTab))
|
|
shiftTab.wantsPriorityOverSystemBehavior = true
|
|
commands.append(shiftTab)
|
|
|
|
// Tab (unmodified)
|
|
let tab = UIKeyCommand(input: "\t", modifierFlags: [], action: #selector(hk_handleTab))
|
|
tab.wantsPriorityOverSystemBehavior = true
|
|
commands.append(tab)
|
|
|
|
// Arrow keys (plain and with Shift)
|
|
for (input, plainSel, shiftSel) in [
|
|
(UIKeyCommand.inputUpArrow, #selector(hk_handleArrowUp(_:)), #selector(hk_handleShiftArrowUp(_:))),
|
|
(UIKeyCommand.inputDownArrow, #selector(hk_handleArrowDown(_:)), #selector(hk_handleShiftArrowDown(_:))),
|
|
(UIKeyCommand.inputLeftArrow, #selector(hk_handleArrowLeft(_:)), #selector(hk_handleShiftArrowLeft(_:))),
|
|
(UIKeyCommand.inputRightArrow, #selector(hk_handleArrowRight(_:)), #selector(hk_handleShiftArrowRight(_:))),
|
|
] as [(String, Selector, Selector)] {
|
|
let plain = UIKeyCommand(input: input, modifierFlags: [], action: plainSel)
|
|
plain.wantsPriorityOverSystemBehavior = true
|
|
commands.append(plain)
|
|
|
|
let shifted = UIKeyCommand(input: input, modifierFlags: .shift, action: shiftSel)
|
|
shifted.wantsPriorityOverSystemBehavior = true
|
|
commands.append(shifted)
|
|
}
|
|
|
|
// Escape
|
|
let esc = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(hk_handleEscape))
|
|
esc.wantsPriorityOverSystemBehavior = true
|
|
commands.append(esc)
|
|
|
|
// Ctrl key combinations
|
|
let ctrlInputs = [
|
|
"a","b","c","d","e","f","g","h","k","l","n","p","q","r","s","t","u","v","w","x","y","z",
|
|
"[","]","\\",
|
|
]
|
|
for input in ctrlInputs {
|
|
let cmd = UIKeyCommand(input: input, modifierFlags: .control, action: #selector(hk_handleCtrlKey(_:)))
|
|
cmd.wantsPriorityOverSystemBehavior = true
|
|
commands.append(cmd)
|
|
}
|
|
|
|
return commands
|
|
}
|
|
|
|
@objc func hk_handleTab() { HardwareKeyboardModule.emitToAll("\t", shift: false) }
|
|
@objc func hk_handleShiftTab() { HardwareKeyboardModule.emitToAll("\t", shift: true) }
|
|
@objc func hk_handleEscape() { HardwareKeyboardModule.emitToAll("Escape", shift: false) }
|
|
|
|
@objc func hk_handleArrowUp(_ sender: UIKeyCommand) { HardwareKeyboardModule.emitToAll("ArrowUp", shift: sender.modifierFlags.contains(.shift)) }
|
|
@objc func hk_handleArrowDown(_ sender: UIKeyCommand) { HardwareKeyboardModule.emitToAll("ArrowDown", shift: sender.modifierFlags.contains(.shift)) }
|
|
@objc func hk_handleArrowLeft(_ sender: UIKeyCommand) { HardwareKeyboardModule.emitToAll("ArrowLeft", shift: sender.modifierFlags.contains(.shift)) }
|
|
@objc func hk_handleArrowRight(_ sender: UIKeyCommand) { HardwareKeyboardModule.emitToAll("ArrowRight", shift: sender.modifierFlags.contains(.shift)) }
|
|
|
|
@objc func hk_handleShiftArrowUp(_ sender: UIKeyCommand) { HardwareKeyboardModule.emitToAll("ArrowUp", shift: true) }
|
|
@objc func hk_handleShiftArrowDown(_ sender: UIKeyCommand) { HardwareKeyboardModule.emitToAll("ArrowDown", shift: true) }
|
|
@objc func hk_handleShiftArrowLeft(_ sender: UIKeyCommand) { HardwareKeyboardModule.emitToAll("ArrowLeft", shift: true) }
|
|
@objc func hk_handleShiftArrowRight(_ sender: UIKeyCommand) { HardwareKeyboardModule.emitToAll("ArrowRight", shift: true) }
|
|
|
|
@objc func hk_handleCtrlKey(_ sender: UIKeyCommand) {
|
|
guard let input = sender.input else { return }
|
|
HardwareKeyboardModule.emitToAll(input, shift: false, ctrl: true)
|
|
}
|
|
}
|