virtui-manager 1.1.5__py3-none-any.whl → 1.3.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 (57) hide show
  1. {virtui_manager-1.1.5.dist-info → virtui_manager-1.3.0.dist-info}/METADATA +1 -1
  2. virtui_manager-1.3.0.dist-info/RECORD +73 -0
  3. vmanager/constants.py +737 -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.po +3012 -0
  9. vmanager/locales/fr/LC_MESSAGES/virtui-manager.mo +0 -0
  10. vmanager/locales/fr/LC_MESSAGES/virtui-manager.po +3124 -0
  11. vmanager/locales/it/LC_MESSAGES/virtui-manager.po +3012 -0
  12. vmanager/locales/virtui-manager.pot +3012 -0
  13. vmanager/modals/bulk_modals.py +13 -12
  14. vmanager/modals/cache_stats_modal.py +6 -5
  15. vmanager/modals/capabilities_modal.py +133 -0
  16. vmanager/modals/config_modal.py +25 -24
  17. vmanager/modals/cpu_mem_pc_modals.py +22 -21
  18. vmanager/modals/custom_migration_modal.py +10 -9
  19. vmanager/modals/disk_pool_modals.py +60 -59
  20. vmanager/modals/host_dashboard_modal.py +137 -0
  21. vmanager/modals/howto_disk_modal.py +13 -72
  22. vmanager/modals/howto_network_modal.py +13 -39
  23. vmanager/modals/howto_overlay_modal.py +13 -52
  24. vmanager/modals/howto_ssh_modal.py +12 -67
  25. vmanager/modals/howto_virtiofs_modal.py +13 -64
  26. vmanager/modals/input_modals.py +11 -10
  27. vmanager/modals/log_modal.py +2 -1
  28. vmanager/modals/migration_modals.py +20 -18
  29. vmanager/modals/network_modals.py +45 -36
  30. vmanager/modals/provisioning_modals.py +56 -56
  31. vmanager/modals/select_server_modals.py +8 -7
  32. vmanager/modals/selection_modals.py +7 -6
  33. vmanager/modals/server_modals.py +24 -23
  34. vmanager/modals/server_prefs_modals.py +103 -87
  35. vmanager/modals/utils_modals.py +10 -9
  36. vmanager/modals/virsh_modals.py +3 -2
  37. vmanager/modals/virtiofs_modals.py +6 -5
  38. vmanager/modals/vm_type_info_modal.py +2 -1
  39. vmanager/modals/vmanager_modals.py +19 -19
  40. vmanager/modals/vmcard_dialog.py +57 -57
  41. vmanager/modals/vmdetails_modals.py +115 -123
  42. vmanager/modals/xml_modals.py +3 -2
  43. vmanager/network_manager.py +4 -1
  44. vmanager/storage_manager.py +182 -42
  45. vmanager/utils.py +39 -6
  46. vmanager/vm_actions.py +28 -24
  47. vmanager/vm_queries.py +67 -25
  48. vmanager/vm_service.py +8 -5
  49. vmanager/vmanager.css +46 -0
  50. vmanager/vmanager.py +178 -112
  51. vmanager/vmcard.py +161 -159
  52. vmanager/webconsole_manager.py +21 -21
  53. virtui_manager-1.1.5.dist-info/RECORD +0 -65
  54. {virtui_manager-1.1.5.dist-info → virtui_manager-1.3.0.dist-info}/WHEEL +0 -0
  55. {virtui_manager-1.1.5.dist-info → virtui_manager-1.3.0.dist-info}/entry_points.txt +0 -0
  56. {virtui_manager-1.1.5.dist-info → virtui_manager-1.3.0.dist-info}/licenses/LICENSE +0 -0
  57. {virtui_manager-1.1.5.dist-info → virtui_manager-1.3.0.dist-info}/top_level.txt +0 -0
@@ -8,6 +8,7 @@ from textual.widgets.text_area import LanguageDoesNotExist
8
8
  from textual.containers import Vertical, Horizontal, ScrollableContainer
9
9
  from textual import on
10
10
 
11
+ from ..constants import ErrorMessages, SuccessMessages, ButtonLabels, StaticText
11
12
  from .base_modals import BaseModal, BaseDialog
12
13
  from ..network_manager import (
13
14
  create_network, get_host_network_interfaces, get_existing_subnets
@@ -35,7 +36,7 @@ class AddEditNetworkInterfaceModal(BaseDialog[dict | None]):
35
36
  if self.is_edit and self.interface_info:
36
37
  network_value = self.interface_info.get("network")
37
38
  if network_value not in self.networks:
38
- self.app.show_error_message(f"Network '{network_value}' not found. Please select an available network.")
39
+ self.app.show_error_message(ErrorMessages.NETWORK_NOT_FOUND_TEMPLATE.format(network=network_value))
39
40
  network_value = self.networks[0] if self.networks else None # Set to first available network if any, otherwise None
40
41
  model_value = self.interface_info.get("model", "virtio")
41
42
  mac_value = self.interface_info.get("mac", "")
@@ -43,7 +44,7 @@ class AddEditNetworkInterfaceModal(BaseDialog[dict | None]):
43
44
  network_value = self.networks[0]
44
45
 
45
46
  with Vertical(id="add-edit-network-dialog"):
46
- yield Label("Select network and model")
47
+ yield Label(StaticText.SELECT_NETWORK_AND_MODEL)
47
48
 
48
49
  if self.networks:
49
50
  yield Select(network_options, id="network-select", prompt="Select a network", value=network_value)
@@ -60,8 +61,8 @@ class AddEditNetworkInterfaceModal(BaseDialog[dict | None]):
60
61
  )
61
62
 
62
63
  with Horizontal(id="dialog-buttons"):
63
- yield Button("Save" if self.is_edit else "Add", variant="success", id="save")
64
- yield Button("Cancel", variant="error", id="cancel")
64
+ yield Button(ButtonLabels.SAVE if self.is_edit else ButtonLabels.ADD, variant="success", id="save")
65
+ yield Button(ButtonLabels.CANCEL, variant="error", id="cancel")
65
66
 
66
67
  def on_button_pressed(self, event: Button.Pressed) -> None:
67
68
  if event.button.id == "save":
@@ -72,7 +73,7 @@ class AddEditNetworkInterfaceModal(BaseDialog[dict | None]):
72
73
  new_model = model_select.value
73
74
 
74
75
  if new_network is Select.BLANK:
75
- self.app.show_error_message("Please select a network.")
76
+ self.app.show_error_message(ErrorMessages.SELECT_NETWORK)
76
77
  return
77
78
 
78
79
  result = {"network": new_network, "model": new_model}
@@ -94,7 +95,7 @@ class AddEditNetworkModal(BaseModal[None]):
94
95
 
95
96
  def compose(self) -> ComposeResult:
96
97
  title = "Edit Network" if self.is_edit else "Create New Network"
97
- button_label = "Save Changes" if self.is_edit else "Create Network"
98
+ button_label = ButtonLabels.SAVE_CHANGES if self.is_edit else ButtonLabels.CREATE_NETWORK
98
99
 
99
100
  name_val = ""
100
101
  forward_mode = "nat"
@@ -137,7 +138,6 @@ class AddEditNetworkModal(BaseModal[None]):
137
138
  dhcp_end_val = "192.168.11.30"
138
139
 
139
140
 
140
-
141
141
  with Vertical(id="create-network-dialog"):
142
142
  yield Label(title, id="create-network-title")
143
143
 
@@ -150,8 +150,8 @@ class AddEditNetworkModal(BaseModal[None]):
150
150
  disabled=self.is_edit
151
151
  )
152
152
  with RadioSet(id="type-network", classes="type-network-radioset"):
153
- yield RadioButton("Nat network", id="type-network-nat", value=(forward_mode == "nat"))
154
- yield RadioButton("Routed network", id="type-network-routed", value=(forward_mode == "route"))
153
+ yield RadioButton(StaticText.NAT_NETWORK, id="type-network-nat", value=(forward_mode == "nat"))
154
+ yield RadioButton(StaticText.ROUTED_NETWORK, id="type-network-routed", value=(forward_mode == "route"))
155
155
  yield Select(
156
156
  [("Loading...", "")],
157
157
  prompt="Select Forward Interface",
@@ -160,33 +160,33 @@ class AddEditNetworkModal(BaseModal[None]):
160
160
  disabled=True
161
161
  )
162
162
  yield Input(
163
- placeholder="IPv4 Network (e.g., 192.168.100.0/24)", id="net-ip-input", value=ip_val
163
+ placeholder=StaticText.IPV4_NETWORK_EXAMPLE, id="net-ip-input", value=ip_val
164
164
  )
165
- yield Checkbox("Enable DHCPv4", id="dhcp-checkbox", value=dhcp_val)
165
+ yield Checkbox(StaticText.ENABLE_DHCPV4, id="dhcp-checkbox", value=dhcp_val)
166
166
  with Vertical(id="dhcp-inputs-horizontal"):
167
167
  dhcp_options_classes = "" if dhcp_val else "hidden"
168
168
  with Horizontal(id="dhcp-options", classes=dhcp_options_classes):
169
169
  yield Input(
170
- placeholder="DHCP Start (e.192.168.100.100)",
170
+ placeholder=StaticText.DHCP_START_EXAMPLE,
171
171
  id="dhcp-start-input",
172
172
  classes="dhcp-input",
173
173
  value=dhcp_start_val
174
174
  )
175
175
  yield Input(
176
- placeholder="DHCP End (e.g., 192.168.100.254)",
176
+ placeholder=StaticText.DHCP_END_EXAMPLE,
177
177
  id="dhcp-end-input",
178
178
  classes="dhcp-input",
179
179
  value=dhcp_end_val
180
180
  )
181
181
  with RadioSet(id="dns-domain-radioset", classes="dns-domain-radioset"):
182
182
  yield RadioButton(
183
- "Use Network Name for DNS Domain", id="dns-use-net-name", value=not use_custom_domain
183
+ StaticText.USE_NETWORK_NAME_FOR_DNS, id="dns-use-net-name", value=not use_custom_domain
184
184
  )
185
- yield RadioButton("Use Custom DNS Domain", id="dns-use-custom", value=use_custom_domain)
185
+ yield RadioButton(StaticText.USE_CUSTOM_DNS_DOMAIN, id="dns-use-custom", value=use_custom_domain)
186
186
 
187
187
  custom_domain_classes = "hidden" if not use_custom_domain else ""
