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.
Files changed (42) hide show
  1. {schenesort-2.3.0 → schenesort-2.4.0}/PKG-INFO +81 -10
  2. {schenesort-2.3.0 → schenesort-2.4.0}/README.md +80 -9
  3. schenesort-2.4.0/docs/gallery.png +0 -0
  4. schenesort-2.4.0/docs/landscape.png +0 -0
  5. {schenesort-2.3.0 → schenesort-2.4.0}/pyproject.toml +2 -2
  6. {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/cli.py +121 -0
  7. schenesort-2.4.0/src/schenesort/thumbnails.py +127 -0
  8. schenesort-2.4.0/src/schenesort/tui/__init__.py +15 -0
  9. schenesort-2.4.0/src/schenesort/tui/grid_app.py +302 -0
  10. schenesort-2.4.0/src/schenesort/tui/widgets/filter_panel.py +249 -0
  11. schenesort-2.4.0/src/schenesort/tui/widgets/thumbnail_grid.py +291 -0
  12. {schenesort-2.3.0 → schenesort-2.4.0}/uv.lock +1 -1
  13. schenesort-2.3.0/src/schenesort/tui/__init__.py +0 -5
  14. {schenesort-2.3.0 → schenesort-2.4.0}/.envrc +0 -0
  15. {schenesort-2.3.0 → schenesort-2.4.0}/.github/workflows/ci.yml +0 -0
  16. {schenesort-2.3.0 → schenesort-2.4.0}/.github/workflows/publish.yml +0 -0
  17. {schenesort-2.3.0 → schenesort-2.4.0}/.gitignore +0 -0
  18. {schenesort-2.3.0 → schenesort-2.4.0}/.pre-commit-config.yaml +0 -0
  19. {schenesort-2.3.0 → schenesort-2.4.0}/.python-version +0 -0
  20. {schenesort-2.3.0 → schenesort-2.4.0}/CLAUDE.md +0 -0
  21. {schenesort-2.3.0 → schenesort-2.4.0}/LICENSE +0 -0
  22. {schenesort-2.3.0 → schenesort-2.4.0}/docs/browse-autumn.png +0 -0
  23. {schenesort-2.3.0 → schenesort-2.4.0}/docs/browse-destruction.png +0 -0
  24. {schenesort-2.3.0 → schenesort-2.4.0}/docs/browse-greek.png +0 -0
  25. {schenesort-2.3.0 → schenesort-2.4.0}/docs/browse-stallman.png +0 -0
  26. {schenesort-2.3.0 → schenesort-2.4.0}/docs/collage.png +0 -0
  27. {schenesort-2.3.0 → schenesort-2.4.0}/justfile +0 -0
  28. {schenesort-2.3.0 → schenesort-2.4.0}/ollama-setup.md +0 -0
  29. {schenesort-2.3.0 → schenesort-2.4.0}/schenesort.yazi/README.md +0 -0
  30. {schenesort-2.3.0 → schenesort-2.4.0}/schenesort.yazi/main.lua +0 -0
  31. {schenesort-2.3.0 → schenesort-2.4.0}/schenesort.yazi/schenesort.config +0 -0
  32. {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/__init__.py +0 -0
  33. {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/config.py +0 -0
  34. {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/db.py +0 -0
  35. {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/tui/app.py +0 -0
  36. {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/tui/widgets/__init__.py +0 -0
  37. {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/tui/widgets/image_preview.py +0 -0
  38. {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/tui/widgets/metadata_panel.py +0 -0
  39. {schenesort-2.3.0 → schenesort-2.4.0}/src/schenesort/xmp.py +0 -0
  40. {schenesort-2.3.0 → schenesort-2.4.0}/tests/__init__.py +0 -0
  41. {schenesort-2.3.0 → schenesort-2.4.0}/tests/test_cli.py +0 -0
  42. {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.0
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.3.0
15
+ # Schenesort v2.4.0
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,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
- ![Collage example](docs/collage.png)
238
+ `schenesort collage landscape.png --cols 4 --rows 4 --tile-width 640 --tile-height 360 --tag landscape`
239
+
240
+ ![Collage example](docs/landscape.png)
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.3.0
1
+ # Schenesort v2.4.0
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,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
- ![Collage example](docs/collage.png)
224
+ `schenesort collage landscape.png --cols 4 --rows 4 --tile-width 640 --tile-height 360 --tag landscape`
225
+
226
+ ![Collage example](docs/landscape.png)
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.0"
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.3.0"
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
+ ]