Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@
# Hike ChangeLog

## Unreleased

**Released: WiP**

- Added a simple YAML front matter display.
([#136](https://github.com/davep/hike/pull/136))

## v1.1.4

**Released: 2025-09-09**
Expand Down
11 changes: 11 additions & 0 deletions docs/source/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ command ([`ChangeCommandLineLocation`](#bindable-commands), bound to
```{.textual path="docs/screenshots/basic_app.py" title="Command line on top" lines=40 columns=120 press="tab,d,ctrl+up,tab"}
```

## Front matter display

By default Hike will show an expandable display of [YAML front
matter](https://jekyllrb.com/docs/front-matter/), if it exists in the
document being viewed. If you aren't ever interested in seeing the front
matter this can be turned off with:

```json
"show_front_matter": false
```

## Keyboard bindings

Hike allows for a degree of configuration of its keyboard bindings;
Expand Down
3 changes: 3 additions & 0 deletions src/hike/data/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ class Configuration:
focus_viewer_on_load: bool = True
"""Should the viewer get focus when a file is loaded?"""

show_front_matter: bool = True
"""Should the viewer allow for the viewing of front matter?"""


##############################################################################
def configuration_file() -> Path:
Expand Down
110 changes: 99 additions & 11 deletions src/hike/widgets/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@
##############################################################################
# MarkdownIt imports.
from markdown_it import MarkdownIt
from markdown_it.token import Token
from mdit_py_plugins import front_matter

##############################################################################
# Textual imports.
from textual import on, work
from textual.app import ComposeResult
from textual.await_complete import AwaitComplete
from textual.containers import Vertical
from textual.events import Click
from textual.message import Message
from textual.reactive import var
from textual.widgets import Label, Markdown, Rule
from textual.widgets import Collapsible, Label, Markdown, Rule
from textual.widgets.markdown import MarkdownBlock

##############################################################################
# Textual enhanced imports.
Expand Down Expand Up @@ -108,6 +111,94 @@ class MarkdownScroll(EnhancedVerticalScroll):
"""


##############################################################################
class FrontMatter(Collapsible):
"""A widget to show the front matter of Markdown document."""

DEFAULT_CSS = """
FrontMatter {
padding: 0;
border-top: none;
display: none;
background: transparent;

&.--exists {
display: block;
}

&.-collapsed {
margin-bottom: 1;
}

CollapsibleTitle {
padding: 0;
color: $accent;
}

Contents {
padding: 0;
Label {
padding: 0 0 0 2;
}
}
}
"""

front_matter: var[str | None] = var(None)
"""The front matter to show."""

def __init__(self) -> None:
super().__init__(Label(), Rule(), title="Front matter")

def _watch_front_matter(self) -> None:
self.set_class(
bool(self.front_matter) and load_configuration().show_front_matter,
"--exists",
)
self.query_one(Label).update(self.front_matter or "")


##############################################################################
class HikeDown(Markdown):
"""Hike's `Markdown` wrapper widget."""

DEFAULT_CSS = """
HikeDown {
background: transparent;
}
"""

front_matter: var[str | None] = var(None)
"""The content of any front matter found in the Markdown file."""

def __init__(self) -> None:
"""Initialise the widget."""
super().__init__(
open_links=False,
parser_factory=lambda: MarkdownIt("gfm-like").use(
front_matter.front_matter_plugin
),
)

def update(self, markdown: str) -> AwaitComplete:
self.front_matter = None
return super().update(markdown)

def unhandled_token(self, token: Token) -> MarkdownBlock | None:
"""Handle tokens that Textual's Markdown didn't.

Args:
token: The token to handle.

Returns:
`None` or a `MarkdownBlock`.
"""
if token.type == "front_matter":
self.front_matter = token.content
return None
return super().unhandled_token(token)


##############################################################################
class Viewer(Vertical, can_focus=False):
"""The Markdown viewer widget."""
Expand All @@ -121,9 +212,6 @@ class Viewer(Vertical, can_focus=False):
EnhancedVerticalScroll {
background: transparent;
}
Markdown {
background: transparent;
}
Rule {
height: 1;
margin: 0 !important;
Expand Down Expand Up @@ -165,13 +253,9 @@ def compose(self) -> ComposeResult:
"""Compose the content of the viewer."""
yield ViewerTitle()
yield Rule(line_style="heavy")
yield FrontMatter()
with MarkdownScroll(id="document"):
yield Markdown(
open_links=False,
parser_factory=lambda: MarkdownIt("gfm-like").use(
front_matter.front_matter_plugin
),
)
yield HikeDown()

def focus(self, scroll_visible: bool = True) -> Self:
"""Focus the viewer.
Expand Down Expand Up @@ -360,9 +444,13 @@ async def _update_markdown(self, message: Loaded) -> None:
Args:
message: The message requesting the update.
"""
front_matter_had_focus = self.query_one(FrontMatter).has_focus_within
self.query_one(ViewerTitle).location = self.location
self._source = message.markdown
await self.query_one(Markdown).update(message.markdown)
await (hikedown := self.query_one(HikeDown)).update(message.markdown)
if front_matter_had_focus and not hikedown.front_matter:
self.query_one(MarkdownScroll).focus()
self.query_one(FrontMatter).front_matter = hikedown.front_matter
if (
message.remember
and self.location
Expand Down