188
188
  yield Input(
189
- placeholder="Custom DNS Domain",
189
+ placeholder=StaticText.CUSTOM_DNS_DOMAIN,
190
190
  id="dns-custom-domain-input",
191
191
  value=domain_name,
192
192
  classes=custom_domain_classes
@@ -195,9 +195,9 @@ class AddEditNetworkModal(BaseModal[None]):
195
195
  with Horizontal(classes="action-buttons"):
196
196
  yield Button(
197
197
  button_label, variant="primary", id="create-net-btn", classes="create-net-btn"
198
- )
199
- yield Button("Close", variant="default", id="close-btn", classes="close-button")
200
-
198
+ )
199
+ yield Button(ButtonLabels.CLOSE, variant="default", id="close-btn", classes="close-button")
200
+
201
201
  def on_mount(self) -> None:
202
202
  """Called when the modal is mounted to populate network interfaces."""
203
203
  self.run_worker(self.populate_interfaces, thread=True)
@@ -225,7 +225,7 @@ class AddEditNetworkModal(BaseModal[None]):
225
225
  else:
226
226
  select.clear()
227
227
  self.app.show_error_message(
228
- f"Warning: Forward device '{forward_dev}' not found on host."
228
+ ErrorMessages.FORWARD_DEVICE_NOT_FOUND_TEMPLATE.format(device=forward_dev)
229
229
  )
230
230
  else:
231
231
  select.clear()
@@ -234,7 +234,7 @@ class AddEditNetworkModal(BaseModal[None]):
234
234
  except Exception as e:
235
235
  self.app.call_from_thread(
236
236
  self.app.show_error_message,
237
- f"Error getting host interfaces: {e}"
237
+ ErrorMessages.ERROR_GETTING_HOST_INTERFACES_TEMPLATE.format(error=e)
238
238
  )
239
239
 
240
240
  @on(Checkbox.Changed, "#dhcp-checkbox")
@@ -270,13 +270,13 @@ class AddEditNetworkModal(BaseModal[None]):
270
270
  dhcp_end = self.query_one("#dhcp-end-input", Input).value
271
271
 
272
272
  domain_radio = self.query_one("#dns-domain-radioset", RadioSet).pressed_button.id
273
-
273
+
274
274
  try:
275
275
  name, name_modified = _sanitize_input(name_raw)
276
276
  if name_modified:
277
- self.app.show_success_message(f"Network name sanitized: '{name_raw}' changed to '{name}'")
277
+ self.app.show_success_message(SuccessMessages.INPUT_SANITIZED.format(original_input=name_raw, sanitized_input=name))
278
278
  except ValueError as e:
279
- self.app.show_error_message(f"Invalid Network Name: {e}")
279
+ self.app.show_error_message(ErrorMessages.INVALID_NETWORK_NAME_TEMPLATE.format(error=e))
280
280
  return
281
281
 
282
282
  domain_name_raw = self.query_one("#dns-custom-domain-input", Input).value
@@ -285,13 +285,13 @@ class AddEditNetworkModal(BaseModal[None]):
285
285
  try:
286
286
  domain_name, domain_name_modified = _sanitize_domain_name(domain_name_raw)
287
287
  if domain_name_modified:
288
- self.app.show_success_message(f"Custom DNS Domain sanitized: '{domain_name_raw}' changed to '{domain_name}'")
288
+ self.app.show_success_message(SuccessMessages.INPUT_SANITIZED.format(original_input=domain_name_raw, sanitized_input=domain_name))
289
289
  except ValueError as e:
290
- self.app.show_error_message(f"Invalid Custom DNS Domain: {e}")
290
+ self.app.show_error_message(ErrorMessages.INVALID_CUSTOM_DNS_DOMAIN_TEMPLATE.format(error=e))
291
291
  return
292
292
 
293
293
  if not name:
294
- self.app.show_error_message("Network Name cannot be empty.")
294
+ self.app.show_error_message(ErrorMessages.NETWORK_NAME_REQUIRED)
295
295
  return
296
296
 
297
297
  if ip:
@@ -302,16 +302,16 @@ class AddEditNetworkModal(BaseModal[None]):
302
302
  dhcp_start_ip = ipaddress.ip_address(dhcp_start)
303
303
  dhcp_end_ip = ipaddress.ip_address(dhcp_end)
304
304
  if dhcp_start_ip not in ip_network or dhcp_end_ip not in ip_network:
305
- self.app.show_error_message(f"DHCP IPs are not in the network {ip_network}")
305
+ self.app.show_error_message(ErrorMessages.DHCP_IPS_NOT_IN_NETWORK_TEMPLATE.format(network=ip_network))
306
306
  return
307
307
  if dhcp_start_ip >= dhcp_end_ip:
308
- self.app.show_error_message("DHCP start IP must be before the end IP.")
308
+ self.app.show_error_message(ErrorMessages.DHCP_START_BEFORE_END)
309
309
  return
310
310
  except ValueError as e:
311
- self.app.show_error_message(f"Invalid IP address or network: {e}")
311
+ self.app.show_error_message(ErrorMessages.INVALID_IP_OR_NETWORK_TEMPLATE.format(error=e))
312
312
  return
313
313
  elif dhcp:
314
- self.app.show_error_message("DHCP cannot be enabled without an IP network.")
314
+ self.app.show_error_message(ErrorMessages.DHCP_REQUIRES_IP)
315
315
  return
316
316
 
317
317
  def do_create_or_update_network():
@@ -344,20 +344,29 @@ class AddEditNetworkModal(BaseModal[None]):
344
344
  if ip_network.overlaps(existing_subnet):
