-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add unstable controller interface
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
Showing
6 changed files
with
271 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters