Select grouped by project or day
This commit is contained in:
parent
56f9322e36
commit
5aaa57a6a2
2 changed files with 480 additions and 43 deletions
355
src/app.rs
355
src/app.rs
|
|
@ -35,6 +35,12 @@ pub enum LogViewGrouping {
|
||||||
ByProject,
|
ByProject,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum LogViewSelection {
|
||||||
|
Entry, // Individual entry selected
|
||||||
|
Project, // Whole project selected (ByProject mode only)
|
||||||
|
Day, // Whole day selected
|
||||||
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub state: AppState,
|
pub state: AppState,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
|
|
@ -48,6 +54,7 @@ pub struct App {
|
||||||
pub reassign_project_cursor: usize,
|
pub reassign_project_cursor: usize,
|
||||||
pub log_view_period: LogViewPeriod,
|
pub log_view_period: LogViewPeriod,
|
||||||
pub log_view_grouping: LogViewGrouping,
|
pub log_view_grouping: LogViewGrouping,
|
||||||
|
pub log_view_selection_level: LogViewSelection,
|
||||||
pub log_view_frames: Vec<WatsonFrame>,
|
pub log_view_frames: Vec<WatsonFrame>,
|
||||||
pub log_view_content: Vec<String>,
|
pub log_view_content: Vec<String>,
|
||||||
pub log_view_scroll: usize,
|
pub log_view_scroll: usize,
|
||||||
|
|
@ -62,6 +69,14 @@ pub enum NewEntryMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
// Helper to determine if a line is an entry line based on grouping mode
|
||||||
|
fn is_entry_line(&self, line: &str) -> bool {
|
||||||
|
match self.log_view_grouping {
|
||||||
|
LogViewGrouping::ByProject => line.starts_with("\t\t"),
|
||||||
|
LogViewGrouping::ByDate => line.starts_with('\t') && !line.starts_with("\t\t"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
state: AppState::load()?,
|
state: AppState::load()?,
|
||||||
|
|
@ -76,6 +91,7 @@ impl App {
|
||||||
reassign_project_cursor: 0,
|
reassign_project_cursor: 0,
|
||||||
log_view_period: LogViewPeriod::Day,
|
log_view_period: LogViewPeriod::Day,
|
||||||
log_view_grouping: LogViewGrouping::ByDate,
|
log_view_grouping: LogViewGrouping::ByDate,
|
||||||
|
log_view_selection_level: LogViewSelection::Entry,
|
||||||
log_view_frames: Vec::new(),
|
log_view_frames: Vec::new(),
|
||||||
log_view_content: Vec::new(),
|
log_view_content: Vec::new(),
|
||||||
log_view_scroll: 0,
|
log_view_scroll: 0,
|
||||||
|
|
@ -321,6 +337,7 @@ impl App {
|
||||||
self.log_view_period = LogViewPeriod::Day;
|
self.log_view_period = LogViewPeriod::Day;
|
||||||
self.log_view_scroll = 0;
|
self.log_view_scroll = 0;
|
||||||
self.log_view_selected = 0;
|
self.log_view_selected = 0;
|
||||||
|
self.log_view_selection_level = LogViewSelection::Entry;
|
||||||
self.load_log_content()?;
|
self.load_log_content()?;
|
||||||
self.needs_clear = true;
|
self.needs_clear = true;
|
||||||
}
|
}
|
||||||
|
|
@ -328,6 +345,7 @@ impl App {
|
||||||
self.log_view_period = LogViewPeriod::Week;
|
self.log_view_period = LogViewPeriod::Week;
|
||||||
self.log_view_scroll = 0;
|
self.log_view_scroll = 0;
|
||||||
self.log_view_selected = 0;
|
self.log_view_selected = 0;
|
||||||
|
self.log_view_selection_level = LogViewSelection::Entry;
|
||||||
self.load_log_content()?;
|
self.load_log_content()?;
|
||||||
self.needs_clear = true;
|
self.needs_clear = true;
|
||||||
}
|
}
|
||||||
|
|
@ -335,24 +353,75 @@ impl App {
|
||||||
self.log_view_period = LogViewPeriod::Month;
|
self.log_view_period = LogViewPeriod::Month;
|
||||||
self.log_view_scroll = 0;
|
self.log_view_scroll = 0;
|
||||||
self.log_view_selected = 0;
|
self.log_view_selected = 0;
|
||||||
|
self.log_view_selection_level = LogViewSelection::Entry;
|
||||||
self.load_log_content()?;
|
self.load_log_content()?;
|
||||||
self.needs_clear = true;
|
self.needs_clear = true;
|
||||||
}
|
}
|
||||||
KeyCode::Char('j') | KeyCode::Down => {
|
KeyCode::Char('j') | KeyCode::Down => {
|
||||||
if self.log_view_selected < self.log_view_frames.len().saturating_sub(1) {
|
match self.log_view_selection_level {
|
||||||
self.log_view_selected += 1;
|
LogViewSelection::Entry => {
|
||||||
|
// Move to next entry
|
||||||
|
if self.log_view_selected < self.log_view_frames.len().saturating_sub(1) {
|
||||||
|
self.log_view_selected += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogViewSelection::Project => {
|
||||||
|
// Jump to next project group
|
||||||
|
self.jump_to_next_project();
|
||||||
|
}
|
||||||
|
LogViewSelection::Day => {
|
||||||
|
// Jump to next day
|
||||||
|
self.jump_to_next_day();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('k') | KeyCode::Up => {
|
KeyCode::Char('k') | KeyCode::Up => {
|
||||||
if self.log_view_selected > 0 {
|
match self.log_view_selection_level {
|
||||||
self.log_view_selected -= 1;
|
LogViewSelection::Entry => {
|
||||||
|
// Move to previous entry
|
||||||
|
if self.log_view_selected > 0 {
|
||||||
|
self.log_view_selected -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogViewSelection::Project => {
|
||||||
|
// Jump to previous project group
|
||||||
|
self.jump_to_previous_project();
|
||||||
|
}
|
||||||
|
LogViewSelection::Day => {
|
||||||
|
// Jump to previous day
|
||||||
|
self.jump_to_previous_day();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('e') => {
|
KeyCode::Char('e') => {
|
||||||
self.edit_selected_frame()?;
|
// Only allow edit when selecting individual entry
|
||||||
|
if matches!(self.log_view_selection_level, LogViewSelection::Entry) {
|
||||||
|
self.edit_selected_frame()?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('x') => {
|
KeyCode::Char('x') => {
|
||||||
self.delete_selected_frame()?;
|
// Only allow delete when selecting individual entry
|
||||||
|
if matches!(self.log_view_selection_level, LogViewSelection::Entry) {
|
||||||
|
self.delete_selected_frame()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('h') | KeyCode::Left => {
|
||||||
|
// Zoom out selection level
|
||||||
|
self.log_view_selection_level = match (&self.log_view_selection_level, &self.log_view_grouping) {
|
||||||
|
(LogViewSelection::Day, _) => LogViewSelection::Day, // Already at highest
|
||||||
|
(LogViewSelection::Project, _) => LogViewSelection::Day,
|
||||||
|
(LogViewSelection::Entry, LogViewGrouping::ByProject) => LogViewSelection::Project,
|
||||||
|
(LogViewSelection::Entry, LogViewGrouping::ByDate) => LogViewSelection::Day,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
KeyCode::Char('l') | KeyCode::Right => {
|
||||||
|
// Zoom in 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,
|
||||||
|
(LogViewSelection::Day, LogViewGrouping::ByProject) => LogViewSelection::Project,
|
||||||
|
(LogViewSelection::Day, LogViewGrouping::ByDate) => LogViewSelection::Entry,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
KeyCode::Char('c') => {
|
KeyCode::Char('c') => {
|
||||||
self.copy_log_to_clipboard()?;
|
self.copy_log_to_clipboard()?;
|
||||||
|
|
@ -364,6 +433,7 @@ impl App {
|
||||||
LogViewGrouping::ByProject => LogViewGrouping::ByDate,
|
LogViewGrouping::ByProject => LogViewGrouping::ByDate,
|
||||||
};
|
};
|
||||||
self.log_view_selected = 0;
|
self.log_view_selected = 0;
|
||||||
|
self.log_view_selection_level = LogViewSelection::Entry;
|
||||||
self.format_log_entries();
|
self.format_log_entries();
|
||||||
self.needs_clear = true;
|
self.needs_clear = true;
|
||||||
}
|
}
|
||||||
|
|
@ -572,15 +642,27 @@ impl App {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Join all the formatted log content
|
// Determine what to copy based on selection level
|
||||||
let text = self.log_view_content.join("\n");
|
let text = match self.log_view_selection_level {
|
||||||
|
LogViewSelection::Entry => {
|
||||||
|
// Copy just the selected entry
|
||||||
|
self.get_selected_entry_text()
|
||||||
|
}
|
||||||
|
LogViewSelection::Project => {
|
||||||
|
// Copy the selected project group
|
||||||
|
self.get_selected_project_text()
|
||||||
|
}
|
||||||
|
LogViewSelection::Day => {
|
||||||
|
// Copy the entire day
|
||||||
|
self.get_selected_day_text()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 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));
|
||||||
}
|
}
|
||||||
// Success - clipboard instance stays alive in App struct
|
|
||||||
} 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
|
||||||
match arboard::Clipboard::new() {
|
match arboard::Clipboard::new() {
|
||||||
|
|
@ -588,7 +670,6 @@ impl App {
|
||||||
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));
|
||||||
}
|
}
|
||||||
// Store it for future use
|
|
||||||
self.clipboard = Some(clipboard);
|
self.clipboard = Some(clipboard);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -600,6 +681,260 @@ impl App {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_selected_entry_text(&self) -> String {
|
||||||
|
// Find the entry line corresponding to the selected frame
|
||||||
|
let entry_lines: Vec<&String> = self
|
||||||
|
.log_view_content
|
||||||
|
.iter()
|
||||||
|
.filter(|l| self.is_entry_line(l))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if self.log_view_selected < entry_lines.len() {
|
||||||
|
entry_lines[self.log_view_selected].to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
for line in &self.log_view_content {
|
||||||
|
if 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());
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
frame_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current_project_lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_selected_day_text(&self) -> String {
|
||||||
|
// Find which day the selected entry belongs to
|
||||||
|
let mut current_day_lines = Vec::new();
|
||||||
|
let mut frame_count = 0;
|
||||||
|
let mut found = false;
|
||||||
|
|
||||||
|
for line in &self.log_view_content {
|
||||||
|
if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") {
|
||||||
|
// This is a date header
|
||||||
|
if found {
|
||||||
|
break; // We've collected the target day
|
||||||
|
}
|
||||||
|
current_day_lines.clear();
|
||||||
|
current_day_lines.push(line.clone());
|
||||||
|
} else if line.starts_with('\t') || line.starts_with(" ") {
|
||||||
|
// Add all lines within the day (project headers and entries)
|
||||||
|
current_day_lines.push(line.clone());
|
||||||
|
|
||||||
|
// Count only actual entries
|
||||||
|
if self.is_entry_line(line) {
|
||||||
|
if frame_count == self.log_view_selected {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
frame_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current_day_lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
frame_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found_current {
|
||||||
|
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) {
|
||||||
|
// Find the previous project and jump to its first entry
|
||||||
|
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 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.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") {
|
||||||
|
if !current_project.is_empty() && current_project != selected_project {
|
||||||
|
last_different_project_entry = previous_project_first_entry;
|
||||||
|
}
|
||||||
|
current_project = line.clone();
|
||||||
|
previous_project_first_entry = Some(frame_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.is_entry_line(line) {
|
||||||
|
if frame_count == self.log_view_selected {
|
||||||
|
selected_project = current_project.clone();
|
||||||
|
selected_date = current_date.clone();
|
||||||
|
// Jump to the last different project we saw
|
||||||
|
if let Some(entry) = last_different_project_entry {
|
||||||
|
self.log_view_selected = entry;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
frame_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found_current {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now find the next day and jump to its first entry
|
||||||
|
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(" ") {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
frame_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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(" ") {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
frame_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn move_selection(&mut self, delta: i32) {
|
fn move_selection(&mut self, delta: i32) {
|
||||||
let items_len = match self.state.current_pane {
|
let items_len = match self.state.current_pane {
|
||||||
0 => self.state.permanent_items.len(),
|
0 => self.state.permanent_items.len(),
|
||||||
|
|
|
||||||
168
src/ui.rs
168
src/ui.rs
|
|
@ -7,7 +7,7 @@ use ratatui::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{App, LogViewGrouping, LogViewPeriod, NewEntryMode, Screen},
|
app::{App, LogViewGrouping, LogViewPeriod, LogViewSelection, NewEntryMode, Screen},
|
||||||
state::{AppState, TimeItem},
|
state::{AppState, TimeItem},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -461,36 +461,129 @@ fn render_log_view(frame: &mut Frame, app: &App) {
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.style(Style::default().fg(ACTIVE_COLOR));
|
.style(Style::default().fg(ACTIVE_COLOR));
|
||||||
|
|
||||||
// Build list items with selection highlighting
|
// Build list items with selection highlighting based on selection level
|
||||||
let items: Vec<ListItem> = app
|
let items: Vec<ListItem> = {
|
||||||
.log_view_content
|
// Pre-calculate which line indices should be highlighted
|
||||||
.iter()
|
let mut selected_date = String::new();
|
||||||
.enumerate()
|
let mut selected_project = String::new();
|
||||||
.map(|(idx, line)| {
|
let mut frame_count = 0;
|
||||||
// Check if this is a frame line (starts with tab) and matches selected frame
|
let mut selected_line_start = 0;
|
||||||
let is_selected = if line.starts_with('\t') {
|
let mut selected_line_end = 0;
|
||||||
// Count how many frame lines we've seen so far
|
|
||||||
let frame_idx = app.log_view_content[..=idx]
|
// First pass: find the date/project containing the selected frame
|
||||||
.iter()
|
let mut current_date = String::new();
|
||||||
.filter(|l| l.starts_with('\t'))
|
let mut current_project = String::new();
|
||||||
.count()
|
|
||||||
.saturating_sub(1);
|
// Determine what counts as an entry based on grouping mode
|
||||||
frame_idx == app.log_view_selected
|
let is_by_project = matches!(app.log_view_grouping, LogViewGrouping::ByProject);
|
||||||
|
|
||||||
|
for (idx, line) in app.log_view_content.iter().enumerate() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only count actual entries (not all tab-indented lines)
|
||||||
|
let is_entry = if is_by_project {
|
||||||
|
line.starts_with("\t\t") // Double tab for ByProject
|
||||||
} else {
|
} else {
|
||||||
false
|
line.starts_with('\t') && !line.starts_with("\t\t") // Single tab for ByDate
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if is_entry {
|
||||||
|
if frame_count == app.log_view_selected {
|
||||||
|
selected_date = current_date.clone();
|
||||||
|
selected_project = current_project.clone();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
frame_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: determine the range of lines to highlight
|
||||||
|
match app.log_view_selection_level {
|
||||||
|
LogViewSelection::Entry => {
|
||||||
|
// Just find the specific entry line
|
||||||
|
frame_count = 0;
|
||||||
|
for (idx, line) in app.log_view_content.iter().enumerate() {
|
||||||
|
let is_entry = if is_by_project {
|
||||||
|
line.starts_with("\t\t")
|
||||||
|
} else {
|
||||||
|
line.starts_with('\t') && !line.starts_with("\t\t")
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_entry {
|
||||||
|
if frame_count == app.log_view_selected {
|
||||||
|
selected_line_start = idx;
|
||||||
|
selected_line_end = idx;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
frame_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogViewSelection::Project => {
|
||||||
|
// Find the range of the selected project (within the same day)
|
||||||
|
let mut in_target_project = false;
|
||||||
|
current_date = String::new();
|
||||||
|
|
||||||
|
for (idx, line) in app.log_view_content.iter().enumerate() {
|
||||||
|
if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") {
|
||||||
|
current_date = line.clone();
|
||||||
|
if in_target_project {
|
||||||
|
break; // End of project group
|
||||||
|
}
|
||||||
|
} else if line.starts_with(" ") && !line.starts_with("\t") {
|
||||||
|
if current_date == selected_date && line == &selected_project {
|
||||||
|
selected_line_start = idx;
|
||||||
|
in_target_project = true;
|
||||||
|
} else if in_target_project {
|
||||||
|
break; // Different project
|
||||||
|
}
|
||||||
|
} else if in_target_project && line.starts_with('\t') {
|
||||||
|
selected_line_end = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogViewSelection::Day => {
|
||||||
|
// Find the range of the selected day
|
||||||
|
let mut in_target_day = false;
|
||||||
|
|
||||||
|
for (idx, line) in app.log_view_content.iter().enumerate() {
|
||||||
|
if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") {
|
||||||
|
if line == &selected_date {
|
||||||
|
selected_line_start = idx;
|
||||||
|
in_target_day = true;
|
||||||
|
} else if in_target_day {
|
||||||
|
break; // End of day
|
||||||
|
}
|
||||||
|
} else if in_target_day && !line.is_empty() {
|
||||||
|
selected_line_end = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third pass: render with highlighting
|
||||||
|
app.log_view_content
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, line)| {
|
||||||
|
let is_selected = idx >= selected_line_start && idx <= selected_line_end;
|
||||||
|
|
||||||
|
let style = if is_selected {
|
||||||
|
Style::default()
|
||||||
|
.fg(ACTIVE_COLOR)
|
||||||
|
.add_modifier(Modifier::REVERSED)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::White)
|
||||||
|
};
|
||||||
|
|
||||||
let style = if is_selected {
|
ListItem::new(Line::from(vec![Span::styled(line.clone(), style)]))
|
||||||
Style::default()
|
})
|
||||||
.fg(ACTIVE_COLOR)
|
.collect()
|
||||||
.add_modifier(Modifier::REVERSED)
|
};
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::White)
|
|
||||||
};
|
|
||||||
|
|
||||||
ListItem::new(Line::from(vec![Span::styled(line.clone(), style)]))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let list = List::new(items).block(block);
|
let list = List::new(items).block(block);
|
||||||
frame.render_widget(list, chunks[0]);
|
frame.render_widget(list, chunks[0]);
|
||||||
|
|
@ -524,13 +617,22 @@ fn render_log_view_help(frame: &mut Frame, _app: &App) {
|
||||||
" - By Project: Groups entries by project within each date",
|
" - By Project: Groups entries by project within each date",
|
||||||
"",
|
"",
|
||||||
"Navigation:",
|
"Navigation:",
|
||||||
"- j/k or ↑/↓: Select entries",
|
"- j/k or ↑/↓: Navigate selection",
|
||||||
"- PageUp/PageDown: Jump 10 entries",
|
" - At Entry level: Move to next/previous entry",
|
||||||
|
" - At Project level: Jump to next/previous project group",
|
||||||
|
" - At Day level: Jump to next/previous day",
|
||||||
|
"- h/l or ←/→: Change selection level (Entry ↔ Project ↔ Day)",
|
||||||
|
"- PageUp/PageDown: Jump 10 entries (Entry level only)",
|
||||||
|
"",
|
||||||
|
"Selection Levels:",
|
||||||
|
"- Entry: Select individual entry (can edit/delete/copy)",
|
||||||
|
"- Project: Select whole project group (can copy only)",
|
||||||
|
"- Day: Select entire day (can copy only)",
|
||||||
"",
|
"",
|
||||||
"Actions:",
|
"Actions:",
|
||||||
"- e: Edit the selected entry (opens Watson's editor)",
|
"- e: Edit the selected entry (Entry level only)",
|
||||||
"- x: Delete the selected entry (no confirmation)",
|
"- x: Delete the selected entry (Entry level only)",
|
||||||
"- c: Copy all visible log entries to clipboard",
|
"- c: Copy selection to clipboard (works at all levels)",
|
||||||
"",
|
"",
|
||||||
"Other:",
|
"Other:",
|
||||||
"- ?: Show this help",
|
"- ?: Show this help",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue