schenesort 2.3.0__tar.gz → 2.4.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.
- {schenesort-2.3.0 → schenesort-2.4.0}/PKG-INFO +81 -10
- {schenesort-2.3.0 → schenesort-2.4.0}/README.md +80 -9
- schenesort-2.4.0/docs/gallery.png +0 -0
- schenesort-2.4.0/docs/landscape.png +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/pyproject.toml +2 -2
- {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/cli.py +121 -0
- schenesort-2.4.0/src/schenesort/thumbnails.py +127 -0
- schenesort-2.4.0/src/schenesort/tui/__init__.py +15 -0
- schenesort-2.4.0/src/schenesort/tui/grid_app.py +302 -0
- schenesort-2.4.0/src/schenesort/tui/widgets/filter_panel.py +249 -0
- schenesort-2.4.0/src/schenesort/tui/widgets/thumbnail_grid.py +291 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/uv.lock +1 -1
- schenesort-2.3.0/src/schenesort/tui/__init__.py +0 -5
- {schenesort-2.3.0 → schenesort-2.4.0}/.envrc +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/.github/workflows/ci.yml +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/.github/workflows/publish.yml +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/.gitignore +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/.pre-commit-config.yaml +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/.python-version +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/CLAUDE.md +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/LICENSE +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/docs/browse-autumn.png +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/docs/browse-destruction.png +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/docs/browse-greek.png +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/docs/browse-stallman.png +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/docs/collage.png +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/justfile +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/ollama-setup.md +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/schenesort.yazi/README.md +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/schenesort.yazi/main.lua +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/schenesort.yazi/schenesort.config +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/__init__.py +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/config.py +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/db.py +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/tui/app.py +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/tui/widgets/__init__.py +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/tui/widgets/image_preview.py +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/tui/widgets/metadata_panel.py +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/xmp.py +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/tests/__init__.py +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/tests/test_cli.py +0 -0
- {schenesort-2.3.0 → schenesort-2.4.0}/tests/test_sanitise.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: schenesort
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Wallpaper collection management CLI tool
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.13
|
|
@@ -12,12 +12,14 @@ Requires-Dist: textual>=0.95.0
|
|
|
12
12
|
Requires-Dist: typer>=0.21.1
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
|
|
15
|
-
# Schenesort v2.
|
|
15
|
+
# Schenesort v2.4.0
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
A cli tool for managing wallpaper collections with model generated metadata, sweet tui, and sql metadata querying.
|
|
20
|
+
|
|
21
|
+
schenesort takes a directory of random wallpapers, with random filenames, and uses olama with a decent vision model to:
|
|
19
22
|
|
|
20
|
-
schenesort takes a directory of random wallpapers, with random filenames, and uses olama with a decent vision model to
|
|
21
23
|
- look at each wallpaper
|
|
22
24
|
- rename the wallpaper to something sensible
|
|
23
25
|
- drop a XMP sidecar with metadata about the file
|
|
@@ -51,12 +53,18 @@ setup](./ollama-setup.md).
|
|
|
51
53
|
# Generate metadata for images
|
|
52
54
|
schenesort metadata generate ~/wallpapers -r
|
|
53
55
|
|
|
54
|
-
# Browse with TUI
|
|
55
|
-
schenesort browse ~/wallpapers
|
|
56
|
-
|
|
57
56
|
# Index the collection
|
|
58
57
|
schenesort index ~/wallpapers
|
|
59
58
|
|
|
59
|
+
# Generate thumbnails for gallery
|
|
60
|
+
schenesort thumbnail ~/wallpapers -r
|
|
61
|
+
|
|
62
|
+
# Browse with gallery (thumbnail grid with filters)
|
|
63
|
+
schenesort gallery
|
|
64
|
+
|
|
65
|
+
# Browse single images with TUI
|
|
66
|
+
schenesort browse ~/wallpapers
|
|
67
|
+
|
|
60
68
|
# Query wallpapers
|
|
61
69
|
schenesort get --mood peaceful --screen 4K
|
|
62
70
|
schenesort get -1 -p | xargs feh # random wallpaper
|
|
@@ -67,6 +75,8 @@ schenesort get -1 -p | xargs feh # random wallpaper
|
|
|
67
75
|
| Command | Description |
|
|
68
76
|
|------------------------------|-----------------------------------------------------|
|
|
69
77
|
| `browse` | Terminal UI browser with image preview and metadata |
|
|
78
|
+
| `gallery` | Thumbnail grid browser with filters |
|
|
79
|
+
| `thumbnail` | Generate thumbnail cache for gallery |
|
|
70
80
|
| `index` | Build SQLite index for fast querying |
|
|
71
81
|
| `get` | Query wallpapers by metadata attributes |
|
|
72
82
|
| `stats` | Show collection statistics from index |
|
|
@@ -100,6 +110,7 @@ schenesort get --mood peaceful -b # browse query results
|
|
|
100
110
|

