From c32bbcc7edc2cf99da52b1407c90238bc781a804 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Thu, 25 Jul 2024 23:31:38 +0100 Subject: feat(gui): directory block, re-org of some code (#2314) --- ui/src/components/runbooks/editor/Editor.tsx | 33 ++- .../runbooks/editor/blocks/Directory/index.tsx | 65 ++++++ .../runbooks/editor/blocks/Run/extensions.ts | 158 +++++++++++++++ .../runbooks/editor/blocks/Run/index.css | 9 + .../runbooks/editor/blocks/Run/index.tsx | 223 +++++++++++++++++++++ .../runbooks/editor/blocks/Run/terminal.tsx | 113 +++++++++++ .../runbooks/editor/blocks/RunBlock/extensions.ts | 158 --------------- .../runbooks/editor/blocks/RunBlock/index.css | 9 - .../runbooks/editor/blocks/RunBlock/index.tsx | 200 ------------------ .../runbooks/editor/blocks/RunBlock/terminal.tsx | 113 ----------- 10 files changed, 594 insertions(+), 487 deletions(-) create mode 100644 ui/src/components/runbooks/editor/blocks/Directory/index.tsx create mode 100644 ui/src/components/runbooks/editor/blocks/Run/extensions.ts create mode 100644 ui/src/components/runbooks/editor/blocks/Run/index.css create mode 100644 ui/src/components/runbooks/editor/blocks/Run/index.tsx create mode 100644 ui/src/components/runbooks/editor/blocks/Run/terminal.tsx delete mode 100644 ui/src/components/runbooks/editor/blocks/RunBlock/extensions.ts delete mode 100644 ui/src/components/runbooks/editor/blocks/RunBlock/index.css delete mode 100644 ui/src/components/runbooks/editor/blocks/RunBlock/index.tsx delete mode 100644 ui/src/components/runbooks/editor/blocks/RunBlock/terminal.tsx (limited to 'ui/src/components/runbooks/editor') diff --git a/ui/src/components/runbooks/editor/Editor.tsx b/ui/src/components/runbooks/editor/Editor.tsx index bbf594d8..6b0522f5 100644 --- a/ui/src/components/runbooks/editor/Editor.tsx +++ b/ui/src/components/runbooks/editor/Editor.tsx @@ -36,10 +36,12 @@ import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/core/fonts/inter.css"; import "@blocknote/mantine/style.css"; -import { Code } from "lucide-react"; +import { CodeIcon, FolderOpenIcon } from "lucide-react"; import { useDebounceCallback } from "usehooks-ts"; -import RunBlock from "@/components/runbooks/editor/blocks/RunBlock"; +import Run from "@/components/runbooks/editor/blocks/Run"; +import Directory from "@/components/runbooks/editor/blocks/Directory"; + import { DeleteBlock } from "@/components/runbooks/editor/ui/DeleteBlockButton"; import { AtuinState, useStore } from "@/state/store"; import Runbook from "@/state/runbooks/runbook"; @@ -52,21 +54,34 @@ const schema = BlockNoteSchema.create({ ...defaultBlockSpecs, // Adds the code block. - run: RunBlock, + run: Run, + directory: Directory, }, }); // Slash menu item to insert an Alert block const insertRun = (editor: typeof schema.BlockNoteEditor) => ({ - title: "Code block", + title: "Code", onItemClick: () => { insertOrUpdateBlock(editor, { type: "run", }); }, - icon: , + icon: , aliases: ["code", "run"], - group: "Code", + group: "Execute", +}); + +const insertDirectory = (editor: typeof schema.BlockNoteEditor) => ({ + title: "Directory", + onItemClick: () => { + insertOrUpdateBlock(editor, { + type: "directory", + }); + }, + icon: , + aliases: ["directory", "dir", "folder"], + group: "Execute", }); export default function Editor() { @@ -161,7 +176,11 @@ export default function Editor() { triggerCharacter={"/"} getItems={async (query: any) => filterSuggestionItems( - [...getDefaultReactSlashMenuItems(editor), insertRun(editor)], + [ + ...getDefaultReactSlashMenuItems(editor), + insertRun(editor), + insertDirectory(editor), + ], query, ) } diff --git a/ui/src/components/runbooks/editor/blocks/Directory/index.tsx b/ui/src/components/runbooks/editor/blocks/Directory/index.tsx new file mode 100644 index 00000000..38e974ff --- /dev/null +++ b/ui/src/components/runbooks/editor/blocks/Directory/index.tsx @@ -0,0 +1,65 @@ +import { useState } from "react"; +import { Input, Tooltip } from "@nextui-org/react"; +import { FolderInputIcon, HelpCircleIcon } from "lucide-react"; + +// @ts-ignore +import { createReactBlockSpec } from "@blocknote/react"; + +interface DirectoryProps { + path: string; + onInputChange: (string) => void; +} + +const Directory = ({ path, onInputChange }: DirectoryProps) => { + const [value, setValue] = useState(path); + + return ( +
+ + { + setValue(val); + onInputChange(val); + }} + startContent={} + /> + +
+ ); +}; + +export default createReactBlockSpec( + { + type: "directory", + propSchema: { + path: { default: "" }, + }, + content: "none", + }, + { + // @ts-ignore + render: ({ block, editor, code, type }) => { + const onInputChange = (val: string) => { + editor.updateBlock(block, { + // @ts-ignore + props: { ...block.props, path: val }, + }); + }; + + return ( + + ); + }, + }, +); diff --git a/ui/src/components/runbooks/editor/blocks/Run/extensions.ts b/ui/src/components/runbooks/editor/blocks/Run/extensions.ts new file mode 100644 index 00000000..76fc4343 --- /dev/null +++ b/ui/src/components/runbooks/editor/blocks/Run/extensions.ts @@ -0,0 +1,158 @@ +// Based on the basicSetup extension, as suggested by the source. Customized for Atuin. + +import { + KeyBinding, + lineNumbers, + highlightActiveLineGutter, + highlightSpecialChars, + drawSelection, + dropCursor, + rectangularSelection, + crosshairCursor, + highlightActiveLine, + keymap, +} from "@codemirror/view"; +import { EditorState, Extension } from "@codemirror/state"; +import { history, defaultKeymap, historyKeymap } from "@codemirror/commands"; +import { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; + +import { + closeBrackets, + autocompletion, + closeBracketsKeymap, + completionKeymap, + CompletionContext, +} from "@codemirror/autocomplete"; + +import { + foldGutter, + indentOnInput, + syntaxHighlighting, + defaultHighlightStyle, + bracketMatching, + indentUnit, + foldKeymap, +} from "@codemirror/language"; + +import { lintKeymap } from "@codemirror/lint"; +import { invoke } from "@tauri-apps/api/core"; + +export interface MinimalSetupOptions { + highlightSpecialChars?: boolean; + history?: boolean; + drawSelection?: boolean; + syntaxHighlighting?: boolean; + + defaultKeymap?: boolean; + historyKeymap?: boolean; +} + +export interface BasicSetupOptions extends MinimalSetupOptions { + lineNumbers?: boolean; + highlightActiveLineGutter?: boolean; + foldGutter?: boolean; + dropCursor?: boolean; + allowMultipleSelections?: boolean; + indentOnInput?: boolean; + bracketMatching?: boolean; + closeBrackets?: boolean; + autocompletion?: boolean; + rectangularSelection?: boolean; + crosshairCursor?: boolean; + highlightActiveLine?: boolean; + highlightSelectionMatches?: boolean; + + closeBracketsKeymap?: boolean; + searchKeymap?: boolean; + foldKeymap?: boolean; + completionKeymap?: boolean; + lintKeymap?: boolean; + tabSize?: number; +} + +function myCompletions(context: CompletionContext) { + let word = context.matchBefore(/^.*/); + + if (!word) return null; + if (word.from == word.to && !context.explicit) return null; + + return invoke("prefix_search", { query: word.text }).then( + // @ts-ignore + (results: string[]) => { + let options = results.map((i) => { + return { label: i, type: "text" }; + }); + + return { + from: word.from, + options, + }; + }, + ); +} + +const buildAutocomplete = (): Extension => { + let ac = autocompletion({ override: [myCompletions] }); + + return ac; +}; + +export const extensions = (options: BasicSetupOptions = {}): Extension[] => { + const { crosshairCursor: initCrosshairCursor = false } = options; + + let keymaps: KeyBinding[] = []; + if (options.closeBracketsKeymap !== false) { + keymaps = keymaps.concat(closeBracketsKeymap); + } + if (options.defaultKeymap !== false) { + keymaps = keymaps.concat(defaultKeymap); + } + if (options.searchKeymap !== false) { + keymaps = keymaps.concat(searchKeymap); + } + if (options.historyKeymap !== false) { + keymaps = keymaps.concat(historyKeymap); + } + if (options.foldKeymap !== false) { + keymaps = keymaps.concat(foldKeymap); + } + if (options.completionKeymap !== false) { + keymaps = keymaps.concat(completionKeymap); + } + if (options.lintKeymap !== false) { + keymaps = keymaps.concat(lintKeymap); + } + const extensions: Extension[] = []; + if (options.lineNumbers !== false) extensions.push(lineNumbers()); + if (options.highlightActiveLineGutter !== false) + extensions.push(highlightActiveLineGutter()); + if (options.highlightSpecialChars !== false) + extensions.push(highlightSpecialChars()); + if (options.history !== false) extensions.push(history()); + if (options.foldGutter !== false) extensions.push(foldGutter()); + if (options.drawSelection !== false) extensions.push(drawSelection()); + if (options.dropCursor !== false) extensions.push(dropCursor()); + if (options.allowMultipleSelections !== false) + extensions.push(EditorState.allowMultipleSelections.of(true)); + if (options.indentOnInput !== false) extensions.push(indentOnInput()); + if (options.syntaxHighlighting !== false) + extensions.push( + syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + ); + + if (options.bracketMatching !== false) extensions.push(bracketMatching()); + if (options.closeBrackets !== false) extensions.push(closeBrackets()); + if (options.autocompletion !== false) extensions.push(buildAutocomplete()); + + if (options.rectangularSelection !== false) + extensions.push(rectangularSelection()); + if (initCrosshairCursor !== false) extensions.push(crosshairCursor()); + if (options.highlightActiveLine !== false) + extensions.push(highlightActiveLine()); + if (options.highlightSelectionMatches !== false) + extensions.push(highlightSelectionMatches()); + if (options.tabSize && typeof options.tabSize === "number") + extensions.push(indentUnit.of(" ".repeat(options.tabSize))); + + return extensions.concat([keymap.of(keymaps.flat())]).filter(Boolean); +}; diff --git a/ui/src/components/runbooks/editor/blocks/Run/index.css b/ui/src/components/runbooks/editor/blocks/Run/index.css new file mode 100644 index 00000000..e854c03b --- /dev/null +++ b/ui/src/components/runbooks/editor/blocks/Run/index.css @@ -0,0 +1,9 @@ +ProseMirror-focused { + outline: none !important; + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1) !important; +} + +.cm-editor.cm-focused { + outline: none !important; + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1) !important; +} diff --git a/ui/src/components/runbooks/editor/blocks/Run/index.tsx b/ui/src/components/runbooks/editor/blocks/Run/index.tsx new file mode 100644 index 00000000..e0989f47 --- /dev/null +++ b/ui/src/components/runbooks/editor/blocks/Run/index.tsx @@ -0,0 +1,223 @@ +// @ts-ignore +import { createReactBlockSpec } from "@blocknote/react"; + +import "./index.css"; + +import CodeMirror from "@uiw/react-codemirror"; +import { keymap } from "@codemirror/view"; +import { langs } from "@uiw/codemirror-extensions-langs"; + +import { Play, Square } from "lucide-react"; +import { useState } from "react"; + +import { extensions } from "./extensions"; +import { platform } from "@tauri-apps/plugin-os"; +import { invoke } from "@tauri-apps/api/core"; +import Terminal from "./terminal.tsx"; + +import "@xterm/xterm/css/xterm.css"; +import { AtuinState, useStore } from "@/state/store.ts"; + +interface RunBlockProps { + onChange: (val: string) => void; + onRun?: (pty: string) => void; + onStop?: (pty: string) => void; + id: string; + code: string; + type: string; + pty: string; + isEditable: boolean; + editor: any; +} + +const findFirstParentOfType = (editor: any, id: string, type: string): any => { + // TODO: the types for blocknote aren't working. Now I'm doing this sort of shit, + // really need to fix that. + const document = editor.document; + var lastOfType = null; + + // Iterate through ALL of the blocks. + for (let i = 0; i < document.length; i++) { + if (document[i].id == id) return lastOfType; + + if (document[i].type == type) lastOfType = document[i]; + } + + return lastOfType; +}; + +const RunBlock = ({ + onChange, + id, + code, + isEditable, + onRun, + onStop, + pty, + editor, +}: RunBlockProps) => { + const [value, setValue] = useState(code); + const cleanupPtyTerm = useStore((store: AtuinState) => store.cleanupPtyTerm); + const terminals = useStore((store: AtuinState) => store.terminals); + + const [currentRunbook, incRunbookPty, decRunbookPty] = useStore( + (store: AtuinState) => [ + store.currentRunbook, + store.incRunbookPty, + store.decRunbookPty, + ], + ); + + const isRunning = pty !== null && pty !== ""; + + const handleToggle = async (event: any | null) => { + if (event) event.stopPropagation(); + + // If there's no code, don't do anything + if (!value) return; + + if (isRunning) { + await invoke("pty_kill", { pid: pty }); + + terminals[pty].terminal.dispose(); + cleanupPtyTerm(pty); + + if (onStop) onStop(pty); + if (currentRunbook) decRunbookPty(currentRunbook); + } + + if (!isRunning) { + const cwd = findFirstParentOfType(editor, id, "directory"); + console.log(cwd.props.path); + let pty = await invoke("pty_open", { cwd: cwd.props.path }); + if (onRun) onRun(pty); + + if (currentRunbook) incRunbookPty(currentRunbook); + + let isWindows = platform() == "windows"; + let cmdEnd = isWindows ? "\r\n" : "\n"; + + let val = !value.endsWith("\n") ? value + cmdEnd : value; + await invoke("pty_write", { pid: pty, data: val }); + } + }; + + const handleCmdEnter = () => { + handleToggle(null); + return true; + }; + + const customKeymap = keymap.of([ + { + key: "Mod-Enter", + run: handleCmdEnter, + }, + ]); + + return ( +
+
+
+ +
+
+ { + setValue(val); + onChange(val); + }} + extensions={[customKeymap, ...extensions(), langs.shell()]} + basicSetup={false} + /> +
+ {pty && } +
+
+
+
+ ); +}; + +export default createReactBlockSpec( + { + type: "run", + propSchema: { + type: { + default: "bash", + }, + code: { default: "" }, + pty: { default: "" }, + global: { default: false }, + }, + content: "none", + }, + { + // @ts-ignore + render: ({ block, editor, code, type }) => { + const onInputChange = (val: string) => { + editor.updateBlock(block, { + // @ts-ignore + props: { ...block.props, code: val }, + }); + }; + + const onRun = (pty: string) => { + editor.updateBlock(block, { + // @ts-ignore + props: { ...block.props, pty: pty }, + }); + }; + + const onStop = (_pty: string) => { + editor?.updateBlock(block, { + props: { ...block.props, pty: "" }, + }); + }; + + return ( + + ); + }, + toExternalHTML: ({ block }) => { + return ( +
+          {block?.props?.code}
+        
+ ); + }, + }, +); diff --git a/ui/src/components/runbooks/editor/blocks/Run/terminal.tsx b/ui/src/components/runbooks/editor/blocks/Run/terminal.tsx new file mode 100644 index 00000000..a6dc589f --- /dev/null +++ b/ui/src/components/runbooks/editor/blocks/Run/terminal.tsx @@ -0,0 +1,113 @@ +import { useState, useEffect, useRef } from "react"; +import { listen } from "@tauri-apps/api/event"; +import "@xterm/xterm/css/xterm.css"; +import { useStore } from "@/state/store"; +import { invoke } from "@tauri-apps/api/core"; +import { IDisposable } from "@xterm/xterm"; + +const usePersistentTerminal = (pty: string) => { + const newPtyTerm = useStore((store) => store.newPtyTerm); + const terminals = useStore((store) => store.terminals); + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + if (!terminals.hasOwnProperty(pty)) { + // create a new terminal and store it in the store. + // this means we can resume the same instance even across mount/dismount + newPtyTerm(pty); + } + + setIsReady(true); + + return () => { + // We don't dispose of the terminal when the component unmounts + }; + }, [pty, terminals, newPtyTerm]); + + return { terminalData: terminals[pty], isReady }; +}; + +const TerminalComponent = ({ pty }: any) => { + const terminalRef = useRef(null); + const { terminalData, isReady } = usePersistentTerminal(pty); + const [isAttached, setIsAttached] = useState(false); + const cleanupListenerRef = useRef<(() => void) | null>(null); + const keyDispose = useRef(null); + + useEffect(() => { + // no pty? no terminal + if (pty == null) return; + + // the terminal may still be being created so hold off + if (!isReady) return; + + const windowResize = () => { + if (!terminalData || !terminalData.fitAddon) return; + + terminalData.fitAddon.fit(); + }; + + // terminal object needs attaching to a ref to a div + if (!isAttached && terminalData && terminalData.terminal) { + // If it's never been attached, attach it + if (!terminalData.terminal.element && terminalRef.current) { + terminalData.terminal.open(terminalRef.current); + + // it might have been previously attached, but need moving elsewhere + } else if (terminalData && terminalRef.current) { + // @ts-ignore + terminalRef.current.appendChild(terminalData.terminal.element); + } + + terminalData.fitAddon.fit(); + setIsAttached(true); + + window.addEventListener("resize", windowResize); + + const disposeOnKey = terminalData.terminal.onKey(async (event) => { + await invoke("pty_write", { pid: pty, data: event.key }); + }); + + keyDispose.current = disposeOnKey; + } + + listen(`pty-${pty}`, (event: any) => { + terminalData.terminal.write(event.payload); + }).then((ul) => { + cleanupListenerRef.current = ul; + }); + + // Customize further as needed + return () => { + if ( + terminalData && + terminalData.terminal && + terminalData.terminal.element + ) { + // Instead of removing, we just detach + if (terminalData.terminal.element.parentElement) { + terminalData.terminal.element.parentElement.removeChild( + terminalData.terminal.element, + ); + } + setIsAttached(false); + } + + if (cleanupListenerRef.current) { + cleanupListenerRef.current(); + } + + if (keyDispose.current) keyDispose.current.dispose(); + + window.removeEventListener("resize", windowResize); + }; + }, [terminalData, isReady]); + + if (!isReady) return null; + + return ( +
+ ); +}; + +export default TerminalComponent; diff --git a/ui/src/components/runbooks/editor/blocks/RunBlock/extensions.ts b/ui/src/components/runbooks/editor/blocks/RunBlock/extensions.ts deleted file mode 100644 index 76fc4343..00000000 --- a/ui/src/components/runbooks/editor/blocks/RunBlock/extensions.ts +++ /dev/null @@ -1,158 +0,0 @@ -// Based on the basicSetup extension, as suggested by the source. Customized for Atuin. - -import { - KeyBinding, - lineNumbers, - highlightActiveLineGutter, - highlightSpecialChars, - drawSelection, - dropCursor, - rectangularSelection, - crosshairCursor, - highlightActiveLine, - keymap, -} from "@codemirror/view"; -import { EditorState, Extension } from "@codemirror/state"; -import { history, defaultKeymap, historyKeymap } from "@codemirror/commands"; -import { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; - -import { - closeBrackets, - autocompletion, - closeBracketsKeymap, - completionKeymap, - CompletionContext, -} from "@codemirror/autocomplete"; - -import { - foldGutter, - indentOnInput, - syntaxHighlighting, - defaultHighlightStyle, - bracketMatching, - indentUnit, - foldKeymap, -} from "@codemirror/language"; - -import { lintKeymap } from "@codemirror/lint"; -import { invoke } from "@tauri-apps/api/core"; - -export interface MinimalSetupOptions { - highlightSpecialChars?: boolean; - history?: boolean; - drawSelection?: boolean; - syntaxHighlighting?: boolean; - - defaultKeymap?: boolean; - historyKeymap?: boolean; -} - -export interface BasicSetupOptions extends MinimalSetupOptions { - lineNumbers?: boolean; - highlightActiveLineGutter?: boolean; - foldGutter?: boolean; - dropCursor?: boolean; - allowMultipleSelections?: boolean; - indentOnInput?: boolean; - bracketMatching?: boolean; - closeBrackets?: boolean; - autocompletion?: boolean; - rectangularSelection?: boolean; - crosshairCursor?: boolean; - highlightActiveLine?: boolean; - highlightSelectionMatches?: boolean; - - closeBracketsKeymap?: boolean; - searchKeymap?: boolean; - foldKeymap?: boolean; - completionKeymap?: boolean; - lintKeymap?: boolean; - tabSize?: number; -} - -function myCompletions(context: CompletionContext) { - let word = context.matchBefore(/^.*/); - - if (!word) return null; - if (word.from == word.to && !context.explicit) return null; - - return invoke("prefix_search", { query: word.text }).then( - // @ts-ignore - (results: string[]) => { - let options = results.map((i) => { - return { label: i, type: "text" }; - }); - - return { - from: word.from, - options, - }; - }, - ); -} - -const buildAutocomplete = (): Extension => { - let ac = autocompletion({ override: [myCompletions] }); - - return ac; -}; - -export const extensions = (options: BasicSetupOptions = {}): Extension[] => { - const { crosshairCursor: initCrosshairCursor = false } = options; - - let keymaps: KeyBinding[] = []; - if (options.closeBracketsKeymap !== false) { - keymaps = keymaps.concat(closeBracketsKeymap); - } - if (options.defaultKeymap !== false) { - keymaps = keymaps.concat(defaultKeymap); - } - if (options.searchKeymap !== false) { - keymaps = keymaps.concat(searchKeymap); - } - if (options.historyKeymap !== false) { - keymaps = keymaps.concat(historyKeymap); - } - if (options.foldKeymap !== false) { - keymaps = keymaps.concat(foldKeymap); - } - if (options.completionKeymap !== false) { - keymaps = keymaps.concat(completionKeymap); - } - if (options.lintKeymap !== false) { - keymaps = keymaps.concat(lintKeymap); - } - const extensions: Extension[] = []; - if (options.lineNumbers !== false) extensions.push(lineNumbers()); - if (options.highlightActiveLineGutter !== false) - extensions.push(highlightActiveLineGutter()); - if (options.highlightSpecialChars !== false) - extensions.push(highlightSpecialChars()); - if (options.history !== false) extensions.push(history()); - if (options.foldGutter !== false) extensions.push(foldGutter()); - if (options.drawSelection !== false) extensions.push(drawSelection()); - if (options.dropCursor !== false) extensions.push(dropCursor()); - if (options.allowMultipleSelections !== false) - extensions.push(EditorState.allowMultipleSelections.of(true)); - if (options.indentOnInput !== false) extensions.push(indentOnInput()); - if (options.syntaxHighlighting !== false) - extensions.push( - syntaxHighlighting(defaultHighlightStyle, { fallback: true }), - ); - - if (options.bracketMatching !== false) extensions.push(bracketMatching()); - if (options.closeBrackets !== false) extensions.push(closeBrackets()); - if (options.autocompletion !== false) extensions.push(buildAutocomplete()); - - if (options.rectangularSelection !== false) - extensions.push(rectangularSelection()); - if (initCrosshairCursor !== false) extensions.push(crosshairCursor()); - if (options.highlightActiveLine !== false) - extensions.push(highlightActiveLine()); - if (options.highlightSelectionMatches !== false) - extensions.push(highlightSelectionMatches()); - if (options.tabSize && typeof options.tabSize === "number") - extensions.push(indentUnit.of(" ".repeat(options.tabSize))); - - return extensions.concat([keymap.of(keymaps.flat())]).filter(Boolean); -}; diff --git a/ui/src/components/runbooks/editor/blocks/RunBlock/index.css b/ui/src/components/runbooks/editor/blocks/RunBlock/index.css deleted file mode 100644 index e854c03b..00000000 --- a/ui/src/components/runbooks/editor/blocks/RunBlock/index.css +++ /dev/null @@ -1,9 +0,0 @@ -ProseMirror-focused { - outline: none !important; - box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1) !important; -} - -.cm-editor.cm-focused { - outline: none !important; - box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1) !important; -} diff --git a/ui/src/components/runbooks/editor/blocks/RunBlock/index.tsx b/ui/src/components/runbooks/editor/blocks/RunBlock/index.tsx deleted file mode 100644 index 25faa211..00000000 --- a/ui/src/components/runbooks/editor/blocks/RunBlock/index.tsx +++ /dev/null @@ -1,200 +0,0 @@ -// @ts-ignore -import { createReactBlockSpec } from "@blocknote/react"; -import "./index.css"; - -import CodeMirror from "@uiw/react-codemirror"; -import { keymap } from "@codemirror/view"; -import { langs } from "@uiw/codemirror-extensions-langs"; - -import { Play, Square } from "lucide-react"; -import { useState } from "react"; - -import { extensions } from "./extensions"; -import { platform } from "@tauri-apps/plugin-os"; -import { invoke } from "@tauri-apps/api/core"; -import Terminal from "./terminal.tsx"; - -import "@xterm/xterm/css/xterm.css"; -import { AtuinState, useStore } from "@/state/store.ts"; - -interface RunBlockProps { - onChange: (val: string) => void; - onRun?: (pty: string) => void; - onStop?: (pty: string) => void; - id: string; - code: string; - type: string; - pty: string; - isEditable: boolean; -} - -const RunBlock = ({ - onChange, - id, - code, - isEditable, - onRun, - onStop, - pty, -}: RunBlockProps) => { - const [value, setValue] = useState(code); - const cleanupPtyTerm = useStore((store: AtuinState) => store.cleanupPtyTerm); - const terminals = useStore((store: AtuinState) => store.terminals); - - const [currentRunbook, incRunbookPty, decRunbookPty] = useStore( - (store: AtuinState) => [ - store.currentRunbook, - store.incRunbookPty, - store.decRunbookPty, - ], - ); - - const isRunning = pty !== null && pty !== ""; - - const handleToggle = async (event: any | null) => { - if (event) event.stopPropagation(); - - // If there's no code, don't do anything - if (!value) return; - - if (isRunning) { - await invoke("pty_kill", { pid: pty }); - - terminals[pty].terminal.dispose(); - cleanupPtyTerm(pty); - - if (onStop) onStop(pty); - if (currentRunbook) decRunbookPty(currentRunbook); - } - - if (!isRunning) { - let pty = await invoke("pty_open"); - if (onRun) onRun(pty); - - if (currentRunbook) incRunbookPty(currentRunbook); - - let isWindows = platform() == "windows"; - let cmdEnd = isWindows ? "\r\n" : "\n"; - - let val = !value.endsWith("\n") ? value + cmdEnd : value; - await invoke("pty_write", { pid: pty, data: val }); - } - }; - - const handleCmdEnter = () => { - handleToggle(null); - return true; - }; - - const customKeymap = keymap.of([ - { - key: "Mod-Enter", - run: handleCmdEnter, - }, - ]); - - return ( -
-
-
- -
-
- { - setValue(val); - onChange(val); - }} - extensions={[customKeymap, ...extensions(), langs.shell()]} - basicSetup={false} - /> -
- {pty && } -
-
-
-
- ); -}; - -export default createReactBlockSpec( - { - type: "run", - propSchema: { - type: { - default: "bash", - }, - code: { default: "" }, - pty: { default: "" }, - }, - content: "none", - }, - { - // @ts-ignore - render: ({ block, editor, code, type }) => { - const onInputChange = (val: string) => { - editor.updateBlock(block, { - // @ts-ignore - props: { ...block.props, code: val }, - }); - }; - - const onRun = (pty: string) => { - editor.updateBlock(block, { - // @ts-ignore - props: { ...block.props, pty: pty }, - }); - }; - - const onStop = (_pty: string) => { - editor?.updateBlock(block, { - props: { ...block.props, pty: "" }, - }); - }; - - return ( - - ); - }, - toExternalHTML: ({ block }) => { - return ( -
-          {block?.props?.code}
-        
- ); - }, - }, -); diff --git a/ui/src/components/runbooks/editor/blocks/RunBlock/terminal.tsx b/ui/src/components/runbooks/editor/blocks/RunBlock/terminal.tsx deleted file mode 100644 index a6dc589f..00000000 --- a/ui/src/components/runbooks/editor/blocks/RunBlock/terminal.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import { listen } from "@tauri-apps/api/event"; -import "@xterm/xterm/css/xterm.css"; -import { useStore } from "@/state/store"; -import { invoke } from "@tauri-apps/api/core"; -import { IDisposable } from "@xterm/xterm"; - -const usePersistentTerminal = (pty: string) => { - const newPtyTerm = useStore((store) => store.newPtyTerm); - const terminals = useStore((store) => store.terminals); - const [isReady, setIsReady] = useState(false); - - useEffect(() => { - if (!terminals.hasOwnProperty(pty)) { - // create a new terminal and store it in the store. - // this means we can resume the same instance even across mount/dismount - newPtyTerm(pty); - } - - setIsReady(true); - - return () => { - // We don't dispose of the terminal when the component unmounts - }; - }, [pty, terminals, newPtyTerm]); - - return { terminalData: terminals[pty], isReady }; -}; - -const TerminalComponent = ({ pty }: any) => { - const terminalRef = useRef(null); - const { terminalData, isReady } = usePersistentTerminal(pty); - const [isAttached, setIsAttached] = useState(false); - const cleanupListenerRef = useRef<(() => void) | null>(null); - const keyDispose = useRef(null); - - useEffect(() => { - // no pty? no terminal - if (pty == null) return; - - // the terminal may still be being created so hold off - if (!isReady) return; - - const windowResize = () => { - if (!terminalData || !terminalData.fitAddon) return; - - terminalData.fitAddon.fit(); - }; - - // terminal object needs attaching to a ref to a div - if (!isAttached && terminalData && terminalData.terminal) { - // If it's never been attached, attach it - if (!terminalData.terminal.element && terminalRef.current) { - terminalData.terminal.open(terminalRef.current); - - // it might have been previously attached, but need moving elsewhere - } else if (terminalData && terminalRef.current) { - // @ts-ignore - terminalRef.current.appendChild(terminalData.terminal.element); - } - - terminalData.fitAddon.fit(); - setIsAttached(true); - - window.addEventListener("resize", windowResize); - - const disposeOnKey = terminalData.terminal.onKey(async (event) => { - await invoke("pty_write", { pid: pty, data: event.key }); - }); - - keyDispose.current = disposeOnKey; - } - - listen(`pty-${pty}`, (event: any) => { - terminalData.terminal.write(event.payload); - }).then((ul) => { - cleanupListenerRef.current = ul; - }); - - // Customize further as needed - return () => { - if ( - terminalData && - terminalData.terminal && - terminalData.terminal.element - ) { - // Instead of removing, we just detach - if (terminalData.terminal.element.parentElement) { - terminalData.terminal.element.parentElement.removeChild( - terminalData.terminal.element, - ); - } - setIsAttached(false); - } - - if (cleanupListenerRef.current) { - cleanupListenerRef.current(); - } - - if (keyDispose.current) keyDispose.current.dispose(); - - window.removeEventListener("resize", windowResize); - }; - }, [terminalData, isReady]); - - if (!isReady) return null; - - return ( -
- ); -}; - -export default TerminalComponent; -- cgit v1.3.1