schenesort 2.3.0__tar.gz → 2.4.1__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.
Files changed (42) hide show
  1. {schenesort-2.3.0 → schenesort-2.4.1}/PKG-INFO +87 -14
  2. {schenesort-2.3.0 → schenesort-2.4.1}/README.md +86 -13
  3. schenesort-2.4.1/docs/gallery.png +0 -0
  4. schenesort-2.4.1/docs/landscape.png +0 -0
  5. {schenesort-2.3.0 → schenesort-2.4.1}/pyproject.toml +2 -2
  6. {schenesort-2.3.0 → schenesort-2.4.1}/src/schenesort/cli.py +128 -7
  7. schenesort-2.4.1/src/schenesort/thumbnails.py +127 -0
  8. schenesort-2.4.1/src/schenesort/tui/__init__.py +15 -0
  9. schenesort-2.4.1/src/schenesort/tui/grid_app.py +302 -0
  10. schenesort-2.4.1/src/schenesort/tui/widgets/filter_panel.py +249 -0
  11. schenesort-2.4.1/src/schenesort/tui/widgets/thumbnail_grid.py +291 -0
  12. {schenesort-2.3.0 → schenesort-2.4.1}/uv.lock +1 -1
  13. schenesort-2.3.0/src/schenesort/tui/__init__.py +0 -5
  14. {schenesort-2.3.0 → schenesort-2.4.1}/.envrc +0 -0
  15. {schenesort-2.3.0 → schenesort-2.4.1}/.github/workflows/ci.yml +0 -0
  16. {schenesort-2.3.0 → schenesort-2.4.1}/.github/workflows/publish.yml +0 -0
  17. {schenesort-2.3.0 → schenesort-2.4.1}/.gitignore +0 -0
  18. {schenesort-2.3.0 → schenesort-2.4.1}/.pre-commit-config.yaml +0 -0
  19. {schenesort-2.3.0 → schenesort-2.4.1}/.python-version +0 -0
  20. {schenesort-2.3.0 → schenesort-2.4.1}/CLAUDE.md +0 -0
  21. {schenesort-2.3.0 → schenesort-2.4.1}/LICENSE +0 -0
  22. {schenesort-2.3.0 → schenesort-2.4.1}/docs/browse-autumn.png +0 -0
  23. {schenesort-2.3.0 → schenesort-2.4.1}/docs/browse-destruction.png +0 -0
  24. {schenesort-2.3.0 → schenesort-2.4.1}/docs/browse-greek.png +0 -0
  25. {schenesort-2.3.0 → schenesort-2.4.1}/docs/browse-stallman.png +0 -0
  26. {schenesort-2.3.0 → schenesort-2.4.1}/docs/collage.png +0 -0
  27. {schenesort-2.3.0 → schenesort-2.4.1}/justfile +0 -0
  28. {schenesort-2.3.0 → schenesort-2.4.1}/ollama-setup.md +0 -0
  29. {schenesort-2.3.0 → schenesort-2.4.1}/schenesort.yazi/README.md +0 -0
  30. {schenesort-2.3.0 → schenesort-2.4.1}/schenesort.yazi/main.lua +0 -0
  31. {schenesort-2.3.0 → schenesort-2.4.1}/schenesort.yazi/schenesort.config +0 -0
  32. {schenesort-2.3.0 → schenesort-2.4.1}/src/schenesort/__init__.py +0 -0
  33. {schenesort-2.3.0 → schenesort-2.4.1}/src/schenesort/config.py +0 -0
  34. {schenesort-2.3.0 → schenesort-2.4.1}/src/schenesort/db.py +0 -0
  35. {schenesort-2.3.0 → schenesort-2.4.1}/src/schenesort/tui/app.py +0 -0
  36. {schenesort-2.3.0 → schenesort-2.4.1}/src/schenesort/tui/widgets/__init__.py +0 -0
  37. {schenesort-2.3.0 → schenesort-2.4.1}/src/schenesort/tui/widgets/image_preview.py +0 -0
  38. {schenesort-2.3.0 → schenesort-2.4.1}/src/schenesort/tui/widgets/metadata_panel.py +0 -0
  39. {schenesort-2.3.0 → schenesort-2.4.1}/src/schenesort/xmp.py +0 -0
  40. {schenesort-2.3.0 → schenesort-2.4.1}/tests/__init__.py +0 -0
  41. {schenesort-2.3.0 → schenesort-2.4.1}/tests/test_cli.py +0 -0
  42. {schenesort-2.3.0 → schenesort-2.4.1}/tests/test_sanitise.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schenesort
