mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-28 03:05:13 +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"
|
name = "atuin-run"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
"comrak",
|
"comrak",
|
||||||
|
"crossterm",
|
||||||
"eyre",
|
"eyre",
|
||||||
"portable-pty",
|
"portable-pty",
|
||||||
|
"ratatui",
|
||||||
|
"tokio",
|
||||||
|
"tui-term",
|
||||||
"vt100",
|
"vt100",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4561,6 +4566,16 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
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]]
|
[[package]]
|
||||||
name = "typed-arena"
|
name = "typed-arena"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
|
@ -40,7 +40,9 @@ typed-builder = "0.18.2"
|
|||||||
pretty_assertions = "1.3.0"
|
pretty_assertions = "1.3.0"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
rustix = { version = "0.38.34", features = ["process", "fs"] }
|
rustix = { version = "0.38.34", features = ["process", "fs"] }
|
||||||
|
crossterm = { version = "0.27", features = ["use-dev-tty"] }
|
||||||
tower = "0.4"
|
tower = "0.4"
|
||||||
|
ratatui = "0.26"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|
||||||
[workspace.dependencies.tracing-subscriber]
|
[workspace.dependencies.tracing-subscriber]
|
||||||
|
@ -13,6 +13,11 @@ readme.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
eyre.workspace = true
|
eyre.workspace = true
|
||||||
|
ratatui.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
crossterm.workspace = true
|
||||||
comrak = "0.22"
|
comrak = "0.22"
|
||||||
portable-pty = "0.8.1"
|
portable-pty = "0.8.1"
|
||||||
vt100 = "0.15.2"
|
vt100 = "0.15.2"
|
||||||
|
tui-term = "0.1.10"
|
||||||
|
bytes = "1.6.0"
|
||||||
|
@ -4,7 +4,7 @@ use comrak::{
|
|||||||
parse_document, Arena, ComrakOptions,
|
parse_document, Arena, ComrakOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Block {
|
pub struct Block {
|
||||||
pub info: String,
|
pub info: String,
|
||||||
pub code: String,
|
pub code: String,
|
||||||
|
@ -1,96 +1,178 @@
|
|||||||
/// Create and manage pseudoterminals
|
/// 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 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;
|
use vt100::Screen;
|
||||||
|
|
||||||
/// Run a command in a pty, return output. Pty is closed once the command has completed.
|
#[derive(Debug)]
|
||||||
/// If a child process would work, prefer that approach - this is a bit slower and heavier.
|
struct Size {
|
||||||
pub fn run_pty() -> Result<String> {
|
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 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
|
let pair = pty_system
|
||||||
.openpty(PtySize {
|
.openpty(PtySize {
|
||||||
rows: 24,
|
rows: size.rows,
|
||||||
cols: 80,
|
cols: size.cols,
|
||||||
pixel_width: 0,
|
pixel_width: 0,
|
||||||
pixel_height: 0,
|
pixel_height: 0,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
// Wait for the child to complete
|
||||||
let cmd = CommandBuilder::new("bash");
|
task::spawn_blocking(move || {
|
||||||
let mut child = pair.slave.spawn_command(cmd).unwrap();
|
let mut child = pair.slave.spawn_command(cmd).unwrap();
|
||||||
|
let _child_exit_status = child.wait().unwrap();
|
||||||
// Release any handles owned by the slave: we don't need it now
|
|
||||||
// that we've spawned the child.
|
|
||||||
drop(pair.slave);
|
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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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.
|
let parser = parser.clone();
|
||||||
// When the writer is dropped, EOF will be sent to
|
task::spawn_blocking(move || {
|
||||||
// the program that was spawned.
|
// Consume the output from the child
|
||||||
// It is important to take the writer even if you don't
|
// Can't read the full buffer, since that would wait for EOF
|
||||||
// send anything to its stdin so that EOF can be
|
let mut buf = [0u8; 8192];
|
||||||
// generated, otherwise you risk deadlocking yourself.
|
let mut processed_buf = Vec::new();
|
||||||
let mut writer = pair.master.take_writer().unwrap();
|
loop {
|
||||||
|
let size = reader.read(&mut buf).unwrap();
|
||||||
if cfg!(target_os = "macos") {
|
if size == 0 {
|
||||||
// macOS quirk: the child and reader must be started and
|
break;
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
|
if size > 0 {
|
||||||
|
processed_buf.extend_from_slice(&buf[..size]);
|
||||||
|
let mut parser = parser.write().unwrap();
|
||||||
|
parser.process(&processed_buf);
|
||||||
|
|
||||||
// This example doesn't need to write anything, but if you
|
// Clear the processed portion of the buffer
|
||||||
// want to send data to the child, you'd set `to_write` to
|
processed_buf.clear();
|
||||||
// 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();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the child to complete
|
|
||||||
println!("child status: {:?}", child.wait().unwrap());
|
|
||||||
|
|
||||||
// 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);
|
drop(pair.master);
|
||||||
|
});
|
||||||
|
|
||||||
// Now wait for the output to be read by our reader thread
|
println!("{blocks:?}");
|
||||||
let output = rx.recv().unwrap();
|
run(&mut terminal, parser, tx, blocks).await?;
|
||||||
|
|
||||||
// We print with escapes escaped because the windows conpty
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
// 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())
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal.draw(|f| ui(f, parser.read().unwrap().screen()))?;
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(1500)).await;
|
||||||
|
|
||||||
|
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-daemon = { path = "../atuin-daemon", version = "0.1.0" }
|
||||||
atuin-run = { path = "../atuin-run", version = "0.1.0" }
|
atuin-run = { path = "../atuin-run", version = "0.1.0" }
|
||||||
|
|
||||||
|
ratatui.workspace = true
|
||||||
|
crossterm.workspace = true
|
||||||
|
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
env_logger = "0.11.2"
|
env_logger = "0.11.2"
|
||||||
time = { workspace = true }
|
time = { workspace = true }
|
||||||
@ -59,7 +62,6 @@ directories = { workspace = true }
|
|||||||
indicatif = "0.17.5"
|
indicatif = "0.17.5"
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
crossterm = { version = "0.27", features = ["use-dev-tty"] }
|
|
||||||
unicode-width = "0.1"
|
unicode-width = "0.1"
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
@ -79,7 +81,6 @@ tiny-bip39 = "1"
|
|||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
fuzzy-matcher = "0.3.7"
|
fuzzy-matcher = "0.3.7"
|
||||||
colored = "2.0.4"
|
colored = "2.0.4"
|
||||||
ratatui = "0.26"
|
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
|
@ -82,8 +82,8 @@ pub enum Cmd {
|
|||||||
Daemon,
|
Daemon,
|
||||||
|
|
||||||
/// Execute a runbook or workflow
|
/// Execute a runbook or workflow
|
||||||
#[command()]
|
#[command(subcommand)]
|
||||||
Run,
|
Run(run::Cmd),
|
||||||
|
|
||||||
/// Print example configuration
|
/// Print example configuration
|
||||||
#[command()]
|
#[command()]
|
||||||
@ -155,7 +155,7 @@ impl Cmd {
|
|||||||
|
|
||||||
Self::Doctor => doctor::run(&settings).await,
|
Self::Doctor => doctor::run(&settings).await,
|
||||||
|
|
||||||
Self::Run => run::run(),
|
Self::Run(r) => r.run().await,
|
||||||
|
|
||||||
Self::DefaultConfig => {
|
Self::DefaultConfig => {
|
||||||
default_config::run();
|
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 atuin_run::{markdown::parse, pty::run_pty};
|
||||||
|
use rustix::path::Arg;
|
||||||
|
|
||||||
pub fn run() -> Result<()> {
|
#[derive(Debug, Subcommand)]
|
||||||
let blocks = parse(
|
pub enum Cmd {
|
||||||
"
|
Markdown { path: String },
|
||||||
1. do a thing
|
}
|
||||||
```sh
|
|
||||||
echo 'foo'
|
|
||||||
```
|
|
||||||
|
|
||||||
2. do another thing
|
impl Cmd {
|
||||||
```sh
|
pub async fn run(&self) -> Result<()> {
|
||||||
echo 'bar'
|
match self {
|
||||||
```
|
Cmd::Markdown { path } => {
|
||||||
",
|
let file = PathBuf::from(path);
|
||||||
);
|
|
||||||
|
|
||||||
println!("{:?}", blocks);
|
if !file.exists() {
|
||||||
|
return Err(eyre!("File does not exist at {path}"));
|
||||||
|
}
|
||||||
|
|
||||||
run_pty();
|
let md = tokio::fs::read_to_string(file).await?;
|
||||||
|
let blocks = parse(md.as_str());
|
||||||
|
run_pty(blocks).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user