feat(gui): directory block, re-org of some code (#2314)

This commit is contained in:
Ellie Huxtable 2024-07-25 23:31:38 +01:00 committed by GitHub
parent 5ba185e7d8
commit c32bbcc7ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 150 additions and 28 deletions

1
ui/backend/Cargo.lock generated
View File

@ -6482,6 +6482,7 @@ dependencies = [
"portable-pty", "portable-pty",
"serde", "serde",
"serde_json", "serde_json",
"shellexpand",
"sqlx", "sqlx",
"syntect", "syntect",
"tauri", "tauri",

View File

@ -24,17 +24,19 @@ serde_json = "1.0"
time = "0.3.36" time = "0.3.36"
uuid = "1.7.0" uuid = "1.7.0"
syntect = "5.2.0" syntect = "5.2.0"
tauri-plugin-http = "2.0.0-beta"
tauri-plugin-single-instance = "2.0.0-beta"
tokio = "1.38.0" tokio = "1.38.0"
tauri-plugin-shell = "2.0.0-beta.7"
comrak = "0.22" comrak = "0.22"
portable-pty = "0.8.1" portable-pty = "0.8.1"
vt100 = "0.15.2" vt100 = "0.15.2"
bytes = "1.6.0" bytes = "1.6.0"
nix = "0.29.0" nix = "0.29.0"
lazy_static = "1.5.0" lazy_static = "1.5.0"
shellexpand = "3.1.0"
tauri-plugin-http = "2.0.0-beta"
tauri-plugin-single-instance = "2.0.0-beta"
tauri-plugin-os = "2.0.0-beta.8" tauri-plugin-os = "2.0.0-beta.8"
tauri-plugin-shell = "2.0.0-beta.7"
[target."cfg(target_os = \"macos\")".dependencies] [target."cfg(target_os = \"macos\")".dependencies]
cocoa = "0.25" cocoa = "0.25"

View File

@ -16,7 +16,7 @@ pub struct Pty {
} }
impl Pty { impl Pty {
pub async fn open<'a>(rows: u16, cols: u16) -> Result<Self> { pub async fn open<'a>(rows: u16, cols: u16, cwd: Option<String>) -> Result<Self> {
let sys = portable_pty::native_pty_system(); let sys = portable_pty::native_pty_system();
let pair = sys let pair = sys
@ -28,7 +28,11 @@ impl Pty {
}) })
.map_err(|e| eyre!("Failed to open pty: {}", e))?; .map_err(|e| eyre!("Failed to open pty: {}", e))?;
let cmd = CommandBuilder::new_default_prog(); let mut cmd = CommandBuilder::new_default_prog();
if let Some(cwd) = cwd {
cmd.cwd(cwd);
}
let child = pair.slave.spawn_command(cmd).unwrap(); let child = pair.slave.spawn_command(cmd).unwrap();
drop(pair.slave); drop(pair.slave);

View File

@ -11,9 +11,12 @@ use atuin_client::{database::Sqlite, record::sqlite_store::SqliteStore, settings
pub async fn pty_open<'a>( pub async fn pty_open<'a>(
app: tauri::AppHandle, app: tauri::AppHandle,
state: State<'a, AtuinState>, state: State<'a, AtuinState>,
cwd: Option<String>,
) -> Result<uuid::Uuid, String> { ) -> Result<uuid::Uuid, String> {
let id = uuid::Uuid::new_v4(); let id = uuid::Uuid::new_v4();
let pty = crate::pty::Pty::open(24, 80).await.unwrap();
let cwd = cwd.map(|c|shellexpand::tilde(c.as_str()).to_string());
let pty = crate::pty::Pty::open(24, 80, cwd).await.unwrap();
let reader = pty.reader.clone(); let reader = pty.reader.clone();

View File

@ -19,7 +19,7 @@ html {
} }
.history-list { .history-list {
height: calc(100vh - 150px - 64px); height: calc(100dvh - 4rem - 2rem);
} }
.history-item { .history-item {

View File

@ -109,8 +109,7 @@ const NoteSidebar = () => {
onPress={async () => { onPress={async () => {
await Runbook.delete(runbook.id); await Runbook.delete(runbook.id);
if (runbook.id == currentRunbook) if (runbook.id == currentRunbook) setCurrentRunbook("");
setCurrentRunbook(null);
refreshRunbooks(); refreshRunbooks();
}} }}

View File

@ -36,10 +36,12 @@ import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/core/fonts/inter.css"; import "@blocknote/core/fonts/inter.css";
import "@blocknote/mantine/style.css"; import "@blocknote/mantine/style.css";
import { Code } from "lucide-react"; import { CodeIcon, FolderOpenIcon } from "lucide-react";
import { useDebounceCallback } from "usehooks-ts"; 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 { DeleteBlock } from "@/components/runbooks/editor/ui/DeleteBlockButton";
import { AtuinState, useStore } from "@/state/store"; import { AtuinState, useStore } from "@/state/store";
import Runbook from "@/state/runbooks/runbook"; import Runbook from "@/state/runbooks/runbook";
@ -52,21 +54,34 @@ const schema = BlockNoteSchema.create({
...defaultBlockSpecs, ...defaultBlockSpecs,
// Adds the code block. // Adds the code block.
run: RunBlock, run: Run,
directory: Directory,
}, },
}); });
// Slash menu item to insert an Alert block // Slash menu item to insert an Alert block
const insertRun = (editor: typeof schema.BlockNoteEditor) => ({ const insertRun = (editor: typeof schema.BlockNoteEditor) => ({
title: "Code block", title: "Code",
onItemClick: () => { onItemClick: () => {
insertOrUpdateBlock(editor, { insertOrUpdateBlock(editor, {
type: "run", type: "run",
}); });
}, },
icon: <Code size={18} />, icon: <CodeIcon size={18} />,
aliases: ["code", "run"], aliases: ["code", "run"],
group: "Code", group: "Execute",
});
const insertDirectory = (editor: typeof schema.BlockNoteEditor) => ({
title: "Directory",
onItemClick: () => {
insertOrUpdateBlock(editor, {
type: "directory",
});
},
icon: <FolderOpenIcon size={18} />,
aliases: ["directory", "dir", "folder"],
group: "Execute",
}); });
export default function Editor() { export default function Editor() {
@ -161,7 +176,11 @@ export default function Editor() {
triggerCharacter={"/"} triggerCharacter={"/"}
getItems={async (query: any) => getItems={async (query: any) =>
filterSuggestionItems( filterSuggestionItems(
[...getDefaultReactSlashMenuItems(editor), insertRun(editor)], [
...getDefaultReactSlashMenuItems(editor),
insertRun(editor),
insertDirectory(editor),
],
query, query,
) )
} }

View File

@ -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 (
<div className="w-full !max-w-full !outline-none overflow-none">
<Tooltip
content="Change working directory for all subsequent code blocks"
delay={1000}
>
<Input
label="Directory"
placeholder="~"
labelPlacement="outside"
value={value}
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
spellCheck="false"
onValueChange={(val) => {
setValue(val);
onInputChange(val);
}}
startContent={<FolderInputIcon />}
/>
</Tooltip>
</div>
);
};
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 (
<Directory path={block.props.path} onInputChange={onInputChange} />
);
},
},
);

View File

@ -1,5 +1,6 @@
// @ts-ignore // @ts-ignore
import { createReactBlockSpec } from "@blocknote/react"; import { createReactBlockSpec } from "@blocknote/react";
import "./index.css"; import "./index.css";
import CodeMirror from "@uiw/react-codemirror"; import CodeMirror from "@uiw/react-codemirror";
@ -26,8 +27,25 @@ interface RunBlockProps {
type: string; type: string;
pty: string; pty: string;
isEditable: boolean; 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 = ({ const RunBlock = ({
onChange, onChange,
id, id,
@ -36,6 +54,7 @@ const RunBlock = ({
onRun, onRun,
onStop, onStop,
pty, pty,
editor,
}: RunBlockProps) => { }: RunBlockProps) => {
const [value, setValue] = useState<String>(code); const [value, setValue] = useState<String>(code);
const cleanupPtyTerm = useStore((store: AtuinState) => store.cleanupPtyTerm); const cleanupPtyTerm = useStore((store: AtuinState) => store.cleanupPtyTerm);
@ -68,7 +87,9 @@ const RunBlock = ({
} }
if (!isRunning) { if (!isRunning) {
let pty = await invoke<string>("pty_open"); const cwd = findFirstParentOfType(editor, id, "directory");
console.log(cwd.props.path);
let pty = await invoke<string>("pty_open", { cwd: cwd.props.path });
if (onRun) onRun(pty); if (onRun) onRun(pty);
if (currentRunbook) incRunbookPty(currentRunbook); if (currentRunbook) incRunbookPty(currentRunbook);
@ -150,6 +171,7 @@ export default createReactBlockSpec(
}, },
code: { default: "" }, code: { default: "" },
pty: { default: "" }, pty: { default: "" },
global: { default: false },
}, },
content: "none", content: "none",
}, },
@ -186,6 +208,7 @@ export default createReactBlockSpec(
isEditable={editor.isEditable} isEditable={editor.isEditable}
onRun={onRun} onRun={onRun}
onStop={onStop} onStop={onStop}
editor={editor}
/> />
); );
}, },