345
345
  self.app.call_from_thread(
346
346
  self.app.show_error_message,
347
- f"Subnet {ip_network} overlaps with an existing network."
347
+ ErrorMessages.SUBNET_OVERLAPS_TEMPLATE.format(subnet=ip_network)
348
348
  )
349
349
  return
350
350
 
351
351
  uuid = self.network_info.get('uuid') if self.is_edit and self.network_info else None
352
352
  create_network(self.conn, name, typenet, forward, ip, dhcp, dhcp_start, dhcp_end, domain_name, uuid=uuid)
353
353
 
354
- message = f"Network {name} {'updated' if self.is_edit else 'created'} successfully."
354
+ if self.is_edit:
355
+ message = SuccessMessages.NETWORK_UPDATED_TEMPLATE.format(name=name)
356
+ else:
357
+ message = SuccessMessages.NETWORK_CREATED_TEMPLATE.format(name=name)
358
+
355
359
  self.app.call_from_thread(self.app.show_success_message, message)
356
360
  self.app.call_from_thread(self.dismiss, True)
357
361
  except Exception as e:
362
+ if self.is_edit:
363
+ err_msg = ErrorMessages.ERROR_UPDATING_NETWORK_TEMPLATE.format(error=e)
364
+ else:
365
+ err_msg = ErrorMessages.ERROR_CREATING_NETWORK_TEMPLATE.format(error=e)
366
+
358
367
  self.app.call_from_thread(
359
368
  self.app.show_error_message,
360
- f"Error {'updating' if self.is_edit else 'creating'} network: {e}"
369
+ err_msg
361
370
  )
362
371
 
