-
Notifications
You must be signed in to change notification settings - Fork 22
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
Comments
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 |
It's been a while since I was working on this so I might be misunderstanding you, but I think what you want with //! 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 termwiz = { git = "https://github.com/wez/wezterm.git", ref = "e5ac32f297cf3dd8f6ea280c130103f3cac4dddb" }
wezterm-term = { git = "https://github.com/wez/wezterm.git", ref = "e5ac32f297cf3dd8f6ea280c130103f3cac4dddb" } |
Sweet, yeah seems totally possible through use of |
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 |
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 |
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. |
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.The text was updated successfully, but these errors were encountered: