Skip to content

Commit 5d1282f

Browse files
authored
Merge pull request #440 from r3bl-org/terminal_async_bug_fix
Fix SharedWriter indentation bug and improve readline type safety
2 parents f9716e3 + 8e18cc1 commit 5d1282f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+5465
-2823
lines changed

.claude/skills/check-bounds-safety/SKILL.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ Follow these principles when working with indices and lengths:
6767
- Instead of `== 0`
6868
- More idiomatic with newtype wrappers
6969

70+
5. **Distinguish navigation from measurement**
71+
- **Navigation** (`index - offset → index`): Moving backward in position space
72+
- **Measurement** (`index.distance_from(other) → length`): Calculating distance between positions
73+
- Use `-` for cursor movement, use `distance_from()` for calculating spans
74+
7075
## Common Imports
7176

7277
```rust
@@ -82,6 +87,9 @@ use r3bl_tui::{
8287

8388
// Type constructors
8489
col, row, width, height, idx, len,
90+
91+
// Terminal delta types (relative cursor movement)
92+
TermRowDelta, TermColDelta, term_row_delta, term_col_delta,
8593
};
8694
```
8795

@@ -95,6 +103,7 @@ use r3bl_tui::{
95103
| **Range validation** | `RangeBoundsExt` | `range.check_range_is_valid_for_length(len)` | Iterator bounds, algorithm parameters |
96104
| **Range membership** | `RangeBoundsExt` | `range.check_index_is_within(index)` | VT-100 scroll regions, text selections |
97105
| **Range conversion** | `RangeConvertExt` | `inclusive_range.to_exclusive()` | Converting VT-100 ranges for Rust iteration |
106+
| **Relative movement** | `TermRowDelta`/`TermColDelta` | `TermRowDelta::new(n)` returns `Option` | ANSI cursor movement preventing CSI zero bug |
98107

99108
## Detailed Examples
100109

@@ -236,6 +245,80 @@ for line in rust_range {
236245
}
237246
```
238247

248+
### Example 7: Navigation vs Measurement
249+
250+
**Use `-` for navigation (moving cursor), `distance_from()` for measurement (calculating spans).**
251+
252+
```rust
253+
use r3bl_tui::{row, height, RowIndex, RowHeight};
254+
255+
// Navigation: Move cursor backward by offset (returns RowIndex).
256+
let cursor_pos = row(10);
257+
let new_pos = cursor_pos - row(3); // row(7) - moved 3 positions back
258+
// Uses saturating subtraction: row(2) - row(5) = row(0), not overflow
259+
260+
// Measurement: Calculate distance between two positions (returns RowHeight).
261+
let start = row(5);
262+
let end = row(15);
263+
let distance: RowHeight = end.distance_from(start); // height(10) - 10 rows apart
264+
// Panics if start > end (negative distance)
265+
```
266+
267+
**When to use which:**
268+
- Moving cursor up/down/left/right → `-` operator
269+
- Calculating scroll amount, viewport span, selection size → `distance_from()`
270+
271+
### Example 8: Terminal Cursor Movement (Make Illegal States Unrepresentable)
272+
273+
**Use `TermRowDelta`/`TermColDelta` for relative cursor movement in ANSI sequences.**
274+
275+
The CSI zero problem: ANSI cursor movement commands interpret parameter 0 as 1:
276+
- `CSI 0 A` (`CursorUp` with n=0) moves cursor **1 row up**, not 0
277+
- `CSI 0 C` (`CursorForward` with n=0) moves cursor **1 column right**, not 0
278+
279+
**Solution:** `TermRowDelta` and `TermColDelta` wrap `NonZeroU16` internally, making zero-valued
280+
deltas **impossible to represent**. Construction is fallible:
281+
282+
```rust
283+
use r3bl_tui::{TermRowDelta, TermColDelta, CsiSequence};
284+
use std::io::Write;
285+
286+
// Calculate cursor movement from position on 80-column terminal.
287+
let position: u16 = 240; // 240 chars from start
288+
let term_width: u16 = 80;
289+
290+
// Fallible construction - must handle the None case.
291+
// For position 240: rows = 3 (Some), cols = 0 (None).
292+
if let Some(delta) = TermRowDelta::new(position / term_width) {
293+
// delta is guaranteed non-zero, safe to emit
294+
term.write_all(CsiSequence::CursorDown(delta).to_string().as_bytes())?;
295+
}
296+
if let Some(delta) = TermColDelta::new(position % term_width) {
297+
// This branch is NOT taken for position 240 (cols = 0)
298+
// Zero cannot be represented, so the bug is prevented at the type level!
299+
term.write_all(CsiSequence::CursorForward(delta).to_string().as_bytes())?;
300+
}
301+
```
302+
303+
**Using the ONE constant for common case:**
304+
305+
```rust
306+
use r3bl_tui::{TermRowDelta, TermColDelta, CsiSequence};
307+
308+
// For the common case of moving exactly 1 row/column, use the ONE constant.
309+
// This avoids the need for fallible construction or unwrap().
310+
let up_one = CsiSequence::CursorUp(TermRowDelta::ONE);
311+
let right_one = CsiSequence::CursorForward(TermColDelta::ONE);
312+
```
313+
314+
**Mathematical law:**
315+
- Zero deltas **cannot exist** - prevented at compile time by `NonZeroU16` wrapper
316+
- `new()` returns `None` for zero, `Some(delta)` for non-zero
317+
318+
**Key difference from absolute positioning:**
319+
- `TermRow`/`TermCol`: 1-based absolute coordinates (for `CursorPosition`)
320+
- `TermRowDelta`/`TermColDelta`: Relative movement amounts (for `CursorUp/Down/Forward/Backward`)
321+
239322
## Decision Trees
240323

241324
See the accompanying `decision-trees.md` file for flowcharts showing which trait to use for
@@ -309,6 +392,41 @@ if row < width { // Compile error! Can't compare RowIndex with ColWidth
309392

310393
This is actually GOOD - the type system prevents nonsensical comparisons!
311394

395+
### ❌ Mistake 4: Using `-` when you need distance
396+
397+
```rust
398+
// Bad - using subtraction to calculate distance.
399+
// This returns RowIndex, not RowHeight!
400+
let current_row = row(5);
401+
let target_row = row(15);
402+
let rows_to_scroll = target_row - current_row; // Returns row(10), a position!
403+
```
404+
405+
**Fix:**
406+
```rust
407+
// Good - use distance_from() for measurement.
408+
let rows_to_scroll: RowHeight = target_row.distance_from(current_row); // height(10)
409+
```
410+
411+
### ❌ Mistake 5: Emitting CSI zero for cursor movement
412+
413+
```rust
414+
// Bad - CSI 0 C moves 1 column right, not 0!
415+
// This won't even compile now - CursorForward requires TermColDelta, not u16!
416+
let cols = position % term_width; // Could be 0!
417+
let seq = CsiSequence::CursorForward(cols); // Compile error!
418+
```
419+
420+
**Fix:**
421+
```rust
422+
// Good - use fallible delta construction (zero is unrepresentable)
423+
if let Some(delta) = TermColDelta::new(position % term_width) {
424+
// delta is guaranteed non-zero
425+
term.write_all(CsiSequence::CursorForward(delta).to_string().as_bytes())?;
426+
}
427+
// No sequence emitted when cols = 0 (correct behavior - branch not taken)
428+
```
429+
312430
## Reporting Results
313431

314432
When applying bounds checking:
@@ -343,3 +461,4 @@ No dedicated command, but used throughout the codebase for safe index/length han
343461
- Main implementation: `tui/src/core/units/bounds_check/mod.rs`
344462
- Type definitions: `tui/src/core/units/`
345463
- Examples in tests: `tui/src/core/units/bounds_check/tests/`
464+
- Terminal delta types: `tui/src/core/coordinates/vt_100_ansi_coords/term_row_delta.rs` and `term_col_delta.rs`

.claude/skills/organize-modules/SKILL.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,94 @@ pub mod serialization;
272272

273273
**Why:** Users need to know which features enable which APIs.
274274

275+
### Inner Modules vs. Separate Files
276+
277+
When organizing code into logical groups, choose between **inner modules** (same file) and
278+
**separate files** based on file size and complexity.
279+
280+
#### Inner Modules (Same File)
281+
282+
**✅ Use inner modules when:**
283+
284+
- File is small-to-medium (under ~300 lines total)
285+
- Groups are logically related and benefit from proximity
286+
- Comment banners (`// ======`) are being used to separate sections
287+
- Each group is relatively small (~20-50 lines)
288+
289+
```rust
290+
// ansi_sequence_generator.rs - Inner module pattern
291+
292+
pub struct AnsiSequenceGenerator;
293+
294+
mod cursor_movement {
295+
use super::*;
296+
impl AnsiSequenceGenerator {
297+
pub fn cursor_position(...) -> String { ... }
298+
pub fn cursor_to_column(...) -> String { ... }
299+
}
300+
}
301+
302+
mod screen_clearing {
303+
use super::*;
304+
impl AnsiSequenceGenerator {
305+
pub fn clear_screen() -> String { ... }
306+
pub fn clear_current_line() -> String { ... }
307+
}
308+
}
309+
310+
mod color_ops {
311+
use super::*;
312+
impl AnsiSequenceGenerator {
313+
pub fn fg_color(...) -> String { ... }
314+
pub fn bg_color(...) -> String { ... }
315+
}
316+
}
317+
```
318+
319+
**Benefits:**
320+
- Single-file cohesion - everything related stays together
321+
- Easier navigation - no jumping between files
322+
- Clear grouping - `mod` keyword is more formal than comment banners
323+
- Scoped imports - each inner mod can import only what it needs
324+
325+
#### Separate Files
326+
327+
**✅ Use separate files when:**
328+
329+
- Individual groups exceed ~100 lines each
330+
- Groups have distinct dependencies (different imports)
331+
- File would exceed ~500 lines total
332+
- Groups are conceptually independent (could be tested separately)
333+
334+
```
335+
generator/
336+
├── mod.rs # Re-exports + struct definition
337+
├── cursor_movement.rs # impl AnsiSequenceGenerator { cursor_* }
338+
├── screen_clearing.rs # impl AnsiSequenceGenerator { clear_* }
339+
├── color_ops.rs # impl AnsiSequenceGenerator { colors }
340+
└── terminal_modes.rs # impl AnsiSequenceGenerator { modes }
341+
```
342+
343+
#### Code Smell: Comment Banners
344+
345+
If you find yourself writing comment banners like this:
346+
347+
```rust
348+
impl MyStruct {
349+
// ==================== Group A ====================
350+
fn method_a1() { ... }
351+
fn method_a2() { ... }
352+
353+
// ==================== Group B ====================
354+
fn method_b1() { ... }
355+
fn method_b2() { ... }
356+
}
357+
```
358+
359+
**This is a signal to formalize the grouping** using either inner modules (small file) or
360+
separate files (large file). Comment banners are informal and don't provide the same benefits
361+
as actual module boundaries (scoped imports, clear boundaries, IDE navigation).
362+
275363
## Complete Examples
276364

277365
### Example 1: Simple Module Organization

.vscode/extensions.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@
1313
//
1414
// List of extensions which should be recommended for users of this workspace.
1515
"recommendations": [
16+
// R3BL Extensions.
17+
"R3BL.r3bl-auto-insert-copyright",
18+
"R3BL.r3bl-copy-selection-path-and-range",
19+
"R3BL.r3bl-extension-pack",
20+
"R3BL.r3bl-fuzzy-search",
21+
"R3BL.r3bl-semantic-config",
22+
"R3BL.r3bl-shared",
23+
"R3BL.r3bl-task-management",
24+
"R3BL.r3bl-theme",
25+
// Others.
1626
"ryuta46.multi-command", // Multi Command.
1727
"streetsidesoftware.code-spell-checker", // [SLOW] Code Spell Checker. Configure it so it doesn't slow down IDE
1828
"usernamehw.errorlens", // Error Lens.

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"Cloneable",
4343
"cmark",
4444
"codegen",
45+
"codepaths",
4546
"codepoint",
4647
"codepoints",
4748
"col_index",
@@ -318,6 +319,7 @@
318319
"unergonomic",
319320
"uninteractive",
320321
"unparseable",
322+
"Unrepresentable",
321323
"unspecial",
322324
"unstaged",
323325
"ustr",

0 commit comments

Comments
 (0)