display hyperlinks
This commit is contained in:
parent
ae5ce58eec
commit
f3bc16b3c5
6 changed files with 93 additions and 7 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1179,6 +1179,7 @@ name = "rustc-logviz"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
|
"itertools",
|
||||||
"jiff",
|
"jiff",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"ratatui-themes",
|
"ratatui-themes",
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,4 @@ tui-widget-list = "0.15"
|
||||||
serde = {version = "1", features = ["derive"]}
|
serde = {version = "1", features = ["derive"]}
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
|
itertools = "0.14"
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,11 @@ struct Args {
|
||||||
#[arg(long = "logs-dir")]
|
#[arg(long = "logs-dir")]
|
||||||
logs_dir: PathBuf,
|
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(trailing_var_arg = true)]
|
||||||
#[arg(allow_hyphen_values = true)]
|
#[arg(allow_hyphen_values = true)]
|
||||||
#[arg(global = true)]
|
#[arg(global = true)]
|
||||||
|
|
@ -60,11 +65,12 @@ fn main() {
|
||||||
preset,
|
preset,
|
||||||
logs_dir,
|
logs_dir,
|
||||||
rest,
|
rest,
|
||||||
|
compiler_root,
|
||||||
} = Args::parse();
|
} = Args::parse();
|
||||||
|
|
||||||
let rustc_log = match preset {
|
let rustc_log = match preset {
|
||||||
Preset::Show => {
|
Preset::Show => {
|
||||||
tui::run(logs_dir);
|
tui::run(logs_dir, compiler_root);
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
Preset::Types => {
|
Preset::Types => {
|
||||||
|
|
|
||||||
|
|
@ -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 temp_iter = self.curr.iter.clone();
|
||||||
let mut res = Vec::new();
|
let mut res = Vec::new();
|
||||||
for _ in 0..max {
|
for _ in 0..max {
|
||||||
|
|
@ -198,6 +198,10 @@ impl LogViewer {
|
||||||
res.push(i);
|
res.push(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.curr.selection_offset > res.len() {
|
||||||
|
self.curr.selection_offset = res.len();
|
||||||
|
}
|
||||||
|
|
||||||
Some((res, self.curr.selection_offset))
|
Some((res, self.curr.selection_offset))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use itertools::Itertools;
|
||||||
use ratatui_themes::{Theme, ThemeName};
|
use ratatui_themes::{Theme, ThemeName};
|
||||||
use std::{
|
use std::{
|
||||||
fs::{self, DirEntry},
|
fs::{self, DirEntry},
|
||||||
|
|
@ -22,7 +23,7 @@ use ratatui::{
|
||||||
crossterm::event::{self, Event, KeyCode, KeyModifiers},
|
crossterm::event::{self, Event, KeyCode, KeyModifiers},
|
||||||
layout::{Constraint, HorizontalAlignment, Layout, Rect},
|
layout::{Constraint, HorizontalAlignment, Layout, Rect},
|
||||||
style::Style,
|
style::Style,
|
||||||
text::Span,
|
text::{Span, Text},
|
||||||
widgets::{
|
widgets::{
|
||||||
Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap,
|
Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap,
|
||||||
},
|
},
|
||||||
|
|
@ -34,10 +35,10 @@ pub mod model;
|
||||||
pub mod processing;
|
pub mod processing;
|
||||||
pub mod reader;
|
pub mod reader;
|
||||||
|
|
||||||
pub fn run(logs_dir: PathBuf) {
|
pub fn run(logs_dir: PathBuf, compiler_root: Option<PathBuf>) {
|
||||||
let terminal = ratatui::init();
|
let terminal = ratatui::init();
|
||||||
let theme = Theme::new(ThemeName::OneDarkPro);
|
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();
|
ratatui::restore();
|
||||||
|
|
||||||
if let Err(e) = app_result {
|
if let Err(e) = app_result {
|
||||||
|
|
@ -97,16 +98,18 @@ fn initialize_filter(lv: &mut LogViewer, kind: Option<FilterKind>) -> WipFilter
|
||||||
struct App {
|
struct App {
|
||||||
tabs: Vec<Tab>,
|
tabs: Vec<Tab>,
|
||||||
logs_dir: PathBuf,
|
logs_dir: PathBuf,
|
||||||
|
compiler_root: Option<PathBuf>,
|
||||||
current_file: Option<LogfileReader>,
|
current_file: Option<LogfileReader>,
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
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 {
|
let mut res = Self {
|
||||||
tabs: Vec::new(),
|
tabs: Vec::new(),
|
||||||
current_file: None,
|
current_file: None,
|
||||||
logs_dir,
|
logs_dir,
|
||||||
|
compiler_root,
|
||||||
theme,
|
theme,
|
||||||
};
|
};
|
||||||
res.replace_tab(res.choose_file());
|
res.replace_tab(res.choose_file());
|
||||||
|
|
@ -366,13 +369,14 @@ impl Widget for &mut App {
|
||||||
};
|
};
|
||||||
|
|
||||||
let footer_area = {
|
let footer_area = {
|
||||||
let block = Block::bordered()
|
let mut block = Block::bordered()
|
||||||
.style(default)
|
.style(default)
|
||||||
.border_style(if footer_focused {
|
.border_style(if footer_focused {
|
||||||
border_selected
|
border_selected
|
||||||
} else {
|
} else {
|
||||||
border
|
border
|
||||||
});
|
});
|
||||||
|
|
||||||
let inner = block.inner(footer_area);
|
let inner = block.inner(footer_area);
|
||||||
block.render(footer_area, buf);
|
block.render(footer_area, buf);
|
||||||
inner
|
inner
|
||||||
|
|
@ -441,6 +445,29 @@ impl Widget for &mut App {
|
||||||
));
|
));
|
||||||
Widget::render(list, main_area, buf);
|
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 items = lv.footer_fields();
|
||||||
let width = 20;
|
let width = 20;
|
||||||
let builder = ListBuilder::new(|cx| {
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
hash::{DefaultHasher, Hash, Hasher},
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
|
path::PathBuf,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::OnceLock,
|
sync::OnceLock,
|
||||||
};
|
};
|
||||||
|
|
@ -38,6 +39,15 @@ pub enum LogEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub fn hash(&self) -> u64 {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
match self {
|
match self {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue