Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for locally viewing the source of a PEP #17

Merged
merged 22 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
bef0dd1
:sparkles: Add an API call for downloading the source of a PEP
davep Jan 28, 2025
cd68210
:sparkles: Add the core framework of a PEP source viewer screen
davep Jan 28, 2025
495f94b
:lipstick: Improve the look of the PEP viewer
davep Jan 28, 2025
dd2bcc3
:hammer: Have API.pep_file return a Path rather than a string
davep Jan 28, 2025
b204d51
:hammer: Simplify the CSS for the PEP viewer
davep Jan 28, 2025
6611517
:sparkles: Add support for a cache location
davep Jan 28, 2025
1b8f02c
:art: Tidy the order of the exports
davep Jan 28, 2025
073589a
:sparkles: Add a cache facility to the PEP source viewer screen
davep Jan 28, 2025
f32faa4
:books: Add mention of the cache files to the "File locations" in README
davep Jan 28, 2025
29e963d
:hammer: Only set loading in one place
davep Jan 29, 2025
4f2fd6a
:hammer: Swap to using a ScrollableContainer
davep Jan 29, 2025
d533a82
:hammer: Swap to using a read-only TextArea to view the PEP source
davep Jan 29, 2025
94f1c7c
:sparkles: Add a text viewing widget
davep Jan 29, 2025
8019258
:sparkles: Swap the PEP source viewer over to the TextViewer widget
davep Jan 29, 2025
cc75e1b
:books: Update the ChangeLog
davep Jan 29, 2025
b816a14
:lipstick: Remove the line highlight from the TextViewer widget
davep Jan 29, 2025
a5a96c9
:sparkles: Add some movement tweaks to the TextViewer
davep Jan 29, 2025
94330a6
:sparkles: Add all the options for copying
davep Jan 29, 2025
19ef679
:hammer: Correctly identify the start and end of the text to view
davep Jan 29, 2025
759b67d
:books: Fix a documentation typo
davep Jan 29, 2025
03ea80a
:lipstick: Improve the cosmetics of the PEP source viewer
davep Jan 29, 2025
86414da
:lipstick: Use the accent colour to highlight the keys in buttons
davep Jan 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Peplum ChangeLog

## Unreleased

**Released: WiP**

- Added the ability to view the source of a PEP.
([#17](https://github.com/davep/peplum/pull/17))

## v0.2.0

**Released: 2025-01-27**
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Expanding for the common locations, the files normally created are:

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

## Getting help

Expand Down
2 changes: 2 additions & 0 deletions src/peplum/app/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
Quit,
RedownloadPEPs,
TogglePEPDetails,
ViewPEP,
)
from .navigation_sorting import (
ToggleAuthorsSortOrder,
Expand Down Expand Up @@ -59,6 +60,7 @@
"TogglePythonVersionsSortOrder",
"ToggleStatusesSortOrder",
"ToggleTypesSortOrder",
"ViewPEP",
]

### __init__.py ends here
8 changes: 8 additions & 0 deletions src/peplum/app/commands/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,12 @@ class TogglePEPDetails(Command):
BINDING_KEY = "f3"


##############################################################################
class ViewPEP(Command):
"""View the source of the currently-highlighted PEP"""

FOOTER_TEXT = "View"
BINDING_KEY = "f4"


### main.py ends here
8 changes: 5 additions & 3 deletions src/peplum/app/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
save_configuration,
update_configuration,
)
from .locations import cache_dir
from .notes import Notes
from .pep import PEP, PEPStatus, PEPType, PostHistory
from .peps import (
Expand All @@ -28,17 +29,18 @@
##############################################################################
# Exports.
__all__ = [
"pep_data",
"AuthorCount",
"cache_dir",
"Configuration",
"Containing",
"load_configuration",
"Notes",
"PEP",
"PEPStatus",
"PEPType",
"pep_data",
"PEPCount",
"PEPs",
"PEPStatus",
"PEPType",
"PostHistory",
"PythonVersionCount",
"save_configuration",
Expand Down
15 changes: 15 additions & 0 deletions src/peplum/app/data/locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ def data_dir() -> Path:
return _app_dir(xdg_data_home())


##############################################################################
def cache_dir() -> Path:
"""The path to the cache directory for the application.

Returns:
The path to the cache directory for the application.

Note:
If the directory doesn't exist, it will be created as a side-effect
of calling this function.
"""
(cache := data_dir() / "cache").mkdir(parents=True, exist_ok=True)
return cache


##############################################################################
def config_dir() -> Path:
"""The path to the configuration directory for the application.
Expand Down
5 changes: 5 additions & 0 deletions src/peplum/app/peplum.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ class Peplum(App[None]):
}
}

/* Make the LoadingIndicator look less like it was just slapped on. */
LoadingIndicator {
background: transparent;
}

/* Remove cruft from the Header. */
Header {
/* The header icon is ugly and pointless. Remove it. */
Expand Down
2 changes: 2 additions & 0 deletions src/peplum/app/providers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
TogglePythonVersionsSortOrder,
ToggleStatusesSortOrder,
ToggleTypesSortOrder,
ViewPEP,
)
from .commands_provider import CommandHits, CommandsProvider

Expand Down Expand Up @@ -59,6 +60,7 @@ def commands(self) -> CommandHits:
yield TogglePythonVersionsSortOrder()
yield ToggleStatusesSortOrder()
yield ToggleTypesSortOrder()
yield ViewPEP()


### main.py ends here
14 changes: 14 additions & 0 deletions src/peplum/app/screens/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
TogglePythonVersionsSortOrder,
ToggleStatusesSortOrder,
ToggleTypesSortOrder,
ViewPEP,
)
from ..data import (
PEP,
Expand Down Expand Up @@ -78,6 +79,7 @@
from ..widgets import Navigation, PEPDetails, PEPsView
from .help import HelpScreen
from .notes_editor import NotesEditor
from .pep_viewer import PEPViewer
from .search_input import SearchInput


Expand Down Expand Up @@ -148,6 +150,7 @@ class Main(Screen[None]):
Help,
EditNotes,
TogglePEPDetails,
ViewPEP,
Quit,
RedownloadPEPs,
# Everything else.
Expand Down Expand Up @@ -529,5 +532,16 @@ async def action_edit_notes_command(self) -> None:
self.all_peps.patch_pep(self.selected_pep.annotate(notes=notes))
)

@on(ViewPEP)
def action_view_pep_command(self) -> None:
"""View the currently-highlighted PEP's source."""
if self.selected_pep is None:
self.notify("Highlight a PEP to view it.", severity="warning")
return
if self.selected_pep.number == 0:
self.notify("PEP0 has no source to view.", severity="warning")
return
self.app.push_screen(PEPViewer(self.selected_pep))


### main.py ends here
151 changes: 151 additions & 0 deletions src/peplum/app/screens/pep_viewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""A dialog for viewing the text of a PEP."""

##############################################################################
# Python imports.
from pathlib import Path

##############################################################################
# Textual imports.
from textual import on, work
from textual.app import ComposeResult
from textual.containers import Horizontal, Vertical
from textual.screen import ModalScreen
from textual.widgets import Button, TextArea

##############################################################################
# Local imports.
from ...peps import API
from ..data import PEP, cache_dir
from ..widgets import TextViewer


##############################################################################
class PEPViewer(ModalScreen[None]):
"""A modal screen for viewing a PEP's source."""

CSS = """
PEPViewer {
align: center middle;

&> Vertical {
width: 80%;
height: 80%;
max-height: 80%;
background: $panel;
border: solid $border;
}

TextViewer {
color: $text-muted;
height: 1fr;
scrollbar-background: $panel;
scrollbar-background-hover: $panel;
scrollbar-background-active: $panel;
&:focus {
color: $text;
}
}

#buttons {
height: auto;
align-horizontal: right;
border-top: solid $border;
}

Button {
margin-right: 1;
}
}
"""

BINDINGS = [("escape", "close"), ("ctrl+r", "refresh"), ("ctrl+c", "copy")]

def __init__(self, pep: PEP) -> None:
"""Initialise the dialog.

Args:
pep: The PEP to view.
"""
super().__init__()
self._pep = pep
"""The PEP to view."""

def compose(self) -> ComposeResult:
"""Compose the dialog's content."""
key_colour = (
"dim" if self.app.current_theme is None else self.app.current_theme.accent
)
with Vertical() as dialog:
dialog.border_title = f"PEP{self._pep.number}"
yield TextViewer()
with Horizontal(id="buttons"):
yield Button(f"Copy [{key_colour}]\\[^c][/]", id="copy")
yield Button(f"Refresh [{key_colour}]\\[^r][/]", id="refresh")
yield Button(f"Close [{key_colour}]\\[Esc][/]", id="close")

@property
def _cache_name(self) -> Path:
"""The name of the file that is the cached version of the PEP source."""
return cache_dir() / API.pep_file(self._pep.number)

@work
async def _download_text(self) -> None:
"""Download the text of the PEP.

Notes:
Once downloaded a local copy will be saved. Subsequently, when
attempting to download the PEP, this local copy will be used
instead.
"""
(text := self.query_one(TextViewer)).loading = True
pep_source = ""

if self._cache_name.exists():
try:
pep_source = self._cache_name.read_text(encoding="utf-8")
except IOError:
pass

if not pep_source:
try:
self._cache_name.write_text(
pep_source := await API().get_pep(self._pep.number),
encoding="utf-8",
)
except IOError:
pass
except API.RequestError as error:
pep_source = "Error downloading PEP source"
self.notify(
str(error), title="Error downloading PEP source", severity="error"
)

text.text = pep_source
text.loading = False
self.set_focus(text)

def on_mount(self) -> None:
"""Populate the dialog once the DOM is ready."""
self._download_text()

@on(Button.Pressed, "#close")
def action_close(self) -> None:
"""Close the dialog."""
self.dismiss(None)

@on(Button.Pressed, "#refresh")
def action_refresh(self) -> None:
"""Refresh the PEP source."""
try:
self._cache_name.unlink(missing_ok=True)
except IOError:
pass
self._download_text()

@on(Button.Pressed, "#copy")
async def action_copy(self) -> None:
"""Copy PEP text to the clipboard."""
await self.query_one(TextArea).run_action("copy")


### pep_viewer.py ends here
3 changes: 2 additions & 1 deletion src/peplum/app/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
from .navigation import Navigation
from .pep_details import PEPDetails
from .peps_view import PEPsView
from .text_viewer import TextViewer

##############################################################################
# Exports.
__all__ = ["Navigation", "PEPDetails", "PEPsView"]
__all__ = ["Navigation", "PEPDetails", "PEPsView", "TextViewer"]


### __init__.py ends here
Loading
Loading