feat: add terminal and simple shortcut system (#16)
This commit is contained in:
39
packages/bolt/app/lib/stores/settings.ts
Normal file
39
packages/bolt/app/lib/stores/settings.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { map } from 'nanostores';
|
||||
import { workbenchStore } from './workbench';
|
||||
|
||||
export interface Shortcut {
|
||||
key: string;
|
||||
ctrlKey?: boolean;
|
||||
shiftKey?: boolean;
|
||||
altKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
ctrlOrMetaKey?: boolean;
|
||||
action: () => void;
|
||||
}
|
||||
|
||||
export interface Shortcuts {
|
||||
toggleTerminal: Shortcut;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
shortcuts: Shortcuts;
|
||||
}
|
||||
|
||||
export const shortcutsStore = map<Shortcuts>({
|
||||
toggleTerminal: {
|
||||
key: 'j',
|
||||
ctrlOrMetaKey: true,
|
||||
action: () => workbenchStore.toggleTerminal(),
|
||||
},
|
||||
});
|
||||
|
||||
export const settingsStore = map<Settings>({
|
||||
shortcuts: shortcutsStore.get(),
|
||||
});
|
||||
|
||||
shortcutsStore.subscribe((shortcuts) => {
|
||||
settingsStore.set({
|
||||
...settingsStore.get(),
|
||||
shortcuts,
|
||||
});
|
||||
});
|
||||
40
packages/bolt/app/lib/stores/terminal.ts
Normal file
40
packages/bolt/app/lib/stores/terminal.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
|
||||
import { atom, type WritableAtom } from 'nanostores';
|
||||
import type { ITerminal } from '~/types/terminal';
|
||||
import { newShellProcess } from '~/utils/shell';
|
||||
import { coloredText } from '~/utils/terminal';
|
||||
|
||||
export class TerminalStore {
|
||||
#webcontainer: Promise<WebContainer>;
|
||||
#terminals: Array<{ terminal: ITerminal; process: WebContainerProcess }> = [];
|
||||
|
||||
showTerminal: WritableAtom<boolean> = import.meta.hot?.data.showTerminal ?? atom(false);
|
||||
|
||||
constructor(webcontainerPromise: Promise<WebContainer>) {
|
||||
this.#webcontainer = webcontainerPromise;
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.data.showTerminal = this.showTerminal;
|
||||
}
|
||||
}
|
||||
|
||||
toggleTerminal(value?: boolean) {
|
||||
this.showTerminal.set(value !== undefined ? value : !this.showTerminal.get());
|
||||
}
|
||||
|
||||
async attachTerminal(terminal: ITerminal) {
|
||||
try {
|
||||
const shellProcess = await newShellProcess(await this.#webcontainer, terminal);
|
||||
this.#terminals.push({ terminal, process: shellProcess });
|
||||
} catch (error: any) {
|
||||
terminal.write(coloredText.red('Failed to spawn shell\n\n') + error.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onTerminalResize(cols: number, rows: number) {
|
||||
for (const { process } of this.#terminals) {
|
||||
process.resize({ cols, rows });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ export function themeIsDark() {
|
||||
return themeStore.get() === 'dark';
|
||||
}
|
||||
|
||||
export const DEFAULT_THEME = 'light';
|
||||
|
||||
export const themeStore = atom<Theme>(initStore());
|
||||
|
||||
function initStore() {
|
||||
@@ -15,10 +17,10 @@ function initStore() {
|
||||
const persistedTheme = localStorage.getItem(kTheme) as Theme | undefined;
|
||||
const themeAttribute = document.querySelector('html')?.getAttribute('data-theme');
|
||||
|
||||
return persistedTheme ?? (themeAttribute as Theme) ?? 'light';
|
||||
return persistedTheme ?? (themeAttribute as Theme) ?? DEFAULT_THEME;
|
||||
}
|
||||
|
||||
return 'light';
|
||||
return DEFAULT_THEME;
|
||||
}
|
||||
|
||||
export function toggleTheme() {
|
||||
|
||||
@@ -3,10 +3,12 @@ import type { EditorDocument, ScrollPosition } from '~/components/editor/codemir
|
||||
import { ActionRunner } from '~/lib/runtime/action-runner';
|
||||
import type { ActionCallbackData, ArtifactCallbackData } from '~/lib/runtime/message-parser';
|
||||
import { webcontainer } from '~/lib/webcontainer';
|
||||
import type { ITerminal } from '~/types/terminal';
|
||||
import { unreachable } from '~/utils/unreachable';
|
||||
import { EditorStore } from './editor';
|
||||
import { FilesStore, type FileMap } from './files';
|
||||
import { PreviewsStore } from './previews';
|
||||
import { TerminalStore } from './terminal';
|
||||
|
||||
export interface ArtifactState {
|
||||
title: string;
|
||||
@@ -22,6 +24,7 @@ export class WorkbenchStore {
|
||||
#previewsStore = new PreviewsStore(webcontainer);
|
||||
#filesStore = new FilesStore(webcontainer);
|
||||
#editorStore = new EditorStore(this.#filesStore);
|
||||
#terminalStore = new TerminalStore(webcontainer);
|
||||
|
||||
artifacts: Artifacts = import.meta.hot?.data.artifacts ?? map({});
|
||||
showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false);
|
||||
@@ -53,6 +56,22 @@ export class WorkbenchStore {
|
||||
return this.#editorStore.selectedFile;
|
||||
}
|
||||
|
||||
get showTerminal() {
|
||||
return this.#terminalStore.showTerminal;
|
||||
}
|
||||
|
||||
toggleTerminal(value?: boolean) {
|
||||
this.#terminalStore.toggleTerminal(value);
|
||||
}
|
||||
|
||||
attachTerminal(terminal: ITerminal) {
|
||||
this.#terminalStore.attachTerminal(terminal);
|
||||
}
|
||||
|
||||
onTerminalResize(cols: number, rows: number) {
|
||||
this.#terminalStore.onTerminalResize(cols, rows);
|
||||
}
|
||||
|
||||
setDocuments(files: FileMap) {
|
||||
this.#editorStore.setDocuments(files);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user