virtui-manager 1.1.6__py3-none-any.whl → 1.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.
Files changed (62) hide show
  1. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/METADATA +1 -1
  2. virtui_manager-1.4.0.dist-info/RECORD +76 -0
  3. vmanager/constants.py +739 -108
  4. vmanager/dialog.css +24 -0
  5. vmanager/firmware_manager.py +4 -1
  6. vmanager/i18n.py +32 -0
  7. vmanager/libvirt_utils.py +132 -3
  8. vmanager/locales/de/LC_MESSAGES/virtui-manager.mo +0 -0
  9. vmanager/locales/de/LC_MESSAGES/virtui-manager.po +3158 -0
  10. vmanager/locales/fr/LC_MESSAGES/virtui-manager.mo +0 -0
  11. vmanager/locales/fr/LC_MESSAGES/virtui-manager.po +3155 -0
  12. vmanager/locales/it/LC_MESSAGES/virtui-manager.mo +0 -0
  13. vmanager/locales/it/LC_MESSAGES/virtui-manager.po +3132 -0
  14. vmanager/locales/virtui-manager.pot +3033 -0
  15. vmanager/modals/bulk_modals.py +13 -12
  16. vmanager/modals/cache_stats_modal.py +6 -5
  17. vmanager/modals/capabilities_modal.py +133 -0
  18. vmanager/modals/config_modal.py +25 -24
  19. vmanager/modals/cpu_mem_pc_modals.py +22 -21
  20. vmanager/modals/custom_migration_modal.py +10 -9
  21. vmanager/modals/disk_pool_modals.py +60 -59
  22. vmanager/modals/host_dashboard_modal.py +137 -0
  23. vmanager/modals/host_stats.py +199 -0
  24. vmanager/modals/howto_disk_modal.py +2 -1
  25. vmanager/modals/howto_network_modal.py +2 -1
  26. vmanager/modals/howto_overlay_modal.py +2 -1
  27. vmanager/modals/howto_ssh_modal.py +2 -1
  28. vmanager/modals/howto_virtiofs_modal.py +2 -1
  29. vmanager/modals/input_modals.py +11 -10
  30. vmanager/modals/log_modal.py +2 -1
  31. vmanager/modals/migration_modals.py +21 -19
  32. vmanager/modals/network_modals.py +45 -36
  33. vmanager/modals/provisioning_modals.py +56 -56
  34. vmanager/modals/select_server_modals.py +8 -7
  35. vmanager/modals/selection_modals.py +7 -6
  36. vmanager/modals/server_modals.py +24 -23
  37. vmanager/modals/server_prefs_modals.py +78 -71
  38. vmanager/modals/utils_modals.py +10 -9
  39. vmanager/modals/virsh_modals.py +3 -2
  40. vmanager/modals/virtiofs_modals.py +6 -5
  41. vmanager/modals/vm_type_info_modal.py +2 -1
  42. vmanager/modals/vmanager_modals.py +19 -19
  43. vmanager/modals/vmcard_dialog.py +57 -57
  44. vmanager/modals/vmdetails_modals.py +115 -123
  45. vmanager/modals/xml_modals.py +3 -2
  46. vmanager/network_manager.py +4 -1
  47. vmanager/storage_manager.py +157 -39
  48. vmanager/utils.py +54 -7
  49. vmanager/vm_actions.py +48 -24
  50. vmanager/vm_migration.py +4 -1
  51. vmanager/vm_queries.py +67 -25
  52. vmanager/vm_service.py +8 -5
  53. vmanager/vmanager.css +55 -1
  54. vmanager/vmanager.py +247 -120
  55. vmanager/vmcard.css +3 -1
  56. vmanager/vmcard.py +270 -205
  57. vmanager/webconsole_manager.py +22 -22
  58. virtui_manager-1.1.6.dist-info/RECORD +0 -65
  59. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/WHEEL +0 -0
  60. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/entry_points.txt +0 -0
  61. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/licenses/LICENSE +0 -0
  62. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,7 @@ from textual import on
7
7
  from textual.widgets import Label, Button, Markdown, Static, RadioSet, RadioButton, Checkbox
8
8
 
9
9
  from .base_modals import BaseModal
10
+ from ..constants import ErrorMessages, StaticText, ButtonLabels
10
11
 
11
12
  class BulkActionModal(BaseModal[None]):
12
13
  """Modal screen for performing bulk actions on selected VMs."""
@@ -17,26 +18,26 @@ class BulkActionModal(BaseModal[None]):
17
18
 
18
19
  def compose(self) -> ComposeResult:
19
20
  with Vertical(id="bulk-action-dialog"):
20
- yield Label("Selected VMs for Bulk Action")
21
+ yield Label(StaticText.SELECTED_VMS_BULK)
21
22
  yield Static(classes="button-separator")
22
23
  with ScrollableContainer():
23
24
  all_vms = ", ".join(self.vm_names)
24
25
  yield Markdown(all_vms, id="selected-vms-list")
25
26
 
26
- yield Label("Choose Action:")
27
+ yield Label(StaticText.CHOOSE_ACTION)
27
28
  with RadioSet(id="bulk-action-radioset"):
28
- yield RadioButton("Start VMs", id="action_start")
29
- yield RadioButton("Stop VMs (Graceful Shutdown)", id="action_stop")
30
- yield RadioButton("Force Off VMs", id="action_force_off")
31
- yield RadioButton("Pause VMs", id="action_pause")
32
- yield RadioButton("Delete VMs", id="action_delete")
33
- yield RadioButton("Edit Configuration", id="action_edit_config")
29
+ yield RadioButton(StaticText.START_VMS, id="action_start")
30
+ yield RadioButton(StaticText.STOP_VMS_GRACEFUL, id="action_stop")
31
+ yield RadioButton(StaticText.FORCE_OFF_VMS, id="action_force_off")
32
+ yield RadioButton(StaticText.PAUSE_VMS, id="action_pause")
33
+ yield RadioButton(StaticText.DELETE_VMS, id="action_delete")
34
+ yield RadioButton(StaticText.EDIT_CONFIGURATION, id="action_edit_config")
34
35
 
35
- yield Checkbox("Delete associated storage", id="delete-storage-checkbox")
36
+ yield Checkbox(StaticText.DELETE_ASSOCIATED_STORAGE, id="delete-storage-checkbox")
36
37
 
37
38
  with Horizontal():
38
- yield Button("Execute", variant="primary", id="execute-action-btn", classes="button-container")
39
- yield Button("Cancel", variant="default", id="cancel-btn", classes="button-container")
39
+ yield Button(ButtonLabels.EXECUTE, variant="primary", id="execute-action-btn", classes="button-container")
40
+ yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn", classes="button-container")
40
41
 
41
42
  def on_mount(self) -> None:
42
43
  """Called when the modal is mounted to initially hide the checkbox."""
@@ -60,6 +61,6 @@ class BulkActionModal(BaseModal[None]):
60
61
  result['delete_storage'] = checkbox.value
61
62
  self.dismiss(result)
62
63
  else:
63
- self.app.show_error_message("Please select an action.")
64
+ self.app.show_error_message(ErrorMessages.PLEASE_SELECT_ACTION)
64
65
  elif event.button.id == "cancel-btn":
65
66
  self.dismiss(None)
@@ -5,6 +5,7 @@ from textual.app import ComposeResult
5
5
  from textual.widgets import Static, Button, DataTable
6
6
  from textual.containers import Vertical, Horizontal
7
7
  from .base_modals import BaseModal
8
+ from ..constants import StaticText, SuccessMessages, ButtonLabels
8
9
 
9
10
  class CacheStatsModal(BaseModal[None]):
10
11
  """Modal displaying cache statistics in a table."""
@@ -15,13 +16,13 @@ class CacheStatsModal(BaseModal[None]):
15
16
 
16
17
  def compose(self) -> ComposeResult:
17
18
  with Vertical(id="cache-stats-dialog"):
18
- yield Static("Cache Performance Statistics", classes="dialog-title")
19
+ yield Static(StaticText.CACHE_PERFORMANCE_STATISTICS, classes="dialog-title")
19
20
  yield DataTable(id="stats-table")