363
372
  self.app.worker_manager.run(
@@ -374,7 +383,7 @@ class NetworkXMLModal(BaseModal[None]):
374
383
 
375
384
  def compose(self) -> ComposeResult:
376
385
  with Vertical(id="network-detail-dialog"):
377
- yield Label(f"Network Details: {self.network_name}", id="title")
386
+ yield Label(StaticText.NETWORK_DETAILS.format(network_name=self.network_name), id="title")
378
387
  with ScrollableContainer():
379
388
  text_area = TextArea(self.network_xml, read_only=True)
380
389
  try:
@@ -384,7 +393,7 @@ class NetworkXMLModal(BaseModal[None]):
384
393
  text_area.styles.height = "auto"
385
394
  yield text_area
386
395
  with Horizontal():
387
- yield Button("Close", variant="default", id="close-btn", classes="close-btn")
396
+ yield Button(ButtonLabels.CLOSE, variant="default", id="close-btn", classes="close-btn")
388
397
 
389
398
  def on_button_pressed(self, event: Button.Pressed) -> None:
390
399
  if event.button.id == "close-btn":
@@ -12,7 +12,7 @@ from textual import on, work
12
12
 
13
13
  import libvirt
14
14
  from ..config import load_config
15
- from ..constants import AppInfo
15
+ from ..constants import AppInfo, ErrorMessages, SuccessMessages, ButtonLabels, StaticText
16
16
  from ..vm_provisioner import VMProvisioner, VMType, OpenSUSEDistro
17
17
  from ..storage_manager import list_storage_pools
18
18
  from ..vm_service import VMService
@@ -43,16 +43,16 @@ class InstallVMModal(BaseModal[str | None]):
43
43
  default_pool = 'default' if any(p[0] == 'default' for p in active_pools) else (active_pools[0][1] if active_pools else None)
44
44
 
45
45
  with ScrollableContainer(id="install-dialog"):
46
- yield Label(f"Install OpenSUSE VM on {self.uri}", classes="title")
47
- yield Label("VM Name:", classes="label")
46
+ yield Label(StaticText.INSTALL_OPENSUSE_VM.format(uri=self.uri), classes="title")
47
+ yield Label(StaticText.VM_NAME, classes="label")
48
48
  yield Input(placeholder="my-new-vm", id="vm-name")
49
49
 
50
- yield Label("VM Type:", classes="label")
50
+ yield Label(StaticText.VM_TYPE, classes="label")
51
51
  with Horizontal(classes="label-row"):
52
52
  yield Select([(t.value, t) for t in VMType], value=VMType.DESKTOP, id="vm-type", allow_blank=False)
53
- yield Button("Info", id="vm-type-info-btn", variant="primary")
53
+ yield Button(ButtonLabels.INFO, id="vm-type-info-btn", variant="primary")
54
54
 
55
- yield Label("Distribution:", classes="label")
55
+ yield Label(StaticText.DISTRIBUTION, classes="label")
56
56
  distro_options = [(d.value, d) for d in OpenSUSEDistro]
57
57
  distro_options.insert(0, ("Cached ISOs", "cached"))
58
58
  custom_repos = self.provisioner.get_custom_repos()
@@ -70,58 +70,58 @@ class InstallVMModal(BaseModal[str | None]):
70
70
 
71
71
  # Container for ISO selection (Repo)
72
72
  with Vertical(id="repo-iso-container"):
73
- yield Label("ISO Image (Repo):", classes="label")
73
+ yield Label(StaticText.ISO_IMAGE_REPO, classes="label")
74
74
  config = load_config()
75
75
  iso_path = Path(config.get('ISO_DOWNLOAD_PATH', str(Path.home() / ".cache" / AppInfo.name / "isos")))
76
- yield Label(f"ISOs will be downloaded to: {iso_path}", classes="info-text", id="iso-path-label")
76
+ yield Label(StaticText.ISOS_DOWNLOAD_PATH.format(iso_path=iso_path), classes="info-text", id="iso-path-label")
77
77
  yield Select([], prompt="Select ISO...", id="iso-select", disabled=True)
78
78
 
79
79
  # Container for Custom ISO
80
80
  with Vertical(id="custom-iso-container"):
81
- yield Label("Custom ISO (Local Path):", classes="label")
81
+ yield Label(StaticText.CUSTOM_ISO_LOCAL_PATH, classes="label")
82
82
  with Horizontal(classes="input-row"):
83
83
  yield Input(placeholder="/path/to/local.iso", id="custom-iso-path", classes="path-input")
84
- yield Button("Browse", id="browse-iso-btn")
84
+ yield Button(ButtonLabels.BROWSE, id="browse-iso-btn")
85
85
 
86
86
  with Vertical(id="checksum-container"):
87
- yield Checkbox("Validate Checksum", id="validate-checksum", value=False)
87
+ yield Checkbox(StaticText.VALIDATE_CHECKSUM, id="validate-checksum", value=False)
88
88
  yield Input(placeholder="SHA256 Checksum (Optional)", id="checksum-input", disabled=True)
89
- yield Label("", id="checksum-status", classes="status-text")
90
-
89
+ yield Label(StaticText.EMPTY_LABEL, id="checksum-status", classes="status-text")
90
+
91
91
  # Container for ISO selection from Storage Pools
92
92
  with Vertical(id="pool-iso-container"):
93
- yield Label("Select Storage Pool:", classes="label")
93
+ yield Label(StaticText.SELECT_STORAGE_POOL, classes="label")
94
94
  yield Select(active_pools, prompt="Select Pool...", id="storage-pool-select", allow_blank=False)
95
- yield Label("Select ISO Volume:", classes="label")
95
+ yield Label(StaticText.SELECT_ISO_VOLUME, classes="label")
96
96
  yield Select([], prompt="Select ISO Volume...", id="iso-volume-select", disabled=True)
97
97
 
98
- yield Label("Storage Pool:", id="vminstall-storage-label")
98
+ yield Label(StaticText.STORAGE_POOL, id="vminstall-storage-label")
99
99
  yield Select(active_pools, value=default_pool, id="pool", allow_blank=False)
100
100
  with Collapsible(title="Expert Mode", id="expert-mode-collapsible"):
101
101
  with Horizontal(id="expert-mode"):
102
102
  with Vertical(id="expert-mem"):
103
- yield Label(" Memory (GB)", classes="label")
103
+ yield Label(StaticText.MEMORY_GB_LABEL, classes="label")
104
104
  yield Input("4", id="memory-input", type="integer")
105
105
  with Vertical(id="expert-cpu"):
106
- yield Label(" CPUs", classes="label")
106
+ yield Label(StaticText.CPUS_LABEL, classes="label")
107
107
  yield Input("2", id="cpu-input", type="integer")
108
108
  with Vertical(id="expert-disk-size"):
109
- yield Label(" Disk Size(GB)", classes="label")
109
+ yield Label(StaticText.DISK_SIZE_GB_LABEL, classes="label")
110
110
  yield Input("8", id="disk-size-input", type="integer")
111
111
  with Vertical(id="expert-disk-format"):
112
- yield Label(" Disk Format", classes="label")
112
+ yield Label(StaticText.DISK_FORMAT_LABEL, classes="label")
113
113
  yield Select([("Qcow2", "qcow2"), ("Raw", "raw")], value="qcow2", id="disk-format")
114
114
  with Vertical(id="expert-firmware"):
115
- yield Label(" Firmware", classes="label")
116
- yield Checkbox("UEFI", id="boot-uefi-checkbox", value=True, tooltip="Unchecked means legacy boot")
115
+ yield Label(StaticText.FIRMWARE_LABEL, classes="label")
116
+ yield Checkbox(StaticText.UEFI, id="boot-uefi-checkbox", value=True, tooltip="Unchecked means legacy boot")
117
117
 
118
- yield Checkbox("Configure before install", id="configure-before-install-checkbox", value=False, tooltip="Show VM configuration before starting")
118
+ yield Checkbox(StaticText.CONFIGURE_BEFORE_INSTALL, id="configure-before-install-checkbox", value=False, tooltip="Show VM configuration before starting")
119
119
  yield ProgressBar(total=100, show_eta=False, id="progress-bar")
120
- yield Label("", id="status-label")
120
+ yield Label(StaticText.EMPTY_LABEL, id="status-label")
121
121
 
122
122
  with Horizontal(classes="buttons"):
123
- yield Button("Install", variant="primary", id="install-btn", disabled=True)
124
- yield Button("Cancel", variant="default", id="cancel-btn")
123
+ yield Button(ButtonLabels.INSTALL, variant="primary", id="install-btn", disabled=True)
124
+ yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
125
125
 
126
126
  def on_mount(self):
127
127
  """Called when modal is mounted."""
@@ -270,7 +270,7 @@ class InstallVMModal(BaseModal[str | None]):
270
270
  self.app.call_from_thread(update_iso_volume_select)
271
271
 
272
272
  except Exception as e:
273
- self.app.call_from_thread(self.app.show_error_message, f"Failed to fetch ISO volumes from {pool_name}: {e}")
273
+ self.app.call_from_thread(self.app.show_error_message, ErrorMessages.FAILED_TO_FETCH_ISO_VOLUMES_TEMPLATE.format(pool_name=pool_name, error=e))
274
274
  self.app.call_from_thread(self._update_iso_status, "Error fetching volumes", False)
275
275
  self.app.call_from_thread(iso_volume_select.clear)
276
276
  self.app.call_from_thread(lambda: setattr(iso_volume_select, 'disabled', True))
@@ -311,7 +311,7 @@ class InstallVMModal(BaseModal[str | None]):
311
311
  self.app.call_from_thread(update_select)
312
312
 
313
313
  except Exception as e:
314
- self.app.call_from_thread(self.app.show_error_message, f"Failed to fetch ISOs: {e}")
314
+ self.app.call_from_thread(self.app.show_error_message, ErrorMessages.FAILED_TO_FETCH_ISOS_TEMPLATE.format(error=e))
315
315
  self.app.call_from_thread(self._update_iso_status, "Error fetching ISOs", False)
316
316
 
317
317
  def _update_iso_status(self, message, loading):
@@ -385,24 +385,24 @@ class InstallVMModal(BaseModal[str | None]):
385
385
  return
386
386
 
387
387
  if was_modified:
388
- self.app.show_quick_message(f"VM name sanitized: '{vm_name_raw}' -> '{vm_name}'")
388
+ self.app.show_quick_message(SuccessMessages.VM_NAME_SANITIZED_TEMPLATE.format(original=vm_name_raw, sanitized=vm_name))
389
389
  self.query_one("#vm-name", Input).value = vm_name
390
390
 
391
391
  if not vm_name:
392
- self.app.show_error_message("VM name cannot be empty.")
392
+ self.app.show_error_message(ErrorMessages.VM_NAME_CANNOT_BE_EMPTY)
393
393
  return
394
394
 
395
395
  # 2. Check if VM exists
396
396
  try:
397
397
  self.conn.lookupByName(vm_name)
398
- self.app.show_error_message(f"A VM with the name '{vm_name}' already exists. Please choose a different name.")
398
+ self.app.show_error_message(ErrorMessages.VM_NAME_ALREADY_EXISTS_TEMPLATE.format(vm_name=vm_name))
399
399
  return
400
400
  except libvirt.libvirtError as e:
401
401
  if e.get_error_code() != libvirt.VIR_ERR_NO_DOMAIN:
402
- self.app.show_error_message(f"Error checking VM name: {e}")
402
+ self.app.show_error_message(ErrorMessages.ERROR_CHECKING_VM_NAME_TEMPLATE.format(error=e))
403
403
  return
404
404
  except Exception as e:
405
- self.app.show_error_message(f"An unexpected error occurred: {e}")
405
+ self.app.show_error_message(ErrorMessages.UNEXPECTED_ERROR_OCCURRED_TEMPLATE.format(error=e))
406
406
  return
407
407
 
408
408
  vm_type = self.query_one("#vm-type", Select).value
@@ -412,7 +412,7 @@ class InstallVMModal(BaseModal[str | None]):
412
412
 
413
413
  # Validate storage pool
414
414
  if not pool_name or pool_name == Select.BLANK:
415
- self.app.show_error_message("Please select a valid storage pool.")
415
+ self.app.show_error_message(ErrorMessages.PLEASE_SELECT_VALID_STORAGE_POOL)
416
416
  return
417
417
 
418
418
  iso_url = None
@@ -428,11 +428,11 @@ class InstallVMModal(BaseModal[str | None]):
428
428
  elif distro == "pool_volumes":
429
429
  iso_url = self.query_one("#iso-volume-select", Select).value
430
430
  if not iso_url or iso_url == Select.BLANK:
431
- self.app.show_error_message("Please select a valid ISO volume from the storage pool.")
431
+ self.app.show_error_message(ErrorMessages.SELECT_VALID_ISO_VOLUME)
432
432
  return
433
433
  # Validate that the volume path exists and is accessible
434
434
  if not os.path.exists(iso_url):
435
- self.app.show_error_message(f"Selected ISO volume does not exist: {iso_url}")
435
+ self.app.show_error_message(ErrorMessages.ISO_VOLUME_NOT_FOUND_TEMPLATE.format(iso_url=iso_url))
436
436
  return
437
437
  else:
438
438
  iso_url = self.query_one("#iso-select", Select).value
@@ -446,27 +446,27 @@ class InstallVMModal(BaseModal[str | None]):
446
446
  disk_format = self.query_one("#disk-format", Select).value
447
447
  boot_uefi = self.query_one("#boot-uefi-checkbox", Checkbox).value
448
448
  except ValueError:
449
- self.app.show_error_message("Invalid input for expert settings. Using defaults.")
449
+ self.app.show_error_message(ErrorMessages.INVALID_EXPERT_SETTINGS)
450
450
  return
451
451
 
452
452
  # Validate expert mode inputs
453
453
  if memory_gb < 1 or memory_gb > 8192:
454
- self.app.show_error_message("Memory must be between 1 and 8192 GB.")
454
+ self.app.show_error_message(ErrorMessages.MEMORY_RANGE_ERROR)
455
455
  return
456
456
  if vcpu < 1 or vcpu > 768:
457
- self.app.show_error_message("CPU count must be between 1 and 768.")
457
+ self.app.show_error_message(ErrorMessages.CPU_RANGE_ERROR)
458
458
  return
459
459
  if disk_size < 1 or disk_size > 10000:
460
- self.app.show_error_message("Disk size must be between 1 and 10000 GB.")
460
+ self.app.show_error_message(ErrorMessages.DISK_SIZE_RANGE_ERROR)
461
461
  return
462
462
 
463
463
  try:
464
464
  pool = self.conn.storagePoolLookupByName(pool_name)
465
465
  if not pool.isActive():
466
- self.app.show_error_message(f"Storage pool '{pool_name}' is not active. Please activate it first.")
466
+ self.app.show_error_message(ErrorMessages.STORAGE_POOL_NOT_ACTIVE_TEMPLATE.format(pool_name=pool_name))
467
467
  return
468
468
  except Exception as e:
469
- self.app.show_error_message(f"Error accessing storage pool '{pool_name}': {e}")
469
+ self.app.show_error_message(ErrorMessages.ERROR_ACCESSING_STORAGE_POOL_TEMPLATE.format(pool_name=pool_name, error=e))
470
470
  return
471
471
 
472
472
  # Disable inputs
@@ -494,17 +494,17 @@ class InstallVMModal(BaseModal[str | None]):
494
494
  if custom_path:
495
495
  # Validate custom path exists
496
496
  if not os.path.exists(custom_path):
497
- raise Exception(f"Custom ISO path does not exist: {custom_path}")
497
+ raise Exception(ErrorMessages.CUSTOM_ISO_PATH_NOT_EXIST_TEMPLATE.format(path=custom_path))
498
498
  if not os.path.isfile(custom_path):
499
- raise Exception(f"Custom ISO path is not a file: {custom_path}")
499
+ raise Exception(ErrorMessages.CUSTOM_ISO_NOT_FILE_TEMPLATE.format(path=custom_path))
500
500
 
501
501
  # 1. Validate Checksum
502
502
  if validate:
503
503
  if not checksum:
504
- raise Exception("Checksum validation enabled but no checksum provided")
504
+ raise Exception(ErrorMessages.CHECKSUM_MISSING)
505
505
  progress_cb("Validating Checksum...", 0)
506
506
  if not self.provisioner.validate_iso(custom_path, checksum):
507
- raise Exception("Checksum validation failed!")
507
+ raise Exception(ErrorMessages.CHECKSUM_VALIDATION_FAILED)
508
508
  progress_cb("Checksum Validated", 10)
509
509
 
510
510
  # 2. Upload
@@ -514,7 +514,7 @@ class InstallVMModal(BaseModal[str | None]):
514
514
 
515
515
  final_iso_url = self.provisioner.upload_iso(custom_path, pool_name, upload_progress)
516
516
  if not final_iso_url:
517
- raise Exception("No ISO URL specified for provisioning")
517
+ raise Exception(ErrorMessages.NO_ISO_URL_SPECIFIED)
518
518
 
519
519
  # 3. Provision
520
520
  # Suspend global updates to prevent UI freeze during heavy provisioning ops
@@ -544,7 +544,7 @@ class InstallVMModal(BaseModal[str | None]):
544
544
  try:
545
545
  if not domain_obj.isActive():
546
546
  domain_obj.create()
547
- app.call_from_thread(app.show_success_message, f"VM '{vm_name}' started.")
547
+ app.call_from_thread(app.show_success_message, SuccessMessages.VM_STARTED_TEMPLATE.format(vm_name=vm_name))
548
548
  # Launch viewer
549
549
  domain_name = domain_obj.name()
550
550
  cmd = remote_viewer_cmd(self.uri, domain_name, app.r_viewer)
@@ -555,10 +555,10 @@ class InstallVMModal(BaseModal[str | None]):
555
555
  preexec_fn=os.setsid,
556
556
  )
