Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions data/tc_builds_sample.csv

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions src/app_state_container.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
//! Shared application state driven by the TUI.
//!
//! Like `crate::ui`, this must not write to stdout/stderr - see `ui/mod.rs`.
#![deny(clippy::print_stdout, clippy::print_stderr)]

use crate::api_client::QueryResponse;
use crate::buffer::{AppMode, BufferAPI, BufferManager, SortOrder};
use crate::debug_service::DebugLevel;
Expand All @@ -17,7 +22,7 @@ use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::time::{Duration, Instant};
use tracing::{info, trace};
use tracing::{debug, info, trace};

/// Platform type for key handling
#[derive(Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -3607,8 +3612,9 @@ impl AppStateContainer {
})
.collect();

eprintln!(
"[DEBUG] Created {} matches in history_search",
debug!(
target: "history",
"Created {} matches in history_search",
history_search.matches.len()
);

Expand Down
31 changes: 31 additions & 0 deletions src/data/data_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,37 @@ impl DataView {
self.visible_columns.clone()
}

/// Translate a `DataTable` (source) column index into its visual position.
///
/// The two index spaces only coincide when the view projects every source column in
/// source order (`SELECT *`). Under a narrower projection - `SELECT a, b, c` from a
/// wider table - `visible_columns` holds arbitrary source indices, so anything keyed
/// by visual position (column names, row values, column widths) must be looked up
/// through this translation rather than with the source index directly.
///
/// Returns `None` if the column is not part of this view.
#[must_use]
pub fn visual_index_of_column(&self, datatable_index: usize) -> Option<usize> {
let real_position = self
.visible_columns
.iter()
.position(|&c| c == datatable_index)?;

// Virtual columns occupy visual slots too, so shift past any that sort before
// this one. Mirrors the assembly order in `get_all_column_names`/`get_row`.
let shift = self
.virtual_columns
.iter()
.filter(|vcol| match vcol.position {
VirtualColumnPosition::Left => true,
VirtualColumnPosition::Index(idx) => idx <= real_position,
VirtualColumnPosition::Right => false,
})
.count();

Some(real_position + shift)
}

/// Get display column names in order (pinned first, then visible)
#[must_use]
pub fn get_display_column_names(&self) -> Vec<String> {
Expand Down
1 change: 1 addition & 0 deletions src/help_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ impl HelpText {
Line::from(" : - Jump to row"),
Line::from(" Space - Toggle viewport lock"),
Line::from(" Ctrl+Space - Toggle viewport lock (alternative)"),
Line::from(" Ctrl+L - Redraw the screen"),
Line::from(" x/X - Toggle cursor lock"),
Line::from(" p - Pin/unpin column"),
Line::from(" P - Clear all pins"),
Expand Down
44 changes: 24 additions & 20 deletions src/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use tracing::{debug, error, info, warn};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryMetadata {
Expand Down Expand Up @@ -493,7 +494,7 @@ impl CommandHistory {
// SAFETY: Create backup before clearing
let current_count = self.entries.len();
if current_count > 0 {
eprintln!("[HISTORY WARNING] Clearing {current_count} entries - creating backup");
warn!(target: "history", "Clearing {current_count} entries - creating backup");
if let Ok(content) = serde_json::to_string_pretty(&self.entries) {
self.protection.backup_before_write(&content, current_count);
}
Expand All @@ -506,33 +507,34 @@ impl CommandHistory {

fn load_from_file(&mut self) -> Result<()> {
if !self.history_file.exists() {
eprintln!("[History] No history file found at {:?}", self.history_file);
debug!(target: "history", "No history file found at {:?}", self.history_file);
return Ok(());
}

eprintln!("[History] Loading history from {:?}", self.history_file);
debug!(target: "history", "Loading history from {:?}", self.history_file);
let content = fs::read_to_string(&self.history_file)?;
if content.trim().is_empty() {
eprintln!("[History] History file is empty");
debug!(target: "history", "History file is empty");
return Ok(());
}

// Try to parse the history file
let entries: Vec<HistoryEntry> = match serde_json::from_str(&content) {
Ok(entries) => entries,
Err(e) => {
eprintln!("[History] ERROR: Failed to parse history file: {e}");
eprintln!("[History] Attempting recovery from backup...");
error!(target: "history", "Failed to parse history file: {e}");
warn!(target: "history", "Attempting recovery from backup...");

// Try to recover from backup
if let Some(backup_content) = self.protection.recover_from_backup() {
eprintln!("[History] Found backup, attempting to restore...");
warn!(target: "history", "Found backup, attempting to restore...");

// Try to parse the backup
match serde_json::from_str::<Vec<HistoryEntry>>(&backup_content) {
Ok(backup_entries) => {
eprintln!(
"[History] Successfully recovered {} entries from backup",
warn!(
target: "history",
"Successfully recovered {} entries from backup",
backup_entries.len()
);

Expand All @@ -545,30 +547,31 @@ impl CommandHistory {
self.history_file.with_extension("json"),
&corrupted_path,
);
eprintln!("[History] Corrupted file moved to {corrupted_path:?}");
warn!(target: "history", "Corrupted file moved to {corrupted_path:?}");

backup_entries
}
Err(backup_err) => {
eprintln!("[History] Backup also corrupted: {backup_err}");
eprintln!("[History] Starting with empty history");
error!(target: "history", "Backup also corrupted: {backup_err}");
warn!(target: "history", "Starting with empty history");
Vec::new()
}
}
} else {
eprintln!("[History] No backup available, starting with empty history");
warn!(target: "history", "No backup available, starting with empty history");

// Move the corrupted file for investigation
let corrupted_path = self.history_file.with_extension("json.corrupted");
let _ = fs::copy(&self.history_file, &corrupted_path);
eprintln!("[History] Corrupted file copied to {corrupted_path:?}");
warn!(target: "history", "Corrupted file copied to {corrupted_path:?}");

Vec::new()
}
}
};
eprintln!(
"[History] Loaded {} entries from history file",
debug!(
target: "history",
"Loaded {} entries from history file",
entries.len()
);
let original_count = entries.len();
Expand All @@ -594,7 +597,7 @@ impl CommandHistory {
// Log if we removed duplicates (only on first load, not every save)
let removed_count = original_count - deduplicated.len();
if removed_count > 0 {
eprintln!("[sql-cli] Cleaned {removed_count} duplicate commands from history");
info!(target: "history", "Cleaned {removed_count} duplicate commands from history");
}

// Rebuild command counts
Expand All @@ -607,8 +610,9 @@ impl CommandHistory {
}

self.entries = deduplicated;
eprintln!(
"[History] Final history contains {} unique entries",
debug!(
target: "history",
"Final history contains {} unique entries",
self.entries.len()
);
Ok(())
Expand All @@ -635,7 +639,7 @@ impl CommandHistory {
.protection
.validate_write(old_count, new_count, &new_content)
{
eprintln!("[HISTORY PROTECTION] Write blocked! Attempting recovery from backup...");
error!(target: "history", "Write blocked! Attempting recovery from backup...");
if let Some(backup_content) = self.protection.recover_from_backup() {
fs::write(&self.history_file, backup_content)?;
return Ok(());
Expand Down
48 changes: 31 additions & 17 deletions src/ui/enhanced_tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,9 @@ pub struct EnhancedTuiApp {
// Debug system
pub(crate) debug_registry: DebugRegistry,
pub(crate) memory_tracker: MemoryTracker,

// Set by Ctrl+L; consumed by the event loop to clear before the next draw
force_redraw: bool,
}

impl DebugContext for EnhancedTuiApp {
Expand Down Expand Up @@ -1547,6 +1550,7 @@ impl EnhancedTuiApp {
query_orchestrator: QueryOrchestrator::with_behavior_config(config.behavior.clone()),
debug_registry: DebugRegistry::new(),
memory_tracker: MemoryTracker::new(100),
force_redraw: false,
};

// Set up state dispatcher
Expand Down Expand Up @@ -1948,6 +1952,11 @@ impl EnhancedTuiApp {
if self.table_widget_manager.borrow().needs_render() {
info!("TableWidgetManager needs render after key event");
}
if std::mem::take(&mut self.force_redraw) {
// Drop ratatui's notion of what is on screen so the next draw
// repaints every cell rather than diffing against a stale buffer.
terminal.clear()?;
}
terminal.draw(|f| self.ui(f))?;
self.table_widget_manager.borrow_mut().rendered();

Expand Down Expand Up @@ -2287,7 +2296,7 @@ impl EnhancedTuiApp {

// Special handling for History mode - initialize history search
if mode == AppMode::History {
eprintln!("[DEBUG] Using AppStateContainer for history search");
debug!(target: "history", "Using AppStateContainer for history search");
let current_input = self.get_input_text();

// Start history search
Expand Down Expand Up @@ -4895,8 +4904,8 @@ impl EnhancedTuiApp {
static FILTER_DEPTH: AtomicUsize = AtomicUsize::new(0);
let depth = FILTER_DEPTH.fetch_add(1, Ordering::SeqCst);
if depth > 0 {
eprintln!(
"WARNING: apply_filter re-entrancy detected! depth={}, pattern='{}', thread={:?}",
warn!(
"apply_filter re-entrancy detected! depth={}, pattern='{}', thread={:?}",
depth,
pattern,
std::thread::current().id()
Expand Down Expand Up @@ -6854,32 +6863,31 @@ impl EnhancedTuiApp {
));
}

// Find current column in display order
if let Some(display_idx) = display_columns
.iter()
.position(|&idx| idx == current_column)
{
// current_column is a VISUAL position, so it indexes display_columns
// directly - it is not a DataTable index to be searched for.
if let Some(&datatable_idx) = display_columns.get(current_column) {
debug_info.push_str(&format!(
"Current column {} is at display index {}/{}\n",
"Current column: visual {}/{} -> DataTable [{}]\n",
current_column,
display_idx,
display_columns.len()
display_columns.len(),
datatable_idx
));

// Show what happens on next move
if display_idx + 1 < display_columns.len() {
let next_col = display_columns[display_idx + 1];
if current_column + 1 < display_columns.len() {
debug_info.push_str(&format!(
"Next 'l' press should move to column {} (display index {})\n",
next_col,
display_idx + 1
"Next 'l' press should move to visual {} -> DataTable [{}]\n",
current_column + 1,
display_columns[current_column + 1]
));
} else {
debug_info.push_str("Next 'l' press should wrap to first column\n");
}
} else {
debug_info.push_str(&format!(
"WARNING: Current column {current_column} not found in display order!\n"
"WARNING: Visual column {} is out of range ({} columns)!\n",
current_column,
display_columns.len()
));
}
}
Expand Down Expand Up @@ -7644,6 +7652,12 @@ impl ActionHandlerContext for EnhancedTuiApp {
}
}

fn request_force_redraw(&mut self) {
debug!("Force redraw requested (Ctrl+L)");
self.force_redraw = true;
self.set_status_message("Screen refreshed");
}

fn toggle_viewport_lock(&mut self) {
// Toggle viewport lock in ViewportManager
let is_locked = {
Expand Down
10 changes: 10 additions & 0 deletions src/ui/input/action_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ pub trait ActionHandlerContext {
// Viewport lock operations
fn toggle_cursor_lock(&mut self);
fn toggle_viewport_lock(&mut self);

/// Request a full repaint on the next frame (Ctrl+L).
fn request_force_redraw(&mut self);
}

/// Handler for navigation actions (Up, Down, Left, Right, `PageUp`, etc.)
Expand Down Expand Up @@ -669,6 +672,10 @@ impl ActionHandler for DebugViewportActionHandler {
tui.toggle_viewport_lock();
Some(Ok(ActionResult::Handled))
}
Action::ForceRedraw => {
tui.request_force_redraw();
Some(Ok(ActionResult::Handled))
}
_ => None,
}
}
Expand Down Expand Up @@ -1072,6 +1079,9 @@ mod tests {
fn toggle_viewport_lock(&mut self) {
self.last_action = "toggle_viewport_lock".to_string();
}
fn request_force_redraw(&mut self) {
self.last_action = "request_force_redraw".to_string();
}

// Input and text editing
fn move_input_cursor_left(&mut self) {
Expand Down
1 change: 1 addition & 0 deletions src/ui/input/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ pub enum Action {
NavigateToViewportBottom,
ToggleCursorLock,
ToggleViewportLock,
ForceRedraw, // Ctrl+L - repaint the whole screen
ToggleCaseInsensitive,
ToggleKeyIndicator,
ShowColumnStatistics, // For 'S' key
Expand Down
Loading
Loading