3
- Version: 2.3.0
3
+ Version: 2.4.1
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.3.0
15
+ # Schenesort v2.4.1
16
16
 
17
- A CLI tool for managing wallpaper collections with model generated metadata, terminal UI browsing, and SQLite-based
18
- querying.
17
+ ![Collage example](docs/collage.png)
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
  ![Browse example - Stallman](docs/browse-stallman.png)
101
111
 
102
112
  **Keyboard shortcuts:**
113
+
103
114
  | Key | Action |
104
115
  |--------------|----------------|
105
116
  | `j` / `Down` | Next image |
@@ -111,6 +122,65 @@ 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
+ ![Gallery example](docs/gallery.png)
128
+
129
+ Browse your indexed collection with a thumbnail grid and filter panel:
130
+
131
+ ```bash
132
+ # Open gallery browser
133
+ schenesort gallery
134
+
135
+ # Pre-filter results
136
+ schenesort gallery --mood peaceful
137
+ schenesort gallery --tag nature --style photography
138
+ ```
139
+
140
+ The gallery requires an indexed collection (`schenesort index`) and cached thumbnails for fast loading.
141
+
142
+ ### Generate Thumbnails
143
+
144
+ ```bash
145
+ # Generate thumbnails for a directory
146
+ schenesort thumbnail ~/wallpapers
147
+
148
+ # Recursive with progress bar
149
+ schenesort thumbnail ~/wallpapers -r
150
+
151
+ # Force regenerate all thumbnails
152
+ schenesort thumbnail ~/wallpapers --force
153
+
154
+ # Clear thumbnail cache
155
+ schenesort thumbnail ~/wallpapers --clear
156
+ ```
157
+
158
+ Thumbnails are cached at `~/.cache/schenesort/thumbnails/` (320x200 JPEG).
159
+
160
+ **Gallery keyboard shortcuts:**
161
+
162
+ | Key | Action |
163
+ |------------------|-----------------------|
164
+ | `j` / `Down` | Move down |
165
+ | `k` / `Up` | Move up |
166
+ | `h` / `Left` | Move left |
167
+ | `l` / `Right` | Move right |
168
+ | `g` / `Home` | First image |
169
+ | `G` / `End` | Last image |
170
+ | `Enter` | Open detail view |
171
+ | `Tab` | Switch panel |
172
+ | `Escape` | Clear filters |
173
+ | `r` | Refresh |
174
+ | `q` / `Ctrl+C` | Quit |
175
+
176
+ **Detail view shortcuts:**
177
+
178
+ | Key | Action |
179
+ |------------------|-----------------------|
180
+ | `j` / `Down` | Next image |
181
+ | `k` / `Up` | Previous image |
182
+ | `Escape` / `q` | Back to grid |
183
+
114
184
  ## Collection Indexing and Querying
115
185
 
116
186
  Build a SQLite index for fast querying across your entire collection:
