dockerpal 0.0.16__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.
- dockerpal/__init__.py +2 -0
- dockerpal/__main__.py +6 -0
- dockerpal/app.py +113 -0
- dockerpal/cli.py +22 -0
- dockerpal/fsm.py +463 -0
- dockerpal-0.0.16.dist-info/METADATA +62 -0
- dockerpal-0.0.16.dist-info/RECORD +11 -0
- dockerpal-0.0.16.dist-info/WHEEL +5 -0
- dockerpal-0.0.16.dist-info/entry_points.txt +2 -0
- dockerpal-0.0.16.dist-info/licenses/LICENSE +6 -0
- dockerpal-0.0.16.dist-info/top_level.txt +1 -0
dockerpal/__init__.py
ADDED
dockerpal/__main__.py
ADDED
dockerpal/app.py
ADDED
|
@@ -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()
|
dockerpal/cli.py
ADDED
|
@@ -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()
|
dockerpal/fsm.py
ADDED
|
@@ -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
|
+

|
|
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,11 @@
|
|
|
1
|
+
dockerpal/__init__.py,sha256=SEQmiS4ERzY67zW7H5L_Ea3qxmqoIOM_4seLSg90V0w,24
|
|
2
|
+
dockerpal/__main__.py,sha256=0i65Yn6gXl6J-SMz6MUzAQ-bE1f3uzkCRENCVAy31Zg,72
|
|
3
|
+
dockerpal/app.py,sha256=idNE77YepG7X16mRSXyTG-4TIRYKJV_38MTAuLphums,2748
|
|
4
|
+
dockerpal/cli.py,sha256=Afzfa1vPYhI4ujVScaRltcg8tdY6dWynz5pmukRWzH0,495
|
|
5
|
+
dockerpal/fsm.py,sha256=Mr3FSTcJlR5t2k_-fREXWv24O5Zd2smnyr5jOjkJVSk,14283
|
|
6
|
+
dockerpal-0.0.16.dist-info/licenses/LICENSE,sha256=AmSIUu2oeurr3OgAepMDlmWip4IgS3qLkcybiElB-2c,1024
|
|
7
|
+
dockerpal-0.0.16.dist-info/METADATA,sha256=8iwPgAiC9_1HpJTdQ_bFLa3AkWHPmXAcdUvPVXW5bXg,1928
|
|
8
|
+
dockerpal-0.0.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
dockerpal-0.0.16.dist-info/entry_points.txt,sha256=joDG6au8Z6tmRjrRIB26oY8P6LxVibvdxD3mu9Z2a-E,49
|
|
10
|
+
dockerpal-0.0.16.dist-info/top_level.txt,sha256=uV2VV8NjnavIHVyQDRJlgbK9jNAdhtAcl14YrFCOPlY,10
|
|
11
|
+
dockerpal-0.0.16.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
dockerpal
|