//! A very limited terminal emulator in a window use log::trace; use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize}; use std::{ io::{self, BufRead, BufReader, Write}, time::Duration, }; use termit::prelude::*; use unicode_segmentation::UnicodeSegmentation; #[async_std::main] async fn main() -> io::Result<()> { env_logger::init(); let exe = std::env::args().nth(1).unwrap_or("top".to_owned()); let exe = exe.as_str(); let mut app = start(exe); let mut ui = Stack::south_east_boxes(vec![]) .add_box( Canvas::new(String::new()) .front(Color::red(true)) .bind_property(|w| w.content_mut(), |m: &mut AppState| &mut m.temu.title) .left(2) .right(2) .width(exe.graphemes(true).count() as u16) .top(1) .height(1), ) .add_box(ChildApp::default().top(1).left(2).right(2).width(90)); let mut termit = Terminal::try_system_default()? .into_termit::() .enter_raw_mode()? //.capture_mouse(true)? //.use_alternate_screen(true)? //.draw_over(true)? .handle_focus_events(true)? .enable_line_wrap(false)?; while !app.is_terminating { termit.step(&mut app, &mut ui).await?; let buff = app.reader.fill_buf().expect("read buff"); let input = std::str::from_utf8(buff); trace!("command output: {:?}", input); if buff.is_empty() { termit .refresh_time_sender() .send_blocking(Duration::from_millis(100)) .expect("refresh interval update"); } else { app.temu.write_all(buff).expect("write buff"); let consumed = buff.len(); app.reader.consume(consumed); app.temu.flush().expect("temu flush"); termit .refresh_time_sender() .send_blocking(Duration::from_millis(10)) .expect("refresh interval update"); } if let Some(_exit) = app.child.try_wait().expect("try wait for child") { app.is_terminating = true; } } let status = app.child.wait().expect("wait for child"); debug_assert!(status.success(), "child status {:?}", status); Ok(()) } struct AppState { is_terminating: bool, master: Box, child: Box, reader: Box, writer: Box, temu: TerminalEmulator, } fn start(exe: impl AsRef) -> AppState { // Use the native pty implementation for the system // Create a new pty let pair = native_pty_system() .openpty(PtySize { rows: 0, cols: 0, pixel_width: 0, pixel_height: 0, }) .expect("pty"); // Spawn a shell into the pty let exe = exe.as_ref(); let mut cmd = CommandBuilder::new(exe); cmd.env("TERM", "xterm"); cmd.env("COLORTERM", "truecolor"); AppState { is_terminating: false, child: pair.slave.spawn_command(cmd).expect(exe), reader: Box::new(BufReader::with_capacity( 1024 * 1024 * 8, pair.master.try_clone_reader().expect("reader"), )), writer: pair.master.take_writer().expect("writer"), master: pair.master, temu: TerminalEmulator::new(exe), } } #[derive(Default)] struct ChildApp; impl AnchorPlacementEnabled for ChildApp {} impl Widget for ChildApp { fn update( &mut self, model: &mut AppState, input: &Event, screen: &mut Screen, painter: &Painter, ) -> Window { if let Event::Key(event) = input { if event.keycode == KeyCode::Char('q') { //model.is_terminating = true; model.writer.write_all(b"q").expect("write q"); model.writer.flush().expect("flush temu"); } } if painter.scope().size() != model.temu.size() { model.temu.resize(painter.scope().size()); model .master .resize(PtySize { // Not all systems support pixel_width, pixel_height, // but it is good practice to set it to something // that matches the size of the selected font. cols: painter.scope().width, rows: painter.scope().height, pixel_width: painter.scope().width.saturating_mul(10), pixel_height: painter.scope().height.saturating_mul(16), }) .expect("resize pty"); } painter.copy(model.temu.symbols(), screen); painter.scope() } }