From e880338704ff0b5111d22507e35490412e54f34a Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Wed, 24 Sep 2025 13:52:15 -0400 Subject: [PATCH] [PM-22785] Initial push with configuration and ipc changes for the configurable autotype keyboard shortcut --- .../desktop_native/autotype/src/lib.rs | 4 +- .../desktop_native/autotype/src/windows.rs | 4 +- apps/desktop/desktop_native/napi/index.d.ts | 2 +- apps/desktop/desktop_native/napi/src/lib.rs | 4 +- .../main/main-desktop-autotype.service.ts | 28 +++--- .../models/main-autotype-keyboard-shortcut.ts | 90 +++++++++++++++++++ apps/desktop/src/autofill/preload.ts | 4 +- .../services/desktop-autotype.service.ts | 46 +++++++++- 8 files changed, 162 insertions(+), 20 deletions(-) create mode 100644 apps/desktop/src/autofill/models/main-autotype-keyboard-shortcut.ts diff --git a/apps/desktop/desktop_native/autotype/src/lib.rs b/apps/desktop/desktop_native/autotype/src/lib.rs index 6d7b9f9db85..6c3c446fd3f 100644 --- a/apps/desktop/desktop_native/autotype/src/lib.rs +++ b/apps/desktop/desktop_native/autotype/src/lib.rs @@ -17,6 +17,6 @@ pub fn get_foreground_window_title() -> std::result::Result { /// /// TODO: The error handling will be improved in a future PR: PM-23615 #[allow(clippy::result_unit_err)] -pub fn type_input(input: Vec) -> std::result::Result<(), ()> { - windowing::type_input(input) +pub fn type_input(input: Vec, keyboardShortcut: Vec) -> std::result::Result<(), ()> { + windowing::type_input(input, keyboardShortcut) } diff --git a/apps/desktop/desktop_native/autotype/src/windows.rs b/apps/desktop/desktop_native/autotype/src/windows.rs index f1f9bee7f60..7ad8b2ed092 100644 --- a/apps/desktop/desktop_native/autotype/src/windows.rs +++ b/apps/desktop/desktop_native/autotype/src/windows.rs @@ -28,7 +28,9 @@ pub fn get_foreground_window_title() -> std::result::Result { /// `input` must be an array of utf-16 encoded characters to insert. /// /// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput -pub fn type_input(input: Vec) -> Result<(), ()> { +pub fn type_input(input: Vec, keyboard_input: Vec) -> Result<(), ()> { + println!("type_input() hit, keyboardInput is: {:?}", keyboard_input); + const TAB_KEY: u16 = 9; let mut keyboard_inputs: Vec = Vec::new(); diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 2212c03f4f8..030bf4c964d 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -234,5 +234,5 @@ export declare namespace chromium_importer { } export declare namespace autotype { export function getForegroundWindowTitle(): string - export function typeInput(input: Array): void + export function typeInput(input: Array, keyboardShortcut: Array): void } diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 4731166852b..23f6a581b3d 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -1044,8 +1044,8 @@ pub mod autotype { } #[napi] - pub fn type_input(input: Vec) -> napi::Result<(), napi::Status> { - autotype::type_input(input).map_err(|_| { + pub fn type_input(input: Vec, keyboard_shortcut: Vec) -> napi::Result<(), napi::Status> { + autotype::type_input(input, keyboard_shortcut).map_err(|_| { napi::Error::from_reason("Autotype Error: failed to type input".to_string()) }) } diff --git a/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts b/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts index 884a4fc1ce4..265263fadf8 100644 --- a/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts @@ -7,18 +7,26 @@ import { WindowMain } from "../../main/window.main"; import { stringIsNotUndefinedNullAndEmpty } from "../../utils"; export class MainDesktopAutotypeService { - keySequence: string = "CommandOrControl+Shift+B"; + autotypeKeyboardShortcut: AutotypeKeyboardShortcut; constructor( private logService: LogService, private windowMain: WindowMain, - ) {} + ) { + this.autotypeKeyboardShortcut = new AutotypeKeyboardShortcut(); + } init() { ipcMain.on("autofill.configureAutotype", (event, data) => { - if (data.enabled === true && !globalShortcut.isRegistered(this.keySequence)) { + const { response } = data; + + let setCorrectly = this.autotypeKeyboardShortcut.set(response.keyboardShortcut); + console.log("Was autotypeKeyboardShortcut set correctly from within the main process? " + setCorrectly); + // TODO: What do we do if it wasn't? The value won't change but we need to send a failure message back + + if (response.enabled === true && !globalShortcut.isRegistered(this.autotypeKeyboardShortcut.getElectronFormat())) { this.enableAutotype(); - } else if (data.enabled === false && globalShortcut.isRegistered(this.keySequence)) { + } else if (response.enabled === false && globalShortcut.isRegistered(this.autotypeKeyboardShortcut.getElectronFormat())) { this.disableAutotype(); } }); @@ -30,21 +38,21 @@ export class MainDesktopAutotypeService { stringIsNotUndefinedNullAndEmpty(response.username) && stringIsNotUndefinedNullAndEmpty(response.password) ) { - this.doAutotype(response.username, response.password); + this.doAutotype(response.username, response.password, this.autotypeKeyboardShortcut.getArrayFormat()); } }); } disableAutotype() { - if (globalShortcut.isRegistered(this.keySequence)) { - globalShortcut.unregister(this.keySequence); + if (globalShortcut.isRegistered(this.autotypeKeyboardShortcut.getElectronFormat())) { + globalShortcut.unregister(this.autotypeKeyboardShortcut.getElectronFormat()); } this.logService.info("Autotype disabled."); } private enableAutotype() { - const result = globalShortcut.register(this.keySequence, () => { + const result = globalShortcut.register(this.autotypeKeyboardShortcut.getElectronFormat(), () => { const windowTitle = autotype.getForegroundWindowTitle(); this.windowMain.win.webContents.send("autofill.listenAutotypeRequest", { @@ -57,7 +65,7 @@ export class MainDesktopAutotypeService { : this.logService.info("Enabling autotype failed."); } - private doAutotype(username: string, password: string) { + private doAutotype(username: string, password: string, keyboardShortcut: string[]) { const inputPattern = username + "\t" + password; const inputArray = new Array(inputPattern.length); @@ -65,6 +73,6 @@ export class MainDesktopAutotypeService { inputArray[i] = inputPattern.charCodeAt(i); } - autotype.typeInput(inputArray); + autotype.typeInput(inputArray, keyboardShortcut); } } diff --git a/apps/desktop/src/autofill/models/main-autotype-keyboard-shortcut.ts b/apps/desktop/src/autofill/models/main-autotype-keyboard-shortcut.ts new file mode 100644 index 00000000000..7571da11ca0 --- /dev/null +++ b/apps/desktop/src/autofill/models/main-autotype-keyboard-shortcut.ts @@ -0,0 +1,90 @@ +/* + This class provides the following: + - A way to get and set an AutotypeKeyboardShortcut value within the main process + - A way to set an AutotypeKeyboardShortcut with validation + - A way to "get" the value in string array format or a single string format for electron + - Default shortcut support + + This is currently only supported for Windows operating systems. +*/ +class AutotypeKeyboardShortcut { + private readonly defaultWindowsAutotypeKeyboardShorcut: string[] = ["Control", "Shift", "B"]; + private autotypeKeyboardShortcut: string[]; + + constructor() { + this.autotypeKeyboardShortcut = this.defaultWindowsAutotypeKeyboardShorcut; + } + + /* + Returns a boolean value indicating if the autotypeKeyboardShortcut + was valid and set or not. + */ + set(newAutotypeKeyboardShortcut: string[]) { + if (!this.#keyboardShortcutIsValid(newAutotypeKeyboardShortcut)) { + return false; + } + + this.autotypeKeyboardShortcut = newAutotypeKeyboardShortcut; + return true; + } + + /* + Returns the autotype keyboard shortcut as a string array. + */ + getArrayFormat() { + return this.autotypeKeyboardShortcut; + } + + /* + Returns the autotype keyboard shortcut as a single string, as + Electron expects. Please note this does not reorder the keys. + + See Electron keyboard shorcut docs for more info: + https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts + */ + getElectronFormat() { + return this.autotypeKeyboardShortcut.join("+"); + } + + /* + This private function validates the strArray input to make sure the array contains + valid, currently accepted shortcut keys for Windows. + + Valid windows shortcut keys: Control, Alt, Super, Shift, letters A - Z + Valid macOS shortcut keys: Control, Alt, Command, Shift, letters A - Z (not yet supported) + + See Electron keyboard shorcut docs for more info: + https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts + */ + #keyboardShortcutIsValid(strArray: string[]) { + const VALID_SHORTCUT_CONTROL_KEYS: string[] = ["Control", "Alt", "Super", "Shift"]; + const UNICODE_LOWER_BOUND = 65; // 'A' in base 10 + const UNICODE_UPPER_BOUND = 90; // 'Z' in base 10 + const MIN_LENGTH: number = 2; + const MAX_LENGTH: number = 3; + + // Ensure strArray is a string array of valid length + if (strArray === undefined || strArray === null || strArray.length < MIN_LENGTH || strArray.length > MAX_LENGTH) { + return false; + } + + // Ensure strArray is all modifier keys, and that the last key is a modifier key OR a letter key + for (let i = 0; i < strArray.length; i++) { + if (i < strArray.length - 1) { + if (!VALID_SHORTCUT_CONTROL_KEYS.includes(strArray[i])) { + return false; + } + } else { + if (!VALID_SHORTCUT_CONTROL_KEYS.includes(strArray[i])) { + let unicodeValue: number = strArray[i].charCodeAt(0); + + if (Number.isNaN(unicodeValue) || unicodeValue < UNICODE_LOWER_BOUND || unicodeValue > UNICODE_UPPER_BOUND) { + return false; + } + } + } + } + + return true; + } +} diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index af238b17e80..fcb2f646743 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -127,8 +127,8 @@ export default { }, ); }, - configureAutotype: (enabled: boolean) => { - ipcRenderer.send("autofill.configureAutotype", { enabled }); + configureAutotype: (enabled: boolean, keyboardShortcut: string[]) => { + ipcRenderer.send("autofill.configureAutotype", { enabled, keyboardShortcut }); }, listenAutotypeRequest: ( fn: ( diff --git a/apps/desktop/src/autofill/services/desktop-autotype.service.ts b/apps/desktop/src/autofill/services/desktop-autotype.service.ts index 60e87aa2aa5..b44f84205ba 100644 --- a/apps/desktop/src/autofill/services/desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autotype.service.ts @@ -17,17 +17,34 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { UserId } from "@bitwarden/user-core"; +// Represents the user's set autotypeEnabled setting export const AUTOTYPE_ENABLED = new KeyDefinition( AUTOTYPE_SETTINGS_DISK, "autotypeEnabled", { deserializer: (b) => b }, ); +/* + Valid windows shortcut keys: Control, Alt, Super, Shift, letters A - Z + Valid macOS shortcut keys: Control, Alt, Command, Shift, letters A - Z + + See Electron keyboard shorcut docs for more info: + https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts +*/ +export const AUTOTYPE_KEYBOARD_SHORTCUT = new KeyDefinition( + AUTOTYPE_SETTINGS_DISK, + "autotypeKeyboardShortcut", + { deserializer: (b) => b }, +); + export class DesktopAutotypeService { + private readonly defaultWindowsAutotypeKeyboardShorcut: string[] = ["Control", "Shift", "B"]; private readonly autotypeEnabledState = this.globalStateProvider.get(AUTOTYPE_ENABLED); + private readonly autotypeKeyboardShortcut = this.globalStateProvider.get(AUTOTYPE_KEYBOARD_SHORTCUT); autotypeEnabledUserSetting$: Observable = of(false); resolvedAutotypeEnabled$: Observable = of(false); + autotypeKeyboardShortcut$: Observable = of([]); constructor( private accountService: AccountService, @@ -51,6 +68,7 @@ export class DesktopAutotypeService { async init() { this.autotypeEnabledUserSetting$ = this.autotypeEnabledState.state$; + this.autotypeKeyboardShortcut$ = this.autotypeKeyboardShortcut.state$; if (this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop) { this.resolvedAutotypeEnabled$ = combineLatest([ @@ -76,18 +94,42 @@ export class DesktopAutotypeService { ), ); - this.resolvedAutotypeEnabled$.subscribe((enabled) => { - ipc.autofill.configureAutotype(enabled); + combineLatest([this.resolvedAutotypeEnabled$, this.autotypeKeyboardShortcut$]).subscribe(([resolvedAutotypeEnabled, autotypeKeyboaardShortcut]) => { + ipc.autofill.configureAutotype(resolvedAutotypeEnabled, autotypeKeyboaardShortcut); }); } } async setAutotypeEnabledState(enabled: boolean): Promise { + + ///// + // let's make sure the storage works + console.log("----- setAutotypeEnabledState() -----"); + let currentValue = await firstValueFrom(this.autotypeKeyboardShortcut.state$); + console.log("autotypeKeyboardShortcut current value: " + currentValue); + + if (currentValue != undefined && currentValue != null && currentValue.length > 0) { + //console.log("clearing the value"); + //await this.setAutotypeKeyboardShortcutState([]); + //let newValue = await firstValueFrom(this.autotypeKeyboardShortcut.state$); + //console.log("autotypeKeyboardShortcut new value: " + newValue); + } else { + console.log("setting the value"); + await this.setAutotypeKeyboardShortcutState(this.defaultWindowsAutotypeKeyboardShorcut); + let newValue = await firstValueFrom(this.autotypeKeyboardShortcut.state$); + console.log("autotypeKeyboardShortcut new value: " + newValue); + } + ///// + await this.autotypeEnabledState.update(() => enabled, { shouldUpdate: (currentlyEnabled) => currentlyEnabled !== enabled, }); } + async setAutotypeKeyboardShortcutState(keyboardShortcut: string[]): Promise { + await this.autotypeKeyboardShortcut.update(() => keyboardShortcut); + } + async matchCiphersToWindowTitle(windowTitle: string): Promise { const URI_PREFIX = "apptitle://"; windowTitle = windowTitle.toLowerCase();