termit 0.7.0

Terminal UI over crossterm
Documentation
//! 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::<NoAppEvent>()
        .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<dyn MasterPty>,
    child: Box<dyn Child>,
    reader: Box<dyn BufRead>,
    writer: Box<dyn Write + Send>,
    temu: TerminalEmulator,
}

fn start(exe: impl AsRef<str>) -> 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<A: AppEvent> Widget<AppState, A> for ChildApp {
    fn update(
        &mut self,
        model: &mut AppState,
        input: &Event<A>,
        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()
    }
}