diff --git a/atuin-client/src/api_client.rs b/atuin-client/src/api_client.rs index a19e5305..f31a796e 100644 --- a/atuin-client/src/api_client.rs +++ b/atuin-client/src/api_client.rs @@ -11,7 +11,7 @@ use reqwest::{ use atuin_common::{ api::{ AddHistoryRequest, ChangePasswordRequest, CountResponse, DeleteHistoryRequest, - ErrorResponse, LoginRequest, LoginResponse, RegisterResponse, StatusResponse, + ErrorResponse, LoginRequest, LoginResponse, MeResponse, RegisterResponse, StatusResponse, SyncHistoryResponse, }, record::RecordStatus, @@ -234,6 +234,18 @@ impl<'a> Client<'a> { Ok(status) } + pub async fn me(&self) -> Result { + let url = format!("{}/api/v0/me", self.sync_addr); + let url = Url::parse(url.as_str())?; + + let resp = self.client.get(url).send().await?; + let resp = handle_resp_error(resp).await?; + + let status = resp.json::().await?; + + Ok(status) + } + pub async fn get_history( &self, sync_ts: OffsetDateTime, diff --git a/atuin-client/src/history.rs b/atuin-client/src/history.rs index bc74aebd..1b590e88 100644 --- a/atuin-client/src/history.rs +++ b/atuin-client/src/history.rs @@ -18,7 +18,7 @@ mod builder; pub mod store; const HISTORY_VERSION: &str = "v0"; -const HISTORY_TAG: &str = "history"; +pub const HISTORY_TAG: &str = "history"; #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct HistoryId(pub String); diff --git a/atuin-client/src/record/sqlite_store.rs b/atuin-client/src/record/sqlite_store.rs index e17893eb..31de311b 100644 --- a/atuin-client/src/record/sqlite_store.rs +++ b/atuin-client/src/record/sqlite_store.rs @@ -180,6 +180,16 @@ impl Store for SqliteStore { self.idx(host, tag, 0).await } + async fn len_all(&self) -> Result { + let res: Result<(i64,), sqlx::Error> = sqlx::query_as("select count(*) from store") + .fetch_one(&self.pool) + .await; + match res { + Err(e) => Err(eyre!("failed to fetch local store len: {}", e)), + Ok(v) => Ok(v.0 as u64), + } + } + async fn len_tag(&self, tag: &str) -> Result { let res: Result<(i64,), sqlx::Error> = sqlx::query_as("select count(*) from store where tag=?1") diff --git a/atuin-client/src/record/store.rs b/atuin-client/src/record/store.rs index 188e18ce..49ca4968 100644 --- a/atuin-client/src/record/store.rs +++ b/atuin-client/src/record/store.rs @@ -25,6 +25,7 @@ pub trait Store { async fn delete(&self, id: RecordId) -> Result<()>; async fn delete_all(&self) -> Result<()>; + async fn len_all(&self) -> Result; async fn len(&self, host: HostId, tag: &str) -> Result; async fn len_tag(&self, tag: &str) -> Result; diff --git a/ui/backend/Cargo.lock b/ui/backend/Cargo.lock index 042e2c5a..abb72ce2 100644 --- a/ui/backend/Cargo.lock +++ b/ui/backend/Cargo.lock @@ -212,7 +212,7 @@ dependencies = [ [[package]] name = "atuin-client" -version = "18.1.0" +version = "18.2.0" dependencies = [ "async-trait", "atuin-common", @@ -259,7 +259,7 @@ dependencies = [ [[package]] name = "atuin-common" -version = "18.1.0" +version = "18.2.0" dependencies = [ "eyre", "lazy_static", @@ -276,7 +276,7 @@ dependencies = [ [[package]] name = "atuin-dotfiles" -version = "0.1.0" +version = "0.2.0" dependencies = [ "atuin-client", "atuin-common", diff --git a/ui/backend/src/main.rs b/ui/backend/src/main.rs index 98967562..fbcf9481 100644 --- a/ui/backend/src/main.rs +++ b/ui/backend/src/main.rs @@ -2,6 +2,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use std::path::PathBuf; +use time::format_description::well_known::Rfc3339; use atuin_client::settings::Settings; @@ -9,9 +10,20 @@ mod db; mod dotfiles; mod store; +use atuin_client::{ + encryption, history::HISTORY_TAG, record::sqlite_store::SqliteStore, record::store::Store, +}; use db::{GlobalStats, HistoryDB, UIHistory}; use dotfiles::aliases::aliases; +#[derive(Debug, serde::Serialize)] +struct HomeInfo { + pub username: String, + pub record_count: u64, + pub history_count: u64, + pub last_sync: String, +} + #[tauri::command] async fn list() -> Result, String> { let settings = Settings::new().map_err(|e| e.to_string())?; @@ -47,6 +59,54 @@ async fn global_stats() -> Result { Ok(stats) } +#[tauri::command] +async fn home_info() -> Result { + let settings = Settings::new().map_err(|e| e.to_string())?; + let record_store_path = PathBuf::from(settings.record_store_path.as_str()); + let sqlite_store = SqliteStore::new(record_store_path, settings.local_timeout) + .await + .map_err(|e| e.to_string())?; + + let client = atuin_client::api_client::Client::new( + &settings.sync_address, + &settings.session_token, + settings.network_connect_timeout, + settings.network_timeout, + ) + .map_err(|e| e.to_string())?; + + let session_path = settings.session_path.as_str(); + let last_sync = Settings::last_sync() + .map_err(|e| e.to_string())? + .format(&Rfc3339) + .map_err(|e| e.to_string())?; + let record_count = sqlite_store.len_all().await.map_err(|e| e.to_string())?; + let history_count = sqlite_store + .len_tag(HISTORY_TAG) + .await + .map_err(|e| e.to_string())?; + + let info = if !PathBuf::from(session_path).exists() { + HomeInfo { + username: String::from(""), + last_sync: last_sync.to_string(), + record_count, + history_count, + } + } else { + let me = client.me().await.map_err(|e| e.to_string())?; + + HomeInfo { + username: me.username, + last_sync: last_sync.to_string(), + record_count, + history_count, + } + }; + + Ok(info) +} + fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ @@ -54,6 +114,7 @@ fn main() { search, global_stats, aliases, + home_info, dotfiles::aliases::import_aliases, dotfiles::aliases::delete_alias, dotfiles::aliases::set_alias, diff --git a/ui/package.json b/ui/package.json index 126adabe..e3025717 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,6 +20,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "core": "link:@tauri-apps/api/core", + "date-fns": "^3.6.0", "highlight.js": "^11.9.0", "lucide-react": "^0.367.0", "luxon": "^3.4.4", @@ -29,7 +30,8 @@ "recharts": "^2.12.4", "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", - "vaul": "^0.9.0" + "vaul": "^0.9.0", + "zustand": "^4.5.2" }, "devDependencies": { "@tauri-apps/cli": "2.0.0-beta.2", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index dc202b20..1b4214fe 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -35,6 +35,9 @@ dependencies: core: specifier: link:@tauri-apps/api/core version: link:@tauri-apps/api/core + date-fns: + specifier: ^3.6.0 + version: 3.6.0 highlight.js: specifier: ^11.9.0 version: 11.9.0 @@ -65,6 +68,9 @@ dependencies: vaul: specifier: ^0.9.0 version: 0.9.0(@types/react-dom@18.2.24)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) + zustand: + specifier: ^4.5.2 + version: 4.5.2(@types/react@18.2.74)(react@18.2.0) devDependencies: '@tauri-apps/cli': @@ -1786,6 +1792,10 @@ packages: engines: {node: '>=12'} dev: false + /date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + dev: false + /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -2679,6 +2689,14 @@ packages: tslib: 2.6.2 dev: false + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2798,3 +2816,23 @@ packages: resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==} engines: {node: '>= 14'} hasBin: true + + /zustand@4.5.2(@types/react@18.2.74)(react@18.2.0): + resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + dependencies: + '@types/react': 18.2.74 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false diff --git a/ui/src/App.css b/ui/src/App.css index a89ebd15..5a32a1a5 100644 --- a/ui/src/App.css +++ b/ui/src/App.css @@ -1,7 +1,11 @@ +html { + overscroll-behavior: none; +} + .logo.vite:hover { - filter: drop-shadow(0 0 2em #747bff); + filter: drop-shadow(0 0 2em #747bff); } .logo.react:hover { - filter: drop-shadow(0 0 2em #61dafb); + filter: drop-shadow(0 0 2em #61dafb); } diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 5d1cd863..ae6ebdb1 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,14 +1,9 @@ import "./App.css"; -import { Fragment, useState, useEffect, ReactElement } from "react"; -import { Dialog, Transition } from "@headlessui/react"; +import { useState, ReactElement } from "react"; import { - Bars3Icon, - ChartPieIcon, Cog6ToothIcon, HomeIcon, - XMarkIcon, - MagnifyingGlassIcon, ClockIcon, WrenchScrewdriverIcon, } from "@heroicons/react/24/outline"; @@ -18,16 +13,20 @@ function classNames(...classes: any) { return classes.filter(Boolean).join(" "); } +import Home from "./pages/Home.tsx"; import History from "./pages/History.tsx"; import Dotfiles from "./pages/Dotfiles.tsx"; enum Section { + Home, History, Dotfiles, } function renderMain(section: Section): ReactElement { switch (section) { + case Section.Home: + return ; case Section.History: return ; case Section.Dotfiles: @@ -39,9 +38,14 @@ 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 [section, setSection] = useState(Section.Home); const navigation = [ + { + name: "Home", + icon: HomeIcon, + section: Section.Home, + }, { name: "History", icon: ClockIcon, diff --git a/ui/src/components/Drawer.tsx b/ui/src/components/Drawer.tsx index 65bb5ab4..91753624 100644 --- a/ui/src/components/Drawer.tsx +++ b/ui/src/components/Drawer.tsx @@ -1,5 +1,3 @@ -import * as React from "react"; - import { Drawer as VDrawer } from "vaul"; export default function Drawer({ diff --git a/ui/src/components/HistoryList.tsx b/ui/src/components/HistoryList.tsx index b31a4be4..9616ecf0 100644 --- a/ui/src/components/HistoryList.tsx +++ b/ui/src/components/HistoryList.tsx @@ -1,75 +1,88 @@ -import { DateTime } from 'luxon'; -import { ChevronRightIcon } from '@heroicons/react/20/solid' +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); +// @ts-ignore +import { DateTime } from "luxon"; + +function msToTime(ms: number) { + let milliseconds = parseInt(ms.toFixed(1)); + let seconds = parseInt((ms / 1000).toFixed(1)); + let minutes = parseInt((ms / (1000 * 60)).toFixed(1)); + let hours = parseInt((ms / (1000 * 60 * 60)).toFixed(1)); + let days = parseInt((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" + else return days + " Days"; } -export default function HistoryList(props){ +export default function HistoryList(props: any) { return ( +
    + {props.history.map((h: any) => ( +
  • +
    +
    +

    + {DateTime.fromMillis(h.timestamp / 1000000).toLocaleString( + DateTime.TIME_WITH_SECONDS, + )} +

    +

    + {DateTime.fromMillis(h.timestamp / 1000000).toLocaleString( + DateTime.DATE_SHORT, + )} +

    +
    +
    +
    +                {h.command}
    +              
    +

    + {h.user} -

      - {props.history.map((h) => ( -
    • -
      -
      -

      { DateTime.fromMillis(h.timestamp / 1000000).toLocaleString(DateTime.TIME_WITH_SECONDS)}

      -

      { DateTime.fromMillis(h.timestamp / 1000000).toLocaleString(DateTime.DATE_SHORT)}

      -
      -
      -
      {h.command}
      -

      - - {h.user} - +  on  -  on  + {h.host} - - {h.host} - +  in  -  in  - - - {h.cwd} - -

      -
      + {h.cwd} +

      +
      +
    +
    +
    +

    {h.exit}

    + {h.duration ? ( +

    + +

    + ) : ( +
    +
    +
    -
    -
    -

    {h.exit}

    - {h.duration ? ( -

    - -

    - ) : ( -
    -
    -
    -
    -

    Online

    -
    - )} -
    -
    -
  • - ))} -
+

Online

+ + )} + +