Skip to main content

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-ByClaude 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

OldNewLine
@@ -180,7 +180,8 @@
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 @@
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
}
OldNewLine
@@ -341,9 +341,16 @@
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 @@
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 @@
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 @@
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)