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

Termwiz instead of vt100-rust? #7

Open
tombh opened this issue May 25, 2022 · 6 comments
Open

Termwiz instead of vt100-rust? #7

tombh opened this issue May 25, 2022 · 6 comments

Comments

@tombh
Copy link

tombh commented May 25, 2022

Hey, I'm the @tombh that commented on your recent Reddit post. I guess you must have seen Wezterm if you're using portable-pty right? Well did you look at Wezterm's termwiz create too? I think it does what vt100-rust does, but better? I saw it when looking at that new(ish) Rust multiplexer Zellij. Here's an example of a nested widget in termwiz.

@bogzbonny
Copy link

AFAICT termwiz does not quite match the feature set of vt100. I did a bit of digging around and couldn't figure out a way that termwiz would be able to feed back a nice Screen object with iterable Cells which have the formatting all done up for you. It seemed possible with a combination of termwiz with wezterm-term crate - however wezterm-term is not a published crate and it seems like there isn't an intention of publishing it soon.. To anyone reading this.. If I'm wrong I'd love to know how this can be accomplished with termwiz or other available crates!

@tombh
Copy link
Author

tombh commented Dec 14, 2024

It's been a while since I was working on this so I might be misunderstanding you, but I think what you want with Screen and Cells is possible. So I'm using termwiz in a project that creates a "shadow" TTY, perhaps analogous to React's shadow DOM? It's not published yet, so here's a copypasta of the main code (my project is called Tattoy in case you see that word and wonder what it means):

//! An in-memory TTY renderer. It takes a stream of bytes and maintains the visual
//! appearance of the terminal without actually physically rendering it.

use std::sync::Arc;

use color_eyre::eyre::Result;
use termwiz::escape::parser::Parser as TermwizParser;
use termwiz::escape::Action as TermwizAction;
use termwiz::surface::Change as TermwizChange;
use termwiz::surface::Position as TermwizPosition;
use tokio::sync::mpsc;

use crate::pty::StreamBytes;
use crate::run::FrameUpdate;
use crate::run::Protocol;
use crate::shared_state::SharedState;

/// Wezterm's internal configuration
#[derive(Debug)]
struct WeztermConfig {
    /// The number of lines to store in the scrollback
    scrollback: usize,
}

#[allow(clippy::missing_trait_methods)]
impl wezterm_term::TerminalConfiguration for WeztermConfig {
    fn scrollback_size(&self) -> usize {
        self.scrollback
    }

    fn color_palette(&self) -> wezterm_term::color::ColorPalette {
        wezterm_term::color::ColorPalette::default()
    }
}

/// Private fields aren't relevant yet
pub struct ShadowTTY {
    /// The Wezterm terminal that does most of the actual work of maintaining the terminal 🙇
    terminal: wezterm_term::Terminal,
    /// Parser that detects all the weird and wonderful TTY conventions
    parser: TermwizParser,
    /// Shared app state
    state: Arc<SharedState>,
}

impl ShadowTTY {
    /// Create a new Shadow TTY
    pub fn new(state: Arc<SharedState>) -> Result<Self> {
        let tty_size = state.get_tty_size()?;

        let terminal = wezterm_term::Terminal::new(
            wezterm_term::TerminalSize {
                cols: tty_size.0,
                rows: tty_size.1,
                pixel_width: 0,
                pixel_height: 0,
                dpi: 0,
            },
            std::sync::Arc::new(WeztermConfig { scrollback: 100 }),
            "Tattoy",
            "O_o",
            Box::<Vec<u8>>::default(),
        );

        Ok(Self {
            terminal,
            parser: TermwizParser::new(),
            state,
        })
    }

    /// Start listening to a stream of PTY bytes and render them to a shadow TTY surface
    pub async fn run(
        &mut self,
        mut pty_output: mpsc::Receiver<StreamBytes>,
        shadow_output: &mpsc::Sender<FrameUpdate>,
        mut protocol_receive: tokio::sync::broadcast::Receiver<Protocol>,
    ) -> Result<()> {
        loop {
            if let Some(bytes) = pty_output.recv().await {
                self.terminal.advance_bytes(bytes);
                self.parse_bytes(bytes);
            };

            // TODO: should this be oneshot?
            if let Ok(message) = protocol_receive.try_recv() {
                match message {
                    Protocol::END => {
                        break;
                    }
                };
            }

            let (surface, surface_copy) = self.build_current_surface()?;
            self.update_state_surface(surface)?;

            shadow_output
                .send(FrameUpdate::PTYSurface(surface_copy))
                .await?;
        }

        tracing::debug!("ShadowTTY loop finished");
        Ok(())
    }

