Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add unstable controller interface #133

Merged
merged 1 commit into from
Jan 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
}
1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
inherit cargoArtifacts src;
partitions = 1;
partitionType = "count";
cargoExtraArgs = "--features unstable";
};
cargoDoc = craneLib.cargoDoc (commonArgs // {inherit cargoArtifacts;});

Expand Down
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 processes
//! 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;
Loading