display hyperlinks

This commit is contained in:
Jana Dönszelmann 2026-02-24 23:16:33 +01:00
parent ae5ce58eec
commit f3bc16b3c5
No known key found for this signature in database
6 changed files with 93 additions and 7 deletions

1
Cargo.lock generated
View file

@ -1179,6 +1179,7 @@ name = "rustc-logviz"
version = "0.1.0"
dependencies = [
"clap",
"itertools",
"jiff",
"ratatui",
"ratatui-themes",

View file

@ -16,3 +16,4 @@ tui-widget-list = "0.15"
serde = {version = "1", features = ["derive"]}
serde_json = "1"
thiserror = "2"
itertools = "0.14"

View file

@ -49,6 +49,11 @@ struct Args {
#[arg(long = "logs-dir")]
logs_dir: PathBuf,
/// Path where the compiler source code lives, for links in the TUI to work.
#[arg(global = true)]
#[arg(long = "compiler-root")]
compiler_root: Option<PathBuf>,
#[arg(trailing_var_arg = true)]
#[arg(allow_hyphen_values = true)]
#[arg(global = true)]
@ -60,11 +65,12 @@ fn main() {
preset,
logs_dir,
rest,
compiler_root,
} = Args::parse();
let rustc_log = match preset {
Preset::Show => {
tui::run(logs_dir);
tui::run(logs_dir, compiler_root);
exit(0);
}
Preset::Types => {

View file

@ -188,7 +188,7 @@ impl LogViewer {
}
}
pub fn items(&self, max: usize) -> Option<(Vec<(Rc<LogEntry>, usize)>, usize)> {
pub fn items(&mut self, max: usize) -> Option<(Vec<(Rc<LogEntry>, usize)>, usize)> {
let mut temp_iter = self.curr.iter.clone();
let mut res = Vec::new();
for _ in 0..max {
@ -198,6 +198,10 @@ impl LogViewer {
res.push(i);
}
if self.curr.selection_offset > res.len() {
self.curr.selection_offset = res.len();
}
Some((res, self.curr.selection_offset))
}

View file

@ -1,3 +1,4 @@
use itertools::Itertools;
use ratatui_themes::{Theme, ThemeName};
use std::{
fs::{self, DirEntry},
@ -22,7 +23,7 @@ use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyModifiers},
layout::{Constraint, HorizontalAlignment, Layout, Rect},
style::Style,
text::Span,
text::{Span, Text},
widgets::{
Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap,
},
@ -34,10 +35,10 @@ pub mod model;
pub mod processing;
pub mod reader;
pub fn run(logs_dir: PathBuf) {
pub fn run(logs_dir: PathBuf, compiler_root: Option<PathBuf>) {
let terminal = ratatui::init();
let theme = Theme::new(ThemeName::OneDarkPro);
let app_result = App::new(logs_dir, theme).run(terminal);
let app_result = App::new(logs_dir, compiler_root, theme).run(terminal);
ratatui::restore();
if let Err(e) = app_result {
@ -97,16 +98,18 @@ fn initialize_filter(lv: &mut LogViewer, kind: Option<FilterKind>) -> WipFilter
struct App {
tabs: Vec<Tab>,
logs_dir: PathBuf,
compiler_root: Option<PathBuf>,
current_file: Option<LogfileReader>,
theme: Theme,
}
impl App {
fn new(logs_dir: PathBuf, theme: Theme) -> Self {
fn new(logs_dir: PathBuf, compiler_root: Option<PathBuf>, theme: Theme) -> Self {
let mut res = Self {
tabs: Vec::new(),
current_file: None,
logs_dir,
compiler_root,
theme,
};
res.replace_tab(res.choose_file());
@ -366,13 +369,14 @@ impl Widget for &mut App {
};
let footer_area = {
let block = Block::bordered()
let mut block = Block::bordered()
.style(default)
.border_style(if footer_focused {
border_selected
} else {
border
});
let inner = block.inner(footer_area);
block.render(footer_area, buf);
inner
@ -441,6 +445,29 @@ impl Widget for &mut App {
));
Widget::render(list, main_area, buf);
Clear.render(footer_area, buf);
let [first_line, footer_area] =
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)])
.areas(footer_area);
if let Some((e, _)) = lv.selected() {
let rustc_root = self.compiler_root.as_ref();
let (file, line) = e.file_line_string();
if let Some(rustc_root) = rustc_root
&& let Ok(canonical_rustc_root) = rustc_root.canonicalize()
{
let full_file_path = canonical_rustc_root.join(&file);
Hyperlink::new(
format!("In file: {}", file.display()),
format!("file://{}:{line}", full_file_path.display()),
)
.render(first_line, buf);
} else {
Span::from(format!("In file: {}:{line}", file.display()))
.render(first_line, buf);
}
}
let items = lv.footer_fields();
let width = 20;
let builder = ListBuilder::new(|cx| {
@ -591,3 +618,40 @@ impl Widget for &mut App {
}
}
}
struct Hyperlink<'content> {
text: Text<'content>,
url: String,
}
impl<'content> Hyperlink<'content> {
fn new(text: impl Into<Text<'content>>, url: impl Into<String>) -> Self {
Self {
text: text.into(),
url: url.into(),
}
}
}
impl Widget for Hyperlink<'_> {
fn render(self, area: Rect, buffer: &mut Buffer) {
(&self.text).render(area, buffer);
// this is a hacky workaround for https://github.com/ratatui/ratatui/issues/902, a bug
// in the terminal code that incorrectly calculates the width of ANSI escape sequences. It
// works by rendering the hyperlink as a series of 2-character chunks, which is the
// calculated width of the hyperlink text.
for (i, two_chars) in self
.text
.to_string()
.chars()
.chunks(2)
.into_iter()
.enumerate()
{
let text = two_chars.collect::<String>();
let hyperlink = format!("\x1B]8;;{}\x07{}\x1B]8;;\x07", self.url, text);
buffer[(area.x + i as u16 * 2, area.y)].set_symbol(hyperlink.as_str());
}
}
}

View file

@ -1,6 +1,7 @@
use std::{
collections::BTreeMap,
hash::{DefaultHasher, Hash, Hasher},
path::PathBuf,
rc::Rc,
sync::OnceLock,
};
@ -38,6 +39,15 @@ pub enum LogEntry {
}
impl LogEntry {
pub fn file_line_string(&self) -> (PathBuf, usize) {
let entry = match self {
LogEntry::Single { raw } => raw,
LogEntry::Sub { enter, .. } => enter,
};
(PathBuf::from(entry.filename.clone()), entry.line_number)
}
pub fn hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
match self {