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",
"serde",
"serde_json",
"shellexpand",
"sqlx",
"syntect",
"tauri",

View File

@ -24,17 +24,19 @@ serde_json = "1.0"
time = "0.3.36"
uuid = "1.7.0"
syntect = "5.2.0"
tauri-plugin-http = "2.0.0-beta"
tauri-plugin-single-instance = "2.0.0-beta"
tokio = "1.38.0"
tauri-plugin-shell = "2.0.0-beta.7"
comrak = "0.22"
portable-pty = "0.8.1"
vt100 = "0.15.2"
bytes = "1.6.0"
nix = "0.29.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-shell = "2.0.0-beta.7"
[target."cfg(target_os = \"macos\")".dependencies]
cocoa = "0.25"

View File

@ -16,7 +16,7 @@ pub struct 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 pair = sys
@ -28,7 +28,11 @@ impl Pty {
})
.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();
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>(
app: tauri::AppHandle,
state: State<'a, AtuinState>,
cwd: Option<String>,
) -> Result<uuid::Uuid, String> {
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();

View File

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

View File

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

View File

@ -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: <Code size={18} />,
icon: <CodeIcon size={18} />,
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() {
@ -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,
)
}

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
import { createReactBlockSpec } from "@blocknote/react";
import "./index.css";
import CodeMirror from "@uiw/react-codemirror";
@ -26,8 +27,25 @@ interface RunBlockProps {
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,
@ -36,6 +54,7 @@ const RunBlock = ({
onRun,
onStop,
pty,
editor,
}: RunBlockProps) => {
const [value, setValue] = useState<String>(code);
const cleanupPtyTerm = useStore((store: AtuinState) => store.cleanupPtyTerm);
@ -68,7 +87,9 @@ const RunBlock = ({
}
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 (currentRunbook) incRunbookPty(currentRunbook);
@ -150,6 +171,7 @@ export default createReactBlockSpec(
},
code: { default: "" },
pty: { default: "" },
global: { default: false },
},
content: "none",
},
@ -186,6 +208,7 @@ export default createReactBlockSpec(
isEditable={editor.isEditable}
onRun={onRun}
onStop={onStop}
editor={editor}
/>
);
},

View File

@ -6,7 +6,7 @@ import HistorySearch from "@/components/HistorySearch.tsx";
import Stats from "@/components/history/Stats.tsx";
import Drawer from "@/components/Drawer.tsx";
import { useStore } from "@/state/store";
import { AtuinState, useStore } from "@/state/store";
function Header() {
return (
@ -49,9 +49,13 @@ function Header() {
}
export default function Search() {
const history = useStore((state) => state.shellHistory);
const refreshHistory = useStore((state) => state.refreshShellHistory);
const historyNextPage = useStore((state) => state.historyNextPage);
const history = useStore((state: AtuinState) => state.shellHistory);
const refreshHistory = useStore(
(state: AtuinState) => state.refreshShellHistory,
);
const historyNextPage = useStore(
(state: AtuinState) => state.historyNextPage,
);
let [query, setQuery] = useState("");
@ -84,12 +88,7 @@ export default function Search() {
return (
<>
<div className="w-full flex-1 flex-col p-4">
<div className="p-10 history-header">
<Header />
<p>A history of all the commands you run in your shell.</p>
</div>
<div className="w-full flex-1 flex-col">
<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
query={query}

View File

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