Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6745108
feat(desktop): add search shortcut settings
velga111 Jun 2, 2026
70d89aa
feat: customize shortcut settings UI
velga111 Jun 4, 2026
0e5759d
feat(desktop): reopen last closed search session
velga111 Jun 5, 2026
e3c0b21
feat(i18n): update zh-CN messages for search actions and history navi…
velga111 Jun 6, 2026
294fb05
feat(desktop): support function key search shortcuts
velga111 Jun 8, 2026
e6d1b13
feat(desktop): recover last active search session
velga111 Jun 8, 2026
b82c137
fix(desktop): reuse select for shortcut presets
velga111 Jun 8, 2026
d669b16
feat(desktop): add shortcut descriptions
velga111 Jun 8, 2026
14c5f35
fix(desktop): normalize function-row shortcuts
velga111 Jun 8, 2026
82e32ed
feat(desktop): complete keybinding customization rollout
velga111 Jun 8, 2026
61905a9
fix(desktop): address keybinding review feedback
velga111 Jun 8, 2026
b62fd54
fix(desktop): reject malformed shortcut strings
velga111 Jun 8, 2026
1b2cf2d
Merge branch 'main' into feat/keybindings-customization
velga111 Jun 8, 2026
7d9f696
feat(desktop): add settings shortcut
velga111 Jun 9, 2026
557bf54
fix(desktop): harden search shortcuts
velga111 Jun 9, 2026
90333e2
fix(desktop): reject shift-only search shortcuts
velga111 Jun 9, 2026
5d1c865
fix(desktop): restore reopen session shortcut default
velga111 Jun 9, 2026
5d903eb
fix(desktop): preserve last closed session retry
velga111 Jun 9, 2026
2c44d9d
fix(desktop): avoid persisted reopen session state
velga111 Jun 9, 2026
a17df8f
fix(desktop): harden search shortcut handling
velga111 Jun 9, 2026
a1d848a
Merge branch 'main' into feat/keybindings-customization
velga111 Jun 9, 2026
ba0f67b
fix(desktop): allow clean keybinding swaps in normalization
velga111 Jun 9, 2026
5bd2979
fix(desktop): use normalized fallback in keybinding conflict resolution
velga111 Jun 9, 2026
bdf3b9e
Merge remote-tracking branch 'upstream/main' into feat/keybindings-cu…
velga111 Jun 10, 2026
048f8d0
fix(desktop): address keybinding review feedback
velga111 Jun 10, 2026
b560f9b
chore: keep prettier ignore scope unchanged
velga111 Jun 10, 2026
64134f4
fix(desktop): tighten shortcut capture handling
velga111 Jun 10, 2026
bb50b14
fix(desktop): capture global shortcut input keys
velga111 Jun 10, 2026
12e2046
fix(desktop): align shortcut group headings
velga111 Jun 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions apps/desktop/src-tauri/src/core/system/shortcut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,28 @@ pub fn register_global_shortcut<R: Runtime>(
) -> Result<(), String> {
let new_shortcut = parse_shortcut(&shortcut)?;

// 注销旧快捷键
if let Ok(current) = CURRENT_SHORTCUT.lock() {
if let Some(old_shortcut) = *current {
let _ = app.global_shortcut().unregister(old_shortcut);
let old_shortcut = CURRENT_SHORTCUT.lock().ok().and_then(|current| *current);
if let Some(old_shortcut) = old_shortcut {
if let Err(error) = app.global_shortcut().unregister(old_shortcut) {
let message = format!("Failed to unregister previous shortcut: {}", error);
if let Ok(mut status) = REGISTRATION_STATUS.lock() {
*status = (true, Some(message.clone()));
}
return Err(message);
}
}

// 尝试注册新快捷键
let result = app
.global_shortcut()
.register(new_shortcut)
.map_err(|e| format!("Failed to register shortcut: {}", e));

// 更新状态
if result.is_err() {
if let Some(old_shortcut) = old_shortcut {
let _ = app.global_shortcut().register(old_shortcut);
}
}

match result {
Ok(_) => {
if let Ok(mut current) = CURRENT_SHORTCUT.lock() {
Expand Down
96 changes: 73 additions & 23 deletions apps/desktop/src/components/CustomSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
SelectTrigger,
SelectValue,
} from '@components/ui/select';
import type { AcceptableValue } from 'reka-ui';
import type { AcceptableValue, SelectTriggerProps } from 'reka-ui';
import { computed, ref, useAttrs } from 'vue';

import { type MessageKey, t, tt } from '@/i18n';
Expand All @@ -29,22 +29,48 @@
placeholderKey?: MessageKey;
disabled?: boolean;
protectOptionText?: boolean;
open?: boolean;
displayLabel?: string;
triggerTestId?: string;
contentTestId?: string;
optionTestIdPrefix?: string;
disablePortal?: boolean;
triggerAs?: SelectTriggerProps['as'];
}

interface Emits {
(e: 'update:modelValue', value: T): void;
(e: 'update:open', value: boolean): void;
(e: 'focus', event: FocusEvent): void;
(e: 'blur', event: FocusEvent): void;
}

const props = withDefaults(defineProps<Props>(), {
placeholder: '',
disabled: false,
protectOptionText: false,
open: undefined,
displayLabel: '',
triggerTestId: '',
contentTestId: '',
optionTestIdPrefix: '',
disablePortal: false,
triggerAs: 'button',
});

const emit = defineEmits<Emits>();
defineSlots<{
trigger?(props: {
option: Option | undefined;
open: boolean;
displayLabel: string;
}): unknown;
}>();
const attrs = useAttrs();

const isOpen = ref(false);
const localOpen = ref(false);

const isOpen = computed(() => props.open ?? localOpen.value);

const selectedOption = computed(() => {
return props.options.find((opt) => opt.value === props.modelValue);
Expand All @@ -66,6 +92,11 @@
selectOption(value as T);
}
};

const updateOpen = (open: boolean) => {
localOpen.value = open;
emit('update:open', open);
};
</script>

<template>
Expand All @@ -74,7 +105,7 @@
:disabled="disabled"
:open="isOpen"
@update:model-value="handleSelectValue"
@update:open="isOpen = $event"
@update:open="updateOpen"
>
<SelectTrigger
v-bind="attrs"
Expand All @@ -86,36 +117,55 @@
}"
:icon-class="isOpen ? 'rotate-180' : ''"
:disabled="disabled"
:as="triggerAs"
:data-testid="triggerTestId || undefined"
@focus="emit('focus', $event)"
@blur="emit('blur', $event)"
>
<template v-if="selectedOption">
<div class="flex min-w-0 items-center gap-2">
<img
v-if="selectedOption.iconSrc"
:src="selectedOption.iconSrc"
:alt="selectedOption.label"
class="h-4 w-4 shrink-0 rounded-sm object-contain"
:data-no-i18n="protectOptionText ? 'true' : undefined"
:translate="protectOptionText ? 'no' : undefined"
/>
<span
class="line-clamp-1"
:data-no-i18n="protectOptionText ? 'true' : undefined"
:translate="protectOptionText ? 'no' : undefined"
>
{{ selectedOption.label }}
</span>
</div>
</template>
<SelectValue v-else :placeholder="resolvedPlaceholder" />
<slot
name="trigger"
:option="selectedOption"
:open="isOpen"
:display-label="displayLabel"
>
<template v-if="displayLabel">
<span class="line-clamp-1">{{ displayLabel }}</span>
</template>
<template v-else-if="selectedOption">
<div class="flex min-w-0 items-center gap-2">
<img
v-if="selectedOption.iconSrc"
:src="selectedOption.iconSrc"
:alt="selectedOption.label"
class="h-4 w-4 shrink-0 rounded-sm object-contain"
:data-no-i18n="protectOptionText ? 'true' : undefined"
:translate="protectOptionText ? 'no' : undefined"
/>
<span
class="line-clamp-1"
:data-no-i18n="protectOptionText ? 'true' : undefined"
:translate="protectOptionText ? 'no' : undefined"
>
{{ selectedOption.label }}
</span>
</div>
</template>
<SelectValue v-else :placeholder="resolvedPlaceholder" />
</slot>
</SelectTrigger>

<SelectContent
:data-testid="contentTestId || undefined"
:disable-portal="disablePortal"
class="w-[var(--reka-select-trigger-width)] rounded-[10px] border border-gray-200 bg-white py-1 shadow-lg"
>
<SelectItem
v-for="option in options"
:key="option.value"
:value="option.value"
:data-testid="
optionTestIdPrefix ? `${optionTestIdPrefix}${option.value}` : undefined
"
class="flex w-full px-3 py-2 text-left text-sm transition-colors hover:bg-gray-100"
:class="{
'bg-primary-50 text-primary-600': option.value === modelValue,
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/components/appIconMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import IconShow from '~icons/bx/show';
import IconStop from '~icons/bx/stop';
import IconTrash from '~icons/bx/trash';
import IconTrashAlt from '~icons/bx/trash-alt';
import IconUndo from '~icons/bx/undo';
import IconWrench from '~icons/bx/wrench';
import IconX from '~icons/bx/x';
import IconXCircle from '~icons/bx/x-circle';
Expand Down Expand Up @@ -85,6 +86,7 @@ export const appIconMap = {
stop: IconStop,
tool: IconBriefcase,
trash: IconTrash,
undo: IconUndo,
bug: IconBug,
wrench: IconWrench,
x: IconX,
Expand Down
18 changes: 15 additions & 3 deletions apps/desktop/src/components/ui/select/SelectContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@
useForwardPropsEmits,
} from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { computed } from 'vue';
import { computed, useAttrs } from 'vue';

import { cn } from '@/lib/utils';

defineOptions({
inheritAttrs: false,
});

interface Props extends SelectContentProps {
class?: HTMLAttributes['class'];
disablePortal?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
Expand All @@ -23,23 +28,30 @@
align: 'start',
sideOffset: 4,
avoidCollisions: true,
disablePortal: false,
});

const emits = defineEmits<SelectContentEmits>();
const attrs = useAttrs();

const delegatedProps = computed(() => {
const delegated = { ...props };
Reflect.deleteProperty(delegated, 'class');
Reflect.deleteProperty(delegated, 'disablePortal');
return delegated;
});

const forwarded = useForwardPropsEmits(delegatedProps, emits);
const contentProps = computed(() => ({
...forwarded.value,
...attrs,
}));
</script>

<template>
<SelectPortal>
<SelectPortal :disabled="disablePortal">
<SelectContent
v-bind="forwarded"
v-bind="contentProps"
:class="
cn(
'relative z-50 min-w-[8rem] overflow-hidden rounded-lg border border-gray-200 bg-white py-1 text-gray-900 shadow-lg',
Expand Down
Loading