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}