Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 41 additions & 0 deletions apps/desktop/src/lib/trpc/routers/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,47 @@ export const createSettingsRouter = () => {
return { success: true };
}),

reorderTerminalPresets: publicProcedure
.input(
z.object({
presetId: z.string(),
targetIndex: z.number().int().min(0),
}),
)
.mutation(({ input }) => {
const row = getSettings();
const presets = row.terminalPresets ?? [];

const currentIndex = presets.findIndex((p) => p.id === input.presetId);
if (currentIndex === -1) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Preset not found",
});
}

if (input.targetIndex < 0 || input.targetIndex >= presets.length) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid target index for reordering presets",
});
}

const [removed] = presets.splice(currentIndex, 1);
presets.splice(input.targetIndex, 0, removed);

localDb
.insert(settings)
.values({ id: 1, terminalPresets: presets })
.onConflictDoUpdate({
target: settings.id,
set: { terminalPresets: presets },
})
.run();

return { success: true };
}),

getDefaultPreset: publicProcedure.query(() => {
const row = getSettings();
const presets = row.terminalPresets ?? [];
Expand Down
18 changes: 18 additions & 0 deletions apps/desktop/src/renderer/react-query/presets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ function useSetDefaultPreset(
});
}

function useReorderTerminalPresets(
options?: Parameters<
typeof electronTrpc.settings.reorderTerminalPresets.useMutation
>[0],
) {
const utils = electronTrpc.useUtils();

return electronTrpc.settings.reorderTerminalPresets.useMutation({
...options,
onSuccess: async (...args) => {
await utils.settings.getTerminalPresets.invalidate();
await options?.onSuccess?.(...args);
},
});
}

