1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-05 11:13:44 +00:00

[PM-22785] Initial push with configuration and ipc changes for the configurable autotype keyboard shortcut

This commit is contained in:
Colton Hurst
2025-09-24 13:52:15 -04:00
parent 7d6ff4394d
commit e880338704
8 changed files with 162 additions and 20 deletions

View File

@@ -17,6 +17,6 @@ pub fn get_foreground_window_title() -> std::result::Result<String, ()> {
///
/// TODO: The error handling will be improved in a future PR: PM-23615
#[allow(clippy::result_unit_err)]
pub fn type_input(input: Vec<u16>) -> std::result::Result<(), ()> {
windowing::type_input(input)
pub fn type_input(input: Vec<u16>, keyboardShortcut: Vec<String>) -> std::result::Result<(), ()> {
windowing::type_input(input, keyboardShortcut)
}

View File

@@ -28,7 +28,9 @@ pub fn get_foreground_window_title() -> std::result::Result<String, ()> {
/// `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<u16>) -> Result<(), ()> {
pub fn type_input(input: Vec<u16>, keyboard_input: Vec<String>) -> Result<(), ()> {
println!("type_input() hit, keyboardInput is: {:?}", keyboard_input);
const TAB_KEY: u16 = 9;
let mut keyboard_inputs: Vec<INPUT> = Vec::new();

View File

@@ -234,5 +234,5 @@ export declare namespace chromium_importer {
}
export declare namespace autotype {
export function getForegroundWindowTitle(): string
export function typeInput(input: Array<number>): void
export function typeInput(input: Array<number>, keyboardShortcut: Array<string>): void
}

View File

@@ -1044,8 +1044,8 @@ pub mod autotype {
}
#[napi]
pub fn type_input(input: Vec<u16>) -> napi::Result<(), napi::Status> {
autotype::type_input(input).map_err(|_| {
pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> napi::Result<(), napi::Status> {
autotype::type_input(input, keyboard_shortcut).map_err(|_| {
napi::Error::from_reason("Autotype Error: failed to type input".to_string())
})
}

View File

@@ -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<number>(inputPattern.length);
@@ -65,6 +73,6 @@ export class MainDesktopAutotypeService {
inputArray[i] = inputPattern.charCodeAt(i);
}
autotype.typeInput(inputArray);
autotype.typeInput(inputArray, keyboardShortcut);
}
}

View File

@@ -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;
}
}

View File

@@ -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: (

View File

@@ -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<boolean>(
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<string[]>(
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<boolean> = of(false);
resolvedAutotypeEnabled$: Observable<boolean> = of(false);
autotypeKeyboardShortcut$: Observable<string[]> = 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<void> {
/////
// 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<void> {
await this.autotypeKeyboardShortcut.update(() => keyboardShortcut);
}
async matchCiphersToWindowTitle(windowTitle: string): Promise<CipherView[]> {
const URI_PREFIX = "apptitle://";
windowTitle = windowTitle.toLowerCase();