mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-24 09:15:09 +01:00
wip
This commit is contained in:
parent
7b804f719a
commit
2b438a0972
15
Cargo.lock
generated
15
Cargo.lock
generated
@ -394,9 +394,14 @@ dependencies = [
|
||||
name = "atuin-run"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"comrak",
|
||||
"crossterm",
|
||||
"eyre",
|
||||
"portable-pty",
|
||||
"ratatui",
|
||||
"tokio",
|
||||
"tui-term",
|
||||
"vt100",
|
||||
]
|
||||
|
||||
@ -4561,6 +4566,16 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tui-term"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c276fa6f3dbd87715b2124b55e3f7479e9b5f632704a37bf6660bdb1410420d2"
|
||||
dependencies = [
|
||||
"ratatui",
|
||||
"vt100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-arena"
|
||||
version = "2.0.2"
|
||||
|
@ -40,7 +40,9 @@ typed-builder = "0.18.2"
|
||||
pretty_assertions = "1.3.0"
|
||||
thiserror = "1.0"
|
||||
rustix = { version = "0.38.34", features = ["process", "fs"] }
|
||||
crossterm = { version = "0.27", features = ["use-dev-tty"] }
|
||||
tower = "0.4"
|
||||
ratatui = "0.26"
|
||||
tracing = "0.1"
|
||||
|
||||
[workspace.dependencies.tracing-subscriber]
|
||||
|
@ -13,6 +13,11 @@ readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
eyre.workspace = true
|
||||
ratatui.workspace = true
|
||||
tokio.workspace = true
|
||||
crossterm.workspace = true
|
||||
comrak = "0.22"
|
||||
portable-pty = "0.8.1"
|
||||
vt100 = "0.15.2"
|
||||
tui-term = "0.1.10"
|
||||
bytes = "1.6.0"
|
||||
|
@ -4,7 +4,7 @@ use comrak::{
|
||||
parse_document, Arena, ComrakOptions,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Block {
|
||||
pub info: String,
|
||||
pub code: String,
|
||||
|
@ -1,96 +1,178 @@
|
||||
/// Create and manage pseudoterminals
|
||||
use eyre::Result;
|
||||
use std::{
|
||||
io::{self, BufWriter, Read, Write},
|
||||
sync::{Arc, RwLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
style::ResetColor,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem};
|
||||
use std::sync::mpsc::channel;
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::Alignment,
|
||||
style::{Modifier, Style},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use tokio::{
|
||||
sync::mpsc::{channel, Sender},
|
||||
task,
|
||||
};
|
||||
use tui_term::widget::PseudoTerminal;
|
||||
use vt100::Screen;
|
||||
|
||||
/// Run a command in a pty, return output. Pty is closed once the command has completed.
|
||||
/// If a child process would work, prefer that approach - this is a bit slower and heavier.
|
||||
pub fn run_pty() -> Result<String> {
|
||||
#[derive(Debug)]
|
||||
struct Size {
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
}
|
||||
|
||||
pub async fn run_pty(blocks: Vec<crate::markdown::Block>) -> io::Result<()> {
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, ResetColor)?;
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let pty_system = NativePtySystem::default();
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
let mut cmd = CommandBuilder::new_default_prog();
|
||||
cmd.cwd(cwd);
|
||||
|
||||
let size = Size {
|
||||
rows: terminal.size()?.height,
|
||||
cols: terminal.size()?.width,
|
||||
};
|
||||
|
||||
let pair = pty_system
|
||||
.openpty(PtySize {
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
rows: size.rows,
|
||||
cols: size.cols,
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let cmd = CommandBuilder::new("bash");
|
||||
let mut child = pair.slave.spawn_command(cmd).unwrap();
|
||||
|
||||
// Release any handles owned by the slave: we don't need it now
|
||||
// that we've spawned the child.
|
||||
drop(pair.slave);
|
||||
|
||||
// Read the output in another thread.
|
||||
// This is important because it is easy to encounter a situation
|
||||
// where read/write buffers fill and block either your process
|
||||
// or the spawned process.
|
||||
let (tx, rx) = channel();
|
||||
let mut reader = pair.master.try_clone_reader().unwrap();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// Consume the output from the child
|
||||
let mut s = String::new();
|
||||
reader.read_to_string(&mut s).unwrap();
|
||||
tx.send(s).unwrap();
|
||||
// Wait for the child to complete
|
||||
task::spawn_blocking(move || {
|
||||
let mut child = pair.slave.spawn_command(cmd).unwrap();
|
||||
let _child_exit_status = child.wait().unwrap();
|
||||
drop(pair.slave);
|
||||
});
|
||||
|
||||
let mut reader = pair.master.try_clone_reader().unwrap();
|
||||
let parser = Arc::new(RwLock::new(vt100::Parser::new(size.rows, size.cols, 0)));
|
||||
|
||||
{
|
||||
// Obtain the writer.
|
||||
// When the writer is dropped, EOF will be sent to
|
||||
// the program that was spawned.
|
||||
// It is important to take the writer even if you don't
|
||||
// send anything to its stdin so that EOF can be
|
||||
// generated, otherwise you risk deadlocking yourself.
|
||||
let mut writer = pair.master.take_writer().unwrap();
|
||||
let parser = parser.clone();
|
||||
task::spawn_blocking(move || {
|
||||
// Consume the output from the child
|
||||
// Can't read the full buffer, since that would wait for EOF
|
||||
let mut buf = [0u8; 8192];
|
||||
let mut processed_buf = Vec::new();
|
||||
loop {
|
||||
let size = reader.read(&mut buf).unwrap();
|
||||
if size == 0 {
|
||||
break;
|
||||
}
|
||||
if size > 0 {
|
||||
processed_buf.extend_from_slice(&buf[..size]);
|
||||
let mut parser = parser.write().unwrap();
|
||||
parser.process(&processed_buf);
|
||||
|
||||
if cfg!(target_os = "macos") {
|
||||
// macOS quirk: the child and reader must be started and
|
||||
// allowed a brief grace period to run before we allow
|
||||
// the writer to drop. Otherwise, the data we send to
|
||||
// the kernel to trigger EOF is interleaved with the
|
||||
// data read by the reader! WTF!?
|
||||
// This appears to be a race condition for very short
|
||||
// lived processes on macOS.
|
||||
// I'd love to find a more deterministic solution to
|
||||
// this than sleeping.
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
// Clear the processed portion of the buffer
|
||||
processed_buf.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let (tx, mut rx) = channel::<Bytes>(32);
|
||||
|
||||
let mut writer = BufWriter::new(pair.master.take_writer().unwrap());
|
||||
|
||||
// Drop writer on purpose
|
||||
tokio::spawn(async move {
|
||||
while let Some(bytes) = rx.recv().await {
|
||||
writer.write_all(&bytes).unwrap();
|
||||
writer.flush().unwrap();
|
||||
}
|
||||
drop(pair.master);
|
||||
});
|
||||
|
||||
// This example doesn't need to write anything, but if you
|
||||
// want to send data to the child, you'd set `to_write` to
|
||||
// that data and do it like this:
|
||||
let to_write = "echo 'omg the pty DID SOMETHING'\r\nexit\r\n";
|
||||
if !to_write.is_empty() {
|
||||
// To avoid deadlock, wrt. reading and waiting, we send
|
||||
// data to the stdin of the child in a different thread.
|
||||
std::thread::spawn(move || {
|
||||
writer.write_all(to_write.as_bytes()).unwrap();
|
||||
});
|
||||
println!("{blocks:?}");
|
||||
run(&mut terminal, parser, tx, blocks).await?;
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen,)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
parser: Arc<RwLock<vt100::Parser>>,
|
||||
sender: Sender<Bytes>,
|
||||
blocks: Vec<crate::markdown::Block>,
|
||||
) -> io::Result<()> {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
for i in blocks {
|
||||
terminal.draw(|f| ui(f, parser.read().unwrap().screen()))?;
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
for line in i.code.lines() {
|
||||
let b = Bytes::from(line.trim_end().to_string().into_bytes());
|
||||
|
||||
sender.send(b).await;
|
||||
sender.send(Bytes::from(vec![b'\n'])).await;
|
||||
|
||||
terminal.draw(|f| ui(f, parser.read().unwrap().screen()))?;
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the child to complete
|
||||
println!("child status: {:?}", child.wait().unwrap());
|
||||
terminal.draw(|f| ui(f, parser.read().unwrap().screen()))?;
|
||||
|
||||
// Take care to drop the master after our processes are
|
||||
// done, as some platforms get unhappy if it is dropped
|
||||
// sooner than that.
|
||||
drop(pair.master);
|
||||
tokio::time::sleep(Duration::from_millis(1500)).await;
|
||||
|
||||
// Now wait for the output to be read by our reader thread
|
||||
let output = rx.recv().unwrap();
|
||||
|
||||
// We print with escapes escaped because the windows conpty
|
||||
// implementation synthesizes title change escape sequences
|
||||
// in the output stream and it can be confusing to see those
|
||||
// printed out raw in another terminal.
|
||||
let out = output.to_string();
|
||||
println!("{out}");
|
||||
|
||||
Ok("".to_string())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, screen: &Screen) {
|
||||
let chunks = ratatui::layout::Layout::default()
|
||||
.direction(ratatui::layout::Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
ratatui::layout::Constraint::Percentage(100),
|
||||
ratatui::layout::Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().add_modifier(Modifier::BOLD));
|
||||
|
||||
let pseudo_term = PseudoTerminal::new(screen).block(block);
|
||||
f.render_widget(pseudo_term, chunks[0]);
|
||||
|
||||
let explanation = "Press q to exit".to_string();
|
||||
let explanation = Paragraph::new(explanation)
|
||||
.style(Style::default().add_modifier(Modifier::BOLD | Modifier::REVERSED))
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(explanation, chunks[1]);
|
||||
}
|
||||
|
@ -51,6 +51,9 @@ atuin-history = { path = "../atuin-history", version = "0.1.0" }
|
||||
atuin-daemon = { path = "../atuin-daemon", version = "0.1.0" }
|
||||
atuin-run = { path = "../atuin-run", version = "0.1.0" }
|
||||
|
||||
ratatui.workspace = true
|
||||
crossterm.workspace = true
|
||||
|
||||
log = { workspace = true }
|
||||
env_logger = "0.11.2"
|
||||
time = { workspace = true }
|
||||
@ -59,7 +62,6 @@ directories = { workspace = true }
|
||||
indicatif = "0.17.5"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
crossterm = { version = "0.27", features = ["use-dev-tty"] }
|
||||
unicode-width = "0.1"
|
||||
itertools = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
@ -79,7 +81,6 @@ tiny-bip39 = "1"
|
||||
futures-util = "0.3"
|
||||
fuzzy-matcher = "0.3.7"
|
||||
colored = "2.0.4"
|
||||
ratatui = "0.26"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
@ -82,8 +82,8 @@ pub enum Cmd {
|
||||
Daemon,
|
||||
|
||||
/// Execute a runbook or workflow
|
||||
#[command()]
|
||||
Run,
|
||||
#[command(subcommand)]
|
||||
Run(run::Cmd),
|
||||
|
||||
/// Print example configuration
|
||||
#[command()]
|
||||
@ -155,7 +155,7 @@ impl Cmd {
|
||||
|
||||
Self::Doctor => doctor::run(&settings).await,
|
||||
|
||||
Self::Run => run::run(),
|
||||
Self::Run(r) => r.run().await,
|
||||
|
||||
Self::DefaultConfig => {
|
||||
default_config::run();
|
||||
|
@ -1,25 +1,32 @@
|
||||
use eyre::Result;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Subcommand;
|
||||
use eyre::{eyre, Result};
|
||||
|
||||
use atuin_run::{markdown::parse, pty::run_pty};
|
||||
use rustix::path::Arg;
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
let blocks = parse(
|
||||
"
|
||||
1. do a thing
|
||||
```sh
|
||||
echo 'foo'
|
||||
```
|
||||
|
||||
2. do another thing
|
||||
```sh
|
||||
echo 'bar'
|
||||
```
|
||||
",
|
||||
);
|
||||
|
||||
println!("{:?}", blocks);
|
||||
|
||||
run_pty();
|
||||
|
||||
Ok(())
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Cmd {
|
||||
Markdown { path: String },
|
||||
}
|
||||
|
||||
impl Cmd {
|
||||
pub async fn run(&self) -> Result<()> {
|
||||
match self {
|
||||
Cmd::Markdown { path } => {
|
||||
let file = PathBuf::from(path);
|
||||
|
||||
if !file.exists() {
|
||||
return Err(eyre!("File does not exist at {path}"));
|
||||
}
|
||||
|
||||
let md = tokio::fs::read_to_string(file).await?;
|
||||
let blocks = parse(md.as_str());
|
||||
run_pty(blocks).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user