diff options
Diffstat (limited to '')
| -rw-r--r-- | ui/backend/src/db.rs | 35 | ||||
| -rw-r--r-- | ui/backend/src/install.rs | 3 | ||||
| -rw-r--r-- | ui/backend/src/main.rs | 27 | ||||
| -rw-r--r-- | ui/backend/src/pty.rs | 112 | ||||
| -rw-r--r-- | ui/backend/src/run/mod.rs | 1 | ||||
| -rw-r--r-- | ui/backend/src/run/pty.rs | 93 | ||||
| -rw-r--r-- | ui/backend/src/state.rs | 10 |
7 files changed, 279 insertions, 2 deletions
diff --git a/ui/backend/src/db.rs b/ui/backend/src/db.rs index 1015ebf1..56d422ab 100644 --- a/ui/backend/src/db.rs +++ b/ui/backend/src/db.rs @@ -174,6 +174,41 @@ impl HistoryDB { 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;"; diff --git a/ui/backend/src/install.rs b/ui/backend/src/install.rs index 43ad0c54..17896e3a 100644 --- a/ui/backend/src/install.rs +++ b/ui/backend/src/install.rs @@ -24,7 +24,8 @@ pub(crate) async fn install_cli() -> Result<(), String> { 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'}"]) + shell + .run_interactive(&["atuin --version; if ($?) {echo 'ATUIN FOUND'}"]) .map_err(|e| format!("Failed to run interactive command"))? } else { shell diff --git a/ui/backend/src/main.rs b/ui/backend/src/main.rs index 2ba67e50..7adbbbe5 100644 --- a/ui/backend/src/main.rs +++ b/ui/backend/src/main.rs @@ -1,6 +1,8 @@ // 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}; @@ -9,6 +11,9 @@ 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; @@ -168,7 +173,7 @@ async fn home_info() -> Result<HomeInfo, String> { } // Match the format that the frontend library we use expects -// All the processing in Rust, not JS. +// All the processing in Rust, not JSunwrap. // Faaaassssssst af ⚡️🦀 #[derive(Debug, serde::Serialize)] pub struct HistoryCalendarDay { @@ -215,6 +220,19 @@ async fn history_calendar() -> Result<Vec<HistoryCalendarDay>, String> { 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) +} + fn show_window(app: &AppHandle) { let windows = app.webview_windows(); @@ -228,9 +246,11 @@ fn show_window(app: &AppHandle) { fn main() { tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![ list, search, + prefix_search, global_stats, aliases, home_info, @@ -239,6 +259,10 @@ fn main() { login, register, history_calendar, + 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, @@ -254,6 +278,7 @@ fn main() { .plugin(tauri_plugin_single_instance::init(|app, args, cwd| { let _ = show_window(app); })) + .manage(state::AtuinState::default()) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/ui/backend/src/pty.rs b/ui/backend/src/pty.rs new file mode 100644 index 00000000..07857824 --- /dev/null +++ b/ui/backend/src/pty.rs @@ -0,0 +1,112 @@ +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>>>, +} + +impl Pty { + pub async fn open<'a>(rows: u16, cols: u16) -> 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 cmd = CommandBuilder::new_default_prog(); + + tokio::task::spawn_blocking(move || { + let mut child = pair.slave.spawn_command(cmd).unwrap(); + // Wait for the child to exit + let _ = child.wait().unwrap(); + + // Ensure slave is dropped + // This closes file handles, we can deadlock if this is not done correctly. + 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)), + }) + } + + 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 + } +} diff --git a/ui/backend/src/run/mod.rs b/ui/backend/src/run/mod.rs new file mode 100644 index 00000000..5ece0912 --- /dev/null +++ b/ui/backend/src/run/mod.rs @@ -0,0 +1 @@ +pub mod pty; diff --git a/ui/backend/src/run/pty.rs b/ui/backend/src/run/pty.rs new file mode 100644 index 00000000..382b45dd --- /dev/null +++ b/ui/backend/src/run/pty.rs @@ -0,0 +1,93 @@ +use eyre::{Result, WrapErr}; +use std::io::BufRead; +use std::path::PathBuf; + +use crate::state::AtuinState; +use tauri::{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>, +) -> Result<uuid::Uuid, String> { + let id = uuid::Uuid::new_v4(); + let pty = crate::pty::Pty::open(24, 80).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).unwrap(); + println!("RIP {pid:?}"); + + Ok(()) +} diff --git a/ui/backend/src/state.rs b/ui/backend/src/state.rs new file mode 100644 index 00000000..de53b4c5 --- /dev/null +++ b/ui/backend/src/state.rs @@ -0,0 +1,10 @@ +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>>, +} |
