Skip to content

Commit

Permalink
feat: add unstable controller interface
Browse files Browse the repository at this point in the history
Add a controller that handles the lifecycle of a process being spawned by
a pseudoterminal.

This is an experimental interface that currently is gated behind the
`unstable` feature flag.

Feedback is appreciated.
  • Loading branch information
a-kenji committed Jan 13, 2024
1 parent 24b7c88 commit b4580ed
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@ categories = ["command-line-interface", "command-line-utilities"]
# "bench",
# ]

[features]
unstable = ["dep:portable-pty"]

[dependencies]
ratatui = "0.25.0"
vt100 = "0.15.2"
portable-pty = { version = "0.8.1", optional = true }

[dev-dependencies]
bytes = "1.5.0"
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ Check out the examples directory, for more information, or run an example:
cargo run --example simple_ls_rw
```


## Controller

The controller is an `experimental` feature helping with managing the lifecycle of commands that are spawned inside a pseudoterminal.
Currently the support is limited to oneshot commands.

To activate the feature:
```
cargo add tui-term -F unstable
```

## Chat Room
Join our matrix chat room, for possibly synchronous communication.

Expand Down
12 changes: 12 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ It demonstrates how to send messages from one thread to another to update the `P
Uses a `RWLock` to manage shared read/write access.
The RWLock ensures that multiple threads can read from the pseudoterminal simultaneously, while exclusive write access is granted to only one thread at a time.

## `simple_ls_controller`

- Required: `ls`

Uses the tui-term's controller to handle the command lifecycle.
This feature is gated behind the `unstable` flag.
Run it with:
```
cargo run --example simple_ls_controller --features unstable
```


## `nested_shell`

- Description: Demonstrates nested shell functionality.
Expand Down
113 changes: 113 additions & 0 deletions examples/simple_ls_controller.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use std::io::{self, BufWriter};

use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use portable_pty::CommandBuilder;
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::Alignment,
style::{Modifier, Style},
text::Line,
widgets::{Block, Borders, Paragraph},
Frame, Terminal,
};
use tui_term::{controller::Controller, widget::PseudoTerminal};
use vt100::Screen;

fn main() -> std::io::Result<()> {
let (mut terminal, size) = setup_terminal().unwrap();

// Subtract the borders from the size
let size = tui_term::controller::Size::new(size.cols - 2, size.rows, 0, 0);

let mut cmd = CommandBuilder::new("ls");
if let Ok(cwd) = std::env::current_dir() {
cmd.cwd(cwd);
}

let mut controller = Controller::new(cmd, Some(size));
controller.run();
let screen = controller.screen();

run(&mut terminal, screen)?;

cleanup_terminal(&mut terminal).unwrap();
Ok(())
}

