introdue app-wide event bus

This commit is contained in:
Nikita Galaiko 2023-04-27 07:44:18 +02:00
parent e94f23d6ce
commit 25b44d7df6
11 changed files with 169 additions and 64 deletions

View File

@ -63,6 +63,7 @@
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
"inter-ui": "^3.19.3",
"nanoevents": "^7.0.1",
"nanoid": "^4.0.1",
"postcss": "^8.4.14",
"postcss-load-config": "^4.0.1",

View File

@ -49,6 +49,7 @@ specifiers:
eslint-config-prettier: ^8.5.0
eslint-plugin-svelte3: ^4.0.0
inter-ui: ^3.19.3
nanoevents: ^7.0.1
nanoid: ^4.0.1
postcss: ^8.4.14
postcss-load-config: ^4.0.1
@ -127,6 +128,7 @@ devDependencies:
eslint-config-prettier: 8.6.0_eslint@8.34.0
eslint-plugin-svelte3: 4.0.0_dbthnr4b2bdkhyiebwn7su3hnq
inter-ui: 3.19.3
nanoevents: 7.0.1
nanoid: 4.0.1
postcss: 8.4.21
postcss-load-config: 4.0.1_postcss@8.4.21
@ -143,8 +145,8 @@ devDependencies:
svelte-french-toast: 1.0.3_svelte@3.55.1
svelte-resize-observer: 2.0.0
tailwindcss: 3.2.4_postcss@8.4.21
tauri-plugin-log-api: github.com/tauri-apps/tauri-plugin-log/36100c269e33ac91246b0a4bfadd18c1caee5296
tauri-plugin-websocket-api: github.com/tauri-apps/tauri-plugin-websocket/9b1883b40e6f53cfc77e029000408da8239fba7a
tauri-plugin-log-api: github.com/tauri-apps/tauri-plugin-log/782a97aeb30cf0f54355d04e6026842fa420bd83
tauri-plugin-websocket-api: github.com/tauri-apps/tauri-plugin-websocket/100b5e9eb3f28c88d59d4fed52586fdaa0149ea1
tinykeys: 1.4.0
tslib: 2.5.0
typescript: 4.9.5
@ -6847,6 +6849,11 @@ packages:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: true
/nanoevents/7.0.1:
resolution: {integrity: sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q==}
engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0}
dev: true
/nanoid/3.3.4:
resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -8985,16 +8992,16 @@ packages:
engines: {node: '>=10'}
dev: true
github.com/tauri-apps/tauri-plugin-log/36100c269e33ac91246b0a4bfadd18c1caee5296:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/36100c269e33ac91246b0a4bfadd18c1caee5296}
github.com/tauri-apps/tauri-plugin-log/782a97aeb30cf0f54355d04e6026842fa420bd83:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/782a97aeb30cf0f54355d04e6026842fa420bd83}
name: tauri-plugin-log-api
version: 0.0.0
dependencies:
'@tauri-apps/api': 1.2.0
dev: true
github.com/tauri-apps/tauri-plugin-websocket/9b1883b40e6f53cfc77e029000408da8239fba7a:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-websocket/tar.gz/9b1883b40e6f53cfc77e029000408da8239fba7a}
github.com/tauri-apps/tauri-plugin-websocket/100b5e9eb3f28c88d59d4fed52586fdaa0149ea1:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-websocket/tar.gz/100b5e9eb3f28c88d59d4fed52586fdaa0149ea1}
name: tauri-plugin-websocket-api
version: 0.0.0
dependencies:

View File

