hyperlink/hyperlink.rs
1//! # [Ratatui] Hyperlink examplew
2//!
3//! Shows how to use [OSC 8] to create hyperlinks in the terminal.
4//!
5//! The latest version of this example is available in the [examples] folder in the repository.
6//!
7//! Please note that the examples are designed to be run against the `main` branch of the Github
8//! repository. This means that you may not be able to compile with the latest release version on
9//! crates.io, or the one that you have installed locally.
10//!
11//! See the [examples readme] for more information on finding examples that match the version of the
12//! library you are using.
13//!
14//! [OSC 8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
15//! [Ratatui]: https://github.com/ratatui/ratatui
16//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
17//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
18
19use color_eyre::Result;
20use itertools::Itertools;
21use ratatui::{
22 buffer::Buffer,
23 crossterm::event::{self, Event, KeyCode},
24 layout::Rect,
25 style::Stylize,
26 text::{Line, Text},
27 widgets::Widget,
28 DefaultTerminal,
29};
30
31fn main() -> Result<()> {
32 color_eyre::install()?;
33 let terminal = ratatui::init();
34 let app_result = App::new().run(terminal);
35 ratatui::restore();
36 app_result
37}
38
39struct App {
40 hyperlink: Hyperlink<'static>,
41}
42
43impl App {
44 fn new() -> Self {
45 let text = Line::from(vec!["Example ".into(), "hyperlink".blue()]);
46 let hyperlink = Hyperlink::new(text, "https://example.com");
47 Self { hyperlink }
48 }
49
50 fn run(self, mut terminal: DefaultTerminal) -> Result<()> {
51 loop {
52 terminal.draw(|frame| frame.render_widget(&self.hyperlink, frame.area()))?;
53 if let Event::Key(key) = event::read()? {
54 if matches!(key.code, KeyCode::Char('q') | KeyCode::Esc) {
55 break;
56 }
57 }
58 }
59 Ok(())
60 }
61}
62
63/// A hyperlink widget that renders a hyperlink in the terminal using [OSC 8].
64///
65/// [OSC 8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
66struct Hyperlink<'content> {
67 text: Text<'content>,
68 url: String,
69}
70
71impl<'content> Hyperlink<'content> {
72 fn new(text: impl Into<Text<'content>>, url: impl Into<String>) -> Self {
73 Self {
74 text: text.into(),
75 url: url.into(),
76 }
77 }
78}
79
80impl Widget for &Hyperlink<'_> {
81 fn render(self, area: Rect, buffer: &mut Buffer) {
82 (&self.text).render(area, buffer);
83
84 // this is a hacky workaround for https://github.com/ratatui/ratatui/issues/902, a bug
85 // in the terminal code that incorrectly calculates the width of ANSI escape sequences. It
86 // works by rendering the hyperlink as a series of 2-character chunks, which is the
87 // calculated width of the hyperlink text.
88 for (i, two_chars) in self
89 .text
90 .to_string()
91 .chars()
92 .chunks(2)
93 .into_iter()
94 .enumerate()
95 {
96 let text = two_chars.collect::<String>();
97 let hyperlink = format!("\x1B]8;;{}\x07{}\x1B]8;;\x07", self.url, text);
98 buffer[(area.x + i as u16 * 2, area.y)].set_symbol(hyperlink.as_str());
99 }
100 }
101}