diff options
| author | Ellie Huxtable <ellie@atuin.sh> | 2024-07-30 16:54:10 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-30 16:54:10 +0100 |
| commit | 808138de633e410c1d3867d4fb7cb74967647605 (patch) | |
| tree | f180b7066b91d8d8d8006219a118439be1621d74 /ui/backend/src | |
| parent | chore(deps): bump debian (#2320) (diff) | |
| download | atuin-808138de633e410c1d3867d4fb7cb74967647605.zip | |
chore: remove ui directory (#2329)
This is still in development, but rather than clutter the commit history
and issues with an unreleased project I've split the UI into its own
repo.
Once ready for release, I'll either merge the ui code back in, or just
make the repo public.
Diffstat (limited to 'ui/backend/src')
| -rw-r--r-- | ui/backend/src/db.rs | 316 | ||||
| -rw-r--r-- | ui/backend/src/dotfiles/aliases.rs | 91 | ||||
| -rw-r--r-- | ui/backend/src/dotfiles/mod.rs | 2 | ||||
| -rw-r--r-- | ui/backend/src/dotfiles/vars.rs | 57 | ||||
| -rw-r--r-- | ui/backend/src/install.rs | 73 | ||||
| -rw-r--r-- | ui/backend/src/main.rs | 329 | ||||
| -rw-r--r-- | ui/backend/src/pty.rs | 124 | ||||
| -rw-r--r-- | ui/backend/src/run/migrations.rs | 13 | ||||
| -rw-r--r-- | ui/backend/src/run/mod.rs | 2 | ||||
| -rw-r--r-- | ui/backend/src/run/pty.rs | 103 | ||||
| -rw-r--r-- | ui/backend/src/state.rs | 10 | ||||
| -rw-r--r-- | ui/backend/src/store.rs | 1 |
12 files changed, 0 insertions, 1121 deletions
diff --git a/ui/backend/src/db.rs b/ui/backend/src/db.rs deleted file mode 100644 index b08d8ebe..00000000 --- a/ui/backend/src/db.rs +++ /dev/null @@ -1,316 +0,0 @@ -// Some wrappers around the Atuin history DB -// I'll probably use this to inform changes to the "upstream" client crate -// We also use Strings a bunch for errors. They're passed to the Tauri frontend, -// which requires that they be serializable. -// Can rework that in the future too, but my main concern is avoiding tauri limitations/reqs -// ending up in the main crate. - -use serde::Serialize; -use sqlx::{sqlite::SqliteRow, Row}; -use std::collections::HashMap; -use std::path::PathBuf; - -use atuin_client::settings::{FilterMode, SearchMode}; -use atuin_client::{ - database::{Context, Database, OptFilters, Sqlite}, - history::History, -}; -use atuin_history::stats; - -// useful for preprocessing data for the frontend -#[derive(Serialize, Debug)] -pub struct NameValue<T> { - pub name: String, - pub value: T, -} - -#[derive(Serialize, Debug)] -pub struct GlobalStats { - pub total_history: u64, - - pub daily: Vec<NameValue<u64>>, - pub stats: Option<stats::Stats>, - - pub last_1d: u64, - pub last_7d: u64, - pub last_30d: u64, -} - -#[derive(Serialize, Debug)] -pub struct UIHistory { - pub id: String, - /// When the command was run. - pub timestamp: i128, - /// How long the command took to run. - pub duration: i64, - /// The exit code of the command. - pub exit: i64, - /// The command that was run. - pub command: String, - /// The current working directory when the command was run. - pub cwd: String, - /// The session ID, associated with a terminal session. - pub session: String, - /// The hostname of the machine the command was run on. - pub user: String, - - pub host: String, -} - -impl From<History> for UIHistory { - fn from(history: History) -> Self { - let parts: Vec<String> = history.hostname.split(':').map(str::to_string).collect(); - - let (host, user) = if parts.len() == 2 { - (parts[0].clone(), parts[1].clone()) - } else { - ("no-host".to_string(), "no-user".to_string()) - }; - - let mac = format!("/Users/{}", user); - let linux = format!("/home/{}", user); - - let cwd = history.cwd.replace(mac.as_str(), "~"); - let cwd = cwd.replace(linux.as_str(), "~"); - - UIHistory { - id: history.id.0, - timestamp: history.timestamp.unix_timestamp_nanos(), - duration: history.duration, - exit: history.exit, - command: history.command, - session: history.session, - host, - user, - cwd, - } - } -} - -pub struct HistoryDB(Sqlite); - -impl HistoryDB { - pub async fn new(path: PathBuf, timeout: f64) -> Result<Self, String> { - let sqlite = Sqlite::new(path, timeout) - .await - .map_err(|e| e.to_string())?; - - Ok(Self(sqlite)) - } - - pub async fn list( - &self, - offset: Option<u64>, - limit: Option<usize>, - ) -> Result<Vec<History>, String> { - let query = if let Some(limit) = limit { - sqlx::query("select * from history order by timestamp desc limit ?1 offset ?2") - .bind(limit as i64) - .bind(offset.unwrap_or(0) as i64) - } else { - sqlx::query("select * from history order by timestamp desc") - }; - - let history: Vec<History> = query - .map(|row: SqliteRow| { - 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 - .map_err(|e| e.to_string())?; - - Ok(history) - } - - pub async fn search(&self, offset: Option<u64>, query: &str) -> Result<Vec<UIHistory>, String> { - let context = Context { - session: "".to_string(), - cwd: "".to_string(), - host_id: "".to_string(), - hostname: "".to_string(), - git_root: None, - }; - - let filters = OptFilters { - limit: Some(200), - offset: offset.map(|offset| offset as i64), - ..OptFilters::default() - }; - - let history = self - .0 - .search( - SearchMode::Fuzzy, - FilterMode::Global, - &context, - query, - filters, - ) - .await - .map_err(|e| e.to_string())?; - - let history = history - .into_iter() - .filter(|h| h.duration > 0) - .map(|h| h.into()) - .collect(); - - Ok(history) - } - - pub async fn prefix_search(&self, query: &str) -> Result<Vec<UIHistory>, String> { - let context = Context { - session: "".to_string(), - cwd: "".to_string(), - host_id: "".to_string(), - hostname: "".to_string(), - git_root: None, - }; - - let filters = OptFilters { - limit: Some(5), - ..OptFilters::default() - }; - - let history = self - .0 - .search( - SearchMode::Prefix, - FilterMode::Global, - &context, - query, - filters, - ) - .await - .map_err(|e| e.to_string())?; - - let history = history - .into_iter() - .filter(|h| h.duration > 0) - .map(|h| h.into()) - .collect(); - - Ok(history) - } - - pub async fn calendar(&self) -> Result<Vec<(String, u64)>, String> { - let query = "select count(1) as count, strftime('%F', datetime(timestamp / 1000000000, 'unixepoch')) as day from history where timestamp > ((unixepoch() - 31536000) * 1000000000) group by day;"; - - let calendar: Vec<(String, u64)> = sqlx::query(query) - // safe to cast, count(x) is never < 0 - .map(|row: SqliteRow| { - ( - row.get::<String, _>("day"), - row.get::<i64, _>("count") as u64, - ) - }) - .fetch_all(&self.0.pool) - .await - .map_err(|e| e.to_string())?; - - Ok(calendar) - } - - pub async fn global_stats(&self) -> Result<GlobalStats, String> { - let day_ago = time::OffsetDateTime::now_utc() - time::Duration::days(1); - let day_ago = day_ago.unix_timestamp_nanos(); - - let week_ago = time::OffsetDateTime::now_utc() - time::Duration::days(7); - let week_ago = week_ago.unix_timestamp_nanos(); - - let month_ago = time::OffsetDateTime::now_utc() - time::Duration::days(30); - let month_ago = month_ago.unix_timestamp_nanos(); - - // get the last 30 days of shell history - let history: Vec<UIHistory> = sqlx::query("SELECT * FROM history WHERE timestamp > ?") - .bind(month_ago as i64) - .map(|row: SqliteRow| { - 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() - }) - .map(|h: History| h.into()) - .fetch_all(&self.0.pool) - .await - .map_err(|e| e.to_string())?; - - let total: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM history") - .fetch_one(&self.0.pool) - .await - .map_err(|e| e.to_string())?; - - let mut day = 0; - let mut week = 0; - let mut month = 0; - - let mut daily = HashMap::new(); - let ymd = time::format_description::parse("[year]-[month]-[day]").unwrap(); - - for i in history { - if i.timestamp > day_ago { - day += 1; - } - - if i.timestamp > week_ago { - week += 1; - } - - if i.timestamp > month_ago { - month += 1; - - // get the start of the day, as a unix timestamp - let date = time::OffsetDateTime::from_unix_timestamp_nanos(i.timestamp) - .unwrap() - .format(&ymd) - .unwrap(); - - daily.entry(date).and_modify(|v| *v += 1).or_insert(1); - } - } - - let mut daily: Vec<NameValue<u64>> = daily - .into_iter() - .map(|(k, v)| NameValue { name: k, value: v }) - .collect(); - daily.sort_by(|a, b| a.name.cmp(&b.name)); - - Ok(GlobalStats { - total_history: total.0 as u64, - last_30d: month, - last_7d: week, - last_1d: day, - daily, - stats: None, - }) - } -} diff --git a/ui/backend/src/dotfiles/aliases.rs b/ui/backend/src/dotfiles/aliases.rs deleted file mode 100644 index 972466fe..00000000 --- a/ui/backend/src/dotfiles/aliases.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::path::PathBuf; - -use atuin_client::{encryption, record::sqlite_store::SqliteStore, settings::Settings}; -use atuin_common::shell::Shell; -use atuin_dotfiles::{ - shell::{existing_aliases, Alias}, - store::AliasStore, -}; - -async fn alias_store() -> eyre::Result<AliasStore> { - let settings = Settings::new()?; - - let record_store_path = PathBuf::from(settings.record_store_path.as_str()); - let sqlite_store = SqliteStore::new(record_store_path, settings.local_timeout).await?; - - let encryption_key: [u8; 32] = encryption::load_key(&settings)?.into(); - - let host_id = Settings::host_id().expect("failed to get host_id"); - - Ok(AliasStore::new(sqlite_store, host_id, encryption_key)) -} - -#[tauri::command] -pub async fn aliases() -> Result<Vec<Alias>, String> { - let alias_store = alias_store().await.map_err(|e| e.to_string())?; - - let aliases = alias_store - .aliases() - .await - .map_err(|e| format!("failed to load aliases: {}", e))?; - - Ok(aliases) -} - -#[tauri::command] -pub async fn delete_alias(name: String) -> Result<(), String> { - let alias_store = alias_store().await.map_err(|e| e.to_string())?; - - alias_store - .delete(name.as_str()) - .await - .map_err(|e| e.to_string())?; - - Ok(()) -} - -#[tauri::command] -pub async fn set_alias(name: String, value: String) -> Result<(), String> { - let alias_store = alias_store().await.map_err(|e| e.to_string())?; - - alias_store - .set(name.as_str(), value.as_str()) - .await - .map_err(|e| e.to_string())?; - - Ok(()) -} - -#[tauri::command] -pub async fn import_aliases() -> Result<Vec<Alias>, String> { - let store = alias_store().await.map_err(|e| e.to_string())?; - let shell = Shell::default_shell().map_err(|e| e.to_string())?; - let shell_name = shell.to_string(); - - if !shell.is_posixish() { - return Err(format!( - "Default shell {shell_name} not supported for import" - )); - } - - let existing_aliases = existing_aliases(Some(shell)).map_err(|e| e.to_string())?; - let store_aliases = store.aliases().await.map_err(|e| e.to_string())?; - - let mut res = Vec::new(); - - for alias in existing_aliases { - // O(n), but n is small, and imports infrequent - // can always make a map - if store_aliases.contains(&alias) { - continue; - } - - res.push(alias.clone()); - store - .set(&alias.name, &alias.value) - .await - .map_err(|e| e.to_string())?; - } - - Ok(res) -} diff --git a/ui/backend/src/dotfiles/mod.rs b/ui/backend/src/dotfiles/mod.rs deleted file mode 100644 index feafe783..00000000 --- a/ui/backend/src/dotfiles/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod aliases; -pub mod vars; diff --git a/ui/backend/src/dotfiles/vars.rs b/ui/backend/src/dotfiles/vars.rs deleted file mode 100644 index d8d5bd75..00000000 --- a/ui/backend/src/dotfiles/vars.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::path::PathBuf; - -use atuin_client::{encryption, record::sqlite_store::SqliteStore, settings::Settings}; -use atuin_common::shell::Shell; -use atuin_dotfiles::{ - shell::{existing_aliases, Alias, Var}, - store::var::VarStore, -}; - -async fn var_store() -> eyre::Result<VarStore> { - let settings = Settings::new()?; - - let record_store_path = PathBuf::from(settings.record_store_path.as_str()); - let sqlite_store = SqliteStore::new(record_store_path, settings.local_timeout).await?; - - let encryption_key: [u8; 32] = encryption::load_key(&settings)?.into(); - - let host_id = Settings::host_id().expect("failed to get host_id"); - - Ok(VarStore::new(sqlite_store, host_id, encryption_key)) -} - -#[tauri::command] -pub async fn vars() -> Result<Vec<Var>, String> { - let var_store = var_store().await.map_err(|e| e.to_string())?; - - let vars = var_store - .vars() - .await - .map_err(|e| format!("failed to load aliases: {}", e))?; - - Ok(vars) -} - -#[tauri::command] -pub async fn delete_var(name: String) -> Result<(), String> { - let var_store = var_store().await.map_err(|e| e.to_string())?; - - var_store - .delete(name.as_str()) - .await - .map_err(|e| e.to_string())?; - - Ok(()) -} - -#[tauri::command] -pub async fn set_var(name: String, value: String, export: bool) -> Result<(), String> { - let var_store = var_store().await.map_err(|e| e.to_string())?; - - var_store - .set(name.as_str(), value.as_str(), export) - .await - .map_err(|e| e.to_string())?; - - Ok(()) -} diff --git a/ui/backend/src/install.rs b/ui/backend/src/install.rs deleted file mode 100644 index 17896e3a..00000000 --- a/ui/backend/src/install.rs +++ /dev/null @@ -1,73 +0,0 @@ -// Handle installing the Atuin CLI -// We can use the standard install script for this - -use std::process::Command; - -use tokio::{ - fs::{read_to_string, OpenOptions}, - io::AsyncWriteExt, -}; - -use atuin_common::shell::Shell; - -#[tauri::command] -pub(crate) async fn install_cli() -> Result<(), String> { - let output = Command::new("sh") - .arg("-c") - .arg("curl --proto '=https' --tlsv1.2 -LsSf https://github.com/atuinsh/atuin/releases/latest/download/atuin-installer.sh | sh") - .output().map_err(|e|format!("Failed to execute Atuin installer: {e}")); - - Ok(()) -} - -#[tauri::command] -pub(crate) async fn is_cli_installed() -> Result<bool, String> { - let shell = Shell::default_shell().map_err(|e| format!("Failed to get default shell: {e}"))?; - let output = if shell == Shell::Powershell { - shell - .run_interactive(&["atuin --version; if ($?) {echo 'ATUIN FOUND'}"]) - .map_err(|e| format!("Failed to run interactive command"))? - } else { - shell - .run_interactive(&["atuin --version && echo 'ATUIN FOUND'"]) - .map_err(|e| format!("Failed to run interactive command"))? - }; - - Ok(output.contains("ATUIN FOUND")) -} - -#[tauri::command] -pub(crate) async fn setup_cli() -> Result<(), String> { - let shell = Shell::default_shell().map_err(|e| format!("Failed to get default shell: {e}"))?; - let config_file_path = shell.config_file(); - - if config_file_path.is_none() { - return Err("Failed to fetch default config file".to_string()); - } - - let config_file_path = config_file_path.unwrap(); - let config_file = read_to_string(config_file_path.clone()) - .await - .map_err(|e| format!("Failed to read config file: {e}"))?; - - if config_file.contains("atuin init") { - return Ok(()); - } - - let mut file = OpenOptions::new() - .write(true) - .append(true) - .open(config_file_path) - .await - .unwrap(); - - let config = format!( - "if [ -x \"$(command -v atuin)\" ]; then eval \"$(atuin init {})\"; fi", - shell.to_string() - ); - file.write_all(config.as_bytes()) - .await - .map_err(|e| format!("Failed to write Atuin shell init: {e}")); - - Ok(()) -} diff --git a/ui/backend/src/main.rs b/ui/backend/src/main.rs deleted file mode 100644 index eed6bfd3..00000000 --- a/ui/backend/src/main.rs +++ /dev/null @@ -1,329 +0,0 @@ -// Prevents additional console window on Windows in release, DO NOT REMOVE!! -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -use tauri::State; - -use std::path::PathBuf; - -use tauri::{AppHandle, Manager}; -use time::format_description::well_known::Rfc3339; - -mod db; -mod dotfiles; -mod install; -mod pty; -mod run; -mod state; -mod store; - -use atuin_client::settings::Settings; -use atuin_client::{ - encryption, history::HISTORY_TAG, record::sqlite_store::SqliteStore, record::store::Store, -}; -use atuin_history::stats; -use db::{GlobalStats, HistoryDB, UIHistory}; -use dotfiles::aliases::aliases; - -#[derive(Debug, serde::Serialize)] -struct HomeInfo { - pub record_count: u64, - pub history_count: u64, - pub username: Option<String>, - pub last_sync: Option<String>, - pub top_commands: Vec<(String, u64)>, - pub recent_commands: Vec<UIHistory>, -} - -#[tauri::command] -async fn list(offset: Option<u64>) -> Result<Vec<UIHistory>, String> { - let settings = Settings::new().map_err(|e| e.to_string())?; - - let db_path = PathBuf::from(settings.db_path.as_str()); - let db = HistoryDB::new(db_path, settings.local_timeout).await?; - - let history = db - .list(Some(offset.unwrap_or(0)), Some(100)) - .await? - .into_iter() - .map(|h| h.into()) - .collect(); - - Ok(history) -} - -#[tauri::command] -async fn search(query: String, offset: Option<u64>) -> Result<Vec<UIHistory>, String> { - let settings = Settings::new().map_err(|e| e.to_string())?; - - let db_path = PathBuf::from(settings.db_path.as_str()); - let db = HistoryDB::new(db_path, settings.local_timeout).await?; - - let history = db.search(offset, query.as_str()).await?; - - Ok(history) -} - -#[tauri::command] -async fn global_stats() -> Result<GlobalStats, String> { - let settings = Settings::new().map_err(|e| e.to_string())?; - let db_path = PathBuf::from(settings.db_path.as_str()); - let db = HistoryDB::new(db_path, settings.local_timeout).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) -} - -#[tauri::command] -async fn config() -> Result<Settings, String> { - Settings::new().map_err(|e| e.to_string()) -} - -#[tauri::command] -async fn session() -> Result<String, String> { - Settings::new() - .map_err(|e| e.to_string())? - .session_token() - .map_err(|e| e.to_string()) -} - -#[tauri::command] -async fn login(username: String, password: String, key: String) -> Result<String, String> { - let settings = Settings::new().map_err(|e| e.to_string())?; - - let record_store_path = PathBuf::from(settings.record_store_path.as_str()); - let store = SqliteStore::new(record_store_path, settings.local_timeout) - .await - .map_err(|e| e.to_string())?; - - if settings.logged_in() { - return Err(String::from("Already logged in")); - } - - let session = atuin_client::login::login(&settings, &store, username, password, key) - .await - .map_err(|e| e.to_string())?; - - Ok(session) -} - -#[tauri::command] -async fn logout() -> Result<(), String> { - let settings = Settings::new().map_err(|e| e.to_string())?; - - atuin_client::logout::logout(&settings).map_err(|e| e.to_string())?; - - Ok(()) -} - -#[tauri::command] -async fn register(username: String, email: String, password: String) -> Result<String, String> { - let settings = Settings::new().map_err(|e| e.to_string())?; - - let session = atuin_client::register::register(&settings, username, email, password) - .await - .map_err(|e| e.to_string())?; - - Ok(session) -} - -#[tauri::command] -async fn home_info() -> Result<HomeInfo, String> { - 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 db_path = PathBuf::from(settings.db_path.as_str()); - let db = HistoryDB::new(db_path, settings.local_timeout).await?; - - 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 history = db.list(None, None).await?; - let stats = stats::compute(&settings, &history, 10, 1) - .map_or(vec![], |stats| stats.top[0..5].to_vec()) - .iter() - .map(|(commands, count)| (commands.join(" "), *count as u64)) - .collect(); - let recent = if history.len() > 5 { - history[0..5].to_vec() - } else { - vec![] - }; - let recent = recent.into_iter().map(|h| h.into()).collect(); - - let info = if !settings.logged_in() { - HomeInfo { - username: None, - last_sync: None, - record_count, - history_count, - top_commands: stats, - recent_commands: recent, - } - } else { - let client = atuin_client::api_client::Client::new( - &settings.sync_address, - settings - .session_token() - .map_err(|e| e.to_string())? - .as_str(), - settings.network_connect_timeout, - settings.network_timeout, - ) - .map_err(|e| e.to_string())?; - - let me = client.me().await.map_err(|e| e.to_string())?; - - HomeInfo { - username: Some(me.username), - last_sync: Some(last_sync.to_string()), - record_count, - history_count, - top_commands: stats, - recent_commands: recent, - } - }; - - Ok(info) -} - -// Match the format that the frontend library we use expects -// All the processing in Rust, not JSunwrap. -// Faaaassssssst af ⚡️🦀 -#[derive(Debug, serde::Serialize)] -pub struct HistoryCalendarDay { - pub date: String, - pub count: u64, - pub level: u8, -} - -#[tauri::command] -async fn history_calendar() -> Result<Vec<HistoryCalendarDay>, String> { - let settings = Settings::new().map_err(|e| e.to_string())?; - let db_path = PathBuf::from(settings.db_path.as_str()); - let db = HistoryDB::new(db_path, settings.local_timeout).await?; - - let calendar = db.calendar().await?; - - // probs don't want to iterate _this_ many times, but it's only the last year. so 365 - // iterations at max. should be quick. - - let max = calendar - .iter() - .max_by_key(|d| d.1) - .expect("Can't find max count"); - - let ret = calendar - .iter() - .map(|d| { - // calculate the "level". we have 5, so figure out which 5th it fits into - let percent: f64 = d.1 as f64 / max.1 as f64; - let level = if d.1 == 0 { - 0.0 - } else { - (percent / 0.2).round() + 1.0 - }; - - HistoryCalendarDay { - date: d.0.clone(), - count: d.1, - level: std::cmp::min(4, level as u8), - } - }) - .collect(); - - Ok(ret) -} - -#[tauri::command] -async fn prefix_search(query: &str) -> Result<Vec<String>, String> { - let settings = Settings::new().map_err(|e| e.to_string())?; - - let db_path = PathBuf::from(settings.db_path.as_str()); - let db = HistoryDB::new(db_path, settings.local_timeout).await?; - - let history = db.prefix_search(query).await?; - let commands = history.into_iter().map(|h| h.command).collect(); - - Ok(commands) -} - -#[tauri::command] -async fn cli_settings() -> Result<Settings, String> { - let settings = Settings::new().map_err(|e| e.to_string())?; - Ok(settings) -} - -fn show_window(app: &AppHandle) { - let windows = app.webview_windows(); - - windows - .values() - .next() - .expect("Sorry, no window found") - .set_focus() - .expect("Can't Bring Window to Focus"); -} - -fn main() { - tauri::Builder::default() - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_os::init()) - .plugin(tauri_plugin_shell::init()) - .invoke_handler(tauri::generate_handler![ - list, - search, - prefix_search, - global_stats, - aliases, - home_info, - config, - session, - login, - logout, - register, - history_calendar, - cli_settings, - run::pty::pty_open, - run::pty::pty_write, - run::pty::pty_resize, - run::pty::pty_kill, - install::install_cli, - install::is_cli_installed, - install::setup_cli, - dotfiles::aliases::import_aliases, - dotfiles::aliases::delete_alias, - dotfiles::aliases::set_alias, - dotfiles::vars::vars, - dotfiles::vars::delete_var, - dotfiles::vars::set_var, - ]) - .plugin( - tauri_plugin_sql::Builder::default() - .add_migrations("sqlite:runbooks.db", run::migrations::migrations()) - .build(), - ) - .plugin(tauri_plugin_http::init()) - .plugin(tauri_plugin_single_instance::init(|app, args, cwd| { - let _ = show_window(app); - })) - .manage(state::AtuinState::default()) - .setup(|app| Ok(())) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); -} diff --git a/ui/backend/src/pty.rs b/ui/backend/src/pty.rs deleted file mode 100644 index af394d95..00000000 --- a/ui/backend/src/pty.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::{ - io::Write, - sync::{Arc, Mutex}, -}; - -use bytes::Bytes; -use eyre::{eyre, Result}; -use portable_pty::{CommandBuilder, MasterPty, PtySize}; - -pub struct Pty { - tx: tokio::sync::mpsc::Sender<Bytes>, - - pub master: Arc<Mutex<Box<dyn MasterPty + Send>>>, - pub reader: Arc<Mutex<Box<dyn std::io::Read + Send>>>, - pub child: Arc<Mutex<Box<dyn portable_pty::Child + Send>>>, -} - -impl Pty { - pub async fn open<'a>(rows: u16, cols: u16, cwd: Option<String>) -> Result<Self> { - let sys = portable_pty::native_pty_system(); - - let pair = sys - .openpty(PtySize { - rows, - cols, - pixel_width: 0, - pixel_height: 0, - }) - .map_err(|e| eyre!("Failed to open pty: {}", e))?; - - let mut cmd = CommandBuilder::new_default_prog(); - - if let Some(cwd) = cwd { - cmd.cwd(cwd); - } - - let child = pair.slave.spawn_command(cmd).unwrap(); - drop(pair.slave); - - // Handle input -> write to master writer - let (master_tx, mut master_rx) = tokio::sync::mpsc::channel::<Bytes>(32); - - let mut writer = pair.master.take_writer().unwrap(); - let reader = pair - .master - .try_clone_reader() - .map_err(|e| e.to_string()) - .expect("Failed to clone reader"); - - tokio::spawn(async move { - while let Some(bytes) = master_rx.recv().await { - writer.write_all(&bytes).unwrap(); - writer.flush().unwrap(); - } - - // When the channel has been closed, we won't be getting any more input. Close the - // writer and the master. - // This will also close the writer, which sends EOF to the underlying shell. Ensuring - // that is also closed. - drop(writer); - }); - - Ok(Pty { - tx: master_tx, - master: Arc::new(Mutex::new(pair.master)), - reader: Arc::new(Mutex::new(reader)), - child: Arc::new(Mutex::new(child)), - }) - } - - pub async fn resize(&self, rows: u16, cols: u16) -> Result<()> { - let master = self - .master - .lock() - .map_err(|e| eyre!("Failed to lock pty master: {e}"))?; - - master - .resize(PtySize { - rows, - cols, - pixel_width: 0, - pixel_height: 0, - }) - .map_err(|e| eyre!("Failed to resize terminal: {e}"))?; - - Ok(()) - } - - pub async fn send_bytes(&self, bytes: Bytes) -> Result<()> { - self.tx - .send(bytes) - .await - .map_err(|e| eyre!("Failed to write to master tx: {}", e)) - } - - pub async fn send_string(&self, cmd: &str) -> Result<()> { - let bytes: Vec<u8> = cmd.bytes().collect(); - let bytes = Bytes::from(bytes); - - self.send_bytes(bytes).await - } - - pub async fn send_single_string(&self, cmd: &str) -> Result<()> { - let mut bytes: Vec<u8> = cmd.bytes().collect(); - bytes.push(0x04); - - let bytes = Bytes::from(bytes); - - self.send_bytes(bytes).await - } - - pub async fn kill_child(&self) -> Result<()> { - let mut child = self - .child - .lock() - .map_err(|e| eyre!("Failed to lock pty child: {e}"))?; - - child - .kill() - .map_err(|e| eyre!("Failed to kill child: {e}"))?; - - Ok(()) - } -} diff --git a/ui/backend/src/run/migrations.rs b/ui/backend/src/run/migrations.rs deleted file mode 100644 index 3516e62a..00000000 --- a/ui/backend/src/run/migrations.rs +++ /dev/null @@ -1,13 +0,0 @@ -use lazy_static::lazy_static; -use tauri_plugin_sql::{Builder, Migration, MigrationKind}; - -pub fn migrations() -> Vec<Migration> { - vec![ - Migration { - version: 1, - description: "create_initial_tables", - sql: "CREATE TABLE runbooks(id string PRIMARY KEY, name TEXT, content TEXT, created bigint, updated bigint);", - kind: MigrationKind::Up, - } - ] -} diff --git a/ui/backend/src/run/mod.rs b/ui/backend/src/run/mod.rs deleted file mode 100644 index a7a28497..00000000 --- a/ui/backend/src/run/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod migrations; -pub mod pty; diff --git a/ui/backend/src/run/pty.rs b/ui/backend/src/run/pty.rs deleted file mode 100644 index 72ca98d2..00000000 --- a/ui/backend/src/run/pty.rs +++ /dev/null @@ -1,103 +0,0 @@ -use eyre::{Result, WrapErr}; -use std::io::BufRead; -use std::path::PathBuf; - -use crate::state::AtuinState; -use tauri::{Emitter, Manager, State}; - -use atuin_client::{database::Sqlite, record::sqlite_store::SqliteStore, settings::Settings}; - -#[tauri::command] -pub async fn pty_open<'a>( - app: tauri::AppHandle, - state: State<'a, AtuinState>, - cwd: Option<String>, -) -> Result<uuid::Uuid, String> { - let id = uuid::Uuid::new_v4(); - - let cwd = cwd.map(|c| shellexpand::tilde(c.as_str()).to_string()); - let pty = crate::pty::Pty::open(24, 80, cwd).await.unwrap(); - - let reader = pty.reader.clone(); - - tauri::async_runtime::spawn_blocking(move || loop { - let mut buf = [0u8; 512]; - - match reader.lock().unwrap().read(&mut buf) { - // EOF - Ok(0) => { - println!("reader loop hit eof"); - break; - } - - Ok(n) => { - println!("read {n} bytes"); - - // TODO: sort inevitable encoding issues - let out = String::from_utf8_lossy(&buf).to_string(); - let out = out.trim_matches(char::from(0)); - let channel = format!("pty-{id}"); - - app.emit(channel.as_str(), out).unwrap(); - } - - Err(e) => { - println!("failed to read: {e}"); - break; - } - } - }); - - state.pty_sessions.write().await.insert(id, pty); - - Ok(id) -} - -#[tauri::command] -pub(crate) async fn pty_write( - pid: uuid::Uuid, - data: String, - state: tauri::State<'_, AtuinState>, -) -> Result<(), String> { - let sessions = state.pty_sessions.read().await; - let pty = sessions.get(&pid).ok_or("Pty not found")?; - - let bytes = data.as_bytes().to_vec(); - pty.send_bytes(bytes.into()) - .await - .map_err(|e| e.to_string())?; - Ok(()) -} - -#[tauri::command] -pub(crate) async fn pty_resize( - pid: uuid::Uuid, - rows: u16, - cols: u16, - state: tauri::State<'_, AtuinState>, -) -> Result<(), String> { - let sessions = state.pty_sessions.read().await; - let pty = sessions.get(&pid).ok_or("Pty not found")?; - - pty.resize(rows, cols).await.map_err(|e| e.to_string())?; - - Ok(()) -} - -#[tauri::command] -pub(crate) async fn pty_kill( - pid: uuid::Uuid, - state: tauri::State<'_, AtuinState>, -) -> Result<(), String> { - let pty = state.pty_sessions.write().await.remove(&pid); - - match pty { - Some(pty) => { - pty.kill_child().await.map_err(|e| e.to_string())?; - println!("RIP {pid:?}"); - } - None => {} - } - - Ok(()) -} diff --git a/ui/backend/src/state.rs b/ui/backend/src/state.rs deleted file mode 100644 index de53b4c5..00000000 --- a/ui/backend/src/state.rs +++ /dev/null @@ -1,10 +0,0 @@ -use std::collections::HashMap; -use std::sync::Mutex; -use tauri::async_runtime::RwLock; - -use crate::pty::Pty; - -#[derive(Default)] -pub(crate) struct AtuinState { - pub pty_sessions: RwLock<HashMap<uuid::Uuid, Pty>>, -} diff --git a/ui/backend/src/store.rs b/ui/backend/src/store.rs deleted file mode 100644 index 8b137891..00000000 --- a/ui/backend/src/store.rs +++ /dev/null @@ -1 +0,0 @@ - |
