Skip to content

Commit

Permalink
Merge pull request #135 from darrenburns/new-theme-system
Browse files Browse the repository at this point in the history
New theme system
  • Loading branch information
darrenburns authored Nov 17, 2024
2 parents 2a3dba6 + 9de9502 commit f8d8d2e
Show file tree
Hide file tree
Showing 73 changed files with 5,441 additions and 4,923 deletions.
Binary file modified .coverage
Binary file not shown.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@

.PHONY: test
test:
$(run) pytest --cov=posting tests/ -n 16 -m "not serial" $(ARGS)
$(run) pytest --cov=posting tests/ -n 24 -m "not serial" $(ARGS)
$(run) pytest --cov-report term-missing --cov-append --cov=posting tests/ -m serial $(ARGS)

.PHONY: test-snapshot-update
test-snapshot-update:
$(run) pytest --cov=posting tests/ -n 16 -m "not serial" --snapshot-update $(ARGS)
$(run) pytest --cov=posting tests/ -n 24 -m "not serial" --snapshot-update $(ARGS)
$(run) pytest --cov-report term-missing --cov-append --cov=posting tests/ -m serial --snapshot-update $(ARGS)


Expand Down
23 changes: 23 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
## 2.2.0 [17th November 2024]

### Added

- Added 15 new themes (4 specific to Posting, 11 inherited from Textual's new theme system).
- Themes are now in submenu of command palette.
- Keybinding assistant can now be displayed as a sidebar, teaching you keybindings as you go.
- New tooltips when hovering over collection browser keybinds in the app footer.

### Changed

- Syntax highlighting colours now derive automatically from the current theme.
- URL bar highlighting now derives automatically from the current theme.
- Method colour-coding in the collection browser is now derived automatically from the current theme.
- Jump mode UI has been refined to be more readable.
- Various refinements to existing themes.
- Options and descriptions in command palette reworded and reordered for clarity.
- Updated to Textual 0.86.1.

### Fixed

- Fixed error notification not rendering correctly when HTTP request times out.

## 2.1.1 [12th November 2024]

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ The table below lists all available configuration options and their environment

| Config Key (Env Var) | Values (Default) | Description |
|----------------------|------------------|-------------|
| `theme` (`POSTING_THEME`) | `"posting"`, `"galaxy"`, `"monokai"`, `"solarized-light"`, `"nautilus"`, `"nebula"`, `"alpine"`, `"cobalt"`, `"twilight"`, `"hacker"` (Default: `"galaxy"`) | Sets the theme of the application. |
| `theme` (`POSTING_THEME`) | See the list of themes in the command palette (Default: `"galaxy"`) | Sets the theme of the application. |
| `load_user_themes` (`POSTING_LOAD_USER_THEMES`) | `true`, `false` (Default: `true`) | If enabled, load user themes from the theme directory, allowing them to be specified in config and selected via the command palette. |
| `load_builtin_themes` (`POSTING_LOAD_BUILTIN_THEMES`) | `true`, `false` (Default: `true`) | If enabled, load builtin themes, allowing them to be specified in config and selected via the command palette. |
| `theme_directory` (`POSTING_THEME_DIRECTORY`) | (Default: `${XDG_DATA_HOME}/posting/themes`) | The directory containing user themes. |
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ This shortcut works globally.
!!! tip "Keyboard shortcuts"

You may also be able to send the request using ++alt+enter++.
This only works on terminals that support the Kitty graphics protocol.
This only works on terminals that support the Kitty keyboard protocol.

### Saving the request

Expand Down
2 changes: 0 additions & 2 deletions docs/guide/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,6 @@ method:
delete: 'strikethrough #b8e986'
```



### X resources themes

Posting supports using X resources for theming. To use this, enable the `use_xresources` option (see above).
Expand Down
3 changes: 2 additions & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ If you have any feedback or suggestions, please open a new discussion on GitHub.

- Keymaps. ✅
- Pre-request and post-response scripts. ✅
- Parse cURL commands.
- Parse cURL commands.
- Watching environment files for changes & updating the UI. ✅
- Editing key/value editor rows without having to delete/re-add them.
- Realtime - WebSocket and SSE.
- Quickly open MDN links for headers.
- Templates. Create a `_template.posting.yaml` file (perhaps a checkbox in the new request modal for this). Any requests created in a collection will be based off of the nearest template (looking upwards to the collection root). Note that this is not "inheritance" - it's a means of quickly pre-filling values in requests based on a template request.
- Saving recently used environments to a file.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies = [
"pyyaml>=6.0.2,<7.0.0",
"pydantic-settings>=2.4.0,<3.0.0",
"python-dotenv>=1.0.1,<2.0.0",
"textual[syntax]==0.85.0",
"textual[syntax]==0.86.1",
# pinned intentionally
"textual-autocomplete==3.0.0a12",
# pinned intentionally
Expand Down
161 changes: 48 additions & 113 deletions src/posting/app.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import inspect
from contextlib import redirect_stdout, redirect_stderr
from itertools import cycle
from pathlib import Path
from typing import Any, Literal, cast

import httpx
from textual.content import Content

from posting.importing.curl import CurlImport
from rich.console import Group
from rich.text import Text
from textual import on, log, work
from textual.command import CommandPalette
from textual.css.query import NoMatches
Expand All @@ -18,6 +18,7 @@
from textual.containers import Horizontal, Vertical
from textual.screen import Screen
from textual.signal import Signal
from textual.theme import Theme, BUILTIN_THEMES as TEXTUAL_THEMES
from textual.widget import Widget
from textual.widgets import (
Button,
Expand All @@ -27,7 +28,6 @@
TextArea,
)
from textual.widgets._tabbed_content import ContentTab
from textual.widgets.text_area import TextAreaTheme
from watchfiles import Change, awatch
from posting.collection import (
Collection,
Expand All @@ -44,7 +44,7 @@
from posting.jump_overlay import JumpOverlay
from posting.jumper import Jumper
from posting.scripts import execute_script, uncache_module, Posting as PostingContext
from posting.themes import BUILTIN_THEMES, Theme, load_user_themes
from posting.themes import BUILTIN_THEMES, load_user_themes
from posting.types import CertTypes, PostingLayout
from posting.user_host import get_user_host_string
from posting.variables import SubstitutionError, get_variables, update_variables
Expand Down Expand Up @@ -78,30 +78,12 @@
class AppHeader(Horizontal):
"""The header of the app."""

DEFAULT_CSS = """\
AppHeader {
color: $accent-lighten-2;
padding: 0 3;
margin-top: 1;
height: 1;
& > #app-title {
dock: left;
}
& > #app-user-host {
dock: right;
color: $text-muted;
}
}
"""

def compose(self) -> ComposeResult:
settings = SETTINGS.get().heading
if settings.show_version:
yield Label(f"Posting [dim]{VERSION}[/]", id="app-title")
yield Label(f"[b]Posting[/] [dim]{VERSION}[/]", id="app-title")
else:
yield Label("Posting", id="app-title")
yield Label("[b]Posting[/]", id="app-title")
if settings.show_host:
yield Label(get_user_host_string(), id="app-user-host")

Expand All @@ -120,6 +102,7 @@ class AppBody(Vertical):

class MainScreen(Screen[None]):
AUTO_FOCUS = None
BINDING_GROUP_TITLE = "Main Screen"
BINDINGS = [
Binding(
"ctrl+j,alt+enter",
Expand Down Expand Up @@ -356,11 +339,12 @@ async def send_request(self) -> None:
# use the CA bundle.
verify = cert_config.ca_bundle

timeout = request_model.options.timeout
async with httpx.AsyncClient(
verify=verify,
cert=cert,
proxy=request_model.options.proxy_url or None,
timeout=request_model.options.timeout,
timeout=timeout,
auth=request_model.auth.to_httpx_auth() if request_model.auth else None,
) as client:
script_context.request = request_model
Expand Down Expand Up @@ -425,7 +409,7 @@ async def send_request(self) -> None:
self.notify(
severity="error",
title="Connect timeout",
message=f"Couldn't connect within {connect_timeout} seconds.",
message=f"Couldn't connect within {timeout} seconds.",
)
except Exception as e:
log.error("Error sending request", e)
Expand Down Expand Up @@ -700,7 +684,7 @@ def on_curl_message(self, event: CurlMessage):
except Exception as e:
self.notify(
title="Import error",
message=f"Couldn't import curl command.",
message="Couldn't import curl command.",
timeout=5,
severity="error",
)
Expand Down Expand Up @@ -819,6 +803,7 @@ class Posting(App[None], inherit_bindings=False):
AUTO_FOCUS = None
COMMANDS = {PostingProvider}
CSS_PATH = Path(__file__).parent / "posting.scss"
BINDING_GROUP_TITLE = "Global Keybinds"
BINDINGS = [
Binding(
"ctrl+p",
Expand Down Expand Up @@ -861,27 +846,6 @@ def __init__(
) -> None:
SETTINGS.set(settings)

available_themes: dict[str, Theme] = {"galaxy": BUILTIN_THEMES["galaxy"]}

if settings.load_builtin_themes:
available_themes |= BUILTIN_THEMES

if settings.use_xresources:
available_themes |= load_xresources_themes()

if settings.load_user_themes:
available_themes |= load_user_themes()

self.themes = available_themes
"""The themes that are available to the app, potentially including
themes loaded from the user's themes directory and xresources themes
if those configuration options are enabled."""

# We need to call super.__init__ after the themes are loaded,
# because our `get_css_variables` override depends on
# the themes dict being available.
super().__init__()

self.settings = settings
"""Settings object which is built via pydantic-settings,
essentially a direct translation of the config.yaml file."""
Expand Down Expand Up @@ -910,9 +874,7 @@ def __init__(
session (until the app is quit). This can be done via the scripting
interface: pre-request or post-response scripts."""

theme: Reactive[str] = reactive("galaxy", init=False)
"""The currently selected theme. Changing this reactive should
trigger a complete refresh via the `watch_theme` method."""
super().__init__()

_jumping: Reactive[bool] = reactive(False, init=False, bindings=True)
"""True if 'jump mode' is currently active, otherwise False."""
Expand Down Expand Up @@ -971,6 +933,33 @@ async def watch_collection_files(self) -> None:
pass

def on_mount(self) -> None:
settings = SETTINGS.get()

available_themes: dict[str, Theme] = {"galaxy": BUILTIN_THEMES["galaxy"]}

if settings.load_builtin_themes:
available_themes |= BUILTIN_THEMES
else:
for theme in TEXTUAL_THEMES.values():
self.unregister_theme(theme.name)

if settings.use_xresources:
available_themes |= load_xresources_themes()

if settings.load_user_themes:
available_themes |= load_user_themes()

for theme in available_themes.values():
self.register_theme(theme)

unwanted_themes = [
"textual-ansi",
]
for theme_name in unwanted_themes:
self.unregister_theme(theme_name)

self.theme = settings.theme

self.set_keymap(self.settings.keymap)
self.jumper = Jumper(
{
Expand All @@ -992,8 +981,6 @@ def on_mount(self) -> None:
},
screen=self.screen,
)
self.theme_change_signal = Signal[Theme](self, "theme-changed")
self.theme = self.settings.theme
if self.settings.watch_env_files:
self.watch_environment_files()

Expand All @@ -1008,26 +995,9 @@ def get_default_screen(self) -> MainScreen:
)
return self.main_screen

def get_css_variables(self) -> dict[str, str]:
if self.theme:
theme = self.themes.get(self.theme)
if theme:
color_system = theme.to_color_system().generate()
else:
color_system = {}
else:
color_system = {}
return {**super().get_css_variables(), **color_system}

def command_layout(self, layout: Literal["vertical", "horizontal"]) -> None:
self.main_screen.current_layout = layout

def command_theme(self, theme: str) -> None:
self.theme = theme
self.notify(
f"Theme is now [b]{theme!r}[/].", title="Theme updated", timeout=2.5
)

def action_save_screenshot(
self,
) -> str:
Expand All @@ -1052,22 +1022,15 @@ def palette_option_highlighted(
if not self.settings.command_palette.theme_preview:
return

prompt: Group = event.highlighted_event.option.prompt
# TODO: This is making quite a lot of assumptions. Fragile, but the only
# way I can think of doing it given the current Textual APIs.
command_name = prompt.renderables[0]
if isinstance(command_name, Text):
command_name = command_name.plain
command_name = command_name.strip()
if ":" in command_name:
name, value = command_name.split(":", maxsplit=1)
name = name.strip()
value = value.strip()
if name == "theme":
if value in self.themes:
self.theme = value
prompt = event.highlighted_event.option.prompt
themes = self.available_themes.keys()
if isinstance(prompt, Content):
candidate = prompt.plain
if candidate in themes:
self.theme = candidate
else:
self.theme = self._original_theme
self.call_next(self.screen._update_styles)

@on(CommandPalette.Closed)
def palette_closed(self, event: CommandPalette.Closed) -> None:
Expand All @@ -1079,34 +1042,6 @@ def palette_closed(self, event: CommandPalette.Closed) -> None:
if not event.option_selected:
self.theme = self._original_theme

def watch_theme(self, theme: str | None) -> None:
self.refresh_css(animate=False)
self.screen._update_styles()
if theme:
theme_object = self.themes[theme]
if syntax := getattr(theme_object, "syntax", None):
if isinstance(syntax, str):
valid_themes = {
theme.name for theme in TextAreaTheme.builtin_themes()
}
valid_themes.add("posting")
if syntax not in valid_themes:
# Default to the posting theme for text areas
# if the specified theme is invalid.
theme_object.syntax = "posting"
self.notify(
f"Theme {theme!r} has an invalid value for 'syntax': {syntax!r}. Defaulting to 'posting'.",
title="Invalid theme",
severity="warning",
timeout=7,
)

self.theme_change_signal.publish(theme_object)

@property
def theme_object(self) -> Theme:
return self.themes[self.theme]

def action_toggle_jump_mode(self) -> None:
self._jumping = not self._jumping

Expand All @@ -1128,7 +1063,7 @@ def handle_jump_target(target: str | Widget | None) -> None:
self.set_focus(target_widget)
else:
target_widget.post_message(
Click(0, 0, 0, 0, 0, False, False, False)
Click(target_widget, 0, 0, 0, 0, 0, False, False, False)
)

elif isinstance(target, Widget):
Expand Down
Loading

0 comments on commit f8d8d2e

Please sign in to comment.