fn run<B: Backend>(terminal: &mut Terminal<B>, screen: Option<vt100::Screen>) -> io::Result<()> {
loop {
if let Some(ref screen) = screen {
terminal.draw(|f| ui(f, &screen))?;
}

if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
if let KeyCode::Char('q') = key.code {
return 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 title = Line::from("[ Running: ls ]");
let block = Block::default()
.borders(Borders::ALL)
.title(title)
.style(Style::default().add_modifier(Modifier::BOLD));
let pseudo_term = PseudoTerminal::new(screen)
.cursor(tui_term::widget::Cursor::default().visibility(false))
.block(block.clone());
f.render_widget(pseudo_term, chunks[0]);
let explanation = "Press q to exit";
let explanation = Paragraph::new(explanation)
.style(Style::default().add_modifier(Modifier::BOLD | Modifier::REVERSED))
.alignment(Alignment::Center);
f.render_widget(explanation, chunks[1]);
}

fn setup_terminal() -> io::Result<(Terminal<CrosstermBackend<BufWriter<io::Stdout>>>, Size)> {
enable_raw_mode()?;
let stdout = io::stdout();
let backend = CrosstermBackend::new(BufWriter::new(stdout));
let mut terminal = Terminal::new(backend)?;
let initial_size = terminal.size()?;
let size = Size {
rows: initial_size.height,
cols: initial_size.width,
};
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
Ok((terminal, size))
}

fn cleanup_terminal(
terminal: &mut Terminal<CrosstermBackend<BufWriter<io::Stdout>>>,
) -> io::Result<()> {
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
disable_raw_mode()?;
terminal.show_cursor()?;
terminal.clear()?;
Ok(())
}

#[derive(Debug, Clone)]
struct Size {
cols: u16,
rows: u16,
}
127 changes: 127 additions & 0 deletions src/controller.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//! This is an unstable interface, and can be activatet with the following
//! feature flag: `unstable`.
//!
//! The controller aims to help manage spawning and reading processess
//! to simplify the usage of `tui-term`, with the tradeoff being less flexible.
//!
//! Please do test this interface out and submit feedback, improvements and bug reports.
//!
//!
//! Currently only oneshot commands are supported by the controller:
//! Commands like `ls`, `cat`.
//! Commands like `htop`, that are persistent still need to be handled manually,
//! please look at the examples for a better overview.
use std::{
io::Result as IoResult,
sync::{Arc, RwLock},
};

use portable_pty::{CommandBuilder, ExitStatus, PtySystem};
use vt100::{Parser, Screen};

/// Controller, in charge of command dispatch
pub struct Controller {
// Needs to be set
cmd: CommandBuilder,
size: Size,
parser: Option<Arc<RwLock<Parser>>>,
exit_status: Option<IoResult<ExitStatus>>,
}

impl Controller {
pub fn new(cmd: CommandBuilder, size: Option<Size>) -> Self {
Self {
cmd,
size: size.unwrap_or_default(),
parser: None,
exit_status: None,
}
}

/// This function is blocking while waiting for the command to end.
pub fn run(&mut self) {
let pair = self.init_pty();
let mut child = pair.slave.spawn_command(self.cmd.clone()).unwrap();
drop(pair.slave);
let mut reader = pair.master.try_clone_reader().unwrap();
let parser = Arc::new(RwLock::new(vt100::Parser::new(
self.size.rows,
self.size.cols,
0,
)));
{
let parser = parser.clone();
std::thread::spawn(move || {
// Consume the output from the child
let mut s = String::new();
reader.read_to_string(&mut s).unwrap();
if !s.is_empty() {
let mut parser = parser.write().unwrap();
parser.process(s.as_bytes());
}
});
}
// Wait for the child to complete
self.exit_status = Some(child.wait());
// Drop writer on purpose
let _writer = pair.master.take_writer().unwrap();

drop(pair.master);
self.parser = Some(parser);
}

fn init_pty(&self) -> portable_pty::PtyPair {
use portable_pty::{NativePtySystem, PtySize};
let pty_system = NativePtySystem::default();

pty_system
.openpty(PtySize {
rows: self.size.rows,
cols: self.size.cols,
pixel_width: self.size.pixel_width,
pixel_height: self.size.pixel_height,
})
.unwrap()
}

pub fn screen(&self) -> Option<Screen> {
if let Some(parser) = &self.parser {
// We convert the read error into an option, since we might call
// the read multiple times, but we only care that we can read at some point
let binding = parser.read().ok()?;
Some(binding.screen().clone())
} else {
None
}
}

/// Whether the command finished running
pub fn finished(&self) -> bool {
self.exit_status.is_some()
}

/// The exit status of the process
pub fn status(&self) -> Option<&IoResult<ExitStatus>> {
self.exit_status.as_ref()
}
}

#[derive(Default, Clone)]
pub struct Size {
pub cols: u16,
pub rows: u16,
pixel_width: u16,
pixel_height: u16,
}

impl Size {
pub fn new(cols: u16, rows: u16, pixel_width: u16, pixel_height: u16) -> Self {
Self {
cols,
rows,
pixel_width,
pixel_height,
}
}
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,8 @@
mod state;
pub mod widget;

#[cfg(feature = "unstable")]
pub mod controller;

/// Reexport of the vt100 crate to ensure correct version compatibility
pub use vt100;

0 comments on commit b4580ed

Please sign in to comment.