557
557
  logging.info(f"{app.r_viewer} started with PID {proc.pid} for {domain_name}")
558
- app.call_from_thread(app.show_quick_message, f"Remote viewer {app.r_viewer} started for {domain_name}")
558
+ app.call_from_thread(app.show_quick_message, SuccessMessages.REMOTE_VIEWER_STARTED_TEMPLATE.format(viewer=app.r_viewer, vm_name=domain_name))
559
559
  except Exception as e:
560
560
  logging.error(f"Failed to start VM or viewer: {e}")
561
- app.call_from_thread(app.show_error_message, f"Failed to start VM or viewer: {e}")
561
+ app.call_from_thread(app.show_error_message, ErrorMessages.FAILED_TO_START_VM_OR_VIEWER_TEMPLATE.format(error=e))
562
562
 
563
563
  app.worker_manager.run(start_and_view, name=f"start_view_{vm_name}")
564
564
 
@@ -567,7 +567,7 @@ class InstallVMModal(BaseModal[str | None]):
567
567
  on_details_closed
568
568
  )
569
569
  else:
570
- app.show_error_message(f"Could not get details for {vm_name}")
570
+ app.show_error_message(ErrorMessages.COULD_NOT_GET_VM_DETAILS_TEMPLATE.format(vm_name=vm_name))
571
571
  self.app.call_from_thread(push_details)
