mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-25 09:44:03 +01:00
feat(ui): scroll history infinitely (#1999)
* wip, history scrolls right! * wip * virtual scroll fucking worksssss * paging works :) * scroll search results now too
This commit is contained in:
parent
851e581e16
commit
754ddeaa8d
@ -1,14 +1,16 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use crossterm::style::{Color, ResetColor, SetAttribute, SetForegroundColor};
|
use crossterm::style::{Color, ResetColor, SetAttribute, SetForegroundColor};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use atuin_client::{history::History, settings::Settings};
|
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
pub struct Stats<'a> {
|
use atuin_client::{history::History, settings::Settings};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Stats {
|
||||||
pub total_commands: usize,
|
pub total_commands: usize,
|
||||||
pub unique_commands: usize,
|
pub unique_commands: usize,
|
||||||
pub top: Vec<(Vec<&'a str>, usize)>,
|
pub top: Vec<(Vec<String>, usize)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn first_non_whitespace(s: &str) -> Option<usize> {
|
fn first_non_whitespace(s: &str) -> Option<usize> {
|
||||||
@ -161,12 +163,12 @@ pub fn pretty_print(stats: Stats, ngram_size: usize) {
|
|||||||
println!("Unique commands: {}", stats.unique_commands);
|
println!("Unique commands: {}", stats.unique_commands);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compute<'a>(
|
pub fn compute(
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
history: &'a [History],
|
history: &[History],
|
||||||
count: usize,
|
count: usize,
|
||||||
ngram_size: usize,
|
ngram_size: usize,
|
||||||
) -> Option<Stats<'a>> {
|
) -> Option<Stats> {
|
||||||
let mut commands = HashSet::<&str>::with_capacity(history.len());
|
let mut commands = HashSet::<&str>::with_capacity(history.len());
|
||||||
let mut total_unignored = 0;
|
let mut total_unignored = 0;
|
||||||
let mut prefixes = HashMap::<Vec<&str>, usize>::with_capacity(history.len());
|
let mut prefixes = HashMap::<Vec<&str>, usize>::with_capacity(history.len());
|
||||||
@ -212,7 +214,10 @@ pub fn compute<'a>(
|
|||||||
Some(Stats {
|
Some(Stats {
|
||||||
unique_commands: unique,
|
unique_commands: unique,
|
||||||
total_commands: total_unignored,
|
total_commands: total_unignored,
|
||||||
top,
|
top: top
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| (t.0.into_iter().map(|s| s.to_string()).collect(), t.1))
|
||||||
|
.collect(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
132
ui/backend/Cargo.lock
generated
132
ui/backend/Cargo.lock
generated
@ -288,6 +288,36 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atuin-history"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"atuin-client",
|
||||||
|
"atuin-common",
|
||||||
|
"base64 0.21.7",
|
||||||
|
"crossterm",
|
||||||
|
"directories",
|
||||||
|
"eyre",
|
||||||
|
"fs-err",
|
||||||
|
"futures-util",
|
||||||
|
"indicatif",
|
||||||
|
"interim",
|
||||||
|
"itertools",
|
||||||
|
"log",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sysinfo",
|
||||||
|
"time",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
|
"uuid",
|
||||||
|
"whoami",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -860,6 +890,32 @@ version = "0.8.19"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.5.0",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"filedescriptor",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm_winapi"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@ -1324,6 +1380,17 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filedescriptor"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"thiserror",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "finl_unicode"
|
name = "finl_unicode"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -2602,6 +2669,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
|
"log",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
@ -4114,6 +4182,27 @@ dependencies = [
|
|||||||
"dirs",
|
"dirs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-mio"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
@ -4819,6 +4908,41 @@ dependencies = [
|
|||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin"
|
||||||
|
version = "2.0.0-beta.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6baaee0a083db1e04a1b7a3b0670d86a4d95dd2a54e7cbfb5547762b8ed098d9"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"glob",
|
||||||
|
"plist",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri-utils",
|
||||||
|
"toml 0.8.12",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-sql"
|
||||||
|
version = "2.0.0-beta.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90c17360ef831e2789aab5439c241c8d7787ebd7c4fc40540181539b151f93bb"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sqlx",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror",
|
||||||
|
"time",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.0.0-beta.11"
|
version = "2.0.0-beta.11"
|
||||||
@ -4864,16 +4988,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-utils"
|
name = "tauri-utils"
|
||||||
version = "2.0.0-beta.11"
|
version = "2.0.0-beta.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0a148adf8077e1891c8b7d1c2be90c1c8eb8c7a071c35bb8edbdfe7cd9d8e23c"
|
checksum = "d4709765385f035338ecc330f3fba753b8ee283c659c235da9768949cdb25469"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"cargo_metadata",
|
"cargo_metadata",
|
||||||
"ctor",
|
"ctor",
|
||||||
"dunce",
|
"dunce",
|
||||||
"glob",
|
"glob",
|
||||||
"heck 0.4.1",
|
"heck 0.5.0",
|
||||||
"html5ever",
|
"html5ever",
|
||||||
"infer",
|
"infer",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
@ -5316,6 +5440,7 @@ dependencies = [
|
|||||||
"atuin-client",
|
"atuin-client",
|
||||||
"atuin-common",
|
"atuin-common",
|
||||||
"atuin-dotfiles",
|
"atuin-dotfiles",
|
||||||
|
"atuin-history",
|
||||||
"eyre",
|
"eyre",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -5323,6 +5448,7 @@ dependencies = [
|
|||||||
"syntect",
|
"syntect",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-sql",
|
||||||
"time",
|
"time",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
@ -14,8 +14,8 @@ tauri-build = { version = "2.0.0-beta", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
atuin-client = { path = "../../crates/atuin-client", version = "18.2.0" }
|
atuin-client = { path = "../../crates/atuin-client", version = "18.2.0" }
|
||||||
atuin-common = { path = "../../crates/atuin-common", version = "18.2.0" }
|
atuin-common = { path = "../../crates/atuin-common", version = "18.2.0" }
|
||||||
|
|
||||||
atuin-dotfiles = { path = "../../crates/atuin-dotfiles", version = "0.2.0" }
|
atuin-dotfiles = { path = "../../crates/atuin-dotfiles", version = "0.2.0" }
|
||||||
|
atuin-history = { path = "../../crates/atuin-history", version = "0.1.0" }
|
||||||
|
|
||||||
eyre = "0.6"
|
eyre = "0.6"
|
||||||
tauri = { version = "2.0.0-beta", features = ["tray-icon"] }
|
tauri = { version = "2.0.0-beta", features = ["tray-icon"] }
|
||||||
@ -36,3 +36,7 @@ custom-protocol = ["tauri/custom-protocol"]
|
|||||||
|
|
||||||
#[lib]
|
#[lib]
|
||||||
#crate-type = ["staticlib", "cdylib", "rlib"]
|
#crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies.tauri-plugin-sql]
|
||||||
|
features = ["sqlite"] # or "postgres", or "mysql"
|
||||||
|
version = "2.0.0-beta"
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
"identifier": "migrated",
|
"identifier": "migrated",
|
||||||
"description": "permissions that were migrated from v1",
|
"description": "permissions that were migrated from v1",
|
||||||
"context": "local",
|
"context": "local",
|
||||||
"windows": [
|
"windows": ["main"],
|
||||||
"main"
|
|
||||||
],
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"path:default",
|
"path:default",
|
||||||
"event:default",
|
"event:default",
|
||||||
@ -12,13 +10,10 @@
|
|||||||
"app:default",
|
"app:default",
|
||||||
"resources:default",
|
"resources:default",
|
||||||
"menu:default",
|
"menu:default",
|
||||||
"tray:default"
|
"tray:default",
|
||||||
|
"sql:allow-load",
|
||||||
|
"sql:allow-execute",
|
||||||
|
"sql:allow-select"
|
||||||
],
|
],
|
||||||
"platforms": [
|
"platforms": ["linux", "macOS", "windows", "android", "iOS"]
|
||||||
"linux",
|
|
||||||
"macOS",
|
|
||||||
"windows",
|
|
||||||
"android",
|
|
||||||
"iOS"
|
|
||||||
]
|
|
||||||
}
|
}
|
@ -15,6 +15,7 @@ use atuin_client::{
|
|||||||
database::{Context, Database, OptFilters, Sqlite},
|
database::{Context, Database, OptFilters, Sqlite},
|
||||||
history::History,
|
history::History,
|
||||||
};
|
};
|
||||||
|
use atuin_history::stats;
|
||||||
|
|
||||||
// useful for preprocessing data for the frontend
|
// useful for preprocessing data for the frontend
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
@ -28,6 +29,7 @@ pub struct GlobalStats {
|
|||||||
pub total_history: u64,
|
pub total_history: u64,
|
||||||
|
|
||||||
pub daily: Vec<NameValue<u64>>,
|
pub daily: Vec<NameValue<u64>>,
|
||||||
|
pub stats: Option<stats::Stats>,
|
||||||
|
|
||||||
pub last_1d: u64,
|
pub last_1d: u64,
|
||||||
pub last_7d: u64,
|
pub last_7d: u64,
|
||||||
@ -55,31 +57,33 @@ pub struct UIHistory {
|
|||||||
pub host: String,
|
pub host: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_ui_history(history: History) -> UIHistory {
|
impl From<History> for UIHistory {
|
||||||
let parts: Vec<String> = history.hostname.split(':').map(str::to_string).collect();
|
fn from(history: History) -> Self {
|
||||||
|
let parts: Vec<String> = history.hostname.split(':').map(str::to_string).collect();
|
||||||
|
|
||||||
let (host, user) = if parts.len() == 2 {
|
let (host, user) = if parts.len() == 2 {
|
||||||
(parts[0].clone(), parts[1].clone())
|
(parts[0].clone(), parts[1].clone())
|
||||||
} else {
|
} else {
|
||||||
("no-host".to_string(), "no-user".to_string())
|
("no-host".to_string(), "no-user".to_string())
|
||||||
};
|
};
|
||||||
|
|
||||||
let mac = format!("/Users/{}", user);
|
let mac = format!("/Users/{}", user);
|
||||||
let linux = format!("/home/{}", user);
|
let linux = format!("/home/{}", user);
|
||||||
|
|
||||||
let cwd = history.cwd.replace(mac.as_str(), "~");
|
let cwd = history.cwd.replace(mac.as_str(), "~");
|
||||||
let cwd = cwd.replace(linux.as_str(), "~");
|
let cwd = cwd.replace(linux.as_str(), "~");
|
||||||
|
|
||||||
UIHistory {
|
UIHistory {
|
||||||
id: history.id.0,
|
id: history.id.0,
|
||||||
timestamp: history.timestamp.unix_timestamp_nanos(),
|
timestamp: history.timestamp.unix_timestamp_nanos(),
|
||||||
duration: history.duration,
|
duration: history.duration,
|
||||||
exit: history.exit,
|
exit: history.exit,
|
||||||
command: history.command,
|
command: history.command,
|
||||||
session: history.session,
|
session: history.session,
|
||||||
host,
|
host,
|
||||||
user,
|
user,
|
||||||
cwd,
|
cwd,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,35 +98,47 @@ impl HistoryDB {
|
|||||||
Ok(Self(sqlite))
|
Ok(Self(sqlite))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list(&self, limit: Option<usize>, unique: bool) -> Result<Vec<UIHistory>, String> {
|
pub async fn list(
|
||||||
let filters = vec![];
|
&self,
|
||||||
|
offset: Option<u64>,
|
||||||
// bit of a hack but provide an empty context
|
limit: Option<usize>,
|
||||||
// shell context makes _no sense_ in a GUI
|
) -> Result<Vec<History>, String> {
|
||||||
let context = Context {
|
let query = if let Some(limit) = limit {
|
||||||
session: "".to_string(),
|
sqlx::query("select * from history order by timestamp desc limit ?1 offset ?2")
|
||||||
cwd: "".to_string(),
|
.bind(limit as i64)
|
||||||
host_id: "".to_string(),
|
.bind(offset.unwrap_or(0) as i64)
|
||||||
hostname: "".to_string(),
|
} else {
|
||||||
git_root: None,
|
sqlx::query("select * from history order by timestamp desc")
|
||||||
};
|
};
|
||||||
|
|
||||||
let history = self
|
let history: Vec<History> = query
|
||||||
.0
|
.map(|row: SqliteRow| {
|
||||||
.list(&filters, &context, limit, unique, false)
|
History::from_db()
|
||||||
|
.id(row.get("id"))
|
||||||
|
.timestamp(
|
||||||
|
time::OffsetDateTime::from_unix_timestamp_nanos(
|
||||||
|
row.get::<i64, _>("timestamp") as i128,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.duration(row.get("duration"))
|
||||||
|
.exit(row.get("exit"))
|
||||||
|
.command(row.get("command"))
|
||||||
|
.cwd(row.get("cwd"))
|
||||||
|
.session(row.get("session"))
|
||||||
|
.hostname(row.get("hostname"))
|
||||||
|
.deleted_at(None)
|
||||||
|
.build()
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.fetch_all(&self.0.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let history = history
|
|
||||||
.into_iter()
|
|
||||||
.filter(|h| h.duration > 0)
|
|
||||||
.map(to_ui_history)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(history)
|
Ok(history)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn search(&self, query: &str) -> Result<Vec<UIHistory>, String> {
|
pub async fn search(&self, offset: Option<u64>, query: &str) -> Result<Vec<UIHistory>, String> {
|
||||||
let context = Context {
|
let context = Context {
|
||||||
session: "".to_string(),
|
session: "".to_string(),
|
||||||
cwd: "".to_string(),
|
cwd: "".to_string(),
|
||||||
@ -133,6 +149,7 @@ impl HistoryDB {
|
|||||||
|
|
||||||
let filters = OptFilters {
|
let filters = OptFilters {
|
||||||
limit: Some(200),
|
limit: Some(200),
|
||||||
|
offset: offset.map(|offset| offset as i64),
|
||||||
..OptFilters::default()
|
..OptFilters::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -151,7 +168,7 @@ impl HistoryDB {
|
|||||||
let history = history
|
let history = history
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|h| h.duration > 0)
|
.filter(|h| h.duration > 0)
|
||||||
.map(to_ui_history)
|
.map(|h| h.into())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(history)
|
Ok(history)
|
||||||
@ -189,7 +206,7 @@ impl HistoryDB {
|
|||||||
.build()
|
.build()
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.map(to_ui_history)
|
.map(|h: History| h.into())
|
||||||
.fetch_all(&self.0.pool)
|
.fetch_all(&self.0.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
@ -240,6 +257,7 @@ impl HistoryDB {
|
|||||||
last_7d: week,
|
last_7d: week,
|
||||||
last_1d: day,
|
last_1d: day,
|
||||||
daily,
|
daily,
|
||||||
|
stats: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ mod store;
|
|||||||
use atuin_client::{
|
use atuin_client::{
|
||||||
encryption, history::HISTORY_TAG, record::sqlite_store::SqliteStore, record::store::Store,
|
encryption, history::HISTORY_TAG, record::sqlite_store::SqliteStore, record::store::Store,
|
||||||
};
|
};
|
||||||
|
use atuin_history::stats;
|
||||||
use db::{GlobalStats, HistoryDB, UIHistory};
|
use db::{GlobalStats, HistoryDB, UIHistory};
|
||||||
use dotfiles::aliases::aliases;
|
use dotfiles::aliases::aliases;
|
||||||
|
|
||||||
@ -25,25 +26,30 @@ struct HomeInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn list() -> Result<Vec<UIHistory>, String> {
|
async fn list(offset: Option<u64>) -> Result<Vec<UIHistory>, String> {
|
||||||
let settings = Settings::new().map_err(|e| e.to_string())?;
|
let settings = Settings::new().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let db_path = PathBuf::from(settings.db_path.as_str());
|
let db_path = PathBuf::from(settings.db_path.as_str());
|
||||||
let db = HistoryDB::new(db_path, settings.local_timeout).await?;
|
let db = HistoryDB::new(db_path, settings.local_timeout).await?;
|
||||||
|
|
||||||
let history = db.list(Some(100), false).await?;
|
let history = db
|
||||||
|
.list(Some(offset.unwrap_or(0)), Some(100))
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|h| h.into())
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(history)
|
Ok(history)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn search(query: String) -> Result<Vec<UIHistory>, String> {
|
async fn search(query: String, offset: Option<u64>) -> Result<Vec<UIHistory>, String> {
|
||||||
let settings = Settings::new().map_err(|e| e.to_string())?;
|
let settings = Settings::new().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let db_path = PathBuf::from(settings.db_path.as_str());
|
let db_path = PathBuf::from(settings.db_path.as_str());
|
||||||
let db = HistoryDB::new(db_path, settings.local_timeout).await?;
|
let db = HistoryDB::new(db_path, settings.local_timeout).await?;
|
||||||
|
|
||||||
let history = db.search(query.as_str()).await?;
|
let history = db.search(offset, query.as_str()).await?;
|
||||||
|
|
||||||
Ok(history)
|
Ok(history)
|
||||||
}
|
}
|
||||||
@ -54,11 +60,21 @@ async fn global_stats() -> Result<GlobalStats, String> {
|
|||||||
let db_path = PathBuf::from(settings.db_path.as_str());
|
let db_path = PathBuf::from(settings.db_path.as_str());
|
||||||
let db = HistoryDB::new(db_path, settings.local_timeout).await?;
|
let db = HistoryDB::new(db_path, settings.local_timeout).await?;
|
||||||
|
|
||||||
let stats = db.global_stats().await?;
|
let mut stats = db.global_stats().await?;
|
||||||
|
|
||||||
|
let history = db.list(None, None).await?;
|
||||||
|
let history_stats = stats::compute(&settings, &history, 10, 1);
|
||||||
|
|
||||||
|
stats.stats = history_stats;
|
||||||
|
|
||||||
Ok(stats)
|
Ok(stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn config() -> Result<Settings, String> {
|
||||||
|
Settings::new().map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn home_info() -> Result<HomeInfo, String> {
|
async fn home_info() -> Result<HomeInfo, String> {
|
||||||
let settings = Settings::new().map_err(|e| e.to_string())?;
|
let settings = Settings::new().map_err(|e| e.to_string())?;
|
||||||
@ -115,6 +131,7 @@ fn main() {
|
|||||||
global_stats,
|
global_stats,
|
||||||
aliases,
|
aliases,
|
||||||
home_info,
|
home_info,
|
||||||
|
config,
|
||||||
dotfiles::aliases::import_aliases,
|
dotfiles::aliases::import_aliases,
|
||||||
dotfiles::aliases::delete_alias,
|
dotfiles::aliases::delete_alias,
|
||||||
dotfiles::aliases::set_alias,
|
dotfiles::aliases::set_alias,
|
||||||
@ -122,6 +139,7 @@ fn main() {
|
|||||||
dotfiles::vars::delete_var,
|
dotfiles::vars::delete_var,
|
||||||
dotfiles::vars::set_var,
|
dotfiles::vars::set_var,
|
||||||
])
|
])
|
||||||
|
.plugin(tauri_plugin_sql::Builder::default().build())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,9 @@
|
|||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@tanstack/react-table": "^8.15.3",
|
"@tanstack/react-table": "^8.15.3",
|
||||||
|
"@tanstack/react-virtual": "^3.5.0",
|
||||||
"@tauri-apps/api": "2.0.0-beta.7",
|
"@tauri-apps/api": "2.0.0-beta.7",
|
||||||
|
"@tauri-apps/plugin-sql": "2.0.0-beta.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"core": "link:@tauri-apps/api/core",
|
"core": "link:@tauri-apps/api/core",
|
||||||
@ -27,6 +29,8 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-spinners": "^0.13.8",
|
"react-spinners": "^0.13.8",
|
||||||
|
"react-window": "^1.8.10",
|
||||||
|
"react-window-infinite-loader": "^1.0.9",
|
||||||
"recharts": "^2.12.4",
|
"recharts": "^2.12.4",
|
||||||
"tailwind-merge": "^2.2.2",
|
"tailwind-merge": "^2.2.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
@ -23,9 +23,15 @@ dependencies:
|
|||||||
'@tanstack/react-table':
|
'@tanstack/react-table':
|
||||||
specifier: ^8.15.3
|
specifier: ^8.15.3
|
||||||
version: 8.15.3(react-dom@18.2.0)(react@18.2.0)
|
version: 8.15.3(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@tanstack/react-virtual':
|
||||||
|
specifier: ^3.5.0
|
||||||
|
version: 3.5.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: 2.0.0-beta.7
|
specifier: 2.0.0-beta.7
|
||||||
version: 2.0.0-beta.7
|
version: 2.0.0-beta.7
|
||||||
|
'@tauri-apps/plugin-sql':
|
||||||
|
specifier: 2.0.0-beta.2
|
||||||
|
version: 2.0.0-beta.2
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.0
|
specifier: ^0.7.0
|
||||||
version: 0.7.0
|
version: 0.7.0
|
||||||
@ -56,6 +62,12 @@ dependencies:
|
|||||||
react-spinners:
|
react-spinners:
|
||||||
specifier: ^0.13.8
|
specifier: ^0.13.8
|
||||||
version: 0.13.8(react-dom@18.2.0)(react@18.2.0)
|
version: 0.13.8(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
react-window:
|
||||||
|
specifier: ^1.8.10
|
||||||
|
version: 1.8.10(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
react-window-infinite-loader:
|
||||||
|
specifier: ^1.0.9
|
||||||
|
version: 1.0.9(react-dom@18.2.0)(react@18.2.0)
|
||||||
recharts:
|
recharts:
|
||||||
specifier: ^2.12.4
|
specifier: ^2.12.4
|
||||||
version: 2.12.4(react-dom@18.2.0)(react@18.2.0)
|
version: 2.12.4(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -584,7 +596,7 @@ packages:
|
|||||||
react: ^16 || ^17 || ^18
|
react: ^16 || ^17 || ^18
|
||||||
react-dom: ^16 || ^17 || ^18
|
react-dom: ^16 || ^17 || ^18
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tanstack/react-virtual': 3.2.0(react-dom@18.2.0)(react@18.2.0)
|
'@tanstack/react-virtual': 3.5.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
client-only: 0.0.1
|
client-only: 0.0.1
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
@ -1300,13 +1312,13 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tanstack/react-virtual@3.2.0(react-dom@18.2.0)(react@18.2.0):
|
/@tanstack/react-virtual@3.5.0(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg==}
|
resolution: {integrity: sha512-rtvo7KwuIvqK9zb0VZ5IL7fiJAEnG+0EiFZz8FUOs+2mhGqdGmjKIaT1XU7Zq0eFqL0jonLlhbayJI/J2SA/Bw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tanstack/virtual-core': 3.2.0
|
'@tanstack/virtual-core': 3.5.0
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
@ -1316,8 +1328,13 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tanstack/virtual-core@3.2.0:
|
/@tanstack/virtual-core@3.5.0:
|
||||||
resolution: {integrity: sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==}
|
resolution: {integrity: sha512-KnPRCkQTyqhanNC0K63GBG3wA8I+D1fQuVnAvcBF8f13akOKeQp1gSbu6f77zCxhEk727iV5oQnbHLYzHrECLg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@tauri-apps/api@2.0.0-beta.4:
|
||||||
|
resolution: {integrity: sha512-Nxtj28NYUo5iwYkpYslxmOPkdI2WkELU2e3UH9nbJm9Ydki2CQwJVGQxx4EANtdZcMNsEsUzRqaDTvEUYH1l6w==}
|
||||||
|
engines: {node: '>= 18', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tauri-apps/api@2.0.0-beta.7:
|
/@tauri-apps/api@2.0.0-beta.7:
|
||||||
@ -1432,6 +1449,12 @@ packages:
|
|||||||
'@tauri-apps/cli-win32-x64-msvc': 2.0.0-beta.2
|
'@tauri-apps/cli-win32-x64-msvc': 2.0.0-beta.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@tauri-apps/plugin-sql@2.0.0-beta.2:
|
||||||
|
resolution: {integrity: sha512-gNX/4VjGl0TD4Ct58ar4bLF82iRp2L5sS79FmtzKlXYj7tVbkxenIi+mGIBz3Ut1JQP5WNL4/5wq74bkDlBggA==}
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.0.0-beta.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/babel__core@7.20.5:
|
/@types/babel__core@7.20.5:
|
||||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2107,6 +2130,10 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/memoize-one@5.2.1:
|
||||||
|
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/merge2@1.4.1:
|
/merge2@1.4.1:
|
||||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -2381,6 +2408,30 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-window-infinite-loader@1.0.9(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-5Hg89IdU4Vrp0RT8kZYKeTIxWZYhNkVXeI1HbKo01Vm/Z7qztDvXljwx16sMzsa9yapRJQW3ODZfMUw38SOWHw==}
|
||||||
|
engines: {node: '>8.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/react-window@1.8.10(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==}
|
||||||
|
engines: {node: '>8.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.24.4
|
||||||
|
memoize-one: 5.2.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react@18.2.0:
|
/react@18.2.0:
|
||||||
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -9,3 +9,19 @@ html {
|
|||||||
.logo.react:hover {
|
.logo.react:hover {
|
||||||
filter: drop-shadow(0 0 2em #61dafb);
|
filter: drop-shadow(0 0 2em #61dafb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-header {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-search {
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list {
|
||||||
|
height: calc(100vh - 150px - 64px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
height: 90px;
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
import { ChevronRightIcon } from "@heroicons/react/20/solid";
|
import { ChevronRightIcon } from "@heroicons/react/20/solid";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -19,70 +20,81 @@ function msToTime(ms: number) {
|
|||||||
|
|
||||||
export default function HistoryList(props: any) {
|
export default function HistoryList(props: any) {
|
||||||
return (
|
return (
|
||||||
<ul
|
<div
|
||||||
role="list"
|
role="list"
|
||||||
className="divide-y divide-gray-100 overflow-hidden bg-white shadow-sm ring-1 ring-gray-900/5"
|
className="divide-y divide-gray-100 bg-white shadow-sm ring-1 ring-gray-900/5 overflow-auto"
|
||||||
|
style={{
|
||||||
|
height: `${props.height}px`,
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{props.history.map((h: any) => (
|
{props.items.map((i: any) => {
|
||||||
<li
|
let h = props.history[i.index];
|
||||||
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> on </span>
|
return (
|
||||||
|
<li
|
||||||
<span className="relative truncate ">{h.host}</span>
|
key={h.id}
|
||||||
|
className="relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6"
|
||||||
<span> in </span>
|
style={{
|
||||||
|
position: "absolute",
|
||||||
<span className="relative truncate ">{h.cwd}</span>
|
top: 0,
|
||||||
</p>
|
left: 0,
|
||||||
</div>
|
width: "100%",
|
||||||
</div>
|
height: `${i.size}px`,
|
||||||
<div className="flex shrink-0 items-center gap-x-4">
|
transform: `translateY(${i.start}px)`,
|
||||||
<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 ? (
|
<div className="flex min-w-0 gap-x-4">
|
||||||
<p className="mt-1 text-xs leading-5 text-gray-500">
|
<div className="flex flex-col justify-center">
|
||||||
<time dateTime={h.duration}>
|
<p className="flex text-xs text-gray-500 justify-center">
|
||||||
{msToTime(h.duration / 1000000)}
|
{DateTime.fromMillis(h.timestamp / 1000000).toLocaleString(
|
||||||
</time>
|
DateTime.TIME_WITH_SECONDS,
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
<p className="flex text-xs mt-1 text-gray-400 justify-center">
|
||||||
<div className="mt-1 flex items-center gap-x-1.5">
|
{DateTime.fromMillis(h.timestamp / 1000000).toLocaleString(
|
||||||
<div className="flex-none rounded-full bg-emerald-500/20 p-1">
|
DateTime.DATE_SHORT,
|
||||||
<div className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
)}
|
||||||
</div>
|
</p>
|
||||||
<p className="text-xs leading-5 text-gray-500">Online</p>
|
</div>
|
||||||
</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> on </span>
|
||||||
|
|
||||||
|
<span className="relative truncate ">{h.host}</span>
|
||||||
|
|
||||||
|
<span> in </span>
|
||||||
|
|
||||||
|
<span className="relative truncate ">{h.cwd}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronRightIcon
|
<div className="flex shrink-0 items-center gap-x-4">
|
||||||
className="h-5 w-5 flex-none text-gray-400"
|
<div className="hidden sm:flex sm:flex-col sm:items-end">
|
||||||
aria-hidden="true"
|
<p className="text-sm leading-6 text-gray-900">{h.exit}</p>
|
||||||
/>
|
{h.duration ? (
|
||||||
</div>
|
<p className="mt-1 text-xs leading-5 text-gray-500">
|
||||||
</li>
|
<time dateTime={h.duration}>
|
||||||
))}
|
{msToTime(h.duration / 1000000)}
|
||||||
</ul>
|
</time>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ChevronRightIcon
|
||||||
|
className="h-5 w-5 flex-none text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,12 @@ import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
|||||||
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
||||||
|
|
||||||
interface HistorySearchProps {
|
interface HistorySearchProps {
|
||||||
refresh: (query: string) => void;
|
query: string;
|
||||||
|
refresh: () => void;
|
||||||
|
setQuery: (query: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HistorySearch(props: HistorySearchProps) {
|
export default function HistorySearch(props: HistorySearchProps) {
|
||||||
let [searchQuery, setSearchQuery] = useState("");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
|
<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
|
||||||
<form
|
<form
|
||||||
@ -35,8 +35,8 @@ export default function HistorySearch(props: HistorySearchProps) {
|
|||||||
type="search"
|
type="search"
|
||||||
name="search"
|
name="search"
|
||||||
onChange={(query) => {
|
onChange={(query) => {
|
||||||
setSearchQuery(query.target.value);
|
props.setQuery(query.target.value);
|
||||||
props.refresh(query.target.value);
|
props.refresh();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
@ -45,7 +45,7 @@ export default function HistorySearch(props: HistorySearchProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
className="-m-2.5 p-2.5 text-gray-400 hover:text-gray-500"
|
className="-m-2.5 p-2.5 text-gray-400 hover:text-gray-500"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.refresh(searchQuery);
|
props.refresh();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowPathIcon className="h-6 w-6" aria-hidden="true" />
|
<ArrowPathIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
|
@ -16,7 +16,7 @@ import { ColumnDef } from "@tanstack/react-table";
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import Drawer from "@/components/Drawer";
|
import Drawer from "@/components/Drawer";
|
||||||
|
|
||||||
import { Alias } from "@/state/models";
|
import { Alias, inspectHistory } from "@/state/models";
|
||||||
import { useStore } from "@/state/store";
|
import { useStore } from "@/state/store";
|
||||||
|
|
||||||
function deleteAlias(name: string, refreshAliases: () => void) {
|
function deleteAlias(name: string, refreshAliases: () => void) {
|
||||||
|
@ -19,12 +19,60 @@ function renderLoading() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TopTable({ stats }: any) {
|
||||||
|
console.log(stats);
|
||||||
|
return (
|
||||||
|
<div className="px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-auto">
|
||||||
|
<h1 className="text-base font-semibold">Top commands</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 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">
|
||||||
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 lg:pl-8"
|
||||||
|
>
|
||||||
|
Command
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
Count
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200 bg-white">
|
||||||
|
{stats.map((stat) => (
|
||||||
|
<tr>
|
||||||
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 lg:pl-8">
|
||||||
|
{stat[0][0]}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
|
{stat[1]}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Stats() {
|
export default function Stats() {
|
||||||
const [stats, setStats]: any = useState([]);
|
const [stats, setStats]: any = useState([]);
|
||||||
|
const [top, setTop]: any = useState([]);
|
||||||
const [chart, setChart]: any = useState([]);
|
const [chart, setChart]: any = useState([]);
|
||||||
|
|
||||||
console.log("Stats mounted");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (stats.length != 0) return;
|
if (stats.length != 0) return;
|
||||||
|
|
||||||
@ -37,6 +85,10 @@ export default function Stats() {
|
|||||||
name: "Total history",
|
name: "Total history",
|
||||||
stat: s.total_history.toLocaleString(),
|
stat: s.total_history.toLocaleString(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Unique history",
|
||||||
|
stat: s.stats.unique_commands.toLocaleString(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Last 1d",
|
name: "Last 1d",
|
||||||
stat: s.last_1d.toLocaleString(),
|
stat: s.last_1d.toLocaleString(),
|
||||||
@ -52,20 +104,23 @@ export default function Stats() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
setChart(s.daily);
|
setChart(s.daily);
|
||||||
|
|
||||||
|
setTop(s.stats);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
console.log(top);
|
||||||
|
|
||||||
if (stats.length == 0) {
|
if (stats.length == 0) {
|
||||||
return renderLoading();
|
return renderLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col overflow-y-scroll">
|
||||||
<div className="flexfull">
|
<div className="flexfull">
|
||||||
<dl className="grid grid-cols-1 sm:grid-cols-4 w-full">
|
<dl className="grid grid-cols-1 sm:grid-cols-5 w-full">
|
||||||
{stats.map((item: any) => (
|
{stats.map((item: any) => (
|
||||||
<div
|
<div
|
||||||
key={item.name}
|
key={item.name}
|
||||||
@ -94,6 +149,10 @@ export default function Stats() {
|
|||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TopTable stats={top.top} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
1
ui/src/global.d.ts
vendored
Normal file
1
ui/src/global.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
type Option<T> = T | null;
|
@ -1,11 +1,17 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
|
|
||||||
import HistoryList from "@/components/HistoryList.tsx";
|
import HistoryList from "@/components/HistoryList.tsx";
|
||||||
import HistorySearch from "@/components/HistorySearch.tsx";
|
import HistorySearch from "@/components/HistorySearch.tsx";
|
||||||
import Stats from "@/components/history/Stats.tsx";
|
import Stats from "@/components/history/Stats.tsx";
|
||||||
import Drawer from "@/components/Drawer.tsx";
|
import Drawer from "@/components/Drawer.tsx";
|
||||||
|
import InfiniteHistory from "@/components/InfiniteHistory.tsx";
|
||||||
|
|
||||||
import { useStore } from "@/state/store";
|
import { useStore } from "@/state/store";
|
||||||
|
|
||||||
|
import { inspectHistory, listHistory } from "@/state/models";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
return (
|
return (
|
||||||
<div className="md:flex md:items-center md:justify-between">
|
<div className="md:flex md:items-center md:justify-between">
|
||||||
@ -49,29 +55,64 @@ function Header() {
|
|||||||
export default function Search() {
|
export default function Search() {
|
||||||
const history = useStore((state) => state.shellHistory);
|
const history = useStore((state) => state.shellHistory);
|
||||||
const refreshHistory = useStore((state) => state.refreshShellHistory);
|
const refreshHistory = useStore((state) => state.refreshShellHistory);
|
||||||
|
const historyNextPage = useStore((state) => state.historyNextPage);
|
||||||
|
|
||||||
|
let [query, setQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
// nothing rn
|
||||||
|
})();
|
||||||
|
|
||||||
refreshHistory();
|
refreshHistory();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const parentRef = useRef();
|
||||||
|
|
||||||
|
const rowVirtualizer = useVirtualizer({
|
||||||
|
count: history.length,
|
||||||
|
getScrollElement: () => parentRef.current,
|
||||||
|
estimateSize: () => 90,
|
||||||
|
overscan: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const [lastItem] = rowVirtualizer.getVirtualItems().slice(-1);
|
||||||
|
|
||||||
|
if (!lastItem) return; // no undefined plz
|
||||||
|
if (lastItem.index < history.length - 1) return; // if we're not at the end yet, bail
|
||||||
|
|
||||||
|
// we're at the end! more rows plz!
|
||||||
|
historyNextPage(query);
|
||||||
|
}, [rowVirtualizer.getVirtualItems()]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="pl-60">
|
<div className="pl-60">
|
||||||
<div className="p-10">
|
<div className="p-10 history-header">
|
||||||
<Header />
|
<Header />
|
||||||
<p>A history of all the commands you run in your shell.</p>
|
<p>A history of all the commands you run in your shell.</p>
|
||||||
</div>
|
</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">
|
<div className="flex h-16 shrink-0 items-center gap-x-4 border-b border-t border-gray-200 bg-white px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8 history-search">
|
||||||
<HistorySearch
|
<HistorySearch
|
||||||
refresh={(query?: string) => {
|
query={query}
|
||||||
|
setQuery={(q) => {
|
||||||
|
setQuery(q);
|
||||||
|
refreshHistory(q);
|
||||||
|
}}
|
||||||
|
refresh={() => {
|
||||||
refreshHistory(query);
|
refreshHistory(query);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main>
|
<main className="overflow-y-scroll history-list" ref={parentRef}>
|
||||||
<HistoryList history={history} />
|
<HistoryList
|
||||||
|
history={history}
|
||||||
|
items={rowVirtualizer.getVirtualItems()}
|
||||||
|
height={rowVirtualizer.getTotalSize()}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import Database from "@tauri-apps/plugin-sql";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
@ -18,7 +20,7 @@ export const DefaultHomeInfo: HomeInfo = {
|
|||||||
lastSyncTime: new Date(),
|
lastSyncTime: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ShellHistory {
|
export class ShellHistory {
|
||||||
id: string;
|
id: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
command: string;
|
command: string;
|
||||||
@ -26,6 +28,24 @@ export interface ShellHistory {
|
|||||||
host: string;
|
host: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
id: string,
|
||||||
|
timestamp: number,
|
||||||
|
command: string,
|
||||||
|
user: string,
|
||||||
|
host: string,
|
||||||
|
cwd: string,
|
||||||
|
duration: number,
|
||||||
|
) {
|
||||||
|
this.id = id;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.command = command;
|
||||||
|
this.user = user;
|
||||||
|
this.host = host;
|
||||||
|
this.cwd = cwd;
|
||||||
|
this.duration = duration;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Alias {
|
export interface Alias {
|
||||||
@ -36,5 +56,15 @@ export interface Alias {
|
|||||||
export interface Var {
|
export interface Var {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
export: bool;
|
export: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function inspectHistory(id: string): Promise<any> {
|
||||||
|
const db = await Database.load(
|
||||||
|
"sqlite:/Users/ellie/.local/share/atuin/history.db",
|
||||||
|
);
|
||||||
|
|
||||||
|
let res = await db.select("select * from history where id=$1", [id]);
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
DefaultHomeInfo,
|
DefaultHomeInfo,
|
||||||
Alias,
|
Alias,
|
||||||
ShellHistory,
|
ShellHistory,
|
||||||
|
Var,
|
||||||
} from "./models";
|
} from "./models";
|
||||||
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
@ -26,9 +27,10 @@ interface AtuinState {
|
|||||||
refreshAliases: () => void;
|
refreshAliases: () => void;
|
||||||
refreshVars: () => void;
|
refreshVars: () => void;
|
||||||
refreshShellHistory: (query?: string) => void;
|
refreshShellHistory: (query?: string) => void;
|
||||||
|
historyNextPage: (query?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useStore = create<AtuinState>()((set) => ({
|
export const useStore = create<AtuinState>()((set, get) => ({
|
||||||
user: DefaultUser,
|
user: DefaultUser,
|
||||||
homeInfo: DefaultHomeInfo,
|
homeInfo: DefaultHomeInfo,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
@ -78,4 +80,23 @@ export const useStore = create<AtuinState>()((set) => ({
|
|||||||
console.log(e);
|
console.log(e);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
historyNextPage: (query?: string) => {
|
||||||
|
let history = get().shellHistory;
|
||||||
|
let offset = history.length - 1;
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
invoke("search", { query: query, offset: offset })
|
||||||
|
.then((res: any) => {
|
||||||
|
set({ shellHistory: [...history, ...res] });
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
invoke("list", { offset: offset }).then((res: any) => {
|
||||||
|
set({ shellHistory: [...history, ...res] });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -1,76 +1,76 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--primary: 222.2 47.4% 11.2%;
|
--primary: 222.2 47.4% 11.2%;
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 210 40% 96.1%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 210 40% 96.1%;
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
|
||||||
--accent: 210 40% 96.1%;
|
--accent: 210 40% 96.1%;
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--border: 214.3 31.8% 91.4%;
|
--border: 214.3 31.8% 91.4%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--input: 214.3 31.8% 91.4%;
|
||||||
--ring: 222.2 84% 4.9%;
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 222.2 84% 4.9%;
|
||||||
--foreground: 210 40% 98%;
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
--card: 222.2 84% 4.9%;
|
--card: 222.2 84% 4.9%;
|
||||||
--card-foreground: 210 40% 98%;
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--popover: 222.2 84% 4.9%;
|
--popover: 222.2 84% 4.9%;
|
||||||
--popover-foreground: 210 40% 98%;
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--primary: 210 40% 98%;
|
--primary: 210 40% 98%;
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--muted: 217.2 32.6% 17.5%;
|
--muted: 217.2 32.6% 17.5%;
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
--accent: 217.2 32.6% 17.5%;
|
--accent: 217.2 32.6% 17.5%;
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--border: 217.2 32.6% 17.5%;
|
--border: 217.2 32.6% 17.5%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--input: 217.2 32.6% 17.5%;
|
||||||
--ring: 212.7 26.8% 83.9%;
|
--ring: 212.7 26.8% 83.9%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user