-
I know that Textual markdown links can be used to navigate between different sections/documents. But I think it should also respect web links and open a web page like it already does with links placed in e.g Static widget. Minimal reproducible example: from textual.app import App
from textual.widgets import MarkdownViewer
class MyApp(App):
def compose(self):
yield MarkdownViewer("[some link](https://www.google.com)")
MyApp().run() Clicking on the link raises: ╭─────────────────────────────────────────────────────────────── Traceback (most recent call last) ───────────────────────────────────────────────────────────────╮
│ /home/dev/1_workspace/textual/src/textual/widgets/_markdown.py:969 in _on_markdown_link_clicked │
│ │
│ 966 │ ╭────────── locals ──────────╮ │
│ 967 │ async def _on_markdown_link_clicked(self, message: Markdown.LinkClicked) -> None: │ message = LinkClicked() │ │
│ 968 │ │ message.stop() │ self = MarkdownViewer() │ │
│ ❱ 969 │ │ await self.go(message.href) ╰────────────────────────────╯ │
│ 970 │ │
│ 971 │ def watch_show_table_of_contents(self, show_table_of_contents: bool) -> None: │
│ 972 │ │ self.set_class(show_table_of_contents, "-show-table-of-contents") │
│ │
│ /home/dev/1_workspace/textual/src/textual/widgets/_markdown.py:955 in go │
│ │
│ 952 │ ╭────────────── locals ───────────────╮ │
│ 953 │ async def go(self, location: str | PurePath) -> bool: │ location = 'https://www.google.com' │ │
│ 954 │ │ """Navigate to a new document path.""" │ self = MarkdownViewer() │ │
│ ❱ 955 │ │ return await self.document.load(self.navigator.go(location)) ╰─────────────────────────────────────╯ │
│ 956 │ │
│ 957 │ async def back(self) -> None: │
│ 958 │ │ """Go back one level in the history.""" │
│ │
│ /home/dev/1_workspace/textual/src/textual/widgets/_markdown.py:644 in load │
│ │
│ 641 │ │ │ The exceptions that can be raised by this method are all of │
│ 642 │ │ │ those that can be raised by calling [`Path.read_text`][pathlib.Path.read_tex │
│ 643 │ │ """ │
│ ❱ 644 │ │ await self.update(path.read_text(encoding="utf-8")) │
│ 645 │ │
│ 646 │ def unhandled_token(self, token: Token) -> MarkdownBlock | None: │
│ 647 │ │ """Process an unhandled token. │
│ │
│ ╭──────────────────────────────── locals ─────────────────────────────────╮ │
│ │ path = PosixPath('/home/dev/1_workspace/textual/https:/www.google.com') │ │
│ │ self = Markdown() │ │
│ ╰─────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /home/dev/.pyenv/versions/3.8.16/lib/python3.8/pathlib.py:1236 in read_text │
│ │
│ 1233 │ │ """ │
│ 1234 │ │ Open the file in text mode, read it, and close the file. │
│ 1235 │ │ """ │
│ ❱ 1236 │ │ with self.open(mode='r', encoding=encoding, errors=errors) as f: │
│ 1237 │ │ │ return f.read() │
│ 1238 │ │
│ 1239 │ def write_bytes(self, data): │
│ │
│ ╭────────────────────────────────── locals ───────────────────────────────────╮ │
│ │ encoding = 'utf-8' │ │
│ │ errors = None │ │
│ │ self = PosixPath('/home/dev/1_workspace/textual/https:/www.google.com') │ │
│ ╰─────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /home/dev/.pyenv/versions/3.8.16/lib/python3.8/pathlib.py:1222 in open │
│ │
│ 1219 │ │ """ │
│ 1220 │ │ if self._closed: │
│ 1221 │ │ │ self._raise_closed() │
│ ❱ 1222 │ │ return io.open(self, mode, buffering, encoding, errors, newline, │
│ 1223 │ │ │ │ │ opener=self._opener) │
│ 1224 │ │
│ 1225 │ def read_bytes(self): │
│ │
│ ╭─────────────────────────────────── locals ───────────────────────────────────╮ │
│ │ buffering = -1 │ │
│ │ encoding = 'utf-8' │ │
│ │ errors = None │ │
│ │ mode = 'r' │ │
│ │ newline = None │ │
│ │ self = PosixPath('/home/dev/1_workspace/textual/https:/www.google.com') │ │
│ ╰──────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /home/dev/.pyenv/versions/3.8.16/lib/python3.8/pathlib.py:1078 in _opener │
│ │
│ 1075 │ │
│ 1076 │ def _opener(self, name, flags, mode=0o666): │
│ 1077 │ │ # A stub for the opener argument to built-in open() │
│ ❱ 1078 │ │ return self._accessor.open(self, flags, mode) │
│ 1079 │ │
│ 1080 │ def _raw_open(self, flags, mode=0o777): │
│ 1081 │ │ """ │
│ │
│ ╭───────────────────────────────── locals ─────────────────────────────────╮ │
│ │ flags = 524288 │ │
│ │ mode = 438 │ │
│ │ name = '/home/dev/1_workspace/textual/https:/www.google.com' │ │
│ │ self = PosixPath('/home/dev/1_workspace/textual/https:/www.google.com') │ │
│ ╰──────────────────────────────────────────────────────────────────────────╯ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
FileNotFoundError: [Errno 2] No such file or directory: '/home/dev/1_workspace/textual/https:/www.google.com' |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments
-
Because following a URI could have many unintended consequences, this would probably be a bad idea out of the box. I think it might however be a good idea to tweak the |
Beta Was this translation helpful? Give feedback.
-
But that's also how it already works with And also this is the way it works in every markdown viewer I know. |
Beta Was this translation helpful? Give feedback.
-
The links in Static will be done by your terminal, by recognising the link syntax. It's not the remit of a library to open those links in an external app by default. It presents a clear security risk. But if you want it, then its trivial to implement. |
Beta Was this translation helpful? Give feedback.
-
Further to what Will says, you could build the kind of viewer you had in mind with something like this: from pathlib import Path
from webbrowser import open
from textual import on
from textual.app import App, ComposeResult
from textual.widgets import Markdown, MarkdownViewer
class LinkableMarkdownViewer(MarkdownViewer):
@on(Markdown.LinkClicked)
def handle_link(self, event: Markdown.LinkClicked) -> None:
if not Path(event.href).exists():
event.prevent_default()
open(event.href)
class LinkyMDViewer(App[None]):
def compose(self) -> ComposeResult:
yield LinkableMarkdownViewer("""\
# Example document
[Here is a link](https://www.example.com/)
""")
if __name__ == "__main__":
LinkyMDViewer().run() Of course, making the decision as to what's local in the filesystem and what isn't could be more sophisticated, but that should give an idea. |
Beta Was this translation helpful? Give feedback.
Further to what Will says, you could build the kind of viewer you had in mind with something like this: