Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
145 lines
4.4 KiB
Rust
145 lines
4.4 KiB
Rust
use crate::app::App;
|
|
|
|
pub fn render(frame: &mut ratatui::Frame, app: &mut App) {
|
|
use ratatui::layout::{Constraint, Layout};
|
|
use ratatui::style::Style;
|
|
use ratatui::widgets::Paragraph;
|
|
|
|
let outer = Layout::vertical([
|
|
Constraint::Length(1),
|
|
Constraint::Min(1),
|
|
Constraint::Length(1),
|
|
])
|
|
.split(frame.area());
|
|
|
|
// ── Title bar ──────────────────────────────────────────────────
|
|
let title_text = if app.is_loaded() {
|
|
let name = app.file_name().unwrap_or("unknown");
|
|
let cursor_display = if app.total_lines() == 0 {
|
|
0
|
|
} else {
|
|
app.cursor_line + 1
|
|
};
|
|
format!(" {} [{}/{}]", name, cursor_display, app.total_lines())
|
|
} else {
|
|
" Log Viewer".to_string()
|
|
};
|
|
frame.render_widget(
|
|
Paragraph::new(title_text).style(Style::default().bold()),
|
|
outer[0],
|
|
);
|
|
|
|
// ── Content area ───────────────────────────────────────────────
|
|
if !app.is_loaded() {
|
|
frame.render_widget(Paragraph::new(" No file loaded"), outer[1]);
|
|
} else {
|
|
render_content(frame, app, outer[1]);
|
|
}
|
|
|
|
// ── Status bar ─────────────────────────────────────────────────
|
|
frame.render_widget(
|
|
Paragraph::new(" j/k:scroll d/u:half-page f/b:page G/gg:jump Tab:format q:quit"),
|
|
outer[2],
|
|
);
|
|
}
|
|
|
|
fn render_content(frame: &mut ratatui::Frame, app: &mut App, area: ratatui::layout::Rect) {
|
|
use ratatui::style::{Color, Style};
|
|
use ratatui::text::Line;
|
|
use ratatui::widgets::Paragraph;
|
|
|
|
let content_width = area.width as usize;
|
|
let content_height = area.height as usize;
|
|
|
|
app.content_height = area.height;
|
|
app.content_width = area.width;
|
|
|
|
let total_lines = app.total_lines();
|
|
let line_num_width = if total_lines > 0 {
|
|
total_lines.to_string().len()
|
|
} else {
|
|
0
|
|
};
|
|
let gutter_width = if total_lines > 0 {
|
|
line_num_width + 1 + 1
|
|
} else {
|
|
0
|
|
};
|
|
|
|
let actual_content_width = content_width.saturating_sub(gutter_width);
|
|
|
|
if content_height == 0 || actual_content_width == 0 {
|
|
return;
|
|
}
|
|
|
|
app.recompute_wrap_cache(actual_content_width);
|
|
let mut visual_acc: usize = 0;
|
|
let mut start_logical: usize = 0;
|
|
let mut offset_in_line: usize = 0;
|
|
let v_offset = app.v_offset;
|
|
|
|
for (i, &h) in app.visual_heights.iter().enumerate() {
|
|
if visual_acc.saturating_add(h) > v_offset {
|
|
start_logical = i;
|
|
offset_in_line = v_offset.saturating_sub(visual_acc);
|
|
break;
|
|
}
|
|
visual_acc += h;
|
|
if i == app.visual_heights.len() - 1 {
|
|
start_logical = i;
|
|
offset_in_line = 0;
|
|
}
|
|
}
|
|
|
|
let mut lines: Vec<Line> = Vec::new();
|
|
let mut current_visual_offset: usize = 0;
|
|
let available_rows = content_height;
|
|
|
|
for logical_line in start_logical..total_lines {
|
|
let wrapped = &app.wrap_cache[logical_line];
|
|
let start_row = if logical_line == start_logical {
|
|
offset_in_line
|
|
} else {
|
|
0
|
|
};
|
|
|
|
for (visual_row, text) in wrapped.iter().enumerate().skip(start_row) {
|
|
if current_visual_offset >= available_rows {
|
|
break;
|
|
}
|
|
|
|
let is_cursor = logical_line == app.cursor_line;
|
|
let style = if is_cursor {
|
|
Style::default().bg(Color::DarkGray)
|
|
} else {
|
|
Style::default()
|
|
};
|
|
|
|
let gutter_text = if visual_row == 0 {
|
|
format!(
|
|
"{:>width$} \u{2502}",
|
|
logical_line + 1,
|
|
width = line_num_width
|
|
)
|
|
} else {
|
|
format!("{:width$} \u{2502}", "", width = line_num_width)
|
|
};
|
|
|
|
let full_line = format!("{}{}", gutter_text, text);
|
|
lines.push(Line::styled(full_line, style));
|
|
|
|
current_visual_offset += 1;
|
|
}
|
|
|
|
if current_visual_offset >= available_rows {
|
|
break;
|
|
}
|
|
}
|
|
|
|
while lines.len() < available_rows {
|
|
lines.push(Line::styled(String::new(), Style::default()));
|
|
}
|
|
|
|
frame.render_widget(Paragraph::new(lines), area);
|
|
}
|