|
|
101
111
|
|
|
102
112
|
**Keyboard shortcuts:**
|
|
113
|
+
|
|
103
114
|
| Key | Action |
|
|
104
115
|
|--------------|----------------|
|
|
105
116
|
| `j` / `Down` | Next image |
|
|
@@ -111,6 +122,63 @@ schenesort get --mood peaceful -b # browse query results
|
|
|
111
122
|
|
|
112
123
|
The TUI uses textual-image for rendering, which auto-detects terminal graphics support (Sixel, iTerm2, Kitty).
|
|
113
124
|
|
|
125
|
+
## Gallery Browser
|
|
126
|
+
|
|
127
|
+
Browse your indexed collection with a thumbnail grid and filter panel:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Open gallery browser
|
|
131
|
+
schenesort gallery
|
|
132
|
+
|
|
133
|
+
# Pre-filter results
|
|
134
|
+
schenesort gallery --mood peaceful
|
|
135
|
+
schenesort gallery --tag nature --style photography
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The gallery requires an indexed collection (`schenesort index`) and cached thumbnails for fast loading.
|
|
139
|
+
|
|
140
|
+
### Generate Thumbnails
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Generate thumbnails for a directory
|
|
144
|
+
schenesort thumbnail ~/wallpapers
|
|
145
|
+
|
|
146
|
+
# Recursive with progress bar
|
|
147
|
+
schenesort thumbnail ~/wallpapers -r
|
|
148
|
+
|
|
149
|
+
# Force regenerate all thumbnails
|
|
150
|
+
schenesort thumbnail ~/wallpapers --force
|
|
151
|
+
|
|
152
|
+
# Clear thumbnail cache
|
|
153
|
+
schenesort thumbnail ~/wallpapers --clear
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Thumbnails are cached at `~/.cache/schenesort/thumbnails/` (320x200 JPEG).
|
|
157
|
+
|
|
158
|
+
**Gallery keyboard shortcuts:**
|
|
159
|
+
|
|
160
|
+
| Key | Action |
|
|
161
|
+
|------------------|-----------------------|
|
|
162
|
+
| `j` / `Down` | Move down |
|
|
163
|
+
| `k` / `Up` | Move up |
|
|
164
|
+
| `h` / `Left` | Move left |
|
|
165
|
+
| `l` / `Right` | Move right |
|
|
166
|
+
| `g` / `Home` | First image |
|
|
167
|
+
| `G` / `End` | Last image |
|
|
168
|
+
| `Enter` | Open detail view |
|
|
169
|
+
| `Tab` | Switch panel |
|
|
170
|
+
| `Escape` | Clear filters |
|
|
171
|
+
| `r` | Refresh |
|
|
172
|
+
| `q` / `Ctrl+C` | Quit |
|
|
173
|
+
|
|
174
|
+
**Detail view shortcuts:**
|
|
175
|
+
|
|
176
|
+
| Key | Action |
|
|
177
|
+
|------------------|-----------------------|
|
|
178
|
+
| `j` / `Down` | Next image |
|
|
179
|
+
| `k` / `Up` | Previous image |
|
|
180
|
+
| `Escape` / `q` | Back to grid |
|
|
181
|
+
|
|
114
182
|
## Collection Indexing and Querying
|
|
115
183
|
|
|
116
184
|
Build a SQLite index for fast querying across your entire collection:
|
|
@@ -167,7 +235,9 @@ schenesort collage collage.png --tile-width 640 --tile-height 360
|
|
|
167
235
|
schenesort collage night_cities.png --time night --subject urban --cols 3 --rows 2
|
|
168
236
|
```
|
|
169
237
|
|
|
170
|
-
|
|
238
|
+
`schenesort collage landscape.png --cols 4 --rows 4 --tile-width 640 --tile-height 360 --tag landscape`
|
|
239
|
+
|
|
240
|
+

|
|
171
241
|
|
|
172
242
|
| Option | Description | Default |
|
|
173
243
|
|-----------------|--------------------------------------|---------|
|
|
@@ -277,6 +347,7 @@ schenesort cleanup ~/wallpapers -r
|
|
|
277
347
|
## Configuration
|
|
278
348
|
|
|
279
349
|
Schenesort follows XDG Base Directory spec:
|
|
350
|
+
|
|
280
351
|
- Config: `$XDG_CONFIG_HOME/schenesort/config.toml` (default: `~/.config/schenesort/config.toml`)
|
|
281
352
|
- Data: `$XDG_DATA_HOME/schenesort/index.db` (default: `~/.local/share/schenesort/index.db`)
|
|
282
353
|
|
|
@@ -354,7 +425,7 @@ See [schenesort.yazi/README.md](schenesort.yazi/README.md) for details.
|
|
|
354
425
|
|
|
355
426
|
## XMP Sidecar Format
|
|
356
427
|
|
|
357
|
-
```
|
|
428
|
+
```text
|
|
358
429
|
~/wallpapers/
|
|
359
430
|
├── mountain_sunset.jpg
|
|
360
431
|
├── mountain_sunset.jpg.xmp ← metadata stored here
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
# Schenesort v2.
|
|
1
|
+
# Schenesort v2.4.0
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
A cli tool for managing wallpaper collections with model generated metadata, sweet tui, and sql metadata querying.
|
|
6
|
+
|
|
7
|
+
schenesort takes a directory of random wallpapers, with random filenames, and uses olama with a decent vision model to:
|
|
5
8
|
|
|
6
|
-
schenesort takes a directory of random wallpapers, with random filenames, and uses olama with a decent vision model to
|
|
7
9
|
- look at each wallpaper
|
|
8
10
|
- rename the wallpaper to something sensible
|
|
9
11
|
- drop a XMP sidecar with metadata about the file
|
|
@@ -37,12 +39,18 @@ setup](./ollama-setup.md).
|
|
|
37
39
|
# Generate metadata for images
|
|
38
40
|
schenesort metadata generate ~/wallpapers -r
|
|
39
41
|
|
|
40
|
-
# Browse with TUI
|
|
41
|
-
schenesort browse ~/wallpapers
|
|
42
|
-
|
|
43
42
|
# Index the collection
|
|
44
43
|
schenesort index ~/wallpapers
|
|
45
44
|
|
|
45
|
+
# Generate thumbnails for gallery
|
|
46
|
+
schenesort thumbnail ~/wallpapers -r
|
|
47
|
+
|
|
48
|
+
# Browse with gallery (thumbnail grid with filters)
|
|
49
|
+
schenesort gallery
|
|
50
|
+
|
|
51
|
+
# Browse single images with TUI
|
|
52
|
+
schenesort browse ~/wallpapers
|
|
53
|
+
|
|
46
54
|
# Query wallpapers
|
|
47
55
|
schenesort get --mood peaceful --screen 4K
|
|
48
56
|
schenesort get -1 -p | xargs feh # random wallpaper
|
|
@@ -53,6 +61,8 @@ schenesort get -1 -p | xargs feh # random wallpaper
|
|
|
53
61
|
| Command | Description |
|
|
54
62
|
|------------------------------|-----------------------------------------------------|
|
|
55
63
|
| `browse` | Terminal UI browser with image preview and metadata |
|
|
64
|
+
| `gallery` | Thumbnail grid browser with filters |
|
|
65
|
+
| `thumbnail` | Generate thumbnail cache for gallery |
|
|
56
66
|
| `index` | Build SQLite index for fast querying |
|
|
57
67
|
| `get` | Query wallpapers by metadata attributes |
|
|
58
68
|
| `stats` | Show collection statistics from index |
|
|
@@ -86,6 +96,7 @@ schenesort get --mood peaceful -b # browse query results
|
|
|
86
96
|

|
|
87
97
|
|
|
88
98
|
**Keyboard shortcuts:**
|
|
99
|
+
|
|
89
100
|
| Key | Action |
|
|
90
101
|
|--------------|----------------|
|
|
91
102
|
| `j` / `Down` | Next image |
|
|
@@ -97,6 +108,63 @@ schenesort get --mood peaceful -b # browse query results
|
|
|
97
108
|
|
|
98
109
|
The TUI uses textual-image for rendering, which auto-detects terminal graphics support (Sixel, iTerm2, Kitty).
|
|
99
110
|
|
|
111
|
+
## Gallery Browser
|
|
112
|
+
|
|
113
|
+
Browse your indexed collection with a thumbnail grid and filter panel:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Open gallery browser
|
|
117
|
+
schenesort gallery
|
|
118
|
+
|
|
119
|
+
# Pre-filter results
|
|
120
|
+
schenesort gallery --mood peaceful
|
|
121
|
+
schenesort gallery --tag nature --style photography
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The gallery requires an indexed collection (`schenesort index`) and cached thumbnails for fast loading.
|
|
125
|
+
|
|
126
|
+
### Generate Thumbnails
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Generate thumbnails for a directory
|
|
130
|
+
schenesort thumbnail ~/wallpapers
|
|
131
|
+
|
|
132
|
+
# Recursive with progress bar
|
|
133
|
+
schenesort thumbnail ~/wallpapers -r
|
|
134
|
+
|
|
135
|
+
# Force regenerate all thumbnails
|
|
136
|
+
schenesort thumbnail ~/wallpapers --force
|
|
137
|
+
|
|
138
|
+
# Clear thumbnail cache
|
|
139
|
+
schenesort thumbnail ~/wallpapers --clear
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Thumbnails are cached at `~/.cache/schenesort/thumbnails/` (320x200 JPEG).
|
|
143
|
+
|
|
144
|
+
**Gallery keyboard shortcuts:**
|
|
145
|
+
|
|
146
|
+
| Key | Action |
|
|
147
|
+
|------------------|-----------------------|
|
|
148
|
+
| `j` / `Down` | Move down |
|
|
149
|
+
| `k` / `Up` | Move up |
|
|
150
|
+
| `h` / `Left` | Move left |
|
|
151
|
+
| `l` / `Right` | Move right |
|
|
152
|
+
| `g` / `Home` | First image |
|
|
153
|
+
| `G` / `End` | Last image |
|
|
154
|
+
| `Enter` | Open detail view |
|
|
155
|
+
| `Tab` | Switch panel |
|
|
156
|
+
| `Escape` | Clear filters |
|
|
157
|
+
| `r` | Refresh |
|
|
158
|
+
| `q` / `Ctrl+C` | Quit |
|
|
159
|
+
|
|
160
|
+
**Detail view shortcuts:**
|
|
161
|
+
|
|
162
|
+
| Key | Action |
|
|
163
|
+
|------------------|-----------------------|
|
|
164
|
+
| `j` / `Down` | Next image |
|
|
165
|
+
| `k` / `Up` | Previous image |
|
|
166
|
+
| `Escape` / `q` | Back to grid |
|
|
167
|
+
|
|
100
168
|
## Collection Indexing and Querying
|
|
101
169
|
|
|
102
170
|
Build a SQLite index for fast querying across your entire collection:
|
|
@@ -153,7 +221,9 @@ schenesort collage collage.png --tile-width 640 --tile-height 360
|
|
|
153
221
|
schenesort collage night_cities.png --time night --subject urban --cols 3 --rows 2
|
|
154
222
|
```
|
|
155
223
|
|
|
156
|
-
|
|
224
|
+
`schenesort collage landscape.png --cols 4 --rows 4 --tile-width 640 --tile-height 360 --tag landscape`
|
|
225
|
+
|
|
226
|
+

