aboutsummaryrefslogtreecommitdiffstats
path: root/ui/src
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@elliehuxtable.com>2024-04-11 16:59:01 +0100
committerGitHub <noreply@github.com>2024-04-11 16:59:01 +0100
commit6cd4319fcf540ef70f74cc2f10d0d4297ee7b788 (patch)
tree3d24dbf70493c377e162d9941faac65c829623f9 /ui/src
parentfeat(bash/blesh): use _ble_exec_time_ata for duration even in bash < 5 (#1940) (diff)
downloadatuin-6cd4319fcf540ef70f74cc2f10d0d4297ee7b788.zip
feat(gui): add base structure (#1935)
* initial * ui things * cargo * update, add history refresh button * history page a bit better, add initial dotfiles page * re-org layout * bye squigglies * add dotfiles ui, show aliases * add default shell detection * put stats in a little drawer, alias import changes * use new table for aliases, add alias deleting * support adding aliases * close drawer when added, no alias autocomplete * clippy, format * attempt to ensure gdk is installed ok * sudo * no linux things on mac ffs * I forgot we build for windows too... end of day * remove tauri backend from workspace
Diffstat (limited to '')
-rw-r--r--ui/src/App.css7
-rw-r--r--ui/src/App.tsx116
-rw-r--r--ui/src/assets/logo-light.svg1
-rw-r--r--ui/src/assets/react.svg1
-rw-r--r--ui/src/components/Drawer.tsx26
-rw-r--r--ui/src/components/HistoryList.tsx75
-rw-r--r--ui/src/components/HistorySearch.tsx56
-rw-r--r--ui/src/components/dotfiles/Aliases.tsx191
-rw-r--r--ui/src/components/history/Stats.tsx143
-rw-r--r--ui/src/components/ui/button.tsx56
-rw-r--r--ui/src/components/ui/data-table.tsx80
-rw-r--r--ui/src/components/ui/dropdown-menu.tsx198
-rw-r--r--ui/src/components/ui/table.tsx117
-rw-r--r--ui/src/lib/utils.ts6
-rw-r--r--ui/src/main.tsx10
-rw-r--r--ui/src/pages/Dotfiles.tsx32
-rw-r--r--ui/src/pages/History.tsx108
-rw-r--r--ui/src/styles.css76
-rw-r--r--ui/src/vite-env.d.ts1
19 files changed, 1300 insertions, 0 deletions
diff --git a/ui/src/App.css b/ui/src/App.css
new file mode 100644
index 00000000..a89ebd15
--- /dev/null
+++ b/ui/src/App.css
@@ -0,0 +1,7 @@
+.logo.vite:hover {
+ filter: drop-shadow(0 0 2em #747bff);
+}
+
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafb);
+}
diff --git a/ui/src/App.tsx b/ui/src/App.tsx
new file mode 100644
index 00000000..5d1cd863
--- /dev/null
+++ b/ui/src/App.tsx
@@ -0,0 +1,116 @@
+import "./App.css";
+
+import { Fragment, useState, useEffect, ReactElement } from "react";
+import { Dialog, Transition } from "@headlessui/react";
+import {
+ Bars3Icon,
+ ChartPieIcon,
+ Cog6ToothIcon,
+ HomeIcon,
+ XMarkIcon,
+ MagnifyingGlassIcon,
+ ClockIcon,
+ WrenchScrewdriverIcon,
+} from "@heroicons/react/24/outline";
+import Logo from "./assets/logo-light.svg";
+
+function classNames(...classes: any) {
+ return classes.filter(Boolean).join(" ");
+}
+
+import History from "./pages/History.tsx";
+import Dotfiles from "./pages/Dotfiles.tsx";
+
+enum Section {
+ History,
+ Dotfiles,
+}
+
+function renderMain(section: Section): ReactElement {
+ switch (section) {
+ case Section.History:
+ return <History />;
+ case Section.Dotfiles:
+ return <Dotfiles />;
+ }
+}
+
+function App() {
+ // routers don't really work in Tauri. It's not a browser!
+ // I think hashrouter may work, but I'd rather avoiding thinking of them as
+ // pages
+ const [section, setSection] = useState(Section.History);
+
+ const navigation = [
+ {
+ name: "History",
+ icon: ClockIcon,
+ section: Section.History,
+ },
+ {
+ name: "Dotfiles",
+ icon: WrenchScrewdriverIcon,
+ section: Section.Dotfiles,
+ },
+ ];
+
+ return (
+ <div>
+ <div className="fixed inset-y-0 z-50 flex w-60 flex-col">
+ <div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 pb-4">
+ <div className="flex h-16 shrink-0 items-center">
+ <img className="h-8 w-auto" src={Logo} alt="Atuin" />
+ </div>
+ <nav className="flex flex-1 flex-col">
+ <ul role="list" className="flex flex-1 flex-col gap-y-7">
+ <li>
+ <ul role="list" className="-mx-2 space-y-1 w-full">
+ {navigation.map((item) => (
+ <li key={item.name}>
+ <button
+ onClick={() => setSection(item.section)}
+ className={classNames(
+ section == item.section
+ ? "bg-gray-50 text-green-600"
+ : "text-gray-700 hover:text-green-600 hover:bg-gray-50",
+ "group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold w-full",
+ )}
+ >
+ <item.icon
+ className={classNames(
+ section == item.section
+ ? "text-green-600"
+ : "text-gray-400 group-hover:text-green-600",
+ "h-6 w-6 shrink-0",
+ )}
+ aria-hidden="true"
+ />
+ {item.name}
+ </button>
+ </li>
+ ))}
+ </ul>
+ </li>
+ <li className="mt-auto">
+ <a
+ href="#"
+ className="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-green-600"
+ >
+ <Cog6ToothIcon
+ className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-green-600"
+ aria-hidden="true"
+ />
+ Settings
+ </a>
+ </li>
+ </ul>
+ </nav>
+ </div>
+ </div>
+
+ {renderMain(section)}
+ </div>
+ );
+}
+
+export default App;
diff --git a/ui/src/assets/logo-light.svg b/ui/src/assets/logo-light.svg
new file mode 100644
index 00000000..697df883
--- /dev/null
+++ b/ui/src/assets/logo-light.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" width="1161.98" height="309.08" viewBox="0 0 1161.98 309.08"><g id="Layer_1-2"><path d="m99.75,291.76c0-5.07,4.12-9.19,9.19-9.19,2.46,0,4.77.96,6.5,2.7,1.4,1.38,2.29,3.16,2.59,5.13.07.41.11.87.11,1.36v10.52s-1.68.27-1.68.27c-2.22.36-4.56.63-6.95.83-2.36.22-4.89.36-7.71.43l-2.05.05v-12.1Zm-2.84,12.05c-.1,0-.19,0-.28,0-2.45-.06-4.4-.15-6.2-.3h-.13c-2.76-.21-5.49-.52-8.1-.94l-1.68-.27v-10.52c0-5.07,4.12-9.19,9.19-9.19,2.46,0,4.77.96,6.5,2.7,1.74,1.72,2.7,4.03,2.7,6.49v12.05h-2Zm22.07-15.26c0-.43.03-.91.1-1.38.68-4.47,4.59-7.82,9.1-7.82,2.44,0,4.74.96,6.49,2.69,1.75,1.76,2.71,4.07,2.71,6.51v5.57s-.54.58-.54.58c-1.71,1.81-4.39,3.42-7.97,4.79-2.19.83-4.7,1.56-7.45,2.17l-2.43.54v-13.65Zm146.15-19.7c0-4.73,3.84-8.58,8.57-8.58,2.29,0,4.44.89,6.06,2.52,1.3,1.3,2.14,2.96,2.41,4.79.06.33.1.77.1,1.28v9.76s-1.68.27-1.68.27c-2.09.33-4.23.59-6.38.76-2.15.2-4.54.33-7.04.39l-2.04.04v-11.22Zm-2.43,11.18c-.08,0-.17,0-.25,0-2.07-.04-3.9-.13-5.68-.27h-.13c-2.55-.2-5.05-.49-7.41-.86l-1.69-.27v-9.37s6.25-1.01,6.25-1.01c3.03-.49,5.97-1.43,8.74-2.8l1,2.01,1-.21c.11.52.16,1.06.16,1.61v11.18h-2Zm20.01-14.12c0-.37.03-.76.08-1.24.65-4.21,4.29-7.33,8.49-7.33,2.28,0,4.43.89,6.06,2.5,1.63,1.63,2.53,3.79,2.53,6.07v5.17s-.55.58-.55.58c-1.6,1.68-4.07,3.17-7.35,4.42-1.95.75-4.25,1.41-6.83,1.99l-2.43.54v-12.69Zm-240.96-5.95c-.09,0-.18,0-.26,0-2.12-.05-3.94-.15-5.66-.28h-.13c-2.46-.18-4.95-.47-7.41-.86l-1.69-.27v-9.77c0-4.31,3.22-7.96,7.49-8.5l1.41-.18,4.2,8.42c1.01,2.01,2.23,3.91,3.61,5.63l.44.55v5.27h-2Z" fill="#fff"/><path d="m14.35,190.88l25.24,50.06c-1.34-.76-2.85-1.16-4.42-1.16-4.96,0-9,4.04-9,9v7.45l-2.45-.57c-1.67-.39-3.27-.84-4.76-1.33-3.39-1.17-9.03-3.68-9.03-7.47v-39.14c.16-6.06,1.7-11.84,4.42-16.84Zm275.98,14.02c-.1-.12-.2-.23-.31-.34l-17.42,42.87c-2.38,5.85-6.64,10.61-11.98,13.64.68.38,1.32.87,1.89,1.44.29.29.55.59.81.93l1.59,2.14,1.61-2.13c1.72-2.28,4.34-3.59,7.18-3.59,2.38,0,4.63.93,6.33,2.61l1.96,1.94,1.23-2.47c1.53-3.05,4.7-5.02,8.06-5.02,1.64,0,3.25.45,4.65,1.28l3.03,1.82v-32.27c-.24-8.66-3.3-16.77-8.63-22.85Zm-163.34,17.02c-6.75-7.69-16.31-11.75-27.65-11.75s-20.91,4.06-27.66,11.75c-5.81,6.62-9.14,15.45-9.39,24.92v42.82c0,4.03,6.09,6.73,9.75,7.99,1.62.53,3.37,1.02,5.19,1.45l2.46.58v-7.92c0-5.53,4.5-10.03,10.03-10.03,2.67,0,5.19,1.04,7.11,2.95.32.32.62.66.91,1.04l1.59,2.14,1.61-2.13c1.92-2.55,4.84-4.01,8.01-4.01,2.66,0,5.16,1.03,7.05,2.91l1.96,1.94,1.24-2.47c1.7-3.4,5.23-5.6,8.99-5.6,1.83,0,3.62.49,5.18,1.43l3.03,1.81v-34.97c-.25-9.42-3.59-18.26-9.39-24.87ZM332.31,60.61c-12.06-21.14-27.47-44.23-52.73-52.68-20.59-7.09-44.02-1.54-59.24,13.76-10.03,9.84-15.89,22.44-19.46,35.27-6.24,23.59-7.07,46.88-7.01,70.75.07,23.56,3.41,44.38,5.07,56.48,1.23,8.87,2.45,16.24,3.36,21.46-4.12,2.78-8.46,6.59-12.06,11.81-9.35,13.58-8.75,29.48-6.64,40.36,1.26,6.49,3.04,11.19,3.59,12.58,5.91,1.03,25.44,4.02,44.35-1.25,1.59,2.58,5.68,4.35,8.37,5.26,1.49.5,3.1.94,4.76,1.33l2.46.58v-7.45c0-1.4.32-2.72.9-3.91l-9.1,1.66c6.45-2.65,12.6-6.46,17.84-11.88,12.54-12.97,15.02-30.01,19.42-60.94,3.06-21.5,3.21-39.68,2.73-52.52,11.25,1.45,23.01-.57,33.51-6.77,6.36-3.67,11.69-8.43,15.91-13.91,12.92-16.83,15.23-40.44,3.98-59.97Z" fill="#26bd65"/><path d="m272.86,215.67c-.92,5.25-1.89,9.97-3.05,14.26l3.05-14.26Z" fill="#26bd65"/><path d="m112.87,102.44c-5.02-.03-9.86-1.12-14.38-3.23l-19.37-9.02c-1.93-.91-2.91-2.84-2.5-4.93.41-2.09,2.05-3.51,4.18-3.61l94.48-4.69c.5-.03,1.02-.04,1.54-.05h.22c1.94,0,3.92.15,5.88.45l10.68,1.68-.25,1.93c-.8,6.24-1.39,12.79-1.75,19.49l-.08,1.6-1.58.26c-1.82.3-3.71.46-5.6.46l-71.47-.35Z" fill="#b3e5fc"/><path d="m280.91,177.2c.86-10.19,1.2-20.5,1-30.64l-.04-2.1,2.1.07c.63.02,1.26.03,1.89.03.89,0,1.77-.02,2.66-.06l1.53-.07.46,1.46c2.85,9.01,2.29,18.71-1.56,27.32-1.06,2.36-2.77,4.38-4.94,5.83l-3.45,2.3.35-4.14Zm-137.27,20.12c2.2,0,4.4-.51,6.37-1.47l44.66-21.73-.19-1.43c-1.58-11.73-3.39-27.44-3.44-44.98,0-4.91,0-10.76.16-16.86v-.46s-.45-1.03-.45-1.03c-1.65-3.67-5.31-6.06-9.32-6.08l-68.83-.34c-4.28,0-8.15,2.7-9.63,6.71l-15.99,43.19c-2.47,6.69.22,14.12,6.38,17.67l42.99,24.85c2.22,1.28,4.74,1.96,7.3,1.96h0Zm-110.02-17.32c2.26,0,4.49-.38,6.62-1.12l32.98-11.48c5.71-1.99,10.19-6.38,12.28-12.04l16.11-43.53c1.88-5.07-.49-10.83-5.39-13.12l-22.31-10.4c-2.09-.98-4.33-1.47-6.64-1.47h-.3c-5.08.09-9.89,2.75-12.88,7.1l-38.98,57.01c-4.92,7.21-3.45,17.12,3.36,22.58l2.55,2.04c3.56,2.85,8.04,4.43,12.61,4.43h0Z" fill="#4fc3f7"/><path d="m162.77,274.65c-4.85,0-9.04-3.62-9.74-8.43l-7.79-53.52c-1.16-7.93,2.91-15.63,10.12-19.15l39.32-19.13,1.07,7.47.38,2.67c.87,6.3,1.84,12.55,2.86,18.57l.2,1.19-.96.73c-4.05,3.09-7.53,6.72-10.34,10.78-9.89,14.37-9.31,31.05-7.08,42.51.89,4.58,2.04,8.32,2.84,10.66l.77,2.24-20.06,3.27c-.57.09-1.06.13-1.53.13h-.06Zm-17.7-.33c-.45,0-.91-.04-1.36-.12l-1.65-.29v-27.24c-.51-20.29-14.11-42.18-42.73-42.18-20.07,0-42,13.21-42.73,42.23v8.28s-3.45-3.63-3.45-3.63c-2.33-2.45-4.07-5.46-5.05-8.7l-13.75-45.29c-2.45-8.07,1.87-16.57,9.84-19.34l31.41-10.93c1.66-.58,3.39-.87,5.16-.87,2.75,0,5.46.73,7.83,2.1l47.65,27.55c4.17,2.41,6.96,6.53,7.67,11.3l8.52,58.55c.32,2.2-.3,4.31-1.73,5.96-1.4,1.64-3.45,2.6-5.61,2.62h-.01ZM11.32,173.97l3.04-2.46,10.23,8.2c3.53,2.82,6.09,6.6,7.4,10.93l12.23,40.27-3.7,1.48-29.2-58.42Zm264.02,43.95c1.26-6.95,2.36-14.63,3.63-23.52l.03-.22c.14-.99.27-2.01.4-3.02v-.09s.12-.5.12-.5c.9-4.25,2.89-8.25,5.75-11.55l8.28-9.59,3.39,2.01-17.75,47.54-3.84-1.06Z" fill="#0288d1"/><path d="m252.88,68.7c-5.28,0-9.56,4.28-9.56,9.56s4.28,9.56,9.56,9.56,9.56-4.28,9.56-9.56-4.28-9.56-9.56-9.56Z" fill="#263238"/><circle cx="317.93" cy="78.26" r="9.56" fill="#263238"/><path d="m310.52,94.56c-1.41,1.55-2.91,2.72-4.44,3.48-1.11.55-2.21.86-3.28.99-.16.02-.32.04-.47.04h-.01c-2.84.2-5.46-.84-7.57-2.15-1.62-.8-3-1.71-4.07-2.5-.4-.29-.88-.49-1.4-.58-.5.12-.96.4-1.29.8-1.53,1.84-4.48,4.75-8.41,5.73-.71.19-1.44.31-2.21.35-.53.02-1.05.01-1.55-.04-4.13-.39-7.31-3.05-8.95-4.71-.94-.96-2.47-.98-3.43-.05-.95.94-.97,2.47-.04,3.43,2.22,2.26,6.68,5.91,12.67,6.2.25.01.5.02.76.02s.52,0,.78-.02c.74-.04,1.45-.12,2.14-.25,4.49-.83,8-3.46,10.31-5.76,4.11,3.18,8.49,4.67,12.69,4.36.66-.05,1.32-.14,1.98-.29,1.19-.25,2.35-.66,3.49-1.22,2.08-1.03,4.06-2.57,5.88-4.56.9-.99.83-2.52-.16-3.42-.99-.9-2.52-.83-3.42.15Z" fill="#263238"/><path d="m336.51,58.2c-13.31-23.33-29.23-46.12-55.36-54.86-22.35-7.7-47.57-1.84-64.2,14.87-9.54,9.36-16.33,21.61-20.75,37.49-1.17,4.42-2.14,8.84-2.97,13.23l-10.17-1.61c-1.8-.28-3.63-.42-5.44-.4-.46,0-.93.01-1.39.04l-103.33,5.13c-10.32.51-19.96,5.9-25.79,14.43L5.8,146.95c-6.81,9.95-7.69,22.82-2.3,33.61l4.02,8.04c-2.64,5.28-4.25,11.58-4.44,18.94v39.31c0,4.21,2.36,10.04,13.68,13.95,7.73,2.53,16.68,3.8,26.69,3.8,3,0,6.1-.11,9.28-.35.61.31,1.24.6,1.87.87v24.54c0,4.61,2.59,10.99,14.97,15.26,8.46,2.77,18.25,4.16,29.2,4.16,3.6,0,7.31-.14,11.16-.44.1-.01.22-.02.33-.04,1.07-.16,2.22-.31,3.42-.47,11.06-1.45,27.76-3.65,30.3-17.6.05-.29.08-.58.08-.87v-5.25l3.96.83c2.3.48,4.66.71,7.02.7,1.7-.02,3.41-.16,5.09-.43l53.15-8.66c4.63-.23,9.46-.77,14.32-1.78,2.08,2.1,5.27,4.13,10.11,5.8,7.74,2.53,16.68,3.81,26.7,3.81,3.28,0,6.68-.13,10.19-.41.1-.01.21-.02.3-.03.99-.15,2.04-.29,3.14-.43,10.1-1.33,25.37-3.33,27.69-16.09.05-.26.07-.53.07-.79v-39.31c-.27-10.59-3.49-18.97-8.44-25.29l10.22-27.36c3.82-10.22,2.37-21.67-3.86-30.62l-.36-.52c3.98-1.28,7.82-2.99,11.48-5.15,6.77-3.91,12.6-9.01,17.33-15.16,8.11-10.56,12.24-23.19,12.24-35.89,0-10.11-2.61-20.25-7.9-29.43Zm-147.48,69.5c.05,17.37,1.84,33.18,3.46,45.24l-43.36,21.1c-3.75,1.83-8.17,1.67-11.79-.42l-42.99-24.85c-5.31-3.06-7.63-9.51-5.51-15.25l15.99-43.19c1.2-3.26,4.32-5.42,7.8-5.4l68.77.34c3.25.02,6.18,1.94,7.51,4.9l.28.62c-.15,5.83-.17,11.48-.16,16.91Zm-110.44-42.06c.18-.92.88-1.93,2.31-2l94.48-4.69c.49-.03.99-.04,1.48-.05,1.92,0,3.86.14,5.76.43l8.75,1.38c-.86,6.67-1.42,13.23-1.76,19.64-1.8.3-3.63.44-5.45.43l-71.28-.35c-4.68-.03-9.3-1.06-13.54-3.04l-19.37-9.02c-1.29-.61-1.56-1.81-1.38-2.73Zm-61.84,66.43l38.98-57.01c2.56-3.74,6.73-6.15,11.26-6.23,2.11-.04,4.16.39,6.07,1.28l22.31,10.4c3.99,1.86,5.89,6.48,4.36,10.61l-16.11,43.53c-1.88,5.09-5.94,9.07-11.06,10.85l-32.98,11.48c-5.9,2.05-12.44.93-17.32-2.98l-2.55-2.04c-6-4.81-7.3-13.54-2.96-19.89Zm7.42,96.7v4.93c-1.58-.37-3.11-.8-4.59-1.28-4.73-1.63-7.65-3.76-7.65-5.57v-39.14c.07-2.67.44-5.38,1.13-8.03l19.23,38.48c-4.67,1.26-8.12,5.54-8.12,10.61Zm17.58,9.19c-.11,0-.21,0-.3,0-1.99-.05-3.8-.14-5.54-.28h-.07c-2.48-.18-4.92-.47-7.24-.84v-8.06c0-3.34,2.5-6.11,5.74-6.52l3.57,7.15c1.08,2.15,2.37,4.16,3.84,5.99v2.57Zm-28.64-84.89l10.23,8.2c3.2,2.56,5.55,6.03,6.74,9.95l12.23,40.27-29.2-58.42Zm83.8,128.74c-.12,0-.23-.01-.33-.01-2.17-.05-4.16-.14-6.06-.3h-.07c-2.72-.2-5.38-.51-7.93-.92v-8.82c0-3.96,3.22-7.19,7.19-7.19,1.92,0,3.73.75,5.09,2.11,1.36,1.35,2.11,3.16,2.11,5.08v10.05Zm19.23-1.23c-2.19.35-4.46.62-6.8.81-2.46.23-5,.36-7.59.42v-10.05c0-3.96,3.22-7.19,7.19-7.19,1.92,0,3.73.75,5.08,2.11,1.11,1.1,1.8,2.48,2.03,4.02.06.34.09.69.09,1.06v8.82Zm1.26-17.37c-2.26-2.24-5.27-3.48-8.46-3.48-3.92,0-7.41,1.89-9.6,4.8-.34-.46-.71-.88-1.12-1.28-2.27-2.26-5.29-3.52-8.51-3.52-6.64,0-12.03,5.4-12.03,12.03v5.39c-1.73-.4-3.41-.87-5.02-1.4-5.17-1.78-8.38-4.11-8.38-6.09v-42.82c.45-16.71,11.6-34.68,35.06-34.68s34.6,17.96,35.04,34.68v31.39c-1.86-1.12-3.99-1.72-6.2-1.72-1.75,0-3.43.38-4.95,1.07-2.51,1.15-4.59,3.14-5.83,5.63Zm17.98,10.12c-1.49,1.58-3.97,3.05-7.23,4.3-2.09.79-4.5,1.49-7.17,2.08v-11.16c0-.36.03-.74.08-1.1.53-3.47,3.59-6.1,7.12-6.1,1.91,0,3.72.76,5.08,2.11,1.36,1.37,2.12,3.18,2.12,5.09v4.78Zm13.8-22.95c-1.04,1.22-2.54,1.92-4.11,1.93-.33,0-.67-.03-1.01-.09v-25.56c-.77-30.35-23.73-44.18-44.73-44.18s-43.97,13.83-44.73,44.18v3.32c-2.09-2.2-3.68-4.89-4.59-7.9l-13.75-45.29c-2.13-7.01,1.66-14.46,8.58-16.87l31.41-10.93c3.75-1.31,7.89-.92,11.33,1.07l47.65,27.55c3.62,2.09,6.08,5.72,6.69,9.86l8.52,58.55c.23,1.6-.21,3.15-1.26,4.36Zm29.67-11.66c.9,4.63,2.05,8.43,2.91,10.93l-17.72,2.89c-.4.06-.81.1-1.21.1-3.9.03-7.26-2.86-7.82-6.72l-7.79-53.52c-1.03-7.05,2.62-13.94,9.02-17.06l36.86-17.93c.16,1.11.31,2.2.46,3.23.21,1.49.41,2.89.59,4.19.86,6.19,1.82,12.45,2.87,18.63-4.23,3.23-7.84,7-10.77,11.23-10.28,14.94-9.7,32.19-7.4,44.03Zm66.28,15.06c-1.59-.37-3.12-.8-4.59-1.29-.05-.01-.1-.03-.15-.05l4.74-.77v2.11Zm17.57-4.93v9.18c-.1,0-.2,0-.3,0-1.98-.04-3.8-.13-5.54-.27h-.06c-2.49-.19-4.92-.47-7.25-.84v-5.96l4.57-.74c3.28-.53,6.41-1.55,9.31-2.98-.28.14-.56.28-.85.41.08.39.12.8.12,1.21Zm17.58,8.06c-2,.32-4.07.57-6.22.74-2.24.21-4.56.33-6.93.38v-9.18c0-3.62,2.94-6.58,6.57-6.58,1.75,0,3.41.69,4.65,1.93,1.01,1.01,1.64,2.27,1.85,3.67.06.31.08.64.08.98v8.06Zm1.15-15.88c-1.78-1.76-4.06-2.84-6.52-3.11-.05.06-.1.12-.15.18,2.15-2.7,3.91-5.74,5.15-9.06l13.45-35.99c2.27,4.62,3.46,9.74,3.6,14.74v28.69c-1.7-1.02-3.65-1.57-5.68-1.57-4.23,0-8.01,2.47-9.85,6.12Zm2.48-114.52c1.58.05,3.14.04,4.7-.03,2.69,8.51,2.17,17.75-1.48,25.9-.94,2.09-2.43,3.78-4.23,4.98.87-10.27,1.21-20.58,1.01-30.85Zm2.87,33.81l8.28-9.59-17.75,47.54c1.28-7.05,2.4-14.92,3.67-23.82.14-1.01.27-2.03.4-3.05l.09-.41c.84-3.95,2.67-7.62,5.31-10.67Zm11.09,85.59v4.37c-1.37,1.44-3.63,2.79-6.61,3.93-1.91.73-4.11,1.36-6.55,1.9v-10.2c0-.33.03-.67.07-1,.49-3.18,3.28-5.57,6.5-5.57,1.76,0,3.41.69,4.65,1.92,1.25,1.25,1.94,2.91,1.94,4.65Zm26.62-148.29c-3.95,5.14-8.82,9.4-14.53,12.7-5.05,2.98-10.52,4.96-16.21,5.9-4.62.77-9.39.85-14.21.23-.06-.01-.12-.01-.18-.01-.02-.01-.04-.01-.07-.02-6.87-.53-13.53-2.18-19.78-4.9-3.9-1.7-7.61-3.8-11.05-6.25-4.46-3.16-8.49-6.9-11.99-11.11-1.7-2.06-4.75-2.34-6.81-.63-2.06,1.7-2.34,4.75-.64,6.81,4.04,4.86,8.69,9.17,13.83,12.82,3.98,2.83,8.28,5.26,12.79,7.23,5.92,2.58,12.14,4.31,18.55,5.2.37,15.84-.56,31.79-2.79,47.5-4.26,29.89-6.61,46.35-18.11,58.26-6.22,6.44-24.61,20.5-62.52,14.7-.72-2.13-1.67-5.33-2.42-9.17-1.94-10.03-2.5-24.53,5.87-36.69,1.44-2.08,3.08-4.01,4.92-5.8.14.56.25,1.12.39,1.68.32,1.29.67,2.59,1.04,3.88.61,2.12,2.55,3.5,4.65,3.5.45,0,.9-.06,1.34-.19,2.57-.74,4.06-3.42,3.32-5.99-.34-1.18-.66-2.37-.96-3.56-.73-2.91-1.35-5.9-1.84-8.86,0-.01,0-.02-.01-.03v-.02c-1.23-7.05-2.35-14.22-3.33-21.29-.18-1.31-.38-2.73-.59-4.23-1.75-12.31-4.38-30.92-4.44-51.61-.05-20.06.62-37.61,3.6-54.59.86-4.98,1.93-9.91,3.23-14.84,3.94-14.14,9.89-24.98,18.23-33.16,14.07-14.14,35.36-19.08,54.27-12.57,23.09,7.72,37.71,28.83,50.07,50.5,9.98,17.33,8.56,38.76-3.62,54.61Z" fill="#263238"/><path d="m425.44,285.08h-44.92c-2.5,0-3.43-1.87-1.87-3.74l161.6-212.76c.94-1.56,2.5-1.87,4.06-1.87h44.92c1.87,0,3.12,1.25,3.12,3.12v212.14c0,1.87-1.25,3.12-3.12,3.12h-44.3c-1.87,0-3.12-1.25-3.12-3.12v-25.89c0-1.56-.94-2.5-2.5-2.5h-84.23c-1.25,0-2.5.62-3.12,1.56l-22.46,28.08c-.94,1.25-2.5,1.87-4.06,1.87Zm69.57-72.06h44.3c1.56,0,2.5-.94,2.5-2.5v-58.34c0-2.18-1.87-2.81-3.12-.94l-44.92,58.65c-1.25,1.87-.62,3.12,1.25,3.12Zm199.03-127.59v41.18c0,1.56.94,2.5,2.5,2.5h21.21c1.87,0,3.12,1.25,3.12,3.12v31.82c0,1.87-1.25,3.12-3.12,3.12h-21.21c-1.56,0-2.5.94-2.5,2.5v112.31c0,1.87-1.25,3.12-3.12,3.12h-44.3c-1.87,0-3.12-1.25-3.12-3.12v-112.31c0-1.56-.94-2.5-2.5-2.5h-23.71c-1.87,0-3.12-1.25-3.12-3.12v-31.82c0-1.87,1.25-3.12,3.12-3.12h23.71c1.56,0,2.5-.94,2.5-2.5v-41.18c0-1.87,1.25-3.12,3.12-3.12h44.3c1.87,0,3.12,1.25,3.12,3.12Zm96.08,46.8l.31,74.87c0,26.21,12.48,33.69,26.52,33.69,22.46,0,30.89-16.53,30.89-35.56v-73c0-1.87,1.25-3.12,3.12-3.12h44.3c1.87,0,3.12,1.25,3.12,3.12v149.74c0,1.87-1.25,3.12-3.12,3.12h-44.3c-1.87,0-3.12-1.25-3.12-3.12v-10.61c0-2.18-1.25-2.5-3.12-1.25-10.3,9.67-23.4,15.29-37.75,15.6-35.25.62-67.07-27.77-67.07-71.13l-.31-82.36c0-1.87,1.25-3.12,3.12-3.12h44.3c1.87,0,3.12.94,3.12,3.12Zm160.66-71.13c15.6,0,27.45,11.85,27.45,26.52s-12.17,26.52-27.45,26.52-27.45-11.85-27.45-26.52,12.17-26.52,27.45-26.52Zm-25.27,220.87v-149.74c0-1.87,1.25-3.12,3.12-3.12h44.3c1.87,0,3.12,1.25,3.12,3.12v149.74c0,1.87-1.25,3.12-3.12,3.12h-44.3c-1.87,0-3.12-1.25-3.12-3.12Zm185.93,0l-.31-74.25c0-26.21-12.48-33.69-26.52-33.69-22.46,0-30.89,16.53-30.89,35.56v72.38c0,1.87-1.25,3.12-3.12,3.12h-44.3c-1.87,0-3.12-1.25-3.12-3.12v-149.74c0-1.87,1.25-3.12,3.12-3.12h44.3c1.87,0,3.12,1.25,3.12,3.12v9.05c0,2.18,1.56,2.81,3.12,1.25,10.3-9.67,23.71-15.91,37.75-15.91,35.25,0,67.07,30.26,67.07,73.62l.31,81.74c0,1.87-1.25,3.12-3.12,3.12h-44.3c-1.87,0-3.12-.94-3.12-3.12Z" fill="#263238"/></g></svg> \ No newline at end of file
diff --git a/ui/src/assets/react.svg b/ui/src/assets/react.svg
new file mode 100644
index 00000000..6c87de9b
--- /dev/null
+++ b/ui/src/assets/react.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> \ No newline at end of file
diff --git a/ui/src/components/Drawer.tsx b/ui/src/components/Drawer.tsx
new file mode 100644
index 00000000..65bb5ab4
--- /dev/null
+++ b/ui/src/components/Drawer.tsx
@@ -0,0 +1,26 @@
+import * as React from "react";
+
+import { Drawer as VDrawer } from "vaul";
+
+export default function Drawer({
+ trigger,
+ children,
+ width,
+ open,
+ onOpenChange,
+}: any) {
+ return (
+ <VDrawer.Root direction="right" open={open} onOpenChange={onOpenChange}>
+ <VDrawer.Trigger asChild>{trigger}</VDrawer.Trigger>
+ <VDrawer.Portal>
+ <VDrawer.Overlay className="fixed inset-0 bg-black/40 z-50" />
+ <VDrawer.Content
+ style={{ width: width || "400px" }}
+ className={`bg-white flex flex-col z-50 h-full mt-24 fixed bottom-0 right-0`}
+ >
+ {children}
+ </VDrawer.Content>
+ </VDrawer.Portal>
+ </VDrawer.Root>
+ );
+}
diff --git a/ui/src/components/HistoryList.tsx b/ui/src/components/HistoryList.tsx
new file mode 100644
index 00000000..b31a4be4
--- /dev/null
+++ b/ui/src/components/HistoryList.tsx
@@ -0,0 +1,75 @@
+import { DateTime } from 'luxon';
+import { ChevronRightIcon } from '@heroicons/react/20/solid'
+
+function msToTime(ms) {
+ let milliseconds = (ms).toFixed(1);
+ let seconds = (ms / 1000).toFixed(1);
+ let minutes = (ms / (1000 * 60)).toFixed(1);
+ let hours = (ms / (1000 * 60 * 60)).toFixed(1);
+ let days = (ms / (1000 * 60 * 60 * 24)).toFixed(1);
+
+ if (milliseconds < 1000) return milliseconds + "ms";
+ else if (seconds < 60) return seconds + "s";
+ else if (minutes < 60) return minutes + "m";
+ else if (hours < 24) return hours + "hr";
+ else return days + " Days"
+}
+
+export default function HistoryList(props){
+ return (
+
+ <ul
+ role="list"
+ className="divide-y divide-gray-100 overflow-hidden bg-white shadow-sm ring-1 ring-gray-900/5"
+ >
+ {props.history.map((h) => (
+ <li key={h.id} className="relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6">
+ <div className="flex min-w-0 gap-x-4">
+ <div className="flex flex-col justify-center">
+ <p className="flex text-xs text-gray-500 justify-center">{ DateTime.fromMillis(h.timestamp / 1000000).toLocaleString(DateTime.TIME_WITH_SECONDS)}</p>
+ <p className="flex text-xs mt-1 text-gray-400 justify-center">{ DateTime.fromMillis(h.timestamp / 1000000).toLocaleString(DateTime.DATE_SHORT)}</p>
+ </div>
+ <div className="min-w-0 flex-col justify-center">
+ <pre className="whitespace-pre-wrap"><code className="text-sm">{h.command}</code></pre>
+ <p className="mt-1 flex text-xs leading-5 text-gray-500">
+ <span className="relative truncate ">
+ {h.user}
+ </span>
+
+ <span>&nbsp;on&nbsp;</span>
+
+ <span className="relative truncate ">
+ {h.host}
+ </span>
+
+ <span>&nbsp;in&nbsp;</span>
+
+ <span className="relative truncate ">
+ {h.cwd}
+ </span>
+ </p>
+ </div>
+ </div>
+ <div className="flex shrink-0 items-center gap-x-4">
+ <div className="hidden sm:flex sm:flex-col sm:items-end">
+ <p className="text-sm leading-6 text-gray-900">{h.exit}</p>
+ {h.duration ? (
+ <p className="mt-1 text-xs leading-5 text-gray-500">
+ <time dateTime={h.duration}>{msToTime(h.duration / 1000000)}</time>
+ </p>
+ ) : (
+ <div className="mt-1 flex items-center gap-x-1.5">
+ <div className="flex-none rounded-full bg-emerald-500/20 p-1">
+ <div className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
+ </div>
+ <p className="text-xs leading-5 text-gray-500">Online</p>
+ </div>
+ )}
+ </div>
+ <ChevronRightIcon className="h-5 w-5 flex-none text-gray-400" aria-hidden="true" />
+ </div>
+ </li>
+ ))}
+ </ul>
+ );
+}
diff --git a/ui/src/components/HistorySearch.tsx b/ui/src/components/HistorySearch.tsx
new file mode 100644
index 00000000..08bed2a8
--- /dev/null
+++ b/ui/src/components/HistorySearch.tsx
@@ -0,0 +1,56 @@
+import { useState } from "react";
+import { ArrowPathIcon } from "@heroicons/react/24/outline";
+import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
+
+interface HistorySearchProps {
+ refresh: (query: string) => void;
+}
+
+export default function HistorySearch(props: HistorySearchProps) {
+ let [searchQuery, setSearchQuery] = useState("");
+
+ return (
+ <div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
+ <form
+ className="relative flex flex-1"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <label htmlFor="search-field" className="sr-only">
+ Search
+ </label>
+ <MagnifyingGlassIcon
+ className="pointer-events-none absolute inset-y-0 left-0 h-full w-5 text-gray-400"
+ aria-hidden="true"
+ />
+ <input
+ id="search-field"
+ className="block h-full w-full border-0 py-0 pl-8 pr-0 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm"
+ placeholder="Search..."
+ autoComplete="off"
+ autoCapitalize="off"
+ autoCorrect="off"
+ spellCheck="false"
+ type="search"
+ name="search"
+ onChange={(query) => {
+ setSearchQuery(query.target.value);
+ props.refresh(query.target.value);
+ }}
+ />
+ </form>
+ <div className="flex items-center gap-x-4 lg:gap-x-6">
+ <button
+ type="button"
+ className="-m-2.5 p-2.5 text-gray-400 hover:text-gray-500"
+ onClick={() => {
+ props.refresh(searchQuery);
+ }}
+ >
+ <ArrowPathIcon className="h-6 w-6" aria-hidden="true" />
+ </button>
+ </div>
+ </div>
+ );
+}
diff --git a/ui/src/components/dotfiles/Aliases.tsx b/ui/src/components/dotfiles/Aliases.tsx
new file mode 100644
index 00000000..4854e6b5
--- /dev/null
+++ b/ui/src/components/dotfiles/Aliases.tsx
@@ -0,0 +1,191 @@
+import React, { useEffect, useState } from "react";
+
+import DataTable from "@/components/ui/data-table";
+import { Button } from "@/components/ui/button";
+import { MoreHorizontal } from "lucide-react";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+
+import { invoke } from "@tauri-apps/api/core";
+import Drawer from "@/components/Drawer";
+
+function loadAliases(
+ setAliases: React.Dispatch<React.SetStateAction<never[]>>,
+) {
+ invoke("aliases").then((aliases: any) => {
+ setAliases(aliases);
+ });
+}
+
+type Alias = {
+ name: string;
+ value: string;
+};
+
+function deleteAlias(
+ name: string,
+ setAliases: React.Dispatch<React.SetStateAction<never[]>>,
+) {
+ invoke("delete_alias", { name: name })
+ .then(() => {
+ console.log("Deleted alias");
+ loadAliases(setAliases);
+ })
+ .catch(() => {
+ console.error("Failed to delete alias");
+ });
+}
+
+function AddAlias({ onAdd: onAdd }: { onAdd?: () => void }) {
+ let [name, setName] = useState("");
+ let [value, setValue] = useState("");
+
+ // simple form to add aliases
+ return (
+ <div className="p-4">
+ <h2 className="text-xl font-semibold leading-6 text-gray-900">
+ Add alias
+ </h2>
+ <p className="mt-2">Add a new alias to your shell</p>
+
+ <form
+ className="mt-4"
+ onSubmit={(e) => {
+ e.preventDefault();
+
+ invoke("set_alias", { name: name, value: value })
+ .then(() => {
+ console.log("Added alias");
+
+ if (onAdd) onAdd();
+ })
+ .catch(() => {
+ console.error("Failed to add alias");
+ });
+ }}
+ >
+ <input
+ className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-md focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
+ type="text"
+ value={name}
+ onChange={(e) => setName(e.target.value)}
+ placeholder="Alias name"
+ />
+
+ <input
+ className="mt-4 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-md focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
+ autoComplete="off"
+ autoCapitalize="off"
+ autoCorrect="off"
+ spellCheck="false"
+ type="text"
+ value={value}
+ onChange={(e) => setValue(e.target.value)}
+ placeholder="Alias value"
+ />
+
+ <input
+ type="submit"
+ className="block mt-4 rounded-md bg-green-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
+ value="Add alias"
+ />
+ </form>
+ </div>
+ );
+}
+
+export default function Aliases() {
+ let [aliases, setAliases] = useState([]);
+ let [aliasDrawerOpen, setAliasDrawerOpen] = useState(false);
+
+ const columns: ColumnDef<Alias>[] = [
+ {
+ accessorKey: "name",
+ header: "Name",
+ },
+ {
+ accessorKey: "value",
+ header: "Value",
+ },
+ {
+ id: "actions",
+ cell: ({ row }: any) => {
+ const alias = row.original;
+
+ return (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button variant="ghost" className="h-8 w-8 p-0 float-right">
+ <span className="sr-only">Open menu</span>
+ <MoreHorizontal className="h-4 w-4 text-right" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end">
+ <DropdownMenuLabel>Actions</DropdownMenuLabel>
+ <DropdownMenuItem
+ onClick={() => deleteAlias(alias.name, setAliases)}
+ >
+ Delete
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ );
+ },
+ },
+ ];
+
+ useEffect(() => {
+ loadAliases(setAliases);
+ }, []);
+
+ return (
+ <div className="pt-10">
+ <div className="sm:flex sm:items-center">
+ <div className="sm:flex-auto">
+ <h1 className="text-base font-semibold leading-6 text-gray-900">
+ Aliases
+ </h1>
+ <p className="mt-2 text-sm text-gray-700">
+ Aliases allow you to condense long commands into short,
+ easy-to-remember commands.
+ </p>
+ </div>
+ <div className="mt-4 sm:ml-16 sm:mt-0 flex-row">
+ <Drawer
+ open={aliasDrawerOpen}
+ onOpenChange={setAliasDrawerOpen}
+ width="30%"
+ trigger={
+ <button
+ type="button"
+ className="block rounded-md bg-green-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
+ >
+ Add
+ </button>
+ }
+ >
+ <AddAlias
+ onAdd={() => {
+ loadAliases(setAliases);
+ setAliasDrawerOpen(false);
+ }}
+ />
+ </Drawer>
+ </div>
+ </div>
+ <div className="mt-8 flow-root">
+ <div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
+ <div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
+ <DataTable columns={columns} data={aliases} />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/ui/src/components/history/Stats.tsx b/ui/src/components/history/Stats.tsx
new file mode 100644
index 00000000..afd9ed89
--- /dev/null
+++ b/ui/src/components/history/Stats.tsx
@@ -0,0 +1,143 @@
+import { useState, useEffect } from "react";
+import { invoke } from "@tauri-apps/api/core";
+import PacmanLoader from "react-spinners/PacmanLoader";
+
+import {
+ BarChart,
+ Bar,
+ Rectangle,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ Legend,
+ ResponsiveContainer,
+} from "recharts";
+
+const tabs = [
+ { name: "Daily", href: "#", current: true },
+ { name: "Weekly", href: "#", current: false },
+ { name: "Monthly", href: "#", current: false },
+];
+
+function classNames(...classes) {
+ return classes.filter(Boolean).join(" ");
+}
+
+function renderLoading() {
+ <div className="flex items-center justify-center h-full">
+ <PacmanLoader color="#26bd65" />
+ </div>;
+}
+
+export default function Stats() {
+ const [stats, setStats]: any = useState([]);
+ const [chart, setChart]: any = useState([]);
+
+ console.log("Stats mounted");
+
+ useEffect(() => {
+ if (stats.length != 0) return;
+
+ invoke("global_stats")
+ .then((s: any) => {
+ console.log(s.daily);
+
+ setStats([
+ {
+ name: "Total history",
+ stat: s.total_history.toLocaleString(),
+ },
+ {
+ name: "Last 1d",
+ stat: s.last_1d.toLocaleString(),
+ },
+ {
+ name: "Last 7d",
+ stat: s.last_7d.toLocaleString(),
+ },
+ {
+ name: "Last 30d",
+ stat: s.last_30d.toLocaleString(),
+ },
+ ]);
+
+ setChart(s.daily);
+ })
+ .catch((e) => {
+ console.log(e);
+ });
+ }, []);
+
+ if (stats.length == 0) {
+ return renderLoading();
+ }
+
+ return (
+ <div className="flex flex-col">
+ <div className="flexfull">
+ <dl className="grid grid-cols-1 sm:grid-cols-4 w-full">
+ {stats.map((item) => (
+ <div
+ key={item.name}
+ className="overflow-hidden bg-white px-4 py-5 shadow sm:p-6"
+ >
+ <dt className="truncate text-sm font-medium text-gray-500">
+ {item.name}
+ </dt>
+ <dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900">
+ {item.stat}
+ </dd>
+ </div>
+ ))}
+ </dl>
+ </div>
+
+ <div className="flex flex-col h-54 py-4 pl-5">
+ <div className="sm:hidden">
+ {/* Use an "onChange" listener to redirect the user to the selected tab URL. */}
+ <select
+ id="tabs"
+ name="tabs"
+ className="block w-full rounded-md border-gray-300 focus:border-green-500 focus:ring-green-500"
+ defaultValue={tabs.find((tab) => tab.current).name}
+ >
+ {tabs.map((tab) => (
+ <option key={tab.name}>{tab.name}</option>
+ ))}
+ </select>
+ </div>
+ <div className="hidden sm:block">
+ <nav className="flex space-x-4" aria-label="Tabs">
+ {tabs.map((tab) => (
+ <a
+ key={tab.name}
+ href={tab.href}
+ className={classNames(
+ tab.current
+ ? "bg-gray-100 text-gray-700"
+ : "text-gray-500 hover:text-gray-700",
+ "rounded-md px-3 py-2 text-sm font-medium",
+ )}
+ aria-current={tab.current ? "page" : undefined}
+ >
+ {tab.name}
+ </a>
+ ))}
+ </nav>
+ </div>
+
+ <div className="flex flex-col h-48 pt-5 pr-5">
+ <ResponsiveContainer width="100%" height="100%">
+ <BarChart width={500} height={300} data={chart}>
+ <XAxis dataKey="name" hide={true} />
+ <YAxis />
+ <Tooltip />
+ <Bar dataKey="value" fill="#26bd65" />
+ </BarChart>
+ </ResponsiveContainer>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/ui/src/components/ui/button.tsx b/ui/src/components/ui/button.tsx
new file mode 100644
index 00000000..0ba42773
--- /dev/null
+++ b/ui/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
+ VariantProps<typeof buttonVariants> {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+ <Comp
+ className={cn(buttonVariants({ variant, size, className }))}
+ ref={ref}
+ {...props}
+ />
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/ui/src/components/ui/data-table.tsx b/ui/src/components/ui/data-table.tsx
new file mode 100644
index 00000000..cf96b620
--- /dev/null
+++ b/ui/src/components/ui/data-table.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import {
+ ColumnDef,
+ flexRender,
+ getCoreRowModel,
+ useReactTable,
+} from "@tanstack/react-table";
+
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+interface DataTableProps<TData, TValue> {
+ columns: ColumnDef<TData, TValue>[];
+ data: TData[];
+}
+
+export default function DataTable<TData, TValue>({
+ columns,
+ data,
+}: DataTableProps<TData, TValue>) {
+ const table = useReactTable({
+ data,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ });
+
+ return (
+ <div className="rounded-md border">
+ <Table>
+ <TableHeader>
+ {table.getHeaderGroups().map((headerGroup) => (
+ <TableRow key={headerGroup.id}>
+ {headerGroup.headers.map((header) => {
+ return (
+ <TableHead key={header.id}>
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+ </TableHead>
+ );
+ })}
+ </TableRow>
+ ))}
+ </TableHeader>
+ <TableBody>
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => (
+ <TableRow
+ key={row.id}
+ data-state={row.getIsSelected() && "selected"}
+ >
+ {row.getVisibleCells().map((cell) => (
+ <TableCell key={cell.id}>
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ </TableCell>
+ ))}
+ </TableRow>
+ ))
+ ) : (
+ <TableRow>
+ <TableCell colSpan={columns.length} className="h-24 text-center">
+ No results.
+ </TableCell>
+ </TableRow>
+ )}
+ </TableBody>
+ </Table>
+ </div>
+ );
+}
diff --git a/ui/src/components/ui/dropdown-menu.tsx b/ui/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 00000000..769ff7aa
--- /dev/null
+++ b/ui/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,198 @@
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+ <DropdownMenuPrimitive.SubTrigger
+ ref={ref}
+ className={cn(
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
+ inset && "pl-8",
+ className
+ )}
+ {...props}
+ >
+ {children}
+ <ChevronRight className="ml-auto h-4 w-4" />
+ </DropdownMenuPrimitive.SubTrigger>
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
+>(({ className, ...props }, ref) => (
+ <DropdownMenuPrimitive.SubContent
+ ref={ref}
+ className={cn(
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+ className
+ )}
+ {...props}
+ />
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
+>(({ className, sideOffset = 4, ...props }, ref) => (
+ <DropdownMenuPrimitive.Portal>
+ <DropdownMenuPrimitive.Content
+ ref={ref}
+ sideOffset={sideOffset}
+ className={cn(
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+ className
+ )}
+ {...props}
+ />
+ </DropdownMenuPrimitive.Portal>
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+ <DropdownMenuPrimitive.Item
+ ref={ref}
+ className={cn(
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+ inset && "pl-8",
+ className
+ )}
+ {...props}
+ />
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
+>(({ className, children, checked, ...props }, ref) => (
+ <DropdownMenuPrimitive.CheckboxItem
+ ref={ref}
+ className={cn(
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+ className
+ )}
+ checked={checked}
+ {...props}
+ >
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+ <DropdownMenuPrimitive.ItemIndicator>
+ <Check className="h-4 w-4" />
+ </DropdownMenuPrimitive.ItemIndicator>
+ </span>
+ {children}
+ </DropdownMenuPrimitive.CheckboxItem>
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
+>(({ className, children, ...props }, ref) => (
+ <DropdownMenuPrimitive.RadioItem
+ ref={ref}
+ className={cn(
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+ className
+ )}
+ {...props}
+ >
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+ <DropdownMenuPrimitive.ItemIndicator>
+ <Circle className="h-2 w-2 fill-current" />
+ </DropdownMenuPrimitive.ItemIndicator>
+ </span>
+ {children}
+ </DropdownMenuPrimitive.RadioItem>
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+ <DropdownMenuPrimitive.Label
+ ref={ref}
+ className={cn(
+ "px-2 py-1.5 text-sm font-semibold",
+ inset && "pl-8",
+ className
+ )}
+ {...props}
+ />
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
+>(({ className, ...props }, ref) => (
+ <DropdownMenuPrimitive.Separator
+ ref={ref}
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
+ {...props}
+ />
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes<HTMLSpanElement>) => {
+ return (
+ <span
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
+ {...props}
+ />
+ )
+}
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+}
diff --git a/ui/src/components/ui/table.tsx b/ui/src/components/ui/table.tsx
new file mode 100644
index 00000000..7f3502f8
--- /dev/null
+++ b/ui/src/components/ui/table.tsx
@@ -0,0 +1,117 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Table = React.forwardRef<
+ HTMLTableElement,
+ React.HTMLAttributes<HTMLTableElement>
+>(({ className, ...props }, ref) => (
+ <div className="relative w-full overflow-auto">
+ <table
+ ref={ref}
+ className={cn("w-full caption-bottom text-sm", className)}
+ {...props}
+ />
+ </div>
+))
+Table.displayName = "Table"
+
+const TableHeader = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes<HTMLTableSectionElement>
+>(({ className, ...props }, ref) => (
+ <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
+))
+TableHeader.displayName = "TableHeader"
+
+const TableBody = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes<HTMLTableSectionElement>
+>(({ className, ...props }, ref) => (
+ <tbody
+ ref={ref}
+ className={cn("[&_tr:last-child]:border-0", className)}
+ {...props}
+ />
+))
+TableBody.displayName = "TableBody"
+
+const TableFooter = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes<HTMLTableSectionElement>
+>(({ className, ...props }, ref) => (
+ <tfoot
+ ref={ref}
+ className={cn(
+ "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+))
+TableFooter.displayName = "TableFooter"
+
+const TableRow = React.forwardRef<
+ HTMLTableRowElement,
+ React.HTMLAttributes<HTMLTableRowElement>
+>(({ className, ...props }, ref) => (
+ <tr
+ ref={ref}
+ className={cn(
+ "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
+ className
+ )}
+ {...props}
+ />
+))
+TableRow.displayName = "TableRow"
+
+const TableHead = React.forwardRef<
+ HTMLTableCellElement,
+ React.ThHTMLAttributes<HTMLTableCellElement>
+>(({ className, ...props }, ref) => (
+ <th
+ ref={ref}
+ className={cn(
+ "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
+ className
+ )}
+ {...props}
+ />
+))
+TableHead.displayName = "TableHead"
+
+const TableCell = React.forwardRef<
+ HTMLTableCellElement,
+ React.TdHTMLAttributes<HTMLTableCellElement>
+>(({ className, ...props }, ref) => (
+ <td
+ ref={ref}
+ className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
+ {...props}
+ />
+))
+TableCell.displayName = "TableCell"
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes<HTMLTableCaptionElement>
+>(({ className, ...props }, ref) => (
+ <caption
+ ref={ref}
+ className={cn("mt-4 text-sm text-muted-foreground", className)}
+ {...props}
+ />
+))
+TableCaption.displayName = "TableCaption"
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts
new file mode 100644
index 00000000..d084ccad
--- /dev/null
+++ b/ui/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/ui/src/main.tsx b/ui/src/main.tsx
new file mode 100644
index 00000000..81da3460
--- /dev/null
+++ b/ui/src/main.tsx
@@ -0,0 +1,10 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./styles.css";
+
+ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
+ <React.StrictMode>
+ <App />
+ </React.StrictMode>,
+);
diff --git a/ui/src/pages/Dotfiles.tsx b/ui/src/pages/Dotfiles.tsx
new file mode 100644
index 00000000..bd209062
--- /dev/null
+++ b/ui/src/pages/Dotfiles.tsx
@@ -0,0 +1,32 @@
+import { useState } from "react";
+
+import { Cog6ToothIcon } from "@heroicons/react/24/outline";
+
+import Aliases from "@/components/dotfiles/Aliases";
+
+import { Drawer } from "@/components/drawer";
+import { invoke } from "@tauri-apps/api/core";
+
+function Header() {
+ return (
+ <div className="md:flex md:items-center md:justify-between">
+ <div className="min-w-0 flex-1">
+ <h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
+ Dotfiles
+ </h2>
+ </div>
+ </div>
+ );
+}
+
+export default function Dotfiles() {
+ return (
+ <div className="pl-60">
+ <div className="p-10">
+ <Header />
+ Manage your shell aliases, variables and paths
+ <Aliases />
+ </div>
+ </div>
+ );
+}
diff --git a/ui/src/pages/History.tsx b/ui/src/pages/History.tsx
new file mode 100644
index 00000000..f74c16ac
--- /dev/null
+++ b/ui/src/pages/History.tsx
@@ -0,0 +1,108 @@
+import { Fragment, useState, useEffect } from "react";
+import { Dialog, Transition } from "@headlessui/react";
+import {
+ Bars3Icon,
+ ChartPieIcon,
+ Cog6ToothIcon,
+ HomeIcon,
+ XMarkIcon,
+} from "@heroicons/react/24/outline";
+
+import Logo from "../assets/logo-light.svg";
+
+import { invoke } from "@tauri-apps/api/core";
+
+import HistoryList from "@/components/HistoryList.tsx";
+import HistorySearch from "@/components/HistorySearch.tsx";
+import Stats from "@/components/history/Stats.tsx";
+import Drawer from "@/components/Drawer.tsx";
+
+function refreshHistory(
+ setHistory: React.Dispatch<React.SetStateAction<never[]>>,
+ query: String | null,
+) {
+ if (query) {
+ invoke("search", { query: query })
+ .then((res: any[]) => {
+ setHistory(res);
+ })
+ .catch((e) => {
+ console.log(e);
+ });
+ } else {
+ invoke("list").then((h: any[]) => {
+ setHistory(h);
+ });
+ }
+}
+
+function Header() {
+ return (
+ <div className="md:flex md:items-center md:justify-between">
+ <div className="min-w-0 flex-1">
+ <h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
+ Shell History
+ </h2>
+ </div>
+ <div className="mt-4 flex md:ml-4 md:mt-0">
+ <Drawer
+ width="70%"
+ trigger={
+ <button
+ type="button"
+ className="inline-flex border-2 items-center hover:shadow-xl rounded-md px-2 py-2 text-sm font-semibold shadow-sm"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ strokeWidth={1.5}
+ stroke="currentColor"
+ className="w-6 h-6"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z"
+ />
+ </svg>
+ </button>
+ }
+ >
+ <Stats />
+ </Drawer>
+ </div>
+ </div>
+ );
+}
+
+export default function Search() {
+ let [history, setHistory] = useState([]);
+
+ useEffect(() => {
+ refreshHistory(setHistory, null);
+ }, []);
+
+ return (
+ <>
+ <div className="pl-60">
+ <div className="p-10">
+ <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">
+ <HistorySearch
+ refresh={(query: String | null) => {
+ refreshHistory(setHistory, query);
+ }}
+ />
+ </div>
+
+ <main>
+ <HistoryList history={history} />
+ </main>
+ </div>
+ </>
+ );
+}
diff --git a/ui/src/styles.css b/ui/src/styles.css
new file mode 100644
index 00000000..b0e6fff5
--- /dev/null
+++ b/ui/src/styles.css
@@ -0,0 +1,76 @@
+@tailwind base;
+ @tailwind components;
+ @tailwind utilities;
+
+ @layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9%;
+ }
+ }
+
+ @layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+ } \ No newline at end of file
diff --git a/ui/src/vite-env.d.ts b/ui/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/ui/src/vite-env.d.ts
@@ -0,0 +1 @@
+/// <reference types="vite/client" />