Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ tokio = { version = "1", features = ["full"] }
toml = { version = "0.9", features = ["preserve_order"] }
uuid = { version = "1", features = ["v4", "fast-rng"] }
xx = { version = "2", features = ["fslock", "hash"] }
ratatui = "0.29"
ratatui = { version = "0.29", features = ["unstable-rendered-line-info"] }
crossterm = "0.28"
fuzzy-matcher = "0.3.7"
glob = "0.3"
Expand Down
18 changes: 6 additions & 12 deletions src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -971,7 +971,7 @@ impl App {
})
.collect();
// Sort by score descending (best matches first)
scored.sort_by(|a, b| b.1.cmp(&a.1));
scored.sort_by_key(|s| s.1);
Comment thread
dimmyjing marked this conversation as resolved.
Outdated
Comment thread
dimmyjing marked this conversation as resolved.
Outdated
scored.into_iter().map(|(d, _)| d).collect()
};

Expand Down Expand Up @@ -1081,7 +1081,7 @@ impl App {
self.log_follow = !self.log_follow;
if self.log_follow && !self.log_content.is_empty() {
// Jump to bottom when enabling follow
self.log_scroll = self.log_content.len().saturating_sub(20);
self.log_scroll = self.log_content.len();
}
}

Expand Down Expand Up @@ -1241,7 +1241,7 @@ impl App {

pub fn scroll_logs_down(&mut self) {
if self.log_content.len() > 20 {
let max_scroll = self.log_content.len().saturating_sub(20);
let max_scroll = self.log_content.len();
self.log_scroll = (self.log_scroll + 1).min(max_scroll);
}
}
Expand All @@ -1254,7 +1254,7 @@ impl App {
pub fn scroll_logs_page_down(&mut self, visible_lines: usize) {
let half_page = visible_lines / 2;
if self.log_content.len() > visible_lines {
let max_scroll = self.log_content.len().saturating_sub(visible_lines);
let max_scroll = self.log_content.len();
self.log_scroll = (self.log_scroll + half_page).min(max_scroll);
}
}
Expand Down Expand Up @@ -1373,16 +1373,10 @@ impl App {

// Auto-scroll to bottom when in follow mode
if self.log_follow {
if self.log_content.len() > 20 {
self.log_scroll = self.log_content.len().saturating_sub(20);
} else {
self.log_scroll = 0;
}
self.log_scroll = self.log_content.len();
} else if prev_len == 0 {
// First load - start at bottom
if self.log_content.len() > 20 {
self.log_scroll = self.log_content.len().saturating_sub(20);
}
self.log_scroll = self.log_content.len();
}
// If not following and not first load, keep scroll position
}
Expand Down
2 changes: 1 addition & 1 deletion src/tui/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ fn handle_logs_event(
KeyCode::Char('G') => {
app.log_follow = true;
if app.log_content.len() > 20 {
app.log_scroll = app.log_content.len().saturating_sub(20);
app.log_scroll = app.log_content.len();
}
Ok(None)
}
Expand Down
46 changes: 33 additions & 13 deletions src/tui/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const CYAN: Color = Color::Rgb(34, 211, 238); // #22d3ee - for available/config-
const BAR_FULL: char = '█';
const BAR_EMPTY: char = '░';

const LOG_VIEWPORT_MAX_LINES: usize = 100;

/// UTF-8 safe string truncation from the end, returning "...{suffix}" if too long.
/// Uses character count instead of byte length to avoid panics on non-ASCII.
fn truncate_path_end(s: &str, max_chars: usize) -> String {
Expand Down Expand Up @@ -879,33 +881,44 @@ fn draw_log_panel(f: &mut Frame, area: Rect, app: &App, daemon_id: &str) {
};
let title = format!(" Logs: {daemon_id}{follow_indicator}{search_indicator} ");

let log_skip = app.log_scroll.saturating_sub(LOG_VIEWPORT_MAX_LINES);
let log_take = app.log_scroll.clamp(1, LOG_VIEWPORT_MAX_LINES);

let visible_height = area.height.saturating_sub(2) as usize;
let visible_lines: Vec<Line> = app
let mut visible_lines: Vec<Line> = app
.log_content
.iter()
.enumerate()
.skip(app.log_scroll)
.take(visible_height)
.skip(log_skip)
.take(log_take)
.map(|(line_idx, line)| (line_idx, clean_log_line(line)))
.map(|(line_idx, line)| highlight_log_line(line, line_idx, app))
.collect();
if visible_lines.len() < LOG_VIEWPORT_MAX_LINES {
let padding = LOG_VIEWPORT_MAX_LINES - visible_lines.len();
let mut padding_vec = std::iter::repeat_n(Line::from(""), padding).collect::<Vec<Line>>();
padding_vec.extend(visible_lines);
visible_lines = padding_vec;
}

let block = Block::default()
.title(title)
.title_style(Style::default().fg(RED).bold())
.borders(Borders::ALL)
.border_style(Style::default().fg(RED));
let inner_width = block.inner(area).width;
let logs = Paragraph::new(visible_lines)
.block(
Block::default()
.title(title)
.title_style(Style::default().fg(RED).bold())
.borders(Borders::ALL)
.border_style(Style::default().fg(RED)),
)
.block(block)
.wrap(Wrap { trim: false });
let line_count = logs.line_count(inner_width);
let logs = logs.scroll((line_count as u16 - area.height, 0));
Comment thread
dimmyjing marked this conversation as resolved.
Outdated

f.render_widget(logs, area);

// Render scrollbar if there are more lines than visible
let total_lines = app.log_content.len();
if total_lines > visible_height {
let mut scrollbar_state = ScrollbarState::new(total_lines.saturating_sub(visible_height))
.position(app.log_scroll);
let mut scrollbar_state = ScrollbarState::new(total_lines).position(app.log_scroll);
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("▲"))
.end_symbol(Some("▼"))
Expand Down Expand Up @@ -1008,8 +1021,15 @@ fn draw_log_search_bar(f: &mut Frame, area: Rect, app: &App) {
f.render_widget(search_bar, area);
}

fn clean_log_line(line: &str) -> String {
// remove clear screen code
let line = line.replace("\x1b[2J", "");
// replace tabs with spaces
line.replace("\t", " ")
Comment thread
dimmyjing marked this conversation as resolved.
Outdated
}

/// Highlight a log line with syntax coloring and search match highlighting
fn highlight_log_line(line: &str, line_idx: usize, app: &App) -> Line<'static> {
fn highlight_log_line(line: String, line_idx: usize, app: &App) -> Line<'static> {
let is_match = app.log_search_matches.contains(&line_idx);
let is_current_match = app
.log_search_matches
Expand Down