|
|
157
227
|
|
|
158
228
|
| Option | Description | Default |
|
|
159
229
|
|-----------------|--------------------------------------|---------|
|
|
@@ -263,6 +333,7 @@ schenesort cleanup ~/wallpapers -r
|
|
|
263
333
|
## Configuration
|
|
264
334
|
|
|
265
335
|
Schenesort follows XDG Base Directory spec:
|
|
336
|
+
|
|
266
337
|
- Config: `$XDG_CONFIG_HOME/schenesort/config.toml` (default: `~/.config/schenesort/config.toml`)
|
|
267
338
|
- Data: `$XDG_DATA_HOME/schenesort/index.db` (default: `~/.local/share/schenesort/index.db`)
|
|
268
339
|
|
|
@@ -340,7 +411,7 @@ See [schenesort.yazi/README.md](schenesort.yazi/README.md) for details.
|
|
|
340
411
|
|
|
341
412
|
## XMP Sidecar Format
|
|
342
413
|
|
|
343
|
-
```
|
|
414
|
+
```text
|
|
344
415
|
~/wallpapers/
|
|
345
416
|
├── mountain_sunset.jpg
|
|
346
417
|
├── mountain_sunset.jpg.xmp ← metadata stored here
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "schenesort"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.4.0"
|
|
4
4
|
description = "Wallpaper collection management CLI tool"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -56,7 +56,7 @@ testpaths = ["tests"]
|
|
|
56
56
|
pythonpath = ["src"]
|
|
57
57
|
|
|
58
58
|
[tool.bumpversion]
|
|
59
|
-
current_version = "2.
|
|
59
|
+
current_version = "2.4.0"
|
|
60
60
|
commit = true
|
|
61
61
|
tag = true
|
|
62
62
|
tag_name = "v{new_version}"
|
|
@@ -412,6 +412,85 @@ def index(
|
|
|
412
412
|
typer.echo(f"With metadata: {stats.get('with_metadata', 0)}")
|
|
413
413
|
|
|
414
414
|
|
|
415
|
+
@app.command()
|
|
416
|
+
def thumbnail(
|
|
417
|
+
path: Annotated[Path, typer.Argument(help="Directory to generate thumbnails for")],
|
|
418
|
+
recursive: Annotated[
|
|
419
|
+
bool, typer.Option("--recursive", "-r", help="Process directories recursively")
|
|
420
|
+
] = True,
|
|
421
|
+
force: Annotated[bool, typer.Option("--force", "-f", help="Regenerate all thumbnails")] = False,
|
|
422
|
+
clear: Annotated[
|
|
423
|
+
bool, typer.Option("--clear", help="Clear thumbnail cache before generating")
|
|
424
|
+
] = False,
|
|
425
|
+
) -> None:
|
|
426
|
+
"""Generate thumbnails for gallery view."""
|
|
427
|
+
from rich.progress import BarColumn, Progress, TaskProgressColumn, TextColumn
|
|
428
|
+
|
|
429
|
+
from schenesort.thumbnails import (
|
|
430
|
+
clear_cache,
|
|
431
|
+
generate_thumbnail,
|
|
432
|
+
get_cache_stats,
|
|
433
|
+
thumbnail_exists,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
path = path.resolve()
|
|
437
|
+
|
|
438
|
+
if not path.exists():
|
|
439
|
+
typer.echo(f"Error: Path '{path}' does not exist.", err=True)
|
|
440
|
+
raise typer.Exit(1)
|
|
441
|
+
|
|
442
|
+
if not path.is_dir():
|
|
443
|
+
typer.echo(f"Error: Path '{path}' is not a directory.", err=True)
|
|
444
|
+
raise typer.Exit(1)
|
|
445
|
+
|
|
446
|
+
if clear:
|
|
447
|
+
cleared = clear_cache()
|
|
448
|
+
typer.echo(f"Cleared {cleared} cached thumbnail(s).")
|
|
449
|
+
|
|
450
|
+
pattern = "**/*" if recursive else "*"
|
|
451
|
+
image_files = [
|
|
452
|
+
f for f in path.glob(pattern) if f.is_file() and f.suffix.lower() in VALID_IMAGE_EXTENSIONS
|
|
453
|
+
]
|
|
454
|
+
|
|
455
|
+
if not image_files:
|
|
456
|
+
typer.echo("No image files found.")
|
|
457
|
+
raise typer.Exit(0)
|
|
458
|
+
|
|
459
|
+
generated = 0
|
|
460
|
+
skipped = 0
|
|
461
|
+
failed = 0
|
|
462
|
+
|
|
463
|
+
with Progress(
|
|
464
|
+
TextColumn("[progress.description]{task.description}"),
|
|
465
|
+
BarColumn(),
|
|
466
|
+
TaskProgressColumn(),
|
|
467
|
+
TextColumn("{task.completed}/{task.total}"),
|
|
468
|
+
) as progress:
|
|
469
|
+
task = progress.add_task("Generating thumbnails", total=len(image_files))
|
|
470
|
+
|
|
471
|
+
for filepath in image_files:
|
|
472
|
+
if not force and thumbnail_exists(filepath):
|
|
473
|
+
skipped += 1
|
|
474
|
+
progress.advance(task)
|
|
475
|
+
continue
|
|
476
|
+
|
|
477
|
+
result = generate_thumbnail(filepath, force=force)
|
|
478
|
+
if result:
|
|
479
|
+
generated += 1
|
|
480
|
+
else:
|
|
481
|
+
failed += 1
|
|
482
|
+
|
|
483
|
+
progress.advance(task)
|
|
484
|
+
|
|
485
|
+
typer.echo(f"\nGenerated: {generated}, Skipped: {skipped}, Failed: {failed}")
|
|
486
|
+
|
|
487
|
+
# Show cache stats
|
|
488
|
+
stats = get_cache_stats()
|
|
489
|
+
typer.echo(f"\nCache: {stats['path']}")
|
|
490
|
+
typer.echo(f"Total thumbnails: {stats['count']}")
|
|
491
|
+
typer.echo(f"Cache size: {stats['size_mb']:.1f} MB")
|
|
492
|
+
|
|
493
|
+
|
|
415
494
|
@app.command()
|
|
416
495
|
def get(
|
|
417
496
|
tag: Annotated[str | None, typer.Option("--tag", "-t", help="Filter by tag")] = None,
|
|
@@ -581,6 +660,48 @@ def browse(
|
|
|
581
660
|
app_instance.run()
|
|
582
661
|
|
|
583
662
|
|
|
663
|
+
@app.command()
|
|
664
|
+
def gallery(
|
|
665
|
+
tag: Annotated[str | None, typer.Option("--tag", "-t", help="Filter by tag")] = None,
|
|
666
|
+
mood: Annotated[str | None, typer.Option("--mood", "-m", help="Filter by mood")] = None,
|
|
667
|
+
color: Annotated[str | None, typer.Option("--color", "-c", help="Filter by color")] = None,
|
|
668
|
+
style: Annotated[str | None, typer.Option("--style", "-s", help="Filter by style")] = None,
|
|
669
|
+
subject: Annotated[str | None, typer.Option("--subject", help="Filter by subject")] = None,
|
|
670
|
+
time: Annotated[str | None, typer.Option("--time", help="Filter by time of day")] = None,
|
|
671
|
+
screen: Annotated[
|
|
672
|
+
str | None, typer.Option("--screen", help="Filter by recommended screen (4K, 1440p, etc)")
|
|
673
|
+
] = None,
|
|
674
|
+
min_width: Annotated[
|
|
675
|
+
int | None, typer.Option("--min-width", help="Minimum width in pixels")
|
|
676
|
+
] = None,
|
|
677
|
+
min_height: Annotated[
|
|
678
|
+
int | None, typer.Option("--min-height", help="Minimum height in pixels")
|
|
679
|
+
] = None,
|
|
680
|
+
search: Annotated[
|
|
681
|
+
str | None, typer.Option("--search", "-q", help="Search description, scene, style, subject")
|
|
682
|
+
] = None,
|
|
683
|
+
) -> None:
|
|
684
|
+
"""Browse wallpapers in a thumbnail grid with filter sidebar."""
|
|
685
|
+
from schenesort.tui.grid_app import GridBrowser
|
|
686
|
+
from schenesort.tui.widgets.filter_panel import FilterValues
|
|
687
|
+
|
|
688
|
+
initial_filters = FilterValues(
|
|
689
|
+
search=search or "",
|
|
690
|
+
tag=tag or "",
|
|
691
|
+
mood=mood or "",
|
|
692
|
+
color=color or "",
|
|
693
|
+
style=style or "",
|
|
694
|
+
subject=subject or "",
|
|
695
|
+
time=time or "",
|
|
696
|
+
screen=screen or "",
|
|
697
|
+
min_width=min_width,
|
|
698
|
+
min_height=min_height,
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
app_instance = GridBrowser(initial_filters=initial_filters)
|
|
702
|
+
app_instance.run()
|
|
703
|
+
|
|
704
|
+
|
|
584
705
|
@app.command()
|
|
585
706
|
def config(
|
|
586
707
|
create: Annotated[
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Thumbnail cache management for gallery view."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from PIL import Image
|
|
8
|
+
|
|
9
|
+
APP_NAME = "schenesort"
|
|
10
|
+
|
|
11
|
+
# Thumbnail dimensions - larger for better quality when rendered in terminal
|
|
12
|
+
# Terminal cells are ~32x14 chars, but textual-image benefits from more source pixels
|
|
13
|
+
THUMBNAIL_WIDTH = 320
|
|
14
|
+
THUMBNAIL_HEIGHT = 200
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_cache_dir() -> Path:
|
|
18
|
+
"""Get the XDG cache directory for schenesort thumbnails."""
|
|
19
|
+
xdg_cache = os.environ.get("XDG_CACHE_HOME", "")
|
|
20
|
+
if xdg_cache:
|
|
21
|
+
cache_dir = Path(xdg_cache) / APP_NAME / "thumbnails"
|
|
22
|
+
else:
|
|
23
|
+
cache_dir = Path.home() / ".cache" / APP_NAME / "thumbnails"
|
|
24
|
+
return cache_dir
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_thumbnail_path(image_path: Path) -> Path:
|
|
28
|
+
"""Get the thumbnail path for an image.
|
|
29
|
+
|
|
30
|
+
Uses MD5 hash of the absolute path to create a unique filename.
|
|
31
|
+
"""
|
|
32
|
+
# Use absolute path for consistent hashing
|
|
33
|
+
abs_path = str(image_path.resolve())
|
|
34
|
+
path_hash = hashlib.md5(abs_path.encode()).hexdigest()
|
|
35
|
+
return get_cache_dir() / f"{path_hash}.jpg"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def thumbnail_exists(image_path: Path) -> bool:
|
|
39
|
+
"""Check if a thumbnail exists for the given image."""
|
|
40
|
+
thumb_path = get_thumbnail_path(image_path)
|
|
41
|
+
if not thumb_path.exists():
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
# Check if thumbnail is newer than original
|
|
45
|
+
try:
|
|
46
|
+
orig_mtime = image_path.stat().st_mtime
|
|
47
|
+
thumb_mtime = thumb_path.stat().st_mtime
|
|
48
|
+
return thumb_mtime >= orig_mtime
|
|
49
|
+
except OSError:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def generate_thumbnail(image_path: Path, force: bool = False) -> Path | None:
|
|
54
|
+
"""Generate a thumbnail for the given image.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
image_path: Path to the original image
|
|
58
|
+
force: If True, regenerate even if thumbnail exists
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Path to the thumbnail, or None if generation failed
|
|
62
|
+
"""
|
|
63
|
+
if not force and thumbnail_exists(image_path):
|
|
64
|
+
return get_thumbnail_path(image_path)
|
|
65
|
+
|
|
66
|
+
thumb_path = get_thumbnail_path(image_path)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# Ensure cache directory exists
|
|
70
|
+
thumb_path.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
|
|
72
|
+
with Image.open(image_path) as img:
|
|
73
|
+
# Convert to RGB if necessary (handles RGBA, palette, etc.)
|
|
74
|
+
if img.mode not in ("RGB", "L"):
|
|
75
|
+
img = img.convert("RGB")
|
|
76
|
+
|
|
77
|
+
# Calculate size preserving aspect ratio
|
|
78
|
+
img.thumbnail((THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT), Image.Resampling.LANCZOS)
|
|
79
|
+
|
|
80
|
+
# Save as JPEG with high quality for better terminal rendering
|
|
81
|
+
img.save(thumb_path, "JPEG", quality=95, optimize=True)
|
|
82
|
+
|
|
83
|
+
return thumb_path
|
|
84
|
+
|
|
85
|
+
except Exception:
|
|
86
|
+
# Clean up partial file if it exists
|
|
87
|
+
if thumb_path.exists():
|
|
88
|
+
thumb_path.unlink()
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def clear_cache() -> int:
|
|
93
|
+
"""Clear all cached thumbnails.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Number of thumbnails deleted
|
|
97
|
+
"""
|
|
98
|
+
cache_dir = get_cache_dir()
|
|
99
|
+
if not cache_dir.exists():
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
count = 0
|
|
103
|
+
for thumb in cache_dir.glob("*.jpg"):
|
|
104
|
+
try:
|
|
105
|
+
thumb.unlink()
|
|
106
|
+
count += 1
|
|
107
|
+
except OSError:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
return count
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_cache_stats() -> dict:
|
|
114
|
+
"""Get statistics about the thumbnail cache."""
|
|
115
|
+
cache_dir = get_cache_dir()
|
|
116
|
+
if not cache_dir.exists():
|
|
117
|
+
return {"count": 0, "size_bytes": 0, "path": str(cache_dir)}
|
|
118
|
+
|
|
119
|
+
thumbnails = list(cache_dir.glob("*.jpg"))
|
|
120
|
+
total_size = sum(t.stat().st_size for t in thumbnails)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"count": len(thumbnails),
|
|
124
|
+
"size_bytes": total_size,
|
|
125
|
+
"size_mb": total_size / (1024 * 1024),
|
|
126
|
+
"path": str(cache_dir),
|
|
127
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Schenesort TUI - Terminal UI for browsing wallpapers."""
|
|
2
|
+
|
|
3
|
+
from schenesort.tui.app import WallpaperBrowser
|
|
4
|
+
from schenesort.tui.grid_app import GridBrowser
|
|
5
|
+
from schenesort.tui.widgets.filter_panel import FilterPanel, FilterValues
|
|
6
|
+
from schenesort.tui.widgets.thumbnail_grid import ThumbnailGrid, ThumbnailText
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"FilterPanel",
|
|
10
|
+
"FilterValues",
|
|
11
|
+
"GridBrowser",
|
|
12
|
+
"ThumbnailGrid",
|
|
13
|
+
"ThumbnailText",
|
|
14
|
+
"WallpaperBrowser",
|
|
15
|
+
]
|