572
572
 
573
573
  dom = self.provisioner.provision_vm(
@@ -591,10 +591,10 @@ class InstallVMModal(BaseModal[str | None]):
591
591
  self.app.call_from_thread(self.app.on_vm_data_update)
592
592
 
593
593
  if configure_before_install:
594
- self.app.call_from_thread(self.app.show_success_message, f"VM '{name}' defined. Please configure and start it.")
594
+ self.app.call_from_thread(self.app.show_success_message, SuccessMessages.VM_DEFINED_CONFIGURE_TEMPLATE.format(vm_name=name))
595
595
  return
596
596
 
597
- self.app.call_from_thread(self.app.show_success_message, f"VM '{name}' created successfully!")
597
+ self.app.call_from_thread(self.app.show_success_message, SuccessMessages.VM_CREATED_SUCCESSFULLY_TEMPLATE.format(vm_name=name))
598
598
 
599
599
  # 4. Auto-connect Remote Viewer
600
600
  def launch_viewer():
@@ -608,12 +608,12 @@ class InstallVMModal(BaseModal[str | None]):
608
608
  preexec_fn=os.setsid,
609
609
  )
610
610
  logging.info(f"{self.app.r_viewer} started with PID {proc.pid} for {domain_name}")
611
- self.app.show_quick_message(f"Remote viewer {self.app.r_viewer} started for {domain_name}")
611
+ self.app.show_quick_message(SuccessMessages.REMOTE_VIEWER_STARTED_TEMPLATE.format(viewer=self.app.r_viewer, vm_name=domain_name))
612
612
  except Exception as e:
