Fix scrolling issues
This commit is contained in:
parent
360af2f0cb
commit
ccab4510ea
2 changed files with 212 additions and 142 deletions
350
src/app.rs
350
src/app.rs
|
|
@ -36,11 +36,13 @@ pub enum LogViewDayOrder {
|
|||
ReverseChronological, // Newest first (default)
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum LogViewGrouping {
|
||||
ByDate,
|
||||
ByProject,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum LogViewSelection {
|
||||
Entry, // Individual entry selected
|
||||
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
|
||||
fn is_entry_line(&self, line: &str) -> bool {
|
||||
pub fn is_entry_line(&self, line: &str) -> bool {
|
||||
// Exclude separator lines (time gap markers)
|
||||
if line.trim() == "---" {
|
||||
return false;
|
||||
|
|
@ -489,6 +491,7 @@ impl App {
|
|||
self.needs_clear = true;
|
||||
}
|
||||
KeyCode::Char('j') | KeyCode::Down => {
|
||||
let old_selected = self.log_view_selected;
|
||||
match self.log_view_selection_level {
|
||||
LogViewSelection::Entry => {
|
||||
// Move to next entry
|
||||
|
|
@ -508,8 +511,13 @@ impl App {
|
|||
// 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 => {
|
||||
let old_selected = self.log_view_selected;
|
||||
match self.log_view_selection_level {
|
||||
LogViewSelection::Entry => {
|
||||
// Move to previous entry
|
||||
|
|
@ -529,6 +537,10 @@ impl App {
|
|||
// 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') => {
|
||||
// Only allow edit when selecting individual entry
|
||||
|
|
@ -561,6 +573,7 @@ impl App {
|
|||
}
|
||||
KeyCode::Char('h') | KeyCode::Left => {
|
||||
// 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) {
|
||||
(LogViewSelection::All, _) => LogViewSelection::All, // Already at highest
|
||||
(LogViewSelection::Day, _) => LogViewSelection::All,
|
||||
|
|
@ -571,6 +584,7 @@ impl App {
|
|||
}
|
||||
KeyCode::Char('l') | KeyCode::Right => {
|
||||
// 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) {
|
||||
(LogViewSelection::Entry, _) => LogViewSelection::Entry, // Already at lowest
|
||||
(LogViewSelection::Project, _) => LogViewSelection::Entry,
|
||||
|
|
@ -612,9 +626,11 @@ impl App {
|
|||
KeyCode::PageDown => {
|
||||
self.log_view_selected = (self.log_view_selected + 10)
|
||||
.min(self.log_view_frame_indices.len().saturating_sub(1));
|
||||
self.update_log_view_scroll();
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
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::ByProject => self.format_by_project(),
|
||||
}
|
||||
|
||||
// Update scroll position after formatting
|
||||
self.update_log_view_scroll();
|
||||
}
|
||||
|
||||
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
|
||||
if let Some(ref mut clipboard) = self.clipboard {
|
||||
if let Err(e) = clipboard.set_text(&text) {
|
||||
self.set_status_message(format!("Failed to copy: {}", e));
|
||||
} else {
|
||||
self.set_status_message(format!("Copied {} chars to clipboard", text.len()));
|
||||
}
|
||||
} else {
|
||||
// Try to create a new clipboard if we don't have one
|
||||
|
|
@ -1048,6 +1072,8 @@ impl App {
|
|||
Ok(mut clipboard) => {
|
||||
if let Err(e) = clipboard.set_text(&text) {
|
||||
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);
|
||||
}
|
||||
|
|
@ -1076,30 +1102,70 @@ impl App {
|
|||
}
|
||||
|
||||
fn get_selected_project_text(&self) -> String {
|
||||
// Find which project group the selected entry belongs to
|
||||
let mut current_project_lines = Vec::new();
|
||||
let mut frame_count = 0;
|
||||
let mut found = false;
|
||||
// Find all project headers and their entry ranges
|
||||
let mut projects = Vec::new(); // (first_entry_index, project_line_index)
|
||||
let mut entry_count = 0;
|
||||
let mut expecting_first_entry = false;
|
||||
let mut current_project_line_index = 0;
|
||||
|
||||
for line in &self.log_view_content {
|
||||
if line.starts_with(" ") && !line.starts_with("\t") {
|
||||
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
|
||||
if found {
|
||||
break; // We've collected the target project
|
||||
}
|
||||
current_project_lines.clear();
|
||||
current_project_lines.push(line.clone());
|
||||
current_project_line_index = line_index;
|
||||
expecting_first_entry = true;
|
||||
} else if self.is_entry_line(line) {
|
||||
// Entry within a project
|
||||
current_project_lines.push(line.clone());
|
||||
if frame_count == self.log_view_selected {
|
||||
found = true;
|
||||
if expecting_first_entry {
|
||||
// This is the first entry of a project
|
||||
projects.push((entry_count, current_project_line_index));
|
||||
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 {
|
||||
|
|
@ -1134,177 +1200,179 @@ impl App {
|
|||
}
|
||||
|
||||
fn jump_to_next_project(&mut self) {
|
||||
// Find the current project, then jump to first entry of next project
|
||||
let mut current_date = String::new();
|
||||
let mut current_project = String::new();
|
||||
let mut selected_project = String::new();
|
||||
let mut selected_date = String::new();
|
||||
let mut frame_count = 0;
|
||||
let mut found_current = false;
|
||||
// Find all project headers and their first entry positions
|
||||
let mut projects = Vec::new(); // (first_entry_index)
|
||||
let mut entry_count = 0;
|
||||
let mut expecting_first_entry = false;
|
||||
|
||||
// Find which project 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();
|
||||
} else if line.starts_with(" ") && !line.starts_with("\t") {
|
||||
current_project = line.clone();
|
||||
}
|
||||
|
||||
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;
|
||||
for line in &self.log_view_content {
|
||||
if line.starts_with(" ") && !line.starts_with(" ") && !line.starts_with("\t") {
|
||||
// This is a project header - next entry will be first entry of this project
|
||||
expecting_first_entry = true;
|
||||
} 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;
|
||||
}
|
||||
frame_count += 1;
|
||||
entry_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if !found_current {
|
||||
return;
|
||||
}
|
||||
// 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
|
||||
};
|
||||
|
||||
// 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 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];
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_entry_line(line) {
|
||||
if current_project == selected_project && current_date == selected_date {
|
||||
past_selected = true;
|
||||
}
|
||||
frame_count += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn jump_to_previous_project(&mut self) {
|
||||
// Find the previous project and jump to its first entry
|
||||
let mut current_project = String::new();
|
||||
let mut _selected_project = String::new();
|
||||
let mut frame_count = 0;
|
||||
let mut previous_project_first_entry = None;
|
||||
let mut last_different_project_entry = None;
|
||||
// Find all project headers and their first entry positions
|
||||
let mut projects = Vec::new(); // (first_entry_index)
|
||||
let mut entry_count = 0;
|
||||
let mut expecting_first_entry = false;
|
||||
|
||||
// Find which project we're currently in
|
||||
for line in &self.log_view_content.clone() {
|
||||
if line.starts_with(" ") && !line.starts_with("\t") {
|
||||
if !current_project.is_empty() && current_project != _selected_project {
|
||||
last_different_project_entry = previous_project_first_entry;
|
||||
for line in &self.log_view_content {
|
||||
if line.starts_with(" ") && !line.starts_with(" ") && !line.starts_with("\t") {
|
||||
// This is a project header - next entry will be first entry of this project
|
||||
expecting_first_entry = true;
|
||||
} 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;
|
||||
}
|
||||
current_project = line.clone();
|
||||
previous_project_first_entry = Some(frame_count);
|
||||
entry_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_entry_line(line) {
|
||||
if frame_count == self.log_view_selected {
|
||||
_selected_project = current_project.clone();
|
||||
// Jump to the last different project we saw
|
||||
if let Some(entry) = last_different_project_entry {
|
||||
self.log_view_selected = entry;
|
||||
}
|
||||
return;
|
||||
// Find which project contains our current selection and jump to previous
|
||||
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 previous if it exists
|
||||
if i > 0 {
|
||||
self.log_view_selected = projects[i - 1];
|
||||
}
|
||||
frame_count += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn jump_to_next_day(&mut self) {
|
||||
// Find the current day, then jump to first entry of next day
|
||||
let mut current_date = String::new();
|
||||
let mut selected_date = String::new();
|
||||
fn update_log_view_scroll(&mut self) {
|
||||
// Calculate which content line the selected entry is on
|
||||
let mut content_line: usize = 0;
|
||||
let mut frame_count = 0;
|
||||
let mut found_current = false;
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
for line in &self.log_view_content {
|
||||
if self.is_entry_line(line) {
|
||||
if frame_count == self.log_view_selected {
|
||||
selected_date = current_date.clone();
|
||||
found_current = true;
|
||||
break;
|
||||
}
|
||||
frame_count += 1;
|
||||
}
|
||||
content_line += 1;
|
||||
}
|
||||
|
||||
if !found_current {
|
||||
return;
|
||||
}
|
||||
// Center the selection in the viewport
|
||||
// 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
|
||||
current_date = String::new();
|
||||
let mut past_selected = false;
|
||||
frame_count = 0;
|
||||
fn jump_to_next_day(&mut self) {
|
||||
// Find all day headers and their first entry positions
|
||||
let mut days = Vec::new(); // (first_entry_index)
|
||||
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 past_selected && line != &selected_date {
|
||||
// This is the next day, continue to find its first entry
|
||||
current_date = line.clone();
|
||||
} else if line == &selected_date {
|
||||
past_selected = true;
|
||||
}
|
||||
// This is a day header - mark that we'll record the next entry
|
||||
current_day_seen = true;
|
||||
} else if self.is_entry_line(line) && current_day_seen {
|
||||
// This is the first entry of a day
|
||||
days.push(entry_count);
|
||||
current_day_seen = false;
|
||||
}
|
||||
|
||||
if self.is_entry_line(line) {
|
||||
if past_selected && !current_date.is_empty() && current_date != selected_date {
|
||||
// First entry of next day
|
||||
self.log_view_selected = frame_count;
|
||||
return;
|
||||
entry_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];
|
||||
}
|
||||
frame_count += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn jump_to_previous_day(&mut self) {
|
||||
// Find the previous day and jump to its first entry
|
||||
let mut current_date = String::new();
|
||||
let mut _selected_date = String::new();
|
||||
let mut frame_count = 0;
|
||||
let mut last_different_day_entry = None;
|
||||
let mut previous_day_first_entry = None;
|
||||
// Find all day headers and their first entry positions
|
||||
let mut days = Vec::new(); // (first_entry_index)
|
||||
let mut entry_count = 0;
|
||||
let mut current_day_seen = false;
|
||||
|
||||
// Find which day we're currently in
|
||||
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 !current_date.is_empty() && current_date != _selected_date {
|
||||
last_different_day_entry = previous_day_first_entry;
|
||||
}
|
||||
current_date = line.clone();
|
||||
previous_day_first_entry = Some(frame_count);
|
||||
// This is a day header - mark that we'll record the next entry
|
||||
current_day_seen = true;
|
||||
} else if self.is_entry_line(line) && current_day_seen {
|
||||
// This is the first entry of a day
|
||||
days.push(entry_count);
|
||||
current_day_seen = false;
|
||||
}
|
||||
|
||||
if self.is_entry_line(line) {
|
||||
if frame_count == self.log_view_selected {
|
||||
_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;
|
||||
}
|
||||
return;
|
||||
entry_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 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];
|
||||
}
|
||||
frame_count += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -901,9 +901,11 @@ fn render_log_view(frame: &mut Frame, app: &App) {
|
|||
.collect()
|
||||
};
|
||||
|
||||
// Use the scroll position calculated by the app
|
||||
let paragraph = Paragraph::new(text_lines)
|
||||
.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]);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue