virtui-manager 1.4.0__py3-none-any.whl → 1.5.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.
- {virtui_manager-1.4.0.dist-info → virtui_manager-1.5.0.dist-info}/METADATA +1 -1
- {virtui_manager-1.4.0.dist-info → virtui_manager-1.5.0.dist-info}/RECORD +20 -19
- {virtui_manager-1.4.0.dist-info → virtui_manager-1.5.0.dist-info}/entry_points.txt +1 -0
- vmanager/constants.py +8 -2
- vmanager/gui_wrapper.py +89 -0
- vmanager/i18n.py +2 -2
- vmanager/{locales → locale}/de/LC_MESSAGES/virtui-manager.mo +0 -0
- vmanager/{locales → locale}/de/LC_MESSAGES/virtui-manager.po +407 -373
- vmanager/{locales → locale}/fr/LC_MESSAGES/virtui-manager.mo +0 -0
- vmanager/{locales → locale}/fr/LC_MESSAGES/virtui-manager.po +407 -377
- vmanager/locale/it/LC_MESSAGES/virtui-manager.mo +0 -0
- vmanager/{locales → locale}/it/LC_MESSAGES/virtui-manager.po +425 -390
- vmanager/{locales → locale}/virtui-manager.pot +406 -370
- vmanager/utils.py +3 -0
- vmanager/vmanager.py +31 -7
- vmanager/vmcard.css +16 -0
- vmanager/vmcard.py +63 -22
- vmanager/locales/it/LC_MESSAGES/virtui-manager.mo +0 -0
- {virtui_manager-1.4.0.dist-info → virtui_manager-1.5.0.dist-info}/WHEEL +0 -0
- {virtui_manager-1.4.0.dist-info → virtui_manager-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {virtui_manager-1.4.0.dist-info → virtui_manager-1.5.0.dist-info}/top_level.txt +0 -0
vmanager/utils.py
CHANGED
|
@@ -13,6 +13,9 @@ from typing import List, Tuple, Union, Callable
|
|
|
13
13
|
from urllib.parse import urlparse
|
|
14
14
|
from .constants import AppInfo
|
|
15
15
|
|
|
16
|
+
def is_running_under_flatpak():
|
|
17
|
+
return 'FLATPAK_ID' in os.environ
|
|
18
|
+
|
|
16
19
|
def find_free_port(start: int, end: int) -> int:
|
|
17
20
|
"""
|
|
18
21
|
Find a free port in the specified range.
|
vmanager/vmanager.py
CHANGED
|
@@ -62,7 +62,7 @@ from .utils import (
|
|
|
62
62
|
generate_webconsole_keys_if_needed,
|
|
63
63
|
get_server_color_cached,
|
|
64
64
|
setup_cache_monitoring,
|
|
65
|
-
setup_logging
|
|
65
|
+
setup_logging, is_running_under_flatpak
|
|
66
66
|
)
|
|
67
67
|
from .vm_queries import (
|
|
68
68
|
get_status,
|
|
@@ -1291,7 +1291,7 @@ class VMManagerTUI(App):
|
|
|
1291
1291
|
|
|
1292
1292
|
if result:
|
|
1293
1293
|
vm_info, domain, conn = result
|
|
1294
|
-
from modals.vmdetails_modals import VMDetailModal # Import here to avoid circular dep if any
|
|
1294
|
+
from .modals.vmdetails_modals import VMDetailModal # Import here to avoid circular dep if any
|
|
1295
1295
|
|
|
1296
1296
|
self.push_screen(
|
|
1297
1297
|
VMDetailModal(
|
|
@@ -1326,7 +1326,7 @@ class VMManagerTUI(App):
|
|
|
1326
1326
|
|
|
1327
1327
|
def _perform_bulk_action_worker(self, action_type: str, vm_uuids: list[str], delete_storage_flag: bool = False) -> None:
|
|
1328
1328
|
"""Worker function to orchestrate a bulk action using the VMService."""
|
|
1329
|
-
|
|
1329
|
+
bulk_failed = False
|
|
1330
1330
|
# Stop workers for all selected VMs to prevent conflicts
|
|
1331
1331
|
for uuid in vm_uuids:
|
|
1332
1332
|
vm_card = self.vm_card_pool.active_cards.get(uuid)
|
|
@@ -1357,8 +1357,10 @@ class VMManagerTUI(App):
|
|
|
1357
1357
|
logging.info(summary)
|
|
1358
1358
|
|
|
1359
1359
|
if successful_vms:
|
|
1360
|
+
bulk_failed = False
|
|
1360
1361
|
self.call_from_thread(self.show_success_message, SuccessMessages.BULK_ACTION_SUCCESS_TEMPLATE.format(action_type=action_type, count=len(successful_vms)))
|
|
1361
1362
|
if failed_vms:
|
|
1363
|
+
bulk_failed = True
|
|
1362
1364
|
self.call_from_thread(self.show_error_message, ErrorMessages.BULK_ACTION_FAILED_TEMPLATE.format(action_type=action_type, count=len(failed_vms)))
|
|
1363
1365
|
|
|
1364
1366
|
except Exception as e:
|
|
@@ -1371,7 +1373,8 @@ class VMManagerTUI(App):
|
|
|
1371
1373
|
# Unlock immediately so UI is not stuck if refresh fails
|
|
1372
1374
|
def unlock_and_refresh():
|
|
1373
1375
|
self.bulk_operation_in_progress = False
|
|
1374
|
-
|
|
1376
|
+
if bulk_failed is False:
|
|
1377
|
+
self.refresh_vm_list(force=True)
|
|
1375
1378
|
|
|
1376
1379
|
self.call_from_thread(unlock_and_refresh)
|
|
1377
1380
|
|
|
@@ -1386,6 +1389,13 @@ class VMManagerTUI(App):
|
|
|
1386
1389
|
|
|
1387
1390
|
def refresh_vm_list(self, force: bool = False, optimize_for_current_page: bool = False, on_complete: Callable | None = None) -> None:
|
|
1388
1391
|
"""Refreshes the list of VMs by running the fetch-and-display logic in a worker."""
|
|
1392
|
+
# Prevent refresh during bulk operations to maintain UI stability
|
|
1393
|
+
if self.bulk_operation_in_progress:
|
|
1394
|
+
logging.debug("Skipping refresh_vm_list because bulk operation is in progress.")
|
|
1395
|
+
if on_complete:
|
|
1396
|
+
on_complete()
|
|
1397
|
+
return
|
|
1398
|
+
|
|
1389
1399
|
# Don't display VMs until initial cache is complete
|
|
1390
1400
|
if self.initial_cache_loading and not self.initial_cache_complete:
|
|
1391
1401
|
return
|
|
@@ -1493,13 +1503,15 @@ class VMManagerTUI(App):
|
|
|
1493
1503
|
cpu = 0
|
|
1494
1504
|
memory = 0
|
|
1495
1505
|
|
|
1506
|
+
simple_uuid = uuid.split('@')[0] if '@' in uuid else uuid
|
|
1507
|
+
|
|
1496
1508
|
return {
|
|
1497
1509
|
'uuid': uuid,
|
|
1498
1510
|
'name': vm_name,
|
|
1499
1511
|
'status': status,
|
|
1500
1512
|
'cpu': cpu,
|
|
1501
1513
|
'memory': memory,
|
|
1502
|
-
'is_selected':
|
|
1514
|
+
'is_selected': simple_uuid in selected_uuids,
|
|
1503
1515
|
'domain': domain,
|
|
1504
1516
|
'conn': conn,
|
|
1505
1517
|
'uri': uri
|
|
@@ -1597,12 +1609,14 @@ class VMManagerTUI(App):
|
|
|
1597
1609
|
card.name = data['name']
|
|
1598
1610
|
card.cpu = data['cpu']
|
|
1599
1611
|
card.memory = data['memory']
|
|
1600
|
-
|
|
1612
|
+
|
|
1601
1613
|
card.server_border_color = self.get_server_color(data['uri'])
|
|
1602
1614
|
card.status = data['status']
|
|
1603
1615
|
card.internal_id = uuid
|
|
1604
1616
|
card.compact_view = self.compact_view
|
|
1605
1617
|
|
|
1618
|
+
card.is_selected = data['is_selected']
|
|
1619
|
+
|
|
1606
1620
|
# Mount any new cards
|
|
1607
1621
|
if cards_to_mount:
|
|
1608
1622
|
vms_container.mount(*cards_to_mount)
|
|
@@ -1910,12 +1924,22 @@ class VMManagerTUI(App):
|
|
|
1910
1924
|
|
|
1911
1925
|
def main():
|
|
1912
1926
|
"""Entry point for vmanager TUI application."""
|
|
1927
|
+
if is_running_under_flatpak():
|
|
1928
|
+
ldir = "/app/share/locale"
|
|
1929
|
+
else:
|
|
1930
|
+
if not os.path.exists("locale"):
|
|
1931
|
+
# Installed on the system
|
|
1932
|
+
ldir = "/usr/share/locale"
|
|
1933
|
+
else:
|
|
1934
|
+
# Devel version from git
|
|
1935
|
+
ldir = "locale"
|
|
1936
|
+
|
|
1913
1937
|
parser = argparse.ArgumentParser(description="A Textual application to manage VMs.")
|
|
1914
1938
|
parser.add_argument("--cmd", action="store_true", help="Run in command-line interpreter mode.")
|
|
1915
1939
|
args = parser.parse_args()
|
|
1916
1940
|
|
|
1917
1941
|
if args.cmd:
|
|
1918
|
-
from vmanager_cmd import VManagerCMD
|
|
1942
|
+
from .vmanager_cmd import VManagerCMD
|
|
1919
1943
|
VManagerCMD().cmdloop()
|
|
1920
1944
|
else:
|
|
1921
1945
|
terminal_size = os.get_terminal_size()
|
vmanager/vmcard.css
CHANGED
|
@@ -158,3 +158,19 @@ Button {
|
|
|
158
158
|
margin: 0 0;
|
|
159
159
|
padding: 0 0;
|
|
160
160
|
}
|
|
161
|
+
|
|
162
|
+
.name-box {
|
|
163
|
+
height: 2;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.quick-actions {
|
|
167
|
+
width: 8;
|
|
168
|
+
height: 2;
|
|
169
|
+
padding: 0 0;
|
|
170
|
+
}
|
|
171
|
+
.btn-small {
|
|
172
|
+
min-width: 2;
|
|
173
|
+
height: 1;
|
|
174
|
+
border: none;
|
|
175
|
+
padding: 0 0;
|
|
176
|
+
}
|
vmanager/vmcard.py
CHANGED
|
@@ -56,7 +56,7 @@ from .utils import (
|
|
|
56
56
|
from .constants import (
|
|
57
57
|
ButtonLabels, TabTitles, StatusText,
|
|
58
58
|
SparklineLabels, ErrorMessages, DialogMessages, VmAction,
|
|
59
|
-
WarningMessages, SuccessMessages
|
|
59
|
+
WarningMessages, SuccessMessages, StaticText, ProgressMessages
|
|
60
60
|
)
|
|
61
61
|
|
|
62
62
|
class VMCardActions(Static):
|
|
@@ -252,6 +252,15 @@ class VMCard(Static):
|
|
|
252
252
|
status_widget.update(status_text)
|
|
253
253
|
|
|
254
254
|
def compose(self):
|
|
255
|
+
self.ui["btn_quick_start"] = Button("▶", id="start", variant="success", classes="btn-small")
|
|
256
|
+
self.ui["btn_quick_start"].tooltip = StaticText.START_VMS
|
|
257
|
+
self.ui["btn_quick_view"] = Button("👁", id="connect", classes="btn-small")
|
|
258
|
+
self.ui["btn_quick_view"].tooltip = StaticText.REMOTE_VIEWER
|
|
259
|
+
self.ui["btn_quick_resume"] = Button("⏯", id="resume", variant="success", classes="btn-small")
|
|
260
|
+
self.ui["btn_quick_resume"].tooltip = ButtonLabels.RESUME
|
|
261
|
+
self.ui["btn_quick_stop"] = Button("■", id="shutdown", variant="primary", classes="btn-small")
|
|
262
|
+
self.ui["btn_quick_stop"].tooltip = ButtonLabels.SHUTDOWN
|
|
263
|
+
|
|
255
264
|
self.ui["checkbox"] = Checkbox("", id="vm-select-checkbox", classes="vm-select-checkbox", value=self.is_selected, tooltip="Select VM")
|
|
256
265
|
self.ui["vmname"] = Static(self._get_vm_display_name(), id="vmname", classes="vmname")
|
|
257
266
|
self.ui["status"] = Static(f"{self.status}{self.webc_status_indicator}", id="status")
|
|
@@ -290,6 +299,12 @@ class VMCard(Static):
|
|
|
290
299
|
with Vertical():
|
|
291
300
|
yield self.ui["vmname"]
|
|
292
301
|
yield self.ui["status"]
|
|
302
|
+
with Horizontal(classes="quick-actions"):
|
|
303
|
+
with Vertical():
|
|
304
|
+
yield self.ui["btn_quick_resume"]
|
|
305
|
+
yield self.ui["btn_quick_stop"]
|
|
306
|
+
yield self.ui["btn_quick_view"]
|
|
307
|
+
yield self.ui["btn_quick_start"]
|
|
293
308
|
|
|
294
309
|
yield self.ui["sparklines_container"]
|
|
295
310
|
yield self.ui["collapsible"]
|
|
@@ -315,6 +330,8 @@ class VMCard(Static):
|
|
|
315
330
|
# Clean up dynamic UI references to avoid memory leaks and stale state
|
|
316
331
|
keys_to_keep = {
|
|
317
332
|
"checkbox", "vmname", "status", "collapsible",
|
|
333
|
+
"btn_quick_start", "btn_quick_stop", "btn_quick_view",
|
|
334
|
+
"btn_quick_resume",
|
|
318
335
|
"sparklines_container",
|
|
319
336
|
# Resource Sparklines
|
|
320
337
|
"cpu_label", "cpu_sparkline", "cpu_sparkline_container",
|
|
@@ -555,19 +572,20 @@ class VMCard(Static):
|
|
|
555
572
|
if not self.ui:
|
|
556
573
|
return
|
|
557
574
|
|
|
558
|
-
|
|
575
|
+
sparklines = self.ui.get("sparklines_container")
|
|
559
576
|
collapsible = self.ui.get("collapsible")
|
|
560
577
|
vmname = self.ui.get("vmname")
|
|
561
578
|
vmstatus = self.ui.get("status")
|
|
562
579
|
checkbox = self.ui.get("checkbox")
|
|
563
580
|
|
|
564
|
-
if value:
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
# sparklines.remove()
|
|
581
|
+
if value:
|
|
582
|
+
if sparklines and sparklines.is_mounted:
|
|
583
|
+
sparklines.display = False
|
|
568
584
|
if collapsible and collapsible.is_mounted:
|
|
569
585
|
collapsible.collapsed = True
|
|
570
586
|
collapsible.remove()
|
|
587
|
+
#if checkbox and checkbox.is_mounted:
|
|
588
|
+
# checkbox.display = False
|
|
571
589
|
else:
|
|
572
590
|
try:
|
|
573
591
|
info_container = self.query_one("#info-container")
|
|
@@ -575,6 +593,11 @@ class VMCard(Static):
|
|
|
575
593
|
# Check if collapsible is already a child of info_container to avoid double mounting
|
|
576
594
|
if collapsible not in info_container.children:
|
|
577
595
|
info_container.mount(collapsible)
|
|
596
|
+
if sparklines:
|
|
597
|
+
sparklines.display = True
|
|
598
|
+
#if checkbox:
|
|
599
|
+
# checkbox.display = True
|
|
600
|
+
|
|
578
601
|
except NoMatches:
|
|
579
602
|
# This can happen if the card is not fully initialized or structures changed
|
|
580
603
|
logging.warning(f"Could not find #info-container on VMCard {self.name} when switching to detailed view.")
|
|
@@ -589,10 +612,11 @@ class VMCard(Static):
|
|
|
589
612
|
if value: # Compact view
|
|
590
613
|
self.styles.height = 4
|
|
591
614
|
self.styles.width = 20
|
|
592
|
-
if vmname:
|
|
615
|
+
if vmname:
|
|
616
|
+
vmname.styles.content_align = ("left", "middle")
|
|
593
617
|
if vmstatus: vmstatus.styles.content_align = ("left", "middle")
|
|
594
618
|
if checkbox: checkbox.styles.width = "2"
|
|
595
|
-
else: # Detailed view
|
|
619
|
+
else: # Detailed view
|
|
596
620
|
self.styles.height = 14
|
|
597
621
|
self.styles.width = 41
|
|
598
622
|
if vmname: vmname.styles.content_align = ("center", "middle")
|
|
@@ -629,7 +653,17 @@ class VMCard(Static):
|
|
|
629
653
|
|
|
630
654
|
def watch_server_border_color(self, old_color: str, new_color: str) -> None:
|
|
631
655
|
"""Called when server_border_color changes."""
|
|
632
|
-
self.
|
|
656
|
+
if self.is_selected:
|
|
657
|
+
self.styles.border = ("panel", "white")
|
|
658
|
+
else:
|
|
659
|
+
self.styles.border = ("solid", new_color)
|
|
660
|
+
|
|
661
|
+
def on_click(self, event: Click) -> None:
|
|
662
|
+
"""Handle click events on the card."""
|
|
663
|
+
if event.button == 3:
|
|
664
|
+
self.is_selected = not self.is_selected
|
|
665
|
+
self.post_message(VMSelectionChanged(vm_uuid=self.raw_uuid, is_selected=self.is_selected))
|
|
666
|
+
event.stop()
|
|
633
667
|
|
|
634
668
|
def on_unmount(self) -> None:
|
|
635
669
|
"""Stop the timer and cancel any running stat workers when the widget is removed."""
|
|
@@ -673,11 +707,10 @@ class VMCard(Static):
|
|
|
673
707
|
|
|
674
708
|
def watch_is_selected(self, old_value: bool, new_value: bool) -> None:
|
|
675
709
|
"""Called when is_selected changes to update the checkbox."""
|
|
676
|
-
if
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
checkbox.value = new_value
|
|
710
|
+
if self.ui:
|
|
711
|
+
checkbox = self.ui.get("checkbox")
|
|
712
|
+
if checkbox:
|
|
713
|
+
checkbox.value = new_value
|
|
681
714
|
|
|
682
715
|
if new_value:
|
|
683
716
|
self.styles.border = ("panel", "white")
|
|
@@ -888,6 +921,14 @@ class VMCard(Static):
|
|
|
888
921
|
is_pmsuspended = self.status == StatusText.PMSUSPENDED
|
|
889
922
|
is_blocked = self.status == StatusText.BLOCKED
|
|
890
923
|
|
|
924
|
+
if not self.ui.get("btn_quick_start"):
|
|
925
|
+
return
|
|
926
|
+
|
|
927
|
+
self.ui["btn_quick_start"].display = is_stopped
|
|
928
|
+
self.ui["btn_quick_stop"].display = is_running or is_blocked
|
|
929
|
+
self.ui["btn_quick_view"].display = (is_running or is_paused or is_blocked)
|
|
930
|
+
self.ui["btn_quick_resume"].display = is_paused or is_pmsuspended
|
|
931
|
+
|
|
891
932
|
if not self.ui.get("rename-button"):
|
|
892
933
|
return
|
|
893
934
|
|
|
@@ -1020,10 +1061,10 @@ class VMCard(Static):
|
|
|
1020
1061
|
pane = tabbed_content.get_tab("snapshot-tab")
|
|
1021
1062
|
if snapshot_count > 0:
|
|
1022
1063
|
latest = snapshot_summary.get('latest')
|
|
1023
|
-
info = f"
|
|
1064
|
+
info = f"{StaticText.LATEST_SNAPSHOT} {latest['name']} ({latest['time']})" if latest else "Unknown"
|
|
1024
1065
|
pane.tooltip = f"{info}\nTotal: {snapshot_count}"
|
|
1025
1066
|
else:
|
|
1026
|
-
|
|
1067
|
+
pane.tooltip = StaticText.NO_SNAPSHOTS_CREATED
|
|
1027
1068
|
except Exception:
|
|
1028
1069
|
pass
|
|
1029
1070
|
|
|
@@ -1183,7 +1224,7 @@ class VMCard(Static):
|
|
|
1183
1224
|
self.app.vm_service.unsuppress_vm_events(self.internal_id)
|
|
1184
1225
|
|
|
1185
1226
|
self.app.push_screen(
|
|
1186
|
-
ConfirmationDialog(
|
|
1227
|
+
ConfirmationDialog(DialogMessages.CONFIRM_DISCARD_CHANGES.format(target_disk=target_disk)),
|
|
1187
1228
|
on_confirm
|
|
1188
1229
|
)
|
|
1189
1230
|
|
|
@@ -1191,7 +1232,7 @@ class VMCard(Static):
|
|
|
1191
1232
|
proceed_with_discard(overlay_disks[0])
|
|
1192
1233
|
else:
|
|
1193
1234
|
self.app.push_screen(
|
|
1194
|
-
SelectDiskModal(overlay_disks,
|
|
1235
|
+
SelectDiskModal(overlay_disks, StaticText.SELECT_OVERLAY_DISCARD),
|
|
1195
1236
|
proceed_with_discard
|
|
1196
1237
|
)
|
|
1197
1238
|
|
|
@@ -1217,7 +1258,7 @@ class VMCard(Static):
|
|
|
1217
1258
|
|
|
1218
1259
|
def on_confirm(confirmed: bool):
|
|
1219
1260
|
if confirmed:
|
|
1220
|
-
progress_modal = ProgressModal(title=
|
|
1261
|
+
progress_modal = ProgressModal(title=ProgressMessages.COMMITTING_CHANGES_FOR.format(name=self.name))
|
|
1221
1262
|
self.app.push_screen(progress_modal)
|
|
1222
1263
|
|
|
1223
1264
|
def do_commit():
|
|
@@ -1236,7 +1277,7 @@ class VMCard(Static):
|
|
|
1236
1277
|
self.app.worker_manager.run(do_commit, name=f"commit_{self.name}")
|
|
1237
1278
|
|
|
1238
1279
|
self.app.push_screen(
|
|
1239
|
-
ConfirmationDialog(
|
|
1280
|
+
ConfirmationDialog(DialogMessages.CONFIRM_MERGE_CHANGES.format(target_disk=target_disk)),
|
|
1240
1281
|
on_confirm
|
|
1241
1282
|
)
|
|
1242
1283
|
|
|
@@ -1751,10 +1792,10 @@ class VMCard(Static):
|
|
|
1751
1792
|
pane = tabbed_content.get_tab("snapshot-tab")
|
|
1752
1793
|
if snapshot_count > 0:
|
|
1753
1794
|
latest = snapshot_summary.get('latest')
|
|
1754
|
-
info = f"
|
|
1795
|
+
info = f"{StaticText.LATEST_SNAPSHOT} {latest['name']} ({latest['time']})" if latest else "Unknown"
|
|
1755
1796
|
pane.tooltip = f"{info}\nTotal: {snapshot_count}"
|
|
1756
1797
|
else:
|
|
1757
|
-
pane.tooltip =
|
|
1798
|
+
pane.tooltip = StaticText.NO_SNAPSHOTS_CREATED
|
|
1758
1799
|
except Exception:
|
|
1759
1800
|
pass
|
|
1760
1801
|
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|