Files
Luke Gustafson 6c40d2f0cc v1.4.0 (#31)
* 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>
2026-06-03 16:49:50 -05:00

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)
}
}