Skip to content

Commit f9af63e

Browse files
authored
🔀 Merge pull request #17 from davep/local-view
Add support for locally viewing the source of a PEP
2 parents 783785f + 86414da commit f9af63e

File tree

13 files changed

+356
-14
lines changed

13 files changed

+356
-14
lines changed

ChangeLog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Peplum ChangeLog
22

3+
## Unreleased
4+
5+
**Released: WiP**
6+
7+
- Added the ability to view the source of a PEP.
8+
([#17](https://github.com/davep/peplum/pull/17))
9+
310
## v0.2.0
411

512
**Released: 2025-01-27**

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Expanding for the common locations, the files normally created are:
5050

5151
- `~/.config/peplum/configuration.json` -- The configuration file.
5252
- `~/.local/share/peplum/*.json` -- The locally-held PEP data.
53+
- `~/.local/share/peplum/cache/*.rst` -- The locally-cached PEP source files.
5354

5455
## Getting help
5556

src/peplum/app/commands/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
Quit,
2626
RedownloadPEPs,
2727
TogglePEPDetails,
28+
ViewPEP,
2829
)
2930
from .navigation_sorting import (
3031
ToggleAuthorsSortOrder,
@@ -59,6 +60,7 @@
5960
"TogglePythonVersionsSortOrder",
6061
"ToggleStatusesSortOrder",
6162
"ToggleTypesSortOrder",
63+
"ViewPEP",
6264
]
6365

6466
### __init__.py ends here

src/peplum/app/commands/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,12 @@ class TogglePEPDetails(Command):
6161
BINDING_KEY = "f3"
6262

6363

64+
##############################################################################
65+
class ViewPEP(Command):
66+
"""View the source of the currently-highlighted PEP"""
67+
68+
FOOTER_TEXT = "View"
69+
BINDING_KEY = "f4"
70+
71+
6472
### main.py ends here

src/peplum/app/data/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
save_configuration,
99
update_configuration,
1010
)
11+
from .locations import cache_dir
1112
from .notes import Notes
1213
from .pep import PEP, PEPStatus, PEPType, PostHistory
1314
from .peps import (
@@ -28,17 +29,18 @@
2829
##############################################################################
2930
# Exports.
3031
__all__ = [
31-
"pep_data",
3232
"AuthorCount",
33+
"cache_dir",
3334
"Configuration",
3435
"Containing",
3536
"load_configuration",
3637
"Notes",
3738
"PEP",
38-
"PEPStatus",
39-
"PEPType",
39+
"pep_data",
4040
"PEPCount",
4141
"PEPs",
42+
"PEPStatus",
43+
"PEPType",
4244
"PostHistory",
4345
"PythonVersionCount",
4446
"save_configuration",

src/peplum/app/data/locations.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ def data_dir() -> Path:
4141
return _app_dir(xdg_data_home())
4242

4343

44+
##############################################################################
45+
def cache_dir() -> Path:
46+
"""The path to the cache directory for the application.
47+
48+
Returns:
49+
The path to the cache directory for the application.
50+
51+
Note:
52+
If the directory doesn't exist, it will be created as a side-effect
53+
of calling this function.
54+
"""
55+
(cache := data_dir() / "cache").mkdir(parents=True, exist_ok=True)
56+
return cache
57+
58+
4459
##############################################################################
4560
def config_dir() -> Path:
4661
"""The path to the configuration directory for the application.

src/peplum/app/peplum.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ class Peplum(App[None]):
3333
}
3434
}
3535
36+
/* Make the LoadingIndicator look less like it was just slapped on. */
37+
LoadingIndicator {
38+
background: transparent;
39+
}
40+
3641
/* Remove cruft from the Header. */
3742
Header {
3843
/* The header icon is ugly and pointless. Remove it. */

src/peplum/app/providers/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
TogglePythonVersionsSortOrder,
2525
ToggleStatusesSortOrder,
2626
ToggleTypesSortOrder,
27+
ViewPEP,
2728
)
2829
from .commands_provider import CommandHits, CommandsProvider
2930

@@ -59,6 +60,7 @@ def commands(self) -> CommandHits:
5960
yield TogglePythonVersionsSortOrder()
6061
yield ToggleStatusesSortOrder()
6162
yield ToggleTypesSortOrder()
63+
yield ViewPEP()
6264

6365

6466
### main.py ends here

src/peplum/app/screens/main.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
TogglePythonVersionsSortOrder,
4545
ToggleStatusesSortOrder,
4646
ToggleTypesSortOrder,
47+
ViewPEP,
4748
)
4849
from ..data import (
4950
PEP,
@@ -78,6 +79,7 @@
7879
from ..widgets import Navigation, PEPDetails, PEPsView
7980
from .help import HelpScreen
8081
from .notes_editor import NotesEditor
82+
from .pep_viewer import PEPViewer
8183
from .search_input import SearchInput
8284

8385

@@ -148,6 +150,7 @@ class Main(Screen[None]):
148150
Help,
149151
EditNotes,
150152
TogglePEPDetails,
153+
ViewPEP,
151154
Quit,
152155
RedownloadPEPs,
153156
# Everything else.
@@ -529,5 +532,16 @@ async def action_edit_notes_command(self) -> None:
529532
self.all_peps.patch_pep(self.selected_pep.annotate(notes=notes))
530533
)
531534

535+
@on(ViewPEP)
536+
def action_view_pep_command(self) -> None:
537+
"""View the currently-highlighted PEP's source."""
538+
if self.selected_pep is None:
539+
self.notify("Highlight a PEP to view it.", severity="warning")
540+
return
541+
if self.selected_pep.number == 0:
542+
self.notify("PEP0 has no source to view.", severity="warning")
543+
return
544+
self.app.push_screen(PEPViewer(self.selected_pep))
545+
532546

533547
### main.py ends here

src/peplum/app/screens/pep_viewer.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""A dialog for viewing the text of a PEP."""
2+
3+
##############################################################################
4+
# Python imports.
5+
from pathlib import Path
6+
7+
##############################################################################
8+
# Textual imports.
9+
from textual import on, work
10+
from textual.app import ComposeResult
11+
from textual.containers import Horizontal, Vertical
12+
from textual.screen import ModalScreen
13+
from textual.widgets import Button, TextArea
14+
15+
##############################################################################
16+
# Local imports.
17+
from ...peps import API
18+
from ..data import PEP, cache_dir
19+
from ..widgets import TextViewer
20+
21+
22+
##############################################################################
23+
class PEPViewer(ModalScreen[None]):
24+
"""A modal screen for viewing a PEP's source."""
25+
26+
CSS = """
27+
PEPViewer {
28+
align: center middle;
29+
30+
&> Vertical {
31+
width: 80%;
32+
height: 80%;
33+
max-height: 80%;
34+
background: $panel;
35+
border: solid $border;
36+
}
37+
38+
TextViewer {
39+
color: $text-muted;
40+
height: 1fr;
41+
scrollbar-background: $panel;
42+
scrollbar-background-hover: $panel;
43+
scrollbar-background-active: $panel;
44+
&:focus {
45+
color: $text;
46+
}
47+
}
48+
49+
#buttons {
50+
height: auto;
51+
align-horizontal: right;
52+
border-top: solid $border;
53+
}
54+
55+
Button {
56+
margin-right: 1;
57+
}
58+
}
59+
"""
60+
61+
BINDINGS = [("escape", "close"), ("ctrl+r", "refresh"), ("ctrl+c", "copy")]
62+
63+
def __init__(self, pep: PEP) -> None:
64+
"""Initialise the dialog.
65+
66+
Args:
67+
pep: The PEP to view.
68+
"""
69+
super().__init__()
70+
self._pep = pep
71+
"""The PEP to view."""
72+
73+
def compose(self) -> ComposeResult:
74+
"""Compose the dialog's content."""
75+
key_colour = (
76+
"dim" if self.app.current_theme is None else self.app.current_theme.accent
77+
)
78+
with Vertical() as dialog:
79+
dialog.border_title = f"PEP{self._pep.number}"
80+
yield TextViewer()
81+
with Horizontal(id="buttons"):
82+
yield Button(f"Copy [{key_colour}]\\[^c][/]", id="copy")
83+
yield Button(f"Refresh [{key_colour}]\\[^r][/]", id="refresh")
84+
yield Button(f"Close [{key_colour}]\\[Esc][/]", id="close")
85+
86+
@property
87+
def _cache_name(self) -> Path:
88+
"""The name of the file that is the cached version of the PEP source."""
89+
return cache_dir() / API.pep_file(self._pep.number)
90+
91+
@work
92+
async def _download_text(self) -> None:
93+
"""Download the text of the PEP.
94+
95+
Notes:
96+
Once downloaded a local copy will be saved. Subsequently, when
97+
attempting to download the PEP, this local copy will be used
98+
instead.
99+
"""
100+
(text := self.query_one(TextViewer)).loading = True
101+
pep_source = ""
102+
103+
if self._cache_name.exists():
104+
try:
105+
pep_source = self._cache_name.read_text(encoding="utf-8")
106+
except IOError:
107+
pass
108+
109+
if not pep_source:
110+
try:
111+
self._cache_name.write_text(
112+
pep_source := await API().get_pep(self._pep.number),
113+
encoding="utf-8",
114+
)
115+
except IOError:
116+
pass
117+
except API.RequestError as error:
118+
pep_source = "Error downloading PEP source"
119+
self.notify(
120+
str(error), title="Error downloading PEP source", severity="error"
121+
)
122+
123+
text.text = pep_source
124+
text.loading = False
125+
self.set_focus(text)
126+
127+
def on_mount(self) -> None:
128+
"""Populate the dialog once the DOM is ready."""
129+
self._download_text()
130+
131+
@on(Button.Pressed, "#close")
132+
def action_close(self) -> None:
133+
"""Close the dialog."""
134+
self.dismiss(None)
135+
136+
@on(Button.Pressed, "#refresh")
137+
def action_refresh(self) -> None:
138+
"""Refresh the PEP source."""
139+
try:
140+
self._cache_name.unlink(missing_ok=True)
141+
except IOError:
142+
pass
143+
self._download_text()
144+
145+
@on(Button.Pressed, "#copy")
146+
async def action_copy(self) -> None:
147+
"""Copy PEP text to the clipboard."""
148+
await self.query_one(TextArea).run_action("copy")
149+
150+
151+
### pep_viewer.py ends here

0 commit comments

Comments
 (0)