Skip to main content

max / makenotwork

Fix mnw-cli TUI: alternate screen, terminal init, cleanup - Enter alternate screen buffer and hide cursor on TUI launch - Use Terminal::with_options(Fixed viewport) instead of Terminal::new to avoid crossterm ioctl size query on SSH pipe (EAGAIN) - Add cleanup (restore cursor, leave alternate screen) on all exit paths - Add channel_success response to shell_request Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-22 23:18 UTC
Commit: 3297262b7a8f26a34199c234516631d396a2b369
Parent: 021553e
2 files changed, +35 insertions, -7 deletions
@@ -180,7 +180,8 @@ impl russh::server::Handler for MnwHandler {
180 180
181 181 let user_clone = user.clone();
182 182 let staging_dir = staging::user_staging_dir(&self.staging_dir, &user.user_id);
183 - let app_handle = tui::launch(
183 +
184 + match tui::launch(
184 185 terminal_handle,
185 186 user_clone,
186 187 cols,
@@ -189,9 +190,16 @@ impl russh::server::Handler for MnwHandler {
189 190 channel,
190 191 self.api.clone(),
191 192 staging_dir,
192 - )?;
193 - self.app = Some(app_handle);
194 - session.channel_success(channel)?;
193 + ) {
194 + Ok(app_handle) => {
195 + self.app = Some(app_handle);
196 + session.channel_success(channel)?;
197 + }
198 + Err(e) => {
199 + tracing::error!(error = ?e, "TUI launch failed");
200 + session.close(channel)?;
201 + }
202 + }
195 203
196 204 Ok(())
197 205 }
@@ -341,9 +341,16 @@ pub fn launch(
341 341 api: MnwApiClient,
342 342 staging_dir: PathBuf,
343 343 ) -> anyhow::Result<AppHandle> {
344 + let mut writer = writer;
345 + // Enter alternate screen, hide cursor, enable raw-like mode
346 + use std::io::Write;
347 + let _ = writer.write_all(b"\x1b[?1049h\x1b[?25l\x1b[2J\x1b[H");
348 + let _ = writer.flush();
344 349 let backend = CrosstermBackend::new(writer);
345 - let mut terminal = Terminal::new(backend)?;
346 - terminal.resize(ratatui::layout::Rect::new(0, 0, cols, rows))?;
350 + let options = ratatui::TerminalOptions {
351 + viewport: ratatui::Viewport::Fixed(ratatui::layout::Rect::new(0, 0, cols, rows)),
352 + };
353 + let mut terminal = Terminal::with_options(backend, options)?;
347 354
348 355 let (tx, mut rx) = mpsc::channel::<AppEvent>(64);
349 356 let handle = AppHandle { tx: tx.clone() };
@@ -361,9 +368,18 @@ pub fn launch(
361 368 let mut screen = Screen::Home;
362 369 let staging_dir = staging_dir;
363 370
371 + /// Write escape codes to leave alternate screen and restore cursor.
372 + fn cleanup(terminal: &mut Terminal<CrosstermBackend<TerminalHandle>>) {
373 + use std::io::Write;
374 + let be = terminal.backend_mut();
375 + let _ = be.write_all(b"\x1b[?25h\x1b[?1049l");
376 + let _ = be.flush();
377 + }
378 +
364 379 // Initial render (loading state)
365 380 if let Err(e) = terminal.draw(|frame| home::render(frame, &app)) {
366 - tracing::error!(error = ?e, "initial render failed");
381 + tracing::error!(error = ?e, "TUI: initial render failed");
382 + cleanup(&mut terminal);
367 383 return;
368 384 }
369 385
@@ -380,6 +396,7 @@ pub fn launch(
380 396 )
381 397 {
382 398 tracing::info!(user = %app.user.username, "user quit");
399 + cleanup(&mut terminal);
383 400 let _ = session_handle.close(channel_id).await;
384 401 return;
385 402 }
@@ -640,9 +657,12 @@ pub fn launch(
640 657 Screen::Settings => settings::render(frame, &app),
641 658 }) {
642 659 tracing::error!(error = ?e, "render failed");
660 + cleanup(&mut terminal);
643 661 return;
644 662 }
645 663 }
664 + // Event loop ended (channel dropped)
665 + cleanup(&mut terminal);
646 666 });
647 667
648 668 Ok(handle)