dockerpal 0.0.16__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.
@@ -0,0 +1,6 @@
1
+
2
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
3
+
4
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
5
+
6
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: dockerpal
3
+ Version: 0.0.16
4
+ Summary: Docker images cleaner and inspector tui app
5
+ Home-page: https://github.com/yell0w4x/dockerpal
6
+ Author: yell0w4x
7
+ Author-email: yell0w4x@gmail.com
8
+ License: MIT
9
+ Project-URL: Bug Tracker, https://github.com/yell0w4x/dockerpal/issues
10
+ Keywords: docker images cleaner inspector tui
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: certifi==2025.11.12
17
+ Requires-Dist: charset-normalizer==3.4.4
18
+ Requires-Dist: docker==7.1.0
19
+ Requires-Dist: idna==3.11
20
+ Requires-Dist: linkify-it-py==2.0.3
21
+ Requires-Dist: markdown-it-py==4.0.0
22
+ Requires-Dist: mdit-py-plugins==0.5.0
23
+ Requires-Dist: mdurl==0.1.2
24
+ Requires-Dist: platformdirs==4.5.1
25
+ Requires-Dist: Pygments==2.19.2
26
+ Requires-Dist: requests==2.32.5
27
+ Requires-Dist: rich==14.2.0
28
+ Requires-Dist: textual==6.12.0
29
+ Requires-Dist: tree-sitter==0.25.2
30
+ Requires-Dist: tree-sitter-bash==0.25.1
31
+ Requires-Dist: tree-sitter-css==0.25.0
32
+ Requires-Dist: tree-sitter-go==0.25.0
33
+ Requires-Dist: tree-sitter-html==0.23.2
34
+ Requires-Dist: tree-sitter-java==0.23.5
35
+ Requires-Dist: tree-sitter-javascript==0.25.0
36
+ Requires-Dist: tree-sitter-json==0.24.8
37
+ Requires-Dist: tree-sitter-markdown==0.5.1
38
+ Requires-Dist: tree-sitter-python==0.25.0
39
+ Requires-Dist: tree-sitter-regex==0.25.0
40
+ Requires-Dist: tree-sitter-rust==0.24.0
41
+ Requires-Dist: tree-sitter-sql==0.3.11
42
+ Requires-Dist: tree-sitter-toml==0.7.0
43
+ Requires-Dist: tree-sitter-xml==0.7.0
44
+ Requires-Dist: tree-sitter-yaml==0.7.2
45
+ Requires-Dist: typing_extensions==4.15.0
46
+ Requires-Dist: uc-micro-py==1.0.3
47
+ Requires-Dist: urllib3==2.6.2
48
+ Dynamic: license-file
49
+
50
+
51
+ # Dockerpal
52
+
53
+ ![dockerpal](https://github.com/yell0w4x/assets/raw/main/dockerpal.png)
54
+
55
+ ```
56
+ pip -m venv .venv
57
+ source .venv/bin/activate
58
+ pip install dockerpal
59
+ dockerpal
60
+ ```
61
+
62
+ Please note it's not even prealpha version.
@@ -0,0 +1,13 @@
1
+
2
+ # Dockerpal
3
+
4
+ ![dockerpal](https://github.com/yell0w4x/assets/raw/main/dockerpal.png)
5
+
6
+ ```
7
+ pip -m venv .venv
8
+ source .venv/bin/activate
9
+ pip install dockerpal
10
+ dockerpal
11
+ ```
12
+
13
+ Please note it's not even prealpha version.
@@ -0,0 +1,7 @@
1
+
2
+ [build-system]
3
+ requires = [
4
+ "setuptools",
5
+ "wheel"
6
+ ]
7
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,67 @@
1
+ [metadata]
2
+ version = 0.0.16
3
+ name = dockerpal
4
+ author = yell0w4x
5
+ author_email = yell0w4x@gmail.com
6
+ description = Docker images cleaner and inspector tui app
7
+ long_description = file: README.md
8
+ long_description_content_type = text/markdown
9
+ url = https://github.com/yell0w4x/dockerpal
10
+ project_urls =
11
+ Bug Tracker = https://github.com/yell0w4x/dockerpal/issues
12
+ classifiers =
13
+ Programming Language :: Python :: 3
14
+ Operating System :: OS Independent
15
+ license = MIT
16
+ keywords = docker images cleaner inspector tui
17
+
18
+ [options]
19
+ package_dir =
20
+ = src
21
+ packages = find:
22
+ python_requires = >=3.11
23
+ install_requires =
24
+ certifi==2025.11.12
25
+ charset-normalizer==3.4.4
26
+ docker==7.1.0
27
+ idna==3.11
28
+ linkify-it-py==2.0.3
29
+ markdown-it-py==4.0.0
30
+ mdit-py-plugins==0.5.0
31
+ mdurl==0.1.2
32
+ platformdirs==4.5.1
33
+ Pygments==2.19.2
34
+ requests==2.32.5
35
+ rich==14.2.0
36
+ textual==6.12.0
37
+ tree-sitter==0.25.2
38
+ tree-sitter-bash==0.25.1
39
+ tree-sitter-css==0.25.0
40
+ tree-sitter-go==0.25.0
41
+ tree-sitter-html==0.23.2
42
+ tree-sitter-java==0.23.5
43
+ tree-sitter-javascript==0.25.0
44
+ tree-sitter-json==0.24.8
45
+ tree-sitter-markdown==0.5.1
46
+ tree-sitter-python==0.25.0
47
+ tree-sitter-regex==0.25.0
48
+ tree-sitter-rust==0.24.0
49
+ tree-sitter-sql==0.3.11
50
+ tree-sitter-toml==0.7.0
51
+ tree-sitter-xml==0.7.0
52
+ tree-sitter-yaml==0.7.2
53
+ typing_extensions==4.15.0
54
+ uc-micro-py==1.0.3
55
+ urllib3==2.6.2
56
+
57
+ [options.packages.find]
58
+ where = src
59
+
60
+ [options.entry_points]
61
+ console_scripts =
62
+ dockerpal = dockerpal.cli:main
63
+
64
+ [egg_info]
65
+ tag_build =
66
+ tag_date = 0
67
+
@@ -0,0 +1,2 @@
1
+
2
+ __version__ = '0.0.16'
@@ -0,0 +1,6 @@
1
+
2
+ from dockerpal.cli import main
3
+
4
+
5
+ if __name__ == '__main__':
6
+ main()
@@ -0,0 +1,113 @@
1
+ from textual.app import App, ComposeResult
2
+ from textual.containers import Grid
3
+ from textual.widgets import DataTable, Footer, Header, Static, Label, Button, TextArea, ListView, ListItem
4
+ from textual.color import Color
5
+ from textual.theme import Theme
6
+ from textual import events
7
+ from textual.screen import Screen, ModalScreen
8
+ from textual._context import active_app
9
+ from textual.reactive import reactive
10
+ from textual.containers import Horizontal, VerticalGroup, Vertical, VerticalScroll, Container
11
+ from textual.binding import Binding
12
+ from textual import on, work
13
+ from time import monotonic
14
+ from rich.text import Text
15
+ from rich.style import Style
16
+
17
+ import docker
18
+
19
+ from dockerpal.fsm import ScreenFSM, SplashScreen
20
+
21
+
22
+ arctic_theme = Theme(
23
+ name="arctic",
24
+ primary="#88C0D0",
25
+ secondary="#81A1C1",
26
+ accent="#B48EAD",
27
+ foreground="#D8DEE9",
28
+ background="#2E3440",
29
+ success="#A3BE8C",
30
+ warning="#EBCB8B",
31
+ error="#BF616A",
32
+ surface="#3B4252",
33
+ panel="#434C5E",
34
+ dark=True,
35
+ variables={
36
+ "block-cursor-text-style": "none",
37
+ "footer-key-foreground": "#88C0D0",
38
+ "input-selection-background": "#81a1c1 35%",
39
+ },
40
+ )
41
+
42
+
43
+ class DockerPalApp(App):
44
+ CSS = """
45
+ DataTable {
46
+ # margin-bottom: 1; /* Leave space for footer */
47
+ }
48
+
49
+ #table-footer {
50
+ dock: bottom;
51
+ height: 2;
52
+ background: $panel;
53
+ # border-top: solid $primary;
54
+ # padding: 0 1;
55
+ align: center middle;
56
+ }
57
+
58
+ #table-footer Label {
59
+ margin-right: 2;
60
+ }
61
+
62
+ #sidebar {
63
+ margin-top: 1;
64
+ dock: left;
65
+ width: 15;
66
+ height: 100%;
67
+ # color: #0f2b41;
68
+ display: none;
69
+ # background: dodgerblue;
70
+ }
71
+
72
+ #initializing-label,#not-implemented-label {
73
+ column-span: 2;
74
+ height: 1fr;
75
+ width: 1fr;
76
+ content-align: center middle;
77
+ }
78
+ """
79
+
80
+ def __init__(self):
81
+ super().__init__()
82
+ active_app.set(self)
83
+
84
+ cli = docker.from_env()
85
+ self.__fsm = ScreenFSM(cli)
86
+
87
+
88
+ def on_key(self, event: events.Key):
89
+ match event.key:
90
+ case 'q':
91
+ self.exit()
92
+
93
+ self.__fsm.on_state_key(event)
94
+
95
+
96
+ async def on_mount(self):
97
+ self.title = 'DockerPal'
98
+ self.register_theme(arctic_theme)
99
+ self.theme = 'arctic'
100
+ self.push_screen(SplashScreen())
101
+
102
+ # loop = asyncio.get_running_loop()
103
+ # images_list = await loop.run_in_executor(None, lambda: self.__docker_cli.images.list())
104
+ self.__fsm.set_images_screen()
105
+
106
+
107
+ def action_toggle_dark(self):
108
+ self.theme = "textual-dark" if self.theme == "textual-light" else "textual-light"
109
+
110
+
111
+ def app():
112
+ app = DockerPalApp()
113
+ app.run()
@@ -0,0 +1,22 @@
1
+
2
+ from dockerpal.app import app
3
+
4
+ from argparse import ArgumentParser, RawDescriptionHelpFormatter
5
+ import sys
6
+
7
+
8
+ def cli(args=sys.argv[1:]):
9
+ parser = ArgumentParser(description='dockerpal description goes here')
10
+ parser.add_argument('--change-me', default='An option sample', required=False,
11
+ help='Just an option sample of your cli to be substituted by real ones')
12
+
13
+ return parser.parse_args(args)
14
+
15
+
16
+ def main():
17
+ args = cli()
18
+ app()
19
+
20
+
21
+ if __name__ == '__main__':
22
+ main()
@@ -0,0 +1,463 @@
1
+ import docker
2
+ from textual.containers import Grid
3
+ from textual.widgets import DataTable, Footer, Header, Label, Button, TextArea, ListView, ListItem
4
+ from textual import events
5
+ from textual.screen import Screen, ModalScreen
6
+ from textual._context import active_app
7
+ from textual.containers import Horizontal
8
+ from textual.binding import Binding
9
+ from textual.css.query import NoMatches
10
+ from time import monotonic
11
+ from rich.text import Text
12
+ from rich.style import Style
13
+
14
+ import json
15
+
16
+
17
+ def compose_sidebar():
18
+ with ListView(id='sidebar'):
19
+ yield ListItem(Label('Images'), id='images-sidebar-item')
20
+ yield ListItem(Label('Containers'), id='containers-sidebar-item')
21
+ yield ListItem(Label('Networks'), id='networks-sidebar-item')
22
+ yield ListItem(Label('Volumens'), id='volumens-sidebar-item')
23
+
24
+
25
+ class ScreenStateBase:
26
+ def __init__(self, ctx):
27
+ super().__init__()
28
+ self.__ctx = ctx
29
+
30
+ def on_state_key(self, event: events.Key):
31
+ pass
32
+
33
+ def on_state_enter(self, data=None):
34
+ pass
35
+
36
+ def on_state_exit(self):
37
+ pass
38
+
39
+ def render(self):
40
+ pass
41
+
42
+ def context(self):
43
+ return self.__ctx
44
+
45
+
46
+ class ScreenFSM:
47
+ def __init__(self, docker_cli):
48
+ self.__state = None
49
+ self.__docker_cli = docker_cli
50
+
51
+
52
+ def set_images_screen(self, images_list=None):
53
+ self.set_state(ImagesScreen(self, self.__docker_cli))
54
+
55
+
56
+ def set_image_details_screen(self, image):
57
+ self.set_state(ImageDetailsScreen(self, image))
58
+
59
+
60
+ def set_state(self, state, data=None):
61
+ if self.__state is not None:
62
+ self.__state.on_state_exit()
63
+ self.__state = state
64
+ self.__state.on_state_enter(data)
65
+
66
+
67
+ def state(self):
68
+ return self.__state
69
+
70
+
71
+ def compose(self):
72
+ return self.__state.compose()
73
+
74
+
75
+ def on_state_key(self, event: events.Key):
76
+ try:
77
+ self.__state.on_state_key(event)
78
+ except NoMatches:
79
+ # Workaround when textual tray is open
80
+ pass
81
+ except Exception as e:
82
+ self.notify(str(e), severity='error')
83
+
84
+
85
+ def send_event(self, event):
86
+ self.__event_bus.publish(event)
87
+
88
+
89
+ def set_subtitle(self, subtitle):
90
+ self.__app().sub_title = subtitle
91
+
92
+
93
+ def switch_screen(self, screen):
94
+ self.__app().switch_screen(screen)
95
+
96
+
97
+ def notify(self, message, severity='information'):
98
+ self.__app().notify(message, severity=severity)
99
+
100
+
101
+ def toggle_sidebar(self):
102
+ app = self.__app()
103
+ sidebar = app.get_child_by_id('sidebar')
104
+ if sidebar.styles.display == 'none':
105
+ sidebar.styles.display = 'block'
106
+ if sidebar.can_focus:
107
+ sidebar.focus()
108
+ else:
109
+ sidebar.styles.display = 'none'
110
+
111
+ return sidebar.styles.display == 'block'
112
+
113
+
114
+ def is_sidebar_visible(self):
115
+ app = self.__app()
116
+ sidebar = app.get_child_by_id('sidebar')
117
+ return sidebar.styles.display != 'none'
118
+
119
+
120
+ def exit(self):
121
+ self.__app().exit()
122
+
123
+
124
+ def app(self):
125
+ return self.__app()
126
+
127
+
128
+ def __app(self):
129
+ return active_app.get()
130
+
131
+
132
+ class ImagesScreen(Screen, ScreenStateBase):
133
+ current_row = None
134
+ SELECTED_SYMBOL = '[✓]'
135
+
136
+ BINDINGS = [
137
+ Binding("d,delete", "delete", "Delete"),
138
+ Binding("r", "refresh", "Refresh"),
139
+ Binding("space", "select_row", "Select row"),
140
+ # Binding("enter", "image_details", "Details"),
141
+ Binding("+", "select_all", "Select all"),
142
+ Binding("-", "deselect_all", "Deselect all"),
143
+ Binding("*", "invert_selection", "Invert selection"),
144
+ Binding("s", "sidebar", "Sidebar"),
145
+ Binding("q,escape", "exit", "Exit"),
146
+ Binding("j", "down", "Down", show=False),
147
+ Binding("k", "up", "Up", show=False),
148
+ Binding("f", "page_down", "Page down", show=False),
149
+ Binding("b", "page_up", "Page up", show=False),
150
+ Binding("g", "go_up", "Go up (double press)", show=False),
151
+ Binding("G", "go_down", "Go down", show=False),
152
+ ]
153
+
154
+ def __init__(self, ctx, docker_cli, images_list=None):
155
+ Screen.__init__(self, id='images-screen')
156
+ ScreenStateBase.__init__(self, ctx)
157
+ self.__cli = docker_cli
158
+ table = DataTable(id='images-table', cursor_type='row', zebra_stripes=False)
159
+ table.add_column(label='Short ID')
160
+ table.add_column('Tags')
161
+ self.__double_press = dict()
162
+ self.__table = table
163
+ self.__num_space_pad = 0
164
+ self.__selected_rows = set()
165
+ self.__total_label = Label()
166
+ self.__selected_label = Label()
167
+ self.renew(images_list)
168
+
169
+
170
+ def renew(self, images_list=None):
171
+ def tag(image):
172
+ return ', '.join(image.tags) if image.tags else '<None>'
173
+
174
+ def short_id(image):
175
+ return image.short_id.split(':')[1]
176
+
177
+ def full_id(image):
178
+ return image.id.split(':')[1]
179
+
180
+ images = self.__cli.images.list() if images_list is None else images_list
181
+ table = self.__table
182
+ table.clear()
183
+ pad = self.__num_space_pad = len(str(len(images))) + len(self.SELECTED_SYMBOL) + 2
184
+ {table.add_row(short_id(image), tag(image), key=full_id(image), label=f'{i: <{pad}}'): image for i, image in enumerate(images, 1)}
185
+ self.__update_footer()
186
+
187
+
188
+ def compose(self):
189
+ yield from compose_sidebar()
190
+ yield Header(name='Images')
191
+ yield self.__table
192
+ # yield Horizontal(self.__total_label, self.__selected_label, id='table-footer')
193
+ with Horizontal(id="table-footer"):
194
+ yield self.__total_label
195
+ yield self.__selected_label
196
+ yield Footer()
197
+
198
+
199
+ def on_state_enter(self, data=None):
200
+ self.context().set_subtitle('Images')
201
+ self.context().switch_screen(self)
202
+ if ImagesScreen.current_row is not None:
203
+ self.__set_cursor_row(ImagesScreen.current_row)
204
+
205
+
206
+ def on_state_key(self, event: events.Key):
207
+ table = self.__table
208
+ context = self.context()
209
+ match event.key:
210
+ case 'escape' | 'q':
211
+ context.exit()
212
+ case 'enter':
213
+ if context.is_sidebar_visible():
214
+ sidebar = context.app().get_child_by_id('sidebar')
215
+ match sidebar.highlighted_child.id:
216
+ case 'images-sidebar-item':
217
+ pass
218
+ case 'containers-sidebar-item':
219
+ context.notify('Not implemented yet.', severity='warning')
220
+ case 'networks-sidebar-item':
221
+ context.notify('Not implemented yet.', severity='warning')
222
+ case 'volumens-sidebar-item':
223
+ context.notify('Not implemented yet.', severity='warning')
224
+
225
+ else:
226
+ ImagesScreen.current_row = table.cursor_row
227
+ context.set_image_details_screen(self.__get_row_image(table.cursor_row))
228
+
229
+
230
+ def action_select_all(self):
231
+ table = self.__table
232
+ sel_rows = self.__selected_rows
233
+ sel_rows.clear()
234
+ for i in range(len(table.rows)):
235
+ self.__toggle_row_sel(i, move_cursor=False)
236
+ self.__update_selected_label()
237
+
238
+
239
+ def action_deselect_all(self):
240
+ table = self.__table
241
+ self.__selected_rows = {i for i in range(len(table.rows))}
242
+ for i in range(len(table.rows)):
243
+ self.__toggle_row_sel(i, move_cursor=False)
244
+ self.__update_selected_label()
245
+
246
+
247
+ def action_invert_selection(self):
248
+ table = self.__table
249
+ for i in range(len(table.rows)):
250
+ self.__toggle_row_sel(i, move_cursor=False)
251
+ self.__update_selected_label()
252
+
253
+
254
+ def action_select_row(self):
255
+ self.__toggle_row_sel()
256
+ self.__update_selected_label()
257
+
258
+
259
+ def action_down(self):
260
+ self.__table.action_cursor_down()
261
+
262
+
263
+ def action_up(self):
264
+ self.__table.action_cursor_up()
265
+
266
+
267
+ def action_page_down(self):
268
+ self.__table.action_page_down()
269
+
270
+
271
+ def action_page_up(self):
272
+ self.__table.action_page_up()
273
+
274
+
275
+ def action_go_up(self):
276
+ table = self.__table
277
+ dp = self.__double_press
278
+ before = dp.get('g')
279
+ if before is None:
280
+ dp['g'] = monotonic()
281
+ return
282
+
283
+ now = monotonic()
284
+ if now - dp.get('g') < 0.2:
285
+ table.action_scroll_top()
286
+ dp.pop('g', None)
287
+ return
288
+
289
+ dp['g'] = now
290
+
291
+
292
+ def action_go_down(self):
293
+ self.__table.action_scroll_bottom()
294
+
295
+
296
+ def action_delete(self):
297
+ def remove_image(row_index):
298
+ rows = tuple(table.rows.items())
299
+ row_key, _ = rows[row_index]
300
+ self.__cli.images.remove(row_key.value)
301
+
302
+ table = self.__table
303
+ sel_rows = self.__selected_rows
304
+ cursor_row = table.cursor_row
305
+ try:
306
+ if not sel_rows:
307
+ remove_image(cursor_row)
308
+ else:
309
+ for i in sel_rows:
310
+ remove_image(i)
311
+ sel_rows.clear()
312
+ except docker.errors.APIError as e:
313
+ self.context().notify(e.explanation, severity='error')
314
+ else:
315
+ self.renew()
316
+ self.__set_cursor_row(cursor_row)
317
+ self.__update_selected_label()
318
+
319
+
320
+ def action_refresh(self):
321
+ self.renew()
322
+
323
+
324
+ def action_sidebar(self):
325
+ self.context().toggle_sidebar()
326
+
327
+
328
+ def on_mount(self):
329
+ self.get_child_by_id('images-table').focus()
330
+
331
+
332
+ def __set_cursor_row(self, row_index):
333
+ table = self.__table
334
+ if table.row_count > row_index:
335
+ table.move_cursor(row=row_index)
336
+ elif table.row_count > 0:
337
+ table.move_cursor(row=table.row_count - 1)
338
+
339
+
340
+ def __update_footer(self):
341
+ self.__total_label.update(f'Total: {len(self.__cli.images.list())}')
342
+ self.__selected_label.update(f'Selected: {len(self.__selected_rows)}')
343
+
344
+
345
+ def __update_selected_label(self):
346
+ self.__selected_label.update(f'Selected: {len(self.__selected_rows)}')
347
+
348
+
349
+ def __toggle_row_sel(self, cursor_row=None, move_cursor=True):
350
+ if cursor_row is None:
351
+ cursor_row = self.__table.cursor_row
352
+
353
+ table = self.__table
354
+ rows = tuple(table.rows.items())
355
+ row_key, row = rows[cursor_row]
356
+ col_key, _ = next(iter(table.columns.items()))
357
+
358
+ sel_rows = self.__selected_rows
359
+ if cursor_row in sel_rows:
360
+ sel_rows.remove(cursor_row)
361
+ row.label = Text(f'{cursor_row + 1: <{self.__num_space_pad}}')
362
+ else:
363
+ sel_rows.add(cursor_row)
364
+ row.label.style = Style(color='#FA8072')
365
+ row.label.set_length(len(self.SELECTED_SYMBOL))
366
+ row.label.append('[✓]')
367
+
368
+ cell_val = table.get_cell(row_key, col_key)
369
+ table.update_cell(row_key, col_key, cell_val, update_width=True)
370
+ if move_cursor:
371
+ table.action_cursor_down()
372
+
373
+
374
+ def __get_row_image(self, row_index):
375
+ rows = tuple(self.__table.rows.items())
376
+ row_key, _ = rows[row_index]
377
+ return self.__cli.images.get(row_key.value)
378
+
379
+
380
+ class ImageDetailsScreen(Screen, ScreenStateBase):
381
+ BINDINGS = [
382
+ Binding("escape", "exit", "Go back"),
383
+ ]
384
+
385
+ def __init__(self, ctx, image):
386
+ Screen.__init__(self, id='image-details-screen')
387
+ ScreenStateBase.__init__(self, ctx)
388
+ self.__image = image
389
+
390
+
391
+ def on_mount(self):
392
+ self.get_child_by_id('image-details').focus()
393
+
394
+
395
+ def on_state_enter(self, data=None):
396
+ if data is not None:
397
+ self.__image = data
398
+
399
+ self.context().set_subtitle('Image details')
400
+ self.context().switch_screen(self)
401
+
402
+
403
+ def compose(self):
404
+ try:
405
+ yield from compose_sidebar()
406
+ yield Header()
407
+ yield Footer()
408
+ details = json.dumps(self.__image.attrs, indent=4)
409
+ yield TextArea(details, read_only=True, language='json', id='image-details')
410
+ except json.JSONDecodeError as e:
411
+ yield TextArea(f'Unable to parse image details: {e}', read_only=True, language='html', id='image-details')
412
+ self.context().notify(f'Unable to parse image details: {e}', severity='error')
413
+ except Exception as e:
414
+ yield TextArea(f'Unable to parse image details: {e}', read_only=True, language='html', id='image-details')
415
+ self.context().notify(str(e), severity='error')
416
+
417
+
418
+ def on_state_key(self, event: events.Key):
419
+ context = self.context()
420
+ match event.key:
421
+ case 'enter':
422
+ sidebar = context.app().get_child_by_id('sidebar')
423
+ match sidebar.highlighted_child.id:
424
+ case 'images-sidebar-item':
425
+ pass
426
+ case 'containers-sidebar-item':
427
+ context.notify('Not implemented yet.', severity='warning')
428
+ case 'networks-sidebar-item':
429
+ context.notify('Not implemented yet.', severity='warning')
430
+ case 'volumens-sidebar-item':
431
+ context.notify('Not implemented yet.', severity='warning')
432
+ case 'escape':
433
+ context.set_images_screen()
434
+ case 's':
435
+ context.toggle_sidebar()
436
+
437
+
438
+ class SplashScreen(Screen):
439
+ def compose(self):
440
+ yield Grid(Label("Initializing...", id='initializing-label'), id="splash-screen")
441
+
442
+
443
+ class NotImplementedScreen(Screen):
444
+ def compose(self):
445
+ yield Grid(Label("Not Implemented", id='not-implemented-label'), id="not-implemented-screen")
446
+
447
+
448
+ class QuitScreen(ModalScreen):
449
+ """Screen with a dialog to quit."""
450
+
451
+ def compose(self):
452
+ yield Grid(
453
+ Label("Are you sure you want to quit?", id="question"),
454
+ Button("Quit", variant="error", id="quit"),
455
+ Button("Cancel", variant="primary", id="cancel"),
456
+ id="dialog",
457
+ )
458
+
459
+ def on_button_pressed(self, event: Button.Pressed) -> None:
460
+ if event.button.id == "quit":
461
+ self.app.exit()
462
+ else:
463
+ self.app.pop_screen()
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: dockerpal
3
+ Version: 0.0.16
4
+ Summary: Docker images cleaner and inspector tui app
5
+ Home-page: https://github.com/yell0w4x/dockerpal
6
+ Author: yell0w4x
7
+ Author-email: yell0w4x@gmail.com
8
+ License: MIT
9
+ Project-URL: Bug Tracker, https://github.com/yell0w4x/dockerpal/issues
10
+ Keywords: docker images cleaner inspector tui
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: certifi==2025.11.12
17
+ Requires-Dist: charset-normalizer==3.4.4
18
+ Requires-Dist: docker==7.1.0
19
+ Requires-Dist: idna==3.11
20
+ Requires-Dist: linkify-it-py==2.0.3
21
+ Requires-Dist: markdown-it-py==4.0.0
22
+ Requires-Dist: mdit-py-plugins==0.5.0
23
+ Requires-Dist: mdurl==0.1.2
24
+ Requires-Dist: platformdirs==4.5.1
25
+ Requires-Dist: Pygments==2.19.2
26
+ Requires-Dist: requests==2.32.5
27
+ Requires-Dist: rich==14.2.0
28
+ Requires-Dist: textual==6.12.0
29
+ Requires-Dist: tree-sitter==0.25.2
30
+ Requires-Dist: tree-sitter-bash==0.25.1
31
+ Requires-Dist: tree-sitter-css==0.25.0
32
+ Requires-Dist: tree-sitter-go==0.25.0
33
+ Requires-Dist: tree-sitter-html==0.23.2
34
+ Requires-Dist: tree-sitter-java==0.23.5
35
+ Requires-Dist: tree-sitter-javascript==0.25.0
36
+ Requires-Dist: tree-sitter-json==0.24.8
37
+ Requires-Dist: tree-sitter-markdown==0.5.1
38
+ Requires-Dist: tree-sitter-python==0.25.0
39
+ Requires-Dist: tree-sitter-regex==0.25.0
40
+ Requires-Dist: tree-sitter-rust==0.24.0
41
+ Requires-Dist: tree-sitter-sql==0.3.11
42
+ Requires-Dist: tree-sitter-toml==0.7.0
43
+ Requires-Dist: tree-sitter-xml==0.7.0
44
+ Requires-Dist: tree-sitter-yaml==0.7.2
45
+ Requires-Dist: typing_extensions==4.15.0
46
+ Requires-Dist: uc-micro-py==1.0.3
47
+ Requires-Dist: urllib3==2.6.2
48
+ Dynamic: license-file
49
+
50
+
51
+ # Dockerpal
52
+
53
+ ![dockerpal](https://github.com/yell0w4x/assets/raw/main/dockerpal.png)
54
+
55
+ ```
56
+ pip -m venv .venv
57
+ source .venv/bin/activate
58
+ pip install dockerpal
59
+ dockerpal
60
+ ```
61
+
62
+ Please note it's not even prealpha version.
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.cfg
5
+ src/dockerpal/__init__.py
6
+ src/dockerpal/__main__.py
7
+ src/dockerpal/app.py
8
+ src/dockerpal/cli.py
9
+ src/dockerpal/fsm.py
10
+ src/dockerpal.egg-info/PKG-INFO
11
+ src/dockerpal.egg-info/SOURCES.txt
12
+ src/dockerpal.egg-info/dependency_links.txt
13
+ src/dockerpal.egg-info/entry_points.txt
14
+ src/dockerpal.egg-info/requires.txt
15
+ src/dockerpal.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dockerpal = dockerpal.cli:main
@@ -0,0 +1,32 @@
1
+ certifi==2025.11.12
2
+ charset-normalizer==3.4.4
3
+ docker==7.1.0
4
+ idna==3.11
5
+ linkify-it-py==2.0.3
6
+ markdown-it-py==4.0.0
7
+ mdit-py-plugins==0.5.0
8
+ mdurl==0.1.2
9
+ platformdirs==4.5.1
10
+ Pygments==2.19.2
11
+ requests==2.32.5
12
+ rich==14.2.0
13
+ textual==6.12.0
14
+ tree-sitter==0.25.2
15
+ tree-sitter-bash==0.25.1
16
+ tree-sitter-css==0.25.0
17
+ tree-sitter-go==0.25.0
18
+ tree-sitter-html==0.23.2
19
+ tree-sitter-java==0.23.5
20
+ tree-sitter-javascript==0.25.0
21
+ tree-sitter-json==0.24.8
22
+ tree-sitter-markdown==0.5.1
23
+ tree-sitter-python==0.25.0
24
+ tree-sitter-regex==0.25.0
25
+ tree-sitter-rust==0.24.0
26
+ tree-sitter-sql==0.3.11
27
+ tree-sitter-toml==0.7.0
28
+ tree-sitter-xml==0.7.0
29
+ tree-sitter-yaml==0.7.2
30
+ typing_extensions==4.15.0
31
+ uc-micro-py==1.0.3
32
+ urllib3==2.6.2
@@ -0,0 +1 @@
1
+ dockerpal