20
21
  with Vertical():
21
22
  with Horizontal():
22
- yield Button("Refresh", id="refresh-btn", variant="primary")
23
- yield Button("Clear All Caches", id="clear-btn", variant="error")
24
- yield Button("Close", id="close-btn", variant="default")
23
+ yield Button(ButtonLabels.REFRESH, id="refresh-btn", variant="primary")
24
+ yield Button(ButtonLabels.CLEAR_CACHES, id="clear-btn", variant="error")
25
+ yield Button(ButtonLabels.CLOSE, id="close-btn", variant="default")
25
26
 
26
27
  def on_mount(self) -> None:
27
28
  """Setup the table."""
@@ -58,6 +59,6 @@ class CacheStatsModal(BaseModal[None]):
58
59
  elif event.button.id == "clear-btn":
59
60
  self.cache_monitor.clear_all_caches()
60
61
  self._update_table()
61
- self.app.show_success_message("All caches cleared")
62
+ self.app.show_success_message(SuccessMessages.ALL_CACHES_CLEARED)
62
63
  elif event.button.id == "close-btn":
63
64
  self.dismiss()
@@ -0,0 +1,133 @@
1
+ """
2
+ Modal for displaying Host Domain Capabilities in a Tree View with Search.
3
+ """
4
+ import xml.etree.ElementTree as ET
5
+ from textual import on
6
+ from textual.app import ComposeResult
7
+ from textual.widgets import Tree, Button, Label, Input
8
+ from textual.containers import Vertical, Horizontal, Container
9
+ from textual.widgets.tree import TreeNode
10
+
11
+ from .base_modals import BaseModal
12
+ from ..libvirt_utils import get_host_domain_capabilities
13
+ from ..constants import ButtonLabels, StaticText
14
+
15
+ class CapabilitiesTreeModal(BaseModal[None]):
16
+ """Modal to show host capabilities XML as a tree."""
17
+
18
+ def __init__(self, conn):
19
+ super().__init__()
20
+ self.conn = conn
21
+ self.xml_root = None
22
+
23
+ def compose(self) -> ComposeResult:
24
+ with Container(id="capabilities-dialog"):
25
+ yield Label(StaticText.HOST_CAPABILITIES, id="dialog-title")
26
+ yield Input(placeholder="Search...", id="search-input")
27
+ yield Tree(StaticText.CAPABILITIES_TREE_LABEL, id="xml-tree")
28
+ with Horizontal(id="dialog-buttons"):
29
+ yield Button(ButtonLabels.CLOSE, id="close-btn")
30
+
31
+ def on_mount(self) -> None:
32
+ tree = self.query_one("#xml-tree", Tree)
33
+ tree.show_root = False
34
+ self.query_one("#search-input").focus()
35
+
36
+ xml_content = get_host_domain_capabilities(self.conn)
37
+ if not xml_content:
38
+ tree.root.add("No capabilities found or error occurred.")
39
+ return
40
+
41
+ try:
42
+ self.xml_root = ET.fromstring(xml_content)
43
+ self.update_tree("")
44
+ except ET.ParseError as e:
45
+ tree.root.add(f"Error parsing XML: {e}")
46
+
47
+ @on(Input.Changed, "#search-input")
48
+ def on_search_changed(self, event: Input.Changed) -> None:
49
+ self.update_tree(event.value)
50
+
51
+ def update_tree(self, filter_text: str) -> None:
52
+ tree = self.query_one("#xml-tree", Tree)
53
+ tree.clear()
54
+
55
+ if self.xml_root is None:
56
+ return
57
+
58
+ # Add root manually
59
+ self._add_node_recursive(tree.root, self.xml_root, filter_text.strip().lower())
60
+
61
+ if not filter_text:
62
+ # Expand the first level if no filter
63
+ for node in tree.root.children:
64
+ node.expand()
65
+
66
+ def _matches(self, text: str | None, filter_text: str) -> bool:
67
+ if not filter_text:
68
+ return True
69
+ return filter_text in (text or "").lower()
70
+
71
+ def _add_node_recursive(self, parent_node: TreeNode, element: ET.Element, filter_text: str) -> bool:
72
+ """
73
+ Recursively adds nodes. Returns True if the node (or any descendant) matches the filter
74
+ and should be kept.
75
+ """
76
+ # 1. Determine if this specific element matches (tag, text, or any attribute)
77
+ tag_match = self._matches(element.tag, filter_text)
78
+ text_match = self._matches(element.text, filter_text) if element.text and element.text.strip() else False
79
+
80
+ matching_attrs = []
81
+ for k, v in element.attrib.items():
82
+ if self._matches(k, filter_text) or self._matches(v, filter_text):
83
+ matching_attrs.append((k, v))
84
+
85
+ self_match = tag_match or text_match or (len(matching_attrs) > 0)
86
+
87
+ # 2. Create the node optimistically
88
+ label = f"[b]{element.tag}[/b]"
89
+
90
+ # Show text inline if short and no children, OR if it matches
91
+ has_xml_children = len(element) > 0
92
+ text = element.text.strip() if element.text else ""
93
+
94
+ if text and (not has_xml_children or len(text) < 50):
95
+ label += f": {text}"
96
+
97
+ # Expand if filtering, or default closed
98
+ should_expand = bool(filter_text)
99
+ node = parent_node.add(label, expand=should_expand)
100
+
101
+ has_content = False
102
+
103
+ # 3. Add attributes
104
+ attrs_to_check = element.attrib.items()
105
+
106
+ for k, v in attrs_to_check:
107
+ is_attr_match = self._matches(k, filter_text) or self._matches(v, filter_text)
108
+ if not filter_text or is_attr_match or self_match:
109
+ # Highlight match if filter exists
110
+ k_display = k
111
+ v_display = v
112
+ node.add(f"[i]@{k_display}[/i]: {v_display}", allow_expand=False)
113
+ if is_attr_match and filter_text:
114
+ has_content = True
115
+
116
+ if self_match and filter_text:
117
+ has_content = True
118
+
119
+ # 4. Recurse children
120
+ for child in element:
121
+ if self._add_node_recursive(node, child, filter_text):
122
+ has_content = True
123
+
124
+ # 5. Cleanup if nothing matched
125
+ if filter_text and not has_content:
126
+ node.remove()
127
+ return False
128
+
129
+ return True
130
+
131
+ def on_button_pressed(self, event: Button.Pressed) -> None:
132
+ if event.button.id == "close-btn":
133
+ self.dismiss()
@@ -8,7 +8,7 @@ from textual import on
8
8
  from textual.widgets import Label, Button, Input, Checkbox, Static, Select
9
9
 
10
10
  from ..config import save_config, get_user_config_path
11
- from ..constants import AppInfo
11
+ from ..constants import AppInfo, WarningMessages, SuccessMessages, ErrorMessages, StaticText, ButtonLabels
12
12
  from .base_modals import BaseModal
13
13
  from ..utils import check_r_viewer
14
14
 
@@ -22,12 +22,12 @@ class ConfigModal(BaseModal[None]):
22
22
  def compose(self) -> ComposeResult:
23
23
  with Vertical(id="config-dialog"):
24
24
  yield Label(f"{AppInfo.namecase} Configuration", id="config-title")
25
- yield Static(f"(Editing: {get_user_config_path()})", id="config-title-file") #classes="config-path-label")
25
+ yield Static(StaticText.EDITING_CONFIG_PATH.format(get_user_config_path=get_user_config_path()), id="config-title-file") #classes="config-path-label")
26
26
  with ScrollableContainer():
27
27
  # Performance settings
28
- yield Label("Performance", classes="config-section-label")
28
+ yield Label(StaticText.PERFORMANCE, classes="config-section-label")
29
29
  with Horizontal():
30
- yield Label("Stats Interval (seconds):")
30
+ yield Label(StaticText.STATS_INTERVAL)
31
31
  yield Input(
32
32
  value=str(self.config.get("STATS_INTERVAL", 5)),
33
33
  id="stats-interval-input",
@@ -36,14 +36,14 @@ class ConfigModal(BaseModal[None]):
36
36
  )
37
37
 
38
38
  # Logging settings
39
- yield Label("Log File Path:")
39
+ yield Label(StaticText.LOG_FILE_PATH)
40
40
  yield Input(
41
41
  value=self.config.get("LOG_FILE_PATH", ""),
42
42
  id="log-file-path-input",
43
43
  tooltip="Full path to the application log file"
44
44
  )
45
45
 
46
- yield Label("Logging Level:")
46
+ yield Label(StaticText.LOGGING_LEVEL)
47
47
  yield Select(
48
48
  [
49
49
  ("DEBUG", "DEBUG"),
@@ -55,11 +55,11 @@ class ConfigModal(BaseModal[None]):
55
55
  value=self.config.get("LOG_LEVEL", "INFO"),
56
56
  id="log-level-select",
57
57
  prompt="Select a logging level"
58
- )
59
-
58
+ )
59
+
60
60
  # Remote Viewer Settings
61
- yield Label("Remote Viewer")
62
-
61
+ yield Label(StaticText.REMOTE_VIEWER)
62
+
63
63
  viewers = []
64
64
  if shutil.which("virtui-remote-viewer"):
65
65
  viewers.append(("virtui-remote-viewer", "virtui-remote-viewer"))
@@ -71,7 +71,7 @@ class ConfigModal(BaseModal[None]):
71
71
  current_viewer = Select.BLANK
72
72
 
73
73
  if not viewers:
74
- yield Label("No remote viewers found (virt-viewer or virtui-remote-viewer)")
74
+ yield Label(StaticText.NO_REMOTE_VIEWERS_FOUND)
75
75
  else:
76
76
  auto_detected = check_r_viewer()
77
77
  yield Label(f"Select Default Remote Viewer (Auto-detect: {auto_detected}):")
@@ -81,29 +81,30 @@ class ConfigModal(BaseModal[None]):
81
81
  id="remote-viewer-select",
82
82
  allow_blank=True,
83
83
  prompt="Select a viewer"
84
- )
84
+ )
85
85
 
86
- # Web console settings yield Label("Web Console (novnc)", classes="config-section-label")
86
+ # Web console settings
87
+ yield Label(StaticText.WEB_CONSOLE_NOVNC, classes="config-section-label")
87
88
  yield Checkbox(
88
- "Enable remote web console",
89
+ StaticText.ENABLE_REMOTE_WEBCONSOLE,
89
90
  self.config.get("REMOTE_WEBCONSOLE", False),
90
91
  id="remote-webconsole-checkbox",
91
92
  tooltip="Enable secure SSH and noVNC remote viewing for headless server environments"
92
93
  )
93
- yield Label("Websockify Path:")
94
+ yield Label(StaticText.WEBSOCKIFY_PATH)
94
95
  yield Input(
95
96
  value=self.config.get("websockify_path", "/usr/bin/websockify"),
96
97
  id="websockify-path-input",
97
98
  tooltip="Path to the websockify binary"
98
99
  )
99
- yield Label("noVNC Path:")
100
+ yield Label(StaticText.NOVNC_PATH)
100
101
  yield Input(
101
102
  value=self.config.get("novnc_path", "/usr/share/novnc/"),
102
103
  id="novnc-path-input",
103
104
  tooltip="Path to noVNC files"
104
105
  )
105
106
  with Horizontal(classes="port-range-container"):
106
- yield Label("Websockify Port Range:", classes="port-range-label")
107
+ yield Label(StaticText.WEBSOCKIFY_PORT_RANGE, classes="port-range-label")
107
108
  yield Input(
108
109
  value=str(self.config.get("WC_PORT_RANGE_START", 40000)),
109
110
  id="wc-port-start-input",
@@ -120,14 +121,14 @@ class ConfigModal(BaseModal[None]):
120
121
  )
121
122
  with Vertical():
122
123
  with Horizontal():
123
- yield Label("VNC Quality (0-9):")
124
+ yield Label(StaticText.VNC_QUALITY)
124
125
  yield Input(
125
126
  value=str(self.config.get("VNC_QUALITY", 0)),
126
127
  id="vnc-quality-input",
127
128
  type="integer",
128
129
  tooltip="VNC quality setting (0-9)"
129
130
  )
130
- yield Label("VNC Compression (0-9):")
131
+ yield Label(StaticText.VNC_COMPRESSION)
131
132
  yield Input(
132
133
  value=str(self.config.get("VNC_COMPRESSION", 9)),
133
134
  id="vnc-compression-input",
@@ -136,8 +137,8 @@ class ConfigModal(BaseModal[None]):
136
137
  )
137
138
 
138
139
  with Horizontal():
139
- yield Button("Save", variant="primary", id="save-config-btn")
140
- yield Button("Cancel", variant="default", id="cancel-btn")
140
+ yield Button(ButtonLabels.SAVE, variant="primary", id="save-config-btn")
141
+ yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
141
142
 
142
143
  @on(Button.Pressed)
143
144
  def on_button_pressed(self, event: Button.Pressed) -> None:
@@ -160,14 +161,14 @@ class ConfigModal(BaseModal[None]):
160
161
  self.config["REMOTE_VIEWER"] = viewer_select.value
161
162
  else:
162
163
  self.config["REMOTE_VIEWER"] = None
163
- self.app.show_warning_message("No remote viewer selected. Auto-detection will be used.")
164
+ self.app.show_warning_message(WarningMessages.NO_REMOTE_VIEWER_SELECTED)
164
165
  except Exception:
165
166
  pass
166
167
 
167
168
  save_config(self.config)
168
- self.app.show_success_message("Configuration saved successfully.")
169
+ self.app.show_success_message(SuccessMessages.CONFIGURATION_SAVED)
169
170
  self.dismiss(self.config)
170
171
  except Exception as e:
171
- self.app.show_error_message(f"Error saving configuration: {e}")
172
+ self.app.show_error_message(ErrorMessages.ERROR_SAVING_CONFIGURATION_TEMPLATE.format(e=e))
172
173
  elif event.button.id == "cancel-btn":
173
174
  self.dismiss(None)
@@ -5,6 +5,7 @@ from textual.app import ComposeResult
5
5
  from textual.containers import Horizontal, ScrollableContainer, Vertical
6
6
  from textual.widgets import Button, Input, Label, ListView, Select
7
7
 
8
+ from ..constants import ErrorMessages, StaticText, ButtonLabels
8
9
  from .base_modals import BaseModal, ValueListItem
9
10
  from .utils_modals import InfoModal
10
11
 
@@ -18,11 +19,11 @@ class EditCpuModal(BaseModal[str | None]):
18
19
 
19
20
  def compose(self) -> ComposeResult:
20
21
  with Vertical(id="edit-cpu-dialog", classes="edit-cpu-dialog"):
21
- yield Label("Enter new VCPU count")
22
+ yield Label(StaticText.ENTER_NEW_VCPU_COUNT)
22
23
  yield Input(placeholder="e.g., 2", id="cpu-input", type="integer", value=self.current_cpu)
23
24
  with Horizontal():
24
- yield Button("Save", variant="primary", id="save-btn")
25
- yield Button("Cancel", variant="default", id="cancel-btn")
25
+ yield Button(ButtonLabels.SAVE, variant="primary", id="save-btn")
26
+ yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
26
27
 
27
28
  def on_button_pressed(self, event: Button.Pressed) -> None:
28
29
  if event.button.id == "save-btn":
@@ -40,11 +41,11 @@ class EditMemoryModal(BaseModal[str | None]):
40
41
 
41
42
  def compose(self) -> ComposeResult:
42
43
  with Vertical(id="edit-memory-dialog", classes="edit-memory-dialog"):
43
- yield Label("Enter new memory size (MB)")
44
+ yield Label(StaticText.ENTER_NEW_MEMORY_SIZE)
44
45
  yield Input(placeholder="e.g., 2048", id="memory-input", type="integer", value=self.current_memory)
45
46
  with Horizontal():
46
- yield Button("Save", variant="primary", id="save-btn")
47
- yield Button("Cancel", variant="default", id="cancel-btn")
47
+ yield Button(ButtonLabels.SAVE, variant="primary", id="save-btn")
48
+ yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
48
49
 