    /// Send the current PTY surface to the shared state.
    /// Needs to be in its own non-async function like this because of the error:
    ///   'future created by async block is not `Send`'
    fn update_state_surface(&self, surface: termwiz::surface::Surface) -> Result<()> {
        let mut shadow_tty = self
            .state
            .shadow_tty
            .write()
            .map_err(|err| color_eyre::eyre::eyre!("{err}"))?;
        *shadow_tty = surface;
        drop(shadow_tty);
        Ok(())
    }

    /// Parse PTY bytes
    /// Just logging for now. But we could do some Tattoy-specific things with this. Like a Tattoy
    /// keyboard shortcut that switches the active tattoy.
    fn parse_bytes(&mut self, bytes: StreamBytes) {
        #[allow(clippy::wildcard_enum_match_arm)]
        self.parser.parse(&bytes, |action| match action {
            TermwizAction::Print(character) => tracing::trace!("{character}"),
            TermwizAction::Control(character) => match character {
                termwiz::escape::ControlCode::HorizontalTab
                | termwiz::escape::ControlCode::LineFeed
                | termwiz::escape::ControlCode::CarriageReturn => {
                    tracing::trace!("{character:?}");
                }
                _ => {}
            },
            TermwizAction::CSI(csi) => {
                tracing::trace!("{csi:?}");
            }
            wild => {
                tracing::trace!("{wild:?}");
            }
        });
    }

    /// Converts Wezterms's maintained virtual TTY into a compositable Termwiz surface
    #[allow(clippy::cast_possible_wrap)]
    #[allow(clippy::cast_possible_truncation)]
    #[allow(clippy::cast_sign_loss)]
    #[allow(clippy::as_conversions)]
    fn build_current_surface(
        &mut self,
    ) -> Result<(termwiz::surface::Surface, termwiz::surface::Surface)> {
        let tty_size = self.state.get_tty_size()?;
        let width = tty_size.0;
        let height = tty_size.1;
        let mut surface1 = termwiz::surface::Surface::new(width, height);
        let mut surface2 = termwiz::surface::Surface::new(width, height);

        let screen = self.terminal.screen_mut();
        for row in 0..=height {
            for column in 0..=width {
                if let Some(cell) = screen.get_cell(column, row as i64) {
                    let attrs = cell.attrs();
                    let cursor = TermwizChange::CursorPosition {
                        x: TermwizPosition::Absolute(column),
                        y: TermwizPosition::Absolute(row),
                    };
                    surface1.add_change(cursor.clone());
                    surface2.add_change(cursor);

                    let colours = vec![
                        TermwizChange::Attribute(termwiz::cell::AttributeChange::Foreground(
                            attrs.foreground(),
                        )),
                        TermwizChange::Attribute(termwiz::cell::AttributeChange::Background(
                            attrs.background(),
                        )),
                    ];
                    surface1.add_changes(colours.clone());
                    surface2.add_changes(colours);

                    let contents = cell.str();
                    surface1.add_change(contents);
                    surface2.add_change(contents);
                }
            }
        }

        let users_cursor = self.terminal.cursor_pos();
        let cursor = TermwizChange::CursorPosition {
            x: TermwizPosition::Absolute(users_cursor.x),
            y: TermwizPosition::Absolute(users_cursor.y as usize),
        };
        surface1.add_change(cursor.clone());
        surface2.add_change(cursor);

        Ok((surface1, surface2))
    }
}

And Cargo.toml contains:

termwiz = { git = "https://github.com/wez/wezterm.git", ref = "e5ac32f297cf3dd8f6ea280c130103f3cac4dddb" }
wezterm-term = { git = "https://github.com/wez/wezterm.git", ref = "e5ac32f297cf3dd8f6ea280c130103f3cac4dddb" }

@bogzbonny
Copy link

Sweet, yeah seems totally possible through use of wezterm-term Thank you for sharing your code, this should be very useful. My only wish was that wezterm-term was published on crates but ce la vie.

@tombh
Copy link
Author

tombh commented Dec 14, 2024

Why is that I wonder? Just because then it gives the crate more guarantees of stability? That the API won't change too painfully?

Because there's no trouble including it in Cargo.toml right? wezterm-term = { git = "https://github.com/wez/wezterm.git" }

@bogzbonny
Copy link

If your project depends on an unpublished repository (such as wezterm-term) then you will not be able to publish your crate on crates.io?? maybe. See this rust-lang/cargo#6738. But also rust-lang/cargo#7237 - so I'm a bit confused on the behaviour of crates. This is my only real issue because I need to publish myself

@tombh
Copy link
Author

tombh commented Dec 15, 2024

Ohhh, I never knew that, that's a shame. So I suppose you could always just publish it yourself, but that seems a bit awkward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants