aboutsummaryrefslogtreecommitdiffstats
path: root/ui/backend/src
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@atuin.sh>2024-07-08 11:17:47 +0100
committerGitHub <noreply@github.com>2024-07-08 11:17:47 +0100
commit5b384487331eaf08031dfe438bb2affa31aafcbb (patch)
tree51904c3df8c54cbc5b7aa5832a5bae49d57f7141 /ui/backend/src
parentfeat(bash/blesh): hook into BLE_ONLOAD to resolve loading order issue (#2234) (diff)
downloadatuin-5b384487331eaf08031dfe438bb2affa31aafcbb.zip
feat(gui): runbooks that run (#2233)
* add initial runbooks frontend * fix buttons, scroll, add shell support to editor * work * some tweaks * wip - run crate * functioning executable blocks * handle resizing, killing ptys * clear properly on stop * move terminal to its own component, handle lifecycle better * fix all build issues * ffs codespelll * update lockfile * clippy is needy once more * only build pty stuff on mac/linux * vendor pty handling into desktop * update lockfile
Diffstat (limited to '')
-rw-r--r--ui/backend/src/db.rs35
-rw-r--r--ui/backend/src/install.rs3
-rw-r--r--ui/backend/src/main.rs27
-rw-r--r--ui/backend/src/pty.rs112
-rw-r--r--ui/backend/src/run/mod.rs1
-rw-r--r--ui/backend/src/run/pty.rs93
-rw-r--r--ui/backend/src/state.rs10
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>>,
+}