49
50
  def on_button_pressed(self, event: Button.Pressed) -> None:
50
51
  if event.button.id == "save-btn":
@@ -63,7 +64,7 @@ class SelectMachineTypeModal(BaseModal[str | None]):
63
64
 
64
65
  def compose(self) -> ComposeResult:
65
66
  with Vertical(id="select-machine-type-dialog", classes="select-machine-type-dialog"):
66
- yield Label("Select Machine Type:")
67
+ yield Label(StaticText.SELECT_MACHINE_TYPE)
67
68
  with ScrollableContainer():
68
69
  yield ListView(
69
70
  *[ValueListItem(Label(mt), value=mt) for mt in self.machine_types],
@@ -71,7 +72,7 @@ class SelectMachineTypeModal(BaseModal[str | None]):
71
72
  classes="machine-type-list"
72
73
  )
73
74
  with Horizontal():
74
- yield Button("Cancel", variant="default", id="cancel-btn")
75
+ yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
75
76
 
76
77
  def on_mount(self) -> None:
77
78
  list_view = self.query_one(ListView)
@@ -102,13 +103,13 @@ class EditCpuTuneModal(BaseModal[list[dict] | None]):
102
103
  current_val = "; ".join([f"{p['vcpu']}:{p['cpuset']}" for p in self.current_vcpupin])
103
104
 
104
105
  with Vertical(id="edit-cpu-tune-dialog", classes="edit-cpu-dialog"):
105
- yield Label(f"Enter CPU Pinning (max vcpu: {self.max_vcpus - 1})")
106
- yield Label("Format: 0:0-3; 1:4-7", classes="help-text")
106
+ yield Label(StaticText.ENTER_CPU_PINNING.format(max_vcpus=self.max_vcpus - 1))
107
+ yield Label(StaticText.CPU_PINNING_FORMAT, classes="help-text")
107
108
  yield Input(placeholder="e.g., 0:0-1; 1:2-3", id="cputune-input", value=current_val)
108
109
  with Horizontal():
109
- yield Button("Save", variant="primary", id="save-btn")
110
- yield Button("Cancel", variant="default", id="cancel-btn")
111
- yield Button("Help", variant="default", id="help-btn")
110
+ yield Button(ButtonLabels.SAVE, variant="primary", id="save-btn")
111
+ yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
112
+ yield Button(ButtonLabels.HELP, variant="default", id="help-btn")
112
113
 
113
114
  def on_button_pressed(self, event: Button.Pressed) -> None:
114
115
  if event.button.id == "save-btn":
@@ -135,9 +136,9 @@ class EditCpuTuneModal(BaseModal[list[dict] | None]):
135
136
  vcpupin_list.append({'vcpu': vcpu, 'cpuset': cpuset})
136
137
  self.dismiss(vcpupin_list)
137
138
  except ValueError as e:
138
- self.app.show_error_message(f"Validation error: {e}")
139
+ self.app.show_error_message(ErrorMessages.VALIDATION_ERROR_TEMPLATE.format(error=e))
139
140
  except Exception as e:
140
- self.app.show_error_message(f"Invalid format: {e}")
141
+ self.app.show_error_message(ErrorMessages.INVALID_FORMAT_TEMPLATE.format(error=e))
141
142
  elif event.button.id == "cancel-btn":
142
143
  self.dismiss(None)
143
144
  elif event.button.id == "help-btn":
@@ -173,14 +174,14 @@ class EditNumaTuneModal(BaseModal[dict | None]):
173
174
  modes = [("strict", "strict"), ("preferred", "preferred"), ("interleave", "interleave"), ("None", "None")]
174
175
 
175
176
  with Vertical(id="edit-numatune-dialog", classes="edit-cpu-dialog"):
176
- yield Label("NUMA Memory Mode")
177
+ yield Label(StaticText.NUMA_MEMORY_MODE)
177
178
  yield Select(modes, value=self.current_mode, id="numa-mode-select", allow_blank=False)
178
- yield Label("Nodeset")
179
+ yield Label(StaticText.NODESET)
179
180
  yield Input(placeholder="e.g., 0-1", id="numa-nodeset-input", value=self.current_nodeset)