View File

@ -6,7 +6,7 @@ import HistorySearch from "@/components/HistorySearch.tsx";
import Stats from "@/components/history/Stats.tsx"; import Stats from "@/components/history/Stats.tsx";
import Drawer from "@/components/Drawer.tsx"; import Drawer from "@/components/Drawer.tsx";
import { useStore } from "@/state/store"; import { AtuinState, useStore } from "@/state/store";
function Header() { function Header() {
return ( return (
@ -49,9 +49,13 @@ function Header() {
} }
export default function Search() { export default function Search() {
const history = useStore((state) => state.shellHistory); const history = useStore((state: AtuinState) => state.shellHistory);
const refreshHistory = useStore((state) => state.refreshShellHistory); const refreshHistory = useStore(
const historyNextPage = useStore((state) => state.historyNextPage); (state: AtuinState) => state.refreshShellHistory,
);
const historyNextPage = useStore(
(state: AtuinState) => state.historyNextPage,
);
let [query, setQuery] = useState(""); let [query, setQuery] = useState("");
@ -84,12 +88,7 @@ export default function Search() {
return ( return (
<> <>
<div className="w-full flex-1 flex-col p-4"> <div className="w-full flex-1 flex-col">
<div className="p-10 history-header">
<Header />
<p>A history of all the commands you run in your shell.</p>
</div>
<div className="flex h-16 shrink-0 items-center gap-x-4 border-b border-t border-gray-200 bg-white px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8 history-search"> <div className="flex h-16 shrink-0 items-center gap-x-4 border-b border-t border-gray-200 bg-white px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8 history-search">
<HistorySearch <HistorySearch
query={query} query={query}

View File

@ -1,5 +1,8 @@
import Editor from "@/components/runbooks/editor/Editor"; import Editor from "@/components/runbooks/editor/Editor";
import List from "@/components/runbooks/List"; import List from "@/components/runbooks/List";
import { Checkbox } from "@nextui-org/react";
import { useStore } from "@/state/store"; import { useStore } from "@/state/store";
export default function Runbooks() { export default function Runbooks() {
@ -8,7 +11,11 @@ export default function Runbooks() {
return ( return (
<div className="flex w-full !max-w-full flex-row "> <div className="flex w-full !max-w-full flex-row ">
<List /> <List />
{currentRunbook && <Editor />} {currentRunbook && (
<div className="flex w-full">
<Editor />
</div>
)}
{!currentRunbook && ( {!currentRunbook && (
<div className="flex align-middle justify-center flex-col h-screen w-full"> <div className="flex align-middle justify-center flex-col h-screen w-full">