mirror of
https://github.com/atuinsh/atuin.git
synced 2025-06-27 21:31:49 +02:00
feat(gui): background terminals and more (#2303)
* fixes & allow for background terminals to stay running * status indicators etc
This commit is contained in:
parent
fde86d3899
commit
95cef71490
@ -86,9 +86,17 @@ pub(crate) async fn pty_kill(
|
|||||||
pid: uuid::Uuid,
|
pid: uuid::Uuid,
|
||||||
state: tauri::State<'_, AtuinState>,
|
state: tauri::State<'_, AtuinState>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let pty = state.pty_sessions.write().await.remove(&pid).unwrap();
|
let pty = state.pty_sessions.write().await.remove(&pid);
|
||||||
|
|
||||||
|
match pty {
|
||||||
|
Some(pty)=>{
|
||||||
|
|
||||||
pty.kill_child().await.map_err(|e|e.to_string())?;
|
pty.kill_child().await.map_err(|e|e.to_string())?;
|
||||||
println!("RIP {pid:?}");
|
println!("RIP {pid:?}");
|
||||||
|
}
|
||||||
|
None=>{}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -131,10 +131,11 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex h-dvh w-screen select-none"
|
className="flex w-screen select-none"
|
||||||
style={{ maxWidth: "100vw" }}
|
style={{ maxWidth: "100vw", height: "calc(100dvh - 2rem)" }}
|
||||||
>
|
>
|
||||||
<div className="relative flex h-full flex-col !border-r-small border-divider transition-width pb-6 pt-9 items-center">
|
<div className="flex w-full">
|
||||||
|
<div className="relative flex flex-col !border-r-small border-divider transition-width pb-6 pt-4 items-center">
|
||||||
<div className="flex items-center gap-0 px-3 justify-center">
|
<div className="flex items-center gap-0 px-3 justify-center">
|
||||||
<div className="flex h-8 w-8">
|
<div className="flex h-8 w-8">
|
||||||
<img src={icon} alt="icon" className="h-8 w-8" />
|
<img src={icon} alt="icon" className="h-8 w-8" />
|
||||||
@ -146,6 +147,7 @@ function App() {
|
|||||||
defaultSelectedKey="home"
|
defaultSelectedKey="home"
|
||||||
isCompact={true}
|
isCompact={true}
|
||||||
items={navigation}
|
items={navigation}
|
||||||
|
className="z-50"
|
||||||
/>
|
/>
|
||||||
</ScrollShadow>
|
</ScrollShadow>
|
||||||
|
|
||||||
@ -193,7 +195,9 @@ function App() {
|
|||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="settings"
|
key="settings"
|
||||||
description="Configure Atuin"
|
description="Configure Atuin"
|
||||||
startContent={<Icon icon="solar:settings-linear" width={24} />}
|
startContent={
|
||||||
|
<Icon icon="solar:settings-linear" width={24} />
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
@ -243,7 +247,11 @@ function App() {
|
|||||||
{renderMain(section)}
|
{renderMain(section)}
|
||||||
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange} placement="top-center">
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
placement="top-center"
|
||||||
|
>
|
||||||
<ModalContent className="p-8">
|
<ModalContent className="p-8">
|
||||||
{(onClose) => (
|
{(onClose) => (
|
||||||
<>
|
<>
|
||||||
@ -253,6 +261,7 @@ function App() {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
DropdownTrigger,
|
DropdownTrigger,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
|
Badge,
|
||||||
} from "@nextui-org/react";
|
} from "@nextui-org/react";
|
||||||
|
|
||||||
import { EllipsisVerticalIcon } from "lucide-react";
|
import { EllipsisVerticalIcon } from "lucide-react";
|
||||||
@ -22,32 +23,39 @@ import { DateTime } from "luxon";
|
|||||||
|
|
||||||
import { NotebookPenIcon } from "lucide-react";
|
import { NotebookPenIcon } from "lucide-react";
|
||||||
import Runbook from "@/state/runbooks/runbook";
|
import Runbook from "@/state/runbooks/runbook";
|
||||||
import { useStore } from "@/state/store";
|
import { AtuinState, useStore } from "@/state/store";
|
||||||
|
|
||||||
const NoteSidebar = () => {
|
const NoteSidebar = () => {
|
||||||
const runbooks = useStore((state) => state.runbooks);
|
const runbooks = useStore((state: AtuinState) => state.runbooks);
|
||||||
const refreshRunbooks = useStore((state) => state.refreshRunbooks);
|
const refreshRunbooks = useStore(
|
||||||
|
(state: AtuinState) => state.refreshRunbooks,
|
||||||
|
);
|
||||||
|
|
||||||
const currentRunbook = useStore((state) => state.currentRunbook);
|
const currentRunbook = useStore((state: AtuinState) => state.currentRunbook);
|
||||||
const setCurrentRunbook = useStore((state) => state.setCurrentRunbook);
|
const setCurrentRunbook = useStore(
|
||||||
|
(state: AtuinState) => state.setCurrentRunbook,
|
||||||
|
);
|
||||||
|
const runbookInfo = useStore((state: AtuinState) => state.runbookInfo);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshRunbooks();
|
refreshRunbooks();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-w-48 h-screen flex flex-col border-r-1">
|
<div className="w-48 flex flex-col border-r-1">
|
||||||
<div className="overflow-y-auto flex-grow">
|
<div className="overflow-y-auto flex-grow">
|
||||||
<Listbox
|
<Listbox
|
||||||
hideSelectedIcon
|
hideSelectedIcon
|
||||||
items={runbooks}
|
items={runbooks.map((runbook) => {
|
||||||
|
return [runbook, runbookInfo[runbook.id]];
|
||||||
|
})}
|
||||||
variant="flat"
|
variant="flat"
|
||||||
aria-label="Runbook list"
|
aria-label="Runbook list"
|
||||||
selectionMode="single"
|
selectionMode="single"
|
||||||
selectedKeys={[currentRunbook]}
|
selectedKeys={[currentRunbook]}
|
||||||
itemClasses={{ base: "data-[selected=true]:bg-gray-200" }}
|
itemClasses={{ base: "data-[selected=true]:bg-gray-200" }}
|
||||||
topContent={
|
topContent={
|
||||||
<ButtonGroup>
|
<ButtonGroup className="z-20">
|
||||||
<Tooltip showArrow content="New Runbook" closeDelay={50}>
|
<Tooltip showArrow content="New Runbook" closeDelay={50}>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
@ -66,7 +74,7 @@ const NoteSidebar = () => {
|
|||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(runbook) => (
|
{([runbook, info]) => (
|
||||||
<ListboxItem
|
<ListboxItem
|
||||||
key={runbook.id}
|
key={runbook.id}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@ -75,6 +83,17 @@ const NoteSidebar = () => {
|
|||||||
textValue={runbook.name || "Untitled"}
|
textValue={runbook.name || "Untitled"}
|
||||||
endContent={
|
endContent={
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
|
<Badge
|
||||||
|
content={info?.ptys}
|
||||||
|
color="primary"
|
||||||
|
style={
|
||||||
|
info && info?.ptys > 0
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
display: "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
<DropdownTrigger className="bg-transparent">
|
<DropdownTrigger className="bg-transparent">
|
||||||
<Button isIconOnly>
|
<Button isIconOnly>
|
||||||
<EllipsisVerticalIcon
|
<EllipsisVerticalIcon
|
||||||
@ -83,6 +102,7 @@ const NoteSidebar = () => {
|
|||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
|
</Badge>
|
||||||
<DropdownMenu aria-label="Dynamic Actions">
|
<DropdownMenu aria-label="Dynamic Actions">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key={"delete"}
|
key={"delete"}
|
||||||
|
@ -4,6 +4,8 @@ import "@blocknote/core/fonts/inter.css";
|
|||||||
import "@blocknote/mantine/style.css";
|
import "@blocknote/mantine/style.css";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
|
import { Spinner } from "@nextui-org/react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BlockNoteSchema,
|
BlockNoteSchema,
|
||||||
BlockNoteEditor,
|
BlockNoteEditor,
|
||||||
@ -102,7 +104,7 @@ export default function Editor() {
|
|||||||
|
|
||||||
const debouncedOnChange = useDebounceCallback(onChange, 1000);
|
const debouncedOnChange = useDebounceCallback(onChange, 1000);
|
||||||
|
|
||||||
const fetchName = (): String => {
|
const fetchName = (): string => {
|
||||||
// Infer the title from the first text block
|
// Infer the title from the first text block
|
||||||
|
|
||||||
let blocks = editor.document;
|
let blocks = editor.document;
|
||||||
@ -119,7 +121,11 @@ export default function Editor() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (editor === undefined) {
|
if (editor === undefined) {
|
||||||
return "Loading content...";
|
return (
|
||||||
|
<div className="flex w-full h-full flex-col justify-center items-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders the editor instance.
|
// Renders the editor instance.
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
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";
|
||||||
|
import { keymap } from "@codemirror/view";
|
||||||
import { langs } from "@uiw/codemirror-extensions-langs";
|
import { langs } from "@uiw/codemirror-extensions-langs";
|
||||||
|
|
||||||
import { Play, Square } from "lucide-react";
|
import { Play, Square } from "lucide-react";
|
||||||
@ -12,58 +14,81 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import Terminal from "./terminal.tsx";
|
import Terminal from "./terminal.tsx";
|
||||||
|
|
||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
|
import { AtuinState, useStore } from "@/state/store.ts";
|
||||||
|
|
||||||
interface RunBlockProps {
|
interface RunBlockProps {
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void;
|
||||||
onPlay?: () => void;
|
onRun?: (pty: string) => void;
|
||||||
onStop?: () => void;
|
onStop?: (pty: string) => void;
|
||||||
id: string;
|
id: string;
|
||||||
code: string;
|
code: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
pty: string;
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RunBlock = ({
|
const RunBlock = ({
|
||||||
onChange,
|
onChange,
|
||||||
onPlay,
|
|
||||||
id,
|
id,
|
||||||
code,
|
code,
|
||||||
isEditable,
|
isEditable,
|
||||||
|
onRun,
|
||||||
|
onStop,
|
||||||
|
pty,
|
||||||
}: RunBlockProps) => {
|
}: RunBlockProps) => {
|
||||||
console.log(code);
|
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
|
||||||
const [showTerminal, setShowTerminal] = useState(false);
|
|
||||||
const [value, setValue] = useState<String>(code);
|
const [value, setValue] = useState<String>(code);
|
||||||
|
const cleanupPtyTerm = useStore((store: AtuinState) => store.cleanupPtyTerm);
|
||||||
|
const terminals = useStore((store: AtuinState) => store.terminals);
|
||||||
|
|
||||||
const [pty, setPty] = useState<string | null>(null);
|
const [currentRunbook, incRunbookPty, decRunbookPty] = useStore(
|
||||||
|
(store: AtuinState) => [
|
||||||
|
store.currentRunbook,
|
||||||
|
store.incRunbookPty,
|
||||||
|
store.decRunbookPty,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
const handleToggle = async (event: any) => {
|
const isRunning = pty !== null;
|
||||||
event.stopPropagation();
|
|
||||||
|
const handleToggle = async (event: any | null) => {
|
||||||
|
if (event) event.stopPropagation();
|
||||||
|
|
||||||
// If there's no code, don't do anything
|
// If there's no code, don't do anything
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
|
|
||||||
setIsRunning(!isRunning);
|
|
||||||
setShowTerminal(!isRunning);
|
|
||||||
|
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
// send sigkill
|
|
||||||
console.log("sending sigkill");
|
|
||||||
await invoke("pty_kill", { pid: pty });
|
await invoke("pty_kill", { pid: pty });
|
||||||
|
|
||||||
|
terminals[pty].terminal.dispose();
|
||||||
|
cleanupPtyTerm(pty);
|
||||||
|
|
||||||
|
if (onStop) onStop(pty);
|
||||||
|
decRunbookPty(currentRunbook);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isRunning) {
|
if (!isRunning) {
|
||||||
if (onPlay) onPlay();
|
|
||||||
|
|
||||||
let pty = await invoke<string>("pty_open");
|
let pty = await invoke<string>("pty_open");
|
||||||
setPty(pty);
|
if (onRun) onRun(pty);
|
||||||
console.log(pty);
|
|
||||||
|
incRunbookPty(currentRunbook);
|
||||||
|
|
||||||
let val = !value.endsWith("\n") ? value + "\r\n" : value;
|
let val = !value.endsWith("\n") ? value + "\r\n" : value;
|
||||||
await invoke("pty_write", { pid: pty, data: val });
|
await invoke("pty_write", { pid: pty, data: val });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCmdEnter = (view) => {
|
||||||
|
handleToggle(null);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const customKeymap = keymap.of([
|
||||||
|
{
|
||||||
|
key: "Mod-Enter",
|
||||||
|
run: handleCmdEnter,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full !max-w-full !outline-none overflow-none">
|
<div className="w-full !max-w-full !outline-none overflow-none">
|
||||||
<div className="flex flex-row items-start">
|
<div className="flex flex-row items-start">
|
||||||
@ -96,12 +121,12 @@ const RunBlock = ({
|
|||||||
setValue(val);
|
setValue(val);
|
||||||
onChange(val);
|
onChange(val);
|
||||||
}}
|
}}
|
||||||
extensions={[...extensions(), langs.shell()]}
|
extensions={[customKeymap, ...extensions(), langs.shell()]}
|
||||||
basicSetup={false}
|
basicSetup={false}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`overflow-hidden transition-all duration-300 ease-in-out min-w-0 ${
|
className={`overflow-hidden transition-all duration-300 ease-in-out min-w-0 ${
|
||||||
showTerminal ? "block" : "hidden"
|
isRunning ? "block" : "hidden"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{pty && <Terminal pty={pty} />}
|
{pty && <Terminal pty={pty} />}
|
||||||
@ -120,6 +145,7 @@ export default createReactBlockSpec(
|
|||||||
default: "bash",
|
default: "bash",
|
||||||
},
|
},
|
||||||
code: { default: "" },
|
code: { default: "" },
|
||||||
|
pty: { default: null },
|
||||||
},
|
},
|
||||||
content: "none",
|
content: "none",
|
||||||
},
|
},
|
||||||
@ -130,7 +156,18 @@ export default createReactBlockSpec(
|
|||||||
editor.updateBlock(block, {
|
editor.updateBlock(block, {
|
||||||
props: { ...block.props, code: val },
|
props: { ...block.props, code: val },
|
||||||
});
|
});
|
||||||
console.log(block.props);
|
};
|
||||||
|
|
||||||
|
const onRun = (pty: string) => {
|
||||||
|
editor.updateBlock(block, {
|
||||||
|
props: { ...block.props, pty: pty },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStop = (pty: string) => {
|
||||||
|
editor.updateBlock(block, {
|
||||||
|
props: { ...block.props, pty: null },
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -139,7 +176,10 @@ export default createReactBlockSpec(
|
|||||||
id={block?.id}
|
id={block?.id}
|
||||||
code={block.props.code}
|
code={block.props.code}
|
||||||
type={block.props.type}
|
type={block.props.type}
|
||||||
|
pty={block.props.pty}
|
||||||
isEditable={editor.isEditable}
|
isEditable={editor.isEditable}
|
||||||
|
onRun={onRun}
|
||||||
|
onStop={onStop}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -1,79 +1,98 @@
|
|||||||
/*
|
import { useState, useEffect, useRef } from "react";
|
||||||
export const openTerm = (pty: string, id: string) => {
|
|
||||||
const term = new Terminal({
|
|
||||||
fontSize: 12,
|
|
||||||
fontFamily: "Courier New",
|
|
||||||
});
|
|
||||||
|
|
||||||
let element = document.getElementById(id);
|
|
||||||
term.open(element);
|
|
||||||
|
|
||||||
//term.onResize(onResize(pty));
|
|
||||||
|
|
||||||
//const fitAddon = new FitAddon();
|
|
||||||
//term.loadAddon(fitAddon);
|
|
||||||
//term.loadAddon(new WebglAddon());
|
|
||||||
|
|
||||||
/*
|
|
||||||
const onSize = (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
fitAddon.fit();
|
|
||||||
};
|
|
||||||
fitAddon.fit();
|
|
||||||
|
|
||||||
window.addEventListener("resize", onSize, false);
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { Terminal } from "@xterm/xterm";
|
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { FitAddon } from "@xterm/addon-fit";
|
|
||||||
import { WebglAddon } from "@xterm/addon-webgl";
|
|
||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { useStore } from "@/state/store";
|
||||||
|
|
||||||
const onResize = (pty: string) => async (size: any) => {
|
const usePersistentTerminal = (pty: string) => {
|
||||||
await invoke("pty_resize", {
|
const newPtyTerm = useStore((store) => store.newPtyTerm);
|
||||||
pid: pty,
|
const terminals = useStore((store) => store.terminals);
|
||||||
cols: size.cols,
|
const [isReady, setIsReady] = useState(false);
|
||||||
rows: size.rows,
|
|
||||||
});
|
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 TerminalComponent = ({ pty }: any) => {
|
||||||
const terminalRef = useRef(null);
|
const terminalRef = useRef(null);
|
||||||
|
const { terminalData, isReady } = usePersistentTerminal(pty);
|
||||||
|
const [isAttached, setIsAttached] = useState(false);
|
||||||
|
const cleanupListenerRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// no pty? no terminal
|
||||||
if (pty == null) return;
|
if (pty == null) return;
|
||||||
if (!terminalRef.current) return;
|
|
||||||
|
|
||||||
const terminal = new Terminal();
|
// the terminal may still be being created so hold off
|
||||||
const fitAddon = new FitAddon();
|
if (!isReady) return;
|
||||||
|
|
||||||
terminal.open(terminalRef.current);
|
|
||||||
terminal.loadAddon(new WebglAddon());
|
|
||||||
terminal.loadAddon(fitAddon);
|
|
||||||
terminal.onResize(onResize(pty));
|
|
||||||
|
|
||||||
fitAddon.fit();
|
|
||||||
const windowResize = () => {
|
const windowResize = () => {
|
||||||
fitAddon.fit();
|
if (!terminalData || !terminalData.fitAddon) return;
|
||||||
|
|
||||||
|
terminalData.fitAddon.fit();
|
||||||
};
|
};
|
||||||
|
|
||||||
listen(`pty-${pty}`, (event: any) => {
|
// terminal object needs attaching to a ref to a div
|
||||||
terminal.write(event.payload);
|
if (!isAttached && terminalData && terminalData.terminal) {
|
||||||
}).then(() => {
|
// If it's never been attached, attach it
|
||||||
console.log("Listening for pty events");
|
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);
|
window.addEventListener("resize", windowResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(`pty-${pty}`, (event: any) => {
|
||||||
|
terminalData.terminal.write(event.payload);
|
||||||
|
}).then((ul) => {
|
||||||
|
cleanupListenerRef.current = ul;
|
||||||
|
});
|
||||||
|
|
||||||
// Customize further as needed
|
// Customize further as needed
|
||||||
return () => {
|
return () => {
|
||||||
terminal.dispose();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
window.removeEventListener("resize", windowResize);
|
window.removeEventListener("resize", windowResize);
|
||||||
};
|
};
|
||||||
}, [pty]);
|
}, [terminalData, isReady]);
|
||||||
|
|
||||||
|
if (!isReady) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="!max-w-full min-w-0 overflow-hidden" ref={terminalRef} />
|
<div className="!max-w-full min-w-0 overflow-hidden" ref={terminalRef} />
|
||||||
|
@ -8,8 +8,14 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<NextUIProvider>
|
<NextUIProvider>
|
||||||
<main className="text-foreground bg-background">
|
<main className="text-foreground bg-background">
|
||||||
<div data-tauri-drag-region className="w-full min-h-8 absolute z-10" />
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="w-full min-h-8 z-10 border-b-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="z-20 ">
|
||||||
<App />
|
<App />
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</NextUIProvider>
|
</NextUIProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
|
@ -19,11 +19,24 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import { sessionToken, settings } from "./client";
|
import { sessionToken, settings } from "./client";
|
||||||
import { getWeekInfo } from "@/lib/utils";
|
import { getWeekInfo } from "@/lib/utils";
|
||||||
import Runbook from "./runbooks/runbook";
|
import Runbook from "./runbooks/runbook";
|
||||||
|
import { Terminal } from "@xterm/xterm";
|
||||||
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
|
import { WebglAddon } from "@xterm/addon-webgl";
|
||||||
|
|
||||||
|
export class TerminalData {
|
||||||
|
terminal: Terminal;
|
||||||
|
fitAddon: FitAddon;
|
||||||
|
|
||||||
|
constructor(terminal: Terminal, fit: FitAddon) {
|
||||||
|
this.terminal = terminal;
|
||||||
|
this.fitAddon = fit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// I'll probs want to slice this up at some point, but for now a
|
// I'll probs want to slice this up at some point, but for now a
|
||||||
// big blobby lump of state is fine.
|
// big blobby lump of state is fine.
|
||||||
// Totally just hoping that structure will be emergent in the future.
|
// Totally just hoping that structure will be emergent in the future.
|
||||||
interface AtuinState {
|
export interface AtuinState {
|
||||||
user: User;
|
user: User;
|
||||||
homeInfo: HomeInfo;
|
homeInfo: HomeInfo;
|
||||||
aliases: Alias[];
|
aliases: Alias[];
|
||||||
@ -32,7 +45,7 @@ interface AtuinState {
|
|||||||
calendar: any[];
|
calendar: any[];
|
||||||
weekStart: number;
|
weekStart: number;
|
||||||
runbooks: Runbook[];
|
runbooks: Runbook[];
|
||||||
currentRunbook: String | null;
|
currentRunbook: string | null;
|
||||||
|
|
||||||
refreshHomeInfo: () => void;
|
refreshHomeInfo: () => void;
|
||||||
refreshCalendar: () => void;
|
refreshCalendar: () => void;
|
||||||
@ -44,6 +57,16 @@ interface AtuinState {
|
|||||||
historyNextPage: (query?: string) => void;
|
historyNextPage: (query?: string) => void;
|
||||||
|
|
||||||
setCurrentRunbook: (id: String) => void;
|
setCurrentRunbook: (id: String) => void;
|
||||||
|
setPtyTerm: (pty: string, terminal: any) => void;
|
||||||
|
newPtyTerm: (pty: string, runbook: string) => TerminalData;
|
||||||
|
cleanupPtyTerm: (pty: string) => void;
|
||||||
|
|
||||||
|
terminals: { [pty: string]: TerminalData };
|
||||||
|
|
||||||
|
// Store ephemeral state for runbooks, that is not persisted to the database
|
||||||
|
runbookInfo: { [runbook: string]: { ptys: number } };
|
||||||
|
incRunbookPty: (runbook: string) => void;
|
||||||
|
decRunbookPty: (runbook: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = (set: any, get: any): AtuinState => ({
|
let state = (set: any, get: any): AtuinState => ({
|
||||||
@ -55,6 +78,8 @@ let state = (set: any, get: any): AtuinState => ({
|
|||||||
calendar: [],
|
calendar: [],
|
||||||
runbooks: [],
|
runbooks: [],
|
||||||
currentRunbook: "",
|
currentRunbook: "",
|
||||||
|
terminals: {},
|
||||||
|
runbookInfo: {},
|
||||||
|
|
||||||
weekStart: getWeekInfo().firstDay,
|
weekStart: getWeekInfo().firstDay,
|
||||||
|
|
||||||
@ -158,8 +183,101 @@ let state = (set: any, get: any): AtuinState => ({
|
|||||||
setCurrentRunbook: (id: String) => {
|
setCurrentRunbook: (id: String) => {
|
||||||
set({ currentRunbook: id });
|
set({ currentRunbook: id });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setPtyTerm: (pty: string, terminal: TerminalData) => {
|
||||||
|
set({
|
||||||
|
terminals: { ...get().terminals, [pty]: terminal },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanupPtyTerm: (pty: string) => {
|
||||||
|
set((state: AtuinState) => {
|
||||||
|
const terminals = Object.keys(state.terminals).reduce(
|
||||||
|
(terms: { [pty: string]: TerminalData }, key) => {
|
||||||
|
if (key !== pty) {
|
||||||
|
terms[key] = state.terminals[key];
|
||||||
|
}
|
||||||
|
return terms;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
return { terminals };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
newPtyTerm: (pty: string) => {
|
||||||
|
let terminal = new Terminal();
|
||||||
|
|
||||||
|
// TODO: fallback to canvas, also some sort of setting to allow disabling webgl usage
|
||||||
|
// probs fine for now though, it's widely supported. maybe issues on linux.
|
||||||
|
terminal.loadAddon(new WebglAddon());
|
||||||
|
|
||||||
|
let fitAddon = new FitAddon();
|
||||||
|
terminal.loadAddon(fitAddon);
|
||||||
|
|
||||||
|
const onResize = (size: { cols: number; rows: number }) => {
|
||||||
|
invoke("pty_resize", {
|
||||||
|
pid: pty,
|
||||||
|
cols: size.cols,
|
||||||
|
rows: size.rows,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
terminal.onResize(onResize);
|
||||||
|
|
||||||
|
let td = new TerminalData(terminal, fitAddon);
|
||||||
|
|
||||||
|
set({
|
||||||
|
terminals: { ...get().terminals, [pty]: td },
|
||||||
|
});
|
||||||
|
|
||||||
|
return td;
|
||||||
|
},
|
||||||
|
|
||||||
|
incRunbookPty: (runbook: string) => {
|
||||||
|
set((state: AtuinState) => {
|
||||||
|
let oldVal = state.runbookInfo[runbook] || { ptys: 0 };
|
||||||
|
let newVal = { ptys: oldVal.ptys + 1 };
|
||||||
|
console.log(newVal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
runbookInfo: {
|
||||||
|
...state.runbookInfo,
|
||||||
|
[runbook]: newVal,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
decRunbookPty: (runbook: string) => {
|
||||||
|
set((state: AtuinState) => {
|
||||||
|
let newVal = state.runbookInfo[runbook];
|
||||||
|
if (!newVal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newVal.ptys--;
|
||||||
|
|
||||||
|
return {
|
||||||
|
runbookInfo: {
|
||||||
|
...state.runbookInfo,
|
||||||
|
[runbook]: newVal,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useStore = create<AtuinState>()(
|
export const useStore = create<AtuinState>()(
|
||||||
persist(state, { name: "atuin-storage" }),
|
persist(state, {
|
||||||
|
name: "atuin-storage",
|
||||||
|
|
||||||
|
// don't serialize the terminals map
|
||||||
|
// it won't work as JSON. too cyclical
|
||||||
|
partialize: (state) =>
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(state).filter(([key]) => !["terminals"].includes(key)),
|
||||||
|
),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user