Fix scrolling issues

This commit is contained in:
Ian Keane 2025-12-03 13:13:19 -05:00
parent 360af2f0cb
commit ccab4510ea
2 changed files with 212 additions and 142 deletions

View file

@ -36,11 +36,13 @@ pub enum LogViewDayOrder {
ReverseChronological, // Newest first (default) ReverseChronological, // Newest first (default)
} }
#[derive(Debug, Copy, Clone)]
pub enum LogViewGrouping { pub enum LogViewGrouping {
ByDate, ByDate,
ByProject, ByProject,
} }
#[derive(Debug, Copy, Clone)]
pub enum LogViewSelection { pub enum LogViewSelection {
Entry, // Individual entry selected Entry, // Individual entry selected
Project, // Whole project selected (ByProject mode only) Project, // Whole project selected (ByProject mode only)
@ -101,7 +103,7 @@ impl App {
} }
// Helper to determine if a line is an entry line based on grouping mode // Helper to determine if a line is an entry line based on grouping mode
fn is_entry_line(&self, line: &str) -> bool { pub fn is_entry_line(&self, line: &str) -> bool {
// Exclude separator lines (time gap markers) // Exclude separator lines (time gap markers)
if line.trim() == "---" { if line.trim() == "---" {
return false; return false;
@ -489,6 +491,7 @@ impl App {
self.needs_clear = true; self.needs_clear = true;
} }
KeyCode::Char('j') | KeyCode::Down => { KeyCode::Char('j') | KeyCode::Down => {
let old_selected = self.log_view_selected;
match self.log_view_selection_level { match self.log_view_selection_level {
LogViewSelection::Entry => { LogViewSelection::Entry => {
// Move to next entry // Move to next entry
@ -508,8 +511,13 @@ impl App {
// No navigation at All level - already selecting everything // No navigation at All level - already selecting everything
} }
} }
// Update scroll if selection changed
if self.log_view_selected != old_selected {
self.update_log_view_scroll();
}
} }
KeyCode::Char('k') | KeyCode::Up => { KeyCode::Char('k') | KeyCode::Up => {
let old_selected = self.log_view_selected;
match self.log_view_selection_level { match self.log_view_selection_level {
LogViewSelection::Entry => { LogViewSelection::Entry => {
// Move to previous entry // Move to previous entry
@ -529,6 +537,10 @@ impl App {
// No navigation at All level - already selecting everything // No navigation at All level - already selecting everything
} }
} }
// Update scroll if selection changed
if self.log_view_selected != old_selected {
self.update_log_view_scroll();
}
} }
KeyCode::Char('e') => { KeyCode::Char('e') => {
// Only allow edit when selecting individual entry // Only allow edit when selecting individual entry
@ -561,6 +573,7 @@ impl App {
} }
KeyCode::Char('h') | KeyCode::Left => { KeyCode::Char('h') | KeyCode::Left => {
// Zoom out selection level // Zoom out selection level
let old_level = self.log_view_selection_level;
self.log_view_selection_level = match (&self.log_view_selection_level, &self.log_view_grouping) { self.log_view_selection_level = match (&self.log_view_selection_level, &self.log_view_grouping) {
(LogViewSelection::All, _) => LogViewSelection::All, // Already at highest (LogViewSelection::All, _) => LogViewSelection::All, // Already at highest
(LogViewSelection::Day, _) => LogViewSelection::All, (LogViewSelection::Day, _) => LogViewSelection::All,
@ -571,6 +584,7 @@ impl App {
} }
KeyCode::Char('l') | KeyCode::Right => { KeyCode::Char('l') | KeyCode::Right => {
// Zoom in selection level // Zoom in selection level
let old_level = self.log_view_selection_level;
self.log_view_selection_level = match (&self.log_view_selection_level, &self.log_view_grouping) { self.log_view_selection_level = match (&self.log_view_selection_level, &self.log_view_grouping) {
(LogViewSelection::Entry, _) => LogViewSelection::Entry, // Already at lowest (LogViewSelection::Entry, _) => LogViewSelection::Entry, // Already at lowest
(LogViewSelection::Project, _) => LogViewSelection::Entry, (LogViewSelection::Project, _) => LogViewSelection::Entry,
@ -612,9 +626,11 @@ impl App {
KeyCode::PageDown => { KeyCode::PageDown => {
self.log_view_selected = (self.log_view_selected + 10) self.log_view_selected = (self.log_view_selected + 10)
.min(self.log_view_frame_indices.len().saturating_sub(1)); .min(self.log_view_frame_indices.len().saturating_sub(1));
self.update_log_view_scroll();
} }
KeyCode::PageUp => { KeyCode::PageUp => {
self.log_view_selected = self.log_view_selected.saturating_sub(10); self.log_view_selected = self.log_view_selected.saturating_sub(10);
self.update_log_view_scroll();
} }
_ => {} _ => {}
}, },
@ -633,6 +649,9 @@ impl App {
LogViewGrouping::ByDate => self.format_by_date(), LogViewGrouping::ByDate => self.format_by_date(),
LogViewGrouping::ByProject => self.format_by_project(), LogViewGrouping::ByProject => self.format_by_project(),
} }
// Update scroll position after formatting
self.update_log_view_scroll();
} }
fn format_by_date(&mut self) { fn format_by_date(&mut self) {
@ -1037,10 +1056,15 @@ impl App {
} }
}; };
// Debug what we're copying
std::fs::write("/tmp/wat_copy_debug.log", format!("Selection level: {:?}\nSelected index: {}\nText to copy:\n{}\n", self.log_view_selection_level, self.log_view_selected, text)).ok();
// Copy to clipboard using the persistent clipboard instance // Copy to clipboard using the persistent clipboard instance
if let Some(ref mut clipboard) = self.clipboard { if let Some(ref mut clipboard) = self.clipboard {
if let Err(e) = clipboard.set_text(&text) { if let Err(e) = clipboard.set_text(&text) {
self.set_status_message(format!("Failed to copy: {}", e)); self.set_status_message(format!("Failed to copy: {}", e));
} else {
self.set_status_message(format!("Copied {} chars to clipboard", text.len()));
} }
} else { } else {
// Try to create a new clipboard if we don't have one // Try to create a new clipboard if we don't have one
@ -1048,6 +1072,8 @@ impl App {
Ok(mut clipboard) => { Ok(mut clipboard) => {
if let Err(e) = clipboard.set_text(&text) { if let Err(e) = clipboard.set_text(&text) {
self.set_status_message(format!("Failed to copy: {}", e)); self.set_status_message(format!("Failed to copy: {}", e));
} else {
self.set_status_message(format!("Copied {} chars to clipboard", text.len()));
} }
self.clipboard = Some(clipboard); self.clipboard = Some(clipboard);
} }
@ -1076,30 +1102,70 @@ impl App {
} }
fn get_selected_project_text(&self) -> String { fn get_selected_project_text(&self) -> String {
// Find which project group the selected entry belongs to // Find all project headers and their entry ranges
let mut current_project_lines = Vec::new(); let mut projects = Vec::new(); // (first_entry_index, project_line_index)
let mut frame_count = 0; let mut entry_count = 0;
let mut found = false; let mut expecting_first_entry = false;
let mut current_project_line_index = 0;
for line in &self.log_view_content { for (line_index, line) in self.log_view_content.iter().enumerate() {
if line.starts_with(" ") && !line.starts_with("\t") { if line.starts_with(" ") && !line.starts_with(" ") && !line.starts_with("\t") {
// This is a project header // This is a project header
if found { current_project_line_index = line_index;
break; // We've collected the target project expecting_first_entry = true;
}
current_project_lines.clear();
current_project_lines.push(line.clone());
} else if self.is_entry_line(line) { } else if self.is_entry_line(line) {
// Entry within a project if expecting_first_entry {
current_project_lines.push(line.clone()); // This is the first entry of a project
if frame_count == self.log_view_selected { projects.push((entry_count, current_project_line_index));
found = true; expecting_first_entry = false;
} }
frame_count += 1; entry_count += 1;
} }
} }
current_project_lines.join("\n") // Find which project contains our current selection
let mut target_project_line_index = None;
for i in 0..projects.len() {
let project_start = projects[i].0;
let project_end = if i + 1 < projects.len() {
projects[i + 1].0
} else {
entry_count
};
if project_start <= self.log_view_selected && self.log_view_selected < project_end {
target_project_line_index = Some(projects[i].1);
break;
}
}
if let Some(target_line_index) = target_project_line_index {
// Collect all lines for the target project by position
let mut project_lines = Vec::new();
let mut in_target_project = false;
let mut current_project_line_index = 0;
for (line_index, line) in self.log_view_content.iter().enumerate() {
if line.starts_with(" ") && !line.starts_with(" ") && !line.starts_with("\t") {
// This is a project header
current_project_line_index = line_index;
if line_index == target_line_index {
in_target_project = true;
project_lines.push(line.clone());
} else if in_target_project {
// Hit a different project header, we're done
break;
}
} else if in_target_project && self.is_entry_line(line) {
// Entry within our target project
project_lines.push(line.clone());
}
}
project_lines.join("\n")
} else {
String::new()
}
} }
fn get_selected_day_text(&self) -> String { fn get_selected_day_text(&self) -> String {
@ -1134,178 +1200,180 @@ impl App {
} }
fn jump_to_next_project(&mut self) { fn jump_to_next_project(&mut self) {
// Find the current project, then jump to first entry of next project // Find all project headers and their first entry positions
let mut current_date = String::new(); let mut projects = Vec::new(); // (first_entry_index)
let mut current_project = String::new(); let mut entry_count = 0;
let mut selected_project = String::new(); let mut expecting_first_entry = false;
let mut selected_date = String::new();
let mut frame_count = 0;
let mut found_current = false;
// Find which project we're currently in for line in &self.log_view_content {
for line in &self.log_view_content.clone() { if line.starts_with(" ") && !line.starts_with(" ") && !line.starts_with("\t") {
if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") { // This is a project header - next entry will be first entry of this project
current_date = line.clone(); expecting_first_entry = true;
} else if line.starts_with(" ") && !line.starts_with("\t") { } else if self.is_entry_line(line) {
current_project = line.clone(); if expecting_first_entry {
// This is the first entry of a project
projects.push(entry_count);
expecting_first_entry = false;
} }
entry_count += 1;
if self.is_entry_line(line) {
if frame_count == self.log_view_selected {
selected_project = current_project.clone();
selected_date = current_date.clone();
found_current = true;
break;
}
frame_count += 1;
} }
} }
if !found_current { // Find which project contains our current selection and jump to next
for i in 0..projects.len() {
let project_start = projects[i];
let project_end = if i + 1 < projects.len() {
projects[i + 1]
} else {
entry_count
};
if project_start <= self.log_view_selected && self.log_view_selected < project_end {
// Found current project, jump to next if it exists
if i + 1 < projects.len() {
self.log_view_selected = projects[i + 1];
}
return; return;
} }
// Now find the next project within the same day first, then next day
current_project = String::new();
current_date = String::new();
let mut past_selected = false;
frame_count = 0;
for line in &self.log_view_content.clone() {
if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") {
current_date = line.clone();
} else if line.starts_with(" ") && !line.starts_with("\t") {
current_project = line.clone();
if past_selected && current_project != selected_project {
// This is the next project, select its first entry
self.log_view_selected = frame_count;
return;
}
}
if self.is_entry_line(line) {
if current_project == selected_project && current_date == selected_date {
past_selected = true;
}
frame_count += 1;
}
} }
} }
fn jump_to_previous_project(&mut self) { fn jump_to_previous_project(&mut self) {
// Find the previous project and jump to its first entry // Find all project headers and their first entry positions
let mut current_project = String::new(); let mut projects = Vec::new(); // (first_entry_index)
let mut _selected_project = String::new(); let mut entry_count = 0;
let mut frame_count = 0; let mut expecting_first_entry = false;
let mut previous_project_first_entry = None;
let mut last_different_project_entry = None;
// Find which project we're currently in for line in &self.log_view_content {
for line in &self.log_view_content.clone() { if line.starts_with(" ") && !line.starts_with(" ") && !line.starts_with("\t") {
if line.starts_with(" ") && !line.starts_with("\t") { // This is a project header - next entry will be first entry of this project
if !current_project.is_empty() && current_project != _selected_project { expecting_first_entry = true;
last_different_project_entry = previous_project_first_entry; } else if self.is_entry_line(line) {
if expecting_first_entry {
// This is the first entry of a project
projects.push(entry_count);
expecting_first_entry = false;
}
entry_count += 1;
} }
current_project = line.clone();
previous_project_first_entry = Some(frame_count);
} }
if self.is_entry_line(line) { // Find which project contains our current selection and jump to previous
if frame_count == self.log_view_selected { for i in 0..projects.len() {
_selected_project = current_project.clone(); let project_start = projects[i];
// Jump to the last different project we saw let project_end = if i + 1 < projects.len() {
if let Some(entry) = last_different_project_entry { projects[i + 1]
self.log_view_selected = entry; } else {
entry_count
};
if project_start <= self.log_view_selected && self.log_view_selected < project_end {
// Found current project, jump to previous if it exists
if i > 0 {
self.log_view_selected = projects[i - 1];
} }
return; return;
} }
frame_count += 1;
}
} }
} }
fn jump_to_next_day(&mut self) { fn update_log_view_scroll(&mut self) {
// Find the current day, then jump to first entry of next day // Calculate which content line the selected entry is on
let mut current_date = String::new(); let mut content_line: usize = 0;
let mut selected_date = String::new();
let mut frame_count = 0; let mut frame_count = 0;
let mut found_current = false; for line in &self.log_view_content {
// Find which day we're currently in
for line in &self.log_view_content.clone() {
if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") {
current_date = line.clone();
}
if self.is_entry_line(line) { if self.is_entry_line(line) {
if frame_count == self.log_view_selected { if frame_count == self.log_view_selected {
selected_date = current_date.clone();
found_current = true;
break; break;
} }
frame_count += 1; frame_count += 1;
} }
content_line += 1;
} }
if !found_current { // Center the selection in the viewport
return; // Assume a reasonable viewport height for centering calculation
let viewport_center_offset: usize = 10; // Lines from top to center selection
self.log_view_scroll = content_line.saturating_sub(viewport_center_offset);
} }
// Now find the next day and jump to its first entry fn jump_to_next_day(&mut self) {
current_date = String::new(); // Find all day headers and their first entry positions
let mut past_selected = false; let mut days = Vec::new(); // (first_entry_index)
frame_count = 0; let mut entry_count = 0;
let mut current_day_seen = false;
for line in &self.log_view_content.clone() { for line in &self.log_view_content {
if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") { if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") {
if past_selected && line != &selected_date { // This is a day header - mark that we'll record the next entry
// This is the next day, continue to find its first entry current_day_seen = true;
current_date = line.clone(); } else if self.is_entry_line(line) && current_day_seen {
} else if line == &selected_date { // This is the first entry of a day
past_selected = true; days.push(entry_count);
} current_day_seen = false;
} }
if self.is_entry_line(line) { if self.is_entry_line(line) {
if past_selected && !current_date.is_empty() && current_date != selected_date { entry_count += 1;
// First entry of next day
self.log_view_selected = frame_count;
return;
} }
frame_count += 1; }
// Find which day contains our current selection and jump to next
for i in 0..days.len() {
let day_start = days[i];
let day_end = if i + 1 < days.len() {
days[i + 1]
} else {
entry_count
};
if day_start <= self.log_view_selected && self.log_view_selected < day_end {
// Found current day, jump to next if it exists
if i + 1 < days.len() {
self.log_view_selected = days[i + 1];
}
return;
} }
} }
} }
fn jump_to_previous_day(&mut self) { fn jump_to_previous_day(&mut self) {
// Find the previous day and jump to its first entry // Find all day headers and their first entry positions
let mut current_date = String::new(); let mut days = Vec::new(); // (first_entry_index)
let mut _selected_date = String::new(); let mut entry_count = 0;
let mut frame_count = 0; let mut current_day_seen = false;
let mut last_different_day_entry = None;
let mut previous_day_first_entry = None;
// Find which day we're currently in for line in &self.log_view_content {
for line in &self.log_view_content.clone() {
if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") { if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") {
if !current_date.is_empty() && current_date != _selected_date { // This is a day header - mark that we'll record the next entry
last_different_day_entry = previous_day_first_entry; current_day_seen = true;
} } else if self.is_entry_line(line) && current_day_seen {
current_date = line.clone(); // This is the first entry of a day
previous_day_first_entry = Some(frame_count); days.push(entry_count);
current_day_seen = false;
} }
if self.is_entry_line(line) { if self.is_entry_line(line) {
if frame_count == self.log_view_selected { entry_count += 1;
_selected_date = current_date.clone(); }
// Jump to the last different day we saw }
if let Some(entry) = last_different_day_entry {
self.log_view_selected = entry; // Find which day contains our current selection and jump to previous
for i in 0..days.len() {
let day_start = days[i];
let day_end = if i + 1 < days.len() {
days[i + 1]
} else {
entry_count
};
if day_start <= self.log_view_selected && self.log_view_selected < day_end {
// Found current day, jump to previous if it exists
if i > 0 {
self.log_view_selected = days[i - 1];
} }
return; return;
} }
frame_count += 1;
}
} }
} }

View file

@ -901,9 +901,11 @@ fn render_log_view(frame: &mut Frame, app: &App) {
.collect() .collect()
}; };
// Use the scroll position calculated by the app
let paragraph = Paragraph::new(text_lines) let paragraph = Paragraph::new(text_lines)
.block(block) .block(block)
.wrap(ratatui::widgets::Wrap { trim: false }); .wrap(ratatui::widgets::Wrap { trim: false })
.scroll((app.log_view_scroll as u16, 0));
frame.render_widget(paragraph, chunks[0]); frame.render_widget(paragraph, chunks[0]);