613
613
  logging.error(f"Failed to spawn {self.app.r_viewer} for {domain_name}: {e}")
614
614
  self.app.call_from_thread(
615
615
  self.app.show_error_message,
616
- f"{self.app.r_viewer} failed to start for {domain_name}: {e}"
616
+ ErrorMessages.REMOTE_VIEWER_FAILED_TO_START_TEMPLATE.format(viewer=self.app.r_viewer, domain_name=domain_name, error=e)
617
617
  )
618
618
  return
619
619
 
@@ -621,5 +621,5 @@ class InstallVMModal(BaseModal[str | None]):
621
621
  self.app.call_from_thread(self.dismiss, True)
622
622
 
623
623
  except Exception as e:
624
- self.app.call_from_thread(self.app.show_error_message, f"Provisioning failed: {e}")
624
+ self.app.call_from_thread(self.app.show_error_message, ErrorMessages.PROVISIONING_FAILED_TEMPLATE.format(error=e))
625
625
  self.app.call_from_thread(self.dismiss)
@@ -12,6 +12,7 @@ from textual.screen import ModalScreen
12
12
  from .base_modals import BaseModal
13
13
  from .utils_modals import LoadingModal
14
14
  from ..connection_manager import ConnectionManager
15
+ from ..constants import ErrorMessages, StaticText, ButtonLabels
15
16
 
16
17
 
17
18
  class SelectServerModal(BaseModal[None]):
@@ -26,7 +27,7 @@ class SelectServerModal(BaseModal[None]):
26
27
 
27
28
  def compose(self) -> ComposeResult:
28
29
  with Vertical(id="select-server-container", classes="info-details"):
29
- yield Label("Select Servers to Display")
30
+ yield Label(StaticText.SELECT_SERVERS_TO_DISPLAY)
30
31
 
31
32
  checkboxes = []
32
33
  for i, server in enumerate(self.servers):
@@ -44,8 +45,8 @@ class SelectServerModal(BaseModal[None]):
44
45
  yield grid
45
46
 
46
47
  with Horizontal(classes="button-details"):
47
- yield Button("Done", id="done-servers", variant="primary", classes="done-button")
48
- yield Button("Cancel", id="cancel-servers", classes="cancel-button")
48
+ yield Button(ButtonLabels.DONE, id="done-servers", variant="primary", classes="done-button")
49
+ yield Button(ButtonLabels.CANCEL, id="cancel-servers", classes="cancel-button")
49
50
 
50
51
  @on(Checkbox.Changed)
51
52
  def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
@@ -57,7 +58,7 @@ class SelectServerModal(BaseModal[None]):
57
58
  return
58
59
 
59
60
  if event.value: # If checkbox is checked
60
- loading_modal = LoadingModal(f"Connecting to {uri}...")
61
+ loading_modal = LoadingModal(ErrorMessages.CONNECTING_TO_SERVER_TEMPLATE.format(uri=uri))
61
62
  self.app.push_screen(loading_modal)
62
63
 
63
64
  def connect_and_update():
@@ -66,7 +67,7 @@ class SelectServerModal(BaseModal[None]):
66
67
  if conn is None:
67
68
  self.app.call_from_thread(
68
69
  self.app.show_error_message,
69
- f"Failed to connect to {uri}"
70
+ ErrorMessages.FAILED_TO_CONNECT_TO_SERVER_TEMPLATE.format(uri=uri)
70
71
  )
71
72
  # Revert checkbox state on failure
72
73
  checkbox = self.query(f"#{checkbox_id}").first()
@@ -104,10 +105,10 @@ class SelectOneServerModal(BaseModal[str]):
104
105
  with Vertical(id="select-one-server-container"):
105
106
  yield Label(self.title_text)
106
107
  yield Select(self.server_options, prompt="Select server...", id="server-select")
107
- yield Label("")
108
+ yield Label(StaticText.EMPTY_LABEL)
108
109
  with Horizontal():
109
110
  yield Button(self.button_label, id="launch-btn", variant="primary", disabled=True)
110
- yield Button("Cancel", id="cancel-btn")
111
+ yield Button(ButtonLabels.CANCEL, id="cancel-btn")
111
112
 
112
113
  @on(Select.Changed, "#server-select")
113
114
  def on_server_select_changed(self, event: Select.Changed) -> None: