4runr-os 2.1.48 → 2.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
Binary file
|
package/mk3-tui/src/app.rs
CHANGED
|
@@ -282,8 +282,9 @@ impl App {
|
|
|
282
282
|
match key.code {
|
|
283
283
|
// Typing - ALL characters go to command input (with debounce)
|
|
284
284
|
KeyCode::Char(c) => {
|
|
285
|
-
|
|
285
|
+
// CRITICAL: Always set focused when typing to show cursor
|
|
286
286
|
self.state.command_focused = true;
|
|
287
|
+
self.state.command_input.push(c);
|
|
287
288
|
// Debounce: schedule render after delay
|
|
288
289
|
self.input_debounce = Some(Instant::now());
|
|
289
290
|
// Don't render immediately - let debounce handle it
|
package/mk3-tui/src/ui/layout.rs
CHANGED
|
@@ -40,6 +40,9 @@ fn render_separator(f: &mut Frame, area: Rect) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
pub fn render(f: &mut Frame, state: &AppState) {
|
|
43
|
+
// CRITICAL: Hide cursor by default at start of each render
|
|
44
|
+
// Only show it when explicitly set in render_command_box
|
|
45
|
+
// This prevents cursor from appearing in wrong places (like operations log)
|
|
43
46
|
let full_area = f.size();
|
|
44
47
|
f.render_widget(Clear, full_area);
|
|
45
48
|
|
|
@@ -508,31 +511,53 @@ fn render_center_column(f: &mut Frame, area: Rect, state: &AppState) {
|
|
|
508
511
|
f.render_widget(Paragraph::new(lines), content_area);
|
|
509
512
|
|
|
510
513
|
// === RENDER CLEAN SCROLLBAR (only if scrollable) ===
|
|
514
|
+
// CRITICAL FIX: Only render scrollbar when actually scrollable to prevent flickering
|
|
511
515
|
if total_logs > visible_height && max_scroll > 0 {
|
|
512
516
|
// Scrollbar positioned inside the border, 1 char from right edge
|
|
513
|
-
|
|
517
|
+
// Use fixed position to prevent jitter
|
|
518
|
+
let scrollbar_x = panel_area.x + panel_area.width.saturating_sub(3);
|
|
514
519
|
let scrollbar_y = content_area.y;
|
|
515
520
|
let scrollbar_height = content_area.height;
|
|
516
521
|
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
let thumb_height =
|
|
522
|
+
// CRITICAL: Use integer math to prevent floating point rounding issues
|
|
523
|
+
// Calculate thumb size (minimum 1 char, proportional to visible/total)
|
|
524
|
+
let thumb_height = ((visible_height as u32 * scrollbar_height as u32) / total_logs as u32) as u16;
|
|
525
|
+
let thumb_height = thumb_height.max(1).min(scrollbar_height);
|
|
520
526
|
|
|
521
|
-
// Calculate thumb position
|
|
522
|
-
let
|
|
523
|
-
|
|
527
|
+
// Calculate thumb position using integer math for stability
|
|
528
|
+
let thumb_start = if max_scroll > 0 {
|
|
529
|
+
// Use integer division to avoid floating point jitter
|
|
530
|
+
((state.log_scroll as u32 * (scrollbar_height.saturating_sub(thumb_height) as u32)) / max_scroll as u32) as u16
|
|
531
|
+
} else {
|
|
532
|
+
0
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// Clamp thumb_start to valid range
|
|
536
|
+
let thumb_start = thumb_start.min(scrollbar_height.saturating_sub(thumb_height));
|
|
537
|
+
let thumb_end = (thumb_start + thumb_height).min(scrollbar_height);
|
|
538
|
+
|
|
539
|
+
// Render scrollbar track (entire height) as single operation to prevent flicker
|
|
540
|
+
// Build scrollbar string first, then render once
|
|
541
|
+
let mut scrollbar_chars = vec![String::from("│"); scrollbar_height as usize];
|
|
524
542
|
|
|
525
|
-
//
|
|
526
|
-
for i in
|
|
527
|
-
|
|
543
|
+
// Fill thumb portion
|
|
544
|
+
for i in thumb_start..thumb_end {
|
|
545
|
+
if i < scrollbar_height {
|
|
546
|
+
scrollbar_chars[i as usize] = String::from("█");
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Render entire scrollbar at once (prevents flickering from multiple renders)
|
|
551
|
+
for (i, c) in scrollbar_chars.iter().enumerate() {
|
|
552
|
+
let y = scrollbar_y + i as u16;
|
|
528
553
|
if y < scrollbar_y + scrollbar_height {
|
|
529
|
-
let
|
|
530
|
-
|
|
554
|
+
let color = if i >= thumb_start as usize && i < thumb_end as usize {
|
|
555
|
+
CYBER_CYAN
|
|
531
556
|
} else {
|
|
532
|
-
|
|
557
|
+
TEXT_MUTED
|
|
533
558
|
};
|
|
534
559
|
f.render_widget(
|
|
535
|
-
Paragraph::new(c).style(Style::default().fg(color)),
|
|
560
|
+
Paragraph::new(c.as_str()).style(Style::default().fg(color)),
|
|
536
561
|
Rect { x: scrollbar_x, y, width: 1, height: 1 }
|
|
537
562
|
);
|
|
538
563
|
}
|
|
@@ -598,13 +623,14 @@ fn render_command_box(f: &mut Frame, area: Rect, state: &AppState) {
|
|
|
598
623
|
// Just use spacing for visual separation
|
|
599
624
|
|
|
600
625
|
// Command prompt - Use ASCII > instead of Unicode ▶
|
|
601
|
-
|
|
626
|
+
// Note: We show a visual cursor character "_" in the text, but the real cursor
|
|
627
|
+
// position is handled separately below
|
|
602
628
|
let prompt_line = Line::from(vec![
|
|
603
629
|
Span::styled("> ", Style::default().fg(BRAND_PURPLE)), // ASCII > instead of Unicode ▶
|
|
604
630
|
Span::styled("4runr", Style::default().fg(BRAND_VIOLET).add_modifier(Modifier::BOLD)),
|
|
605
631
|
Span::styled(": ", Style::default().fg(TEXT_DIM)), // ASCII : instead of Unicode ›
|
|
606
632
|
Span::styled(&state.command_input, Style::default().fg(TEXT_PRIMARY)),
|
|
607
|
-
Span::styled(
|
|
633
|
+
Span::styled("_", Style::default().fg(CYBER_CYAN)), // Visual cursor indicator
|
|
608
634
|
]);
|
|
609
635
|
f.render_widget(Paragraph::new(prompt_line), Rect {
|
|
610
636
|
x: bar_area.x, y: bar_area.y + 1, width: bar_area.width, height: 1
|
|
@@ -623,12 +649,26 @@ fn render_command_box(f: &mut Frame, area: Rect, state: &AppState) {
|
|
|
623
649
|
x: bar_area.x, y: bar_area.y + 2, width: bar_area.width, height: 1
|
|
624
650
|
});
|
|
625
651
|
|
|
626
|
-
//
|
|
627
|
-
|
|
628
|
-
|
|
652
|
+
// CRITICAL FIX: Only set cursor position when user is actively typing
|
|
653
|
+
// This prevents cursor from appearing in wrong places (operations log, etc.)
|
|
654
|
+
// IMPORTANT: In Ratatui, set_cursor() shows the cursor, so we only call it here
|
|
655
|
+
if state.command_focused || !state.command_input.is_empty() {
|
|
656
|
+
// Calculate correct cursor position: "> 4runr: " = 9 chars
|
|
657
|
+
let prompt_len = 9u16; // "> 4runr: " = 9 characters ("> " + "4runr" + ": " = 2+5+2 = 9)
|
|
658
|
+
let input_len = state.command_input.len() as u16;
|
|
659
|
+
let cursor_x = bar_area.x + prompt_len + input_len;
|
|
629
660
|
let cursor_y = bar_area.y + 1;
|
|
630
|
-
|
|
661
|
+
|
|
662
|
+
// Ensure cursor doesn't go beyond the safe area (respects 15% margin)
|
|
663
|
+
let max_x = (bar_area.x + bar_area.width).saturating_sub(1);
|
|
664
|
+
let final_x = cursor_x.min(max_x);
|
|
665
|
+
|
|
666
|
+
// Only set cursor if we're actually at the input field
|
|
667
|
+
// This prevents cursor from appearing elsewhere (operations log, etc.)
|
|
668
|
+
f.set_cursor(final_x, cursor_y);
|
|
631
669
|
}
|
|
670
|
+
// IMPORTANT: If not focused and no input, we don't call set_cursor()
|
|
671
|
+
// This keeps the cursor hidden (hidden at start of main loop)
|
|
632
672
|
}
|
|
633
673
|
|
|
634
674
|
fn render_too_small(f: &mut Frame, viewport: &SafeViewport) {
|