feat: update the shortcuts in the desktop app for macOS (#96)
* feat(ui): update keyboard shortcuts to use platform-appropriate modifier keys * fix(macos): update keyboard shortcuts to use Cmd modifier Change Command Palette shortcut from Ctrl+X to Cmd+K Update all menu shortcuts to use Cmd instead of Ctrl on macOS * Fix linter issue
This commit is contained in:
@@ -391,7 +391,7 @@ fn build_macos_menu<R: tauri::Runtime>(
|
|||||||
MENU_ITEM_COMMAND_PALETTE_ID,
|
MENU_ITEM_COMMAND_PALETTE_ID,
|
||||||
"Command Palette",
|
"Command Palette",
|
||||||
true,
|
true,
|
||||||
Some("Ctrl+X"),
|
Some("Cmd+K"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// File menu items
|
// File menu items
|
||||||
@@ -400,7 +400,7 @@ fn build_macos_menu<R: tauri::Runtime>(
|
|||||||
MENU_ITEM_NEW_SESSION_ID,
|
MENU_ITEM_NEW_SESSION_ID,
|
||||||
"New Session",
|
"New Session",
|
||||||
true,
|
true,
|
||||||
Some("Ctrl+N"),
|
Some("Cmd+N"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let worktree_creator = MenuItem::with_id(
|
let worktree_creator = MenuItem::with_id(
|
||||||
@@ -408,7 +408,7 @@ fn build_macos_menu<R: tauri::Runtime>(
|
|||||||
MENU_ITEM_WORKTREE_CREATOR_ID,
|
MENU_ITEM_WORKTREE_CREATOR_ID,
|
||||||
"New Worktree",
|
"New Worktree",
|
||||||
true,
|
true,
|
||||||
Some("Ctrl+Shift+N"),
|
Some("Cmd+Shift+N"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let change_workspace = MenuItem::with_id(
|
let change_workspace = MenuItem::with_id(
|
||||||
@@ -420,15 +420,20 @@ fn build_macos_menu<R: tauri::Runtime>(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
// View menu items
|
// View menu items
|
||||||
let open_git_tab =
|
let open_git_tab = MenuItem::with_id(
|
||||||
MenuItem::with_id(app, MENU_ITEM_OPEN_GIT_TAB_ID, "Git", true, Some("Ctrl+G"))?;
|
app,
|
||||||
|
MENU_ITEM_OPEN_GIT_TAB_ID,
|
||||||
|
"Git",
|
||||||
|
true,
|
||||||
|
Some("Cmd+G"),
|
||||||
|
)?;
|
||||||
|
|
||||||
let open_diff_tab = MenuItem::with_id(
|
let open_diff_tab = MenuItem::with_id(
|
||||||
app,
|
app,
|
||||||
MENU_ITEM_OPEN_DIFF_TAB_ID,
|
MENU_ITEM_OPEN_DIFF_TAB_ID,
|
||||||
"Diff",
|
"Diff",
|
||||||
true,
|
true,
|
||||||
Some("Ctrl+E"),
|
Some("Cmd+E"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let open_terminal_tab = MenuItem::with_id(
|
let open_terminal_tab = MenuItem::with_id(
|
||||||
@@ -436,7 +441,7 @@ fn build_macos_menu<R: tauri::Runtime>(
|
|||||||
MENU_ITEM_OPEN_TERMINAL_TAB_ID,
|
MENU_ITEM_OPEN_TERMINAL_TAB_ID,
|
||||||
"Terminal",
|
"Terminal",
|
||||||
true,
|
true,
|
||||||
Some("Ctrl+T"),
|
Some("Cmd+T"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let theme_light = MenuItem::with_id(
|
let theme_light = MenuItem::with_id(
|
||||||
@@ -468,7 +473,7 @@ fn build_macos_menu<R: tauri::Runtime>(
|
|||||||
MENU_ITEM_TOGGLE_SIDEBAR_ID,
|
MENU_ITEM_TOGGLE_SIDEBAR_ID,
|
||||||
"Toggle Session Sidebar",
|
"Toggle Session Sidebar",
|
||||||
true,
|
true,
|
||||||
Some("Ctrl+L"),
|
Some("Cmd+L"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let toggle_memory_debug = MenuItem::with_id(
|
let toggle_memory_debug = MenuItem::with_id(
|
||||||
@@ -485,7 +490,7 @@ fn build_macos_menu<R: tauri::Runtime>(
|
|||||||
MENU_ITEM_HELP_DIALOG_ID,
|
MENU_ITEM_HELP_DIALOG_ID,
|
||||||
"Keyboard Shortcuts",
|
"Keyboard Shortcuts",
|
||||||
true,
|
true,
|
||||||
Some("Ctrl+H"),
|
Some("Cmd+H"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let download_logs = MenuItem::with_id(
|
let download_logs = MenuItem::with_id(
|
||||||
@@ -493,7 +498,7 @@ fn build_macos_menu<R: tauri::Runtime>(
|
|||||||
MENU_ITEM_DOWNLOAD_LOGS_ID,
|
MENU_ITEM_DOWNLOAD_LOGS_ID,
|
||||||
"Download Logs",
|
"Download Logs",
|
||||||
true,
|
true,
|
||||||
Some("Ctrl+Shift+L"),
|
Some("Cmd+Shift+L"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let report_bug = MenuItem::with_id(
|
let report_bug = MenuItem::with_id(
|
||||||
@@ -1237,6 +1242,7 @@ async fn handle_agent_route(
|
|||||||
// Get working directory for project-level agent detection
|
// Get working directory for project-level agent detection
|
||||||
let working_directory = state.opencode.get_working_directory();
|
let working_directory = state.opencode.get_working_directory();
|
||||||
|
|
||||||
|
|
||||||
match method {
|
match method {
|
||||||
Method::GET => {
|
Method::GET => {
|
||||||
match opencode_config::get_agent_sources(&name, Some(&working_directory)).await {
|
match opencode_config::get_agent_sources(&name, Some(&working_directory)).await {
|
||||||
@@ -1270,6 +1276,7 @@ async fn handle_agent_route(
|
|||||||
Err(resp) => return Ok(resp),
|
Err(resp) => return Ok(resp),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Extract scope from payload if present
|
// Extract scope from payload if present
|
||||||
let scope = payload
|
let scope = payload
|
||||||
.get("scope")
|
.get("scope")
|
||||||
@@ -1418,6 +1425,7 @@ async fn handle_skill_list_route(state: &ServerState) -> Result<Response<Body>,
|
|||||||
let working_directory = state.opencode.get_working_directory();
|
let working_directory = state.opencode.get_working_directory();
|
||||||
let discovered = opencode_config::discover_skills(Some(&working_directory));
|
let discovered = opencode_config::discover_skills(Some(&working_directory));
|
||||||
|
|
||||||
|
|
||||||
let mut skills = Vec::new();
|
let mut skills = Vec::new();
|
||||||
for skill in discovered {
|
for skill in discovered {
|
||||||
match opencode_config::get_skill_sources(&skill.name, Some(&working_directory)).await {
|
match opencode_config::get_skill_sources(&skill.name, Some(&working_directory)).await {
|
||||||
@@ -1439,10 +1447,7 @@ async fn handle_skill_list_route(state: &ServerState) -> Result<Response<Body>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(json_response(
|
Ok(json_response(StatusCode::OK, serde_json::json!({ "skills": skills })))
|
||||||
StatusCode::OK,
|
|
||||||
serde_json::json!({ "skills": skills }),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_skill_route(
|
async fn handle_skill_route(
|
||||||
@@ -1454,6 +1459,7 @@ async fn handle_skill_route(
|
|||||||
) -> Result<Response<Body>, StatusCode> {
|
) -> Result<Response<Body>, StatusCode> {
|
||||||
let working_directory = state.opencode.get_working_directory();
|
let working_directory = state.opencode.get_working_directory();
|
||||||
|
|
||||||
|
|
||||||
// Handle file operations: /api/config/skills/:name/files/*
|
// Handle file operations: /api/config/skills/:name/files/*
|
||||||
if let Some(ref fp) = file_path {
|
if let Some(ref fp) = file_path {
|
||||||
match method {
|
match method {
|
||||||
@@ -1502,10 +1508,7 @@ async fn handle_skill_route(
|
|||||||
Ok(data) => data,
|
Ok(data) => data,
|
||||||
Err(resp) => return Ok(resp),
|
Err(resp) => return Ok(resp),
|
||||||
};
|
};
|
||||||
let content = payload
|
let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
.get("content")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
match opencode_config::get_skill_sources(&name, Some(&working_directory)).await {
|
match opencode_config::get_skill_sources(&name, Some(&working_directory)).await {
|
||||||
Ok(sources) => {
|
Ok(sources) => {
|
||||||
@@ -1618,8 +1621,7 @@ async fn handle_skill_route(
|
|||||||
Err(resp) => return Ok(resp),
|
Err(resp) => return Ok(resp),
|
||||||
};
|
};
|
||||||
|
|
||||||
let scope = payload
|
let scope = payload.get("scope")
|
||||||
.get("scope")
|
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.and_then(|s| match s {
|
.and_then(|s| match s {
|
||||||
"project" => Some(opencode_config::SkillScope::Project),
|
"project" => Some(opencode_config::SkillScope::Project),
|
||||||
@@ -1748,6 +1750,7 @@ async fn handle_command_route(
|
|||||||
// Get working directory for project-level command detection
|
// Get working directory for project-level command detection
|
||||||
let working_directory = state.opencode.get_working_directory();
|
let working_directory = state.opencode.get_working_directory();
|
||||||
|
|
||||||
|
|
||||||
match method {
|
match method {
|
||||||
Method::GET => {
|
Method::GET => {
|
||||||
match opencode_config::get_command_sources(&name, Some(&working_directory)).await {
|
match opencode_config::get_command_sources(&name, Some(&working_directory)).await {
|
||||||
@@ -1781,6 +1784,7 @@ async fn handle_command_route(
|
|||||||
Err(resp) => return Ok(resp),
|
Err(resp) => return Ok(resp),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Extract scope from payload if present
|
// Extract scope from payload if present
|
||||||
let scope = payload
|
let scope = payload
|
||||||
.get("scope")
|
.get("scope")
|
||||||
@@ -2055,6 +2059,7 @@ async fn handle_config_routes(
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let trimmed = rest.trim();
|
let trimmed = rest.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
return Ok(config_error_response(
|
return Ok(config_error_response(
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap';
|
|||||||
import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup';
|
import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup';
|
||||||
import { GitPollingProvider } from '@/hooks/useGitPolling';
|
import { GitPollingProvider } from '@/hooks/useGitPolling';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
|
import { hasModifier } from '@/lib/utils';
|
||||||
import { useSessionStore } from '@/stores/useSessionStore';
|
import { useSessionStore } from '@/stores/useSessionStore';
|
||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
import { opencodeClient } from '@/lib/opencode/client';
|
import { opencodeClient } from '@/lib/opencode/client';
|
||||||
@@ -156,7 +157,7 @@ function App({ apis }: AppProps) {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'M') {
|
if (hasModifier(e) && e.shiftKey && e.key === 'M') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setShowMemoryDebug(prev => !prev);
|
setShowMemoryDebug(prev => !prev);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useConfigStore } from '@/stores/useConfigStore';
|
|||||||
import { useSessionStore } from '@/stores/useSessionStore';
|
import { useSessionStore } from '@/stores/useSessionStore';
|
||||||
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn, getModifierLabel, hasModifier } from '@/lib/utils';
|
||||||
import { useDiffFileCount } from '@/components/views/DiffView';
|
import { useDiffFileCount } from '@/components/views/DiffView';
|
||||||
|
|
||||||
interface TabConfig {
|
interface TabConfig {
|
||||||
@@ -277,7 +277,7 @@ export const Header: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
|
||||||
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
|
if (hasModifier(e) && !e.shiftKey && !e.altKey) {
|
||||||
const num = parseInt(e.key, 10);
|
const num = parseInt(e.key, 10);
|
||||||
if (num >= 1 && num <= tabs.length) {
|
if (num >= 1 && num <= tabs.length) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -410,7 +410,7 @@ export const Header: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Command Palette (Ctrl+X)</p>
|
<p>Command Palette ({getModifierLabel()}+K)</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip delayDuration={500}>
|
<Tooltip delayDuration={500}>
|
||||||
@@ -425,7 +425,7 @@ export const Header: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Keyboard Shortcuts (Ctrl+H)</p>
|
<p>Keyboard Shortcuts ({getModifierLabel()}+H)</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useThemeSystem } from '@/contexts/useThemeSystem';
|
|||||||
import type { ThemeMode } from '@/types/theme';
|
import type { ThemeMode } from '@/types/theme';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useMessageQueueStore } from '@/stores/messageQueueStore';
|
import { useMessageQueueStore } from '@/stores/messageQueueStore';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn, getModifierLabel } from '@/lib/utils';
|
||||||
import { ButtonSmall } from '@/components/ui/button-small';
|
import { ButtonSmall } from '@/components/ui/button-small';
|
||||||
import { NumberInput } from '@/components/ui/number-input';
|
import { NumberInput } from '@/components/ui/number-input';
|
||||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
@@ -355,8 +355,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
</label>
|
</label>
|
||||||
<p className="typography-meta text-muted-foreground pl-5">
|
<p className="typography-meta text-muted-foreground pl-5">
|
||||||
{queueModeEnabled
|
{queueModeEnabled
|
||||||
? 'Enter queues messages, Ctrl+Enter sends immediately.'
|
? `Enter queues messages, ${getModifierLabel()}+Enter sends immediately.`
|
||||||
: 'Enter sends immediately, Ctrl+Enter queues messages.'}
|
: `Enter sends immediately, ${getModifierLabel()}+Enter queues messages.`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { useThemeSystem } from '@/contexts/useThemeSystem';
|
|||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiGitBranchLine, RiLayoutLeftLine, RiMoonLine, RiQuestionLine, RiRestartLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine } from '@remixicon/react';
|
import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiGitBranchLine, RiLayoutLeftLine, RiMoonLine, RiQuestionLine, RiRestartLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine } from '@remixicon/react';
|
||||||
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
|
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
|
||||||
|
import { getModifierLabel } from '@/lib/utils';
|
||||||
|
|
||||||
export const CommandPalette: React.FC = () => {
|
export const CommandPalette: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@@ -121,42 +122,42 @@ export const CommandPalette: React.FC = () => {
|
|||||||
<CommandItem onSelect={handleOpenSessionList}>
|
<CommandItem onSelect={handleOpenSessionList}>
|
||||||
<RiLayoutLeftLine className="mr-2 h-4 w-4" />
|
<RiLayoutLeftLine className="mr-2 h-4 w-4" />
|
||||||
<span>Open Session List</span>
|
<span>Open Session List</span>
|
||||||
<CommandShortcut>Ctrl + L</CommandShortcut>
|
<CommandShortcut>{getModifierLabel()} + L</CommandShortcut>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem onSelect={handleCreateSession}>
|
<CommandItem onSelect={handleCreateSession}>
|
||||||
<RiAddLine className="mr-2 h-4 w-4" />
|
<RiAddLine className="mr-2 h-4 w-4" />
|
||||||
<span>New Session</span>
|
<span>New Session</span>
|
||||||
<CommandShortcut>Ctrl + N</CommandShortcut>
|
<CommandShortcut>{getModifierLabel()} + N</CommandShortcut>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem onSelect={handleOpenAdvancedSession}>
|
<CommandItem onSelect={handleOpenAdvancedSession}>
|
||||||
<RiGitBranchLine className="mr-2 h-4 w-4" />
|
<RiGitBranchLine className="mr-2 h-4 w-4" />
|
||||||
<span>New Session with Worktree</span>
|
<span>New Session with Worktree</span>
|
||||||
<CommandShortcut>Shift + Ctrl + N</CommandShortcut>
|
<CommandShortcut>Shift + {getModifierLabel()} + N</CommandShortcut>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem onSelect={handleShowHelp}>
|
<CommandItem onSelect={handleShowHelp}>
|
||||||
<RiQuestionLine className="mr-2 h-4 w-4" />
|
<RiQuestionLine className="mr-2 h-4 w-4" />
|
||||||
<span>Keyboard Shortcuts</span>
|
<span>Keyboard Shortcuts</span>
|
||||||
<CommandShortcut>Ctrl + H</CommandShortcut>
|
<CommandShortcut>{getModifierLabel()} + H</CommandShortcut>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem onSelect={handleOpenDiffPanel}>
|
<CommandItem onSelect={handleOpenDiffPanel}>
|
||||||
<RiCodeLine className="mr-2 h-4 w-4" />
|
<RiCodeLine className="mr-2 h-4 w-4" />
|
||||||
<span>Open Diff Panel</span>
|
<span>Open Diff Panel</span>
|
||||||
<CommandShortcut>Ctrl + E</CommandShortcut>
|
<CommandShortcut>{getModifierLabel()} + E</CommandShortcut>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem onSelect={handleOpenGitPanel}>
|
<CommandItem onSelect={handleOpenGitPanel}>
|
||||||
<RiGitBranchLine className="mr-2 h-4 w-4" />
|
<RiGitBranchLine className="mr-2 h-4 w-4" />
|
||||||
<span>Open Git Panel</span>
|
<span>Open Git Panel</span>
|
||||||
<CommandShortcut>Ctrl + G</CommandShortcut>
|
<CommandShortcut>{getModifierLabel()} + G</CommandShortcut>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem onSelect={handleOpenTerminal}>
|
<CommandItem onSelect={handleOpenTerminal}>
|
||||||
<RiTerminalBoxLine className="mr-2 h-4 w-4" />
|
<RiTerminalBoxLine className="mr-2 h-4 w-4" />
|
||||||
<span>Open Terminal</span>
|
<span>Open Terminal</span>
|
||||||
<CommandShortcut>Ctrl + T</CommandShortcut>
|
<CommandShortcut>{getModifierLabel()} + T</CommandShortcut>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem onSelect={handleOpenSettings}>
|
<CommandItem onSelect={handleOpenSettings}>
|
||||||
<RiSettings3Line className="mr-2 h-4 w-4" />
|
<RiSettings3Line className="mr-2 h-4 w-4" />
|
||||||
<span>Open Settings</span>
|
<span>Open Settings</span>
|
||||||
<CommandShortcut>Ctrl + ,</CommandShortcut>
|
<CommandShortcut>{getModifierLabel()} + ,</CommandShortcut>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem onSelect={handleReloadConfiguration}>
|
<CommandItem onSelect={handleReloadConfiguration}>
|
||||||
<RiRestartLine className="mr-2 h-4 w-4" />
|
<RiRestartLine className="mr-2 h-4 w-4" />
|
||||||
|
|||||||
@@ -1,41 +1,66 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from "@/components/ui/dialog";
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from "@/stores/useUIStore";
|
||||||
import { RiAddLine, RiArrowUpSLine, RiArrowUpWideLine, RiBrainAi3Line, RiCloseCircleLine, RiCodeLine, RiCommandLine, RiGitBranchLine, RiLayoutLeftLine, RiPaletteLine, RiQuestionLine, RiSettings3Line, RiTerminalBoxLine, RiText } from '@remixicon/react';
|
import {
|
||||||
|
RiAddLine,
|
||||||
|
RiArrowUpSLine,
|
||||||
|
RiBrainAi3Line,
|
||||||
|
RiCloseCircleLine,
|
||||||
|
RiCodeLine,
|
||||||
|
RiCommandLine,
|
||||||
|
RiGitBranchLine,
|
||||||
|
RiLayoutLeftLine,
|
||||||
|
RiPaletteLine,
|
||||||
|
RiQuestionLine,
|
||||||
|
RiSettings3Line,
|
||||||
|
RiTerminalBoxLine,
|
||||||
|
RiText,
|
||||||
|
} from "@remixicon/react";
|
||||||
|
import { getModifierLabel } from "@/lib/utils";
|
||||||
|
|
||||||
const renderKeyToken = (token: string, index: number) => {
|
const renderKeyToken = (token: string, index: number) => {
|
||||||
const normalized = token.trim().toLowerCase();
|
const normalized = token.trim().toLowerCase();
|
||||||
|
|
||||||
if (normalized === 'ctrl' || normalized === 'control') {
|
if (normalized === "ctrl" || normalized === "control") {
|
||||||
return <RiArrowUpSLine key={`ctrl-${index}`} className="h-3.5 w-3.5" />;
|
return <RiArrowUpSLine key={`ctrl-${index}`} className="h-3.5 w-3.5" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized === 'shift' || normalized === '⇧') {
|
if (
|
||||||
return <RiArrowUpWideLine key={`shift-${index}`} className="h-3.5 w-3.5" />;
|
normalized === "⌘" ||
|
||||||
}
|
normalized === "cmd" ||
|
||||||
|
normalized === "command" ||
|
||||||
if (normalized === '⌘' || normalized === 'cmd' || normalized === 'command' || normalized === 'meta') {
|
normalized === "meta"
|
||||||
|
) {
|
||||||
return <RiCommandLine key={`cmd-${index}`} className="h-3.5 w-3.5" />;
|
return <RiCommandLine key={`cmd-${index}`} className="h-3.5 w-3.5" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <span key={`key-${index}`} className="text-xs font-medium">{token.trim()}</span>;
|
return (
|
||||||
|
<span key={`key-${index}`} className="text-xs font-medium">
|
||||||
|
{token.trim()}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderKeyCombo = (combo: string) => {
|
const renderKeyCombo = (combo: string) => {
|
||||||
const tokens = combo.split('+').map((token) => token.trim()).filter(Boolean);
|
const tokens = combo
|
||||||
|
.split("+")
|
||||||
|
.map((token) => token.trim())
|
||||||
|
.filter(Boolean);
|
||||||
if (tokens.length === 0) {
|
if (tokens.length === 0) {
|
||||||
return combo.trim();
|
return combo.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokens.map((token, index) => (
|
return tokens.map((token, index) => (
|
||||||
<React.Fragment key={`${token}-${index}`}>
|
<React.Fragment key={`${token}-${index}`}>
|
||||||
{index > 0 && <span className="text-muted-foreground text-[10px]">+</span>}
|
{index > 0 && (
|
||||||
|
<span className="text-muted-foreground text-[10px]">+</span>
|
||||||
|
)}
|
||||||
{renderKeyToken(token, index)}
|
{renderKeyToken(token, index)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
));
|
));
|
||||||
@@ -57,35 +82,85 @@ type ShortcutSection = {
|
|||||||
export const HelpDialog: React.FC = () => {
|
export const HelpDialog: React.FC = () => {
|
||||||
const { isHelpDialogOpen, setHelpDialogOpen } = useUIStore();
|
const { isHelpDialogOpen, setHelpDialogOpen } = useUIStore();
|
||||||
|
|
||||||
|
const mod = getModifierLabel();
|
||||||
|
|
||||||
const shortcuts: ShortcutSection[] = [
|
const shortcuts: ShortcutSection[] = [
|
||||||
{
|
{
|
||||||
category: "Navigation & Commands",
|
category: "Navigation & Commands",
|
||||||
items: [
|
items: [
|
||||||
{ keys: ["Ctrl + X"], description: "Open Command Palette", icon: RiCommandLine },
|
{
|
||||||
{ keys: ["Ctrl + H"], description: "Show Keyboard Shortcuts (this dialog)", icon: RiQuestionLine },
|
keys: [`${mod} + K`],
|
||||||
{ keys: ["Ctrl + L"], description: "Toggle Session Sidebar", icon: RiLayoutLeftLine },
|
description: "Open Command Palette",
|
||||||
{ keys: ["Ctrl + M"], description: "Open Model Selector", icon: RiBrainAi3Line },
|
icon: RiCommandLine,
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
keys: [`${mod} + H`],
|
||||||
|
description: "Show Keyboard Shortcuts (this dialog)",
|
||||||
|
icon: RiQuestionLine,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [`${mod} + L`],
|
||||||
|
description: "Toggle Session Sidebar",
|
||||||
|
icon: RiLayoutLeftLine,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [`${mod} + M`],
|
||||||
|
description: "Open Model Selector",
|
||||||
|
icon: RiBrainAi3Line,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "Session Management",
|
category: "Session Management",
|
||||||
items: [
|
items: [
|
||||||
{ keys: ["Ctrl + N"], description: "Create New Session", icon: RiAddLine },
|
{
|
||||||
{ keys: ["Shift + Ctrl + N"], description: "Open Worktree Creator", icon: RiGitBranchLine },
|
keys: [`${mod} + N`],
|
||||||
{ keys: ["Ctrl + I"], description: "Focus Chat Input", icon: RiText },
|
description: "Create New Session",
|
||||||
{ keys: ["Esc + Esc"], description: "Abort active run (double press)", icon: RiCloseCircleLine },
|
icon: RiAddLine,
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
keys: [`Shift + ${mod} + N`],
|
||||||
|
description: "Open Worktree Creator",
|
||||||
|
icon: RiGitBranchLine,
|
||||||
|
},
|
||||||
|
{ keys: [`${mod} + I`], description: "Focus Chat Input", icon: RiText },
|
||||||
|
{
|
||||||
|
keys: ["Esc + Esc"],
|
||||||
|
description: "Abort active run (double press)",
|
||||||
|
icon: RiCloseCircleLine,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "Interface",
|
category: "Interface",
|
||||||
items: [
|
items: [
|
||||||
{ keys: ["⌘ + /", "Ctrl + /"], description: "Cycle Theme (Light → Dark → System)", icon: RiPaletteLine },
|
{
|
||||||
{ keys: ["Ctrl + E"], description: "Open Diff Panel", icon: RiCodeLine },
|
keys: [`${mod} + /`],
|
||||||
{ keys: ["Ctrl + G"], description: "Open Git Panel", icon: RiGitBranchLine },
|
description: "Cycle Theme (Light → Dark → System)",
|
||||||
{ keys: ["Ctrl + T"], description: "Open Terminal", icon: RiTerminalBoxLine },
|
icon: RiPaletteLine,
|
||||||
{ keys: ["Ctrl + ,"], description: "Open Settings", icon: RiSettings3Line },
|
},
|
||||||
]
|
{
|
||||||
}
|
keys: [`${mod} + E`],
|
||||||
|
description: "Open Diff Panel",
|
||||||
|
icon: RiCodeLine,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [`${mod} + G`],
|
||||||
|
description: "Open Git Panel",
|
||||||
|
icon: RiGitBranchLine,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [`${mod} + T`],
|
||||||
|
description: "Open Terminal",
|
||||||
|
icon: RiTerminalBoxLine,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [`${mod} + ,`],
|
||||||
|
description: "Open Settings",
|
||||||
|
icon: RiSettings3Line,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -117,12 +192,21 @@ export const HelpDialog: React.FC = () => {
|
|||||||
{shortcut.icon && (
|
{shortcut.icon && (
|
||||||
<shortcut.icon className="h-3.5 w-3.5 text-muted-foreground" />
|
<shortcut.icon className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
<span className="typography-meta">{shortcut.description}</span>
|
<span className="typography-meta">
|
||||||
|
{shortcut.description}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{(Array.isArray(shortcut.keys) ? shortcut.keys : shortcut.keys.split(' / ')).map((keyCombo: string, i: number) => (
|
{(Array.isArray(shortcut.keys)
|
||||||
|
? shortcut.keys
|
||||||
|
: shortcut.keys.split(" / ")
|
||||||
|
).map((keyCombo: string, i: number) => (
|
||||||
<React.Fragment key={`${keyCombo}-${i}`}>
|
<React.Fragment key={`${keyCombo}-${i}`}>
|
||||||
{i > 0 && <span className="typography-meta text-muted-foreground mx-1">or</span>}
|
{i > 0 && (
|
||||||
|
<span className="typography-meta text-muted-foreground mx-1">
|
||||||
|
or
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<kbd className="inline-flex items-center gap-1 px-1.5 py-0.5 typography-meta font-mono bg-muted rounded border border-border/20">
|
<kbd className="inline-flex items-center gap-1 px-1.5 py-0.5 typography-meta font-mono bg-muted rounded border border-border/20">
|
||||||
{renderKeyCombo(keyCombo)}
|
{renderKeyCombo(keyCombo)}
|
||||||
</kbd>
|
</kbd>
|
||||||
@@ -141,11 +225,18 @@ export const HelpDialog: React.FC = () => {
|
|||||||
<RiQuestionLine className="h-3.5 w-3.5 text-muted-foreground mt-0.5" />
|
<RiQuestionLine className="h-3.5 w-3.5 text-muted-foreground mt-0.5" />
|
||||||
<div className="typography-meta text-muted-foreground">
|
<div className="typography-meta text-muted-foreground">
|
||||||
<p className="font-medium mb-1">Pro Tips:</p>
|
<p className="font-medium mb-1">Pro Tips:</p>
|
||||||
<ul className="space-y-0.5 typography-meta">
|
<ul className="space-y-0.5 typography-meta">
|
||||||
<li>• Use Command Palette (Ctrl + X) to quickly access all actions</li>
|
<li>
|
||||||
<li>• The 5 most recent sessions appear in the Command Palette</li>
|
• Use Command Palette ({mod} + K) to quickly access all
|
||||||
<li>• Theme cycling remembers your preference across sessions</li>
|
actions
|
||||||
</ul>
|
</li>
|
||||||
|
<li>
|
||||||
|
• The 5 most recent sessions appear in the Command Palette
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• Theme cycling remembers your preference across sessions
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn, getModifierLabel } from '@/lib/utils';
|
||||||
import { SIDEBAR_SECTIONS } from '@/constants/sidebar';
|
import { SIDEBAR_SECTIONS } from '@/constants/sidebar';
|
||||||
import type { SidebarSection } from '@/constants/sidebar';
|
import type { SidebarSection } from '@/constants/sidebar';
|
||||||
import { RiArrowLeftSLine, RiCloseLine } from '@remixicon/react';
|
import { RiArrowLeftSLine, RiCloseLine } from '@remixicon/react';
|
||||||
@@ -244,7 +244,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Keyboard shortcut display based on platform
|
// Keyboard shortcut display based on platform
|
||||||
const shortcutKey = isMacPlatform ? '⌘' : 'Ctrl';
|
const shortcutKey = getModifierLabel();
|
||||||
|
|
||||||
// Desktop padding for Mac titlebar area
|
// Desktop padding for Mac titlebar area
|
||||||
const desktopPaddingClass = React.useMemo(() => {
|
const desktopPaddingClass = React.useMemo(() => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useUIStore } from '@/stores/useUIStore';
|
|||||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
import { useAssistantStatus } from '@/hooks/useAssistantStatus';
|
import { useAssistantStatus } from '@/hooks/useAssistantStatus';
|
||||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||||
|
import { hasModifier } from '@/lib/utils';
|
||||||
|
|
||||||
export const useKeyboardShortcuts = () => {
|
export const useKeyboardShortcuts = () => {
|
||||||
const { openNewSessionDraft, abortCurrentOperation, armAbortPrompt, clearAbortPrompt, currentSessionId } = useSessionStore();
|
const { openNewSessionDraft, abortCurrentOperation, armAbortPrompt, clearAbortPrompt, currentSessionId } = useSessionStore();
|
||||||
@@ -35,12 +36,12 @@ export const useKeyboardShortcuts = () => {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
|
||||||
if (e.ctrlKey && e.key === 'x') {
|
if (hasModifier(e) && e.key === 'k') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
toggleCommandPalette();
|
toggleCommandPalette();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'l') {
|
if (hasModifier(e) && e.shiftKey && e.key.toLowerCase() === 'l') {
|
||||||
const runtimeAPIs = getRegisteredRuntimeAPIs();
|
const runtimeAPIs = getRegisteredRuntimeAPIs();
|
||||||
const diagnostics = runtimeAPIs?.diagnostics;
|
const diagnostics = runtimeAPIs?.diagnostics;
|
||||||
if (!diagnostics) {
|
if (!diagnostics) {
|
||||||
@@ -73,12 +74,12 @@ export const useKeyboardShortcuts = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.ctrlKey && e.key === 'h') {
|
if (hasModifier(e) && e.key === 'h') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
toggleHelpDialog();
|
toggleHelpDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.ctrlKey && !e.metaKey && e.key.toLowerCase() === 'n') {
|
if (hasModifier(e) && e.key.toLowerCase() === 'n') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
setSessionCreateDialogOpen(true);
|
setSessionCreateDialogOpen(true);
|
||||||
@@ -90,7 +91,7 @@ export const useKeyboardShortcuts = () => {
|
|||||||
openNewSessionDraft();
|
openNewSessionDraft();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === '/') {
|
if (hasModifier(e) && e.key === '/') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const modes: Array<'light' | 'dark' | 'system'> = ['light', 'dark', 'system'];
|
const modes: Array<'light' | 'dark' | 'system'> = ['light', 'dark', 'system'];
|
||||||
const currentIndex = modes.indexOf(themeMode);
|
const currentIndex = modes.indexOf(themeMode);
|
||||||
@@ -98,35 +99,35 @@ export const useKeyboardShortcuts = () => {
|
|||||||
setThemeMode(modes[nextIndex]);
|
setThemeMode(modes[nextIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'g') {
|
if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 'g') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { activeMainTab } = useUIStore.getState();
|
const { activeMainTab } = useUIStore.getState();
|
||||||
setActiveMainTab(activeMainTab === 'git' ? 'chat' : 'git');
|
setActiveMainTab(activeMainTab === 'git' ? 'chat' : 'git');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'e') {
|
if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 'e') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { activeMainTab } = useUIStore.getState();
|
const { activeMainTab } = useUIStore.getState();
|
||||||
setActiveMainTab(activeMainTab === 'diff' ? 'chat' : 'diff');
|
setActiveMainTab(activeMainTab === 'diff' ? 'chat' : 'diff');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 't') {
|
if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 't') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { activeMainTab } = useUIStore.getState();
|
const { activeMainTab } = useUIStore.getState();
|
||||||
setActiveMainTab(activeMainTab === 'terminal' ? 'chat' : 'terminal');
|
setActiveMainTab(activeMainTab === 'terminal' ? 'chat' : 'terminal');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === ',') {
|
if (hasModifier(e) && !e.shiftKey && e.key === ',') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { isSettingsDialogOpen } = useUIStore.getState();
|
const { isSettingsDialogOpen } = useUIStore.getState();
|
||||||
setSettingsDialogOpen(!isSettingsDialogOpen);
|
setSettingsDialogOpen(!isSettingsDialogOpen);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'l') {
|
if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 'l') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { isMobile, isSessionSwitcherOpen } = useUIStore.getState();
|
const { isMobile, isSessionSwitcherOpen } = useUIStore.getState();
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
@@ -137,7 +138,7 @@ export const useKeyboardShortcuts = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'i') {
|
if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 'i') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const textarea = document.querySelector<HTMLTextAreaElement>('textarea[data-chat-input="true"]');
|
const textarea = document.querySelector<HTMLTextAreaElement>('textarea[data-chat-input="true"]');
|
||||||
textarea?.focus();
|
textarea?.focus();
|
||||||
@@ -145,7 +146,7 @@ export const useKeyboardShortcuts = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl+M: Open model selector (same conditions as double-ESC: chat tab, no overlays)
|
// Ctrl+M: Open model selector (same conditions as double-ESC: chat tab, no overlays)
|
||||||
if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'm') {
|
if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 'm') {
|
||||||
const {
|
const {
|
||||||
isSettingsDialogOpen,
|
isSettingsDialogOpen,
|
||||||
isCommandPaletteOpen,
|
isCommandPaletteOpen,
|
||||||
|
|||||||
@@ -1,10 +1,38 @@
|
|||||||
import { clsx, type ClassValue } from "clsx";
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { isDesktopRuntime } from "@/lib/desktop";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects if the current platform is macOS.
|
||||||
|
* Uses navigator.userAgent in browser environments.
|
||||||
|
*/
|
||||||
|
export const isMacOS = (): boolean => {
|
||||||
|
if (typeof navigator === 'undefined') return false;
|
||||||
|
return /Macintosh|Mac OS X/.test(navigator.userAgent || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the platform-appropriate modifier key is pressed.
|
||||||
|
* On macOS desktop app: Cmd (metaKey), on other platforms or web: Ctrl (ctrlKey).
|
||||||
|
* Browser intercepts Cmd shortcuts, so we only use Cmd in Tauri desktop app.
|
||||||
|
*/
|
||||||
|
export const hasModifier = (e: KeyboardEvent | React.KeyboardEvent): boolean => {
|
||||||
|
return isMacOS() && isDesktopRuntime() ? e.metaKey : e.ctrlKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the platform-appropriate modifier key label.
|
||||||
|
* On macOS desktop app: "⌘", on other platforms or web: "Ctrl"
|
||||||
|
* Browser intercepts Cmd shortcuts, so we only show Cmd in Tauri desktop app.
|
||||||
|
*/
|
||||||
|
export const getModifierLabel = (): string => {
|
||||||
|
return isMacOS() && isDesktopRuntime() ? '⌘' : 'Ctrl';
|
||||||
|
};
|
||||||
|
|
||||||
export const truncatePathMiddle = (
|
export const truncatePathMiddle = (
|
||||||
value: string,
|
value: string,
|
||||||
options?: { maxLength?: number }
|
options?: { maxLength?: number }
|
||||||
|
|||||||
Reference in New Issue
Block a user