Skip to content

Commit a92c7fd

Browse files
committed
✨ Add initial support for browsing gemtext in file system
1 parent 06b79bd commit a92c7fd

5 files changed

Lines changed: 107 additions & 1 deletion

File tree

src/rogallo/preflight.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
##############################################################################
44
# Python imports.
5+
from pathlib import Path
56
from urllib.parse import urlparse
67

78
##############################################################################
@@ -41,4 +42,39 @@ def is_likely_capsule(uri: str) -> bool:
4142
return is_likely_page_relative(uri)
4243

4344

45+
##############################################################################
46+
def path_from_uri(uri: str) -> Path:
47+
"""Get the path from a URI.
48+
49+
Args:
50+
uri: The URI to get the path from.
51+
52+
Returns:
53+
The path from the URI.
54+
"""
55+
56+
if (parsed := urlparse(uri)).scheme == "file":
57+
return Path(parsed.path)
58+
elif not parsed.scheme and not parsed.netloc:
59+
return Path(uri)
60+
raise ValueError(f"URI is not a local file: {uri}")
61+
62+
63+
##############################################################################
64+
def is_likely_local_file(uri: str) -> bool:
65+
"""Determine if a URI is likely a local file.
66+
67+
Args:
68+
uri: The URI to check.
69+
70+
Returns:
71+
`True` if the URI is likely a local file, `False` otherwise.
72+
"""
73+
try:
74+
candidate = path_from_uri(uri)
75+
except ValueError:
76+
return False
77+
return candidate.exists() and candidate.is_file()
78+
79+
4480
### location_tests.py ends here

src/rogallo/screens/main.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
##############################################################################
44
# Python imports.
55
from argparse import Namespace
6+
from pathlib import Path
67
from webbrowser import open as open_in_browser
78

89
##############################################################################
@@ -50,6 +51,7 @@
5051
update_configuration,
5152
)
5253
from ..messages import OpenLocation, OpenText, OpenURI
54+
from ..preflight import is_likely_local_file, path_from_uri
5355
from ..providers import MainCommands
5456
from ..widgets import CommandLine, HistoryViewer, Viewer
5557

@@ -305,6 +307,27 @@ async def _load_from_capsule(self, request: OpenLocation) -> None:
305307
finally:
306308
self._command_line.working = False
307309

310+
@work(thread=True)
311+
def _load_from_filesystem(self, request: OpenLocation) -> None:
312+
"""Load a document from the filesystem.
313+
314+
Args:
315+
request: The request to load the document from.
316+
"""
317+
assert isinstance(request.location, Path)
318+
try:
319+
self.post_message(
320+
OpenText(request.location.read_text(encoding="utf-8"), request.location)
321+
)
322+
# TODO: Remember in history.
323+
except IOError as error:
324+
self.notify(
325+
f"Error loading {request.location}:\n\n{error}",
326+
severity="error",
327+
title="Filesystem Error",
328+
)
329+
return
330+
308331
@on(OpenText)
309332
def open_text(self, message: OpenText) -> None:
310333
"""Open text in the viewer.
@@ -324,6 +347,8 @@ def open_location(self, message: OpenLocation) -> None:
324347
"""
325348
if isinstance(message.location, GeminiURI):
326349
self._load_from_capsule(message)
350+
else:
351+
self._load_from_filesystem(message)
327352

328353
@on(OpenURI)
329354
def open_uri(self, message: OpenURI) -> None:
@@ -340,7 +365,10 @@ def open_uri(self, message: OpenURI) -> None:
340365
except URIError:
341366
pass
342367

343-
# TODO: Handle gmi files in the filesystem.
368+
# Perhaps it's a local file?
369+
if is_likely_local_file(message.uri):
370+
self.post_message(OpenLocation(path_from_uri(message.uri)))
371+
return
344372

345373
# Otherwise, try to open it in the system browser.
346374
open_in_browser(message.uri)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Provides a command for opening a gemtext file in the local filesystem."""
2+
3+
##############################################################################
4+
# Textual imports.
5+
from textual.widget import Widget
6+
7+
##############################################################################
8+
# Local imports.
9+
from ...messages import OpenLocation
10+
from ...preflight import is_likely_local_file, path_from_uri
11+
from .base_command import InputCommand
12+
13+
14+
##############################################################################
15+
class OpenFileCommand(InputCommand):
16+
"""Open `<file>` in your external browser"""
17+
18+
COMMAND = "`<file>`"
19+
20+
@classmethod
21+
def handle(cls, text: str, for_widget: Widget) -> bool:
22+
"""Handle the command.
23+
24+
Args:
25+
text: The text of the command.
26+
for_widget: The widget to handle the command for.
27+
28+
Returns:
29+
`True` if the command was handled; `False` if not.
30+
"""
31+
if is_likely_local_file(text):
32+
for_widget.post_message(OpenLocation(path_from_uri(text)))
33+
return True
34+
return False
35+
36+
37+
### open_file.py ends here

src/rogallo/widgets/command_line/widget.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@
3333
from ...data import CommandLineHistory
3434
from .base_command import InputCommand
3535
from .general import HelpCommand, QuitCommand
36+
from .open_file import OpenFileCommand
3637
from .open_gemini_uri import OpenGeminiURICommand
3738
from .open_other_uri import OpenOtherURICommand
3839

3940
##############################################################################
4041
COMMANDS: Final[tuple[type[InputCommand], ...]] = (
4142
OpenGeminiURICommand,
4243
OpenOtherURICommand,
44+
OpenFileCommand,
4345
HelpCommand,
4446
QuitCommand,
4547
)

src/rogallo/widgets/viewer/gemtext_blocks.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Python imports.
55
from collections.abc import Callable
66
from functools import cache
7+
from pathlib import Path
78
from typing import Final
89
from urllib.parse import urlparse
910

@@ -226,6 +227,8 @@ def normalise_uri(self, base_uri: GeminiLocation | None) -> None:
226227
return
227228
if isinstance(base_uri, GeminiURI):
228229
self._normalised_uri = str(base_uri.resolve(self._link.uri))
230+
elif isinstance(base_uri, Path):
231+
self._normalised_uri = (base_uri.parent / self._link.uri).resolve().as_uri()
229232

230233
def _watch__normalised_uri(self) -> None:
231234
"""Watch for changes to the normalised URI."""

0 commit comments

Comments
 (0)