Skip to content

Commit 96c11e3

Browse files
committed
[readline_async] Fix multi-line output column alignment in raw mode
In raw terminal mode, LF only moves the cursor down without returning to column 1. Removing CHA(1) after ALL newline-terminated segments to avoid redundant escape sequences caused multi-line output to start at wrong columns. The fix: emit CHA(1) after every segment EXCEPT the last one when it ends with newline (avoiding redundant [LF][CHA(1)][CHA(1)] before render_and_flush while still ensuring each line starts at column 1). Changes: - Fix print_data_and_flush to emit CHA(1) between multi-line segments - Add PTY tests using OffscreenBuffer to verify rendered output: - pty_multiline_output_test: verifies lines start at column 1 - pty_shared_writer_no_blank_line_test: verifies no extra blank line before prompt - Add readline_async_multiline example for manual verification Closes #442
1 parent e295132 commit 96c11e3

File tree

8 files changed

+959
-9
lines changed

8 files changed

+959
-9
lines changed

.vscode/task-spaces.json

Lines changed: 127 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,154 @@
1010
"isPinned": true
1111
},
1212
{
13-
"path": "tui/src/readline_async/readline_async_impl/line_state/mod.rs",
13+
"path": "task/task_refactor_input_device.md",
1414
"isPinned": false
1515
},
1616
{
17-
"path": "tui/src/readline_async/readline_async_impl/line_state/core.rs",
17+
"path": "tui/src/core/ansi/vt_100_terminal_input_parser/mod.rs",
1818
"isPinned": false
1919
},
2020
{
21-
"path": "tui/src/readline_async/readline_async_impl/line_state/cursor.rs",
21+
"path": "tui/src/core/ansi/vt_100_terminal_input_parser/router.rs",
2222
"isPinned": false
2323
},
2424
{
25-
"path": "tui/src/readline_async/readline_async_impl/line_state/event_handlers.rs",
25+
"path": "tui/src/core/ansi/constants/input_sequences.rs",
2626
"isPinned": false
2727
},
2828
{
29-
"path": "tui/src/readline_async/readline_async_impl/line_state/output.rs",
29+
"path": "tui/src/core/ansi/vt_100_terminal_input_parser/test_fixtures/input_sequence_generator.rs",
3030
"isPinned": false
3131
},
3232
{
33-
"path": "tui/src/readline_async/readline_async_impl/line_state/render.rs",
33+
"path": "tui/src/core/ansi/vt_100_terminal_input_parser/ir_event_types.rs",
34+
"isPinned": false
35+
},
36+
{
37+
"path": "tui/src/core/ansi/terminal_raw_mode/raw_mode_core.rs",
38+
"isPinned": false
39+
},
40+
{
41+
"path": "tui/src/core/ansi/vt_100_terminal_input_parser/integration_tests/pty_sigwinch_test.rs",
42+
"isPinned": false
43+
},
44+
{
45+
"path": "tui/src/tui/editor/editor_engine/validate_scroll_on_resize.rs",
46+
"isPinned": false
47+
},
48+
{
49+
"path": "tui/src/tui/terminal_lib_backends/raw_mode.rs",
50+
"isPinned": false
51+
},
52+
{
53+
"path": "tui/src/core/term.rs",
54+
"isPinned": false
55+
},
56+
{
57+
"path": "tui/src/core/mod.rs",
58+
"isPinned": false
59+
},
60+
{
61+
"path": "tui/src/core/ansi/terminal_raw_mode/raw_mode_unix.rs",
62+
"isPinned": false
63+
},
64+
{
65+
"path": "tui/src/core/ansi/terminal_raw_mode/mod.rs",
66+
"isPinned": false
67+
},
68+
{
69+
"path": "tui/src/tui/terminal_lib_backends/backend_selection.rs",
70+
"isPinned": false
71+
},
72+
{
73+
"path": "tui/src/core/ansi/vt_100_terminal_input_parser/keyboard.rs",
74+
"isPinned": false
75+
},
76+
{
77+
"path": "tui/src/core/ansi/constants/raw_mode.rs",
78+
"isPinned": false
79+
},
80+
{
81+
"path": "tui/src/tui/terminal_window/main_event_loop.rs",
82+
"isPinned": false
83+
},
84+
{
85+
"path": "tui/src/tui/terminal_window/terminal_window_api.rs",
86+
"isPinned": false
87+
},
88+
{
89+
"path": "tui/examples/tui_apps/main.rs",
90+
"isPinned": false
91+
},
92+
{
93+
"path": "tui/src/readline_async/readline_async_impl/readline.rs",
94+
"isPinned": false
95+
},
96+
{
97+
"path": "tui/src/tui/mod.rs",
98+
"isPinned": false
99+
},
100+
{
101+
"path": "tui/src/core/ansi/constants/mouse.rs",
102+
"isPinned": false
103+
},
104+
{
105+
"path": "tui/src/core/ansi/constants/utf8.rs",
106+
"isPinned": false
107+
},
108+
{
109+
"path": "tui/src/core/ansi/vt_100_terminal_input_parser/mouse.rs",
110+
"isPinned": false
111+
},
112+
{
113+
"path": "tui/src/core/ansi/vt_100_terminal_input_parser/terminal_events.rs",
114+
"isPinned": false
115+
},
116+
{
117+
"path": "tui/src/core/terminal_io/input_device.rs",
118+
"isPinned": false
119+
},
120+
{
121+
"path": "tui/src/tui/terminal_lib_backends/direct_to_ansi/input/mod.rs",
122+
"isPinned": false
123+
},
124+
{
125+
"path": "tui/src/tui/terminal_lib_backends/direct_to_ansi/input/input_device.rs",
126+
"isPinned": false
127+
},
128+
{
129+
"path": "tui/src/tui/terminal_lib_backends/direct_to_ansi/mod.rs",
130+
"isPinned": false
131+
},
132+
{
133+
"path": "tui/src/tui/terminal_lib_backends/direct_to_ansi/input/protocol_conversion.rs",
134+
"isPinned": false
135+
},
136+
{
137+
"path": "tui/src/tui/terminal_lib_backends/direct_to_ansi/input/types.rs",
138+
"isPinned": false
139+
},
140+
{
141+
"path": "tui/src/tui/terminal_lib_backends/direct_to_ansi/input/paste_state_machine.rs",
142+
"isPinned": false
143+
},
144+
{
145+
"path": "tui/src/tui/terminal_lib_backends/direct_to_ansi/input/process_global_stdin.rs",
146+
"isPinned": false
147+
},
148+
{
149+
"path": "tui/src/readline_async/readline_async_impl/integration_tests/pty_multiline_output_test.rs",
150+
"isPinned": false
151+
},
152+
{
153+
"path": "tui/src/readline_async/readline_async_impl/integration_tests/pty_shared_writer_no_blank_line_test.rs",
34154
"isPinned": false
35155
}
36156
],
37157
"taskFile": "task/task_refactor_input_device.md",
38158
"createdAt": 1763496885607,
39159
"lastAccessed": 1763666457216,
40-
"activeTab": "tui/src/readline_async/readline_async_impl/line_state/mod.rs"
160+
"activeTab": "tui/src/readline_async/readline_async_impl/integration_tests/pty_shared_writer_no_blank_line_test.rs"
41161
},
42162
{
43163
"name": "[1.1] task_readline_async_add_shortcuts",
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) 2025 R3BL LLC. Licensed under Apache License, Version 2.0.
2+
3+
//! Multi-line async output example for readline.
4+
//!
5+
//! Demonstrates proper rendering of multiple log lines via [`SharedWriter`] in a
6+
//! readline REPL loop. Each line should:
7+
//! 1. Start at column 1 (proper carriage return after line feed)
8+
//! 2. Not create extra blank lines before the prompt
9+
//!
10+
//! This example validates the fix for issue #442:
11+
//! <https://github.com/r3bl-org/r3bl-open-core/issues/442>
12+
//!
13+
//! Run with: `cargo run -p r3bl_tui --example readline_async_multiline`
14+
//!
15+
//! ## Expected behavior
16+
//!
17+
//! ```text
18+
//! banner
19+
//! line 1
20+
//! line 2
21+
//! >
22+
//! ```
23+
//!
24+
//! ## Bug behavior (before fix)
25+
//!
26+
//! Lines would either:
27+
//! - Not start at column 1 (missing carriage return)
28+
//! - Have extra blank line before prompt
29+
//!
30+
//! [`SharedWriter`]: r3bl_tui::SharedWriter
31+
32+
use miette::IntoDiagnostic;
33+
use r3bl_tui::{readline_async::{ReadlineAsyncContext, ReadlineEvent}, rla_println};
34+
use std::io::Write;
35+
36+
#[tokio::main]
37+
async fn main() -> miette::Result<()> {
38+
let maybe_rl_ctx = ReadlineAsyncContext::try_new(Some("> "), None).await?;
39+
40+
let Some(mut rl_ctx) = maybe_rl_ctx else {
41+
println!("Not an interactive terminal.");
42+
return Ok(());
43+
};
44+
45+
// Get the shared writer for logging.
46+
let mut shared_writer = rl_ctx.clone_shared_writer();
47+
48+
// Print banner (simulates user code printing before REPL loop).
49+
rla_println!(rl_ctx, "banner");
50+
51+
loop {
52+
// Simulate async log output (this is where the bug manifests).
53+
writeln!(shared_writer, "line 1").into_diagnostic()?;
54+
writeln!(shared_writer, "line 2").into_diagnostic()?;
55+
56+
let event = rl_ctx.read_line().await?;
57+
58+
match event {
59+
ReadlineEvent::Line(line) => {
60+
let trimmed = line.trim();
61+
62+
if trimmed.eq_ignore_ascii_case("exit") {
63+
rla_println!(rl_ctx, "Exiting...");
64+
break;
65+
}
66+
67+
rla_println!(rl_ctx, "You entered: {}", trimmed);
68+
69+
// Log more lines after input to test the fix persists.
70+
writeln!(shared_writer, "After input: line A").into_diagnostic()?;
71+
writeln!(shared_writer, "After input: line B").into_diagnostic()?;
72+
}
73+
74+
ReadlineEvent::Resized => {
75+
writeln!(shared_writer, "Terminal resized").into_diagnostic()?;
76+
}
77+
78+
ReadlineEvent::Eof | ReadlineEvent::Interrupted => break,
79+
}
80+
}
81+
82+
rl_ctx
83+
.request_shutdown(Some("Shutting down test..."))
84+
.await?;
85+
rl_ctx.await_shutdown().await;
86+
87+
Ok(())
88+
}

tui/src/core/ansi/vt_100_terminal_input_parser/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) 2025 R3BL LLC. Licensed under Apache License, Version 2.0.
22

3+
// cspell:words desynchronization
4+
35
//! VT-100 Terminal Input Parsing Layer
46
//!
57
//! This module provides pure, reusable ANSI sequence parsing for terminal user input.

tui/src/core/ansi/vt_100_terminal_input_parser/test_fixtures/input_sequence_generator.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) 2025 R3BL LLC. Licensed under Apache License, Version 2.0.
22

3+
// cspell:words rowm
4+
35
//! Input event generator - converts high-level input events to ANSI sequences.
46
//!
57
//! This module provides the inverse operation to the input parsers in

tui/src/readline_async/readline_async_impl/integration_tests/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,7 @@ pub mod pty_ctrl_navigation_test;
4646
pub mod pty_alt_navigation_test;
4747
#[cfg(any(test, doc))]
4848
pub mod pty_alt_kill_test;
49+
#[cfg(any(test, doc))]
50+
pub mod pty_shared_writer_no_blank_line_test;
51+
#[cfg(any(test, doc))]
52+
pub mod pty_multiline_output_test;

0 commit comments

Comments
 (0)