=> {
- await app.hide();
- await window.getCurrentWindow().hide();
-};
+const { hideApp } = useAppControl();
const focusSearchInput = (): void => {
nextTick(() => {
@@ -628,6 +618,24 @@ onMounted(async () => {
?.viewport?.addEventListener("scroll", handleScroll);
await setupEventListeners();
+
+ $keyboard.setupAppShortcuts({
+ onNavigateDown: selectNext,
+ onNavigateUp: selectPrevious,
+ onSelect: pasteSelectedItem,
+ onEscape: () => {
+ if (isActionsMenuVisible.value) {
+ closeActionsMenu();
+ } else {
+ hideApp();
+ }
+ },
+ onToggleActions: toggleActionsMenu,
+ contextName: 'main',
+ priority: $keyboard.PRIORITY.HIGH
+ });
+ $keyboard.disableContext('settings');
+ $keyboard.enableContext('main');
} catch (error) {
console.error("Error during onMounted:", error);
}
diff --git a/pages/settings.vue b/pages/settings.vue
index f67a52b..4bbf980 100644
--- a/pages/settings.vue
+++ b/pages/settings.vue
@@ -45,6 +45,7 @@
>(new Set());
const isKeybindInputFocused = ref(false);
const keybind = ref([]);
const keybindInput = ref(null);
const lastBlurTime = ref(0);
+const blurredByEscape = ref(false);
const os = ref("");
const router = useRouter();
const showEmptyKeybindError = ref(false);
const autostart = ref(false);
-const { $settings } = useNuxtApp();
+const { $settings, $keyboard } = useNuxtApp();
const modifierKeySet = new Set([
KeyValues.AltLeft,
@@ -127,6 +129,7 @@ const onBlur = () => {
const onFocus = () => {
isKeybindInputFocused.value = true;
+ blurredByEscape.value = false;
activeModifiers.clear();
keybind.value = [];
showEmptyKeybindError.value = false;
@@ -137,7 +140,10 @@ const onKeyDown = (event: KeyboardEvent) => {
if (key === KeyValues.Escape) {
if (keybindInput.value) {
+ blurredByEscape.value = true;
keybindInput.value.blur();
+ event.preventDefault();
+ event.stopPropagation();
}
return;
}
@@ -174,56 +180,64 @@ const toggleAutostart = async () => {
os.value = platform();
onMounted(async () => {
- keyboard.prevent.down([Key.All], (event: KeyboardEvent) => {
- if (isKeybindInputFocused.value) {
- onKeyDown(event);
+ $keyboard.setupKeybindCapture({
+ onCapture: (key: string) => {
+ if (isKeybindInputFocused.value) {
+ const keyValue = key as KeyValues;
+
+ if (isModifier(keyValue)) {
+ activeModifiers.add(keyValue);
+ } else if (!keybind.value.includes(keyValue)) {
+ keybind.value = keybind.value.filter((k) => isModifier(k));
+ keybind.value.push(keyValue);
+ }
+
+ updateKeybind();
+ showEmptyKeybindError.value = false;
+ }
+ },
+ onComplete: () => {
+ if (isKeybindInputFocused.value) {
+ keybindInput.value?.blur();
+ } else {
+ router.push("/");
+ }
}
});
- keyboard.prevent.down([Key.Escape], () => {
- if (isKeybindInputFocused.value) {
- keybindInput.value?.blur();
- } else {
- router.push("/");
- }
- });
-
- switch (os.value) {
- case "macos":
- keyboard.prevent.down([Key.LeftMeta, Key.Enter], () => {
- if (!isKeybindInputFocused.value) {
- saveKeybind();
- }
- });
-
- keyboard.prevent.down([Key.RightMeta, Key.Enter], () => {
- if (!isKeybindInputFocused.value) {
- saveKeybind();
- }
- });
- break;
-
- case "linux":
- case "windows":
- keyboard.prevent.down([Key.LeftControl, Key.Enter], () => {
- if (!isKeybindInputFocused.value) {
- saveKeybind();
- }
- });
-
- keyboard.prevent.down([Key.RightControl, Key.Enter], () => {
- if (!isKeybindInputFocused.value) {
- saveKeybind();
- }
- });
- break;
+ if (os.value === "macos") {
+ $keyboard.on("settings", [$keyboard.Key.LeftMeta, $keyboard.Key.Enter], () => {
+ saveKeybind();
+ }, { priority: $keyboard.PRIORITY.HIGH });
+
+ $keyboard.on("settings", [$keyboard.Key.RightMeta, $keyboard.Key.Enter], () => {
+ saveKeybind();
+ }, { priority: $keyboard.PRIORITY.HIGH });
+ } else {
+ $keyboard.on("settings", [$keyboard.Key.LeftControl, $keyboard.Key.Enter], () => {
+ saveKeybind();
+ }, { priority: $keyboard.PRIORITY.HIGH });
+
+ $keyboard.on("settings", [$keyboard.Key.RightControl, $keyboard.Key.Enter], () => {
+ saveKeybind();
+ }, { priority: $keyboard.PRIORITY.HIGH });
}
+ $keyboard.on("settings", [$keyboard.Key.Escape], () => {
+ if (!isKeybindInputFocused.value && !blurredByEscape.value) {
+ router.push("/");
+ }
+ blurredByEscape.value = false;
+ }, { priority: $keyboard.PRIORITY.HIGH });
+
+ $keyboard.disableContext("main");
+ $keyboard.enableContext("settings");
+
autostart.value = (await $settings.getSetting("autostart")) === "true";
});
onUnmounted(() => {
- keyboard.clear();
+ $keyboard.disableContext("settings");
});
diff --git a/plugins/keyboard.ts b/plugins/keyboard.ts
new file mode 100644
index 0000000..17e8d63
--- /dev/null
+++ b/plugins/keyboard.ts
@@ -0,0 +1,281 @@
+import { Key, keyboard } from "wrdu-keyboard";
+import { platform } from "@tauri-apps/plugin-os";
+
+type KeyboardHandler = (event: KeyboardEvent) => void;
+
+const activeContexts = new Set();
+const handlersByContext: Record<
+ string,
+ Array<{
+ keys: Key[];
+ callback: KeyboardHandler;
+ prevent: boolean;
+ priority?: number;
+ }>
+> = {};
+
+const PRIORITY = {
+ HIGH: 100,
+ MEDIUM: 50,
+ LOW: 0,
+};
+
+let currentOS = "windows";
+
+const useKeyboard = {
+ PRIORITY,
+
+ registerContext: (contextName: string) => {
+ if (!handlersByContext[contextName]) {
+ handlersByContext[contextName] = [];
+ }
+ },
+
+ enableContext: (contextName: string) => {
+ if (!handlersByContext[contextName]) {
+ useKeyboard.registerContext(contextName);
+ }
+ activeContexts.add(contextName);
+
+ initKeyboardHandlers();
+ },
+
+ disableContext: (contextName: string) => {
+ activeContexts.delete(contextName);
+
+ initKeyboardHandlers();
+ },
+
+ on: (
+ contextName: string,
+ keys: Key[],
+ callback: KeyboardHandler,
+ options: { prevent?: boolean; priority?: number } = {}
+ ) => {
+ if (!handlersByContext[contextName]) {
+ useKeyboard.registerContext(contextName);
+ }
+
+ const existingHandlerIndex = handlersByContext[contextName].findIndex(
+ (handler) =>
+ handler.keys.length === keys.length &&
+ handler.keys.every((key, i) => key === keys[i]) &&
+ handler.callback.toString() === callback.toString()
+ );
+
+ if (existingHandlerIndex !== -1) {
+ handlersByContext[contextName][existingHandlerIndex] = {
+ keys,
+ callback,
+ prevent: options.prevent ?? true,
+ priority: options.priority ?? PRIORITY.LOW,
+ };
+ } else {
+ handlersByContext[contextName].push({
+ keys,
+ callback,
+ prevent: options.prevent ?? true,
+ priority: options.priority ?? PRIORITY.LOW,
+ });
+ }
+
+ if (activeContexts.has(contextName)) {
+ initKeyboardHandlers();
+ }
+ },
+
+ clearAll: () => {
+ keyboard.clear();
+ },
+
+ setupAppShortcuts: (options: {
+ onNavigateUp?: () => void;
+ onNavigateDown?: () => void;
+ onSelect?: () => void;
+ onEscape?: () => void;
+ onToggleActions?: () => void;
+ contextName?: string;
+ priority?: number;
+ }) => {
+ const {
+ onNavigateUp,
+ onNavigateDown,
+ onSelect,
+ onEscape,
+ onToggleActions,
+ contextName = "app",
+ priority = PRIORITY.LOW,
+ } = options;
+
+ if (!handlersByContext[contextName]) {
+ useKeyboard.registerContext(contextName);
+ }
+
+ if (onNavigateUp) {
+ useKeyboard.on(contextName, [Key.UpArrow], () => onNavigateUp(), {
+ priority,
+ });
+ }
+
+ if (onNavigateDown) {
+ useKeyboard.on(contextName, [Key.DownArrow], () => onNavigateDown(), {
+ priority,
+ });
+ }
+
+ if (onSelect) {
+ useKeyboard.on(contextName, [Key.Enter], () => onSelect(), { priority });
+ }
+
+ if (onEscape) {
+ useKeyboard.on(contextName, [Key.Escape], () => onEscape(), { priority });
+ }
+
+ if (onToggleActions) {
+ const togglePriority = Math.max(priority, PRIORITY.HIGH);
+
+ if (currentOS === "macos") {
+ useKeyboard.on(
+ contextName,
+ [Key.LeftMeta, Key.K],
+ () => onToggleActions(),
+ { priority: togglePriority }
+ );
+ useKeyboard.on(
+ contextName,
+ [Key.RightMeta, Key.K],
+ () => onToggleActions(),
+ { priority: togglePriority }
+ );
+ } else {
+ useKeyboard.on(
+ contextName,
+ [Key.LeftControl, Key.K],
+ () => onToggleActions(),
+ { priority: togglePriority }
+ );
+ useKeyboard.on(
+ contextName,
+ [Key.RightControl, Key.K],
+ () => onToggleActions(),
+ { priority: togglePriority }
+ );
+ }
+ }
+ },
+
+ setupKeybindCapture: (options: {
+ onCapture: (key: string) => void;
+ onComplete: () => void;
+ }) => {
+ const { onCapture, onComplete } = options;
+
+ keyboard.prevent.down([Key.All], (event: KeyboardEvent) => {
+ if (event.code === "Escape") {
+ onComplete();
+ return;
+ }
+ onCapture(event.code);
+ });
+ },
+};
+
+const initKeyboardHandlers = () => {
+ keyboard.clear();
+
+ let allHandlers: Array<{
+ keys: Key[];
+ callback: KeyboardHandler;
+ prevent: boolean;
+ priority: number;
+ contextName: string;
+ }> = [];
+
+ for (const contextName of activeContexts) {
+ const handlers = handlersByContext[contextName] || [];
+ allHandlers = [
+ ...allHandlers,
+ ...handlers.map((handler) => ({
+ ...handler,
+ priority: handler.priority ?? PRIORITY.LOW,
+ contextName,
+ })),
+ ];
+ }
+
+ allHandlers.sort((a, b) => b.priority - a.priority);
+
+ const handlersByKeyCombination: Record<
+ string,
+ Array<(typeof allHandlers)[0]>
+ > = {};
+
+ allHandlers.forEach((handler) => {
+ const keyCombo = handler.keys.sort().join("+");
+ if (!handlersByKeyCombination[keyCombo]) {
+ handlersByKeyCombination[keyCombo] = [];
+ }
+ handlersByKeyCombination[keyCombo].push(handler);
+ });
+
+ Object.entries(handlersByKeyCombination).forEach(([_keyCombo, handlers]) => {
+ handlers.sort((a, b) => b.priority - a.priority);
+ const handler = handlers[0];
+
+ const wrappedCallback: KeyboardHandler = (event) => {
+ const isMetaCombo =
+ handler.keys.length > 1 &&
+ (handler.keys.includes(Key.LeftMeta) ||
+ handler.keys.includes(Key.RightMeta) ||
+ handler.keys.includes(Key.LeftControl) ||
+ handler.keys.includes(Key.RightControl));
+
+ const isNavigationKey =
+ event.key === "ArrowUp" ||
+ event.key === "ArrowDown" ||
+ event.key === "Enter" ||
+ event.key === "Escape";
+
+ const isInInput =
+ event.target instanceof HTMLInputElement ||
+ event.target instanceof HTMLTextAreaElement;
+
+ if (
+ (isMetaCombo || isNavigationKey || !isInInput) &&
+ activeContexts.has(handler.contextName)
+ ) {
+ handler.callback(event);
+ }
+ };
+
+ if (handler.prevent) {
+ keyboard.prevent.down(handler.keys, wrappedCallback);
+ } else {
+ keyboard.down(handler.keys, wrappedCallback);
+ }
+ });
+};
+
+export default defineNuxtPlugin(async (nuxtApp) => {
+ try {
+ const osName = platform();
+ currentOS = osName.toLowerCase().includes("mac") ? "macos" : "windows";
+ } catch (error) {
+ console.error("Error detecting platform:", error);
+ }
+
+ initKeyboardHandlers();
+
+ nuxtApp.hook("page:finish", () => {
+ initKeyboardHandlers();
+ });
+
+ return {
+ provide: {
+ keyboard: {
+ ...useKeyboard,
+ Key,
+ },
+ },
+ };
+});
diff --git a/plugins/selectedResult.ts b/plugins/selectedResult.ts
new file mode 100644
index 0000000..d03babd
--- /dev/null
+++ b/plugins/selectedResult.ts
@@ -0,0 +1,68 @@
+import { ref, computed } from 'vue';
+import type { HistoryItem } from '~/types/types';
+
+interface GroupedHistory {
+ label: string;
+ items: HistoryItem[];
+}
+
+const selectedGroupIndex = ref(0);
+const selectedItemIndex = ref(0);
+const selectedElement = ref(null);
+
+const useSelectedResult = (groupedHistory: Ref) => {
+ const selectedItem = computed(() => {
+ const group = groupedHistory.value[selectedGroupIndex.value];
+ return group?.items[selectedItemIndex.value] ?? undefined;
+ });
+
+ const isSelected = (groupIndex: number, itemIndex: number): boolean => {
+ return selectedGroupIndex.value === groupIndex && selectedItemIndex.value === itemIndex;
+ };
+
+ const selectNext = (): void => {
+ const currentGroup = groupedHistory.value[selectedGroupIndex.value];
+ if (selectedItemIndex.value < currentGroup.items.length - 1) {
+ selectedItemIndex.value++;
+ } else if (selectedGroupIndex.value < groupedHistory.value.length - 1) {
+ selectedGroupIndex.value++;
+ selectedItemIndex.value = 0;
+ }
+ };
+
+ const selectPrevious = (): void => {
+ if (selectedItemIndex.value > 0) {
+ selectedItemIndex.value--;
+ } else if (selectedGroupIndex.value > 0) {
+ selectedGroupIndex.value--;
+ selectedItemIndex.value = groupedHistory.value[selectedGroupIndex.value].items.length - 1;
+ }
+ };
+
+ const selectItem = (groupIndex: number, itemIndex: number): void => {
+ selectedGroupIndex.value = groupIndex;
+ selectedItemIndex.value = itemIndex;
+ };
+
+ return {
+ selectedItem,
+ isSelected,
+ selectNext,
+ selectPrevious,
+ selectItem,
+ selectedElement
+ };
+};
+
+export default defineNuxtPlugin(() => {
+ return {
+ provide: {
+ selectedResult: {
+ selectedGroupIndex,
+ selectedItemIndex,
+ selectedElement,
+ useSelectedResult
+ }
+ }
+ };
+});
\ No newline at end of file
diff --git a/src-tauri/src/api/clipboard.rs b/src-tauri/src/api/clipboard.rs
index f06881c..5a7d8d3 100644
--- a/src-tauri/src/api/clipboard.rs
+++ b/src-tauri/src/api/clipboard.rs
@@ -230,7 +230,6 @@ pub fn setup(app: &AppHandle) {
}
}
- let _ = app_handle.emit("clipboard-content-updated", ());
let _ = app_handle.track_event(
"clipboard_copied",
Some(
diff --git a/src-tauri/src/db/history.rs b/src-tauri/src/db/history.rs
index 4f70310..a058eeb 100644
--- a/src-tauri/src/db/history.rs
+++ b/src-tauri/src/db/history.rs
@@ -5,6 +5,7 @@ use rand::distr::Alphanumeric;
use sqlx::{ Row, SqlitePool };
use std::fs;
use tauri_plugin_aptabase::EventTracker;
+use tauri::Emitter;
pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box> {
let id: String = rng()
@@ -71,8 +72,12 @@ pub async fn add_history_item(
Some(_) => {
sqlx
::query(
- "UPDATE history SET timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now') WHERE content = ? AND content_type = ?"
+ "UPDATE history SET source = ?, source_icon = ?, timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now'), favicon = ?, language = ? WHERE content = ? AND content_type = ?"
)
+ .bind(&source)
+ .bind(&source_icon)
+ .bind(&favicon)
+ .bind(&language)
.bind(&content)
.bind(&content_type)
.execute(&*pool).await
@@ -102,6 +107,8 @@ pub async fn add_history_item(
"content_type": item.content_type.to_string()
}))
);
+
+ let _ = app_handle.emit("clipboard-content-updated", ());
Ok(())
}
@@ -191,6 +198,7 @@ pub async fn delete_history_item(
.map_err(|e| e.to_string())?;
let _ = app_handle.track_event("history_item_deleted", None);
+ let _ = app_handle.emit("clipboard-content-updated", ());
Ok(())
}
@@ -206,6 +214,7 @@ pub async fn clear_history(
.map_err(|e| e.to_string())?;
let _ = app_handle.track_event("history_cleared", None);
+ let _ = app_handle.emit("clipboard-content-updated", ());
Ok(())
}
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 3629212..ff0d817 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -127,7 +127,8 @@ fn main() {
db::history::read_image,
db::settings::get_setting,
db::settings::save_setting,
- utils::commands::fetch_page_meta
+ utils::commands::fetch_page_meta,
+ utils::commands::get_app_info
]
)
.run(tauri::generate_context!())
diff --git a/src-tauri/src/utils/commands.rs b/src-tauri/src/utils/commands.rs
index ebf71b1..dca7528 100644
--- a/src-tauri/src/utils/commands.rs
+++ b/src-tauri/src/utils/commands.rs
@@ -36,31 +36,49 @@ pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
}
}
+#[tauri::command]
pub fn get_app_info() -> (String, Option) {
println!("Getting app info");
let mut ctx = AppInfoContext::new(vec![]);
println!("Created AppInfoContext");
- ctx.refresh_apps().unwrap();
+
+ if let Err(e) = ctx.refresh_apps() {
+ println!("Failed to refresh apps: {:?}", e);
+ return ("System".to_string(), None);
+ }
+
println!("Refreshed apps");
- match ctx.get_frontmost_application() {
- Ok(window) => {
- println!("Found frontmost application: {}", window.name);
- let name = window.name.clone();
- let icon = window
- .load_icon()
- .ok()
- .map(|i| {
- println!("Loading icon for {}", name);
- let png = i.to_png().unwrap();
- let encoded = STANDARD.encode(png.get_bytes());
- println!("Icon encoded successfully");
- encoded
- });
- println!("Returning app info: {} with icon: {}", name, icon.is_some());
- (name, icon)
+
+ let result = std::panic::catch_unwind(|| {
+ match ctx.get_frontmost_application() {
+ Ok(window) => {
+ println!("Found frontmost application: {}", window.name);
+ let name = window.name.clone();
+ let icon = window
+ .load_icon()
+ .ok()
+ .and_then(|i| {
+ println!("Loading icon for {}", name);
+ i.to_png().ok().map(|png| {
+ let encoded = STANDARD.encode(png.get_bytes());
+ println!("Icon encoded successfully");
+ encoded
+ })
+ });
+ println!("Returning app info: {} with icon: {}", name, icon.is_some());
+ (name, icon)
+ }
+ Err(e) => {
+ println!("Failed to get frontmost application: {:?}", e);
+ ("System".to_string(), None)
+ }
}
- Err(e) => {
- println!("Failed to get frontmost application: {:?}", e);
+ });
+
+ match result {
+ Ok(info) => info,
+ Err(_) => {
+ println!("Panic occurred while getting app info");
("System".to_string(), None)
}
}
diff --git a/styles/index.scss b/styles/index.scss
index 20a154d..2608ce2 100644
--- a/styles/index.scss
+++ b/styles/index.scss
@@ -138,7 +138,6 @@ main {
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid $divider;
- line-height: 1;
&:last-child {
border-bottom: none;
@@ -146,7 +145,7 @@ main {
}
&:first-child {
- padding-top: 22px;
+ padding-top: 14px;
}
p {