180
181
  with Horizontal():
181
- yield Button("Save", variant="primary", id="save-btn")
182
- yield Button("Cancel", variant="default", id="cancel-btn")
183
- yield Button("Help", variant="default", id="help-btn")
182
+ yield Button(ButtonLabels.SAVE, variant="primary", id="save-btn")
183
+ yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
184
+ yield Button(ButtonLabels.HELP, variant="default", id="help-btn")
184
185
 
185
186
  def on_button_pressed(self, event: Button.Pressed) -> None:
186
187
  if event.button.id == "save-btn":
@@ -194,7 +195,7 @@ class EditNumaTuneModal(BaseModal[dict | None]):
194
195
  # Validate nodeset syntax
195
196
  if nodeset:
196
197
  if not all(c.isdigit() or c in ',-' for c in nodeset):
197
- self.app.show_error_message(f"Invalid nodeset syntax: {nodeset}")
198
+ self.app.show_error_message(ErrorMessages.INVALID_NODESET_SYNTAX_TEMPLATE.format(nodeset=nodeset))
198
199
  return
199
200
 
200
201
  self.dismiss({'mode': mode, 'nodeset': nodeset})
@@ -3,6 +3,7 @@ from textual.app import ComposeResult
3
3
  from textual.screen import ModalScreen
4
4
  from textual.widgets import Button, Static, Select, Checkbox
5
5
  from textual.containers import Vertical
6
+ from ..constants import StaticText, ButtonLabels
6
7
 
7
8
  class CustomMigrationModal(ModalScreen[dict | None]):
8
9
  """A modal to confirm custom migration actions."""
@@ -14,12 +15,12 @@ class CustomMigrationModal(ModalScreen[dict | None]):
14
15
 
15
16
  def compose(self) -> ComposeResult:
16
17
  with Vertical(id="custom-migration-dialog"):
17
- yield Static("[bold]Custom Migration Plan[/bold]")
18
+ yield Static(StaticText.CUSTOM_MIGRATION_PLAN)
18
19
 
19
20
  for i, action in enumerate(self.actions):
20
21
  if action["type"] == "move_volume":
21
- yield Static(f"Disk: [b]{action['volume_name']}[/b]")
22
- yield Static(f" Source Pool: {action['source_pool']}")
22
+ yield Static(StaticText.DISK_VOLUME_NAME.format(volume_name=action['volume_name']))
23
+ yield Static(StaticText.SOURCE_POOL.format(source_pool=action['source_pool']))
23
24
  dest_pools = action.get("dest_pools", [])
24
25
  if dest_pools:
25
26
  yield Select(
@@ -28,16 +29,16 @@ class CustomMigrationModal(ModalScreen[dict | None]):
28
29
  id=f"pool-select-{i}"
29
30
  )
30
31
  else:
31
- yield Static(" No destination pools available.")
32
+ yield Static(StaticText.NO_DESTINATION_POOLS)
32
33
  elif action["type"] == "manual_copy":
33
- yield Static(f"Disk: [b]{action['disk_path']}[/b]")
34
- yield Static(f" Action: {action['message']}")
34
+ yield Static(StaticText.DISK_PATH.format(disk_path=action['disk_path']))
35
+ yield Static(StaticText.ACTION_MESSAGE.format(message=action['message']))
35
36
 
36
- yield Checkbox("Undefine source VM", value=True, id="undefine-checkbox")
37
+ yield Checkbox(StaticText.UNDEFINE_SOURCE_VM, value=True, id="undefine-checkbox")
37
38
 
38
39
  with Vertical(classes="modal-buttons"):
39
- yield Button("Confirm", variant="primary", id="confirm")
40
- yield Button("Cancel", variant="default", id="cancel")
40
+ yield Button(ButtonLabels.CONFIRM, variant="primary", id="confirm")
41
+ yield Button(ButtonLabels.CANCEL, variant="default", id="cancel")
41
42
 
42
43
  def on_button_pressed(self, event: Button.Pressed) -> None:
43
44
  if event.button.id == "confirm":