xkcdkit 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xkcdkit-0.1.0 → xkcdkit-0.2.0}/PKG-INFO +41 -2
- {xkcdkit-0.1.0 → xkcdkit-0.2.0}/README.md +40 -1
- {xkcdkit-0.1.0 → xkcdkit-0.2.0}/pyproject.toml +1 -1
- {xkcdkit-0.1.0 → xkcdkit-0.2.0}/pyproject.toml.orig +1 -1
- {xkcdkit-0.1.0 → xkcdkit-0.2.0}/src/xkcdkit/cli.py +50 -8
- xkcdkit-0.2.0/src/xkcdkit/config.py +76 -0
- {xkcdkit-0.1.0 → xkcdkit-0.2.0}/src/xkcdkit/render.py +20 -4
- {xkcdkit-0.1.0 → xkcdkit-0.2.0}/src/xkcdkit/tui.py +27 -7
- {xkcdkit-0.1.0 → xkcdkit-0.2.0}/src/xkcdkit/__init__.py +0 -0
- {xkcdkit-0.1.0 → xkcdkit-0.2.0}/src/xkcdkit/client.py +0 -0
- {xkcdkit-0.1.0 → xkcdkit-0.2.0}/src/xkcdkit/models.py +0 -0
- {xkcdkit-0.1.0 → xkcdkit-0.2.0}/src/xkcdkit/py.typed +0 -0
- {xkcdkit-0.1.0 → xkcdkit-0.2.0}/src/xkcdkit/sources.py +0 -0
- {xkcdkit-0.1.0 → xkcdkit-0.2.0}/src/xkcdkit/store.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xkcdkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: xkcd comics as a Python library, a CLI and a TUI, with a local cache and explainxkcd explanations.
|
|
5
5
|
Keywords: xkcd,comics,cli,tui,textual
|
|
6
6
|
Author: Eivind Teig
|
|
@@ -130,6 +130,40 @@ xkcd search "sandwich" --json | jq '.[].num'
|
|
|
130
130
|
`--no-image` skips the picture. Images are also skipped automatically when
|
|
131
131
|
stdout is not a terminal, so pipes always get clean text.
|
|
132
132
|
|
|
133
|
+
Images are drawn at the size the terminal reports for them, which can be
|
|
134
|
+
small on a high-resolution screen. `--scale 2` (or `-s 3`, any factor) makes
|
|
135
|
+
them bigger, and `--fit` fills the terminal while keeping the aspect ratio:
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
xkcd show 614 --scale 2
|
|
139
|
+
xkcd latest --fit
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Settings file
|
|
143
|
+
|
|
144
|
+
Defaults for these live in a small TOML file. `xkcd config` prints its
|
|
145
|
+
location and the current values; `xkcd config --init` writes a commented
|
|
146
|
+
file with every key:
|
|
147
|
+
|
|
148
|
+
| Platform | Location |
|
|
149
|
+
| -------- | -------- |
|
|
150
|
+
| Linux | `~/.config/xkcdkit/config.toml` |
|
|
151
|
+
| macOS | `~/Library/Application Support/xkcdkit/config.toml` |
|
|
152
|
+
| Windows | `%APPDATA%\xkcdkit\config.toml` |
|
|
153
|
+
|
|
154
|
+
Set `XKCDKIT_CONFIG` to use another path.
|
|
155
|
+
|
|
156
|
+
```toml
|
|
157
|
+
[image]
|
|
158
|
+
scale = 2.0 # CLI image size multiplier
|
|
159
|
+
fit = false # CLI: fill the terminal instead
|
|
160
|
+
|
|
161
|
+
[tui]
|
|
162
|
+
image_pane = 3 # browser: image pane height relative to the text pane (1 to 8)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Command line flags always win over the file.
|
|
166
|
+
|
|
133
167
|
### The browser
|
|
134
168
|
|
|
135
169
|
`xkcd tui` opens a two-pane browser in the spirit of lazygit: the archive on
|
|
@@ -139,15 +173,20 @@ explanation.
|
|
|
139
173
|
| Key | Action |
|
|
140
174
|
| ------- | -------------------------------- |
|
|
141
175
|
| `j`/`k` | move through the archive |
|
|
142
|
-
| `/` | filter the archive by title
|
|
176
|
+
| `/` | filter the archive by title; up/down still move the list, Esc returns to it |
|
|
143
177
|
| `Enter` | open the selected comic |
|
|
144
178
|
| `r` | random comic |
|
|
145
179
|
| `l` | latest comic |
|
|
146
180
|
| `i` | show or hide the image pane |
|
|
181
|
+
| `+`/`-` | grow or shrink the image pane |
|
|
147
182
|
| `s` | sync the archive index |
|
|
148
183
|
| `?` | key help |
|
|
149
184
|
| `q` | quit |
|
|
150
185
|
|
|
186
|
+
The image always fills its pane while keeping its aspect ratio, so the pane
|
|
187
|
+
height is the zoom. The starting height comes from `image_pane` in the
|
|
188
|
+
settings file.
|
|
189
|
+
|
|
151
190
|
## Library
|
|
152
191
|
|
|
153
192
|
The async client is the primary API. It is built for I/O bound programs such
|
|
@@ -98,6 +98,40 @@ xkcd search "sandwich" --json | jq '.[].num'
|
|
|
98
98
|
`--no-image` skips the picture. Images are also skipped automatically when
|
|
99
99
|
stdout is not a terminal, so pipes always get clean text.
|
|
100
100
|
|
|
101
|
+
Images are drawn at the size the terminal reports for them, which can be
|
|
102
|
+
small on a high-resolution screen. `--scale 2` (or `-s 3`, any factor) makes
|
|
103
|
+
them bigger, and `--fit` fills the terminal while keeping the aspect ratio:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
xkcd show 614 --scale 2
|
|
107
|
+
xkcd latest --fit
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Settings file
|
|
111
|
+
|
|
112
|
+
Defaults for these live in a small TOML file. `xkcd config` prints its
|
|
113
|
+
location and the current values; `xkcd config --init` writes a commented
|
|
114
|
+
file with every key:
|
|
115
|
+
|
|
116
|
+
| Platform | Location |
|
|
117
|
+
| -------- | -------- |
|
|
118
|
+
| Linux | `~/.config/xkcdkit/config.toml` |
|
|
119
|
+
| macOS | `~/Library/Application Support/xkcdkit/config.toml` |
|
|
120
|
+
| Windows | `%APPDATA%\xkcdkit\config.toml` |
|
|
121
|
+
|
|
122
|
+
Set `XKCDKIT_CONFIG` to use another path.
|
|
123
|
+
|
|
124
|
+
```toml
|
|
125
|
+
[image]
|
|
126
|
+
scale = 2.0 # CLI image size multiplier
|
|
127
|
+
fit = false # CLI: fill the terminal instead
|
|
128
|
+
|
|
129
|
+
[tui]
|
|
130
|
+
image_pane = 3 # browser: image pane height relative to the text pane (1 to 8)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Command line flags always win over the file.
|
|
134
|
+
|
|
101
135
|
### The browser
|
|
102
136
|
|
|
103
137
|
`xkcd tui` opens a two-pane browser in the spirit of lazygit: the archive on
|
|
@@ -107,15 +141,20 @@ explanation.
|
|
|
107
141
|
| Key | Action |
|
|
108
142
|
| ------- | -------------------------------- |
|
|
109
143
|
| `j`/`k` | move through the archive |
|
|
110
|
-
| `/` | filter the archive by title
|
|
144
|
+
| `/` | filter the archive by title; up/down still move the list, Esc returns to it |
|
|
111
145
|
| `Enter` | open the selected comic |
|
|
112
146
|
| `r` | random comic |
|
|
113
147
|
| `l` | latest comic |
|
|
114
148
|
| `i` | show or hide the image pane |
|
|
149
|
+
| `+`/`-` | grow or shrink the image pane |
|
|
115
150
|
| `s` | sync the archive index |
|
|
116
151
|
| `?` | key help |
|
|
117
152
|
| `q` | quit |
|
|
118
153
|
|
|
154
|
+
The image always fills its pane while keeping its aspect ratio, so the pane
|
|
155
|
+
height is the zoom. The starting height comes from `image_pane` in the
|
|
156
|
+
settings file.
|
|
157
|
+
|
|
119
158
|
## Library
|
|
120
159
|
|
|
121
160
|
The async client is the primary API. It is built for I/O bound programs such
|
|
@@ -12,6 +12,7 @@ from rich.text import Text
|
|
|
12
12
|
|
|
13
13
|
from xkcdkit import XKCDError, __version__
|
|
14
14
|
from xkcdkit.client import AsyncXKCD
|
|
15
|
+
from xkcdkit.config import config_path, load_settings, write_default
|
|
15
16
|
from xkcdkit.models import Comic
|
|
16
17
|
from xkcdkit.render import can_draw, comic_image, comic_text
|
|
17
18
|
from xkcdkit.sources import fetch_image
|
|
@@ -21,6 +22,8 @@ console = Console()
|
|
|
21
22
|
errors = Console(stderr=True)
|
|
22
23
|
Num = Annotated[int, typer.Argument(help="Comic number.")]
|
|
23
24
|
Json = Annotated[bool, typer.Option("--json", help="Print JSON for piping.")]
|
|
25
|
+
Scale = Annotated[float | None, typer.Option("--scale", "-s", help="Image size multiplier.")]
|
|
26
|
+
Fit = Annotated[bool, typer.Option("--fit", help="Fill the terminal with the image.")]
|
|
24
27
|
NoImage = Annotated[bool, typer.Option("--no-image", help="Do not draw the image.")]
|
|
25
28
|
Explain = Annotated[bool, typer.Option("--explain", "-e", help="Include the explanation.")]
|
|
26
29
|
|
|
@@ -75,6 +78,14 @@ class View:
|
|
|
75
78
|
json: bool = False
|
|
76
79
|
image: bool = True
|
|
77
80
|
explain: bool = False
|
|
81
|
+
scale: float = 1.0
|
|
82
|
+
fit: bool = False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def view(json: bool, no_image: bool, explain: bool, scale: float | None, fit: bool) -> View:
|
|
86
|
+
"""Combine command flags with the settings file; flags win."""
|
|
87
|
+
image = load_settings().image
|
|
88
|
+
return View(json, not no_image, explain, scale or image.scale, fit or image.fit)
|
|
78
89
|
|
|
79
90
|
|
|
80
91
|
async def present(xk: AsyncXKCD, pending: Awaitable[Comic], view: View) -> None:
|
|
@@ -84,32 +95,48 @@ async def present(xk: AsyncXKCD, pending: Awaitable[Comic], view: View) -> None:
|
|
|
84
95
|
console.print_json(comic.model_dump_json())
|
|
85
96
|
return
|
|
86
97
|
if view.image and can_draw():
|
|
87
|
-
console.print(comic_image(await fetch_image(xk.http, comic)))
|
|
98
|
+
console.print(comic_image(await fetch_image(xk.http, comic), view.scale, view.fit))
|
|
88
99
|
console.print(comic_text(comic, explain=view.explain))
|
|
89
100
|
|
|
90
101
|
|
|
91
102
|
@app.command()
|
|
92
103
|
def show(
|
|
93
|
-
ctx: typer.Context,
|
|
104
|
+
ctx: typer.Context,
|
|
105
|
+
num: Num,
|
|
106
|
+
no_image: NoImage = False,
|
|
107
|
+
explain: Explain = False,
|
|
108
|
+
json: Json = False,
|
|
109
|
+
scale: Scale = None,
|
|
110
|
+
fit: Fit = False,
|
|
94
111
|
) -> None:
|
|
95
112
|
"""Show one comic by number."""
|
|
96
|
-
run(ctx, lambda xk: present(xk, xk.get(num),
|
|
113
|
+
run(ctx, lambda xk: present(xk, xk.get(num), view(json, no_image, explain, scale, fit)))
|
|
97
114
|
|
|
98
115
|
|
|
99
116
|
@app.command()
|
|
100
117
|
def latest(
|
|
101
|
-
ctx: typer.Context,
|
|
118
|
+
ctx: typer.Context,
|
|
119
|
+
no_image: NoImage = False,
|
|
120
|
+
explain: Explain = False,
|
|
121
|
+
json: Json = False,
|
|
122
|
+
scale: Scale = None,
|
|
123
|
+
fit: Fit = False,
|
|
102
124
|
) -> None:
|
|
103
125
|
"""Show the newest comic."""
|
|
104
|
-
run(ctx, lambda xk: present(xk, xk.latest(),
|
|
126
|
+
run(ctx, lambda xk: present(xk, xk.latest(), view(json, no_image, explain, scale, fit)))
|
|
105
127
|
|
|
106
128
|
|
|
107
129
|
@app.command()
|
|
108
130
|
def random(
|
|
109
|
-
ctx: typer.Context,
|
|
131
|
+
ctx: typer.Context,
|
|
132
|
+
no_image: NoImage = False,
|
|
133
|
+
explain: Explain = False,
|
|
134
|
+
json: Json = False,
|
|
135
|
+
scale: Scale = None,
|
|
136
|
+
fit: Fit = False,
|
|
110
137
|
) -> None:
|
|
111
138
|
"""Show a random comic."""
|
|
112
|
-
run(ctx, lambda xk: present(xk, xk.random(),
|
|
139
|
+
run(ctx, lambda xk: present(xk, xk.random(), view(json, no_image, explain, scale, fit)))
|
|
113
140
|
|
|
114
141
|
|
|
115
142
|
@app.command()
|
|
@@ -176,4 +203,19 @@ def tui(ctx: typer.Context) -> None:
|
|
|
176
203
|
"""Open the interactive browser."""
|
|
177
204
|
from xkcdkit.tui import run_tui
|
|
178
205
|
|
|
179
|
-
run_tui(open_client(ctx.obj.db))
|
|
206
|
+
run_tui(open_client(ctx.obj.db), load_settings())
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@app.command()
|
|
210
|
+
def config(
|
|
211
|
+
init: Annotated[bool, typer.Option("--init", help="Create the file with defaults.")] = False,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Show where settings live and what they are; --init writes a commented default file."""
|
|
214
|
+
path = config_path()
|
|
215
|
+
if init:
|
|
216
|
+
console.print(
|
|
217
|
+
f"{'created' if write_default(path) else 'exists'}: {path}", markup=False, soft_wrap=True
|
|
218
|
+
)
|
|
219
|
+
return
|
|
220
|
+
console.print(f"settings file: {path}", markup=False, soft_wrap=True)
|
|
221
|
+
console.print_json(load_settings(path).model_dump_json())
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""User settings from a small TOML file in the platform config directory."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tomllib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from platformdirs import user_config_path
|
|
8
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
9
|
+
|
|
10
|
+
from xkcdkit import XKCDError
|
|
11
|
+
|
|
12
|
+
DEFAULT_TOML = """# xkcdkit settings. Every key is optional; these are the defaults.
|
|
13
|
+
|
|
14
|
+
[image]
|
|
15
|
+
# CLI: multiply the image's natural terminal size (1 = as the terminal sizes it, 2 = twice as big).
|
|
16
|
+
scale = 1.0
|
|
17
|
+
# CLI: ignore scale and fill the terminal, keeping the aspect ratio.
|
|
18
|
+
fit = false
|
|
19
|
+
|
|
20
|
+
[tui]
|
|
21
|
+
# Height of the image pane relative to the text pane (1 to 8). Change live with + and -.
|
|
22
|
+
image_pane = 2
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ImageSettings(BaseModel):
|
|
27
|
+
"""How the CLI draws images."""
|
|
28
|
+
|
|
29
|
+
scale: float = Field(default=1.0, gt=0, le=16)
|
|
30
|
+
fit: bool = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TuiSettings(BaseModel):
|
|
34
|
+
"""How the browser lays itself out."""
|
|
35
|
+
|
|
36
|
+
image_pane: int = Field(default=2, ge=1, le=8)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Settings(BaseModel):
|
|
40
|
+
"""All settings, mirroring the sections of the TOML file."""
|
|
41
|
+
|
|
42
|
+
image: ImageSettings = ImageSettings()
|
|
43
|
+
tui: TuiSettings = TuiSettings()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def config_path() -> Path:
|
|
47
|
+
"""Settings file location: ``$XKCDKIT_CONFIG`` or the platform user config directory."""
|
|
48
|
+
override = os.environ.get("XKCDKIT_CONFIG")
|
|
49
|
+
if override:
|
|
50
|
+
return Path(override)
|
|
51
|
+
return user_config_path("xkcdkit") / "config.toml"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_settings(path: Path | None = None) -> Settings:
|
|
55
|
+
"""Read the settings file, falling back to defaults when it does not exist.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
XKCDError: when the file is not valid TOML or holds an invalid value.
|
|
59
|
+
"""
|
|
60
|
+
path = path or config_path()
|
|
61
|
+
if not path.exists():
|
|
62
|
+
return Settings()
|
|
63
|
+
try:
|
|
64
|
+
return Settings.model_validate(tomllib.loads(path.read_text(encoding="utf-8")))
|
|
65
|
+
except (tomllib.TOMLDecodeError, ValidationError) as exc:
|
|
66
|
+
raise XKCDError(f"{path}: {exc}") from exc
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def write_default(path: Path | None = None) -> bool:
|
|
70
|
+
"""Create the settings file with documented defaults. Returns False if it already exists."""
|
|
71
|
+
path = path or config_path()
|
|
72
|
+
if path.exists():
|
|
73
|
+
return False
|
|
74
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
path.write_text(DEFAULT_TOML, encoding="utf-8")
|
|
76
|
+
return True
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
import io
|
|
4
4
|
import sys
|
|
5
5
|
|
|
6
|
+
from PIL import Image as PILImage
|
|
6
7
|
from rich.console import Group, RenderableType
|
|
7
8
|
from rich.panel import Panel
|
|
8
9
|
from rich.text import Text
|
|
9
10
|
|
|
11
|
+
from xkcdkit import XKCDError
|
|
10
12
|
from xkcdkit.models import Comic
|
|
11
13
|
|
|
12
14
|
|
|
@@ -26,15 +28,29 @@ def explanation_text(comic: Comic) -> Text:
|
|
|
26
28
|
return Text(comic.explanation_text)
|
|
27
29
|
|
|
28
30
|
|
|
29
|
-
def comic_image(data: bytes) -> RenderableType:
|
|
31
|
+
def comic_image(data: bytes, scale: float = 1.0, fit: bool = False) -> RenderableType:
|
|
30
32
|
"""A Rich renderable for the image bytes using the best protocol the terminal supports.
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
``scale`` multiplies the image's natural terminal size; ``fit`` instead fills the
|
|
35
|
+
terminal while keeping the aspect ratio. textual-image probes the terminal on
|
|
36
|
+
import, so it is imported here rather than at module load.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
XKCDError: when the bytes are not an image.
|
|
34
40
|
"""
|
|
35
41
|
from textual_image.renderable import Image
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
try:
|
|
44
|
+
picture: PILImage.Image = PILImage.open(io.BytesIO(data))
|
|
45
|
+
picture.load()
|
|
46
|
+
except OSError as exc:
|
|
47
|
+
raise XKCDError(f"not an image: {exc}") from exc
|
|
48
|
+
if fit:
|
|
49
|
+
renderable: RenderableType = Image(picture, width="auto", height="auto")
|
|
50
|
+
return renderable
|
|
51
|
+
if scale != 1:
|
|
52
|
+
picture = picture.resize((round(picture.width * scale), round(picture.height * scale)))
|
|
53
|
+
renderable = Image(picture)
|
|
38
54
|
return renderable
|
|
39
55
|
|
|
40
56
|
|
|
@@ -16,10 +16,12 @@ from textual_image.widget import Image
|
|
|
16
16
|
|
|
17
17
|
from xkcdkit import XKCDError
|
|
18
18
|
from xkcdkit.client import AsyncXKCD
|
|
19
|
+
from xkcdkit.config import Settings
|
|
19
20
|
from xkcdkit.models import ArchiveEntry, Comic
|
|
20
21
|
from xkcdkit.sources import fetch_image
|
|
21
22
|
|
|
22
|
-
HELP = "j/k move / filter enter open r random l latest i image s sync q quit"
|
|
23
|
+
HELP = "j/k move / filter enter open r random l latest i image +/- zoom s sync q quit"
|
|
24
|
+
MAX_PANE = 8
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
def option(entry: ArchiveEntry) -> Option:
|
|
@@ -35,6 +37,7 @@ class XKCDApp(App[None]):
|
|
|
35
37
|
#archive-pane { width: 40; border: round $primary; }
|
|
36
38
|
#archive { height: 1fr; }
|
|
37
39
|
#image-pane { height: 2fr; border: round $secondary; align: center middle; }
|
|
40
|
+
#image { width: auto; height: auto; }
|
|
38
41
|
TabbedContent { height: 1fr; }
|
|
39
42
|
.text { padding: 0 1; }
|
|
40
43
|
"""
|
|
@@ -45,15 +48,21 @@ class XKCDApp(App[None]):
|
|
|
45
48
|
Binding("r", "random", "Random"),
|
|
46
49
|
Binding("l", "latest", "Latest"),
|
|
47
50
|
Binding("i", "toggle_image", "Image"),
|
|
51
|
+
Binding("plus,equals_sign", "zoom(1)", "Zoom", key_display="+/-"),
|
|
52
|
+
Binding("minus", "zoom(-1)", "Zoom out", show=False),
|
|
48
53
|
Binding("s", "sync", "Sync"),
|
|
54
|
+
Binding("down", "cursor('down')", "Down", show=False),
|
|
55
|
+
Binding("up", "cursor('up')", "Up", show=False),
|
|
56
|
+
Binding("escape", "focus_list", "List", show=False),
|
|
49
57
|
Binding("question_mark", "help", "Help", key_display="?"),
|
|
50
58
|
Binding("q", "quit", "Quit"),
|
|
51
59
|
]
|
|
52
60
|
|
|
53
|
-
def __init__(self, client: AsyncXKCD) -> None:
|
|
54
|
-
"""Keep the client
|
|
61
|
+
def __init__(self, client: AsyncXKCD, settings: Settings | None = None) -> None:
|
|
62
|
+
"""Keep the client (its store feeds the archive list) and the user's settings."""
|
|
55
63
|
super().__init__()
|
|
56
64
|
self.client = client
|
|
65
|
+
self.settings = settings or Settings()
|
|
57
66
|
self.entries: list[ArchiveEntry] = []
|
|
58
67
|
|
|
59
68
|
def compose(self) -> ComposeResult:
|
|
@@ -77,6 +86,7 @@ class XKCDApp(App[None]):
|
|
|
77
86
|
def on_mount(self) -> None:
|
|
78
87
|
"""Show the cached archive (syncing it on first run) and open the latest comic."""
|
|
79
88
|
self.query_one(OptionList).focus()
|
|
89
|
+
self.query_one("#image-pane").styles.height = f"{self.settings.tui.image_pane}fr"
|
|
80
90
|
self.entries = self.client.store.archive()
|
|
81
91
|
self.show_entries(self.entries)
|
|
82
92
|
if not self.entries:
|
|
@@ -100,10 +110,10 @@ class XKCDApp(App[None]):
|
|
|
100
110
|
self.show_entries([e for score, e in scored if score >= 70])
|
|
101
111
|
|
|
102
112
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
103
|
-
"""Enter in the filter opens the first
|
|
113
|
+
"""Enter in the filter opens the highlighted match (the first unless moved)."""
|
|
104
114
|
archive = self.query_one(OptionList)
|
|
105
115
|
if archive.option_count:
|
|
106
|
-
self.open(int(str(archive.get_option_at_index(0).id)))
|
|
116
|
+
self.open(int(str(archive.get_option_at_index(archive.highlighted or 0).id)))
|
|
107
117
|
|
|
108
118
|
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
|
109
119
|
"""Enter on a row opens that comic."""
|
|
@@ -150,6 +160,16 @@ class XKCDApp(App[None]):
|
|
|
150
160
|
"""Jump to the filter box."""
|
|
151
161
|
self.query_one("#filter", Input).focus()
|
|
152
162
|
|
|
163
|
+
def action_focus_list(self) -> None:
|
|
164
|
+
"""Return to the archive list."""
|
|
165
|
+
self.query_one(OptionList).focus()
|
|
166
|
+
|
|
167
|
+
def action_zoom(self, step: int) -> None:
|
|
168
|
+
"""Grow or shrink the image pane relative to the text pane."""
|
|
169
|
+
pane = self.query_one("#image-pane")
|
|
170
|
+
self.settings.tui.image_pane = min(MAX_PANE, max(1, self.settings.tui.image_pane + step))
|
|
171
|
+
pane.styles.height = f"{self.settings.tui.image_pane}fr"
|
|
172
|
+
|
|
153
173
|
def action_random(self) -> None:
|
|
154
174
|
"""Open a random comic."""
|
|
155
175
|
self.open(-1)
|
|
@@ -180,9 +200,9 @@ class XKCDApp(App[None]):
|
|
|
180
200
|
self.notify(HELP, title="Keys", timeout=8)
|
|
181
201
|
|
|
182
202
|
|
|
183
|
-
def run_tui(client: AsyncXKCD) -> None:
|
|
203
|
+
def run_tui(client: AsyncXKCD, settings: Settings | None = None) -> None:
|
|
184
204
|
"""Start the browser and close the client when it exits."""
|
|
185
205
|
try:
|
|
186
|
-
XKCDApp(client).run()
|
|
206
|
+
XKCDApp(client, settings).run()
|
|
187
207
|
finally:
|
|
188
208
|
client.store.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|