@@ -151,10 +221,10 @@ The database is stored at `$XDG_DATA_HOME/schenesort/index.db` (default: `~/.loc
151
221
 
152
222
  ## Collage Generation
153
223
 
154
- Create a collage grid (up to 4x4) from wallpapers matching query criteria:
224
+ Create a collage grid from wallpapers matching query criteria:
155
225
 
156
226
  ```bash
157
- # Create a 2x2 collage (default)
227
+ # Create a 6x6 collage (default)
158
228
  schenesort collage output.png --mood peaceful
159
229
 
160
230
  # Create a 4x4 collage of landscape images
@@ -167,12 +237,14 @@ schenesort collage collage.png --tile-width 640 --tile-height 360
167
237
  schenesort collage night_cities.png --time night --subject urban --cols 3 --rows 2
168
238
  ```
169
239
 
170
- ![Collage example](docs/collage.png)
240
+ `schenesort collage landscape.png --cols 4 --rows 4 --tile-width 640 --tile-height 360 --tag landscape`
241
+
242
+ ![Collage example](docs/landscape.png)
171
243
 
172
244
  | Option | Description | Default |
173
245
  |-----------------|--------------------------------------|---------|
174
- | `--cols` | Number of columns (1-4) | 2 |
175
- | `--rows` | Number of rows (1-4) | 2 |
246
+ | `--cols` | Number of columns | 6 |
247
+ | `--rows` | Number of rows | 6 |
176
248
  | `--tile-width` | Width of each tile in pixels | 480 |
177
249
  | `--tile-height` | Height of each tile in pixels | 270 |
178
250
  | `--random` | Select images randomly | True |
@@ -277,6 +349,7 @@ schenesort cleanup ~/wallpapers -r
277
349
  ## Configuration
278
350
 
279
351
  Schenesort follows XDG Base Directory spec:
352
+
280
353
  - Config: `$XDG_CONFIG_HOME/schenesort/config.toml` (default: `~/.config/schenesort/config.toml`)
281
354
  - Data: `$XDG_DATA_HOME/schenesort/index.db` (default: `~/.local/share/schenesort/index.db`)
282
355
 
@@ -354,7 +427,7 @@ See [schenesort.yazi/README.md](schenesort.yazi/README.md) for details.
354
427
 
355
428
  ## XMP Sidecar Format
356
429
 
357
- ```
430
+ ```text
358
431
  ~/wallpapers/
359
432
  ├── mountain_sunset.jpg
360
433
  ├── mountain_sunset.jpg.xmp ← metadata stored here
@@ -1,9 +1,11 @@
1
- # Schenesort v2.3.0
1
+ # Schenesort v2.4.1
2
2
 
3
- A CLI tool for managing wallpaper collections with model generated metadata, terminal UI browsing, and SQLite-based
4
- querying.
3
+ ![Collage example](docs/collage.png)
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
  ![Browse example - Stallman](docs/browse-stallman.png)
87
97
 
88
98
  **Keyboard shortcuts:**
99
+
89
100
  | Key | Action |
90
101
  |--------------|----------------|
91
102
  | `j` / `Down` | Next image |
@@ -97,6 +108,65 @@ 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
+ ![Gallery example](docs/gallery.png)
114
+
115
+ Browse your indexed collection with a thumbnail grid and filter panel:
116
+
117
+ ```bash
118
+ # Open gallery browser
119
+ schenesort gallery
120
+
121
+ # Pre-filter results
122
+ schenesort gallery --mood peaceful
123
+ schenesort gallery --tag nature --style photography
124
+ ```
125
+
126
+ The gallery requires an indexed collection (`schenesort index`) and cached thumbnails for fast loading.
127
+
128
+ ### Generate Thumbnails
129
+
130
+ ```bash
131
+ # Generate thumbnails for a directory
132
+ schenesort thumbnail ~/wallpapers
133
+
134
+ # Recursive with progress bar
135
+ schenesort thumbnail ~/wallpapers -r
136
+
137
+ # Force regenerate all thumbnails
138
+ schenesort thumbnail ~/wallpapers --force
139
+
140
+ # Clear thumbnail cache
141
+ schenesort thumbnail ~/wallpapers --clear
142
+ ```
143
+
144
+ Thumbnails are cached at `~/.cache/schenesort/thumbnails/` (320x200 JPEG).
145
+
146
+ **Gallery keyboard shortcuts:**
147
+
148
+ | Key | Action |
149
+ |------------------|-----------------------|
150
+ | `j` / `Down` | Move down |
151
+ | `k` / `Up` | Move up |
152
+ | `h` / `Left` | Move left |
153
+ | `l` / `Right` | Move right |
154
+ | `g` / `Home` | First image |
155
+ | `G` / `End` | Last image |
156
+ | `Enter` | Open detail view |
157
+ | `Tab` | Switch panel |
158
+ | `Escape` | Clear filters |
159
+ | `r` | Refresh |
160
+ | `q` / `Ctrl+C` | Quit |
161
+
162
+ **Detail view shortcuts:**
163
+
164
+ | Key | Action |
165
+ |------------------|-----------------------|
166
+ | `j` / `Down` | Next image |
167
+ | `k` / `Up` | Previous image |
168
+ | `Escape` / `q` | Back to grid |
169
+
100
170
  ## Collection Indexing and Querying
101
171
 
102
172
  Build a SQLite index for fast querying across your entire collection:
@@ -137,10 +207,10 @@ The database is stored at `$XDG_DATA_HOME/schenesort/index.db` (default: `~/.loc
137
207
 
138
208
  ## Collage Generation
139
209
 
140
- Create a collage grid (up to 4x4) from wallpapers matching query criteria:
210
+ Create a collage grid from wallpapers matching query criteria:
141
211
 
142
212
  ```bash
143
- # Create a 2x2 collage (default)
213
+ # Create a 6x6 collage (default)
144
214
  schenesort collage output.png --mood peaceful
145
215
 
146
216
  # Create a 4x4 collage of landscape images
@@ -153,12 +223,14 @@ schenesort collage collage.png --tile-width 640 --tile-height 360
153
223
  schenesort collage night_cities.png --time night --subject urban --cols 3 --rows 2
154
224
  ```
155
225
 
156
- ![Collage example](docs/collage.png)
226
+ `schenesort collage landscape.png --cols 4 --rows 4 --tile-width 640 --tile-height 360 --tag landscape`
227
+
228
+ ![Collage example](docs/landscape.png)
157
229
 
158
230
  | Option | Description | Default |
159
231
  |-----------------|--------------------------------------|---------|
160
- | `--cols` | Number of columns (1-4) | 2 |
161
- | `--rows` | Number of rows (1-4) | 2 |
232
+ | `--cols` | Number of columns | 6 |
233
+ | `--rows` | Number of rows | 6 |
162
234
  | `--tile-width` | Width of each tile in pixels | 480 |
163
235
  | `--tile-height` | Height of each tile in pixels | 270 |
164
236
  | `--random` | Select images randomly | True |
@@ -263,6 +335,7 @@ schenesort cleanup ~/wallpapers -r
263
335
  ## Configuration
264
336
 
265
337
  Schenesort follows XDG Base Directory spec:
338
+
266
339
  - Config: `$XDG_CONFIG_HOME/schenesort/config.toml` (default: `~/.config/schenesort/config.toml`)
267
340
  - Data: `$XDG_DATA_HOME/schenesort/index.db` (default: `~/.local/share/schenesort/index.db`)
268
341
 
@@ -340,7 +413,7 @@ See [schenesort.yazi/README.md](schenesort.yazi/README.md) for details.
340
413
 
341
414
  ## XMP Sidecar Format
342
415
 
343
- ```
416
+ ```text
344
417
  ~/wallpapers/
345
418
  ├── mountain_sunset.jpg
346
419
  ├── 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.0"
3
+ version = "2.4.1"
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.3.0"
59
+ current_version = "2.4.1"
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[
@@ -1246,8 +1367,8 @@ def collage(
1246
1367
  search: Annotated[
1247
1368
  str | None, typer.Option("--search", "-q", help="Search description, scene, style, subject")
1248
1369
  ] = None,
1249
- cols: Annotated[int, typer.Option("--cols", help="Number of columns (1-4)")] = 2,
1250
- rows: Annotated[int, typer.Option("--rows", help="Number of rows (1-4)")] = 2,
1370
+ cols: Annotated[int, typer.Option("--cols", help="Number of columns")] = 6,
1371
+ rows: Annotated[int, typer.Option("--rows", help="Number of rows")] = 6,
1251
1372
  tile_width: Annotated[
1252
1373
  int, typer.Option("--tile-width", "-w", help="Width of each tile in pixels")
1253
1374
  ] = 480,
@@ -1256,17 +1377,17 @@ def collage(
1256
1377
  ] = 270,
1257
1378
  random: Annotated[bool, typer.Option("--random", "-R", help="Select images randomly")] = True,
1258
1379
  ) -> None:
1259
- """Create a collage of wallpapers matching the given criteria (max 4x4 grid)."""
1380
+ """Create a collage of wallpapers matching the given criteria."""
1260
1381
  from PIL import Image
1261
1382
 
1262
1383
  from schenesort.db import WallpaperDB
1263
1384
 
1264
1385
  # Validate grid size
1265
- if cols < 1 or cols > 4:
1266
- typer.echo("Error: --cols must be between 1 and 4.", err=True)
1386
+ if cols < 1:
1387
+ typer.echo("Error: --cols must be at least 1.", err=True)
1267
1388
  raise typer.Exit(1)
1268
- if rows < 1 or rows > 4:
1269
- typer.echo("Error: --rows must be between 1 and 4.", err=True)
1389
+ if rows < 1:
1390
+ typer.echo("Error: --rows must be at least 1.", err=True)
1270
1391
  raise typer.Exit(1)
1271
1392
 
1272
1393
  num_images = cols * rows
@@ -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
+ ]