diff options
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 @@ - |
