-
Notifications
You must be signed in to change notification settings - Fork 5
Description
I've reproduced this on Windows (and WSL) in various terminals: Windows Terminal, WezTerm and Alacritty.
When one enables SGRMouse (CSI ? 1006 h) and AnyEventMouse (CSI ? 1003 h) sometimes when the program exits (after unsetting those modes, flushing the terminal output and switching back to cooked mode), some of the ANSI data is printed to the terminal. Which happens to be \x1b[<51;21;15m\x1b[<51;21;16m\x1b[<51;22;16m\x1b[<51;23;16m\x1b[<51;24;16m or similar -- which are the 1003 SGR encoding for the mouse moving between 21,15 and 24,16.
If some of the events are read, then this doesn't always happen. But if you don't read any of the events, then it'll never parse them and it gets buffered from stdin and printed out after the terminal exits.
I'm not entirely sure how you can 'finish' reading the buffer after switching to the cooked mode, I guess stdin is still valid, so you could do read-to-end. I don't know how this'll work with /dev/tty or a piped ptty though.
Here's a sample program (that does not read any inputs which causes the problem to happen every time):
After running it, if you move the mouse in the terminal window it'll print the sgr stuff after the alternative screen is restored.
use std::io::Write as _;
use termina::{PlatformTerminal, Terminal};
// helpers for these maze of types
const fn set(f: termina::escape::csi::DecPrivateModeCode) -> termina::escape::csi::Csi {
termina::escape::csi::Csi::Mode(termina::escape::csi::Mode::SetDecPrivateMode(
termina::escape::csi::DecPrivateMode::Code(f),
))
}
const fn reset(f: termina::escape::csi::DecPrivateModeCode) -> termina::escape::csi::Csi {
termina::escape::csi::Csi::Mode(termina::escape::csi::Mode::ResetDecPrivateMode(
termina::escape::csi::DecPrivateMode::Code(f),
))
}
struct Context {
terminal: PlatformTerminal,
}
impl Context {
fn new() -> std::io::Result<Self> {
let mut terminal = PlatformTerminal::new()?;
terminal.enter_raw_mode()?;
terminal.set_panic_hook(move |out| Self::reset(out));
Self::initialize(&mut terminal)?;
Ok(Self { terminal })
}
fn change_mode(
terminal: &mut impl std::io::Write,
// just a way to reduce how many times these modes have to be spelled out
mode: fn(termina::escape::csi::DecPrivateModeCode) -> termina::escape::csi::Csi,
) -> std::io::Result<()> {
use termina::escape::csi::DecPrivateModeCode as Dec;
for request in [
Dec::ClearAndEnableAlternateScreen, //
Dec::AnyEventMouse,
Dec::SGRMouse,
] {
write!(terminal, "{}", mode(request))?;
// flush after each, just to be cautious (performance overhead here is irrelevant)
terminal.flush()?;
}
Ok(())
}
fn initialize(terminal: &mut impl std::io::Write) -> std::io::Result<()> {
Self::change_mode(terminal, set)
}
fn reset(terminal: &mut impl std::io::Write) {
_ = Self::change_mode(terminal, reset)
}
}
impl Drop for Context {
fn drop(&mut self) {
Self::reset(&mut self.terminal);
let _ = self.terminal.enter_cooked_mode();
}
}
fn main() {
let mut context = Context::new().unwrap();
writeln!(
&mut context.terminal,
"\x1b[1;1;Hwaiting for a second. move the mouse to see the problem"
)
.unwrap();
context.terminal.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1));
}[package]
name = "repo"
version = "0.1.0"
edition = "2024"
[dependencies]
termina = { git = "https://github.com/helix-editor/termina", rev = "8ae1d58a292775b6e409ff5b518f0fcb0092525c" }Without the EventReader calling down to Shared::poll which will be the call for WindowsEventSource::try_read which'll read from the input handle which does the buffer filling for the parser then none of the bytes are consumed, so they get presented as user-input after the program exits.
So, clearly without any EventReader pumping the message loop the bytes never get read/sunk. This can probably be classified a user error, but a subtle one. I don't really have a solid solution for this that wouldn't be fragile. (e.g. checking the strong count to see if an EventReader is cloned. but that doesn't preclude Terminal::read or Terminal::poll from ever being called. So, perhaps some documentation stating that you must consume events otherwise the terminal may not be happy (on Windows atleast)).
But, sometimes the program using termina will exit before the WaitForMultipleObjects resolves leaving some data unread and the terminal switching back to cooked mode -- which causes them to be barfed into the terminal. Its hard to reproduce this (its very much a data race).
I think a solution, on Windows atleast, would to poll ReadConsoleInputA until it returns 0 after some drop token is consumed (e.g. an Arc<AtomicBool> when Terminal drops) that is shared by the WindowsEventSource.
(A Semaphore https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsemaphorea could be used to have a third 'token' in the IOCP WaitForMultipleObjects could also be used instead of polling a bool from the rust side. But just have a drain path on cleanup isn't that performance sensitive nor does it probably need to be timed out.)