From efbfe73e68c80705ed1abb1a94f91763164a0282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20G=C3=B6nnenwein?= Date: Thu, 18 Jun 2026 17:02:08 +0200 Subject: [PATCH] feat(sender): add per-session configurable shortcut bindings Map a trigger combo pressed on the master to an action combo injected on the slave (direction follows the input role), edited via a key-recorder UI in the session settings and persisted per session. The action is injected through in-process System Events (NSAppleScript), because macOS ignores synthetic CGEvents for protected symbolic hotkeys such as Space switching. --- .../TBDisplaySenderContentView.swift | 6 + .../TBDisplaySenderManager.swift | 21 +++- .../TBDisplaySenderService.swift | 95 +++++++++++++- .../TBDisplaySender/TBInputBinding.swift | 118 ++++++++++++++++++ .../TBDisplaySender/TBInputBindingsView.swift | 114 +++++++++++++++++ .../TBInputRelayController.swift | 43 ++++++- .../TargetBridge.xcodeproj/project.pbxproj | 8 ++ 7 files changed, 398 insertions(+), 7 deletions(-) create mode 100644 TargetBridge-Sender/TBDisplaySender/TBInputBinding.swift create mode 100644 TargetBridge-Sender/TBDisplaySender/TBInputBindingsView.swift diff --git a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderContentView.swift b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderContentView.swift index 8778c08..9c46a96 100644 --- a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderContentView.swift +++ b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderContentView.swift @@ -631,6 +631,12 @@ private struct TBDisplaySenderSessionSettingsSheet: View { } } + if session.inputControlRole != .off { + SurfaceSubcard { + TBInputBindingsView(session: session) + } + } + if session.inputControlRole == .senderMaster, !service.localInputMonitoringTrusted { SurfaceSubcard { permissionWarningCard( diff --git a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderManager.swift b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderManager.swift index caa4b63..065f6da 100644 --- a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderManager.swift +++ b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderManager.swift @@ -237,9 +237,11 @@ final class TBDisplaySenderService: ObservableObject { private static let persistedSessionsKey = "fd.tbdisplaysender.sessions.v1" - /// Snapshot of the user-configurable settings for a single session. Runtime - /// state (connection, input master role, FPS, …) is intentionally excluded — - /// only the choices the user makes in the UI are remembered across launches. + /// Snapshot of the user-configurable settings for a single session. Transient + /// runtime state (connection, FPS, …) is intentionally excluded — only the + /// choices the user makes in the UI are remembered across launches. + /// `inputBindings` is optional for backward compatibility with sessions saved + /// before it was added. private struct PersistedSession: Codable { var transportKind: String var localInterfaceIP: String @@ -251,6 +253,7 @@ final class TBDisplaySenderService: ObservableObject { var brightness: Double var inputGestureMode: String var volume: Double? + var inputBindings: [TBInputBinding]? } private var lastPersistedData: Data? @@ -282,7 +285,8 @@ final class TBDisplaySenderService: ObservableObject { audioEnabled: session.audioEnabled, brightness: session.brightness, inputGestureMode: session.inputGestureMode.rawValue, - volume: session.volume + volume: session.volume, + inputBindings: session.inputBindings ) } guard let data = try? JSONEncoder().encode(configs) else { return } @@ -336,6 +340,9 @@ final class TBDisplaySenderService: ObservableObject { if let gesture = TBInputGestureMode(rawValue: config.inputGestureMode) { session.inputGestureMode = gesture } + if let bindings = config.inputBindings { + session.inputBindings = bindings + } session.receiverIP = config.receiverIP session.selectedReceiverID = config.selectedReceiverID session.localInterfaceIP = config.localInterfaceIP @@ -550,6 +557,12 @@ final class TBDisplaySenderService: ObservableObject { deactivateHandler: { [weak self, weak session] in guard let self, let session else { return } self.setInputControlRole(.off, for: session) + }, + triggerMatcher: { [weak session] keyCode, modifiers in + guard let session, + let binding = TBInputBindingEngine.match(keyCode: keyCode, modifiers: modifiers, in: session.inputBindings) + else { return nil } + return TBInputBindingEngine.injectionSequence(heldModifiers: modifiers, action: binding.action) } ) } diff --git a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderService.swift b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderService.swift index b4ad6ab..800fd12 100644 --- a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderService.swift +++ b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderService.swift @@ -962,6 +962,9 @@ final class TBDisplaySenderSession: NSObject, ObservableObject, Identifiable, @u } } @Published var inputGestureMode: TBInputGestureMode = .native + /// User-defined shortcut bindings for this session (trigger on master → + /// action injected on slave). See TBInputBinding. + @Published var inputBindings: [TBInputBinding] = [] private var connection: NWConnection? private let connectionQueue = DispatchQueue(label: "fd.tbmonitor.sender.connection", qos: .userInteractive) @@ -999,6 +1002,9 @@ final class TBDisplaySenderSession: NSObject, ObservableObject, Identifiable, @u private var injectedOptionDown = false private var injectedControlDown = false private var injectedCapsDown = false + /// While a binding trigger key is held (matched), swallow its key-up so the + /// raw trigger key never reaches the slave. + private var suppressedTriggerKeyCode: UInt16? private static var cachedSupportsHEVCHardwareEncode: Bool? private var receivedInputEventCount: UInt64 = 0 var onRemoteSwitchRequest: ((Int) -> Void)? @@ -1847,14 +1853,99 @@ final class TBDisplaySenderSession: NSObject, ObservableObject, Identifiable, @u case "scroll": postLocalScroll(scrollX: event.scrollX ?? 0, scrollY: event.scrollY ?? 0) case "keyDown": - if let keyCode = event.keyCode { postLocalKey(keyCode: keyCode, isDown: true) } + if let keyCode = event.keyCode { + if handleIncomingTriggerKeyDown(keyCode) { return } + postLocalKey(keyCode: keyCode, isDown: true) + } case "keyUp": - if let keyCode = event.keyCode { postLocalKey(keyCode: keyCode, isDown: false) } + if let keyCode = event.keyCode { + if keyCode == suppressedTriggerKeyCode { + suppressedTriggerKeyCode = nil + return + } + postLocalKey(keyCode: keyCode, isDown: false) + } default: break } } + /// receiverMaster: if the incoming key-down completes a binding trigger, + /// inject the action locally and swallow the trigger. Returns true if handled. + private func handleIncomingTriggerKeyDown(_ keyCode: UInt16) -> Bool { + guard !TBInputBindingEngine.isModifierKeyCode(keyCode), !inputBindings.isEmpty else { return false } + // Debounce key-repeat: ignore repeats while the trigger is still held. + if keyCode == suppressedTriggerKeyCode { return true } + let held = currentHeldModifierBits() + guard let binding = TBInputBindingEngine.match(keyCode: keyCode, modifiers: held, in: inputBindings) else { + return false + } + suppressedTriggerKeyCode = keyCode + TBInputDebugLog.log("binding MATCH: trigger=\(binding.trigger.displayString) -> inject \(binding.action.displayString)") + injectActionViaSystemEvents(binding.action) + return true + } + + /// Inject a binding action through System Events (AppleScript) rather than a + /// raw CGEvent. The WindowServer ignores synthetic CGEvent presses for + /// protected symbolic hotkeys (e.g. ⌃← to switch Spaces), but honors the same + /// shortcut when it comes from the trusted System Events process. + /// + /// The user may be holding the trigger's modifiers, which we inject as held + /// CGEvent state — that would contaminate the action (e.g. a stray ⌥). So we + /// release the held modifiers first so System Events sees a clean combo, then + /// restore them once osascript finishes. + private func injectActionViaSystemEvents(_ action: TBInputShortcut) { + let heldKeyCodes = currentlyHeldModifierKeyCodes() + for keyCode in heldKeyCodes { postLocalKey(keyCode: keyCode, isDown: false) } + + // Run the AppleScript in-process (NSAppleScript), NOT via /usr/bin/osascript: + // when spawned, osascript is the keystroke-sending client and lacks + // Accessibility (error 1002). In-process, this app is the client and it + // already holds Accessibility + Automation, so System Events is allowed + // to post the shortcut. + let source = "tell application \"System Events\" to key code \(action.keyCode)\(Self.appleScriptModifierClause(action.modifiers))" + DispatchQueue.global(qos: .userInitiated).async { + var errorInfo: NSDictionary? + NSAppleScript(source: source)?.executeAndReturnError(&errorInfo) + let failure: String? = errorInfo.map { "\($0)" } + Task { @MainActor [weak self] in + guard let self else { return } + if let failure { TBInputDebugLog.log("system-events inject error: \(failure)") } + for keyCode in heldKeyCodes { self.postLocalKey(keyCode: keyCode, isDown: true) } + } + } + } + + private func currentlyHeldModifierKeyCodes() -> [UInt16] { + var codes: [UInt16] = [] + if injectedControlDown { codes.append(59) } + if injectedOptionDown { codes.append(58) } + if injectedShiftDown { codes.append(56) } + if injectedCommandDown { codes.append(55) } + return codes + } + + private static func appleScriptModifierClause(_ modifiers: UInt32) -> String { + var parts: [String] = [] + if modifiers & TBInputShortcut.control != 0 { parts.append("control down") } + if modifiers & TBInputShortcut.option != 0 { parts.append("option down") } + if modifiers & TBInputShortcut.shift != 0 { parts.append("shift down") } + if modifiers & TBInputShortcut.command != 0 { parts.append("command down") } + guard !parts.isEmpty else { return "" } + return " using {" + parts.joined(separator: ", ") + "}" + } + + /// Current held modifier state (our bitmask) reconstructed from injected keys. + private func currentHeldModifierBits() -> UInt32 { + var m: UInt32 = 0 + if injectedControlDown { m |= TBInputShortcut.control } + if injectedOptionDown { m |= TBInputShortcut.option } + if injectedShiftDown { m |= TBInputShortcut.shift } + if injectedCommandDown { m |= TBInputShortcut.command } + return m + } + private func handleDisplayProfile(_ payload: Data) { guard activeProfile == nil, let profile = TBMonitorProtocol.decodeJSON(TBMonitorDisplayProfile.self, from: payload) diff --git a/TargetBridge-Sender/TBDisplaySender/TBInputBinding.swift b/TargetBridge-Sender/TBDisplaySender/TBInputBinding.swift new file mode 100644 index 0000000..51c6431 --- /dev/null +++ b/TargetBridge-Sender/TBDisplaySender/TBInputBinding.swift @@ -0,0 +1,118 @@ +import AppKit +import CoreGraphics +import Foundation + +/// A keyboard shortcut: a key plus a stable modifier bitmask. +/// +/// Modifiers use our own bits (independent of AppKit/CoreGraphics) so they +/// persist reliably: control = 1, option = 2, shift = 4, command = 8. +struct TBInputShortcut: Codable, Equatable, Hashable { + var keyCode: UInt16 + var modifiers: UInt32 + + static let control: UInt32 = 1 << 0 + static let option: UInt32 = 1 << 1 + static let shift: UInt32 = 1 << 2 + static let command: UInt32 = 1 << 3 + + /// (bit, left-hand modifier key code), in a stable nesting order. + static let modifierTable: [(bit: UInt32, keyCode: UInt16)] = [ + (control, 59), + (option, 58), + (shift, 56), + (command, 55) + ] + + var hasModifiers: Bool { modifiers != 0 } + + static func modifiers(from flags: NSEvent.ModifierFlags) -> UInt32 { + var m: UInt32 = 0 + if flags.contains(.control) { m |= control } + if flags.contains(.option) { m |= option } + if flags.contains(.shift) { m |= shift } + if flags.contains(.command) { m |= command } + return m + } + + /// Display string such as `⌃⌥←` or `⌘⇧A`. + var displayString: String { + var s = "" + if modifiers & Self.control != 0 { s += "⌃" } + if modifiers & Self.option != 0 { s += "⌥" } + if modifiers & Self.shift != 0 { s += "⇧" } + if modifiers & Self.command != 0 { s += "⌘" } + s += TBKeyCodeNames.name(for: keyCode) + return s + } +} + +/// A per-session binding: pressing `trigger` on the master injects `action` on +/// the slave (direction follows the session's input master role). +struct TBInputBinding: Codable, Identifiable, Equatable { + var id: UUID = UUID() + var trigger: TBInputShortcut + var action: TBInputShortcut + var enabled: Bool = true +} + +enum TBInputBindingEngine { + /// Build the self-contained key sequence that injects `action` while the + /// master currently holds `heldModifiers`. Returns `(keyCode, isDown)` steps. + /// + /// It temporarily releases held modifiers the action doesn't use, presses the + /// action's missing modifiers, taps the action key, then restores the held + /// state — so the slave sees exactly the action's modifiers (e.g. trigger + /// `⌃⌥←` can inject a clean `⌃←`), and modifier tracking ends where it began. + static func injectionSequence(heldModifiers: UInt32, action: TBInputShortcut) -> [(keyCode: UInt16, isDown: Bool)] { + let toRelease = TBInputShortcut.modifierTable.filter { + heldModifiers & $0.bit != 0 && action.modifiers & $0.bit == 0 + } + let toPress = TBInputShortcut.modifierTable.filter { + action.modifiers & $0.bit != 0 && heldModifiers & $0.bit == 0 + } + + var seq: [(keyCode: UInt16, isDown: Bool)] = [] + for mod in toRelease.reversed() { seq.append((mod.keyCode, false)) } + for mod in toPress { seq.append((mod.keyCode, true)) } + seq.append((action.keyCode, true)) + seq.append((action.keyCode, false)) + for mod in toPress.reversed() { seq.append((mod.keyCode, false)) } + for mod in toRelease { seq.append((mod.keyCode, true)) } + return seq + } + + static func isModifierKeyCode(_ keyCode: UInt16) -> Bool { + switch keyCode { + case 54, 55, 56, 57, 58, 59, 60, 61, 62: return true + default: return false + } + } + + /// Find the enabled binding whose trigger matches `keyCode` + exact `modifiers`. + static func match(keyCode: UInt16, modifiers: UInt32, in bindings: [TBInputBinding]) -> TBInputBinding? { + bindings.first { $0.enabled && $0.trigger.keyCode == keyCode && $0.trigger.modifiers == modifiers } + } +} + +/// Human-readable names for common virtual key codes (for shortcut display). +enum TBKeyCodeNames { + private static let table: [UInt16: String] = [ + 123: "←", 124: "→", 125: "↓", 126: "↑", + 36: "↩", 48: "⇥", 49: "Space", 51: "⌫", 53: "⎋", 117: "⌦", + 115: "↖", 119: "↘", 116: "⇞", 121: "⇟", + 122: "F1", 120: "F2", 99: "F3", 118: "F4", 96: "F5", 97: "F6", + 98: "F7", 100: "F8", 101: "F9", 109: "F10", 103: "F11", 111: "F12", + 0: "A", 11: "B", 8: "C", 2: "D", 14: "E", 3: "F", 5: "G", 4: "H", + 34: "I", 38: "J", 40: "K", 37: "L", 46: "M", 45: "N", 31: "O", 35: "P", + 12: "Q", 15: "R", 1: "S", 17: "T", 32: "U", 9: "V", 13: "W", 7: "X", + 16: "Y", 6: "Z", + 29: "0", 18: "1", 19: "2", 20: "3", 21: "4", 23: "5", 22: "6", + 26: "7", 28: "8", 25: "9", + 27: "-", 24: "=", 33: "[", 30: "]", 41: ";", 39: "'", 43: ",", + 47: ".", 44: "/", 42: "\\", 50: "`" + ] + + static func name(for keyCode: UInt16) -> String { + table[keyCode] ?? "key\(keyCode)" + } +} diff --git a/TargetBridge-Sender/TBDisplaySender/TBInputBindingsView.swift b/TargetBridge-Sender/TBDisplaySender/TBInputBindingsView.swift new file mode 100644 index 0000000..aa1e6de --- /dev/null +++ b/TargetBridge-Sender/TBDisplaySender/TBInputBindingsView.swift @@ -0,0 +1,114 @@ +import AppKit +import SwiftUI + +/// Per-session editor for shortcut bindings (trigger on master → action on slave). +struct TBInputBindingsView: View { + @ObservedObject var session: TBDisplaySenderSession + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + HStack { + Text("Shortcut bindings") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(.secondary) + Spacer() + Button { + session.inputBindings.append( + TBInputBinding( + trigger: TBInputShortcut(keyCode: 123, modifiers: TBInputShortcut.control | TBInputShortcut.option), + action: TBInputShortcut(keyCode: 123, modifiers: TBInputShortcut.control) + ) + ) + } label: { + Label("Add", systemImage: "plus") + } + .buttonStyle(.bordered) + .controlSize(.small) + } + + if session.inputBindings.isEmpty { + Text("No bindings. Add one to map a trigger you press on the master to a shortcut injected on the slave.") + .font(.footnote) + .foregroundStyle(.secondary) + } else { + ForEach($session.inputBindings) { $binding in + HStack(spacing: 8) { + Toggle("", isOn: $binding.enabled) + .labelsHidden() + .toggleStyle(.checkbox) + TBShortcutRecorderButton(shortcut: $binding.trigger) + Image(systemName: "arrow.right") + .font(.caption) + .foregroundStyle(.secondary) + TBShortcutRecorderButton(shortcut: $binding.action) + Spacer() + Button(role: .destructive) { + session.inputBindings.removeAll { $0.id == binding.id } + } label: { + Image(systemName: "trash") + } + .buttonStyle(.borderless) + } + } + } + + Text("Trigger is pressed on the master; Action is injected on the slave (direction follows the input role). A trigger can't be a combo macOS reserves (e.g. ⌃← for Spaces) — the OS grabs it first; use something like ⌃⌥←. The Action may be any combo, including reserved ones, since it fires the slave's own shortcut (e.g. ⌃← to switch the slave's Space). Bindings apply while the session is connected.") + .font(.footnote) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } +} + +/// A button that records a single key combo when clicked (press the shortcut). +struct TBShortcutRecorderButton: View { + @Binding var shortcut: TBInputShortcut + @State private var recording = false + @State private var monitor: Any? + + var body: some View { + Button { + toggleRecording() + } label: { + Text(recording ? "Press…" : shortcut.displayString) + .font(.system(.body, design: .rounded).monospacedDigit()) + .frame(minWidth: 64) + } + .buttonStyle(.bordered) + .controlSize(.small) + .tint(recording ? .accentColor : nil) + .onDisappear(perform: stopRecording) + } + + private func toggleRecording() { + if recording { + stopRecording() + } else { + startRecording() + } + } + + private func startRecording() { + recording = true + monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in + // Ignore standalone modifier presses; require a real key. + if TBInputBindingEngine.isModifierKeyCode(event.keyCode) { + return nil + } + shortcut = TBInputShortcut( + keyCode: event.keyCode, + modifiers: TBInputShortcut.modifiers(from: event.modifierFlags) + ) + stopRecording() + return nil // swallow so the captured key doesn't act + } + } + + private func stopRecording() { + if let monitor { + NSEvent.removeMonitor(monitor) + self.monitor = nil + } + recording = false + } +} diff --git a/TargetBridge-Sender/TBDisplaySender/TBInputRelayController.swift b/TargetBridge-Sender/TBDisplaySender/TBInputRelayController.swift index b0fd8f3..6560c22 100644 --- a/TargetBridge-Sender/TBDisplaySender/TBInputRelayController.swift +++ b/TargetBridge-Sender/TBDisplaySender/TBInputRelayController.swift @@ -12,9 +12,16 @@ final class TBInputRelayController { private var localMonitors: [Any] = [] private var globalMonitors: [Any] = [] + /// Given a captured key-down (keyCode + our modifier bitmask), returns the + /// bracketed key sequence to forward instead (binding action), or nil if no + /// binding matches. Set by the manager from the session's bindings. + typealias TriggerMatcher = (_ keyCode: UInt16, _ modifiers: UInt32) -> [(keyCode: UInt16, isDown: Bool)]? + private var handler: Handler? private var switchHandler: SwitchHandler? private var deactivateHandler: DeactivateHandler? + private var triggerMatcher: TriggerMatcher? + private var suppressedTriggerKeyCode: UInt16? private var gestureMode: TBInputGestureMode = .native private var lastEdgeSwitchTime: TimeInterval = 0 private var activityToken: NSObjectProtocol? @@ -23,13 +30,15 @@ final class TBInputRelayController { gestureMode: TBInputGestureMode, handler: @escaping Handler, switchHandler: @escaping SwitchHandler, - deactivateHandler: @escaping DeactivateHandler + deactivateHandler: @escaping DeactivateHandler, + triggerMatcher: TriggerMatcher? = nil ) { stop() self.gestureMode = gestureMode self.handler = handler self.switchHandler = switchHandler self.deactivateHandler = deactivateHandler + self.triggerMatcher = triggerMatcher beginKeepAwakeActivity() installKeyboardMonitors() @@ -50,6 +59,8 @@ final class TBInputRelayController { handler = nil switchHandler = nil deactivateHandler = nil + triggerMatcher = nil + suppressedTriggerKeyCode = nil } private func installKeyboardMonitors() { @@ -125,10 +136,40 @@ final class TBInputRelayController { } private func handle(_ event: NSEvent) { + if handleBindingTrigger(event) { return } guard let handler, let relayEvent = convert(event) else { return } handler(relayEvent) } + /// senderMaster: if a key-down completes a binding trigger, forward the + /// action's bracketed key sequence instead of the trigger, and swallow the + /// trigger's key-up. Returns true if the event was handled here. + private func handleBindingTrigger(_ event: NSEvent) -> Bool { + guard let handler, triggerMatcher != nil else { return false } + switch event.type { + case .keyUp: + if event.keyCode == suppressedTriggerKeyCode { + suppressedTriggerKeyCode = nil + return true + } + return false + case .keyDown: + if event.keyCode == suppressedTriggerKeyCode { return true } // debounce repeat + let modifiers = TBInputShortcut.modifiers(from: event.modifierFlags) + guard let sequence = triggerMatcher?(event.keyCode, modifiers) else { return false } + suppressedTriggerKeyCode = event.keyCode + for step in sequence { + handler(TBMonitorInputEvent( + kind: step.isDown ? "keyDown" : "keyUp", + dx: nil, dy: nil, scrollX: nil, scrollY: nil, keyCode: step.keyCode + )) + } + return true + default: + return false + } + } + private func convert(_ event: NSEvent) -> TBMonitorInputEvent? { switch event.type { case .mouseMoved: diff --git a/TargetBridge-Sender/TargetBridge.xcodeproj/project.pbxproj b/TargetBridge-Sender/TargetBridge.xcodeproj/project.pbxproj index 7a3bdb3..207439a 100644 --- a/TargetBridge-Sender/TargetBridge.xcodeproj/project.pbxproj +++ b/TargetBridge-Sender/TargetBridge.xcodeproj/project.pbxproj @@ -14,11 +14,13 @@ 337BD561622AA19F43D3B95B /* ReceiverBackedVirtualDisplaySession.swift in Sources */ = {isa = PBXBuildFile; fileRef = C242540F9DE94E1364F106F4 /* ReceiverBackedVirtualDisplaySession.swift */; }; 4290B6572B8CA96E01C56425 /* TBDisplaySenderBuildInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB4DF6C72B5C50C29AE6E45 /* TBDisplaySenderBuildInfo.swift */; }; 4B770D1232C53EF66F011F7F /* TBAddonManifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59E4D00FB492A41B5218CD52 /* TBAddonManifest.swift */; }; + 5A8811B2F7DB1830CBA7B538 /* TBInputBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A72C42968FA2A6B2A0515B9 /* TBInputBinding.swift */; }; 5C0CB53005B6D67362F14719 /* TBDisplaySenderContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 186812C77BFF98DBE0118100 /* TBDisplaySenderContentView.swift */; }; 6A54029FE56DEECA996A07B4 /* TBDisplaySenderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B483A658326FE85720BCBE2 /* TBDisplaySenderService.swift */; }; 7F1AE9053DA59169FB761FAA /* de.json in Resources */ = {isa = PBXBuildFile; fileRef = C3658E86F5BE2168F4591491 /* de.json */; }; 7FB34AA1E75B01E7D1EC3E21 /* it.json in Resources */ = {isa = PBXBuildFile; fileRef = 036E14097FA59989FFC456FD /* it.json */; }; 8A729C68ECAC4642FA20A284 /* TBLocalizationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C932E960346E674A0659624 /* TBLocalizationStore.swift */; }; + 8D544352D7E2E226D551E46E /* TBInputBindingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20FE01BF2EA2F329442890AD /* TBInputBindingsView.swift */; }; 8D9D2A40A1CD90BD4D4DFC2C /* TBDisplaySenderAutomation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CEA5EB707F01E1708DA9170 /* TBDisplaySenderAutomation.swift */; }; 96DDF119DE60DFBA437D32D2 /* en.json in Resources */ = {isa = PBXBuildFile; fileRef = D386D0FCD0F93494220CC3DC /* en.json */; }; A3FD74604472363535D63273 /* TBDisplaySenderSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B95D015FB8DF172C7418CB5D /* TBDisplaySenderSettingsView.swift */; }; @@ -44,8 +46,10 @@ 17FD38CAA4E2CF350CC210D2 /* TBDisplaySender.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TBDisplaySender.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1819A55F8D993D9347F0E407 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 186812C77BFF98DBE0118100 /* TBDisplaySenderContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TBDisplaySenderContentView.swift; sourceTree = ""; }; + 20FE01BF2EA2F329442890AD /* TBInputBindingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TBInputBindingsView.swift; sourceTree = ""; }; 374B5A5AA247C6922BD9AF72 /* TBDisplaySenderAboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TBDisplaySenderAboutView.swift; sourceTree = ""; }; 4644CFB05098C895378B648A /* TBDisplaySenderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TBDisplaySenderManager.swift; sourceTree = ""; }; + 4A72C42968FA2A6B2A0515B9 /* TBInputBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TBInputBinding.swift; sourceTree = ""; }; 4C932E960346E674A0659624 /* TBLocalizationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TBLocalizationStore.swift; sourceTree = ""; }; 59E4D00FB492A41B5218CD52 /* TBAddonManifest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TBAddonManifest.swift; sourceTree = ""; }; 6579B499CBC11196966318EA /* network-link.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "network-link.json"; sourceTree = ""; }; @@ -93,6 +97,8 @@ B95D015FB8DF172C7418CB5D /* TBDisplaySenderSettingsView.swift */, 6AC1AF99431D4889F947CE1B /* TBDisplaySenderStatusItemController.swift */, DD33D06A2FA6BA7E075A9E46 /* TBDisplaySenderSurfaceViews.swift */, + 4A72C42968FA2A6B2A0515B9 /* TBInputBinding.swift */, + 20FE01BF2EA2F329442890AD /* TBInputBindingsView.swift */, 7E3DC447E3E8E522565E725B /* TBInputRelayController.swift */, 06BABA1447D1E8DD50A6C2F0 /* TBReceiverDiscovery.swift */, ); @@ -269,6 +275,8 @@ A3FD74604472363535D63273 /* TBDisplaySenderSettingsView.swift in Sources */, E9F7A263085592067562F768 /* TBDisplaySenderStatusItemController.swift in Sources */, BB4D986A5A0F32BC1480BD4D /* TBDisplaySenderSurfaceViews.swift in Sources */, + 5A8811B2F7DB1830CBA7B538 /* TBInputBinding.swift in Sources */, + 8D544352D7E2E226D551E46E /* TBInputBindingsView.swift in Sources */, D5E6B5B7B49E740C92010C6F /* TBInputDebugLog.swift in Sources */, CBA7C2F0FA57CC475E2307AF /* TBInputRelayController.swift in Sources */, 8A729C68ECAC4642FA20A284 /* TBLocalizationStore.swift in Sources */,