@ -8,10 +8,12 @@
import { onMount } from 'svelte';
import { open } from '@tauri-apps/api/shell';
import { IconExternalLink } from '../icons';
import type Events from '$lib/events';
export let projects: Readable<Project[]>;
export let addProject: (params: { path: string }) => Promise<Project>;
export let project = readable<Project | undefined>(undefined);
export let events: ReturnType<typeof Events>;
const input = writable('');
const scopeToProject = writable(!!$project);
@ -27,7 +29,8 @@
addProject,
projects,
project: scopeToProject ? project : undefined,
input
input,
events
})
);
@ -128,20 +131,45 @@
onMount(() =>
tinykeys(window, {
Backspace: () => {
Backspace: (events) => {
if (!modal?.isOpen()) return;
events.preventDefault();
events.stopPropagation();
if ($selectedGroup) {
selectedGroup.set(undefined);
} else if ($input.length === 0) {
scopeToProject.set(false);
}
},
ArrowDown: selectNextCommand,
ArrowUp: selectPreviousCommand,
'Control+n': selectNextCommand,
'Control+p': selectPreviousCommand,
Enter: () => {
ArrowDown: (event) => {
if (!modal?.isOpen()) return;
event.preventDefault();
event.stopPropagation();
selectNextCommand();
},
ArrowUp: (event) => {
if (!modal?.isOpen()) return;
event.preventDefault();
event.stopPropagation();
selectPreviousCommand();
},
'Control+n': (event) => {
if (!modal?.isOpen()) return;
event.preventDefault();
event.stopPropagation();
selectNextCommand;
},
'Control+p': (event) => {
if (!modal?.isOpen()) return;
event.preventDefault();
event.stopPropagation();
selectPreviousCommand();
},
Enter: (event) => {
if (!modal?.isOpen()) return;
event.preventDefault();
event.stopPropagation();
Promise.resolve($commandGroups[$selection[0]]).then((group) =>
trigger(group.commands[$selection[1]].action)
);

View File

@ -1,5 +1,6 @@
import { type Project, git } from '$lib/api';
import { open } from '@tauri-apps/api/dialog';
import type Events from '$lib/events';
import { toasts } from '$lib';
import {
IconGitCommit,
@ -99,11 +100,25 @@ const projectsGroup = ({
].filter(({ title }) => input.length === 0 || title.toLowerCase().includes(input.toLowerCase()))
});
const commandsGroup = ({ project, input }: { project?: Project; input: string }): Group => ({
const commandsGroup = ({
project,
input,
events
}: {
project?: Project;
input: string;
events: ReturnType<typeof Events>;
}): Group => ({
title: 'Commands',
commands: [
...(project
? [
{
title: 'Quick commits...',
hotkey: 'C',
action: () => events.openQuickCommitModal(),
icon: IconGitCommit
},
{
title: 'Replay',
hotkey: 'Meta+R',
@ -224,11 +239,12 @@ export default (params: {
projects: Project[];
project?: Project;
input: string;
events: ReturnType<typeof Events>;
}) => {
const { addProject, projects, input, project } = params;
const { addProject, projects, input, project, events } = params;
const groups = [];
groups.push(commandsGroup({ project, input }));
groups.push(commandsGroup({ project, input, events }));
groups.push(navigateGroup({ project, input }));
!project && groups.push(projectsGroup({ addProject, projects, input }));
project && groups.push(fileGroup({ project, input }));

View File

@ -3,7 +3,6 @@
import { Status, type Project, git } from '$lib/api';
import type { CloudApi, User } from '$lib/api';
import { Button, Modal, Link } from '$lib/components';
import { onMount } from 'svelte';
import { IconGitBranch } from './icons';
export const show = () => modal.show();
@ -109,7 +108,7 @@
<Button
role="purple"
height="small"
disabled={isCommitting}
disabled={isCommitting || !project.api?.sync}
loading={isAutowriting}
on:click={onAutowrite}
>

22
src/lib/events.ts Normal file
View File

@ -0,0 +1,22 @@
import { createNanoEvents } from 'nanoevents';
interface Events {
goto: (path: string) => void;
openCommandPalette: () => void;
closeCommandPalette: () => void;
openNewProjectModal: () => void;
openQuickCommitModal: () => void;
}
export default () => {
const emitter = createNanoEvents<Events>();
return {
on: (...args: Parameters<(typeof emitter)['on']>) => emitter.on(...args),
goto: (path: string) => emitter.emit('goto', path),
openCommandPalette: () => emitter.emit('openCommandPalette'),
openNewProjectModal: () => emitter.emit('openNewProjectModal'),
openQuickCommitModal: () => emitter.emit('openQuickCommitModal'),
closeCommandPalette: () => emitter.emit('closeCommandPalette')
};
};

23
src/lib/hotkeys.ts Normal file
View File

@ -0,0 +1,23 @@
import { building } from '$app/environment';
import tinykeys from 'tinykeys';
import type Events from '$lib/events';
export default (events: ReturnType<typeof Events>) => ({
on: (combo: string, callback: (event: KeyboardEvent) => void) => {
if (building) return () => {};
const comboContainsControlKeys =
combo.includes('Meta') || combo.includes('Alt') || combo.includes('Ctrl');
return tinykeys(window, {
[combo]: (event) => {
event.preventDefault();
event.stopPropagation();
const target = event.target as HTMLElement;
const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA';
if (isInput && !comboContainsControlKeys) return;
events.closeCommandPalette();
callback(event);
}
});
}
});

View File

@ -7,3 +7,8 @@ export const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number
};
export const clone = <T>(obj: T): T => structuredClone(obj);
export const unsubscribe =
(...unsubscribers: (() => void)[]) =>
() =>
unsubscribers.forEach((unsubscriber) => unsubscriber());

View File

@ -3,17 +3,16 @@
import { open } from '@tauri-apps/api/dialog';
import { toasts, Toaster } from '$lib';
import tinykeys, { type KeyBindingMap } from 'tinykeys';
import { format } from 'date-fns';
import type { LayoutData } from './$types';
import { BackForwardButtons, Link, CommandPalette, Breadcrumbs } from '$lib/components';
import { page } from '$app/stores';
import { derived } from 'svelte/store';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { unsubscribe } from '$lib/utils';
export let data: LayoutData;
const { user, posthog, projects, sentry } = data;
const { user, posthog, projects, sentry, events, hotkeys } = data;
const project = derived([page, projects], ([page, projects]) =>
projects.find((project) => project.id === page.params.projectId)
@ -21,12 +20,9 @@
export let commandPalette: CommandPalette;
onMount(() => {
const keybindings: KeyBindingMap = {
// global
'Meta+k': () => commandPalette.show(),
'Meta+,': () => goto('/users/'),
'Meta+Shift+N': async () => {
onMount(() =>
unsubscribe(
events.on('openNewProjectModal', async () => {
const selectedPath = await open({
directory: true,
recursive: true
@ -40,40 +36,16 @@
} catch (e: any) {
toasts.error(e.message);
}
},
}),
events.on('openCommandPalette', () => commandPalette?.show()),
events.on('closeCommandPalette', () => commandPalette?.close()),
events.on('goto', (path: string) => goto(path)),
// project specific
'Meta+Shift+C': () => $project && goto(`/projects/${$project.id}/commit/`),
'Meta+T': () => $project && goto(`/projects/${$project.id}/terminal/`),
'Meta+P': () => $project && goto(`/projects/${$project.id}/`),
'Meta+Shift+,': () => $project && goto(`/projects/${$project.id}/settings/`),
'Meta+R': () =>
$project && goto(`/projects/${$project.id}/player/${format(new Date(), 'yyyy-MM-dd')}`),
'a i p': () => $project && goto(`/projects/${$project.id}/aiplayground/`)
};
return tinykeys(
window,
Object.fromEntries(
Object.entries(keybindings).map(([combo, handler]) => {
const comboContainsControlKeys =
combo.includes('Meta') || combo.includes('Alt') || combo.includes('Ctrl');
return [
combo,
(e: KeyboardEvent) => {
const target = e.target as HTMLElement;
const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA';
if (isInput && !comboContainsControlKeys) return;
commandPalette?.close();
handler(e);
}
];
})
)
);
});
hotkeys.on('Meta+k', () => events.openCommandPalette()),
hotkeys.on('Meta+,', () => events.goto('/users/')),
hotkeys.on('Meta+Shift+N', () => events.openNewProjectModal())
)
);
user.subscribe(posthog.identify);
user.subscribe(sentry.identify);
@ -109,5 +81,11 @@
<slot />
</div>
<Toaster />
<CommandPalette bind:this={commandPalette} {projects} {project} addProject={projects.add} />
<CommandPalette
bind:this={commandPalette}
{projects}
{project}
addProject={projects.add}
{events}
/>
</div>

View File

@ -7,6 +7,8 @@ import Posthog from '$lib/posthog';
import Sentry from '$lib/sentry';
import { setup as setupLogger } from '$lib/log';
import { wrapLoadWithSentry } from '@sentry/sveltekit';
import Events from '$lib/events';
import Hotkeys from '$lib/hotkeys';
export const ssr = false;
export const prerender = true;
@ -36,11 +38,14 @@ export const load: LayoutLoad = wrapLoadWithSentry(async ({ fetch }) => {
}
: await (await import('$lib/api')).users.CurrentUser();
setupLogger();
const events = Events();
return {
projects,
user,
api: Api({ fetch }),
posthog: Posthog(),
sentry: Sentry()
sentry: Sentry(),
events,
hotkeys: Hotkeys(events)
};
});

View File

@ -6,9 +6,12 @@
import { goto } from '$app/navigation';
import { IconSearch, IconSettings, IconTerminal } from '$lib/components/icons';
import QuickCommitModal from '$lib/components/QuickCommitModal.svelte';
import { onMount } from 'svelte';
import { unsubscribe } from '$lib/utils';
import { format } from 'date-fns';
export let data: LayoutData;
const { user, api, project, head, statuses, diffs } = data;
const { hotkeys, events, user, api, project, head, statuses, diffs } = data;
let query: string;
@ -25,6 +28,23 @@
}
$: selection = $page?.route?.id?.split('/')?.[3];
let quickCommitModal: QuickCommitModal;
onMount(() =>
unsubscribe(
events.on('openQuickCommitModal', () => quickCommitModal.show()),
hotkeys.on('C', () => events.openQuickCommitModal()),
hotkeys.on('Meta+Shift+C', () => goto(`/projects/${$project.id}/commit/`)),
hotkeys.on('Meta+T', () => goto(`/projects/${$project.id}/terminal/`)),
hotkeys.on('Meta+P', () => goto(`/projects/${$project.id}/`)),
hotkeys.on('Meta+Shift+,', () => goto(`/projects/${$project.id}/settings/`)),
hotkeys.on('Meta+R', () =>
goto(`/projects/${$project.id}/player/${format(new Date(), 'yyyy-MM-dd')}`)
),
hotkeys.on('a i p', () => goto(`/projects/${$project.id}/aiplayground/`))
)
);
</script>
<div class="flex h-full w-full flex-col">
@ -122,8 +142,9 @@
{#if $user}
<QuickCommitModal
bind:this={quickCommitModal}
user={$user}
api={api}
{api}
project={$project}
head={$head}
diffs={$diffs}