/**
* Combined hook for accessing terminal presets with all CRUD operations
* Provides easy access to presets data and mutations from anywhere in the app
Expand All @@ -80,6 +96,7 @@ export function usePresets() {
const updatePreset = useUpdateTerminalPreset();
const deletePreset = useDeleteTerminalPreset();
const setDefaultPreset = useSetDefaultPreset();
const reorderPresets = useReorderTerminalPresets();

return {
presets,
Expand All @@ -89,5 +106,6 @@ export function usePresets() {
updatePreset,
deletePreset,
setDefaultPreset,
reorderPresets,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useAppHotkey } from "renderer/stores/hotkeys";
import type { HotkeyId } from "shared/hotkeys";

const PRESET_HOTKEY_IDS: HotkeyId[] = [
"OPEN_PRESET_1",
"OPEN_PRESET_2",
"OPEN_PRESET_3",
"OPEN_PRESET_4",
"OPEN_PRESET_5",
"OPEN_PRESET_6",
"OPEN_PRESET_7",
"OPEN_PRESET_8",
"OPEN_PRESET_9",
];

export function usePresetHotkeys(
openTabWithPreset: (presetIndex: number) => void,
) {
useAppHotkey(PRESET_HOTKEY_IDS[0], () => openTabWithPreset(0), undefined, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[1], () => openTabWithPreset(1), undefined, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[2], () => openTabWithPreset(2), undefined, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[3], () => openTabWithPreset(3), undefined, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[4], () => openTabWithPreset(4), undefined, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[5], () => openTabWithPreset(5), undefined, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[6], () => openTabWithPreset(6), undefined, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[7], () => openTabWithPreset(7), undefined, [
openTabWithPreset,
]);
useAppHotkey(PRESET_HOTKEY_IDS[8], () => openTabWithPreset(8), undefined, [
openTabWithPreset,
]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { createFileRoute, notFound, useNavigate } from "@tanstack/react-router";
import { useCallback, useMemo } from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { electronTrpcClient as trpcClient } from "renderer/lib/trpc-client";
import { usePresets } from "renderer/react-query/presets";
import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation";
import { usePresetHotkeys } from "renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/hooks/usePresetHotkeys";
import { NotFound } from "renderer/routes/not-found";
import { WorkspaceInitializingView } from "renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView";
import { WorkspaceLayout } from "renderer/screens/main/components/WorkspaceView/WorkspaceLayout";
Expand Down Expand Up @@ -111,16 +113,33 @@ function WorkspacePage() {

const focusedPaneId = activeTabId ? focusedPaneIds[activeTabId] : null;

// Tab management shortcuts
useAppHotkey(
"NEW_GROUP",
() => {
addTab(workspaceId);
const { presets } = usePresets();
const renameTab = useTabsStore((s) => s.renameTab);

const openTabWithPreset = useCallback(
(presetIndex: number) => {
const preset = presets[presetIndex];
if (preset) {
const result = addTab(workspaceId, {
initialCommands: preset.commands,
initialCwd: preset.cwd || undefined,
});
if (preset.name) {
renameTab(result.tabId, preset.name);
}
} else {
addTab(workspaceId);
}
},
undefined,
[workspaceId, addTab],
[presets, workspaceId, addTab, renameTab],
);

useAppHotkey("NEW_GROUP", () => addTab(workspaceId), undefined, [
workspaceId,
addTab,
]);
usePresetHotkeys(openTabWithPreset);

useAppHotkey(
"CLOSE_TERMINAL",
() => {
Expand All @@ -132,9 +151,8 @@ function WorkspacePage() {
[focusedPaneId, removePane],
);

// Switch between tabs
useAppHotkey(
"PREV_TERMINAL",
"PREV_TAB",
() => {
if (!activeTabId) return;
const index = tabs.findIndex((t) => t.id === activeTabId);
Expand All @@ -147,7 +165,7 @@ function WorkspacePage() {
);

useAppHotkey(
"NEXT_TERMINAL",
"NEXT_TAB",
() => {
if (!activeTabId) return;
const index = tabs.findIndex((t) => t.id === activeTabId);
Expand All @@ -159,7 +177,6 @@ function WorkspacePage() {
[workspaceId, activeTabId, tabs, setActiveTab],
);

// Switch between panes within a tab
useAppHotkey(
"PREV_PANE",
() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,18 @@ function KeyboardShortcutsPage() {

const showHotkeysDisplay = useHotkeyDisplay("SHOW_HOTKEYS");

const allHotkeys = useMemo(
() =>
CATEGORY_ORDER.flatMap((category) => hotkeysByCategory[category] ?? []),
[hotkeysByCategory],
);

const filteredHotkeys = useMemo(() => {
if (!searchQuery) return allHotkeys;
const filteredHotkeysByCategory = useMemo(() => {
if (!searchQuery) return hotkeysByCategory;
const lower = searchQuery.toLowerCase();
return allHotkeys.filter((hotkey) =>
hotkey.label.toLowerCase().includes(lower),
);
}, [allHotkeys, searchQuery]);
return Object.fromEntries(
CATEGORY_ORDER.map((category) => [
category,
(hotkeysByCategory[category] ?? []).filter((hotkey) =>
hotkey.label.toLowerCase().includes(lower),
),
]),
) as typeof hotkeysByCategory;
}, [hotkeysByCategory, searchQuery]);

useEffect(() => {
if (!recordingId) return;
Expand Down Expand Up @@ -296,36 +295,51 @@ function KeyboardShortcutsPage() {
/>
</div>

{/* Table */}
<div className="rounded-lg border border-border overflow-hidden">
<div className="flex items-center justify-between py-2 px-4 bg-accent/10 border-b border-border">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Command
</span>
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Shortcut
</span>
</div>
{/* Tables by Category */}
<div className="max-h-[calc(100vh-320px)] overflow-y-auto space-y-6">
{CATEGORY_ORDER.map((category) => {
const hotkeys = filteredHotkeysByCategory[category] ?? [];
if (hotkeys.length === 0) return null;

<div className="max-h-[calc(100vh-320px)] overflow-y-auto divide-y divide-border">
{filteredHotkeys.length > 0 ? (
filteredHotkeys.map((hotkey) => (
<HotkeyRow
key={hotkey.id}
id={hotkey.id}
label={hotkey.label}
description={hotkey.description}
isRecording={recordingId === hotkey.id}
onStartRecording={() => handleStartRecording(hotkey.id)}
onReset={() => resetHotkey(hotkey.id)}
/>
))
) : (
<div className="py-8 text-center text-sm text-muted-foreground">
No shortcuts found matching "{searchQuery}"
return (
<div key={category}>
<h3 className="text-sm font-medium text-muted-foreground mb-2">
{category}
</h3>
<div className="rounded-lg border border-border overflow-hidden">
<div className="flex items-center justify-between py-2 px-4 bg-accent/10 border-b border-border">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Command
</span>
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Shortcut
</span>
</div>
<div className="divide-y divide-border">
{hotkeys.map((hotkey) => (
<HotkeyRow
key={hotkey.id}
id={hotkey.id}
label={hotkey.label}
description={hotkey.description}
isRecording={recordingId === hotkey.id}
onStartRecording={() => handleStartRecording(hotkey.id)}
onReset={() => resetHotkey(hotkey.id)}
/>
))}
</div>
</div>
</div>
)}
</div>
);
})}

{CATEGORY_ORDER.every(
(cat) => (filteredHotkeysByCategory[cat] ?? []).length === 0,
) && (
<div className="py-8 text-center text-sm text-muted-foreground">
No shortcuts found matching "{searchQuery}"
</div>
)}
</div>

{/* Conflict dialog */}
Expand Down
Loading
Loading