schenesort 2.2.0__py3-none-any.whl → 2.4.0__py3-none-any.whl
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/cli.py +253 -0
- schenesort/thumbnails.py +127 -0
- schenesort/tui/__init__.py +11 -1
- schenesort/tui/grid_app.py +302 -0
- schenesort/tui/widgets/filter_panel.py +249 -0
- schenesort/tui/widgets/thumbnail_grid.py +291 -0
- {schenesort-2.2.0.dist-info → schenesort-2.4.0.dist-info}/METADATA +116 -13
- schenesort-2.4.0.dist-info/RECORD +19 -0
- schenesort-2.2.0.dist-info/RECORD +0 -15
- {schenesort-2.2.0.dist-info → schenesort-2.4.0.dist-info}/WHEEL +0 -0
- {schenesort-2.2.0.dist-info → schenesort-2.4.0.dist-info}/entry_points.txt +0 -0
- {schenesort-2.2.0.dist-info → schenesort-2.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Thumbnail grid widget for gallery view."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
from textual.containers import Container, VerticalScroll
|
|
8
|
+
from textual.message import Message
|
|
9
|
+
from textual.widgets import Static
|
|
10
|
+
from textual_image.widget import Image
|
|
11
|
+
|
|
12
|
+
from schenesort.thumbnails import get_thumbnail_path, thumbnail_exists
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ThumbnailText(Static):
|
|
16
|
+
"""Text fallback for when no thumbnail exists."""
|
|
17
|
+
|
|
18
|
+
DEFAULT_CSS = """
|
|
19
|
+
ThumbnailText {
|
|
20
|
+
width: 32;
|
|
21
|
+
height: 14;
|
|
22
|
+
content-align: center middle;
|
|
23
|
+
text-align: center;
|
|
24
|
+
color: $text-muted;
|
|
25
|
+
background: $surface;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ThumbnailText.selected {
|
|
29
|
+
border: solid $primary;
|
|
30
|
+
}
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, image_path: Path, index: int, **kwargs) -> None:
|
|
34
|
+
name = image_path.stem
|
|
35
|
+
if len(name) > 28:
|
|
36
|
+
name = name[:25] + "..."
|
|
37
|
+
super().__init__(name, **kwargs)
|
|
38
|
+
self.image_path = image_path
|
|
39
|
+
self.index = index
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Type alias for cell widgets
|
|
43
|
+
CellWidget = Image | ThumbnailText
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_thumbnail_cell(image_path: Path, index: int) -> CellWidget:
|
|
47
|
+
"""Create a cell widget for the given image using its thumbnail."""
|
|
48
|
+
if thumbnail_exists(image_path):
|
|
49
|
+
thumb_path = get_thumbnail_path(image_path)
|
|
50
|
+
img = Image(thumb_path)
|
|
51
|
+
# Store metadata on the widget
|
|
52
|
+
img.image_path = image_path # type: ignore[attr-defined]
|
|
53
|
+
img.index = index # type: ignore[attr-defined]
|
|
54
|
+
return img
|
|
55
|
+
return ThumbnailText(image_path, index)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ThumbnailGrid(VerticalScroll, can_focus=True):
|
|
59
|
+
"""Scrollable grid of image thumbnails with keyboard navigation."""
|
|
60
|
+
|
|
61
|
+
BINDINGS = [
|
|
62
|
+
Binding("up", "move_up", "Up", show=False),
|
|
63
|
+
Binding("down", "move_down", "Down", show=False),
|
|
64
|
+
Binding("left", "move_left", "Left", show=False),
|
|
65
|
+
Binding("right", "move_right", "Right", show=False),
|
|
66
|
+
Binding("k", "move_up", "Up", show=False),
|
|
67
|
+
Binding("j", "move_down", "Down", show=False),
|
|
68
|
+
Binding("h", "move_left", "Left", show=False),
|
|
69
|
+
Binding("l", "move_right", "Right", show=False),
|
|
70
|
+
Binding("enter", "select", "Select", show=False),
|
|
71
|
+
Binding("home", "first", "First", show=False),
|
|
72
|
+
Binding("end", "last", "Last", show=False),
|
|
73
|
+
Binding("g", "first", "First", show=False),
|
|
74
|
+
Binding("G", "last", "Last", show=False, key_display="shift+g"),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
class ImageSelected(Message):
|
|
78
|
+
"""Emitted when an image is selected (Enter pressed)."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, image_path: Path, index: int) -> None:
|
|
81
|
+
self.image_path = image_path
|
|
82
|
+
self.index = index
|
|
83
|
+
super().__init__()
|
|
84
|
+
|
|
85
|
+
class SelectionChanged(Message):
|
|
86
|
+
"""Emitted when the selection changes."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, image_path: Path, index: int) -> None:
|
|
89
|
+
self.image_path = image_path
|
|
90
|
+
self.index = index
|
|
91
|
+
super().__init__()
|
|
92
|
+
|
|
93
|
+
DEFAULT_CSS = """
|
|
94
|
+
ThumbnailGrid {
|
|
95
|
+
width: 100%;
|
|
96
|
+
height: 100%;
|
|
97
|
+
background: $background;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ThumbnailGrid:focus {
|
|
101
|
+
border: solid $accent;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
ThumbnailGrid #grid-container {
|
|
105
|
+
layout: grid;
|
|
106
|
+
grid-gutter: 0;
|
|
107
|
+
width: 100%;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
ThumbnailGrid #grid-container Image {
|
|
111
|
+
width: 32;
|
|
112
|
+
height: 14;
|
|
113
|
+
margin: 0;
|
|
114
|
+
padding: 0;
|
|
115
|
+
border: solid $background;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ThumbnailGrid #grid-container Image.selected {
|
|
119
|
+
border: solid $primary;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ThumbnailGrid .empty-message {
|
|
123
|
+
color: $text-disabled;
|
|
124
|
+
text-style: italic;
|
|
125
|
+
text-align: center;
|
|
126
|
+
width: 100%;
|
|
127
|
+
padding: 4;
|
|
128
|
+
}
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
CELL_WIDTH = 32
|
|
132
|
+
CELL_HEIGHT = 14
|
|
133
|
+
|
|
134
|
+
def __init__(self, images: list[Path] | None = None, **kwargs) -> None:
|
|
135
|
+
super().__init__(**kwargs)
|
|
136
|
+
self._images: list[Path] = images or []
|
|
137
|
+
self._selected_index: int = 0
|
|
138
|
+
self._columns: int = 1
|
|
139
|
+
self._cells: list[CellWidget] = []
|
|
140
|
+
self._grid_container: Container | None = None
|
|
141
|
+
|
|
142
|
+
def compose(self) -> ComposeResult:
|
|
143
|
+
self._grid_container = Container(id="grid-container")
|
|
144
|
+
yield self._grid_container
|
|
145
|
+
|
|
146
|
+
def on_mount(self) -> None:
|
|
147
|
+
"""Initialize the grid on mount."""
|
|
148
|
+
# Defer initial build to after layout
|
|
149
|
+
self.call_after_refresh(self._initial_build)
|
|
150
|
+
|
|
151
|
+
def _initial_build(self) -> None:
|
|
152
|
+
"""Build grid after initial layout."""
|
|
153
|
+
self._calculate_columns()
|
|
154
|
+
self._rebuild_grid()
|
|
155
|
+
|
|
156
|
+
def on_resize(self) -> None:
|
|
157
|
+
"""Recalculate columns when resized."""
|
|
158
|
+
old_columns = self._columns
|
|
159
|
+
self._calculate_columns()
|
|
160
|
+
if old_columns != self._columns:
|
|
161
|
+
self._rebuild_grid()
|
|
162
|
+
|
|
163
|
+
def _calculate_columns(self) -> None:
|
|
164
|
+
"""Calculate number of columns based on container width."""
|
|
165
|
+
available_width = max(self.size.width - 2, self.CELL_WIDTH)
|
|
166
|
+
self._columns = max(1, available_width // self.CELL_WIDTH)
|
|
167
|
+
|
|
168
|
+
def _rebuild_grid(self) -> None:
|
|
169
|
+
"""Rebuild the grid with current images and column count."""
|
|
170
|
+
if self._grid_container is None:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
container = self._grid_container
|
|
174
|
+
|
|
175
|
+
# Clear all children from container
|
|
176
|
+
container.remove_children()
|
|
177
|
+
self._cells.clear()
|
|
178
|
+
|
|
179
|
+
if not self._images:
|
|
180
|
+
container.mount(Static("No images to display", classes="empty-message"))
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
# Update grid columns CSS
|
|
184
|
+
container.styles.grid_size_columns = self._columns
|
|
185
|
+
|
|
186
|
+
# Calculate and set container height based on rows
|
|
187
|
+
# (textual-image needs explicit height, not height: auto, to render in scroll containers)
|
|
188
|
+
num_rows = (len(self._images) + self._columns - 1) // self._columns
|
|
189
|
+
container_height = num_rows * self.CELL_HEIGHT
|
|
190
|
+
container.styles.height = container_height
|
|
191
|
+
|
|
192
|
+
# Create cells
|
|
193
|
+
for idx, image_path in enumerate(self._images):
|
|
194
|
+
cell = create_thumbnail_cell(image_path, idx)
|
|
195
|
+
self._cells.append(cell)
|
|
196
|
+
container.mount(cell)
|
|
197
|
+
|
|
198
|
+
# Update selection
|
|
199
|
+
self._update_selection()
|
|
200
|
+
|
|
201
|
+
def set_images(self, images: list[Path]) -> None:
|
|
202
|
+
"""Update the images displayed in the grid."""
|
|
203
|
+
self._images = images
|
|
204
|
+
self._selected_index = 0 if images else -1
|
|
205
|
+
self._calculate_columns()
|
|
206
|
+
self._rebuild_grid()
|
|
207
|
+
|
|
208
|
+
def _update_selection(self) -> None:
|
|
209
|
+
"""Update the visual selection state."""
|
|
210
|
+
for idx, cell in enumerate(self._cells):
|
|
211
|
+
if idx == self._selected_index:
|
|
212
|
+
cell.add_class("selected")
|
|
213
|
+
else:
|
|
214
|
+
cell.remove_class("selected")
|
|
215
|
+
|
|
216
|
+
# Scroll selected cell into view
|
|
217
|
+
if 0 <= self._selected_index < len(self._cells):
|
|
218
|
+
self._cells[self._selected_index].scroll_visible()
|
|
219
|
+
|
|
220
|
+
def _move_selection(self, delta: int) -> None:
|
|
221
|
+
"""Move selection by delta amount."""
|
|
222
|
+
if not self._images:
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
new_index = self._selected_index + delta
|
|
226
|
+
new_index = max(0, min(len(self._images) - 1, new_index))
|
|
227
|
+
|
|
228
|
+
if new_index != self._selected_index:
|
|
229
|
+
self._selected_index = new_index
|
|
230
|
+
self._update_selection()
|
|
231
|
+
self.post_message(
|
|
232
|
+
self.SelectionChanged(self._images[self._selected_index], self._selected_index)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def action_move_up(self) -> None:
|
|
236
|
+
"""Move selection up by one row."""
|
|
237
|
+
self._move_selection(-self._columns)
|
|
238
|
+
|
|
239
|
+
def action_move_down(self) -> None:
|
|
240
|
+
"""Move selection down by one row."""
|
|
241
|
+
self._move_selection(self._columns)
|
|
242
|
+
|
|
243
|
+
def action_move_left(self) -> None:
|
|
244
|
+
"""Move selection left by one cell."""
|
|
245
|
+
self._move_selection(-1)
|
|
246
|
+
|
|
247
|
+
def action_move_right(self) -> None:
|
|
248
|
+
"""Move selection right by one cell."""
|
|
249
|
+
self._move_selection(1)
|
|
250
|
+
|
|
251
|
+
def action_first(self) -> None:
|
|
252
|
+
"""Move selection to first image."""
|
|
253
|
+
if self._images:
|
|
254
|
+
self._selected_index = 0
|
|
255
|
+
self._update_selection()
|
|
256
|
+
self.post_message(
|
|
257
|
+
self.SelectionChanged(self._images[self._selected_index], self._selected_index)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def action_last(self) -> None:
|
|
261
|
+
"""Move selection to last image."""
|
|
262
|
+
if self._images:
|
|
263
|
+
self._selected_index = len(self._images) - 1
|
|
264
|
+
self._update_selection()
|
|
265
|
+
self.post_message(
|
|
266
|
+
self.SelectionChanged(self._images[self._selected_index], self._selected_index)
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def action_select(self) -> None:
|
|
270
|
+
"""Select the current image (open in detail view)."""
|
|
271
|
+
if 0 <= self._selected_index < len(self._images):
|
|
272
|
+
self.post_message(
|
|
273
|
+
self.ImageSelected(self._images[self._selected_index], self._selected_index)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def selected_image(self) -> Path | None:
|
|
278
|
+
"""Get the currently selected image path."""
|
|
279
|
+
if 0 <= self._selected_index < len(self._images):
|
|
280
|
+
return self._images[self._selected_index]
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def selected_index(self) -> int:
|
|
285
|
+
"""Get the currently selected index."""
|
|
286
|
+
return self._selected_index
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def image_count(self) -> int:
|
|
290
|
+
"""Get the number of images in the grid."""
|
|
291
|
+
return len(self._images)
|
|
@@ -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
|
|
@@ -26,7 +28,7 @@ Once you have a collection of wallpapers re-named with metadata sidecars, run th
|
|
|
26
28
|
that can be queried to retrieve suggestions based on tags, colours, names and the like. Then use `get` to get a
|
|
27
29
|
wallpaper path `feh $(schenesort get -1 -p)` or `hyprctl hyprpaper wallpaper "eDP-1,$(schenesort get -1 -p)"`
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+

|
|
30
32
|
|
|
31
33
|
## Installation
|
|
32
34
|
|
|
@@ -44,16 +46,25 @@ uv sync
|
|
|
44
46
|
|
|
45
47
|
## Quick Start
|
|
46
48
|
|
|
49
|
+
To generate metadata you need a olama server serving llava nearby, it takes a minute to set one up see [ollama
|
|
50
|
+
setup](./ollama-setup.md).
|
|
51
|
+
|
|
47
52
|
```bash
|
|
48
53
|
# Generate metadata for images
|
|
49
54
|
schenesort metadata generate ~/wallpapers -r
|
|
50
55
|
|
|
51
|
-
# Browse with TUI
|
|
52
|
-
schenesort browse ~/wallpapers
|
|
53
|
-
|
|
54
56
|
# Index the collection
|
|
55
57
|
schenesort index ~/wallpapers
|
|
56
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
|
+
|
|
57
68
|
# Query wallpapers
|
|
58
69
|
schenesort get --mood peaceful --screen 4K
|
|
59
70
|
schenesort get -1 -p | xargs feh # random wallpaper
|
|
@@ -64,6 +75,8 @@ schenesort get -1 -p | xargs feh # random wallpaper
|
|
|
64
75
|
| Command | Description |
|
|
65
76
|
|------------------------------|-----------------------------------------------------|
|
|
66
77
|
| `browse` | Terminal UI browser with image preview and metadata |
|
|
78
|
+
| `gallery` | Thumbnail grid browser with filters |
|
|
79
|
+
| `thumbnail` | Generate thumbnail cache for gallery |
|
|
67
80
|
| `index` | Build SQLite index for fast querying |
|
|
68
81
|
| `get` | Query wallpapers by metadata attributes |
|
|
69
82
|
| `stats` | Show collection statistics from index |
|
|
@@ -74,6 +87,7 @@ schenesort get -1 -p | xargs feh # random wallpaper
|
|
|
74
87
|
| `info` | Show collection file statistics |
|
|
75
88
|
| `describe` | AI-rename images based on content (Ollama) |
|
|
76
89
|
| `models` | List available Ollama models |
|
|
90
|
+
| `collage` | Create a collage grid from matching wallpapers |
|
|
77
91
|
| `metadata show` | Display XMP sidecar metadata |
|
|
78
92
|
| `metadata set` | Manually set metadata fields |
|
|
79
93
|
| `metadata generate` | Generate metadata with AI (Ollama) |
|
|
@@ -91,13 +105,12 @@ schenesort browse # uses paths.wallpaper from config
|
|
|
91
105
|
schenesort get --mood peaceful -b # browse query results
|
|
92
106
|
```
|
|
93
107
|
|
|
94
|
-

|
|
95
|
-
|
|
96
108
|

|
|
97
109
|
|
|
98
110
|

|
|
99
111
|
|
|
100
112
|
**Keyboard shortcuts:**
|
|
113
|
+
|
|
101
114
|
| Key | Action |
|
|
102
115
|
|--------------|----------------|
|
|
103
116
|
| `j` / `Down` | Next image |
|
|
@@ -109,6 +122,63 @@ schenesort get --mood peaceful -b # browse query results
|
|
|
109
122
|
|
|
110
123
|
The TUI uses textual-image for rendering, which auto-detects terminal graphics support (Sixel, iTerm2, Kitty).
|
|
111
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
|
+
|
|
112
182
|
## Collection Indexing and Querying
|
|
113
183
|
|
|
114
184
|
Build a SQLite index for fast querying across your entire collection:
|
|
@@ -147,6 +217,38 @@ schenesort stats
|
|
|
147
217
|
|
|
148
218
|
The database is stored at `$XDG_DATA_HOME/schenesort/index.db` (default: `~/.local/share/schenesort/index.db`).
|
|
149
219
|
|
|
220
|
+
## Collage Generation
|
|
221
|
+
|
|
222
|
+
Create a collage grid (up to 4x4) from wallpapers matching query criteria:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
# Create a 2x2 collage (default)
|
|
226
|
+
schenesort collage output.png --mood peaceful
|
|
227
|
+
|
|
228
|
+
# Create a 4x4 collage of landscape images
|
|
229
|
+
schenesort collage wall.png --subject landscape --cols 4 --rows 4
|
|
230
|
+
|
|
231
|
+
# Custom tile size (default: 480x270)
|
|
232
|
+
schenesort collage collage.png --tile-width 640 --tile-height 360
|
|
233
|
+
|
|
234
|
+
# Combine filters
|
|
235
|
+
schenesort collage night_cities.png --time night --subject urban --cols 3 --rows 2
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`schenesort collage landscape.png --cols 4 --rows 4 --tile-width 640 --tile-height 360 --tag landscape`
|
|
239
|
+
|
|
240
|
+

|
|
241
|
+
|
|
242
|
+
| Option | Description | Default |
|
|
243
|
+
|-----------------|--------------------------------------|---------|
|
|
244
|
+
| `--cols` | Number of columns (1-4) | 2 |
|
|
245
|
+
| `--rows` | Number of rows (1-4) | 2 |
|
|
246
|
+
| `--tile-width` | Width of each tile in pixels | 480 |
|
|
247
|
+
| `--tile-height` | Height of each tile in pixels | 270 |
|
|
248
|
+
| `--random` | Select images randomly | True |
|
|
249
|
+
|
|
250
|
+
All `get` query filters are supported: `--tag`, `--mood`, `--color`, `--style`, `--subject`, `--time`, `--screen`, `--min-width`, `--min-height`, `--search`.
|
|
251
|
+
|
|
150
252
|
## Metadata Management
|
|
151
253
|
|
|
152
254
|
Store metadata in XMP sidecar files (`.xmp`) alongside images without modifying the original files.
|
|
@@ -168,7 +270,7 @@ Store metadata in XMP sidecar files (`.xmp`) alongside images without modifying
|
|
|
168
270
|
| `source` | Source URL or info |
|
|
169
271
|
| `ai_model` | Model used for metadata generation |
|
|
170
272
|
|
|
171
|
-
### Generate Metadata
|
|
273
|
+
### Generate Metadata using ollama
|
|
172
274
|
|
|
173
275
|
```bash
|
|
174
276
|
# Preview what would be generated
|
|
@@ -245,6 +347,7 @@ schenesort cleanup ~/wallpapers -r
|
|
|
245
347
|
## Configuration
|
|
246
348
|
|
|
247
349
|
Schenesort follows XDG Base Directory spec:
|
|
350
|
+
|
|
248
351
|
- Config: `$XDG_CONFIG_HOME/schenesort/config.toml` (default: `~/.config/schenesort/config.toml`)
|
|
249
352
|
- Data: `$XDG_DATA_HOME/schenesort/index.db` (default: `~/.local/share/schenesort/index.db`)
|
|
250
353
|
|
|
@@ -322,7 +425,7 @@ See [schenesort.yazi/README.md](schenesort.yazi/README.md) for details.
|
|
|
322
425
|
|
|
323
426
|
## XMP Sidecar Format
|
|
324
427
|
|
|
325
|
-
```
|
|
428
|
+
```text
|
|
326
429
|
~/wallpapers/
|
|
327
430
|
├── mountain_sunset.jpg
|
|
328
431
|
├── mountain_sunset.jpg.xmp ← metadata stored here
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
schenesort/__init__.py,sha256=Z7bmXIWiFUCKj1j4JRlPZvmfzc4RC10Lh1WCKNsJn20,61
|
|
2
|
+
schenesort/cli.py,sha256=TFq_AjH-PSJYAr7b14rXdXl7xEKovqNnt7SQAZ7psZE,55702
|
|
3
|
+
schenesort/config.py,sha256=8EuYv2nma3QKnLPSAgAsaOHZlORzinHjy-Bj_LKPfJA,2739
|
|
4
|
+
schenesort/db.py,sha256=RbfZN6d5O5MkTRPbu51fA7tQLOJf1huIjxFdoT4sESk,11398
|
|
5
|
+
schenesort/thumbnails.py,sha256=-B4vlh2w_NdUkO0tshcpZPUSs6M8S6G_abiBPp3xVOY,3631
|
|
6
|
+
schenesort/xmp.py,sha256=1VS_I4akY8Dv_KLPOdzPgBCFy0280oSCsMmo-_A9cNE,9749
|
|
7
|
+
schenesort/tui/__init__.py,sha256=BsOc1PedYBguvYBpnny_T9ST-dIa_52xYWlLdNemha4,449
|
|
8
|
+
schenesort/tui/app.py,sha256=HGKBnszTJXtLu9Mo7eJY4wiO2qkB7dFftBoM4GC_js4,6202
|
|
9
|
+
schenesort/tui/grid_app.py,sha256=hrS-dEwQ9wFmcQz2ZT7jy6lYOdSCidEY5Zlfmpv8sI8,10032
|
|
10
|
+
schenesort/tui/widgets/__init__.py,sha256=3cm7vfXG5-xC_UhIbgEtuMxq5I5tXg6okokJ4ecjGIE,202
|
|
11
|
+
schenesort/tui/widgets/filter_panel.py,sha256=BZf8T1H4u4eTUj2bt6ILvQ3m_hwx20DRfc02YVJ4zQk,7273
|
|
12
|
+
schenesort/tui/widgets/image_preview.py,sha256=j_KTQBJk0vbO18FzTo-GYkRlgerBe1K3VGQwMPbEuUw,2846
|
|
13
|
+
schenesort/tui/widgets/metadata_panel.py,sha256=8DnsvDdKf3jzqHOo7dDmxJxL0Mjx5mX7HNwwcMUcJn0,4860
|
|
14
|
+
schenesort/tui/widgets/thumbnail_grid.py,sha256=-81CL4pBI7Q7J_PD2MbeaI-SNckEdfuNZdA14m7q348,9407
|
|
15
|
+
schenesort-2.4.0.dist-info/METADATA,sha256=rIqVFt9JgHlVGSbsG1irhOllNWNzD0zEGMUjduxXhWc,14698
|
|
16
|
+
schenesort-2.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
17
|
+
schenesort-2.4.0.dist-info/entry_points.txt,sha256=J5lS-N6KgmzjutFi5bG1jv-4Wszbz3MfcOHcbznBVcw,50
|
|
18
|
+
schenesort-2.4.0.dist-info/licenses/LICENSE,sha256=sMw3SMb9ec9dbM2twEMVeunsGwuljza-9kEXg4kSJpg,1070
|
|
19
|
+
schenesort-2.4.0.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
schenesort/__init__.py,sha256=Z7bmXIWiFUCKj1j4JRlPZvmfzc4RC10Lh1WCKNsJn20,61
|
|
2
|
-
schenesort/cli.py,sha256=lGbQ5nJ5TjusG7XlZfOVmdaCeef9eRO_a9JL1I-V_Fc,46291
|
|
3
|
-
schenesort/config.py,sha256=8EuYv2nma3QKnLPSAgAsaOHZlORzinHjy-Bj_LKPfJA,2739
|
|
4
|
-
schenesort/db.py,sha256=RbfZN6d5O5MkTRPbu51fA7tQLOJf1huIjxFdoT4sESk,11398
|
|
5
|
-
schenesort/xmp.py,sha256=1VS_I4akY8Dv_KLPOdzPgBCFy0280oSCsMmo-_A9cNE,9749
|
|
6
|
-
schenesort/tui/__init__.py,sha256=bqSyRlefPfsYhUxsub4Rltz7yjCQVPXvhzj9n2bn370,141
|
|
7
|
-
schenesort/tui/app.py,sha256=HGKBnszTJXtLu9Mo7eJY4wiO2qkB7dFftBoM4GC_js4,6202
|
|
8
|
-
schenesort/tui/widgets/__init__.py,sha256=3cm7vfXG5-xC_UhIbgEtuMxq5I5tXg6okokJ4ecjGIE,202
|
|
9
|
-
schenesort/tui/widgets/image_preview.py,sha256=j_KTQBJk0vbO18FzTo-GYkRlgerBe1K3VGQwMPbEuUw,2846
|
|
10
|
-
schenesort/tui/widgets/metadata_panel.py,sha256=8DnsvDdKf3jzqHOo7dDmxJxL0Mjx5mX7HNwwcMUcJn0,4860
|
|
11
|
-
schenesort-2.2.0.dist-info/METADATA,sha256=FOfxFWXroyg2RtZxMCrHL1Bf5qC5d8OPeMb_3cHmK7M,11251
|
|
12
|
-
schenesort-2.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
13
|
-
schenesort-2.2.0.dist-info/entry_points.txt,sha256=J5lS-N6KgmzjutFi5bG1jv-4Wszbz3MfcOHcbznBVcw,50
|
|
14
|
-
schenesort-2.2.0.dist-info/licenses/LICENSE,sha256=sMw3SMb9ec9dbM2twEMVeunsGwuljza-9kEXg4kSJpg,1070
|
|
15
|
-
schenesort-2.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|