diff --git a/qubes_config/global_config/rule_list_widgets.py b/qubes_config/global_config/rule_list_widgets.py
index a81e2548..c43f3761 100644
--- a/qubes_config/global_config/rule_list_widgets.py
+++ b/qubes_config/global_config/rule_list_widgets.py
@@ -240,7 +240,7 @@ def _combobox_changed(self, *_args):
self.callback()
def _format_new_value(self, new_value):
- self.name_widget.set_markup(f'{self.choices[new_value]}')
+ self.name_widget.set_text(f'{self.choices[new_value]}')
if self.verb_description:
self.additional_text_widget.set_text(
self.verb_description.get_verb_for_action_and_target(
diff --git a/qubes_config/global_config/thisdevice_handler.py b/qubes_config/global_config/thisdevice_handler.py
index 1212803d..05f6f43a 100644
--- a/qubes_config/global_config/thisdevice_handler.py
+++ b/qubes_config/global_config/thisdevice_handler.py
@@ -23,13 +23,14 @@
import qubesadmin.vm
from ..widgets.gtk_utils import show_error, load_icon, copy_to_global_clipboard
+from ..widgets.gtk_utils import markup_format
from .page_handler import PageHandler
from .policy_manager import PolicyManager
import gi
gi.require_version('Gtk', '3.0')
-from gi.repository import Gtk
+from gi.repository import Gtk, GLib
import gettext
t = gettext.translation("desktop-linux-manager", fallback=True)
@@ -101,7 +102,7 @@ def __init__(self,
['qubes-hcl-report', '-y']).decode()
except subprocess.CalledProcessError as ex:
label_text += _("Failed to load system data: {ex}\n").format(
- ex=str(ex))
+ ex=GLib.markup_escape_text(str(ex)))
self.hcl_check = ""
try:
@@ -115,7 +116,7 @@ def __init__(self,
label_text += _("Failed to load system data.\n")
self.data_label.get_style_context().add_class('red_code')
- label_text += _("""Brand: {brand}
+ label_text += markup_format("""Brand: {brand}
Model: {model}
CPU: {cpu}
@@ -128,7 +129,8 @@ def __init__(self,
BIOS: {bios}
Kernel: {kernel_ver}
Xen: {xen_ver}
-""").format(brand=self._get_data('brand'),
+""",
+ brand=self._get_data('brand'),
model=self._get_data('model'),
cpu=self._get_data('cpu'),
chipset=self._get_data('chipset'),
@@ -139,15 +141,15 @@ def __init__(self,
kernel_ver=self._get_version('kernel'),
xen_ver=self._get_version('xen'))
self.set_state(self.compat_hvm_image, self._get_data('hvm'))
- self.compat_hvm_label.set_markup(f"HVM: {self._get_data('hvm')}")
+ self.compat_hvm_label.set_markup(markup_format("HVM: {}", self._get_data('hvm')))
self.set_state(self.compat_iommu_image, self._get_data('iommu'))
self.compat_iommu_label.set_markup(
- f"I/O MMU: {self._get_data('iommu')}")
+ markup_format("I/O MMU: {}", self._get_data('iommu')))
self.set_state(self.compat_hap_image, self._get_data('slat'))
self.compat_hap_label.set_markup(
- f"HAP/SLAT: {self._get_data('slat')}")
+ markup_format("HAP/SLAT: {}", self._get_data('slat')))
self.set_state(self.compat_tpm_image,
'yes' if self._get_data('tpm') == '1.2' else 'maybe')
@@ -166,7 +168,7 @@ def __init__(self,
self.set_state(self.compat_remapping_image, self._get_data('remap'))
self.compat_remapping_label.set_markup(
- f"Remapping: {self._get_data('remap')}")
+ markup_format(_("Remapping: {}"), self._get_data('remap')))
self.set_policy_state()
@@ -178,7 +180,7 @@ def __init__(self,
_("PV qubes: {num_pvs} found").format(num_pvs=len(pv_vms)))
self.compat_pv_tooltip.set_tooltip_markup(
_("The following qubes have PV virtualization mode:\n - ") +
- '\n - '.join([vm.name for vm in pv_vms]))
+ '\n - '.join([GLib.markup_escape_text(vm.name) for vm in pv_vms]))
self.compat_pv_tooltip.set_visible(bool(pv_vms))
self.data_label.set_markup(label_text)
diff --git a/qubes_config/global_config/usb_devices.py b/qubes_config/global_config/usb_devices.py
index 57c1722a..5a97ece8 100644
--- a/qubes_config/global_config/usb_devices.py
+++ b/qubes_config/global_config/usb_devices.py
@@ -488,7 +488,7 @@ def load_rules_for_usb_qube(self):
def disable_u2f(self, reason: str):
self.problem_fatal_box.set_visible(True)
self.problem_fatal_box.show_all()
- self.problem_fatal_label.set_markup(reason)
+ self.problem_fatal_label.set_text(reason)
self.enable_check.set_active(False)
self.enable_check.set_sensitive(False)
self.box.set_visible(False)
diff --git a/qubes_config/widgets/gtk_utils.py b/qubes_config/widgets/gtk_utils.py
index 0a333eed..e37b0b3b 100644
--- a/qubes_config/widgets/gtk_utils.py
+++ b/qubes_config/widgets/gtk_utils.py
@@ -84,6 +84,18 @@ def load_icon(icon_name: str, width: int = 24, height: int = 24):
pixbuf.fill(0x000)
return pixbuf
+def _escape_str(s: Union[str, float, int]) -> Union[str, float, int]:
+ if type(s) is str:
+ return GLib.markup_escape_text(s)
+ elif type(s) in (float, int, bool):
+ return s
+ else: # Neither escapable nor known safe to passthrough
+ raise TypeError(f"Unsupported input type {type(s)}")
+
+def markup_format(s, *args, **kwargs) -> str:
+ escaped_args = [_escape_str(i) for i in args]
+ escaped_kwargs = {k: _escape_str(v) for k, v in kwargs.items()}
+ return s.format(*escaped_args, **escaped_kwargs)
def show_error(parent, title, text):
"""
@@ -175,7 +187,7 @@ def show_dialog(
if isinstance(text, str):
label: Gtk.Label = Gtk.Label()
- label.set_markup(text)
+ label.set_text(text)
label.set_line_wrap_mode(Gtk.WrapMode.WORD)
label.set_max_width_chars(200)
label.set_xalign(0)
diff --git a/qui/clipboard.py b/qui/clipboard.py
index 0118bf94..ea31daa7 100644
--- a/qui/clipboard.py
+++ b/qui/clipboard.py
@@ -48,7 +48,7 @@
t = gettext.translation("desktop-linux-manager", fallback=True)
_ = t.gettext
-from .utils import run_asyncio_and_show_errors
+from .utils import run_asyncio_and_show_errors, markup_format
gbulb.install()
@@ -127,19 +127,20 @@ def _copy(self, metadata: dict) -> None:
size = clipboard_formatted_size(metadata["sent_size"])
if metadata["malformed_request"]:
- body = ERROR_MALFORMED_DATA.format(vmname=metadata["vmname"])
+ body = markup_format(ERROR_MALFORMED_DATA,
+ vmname=metadata["vmname"])
icon = "dialog-error"
elif (
metadata["qrexec_clipboard"]
and metadata["sent_size"] >= metadata["buffer_size"]
):
# Microsoft Windows clipboard case
- body = WARNING_POSSIBLE_TRUNCATION.format(
+ body = markup_format(WARNING_POSSIBLE_TRUNCATION,
vmname=metadata["vmname"], size=size
)
icon = "dialog-warning"
elif metadata["oversized_request"]:
- body = ERROR_OVERSIZED_DATA.format(
+ body = markup_format(ERROR_OVERSIZED_DATA,
vmname=metadata["vmname"],
size=size,
limit=clipboard_formatted_size(metadata["buffer_size"]),
@@ -150,13 +151,14 @@ def _copy(self, metadata: dict) -> None:
and metadata["cleared"]
and metadata["sent_size"] == 0
):
- body = WARNING_EMPTY_CLIPBOARD.format(vmname=metadata["vmname"])
+ body = markup_format(WARNING_EMPTY_CLIPBOARD,
+ vmname=metadata["vmname"])
icon = "dialog-warning"
elif not metadata["successful"]:
- body = ERROR_ON_COPY.format(vmname=metadata["vmname"])
+ body = markup_format(ERROR_ON_COPY, vmname=metadata["vmname"])
icon = "dialog-error"
else:
- body = MSG_COPY_SUCCESS.format(
+ body = markup_format(MSG_COPY_SUCCESS,
vmname=metadata["vmname"],
size=size,
shortcut=self.gtk_app.paste_shortcut,
@@ -173,14 +175,14 @@ def _copy(self, metadata: dict) -> None:
def _paste(self, metadata: dict) -> None:
"""Sends Paste notification via Gio.Notification."""
if not metadata["successful"] or metadata["malformed_request"]:
- body = ERROR_ON_PASTE.format(vmname=metadata["vmname"])
+ body = markup_format(ERROR_ON_PASTE, vmname=metadata["vmname"])
body += MSG_WIPED
icon = "dialog-error"
elif (
"protocol_version_xside" in metadata.keys()
and metadata["protocol_version_xside"] >= 0x00010008
):
- body = MSG_PASTE_SUCCESS_METADATA.format(
+ body = markup_format(MSG_PASTE_SUCCESS_METADATA,
size=clipboard_formatted_size(metadata["sent_size"]),
vmname=metadata["vmname"],
)
@@ -355,9 +357,9 @@ def update_clipboard_contents(
else:
self.clipboard_label.set_markup(
- _(
+ markup_format(_(
"Global clipboard contents: {0} from {1}"
- ).format(size, vm)
+ ), size, vm)
)
self.icon.set_from_icon_name("edit-copy")
@@ -391,10 +393,10 @@ def setup_ui(self, *_args, **_kwargs):
help_label = Gtk.Label(xalign=0)
help_label.set_markup(
- _(
+ markup_format(_(
"Use {copy} to copy and "
"{paste} to paste."
- ).format(copy=self.copy_shortcut, paste=self.paste_shortcut)
+ ), copy=self.copy_shortcut, paste=self.paste_shortcut)
)
help_item = Gtk.MenuItem()
help_item.set_margin_left(10)
@@ -442,9 +444,10 @@ def copy_dom0_clipboard(self, *_args, **_kwargs):
'"protocol_version_xside":65544,\n'
'"protocol_version_vmside":65544,\n'
"}}\n".format(
- xevent_timestamp=str(Gtk.get_current_event_time()),
- sent_size=os.path.getsize(DATA),
- buffer_size="256000",
+ xevent_timestamp=json.dumps(
+ Gtk.get_current_event_time()),
+ sent_size=json.dumps(os.path.getsize(DATA)),
+ buffer_size=json.dumps(256000),
)
)
except Exception: # pylint: disable=broad-except
diff --git a/qui/decorators.py b/qui/decorators.py
index 8a76fb6a..dcfd6a38 100644
--- a/qui/decorators.py
+++ b/qui/decorators.py
@@ -9,6 +9,7 @@
from gi.repository import Gtk, Pango, GLib, GdkPixbuf # isort:skip
from qubesadmin import exc
from qubesadmin.utils import size_to_human
+from .utils import markup_format
import gettext
t = gettext.translation("desktop-linux-manager", fallback=True)
@@ -137,10 +138,10 @@ def update_tooltip(self,
perc_storage = self.cur_storage / self.max_storage
tooltip += \
- _("\nTemplate: {template}"
+ markup_format(_("\nTemplate: {template}"
"\nNetworking: {netvm}"
"\nPrivate storage: {current_storage:.2f}GB/"
- "{max_storage:.2f}GB ({perc_storage:.1%})").format(
+ "{max_storage:.2f}GB ({perc_storage:.1%})"),
template=self.template_name,
netvm=self.netvm_name,
current_storage=self.cur_storage,
@@ -177,7 +178,8 @@ def update_state(self, cpu=0, header=False):
else:
color = self.cpu_label.get_style_context() \
.get_color(Gtk.StateFlags.INSENSITIVE).to_color()
- markup = f'0%'
+ escaped_color = GLib.markup_escape_text(color.to_string())
+ markup = f'0%'
self.cpu_label.set_markup(markup)
@@ -249,8 +251,9 @@ def device_hbox(device) -> Gtk.Box:
name_label = Gtk.Label(xalign=0)
name = f"{device.backend_domain}:{device.port_id} - {device.description}"
if device.attachments:
- dev_list = ", ".join(list(device.attachments))
- name_label.set_markup(f'{name} ({dev_list})')
+ dev_list = GLib.markup_escape_text(", ".join(list(device.attachments)))
+ name_escaped = GLib.markup_escape_text(name)
+ name_label.set_markup(f'{name_escaped} ({dev_list})')
else:
name_label.set_text(name)
name_label.set_max_width_chars(64)
@@ -281,7 +284,7 @@ def device_domain_hbox(vm, attached: bool) -> Gtk.Box:
name = Gtk.Label(xalign=0)
if attached:
- name.set_markup(f'{vm.vm_name}')
+ name.set_markup(f'{GLib.markup_escape_text(vm.vm_name)}')
else:
name.set_text(vm.vm_name)
diff --git a/qui/devices/actionable_widgets.py b/qui/devices/actionable_widgets.py
index 266bd8a1..01d9835f 100644
--- a/qui/devices/actionable_widgets.py
+++ b/qui/devices/actionable_widgets.py
@@ -130,7 +130,7 @@ def __init__(self, vm: backend.VM, size: int = 18, variant: str = 'dark',
self.backend_label = Gtk.Label(xalign=0)
backend_label: str = vm.name
if name_extension:
- backend_label += ": " + name_extension
+ backend_label += ": " + GLib.markup_escape_text(name_extension)
self.backend_label.set_markup(backend_label)
self.pack_start(self.backend_icon, False, False, 4)
@@ -227,7 +227,9 @@ class DetachWidget(ActionableWidget, SimpleActionWidget):
"""Detach device from a VM"""
def __init__(self, vm: backend.VM, device: backend.Device,
variant: str = 'dark'):
- super().__init__('detach', 'Detach from ' + vm.name + '',
+ super().__init__('detach',
+ 'Detach from ' +
+ GLib.markup_escape_text(vm.name) + '',
variant)
self.vm = vm
self.device = device
@@ -345,14 +347,14 @@ def __init__(self, device: backend.Device, variant: str = 'dark'):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
# FUTURE: this is proposed layout for new API
# self.device_label = Gtk.Label()
- # self.device_label.set_markup(device.name)
+ # self.device_label.set_text(device.name)
# self.device_label.get_style_context().add_class('device_name')
# self.edit_icon = VariantIcon('edit', 'dark', 24)
# self.detailed_description_label = Gtk.Label()
# self.detailed_description_label.set_text(device.description)
# self.backend_icon = VariantIcon(device.vm_icon, 'dark', 24)
# self.backend_label = Gtk.Label(xalign=0)
- # self.backend_label.set_markup(str(device.backend_domain))
+ # self.backend_label.set_text(str(device.backend_domain))
#
# self.title_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
# self.title_box.add(self.device_label)
@@ -367,7 +369,7 @@ def __init__(self, device: backend.Device, variant: str = 'dark'):
# self.add(self.attachment_box)
self.device_label = Gtk.Label()
- self.device_label.set_markup(device.name)
+ self.device_label.set_text(device.name)
self.device_label.get_style_context().add_class('device_name')
self.device_label.set_xalign(Gtk.Align.CENTER)
self.device_label.set_halign(Gtk.Align.CENTER)
@@ -408,7 +410,7 @@ def __init__(self, device: backend.Device, variant: str = 'dark'):
self.device_label = Gtk.Label(xalign=0)
- label_markup = device.name
+ label_markup = GLib.markup_escape_text(device.name)
if (device.connection_timestamp and
int(time.monotonic() - device.connection_timestamp) < 120):
label_markup += ' NEW'
diff --git a/qui/tray/disk_space.py b/qui/tray/disk_space.py
index 8d8c8094..a70249b0 100644
--- a/qui/tray/disk_space.py
+++ b/qui/tray/disk_space.py
@@ -85,9 +85,10 @@ def __create_widgets(vm_usage):
for volume_name, usage in vm_usage.problem_volumes.items():
# pylint: disable=consider-using-f-string
label_contents.append(_('volume {} is {:.1%} full').format(
- volume_name, usage))
+ GLib.markup_escape_text(volume_name), usage))
- label_text = f"{vm.name}: " + ", ".join(label_contents)
+ label_text = f"{GLib.markup_escape_text(vm.name)}: " + \
+ ", ".join(label_contents)
label_widget.set_markup(label_text)
return vm, icon_img, label_widget
@@ -241,14 +242,17 @@ def __create_box(pool: PoolWrapper):
if pool.has_error:
# Pool with errors
- formatted_name = \
- f'{pool.name}'
+ formatted_name = ('' +
+ GLib.markup_escape_text(pool.name) +
+ '')
elif pool.size and 'included_in' not in pool.config:
# normal pool
- formatted_name = f'{pool.name}'
+ formatted_name = f'{GLib.markup_escape_text(pool.name)}'
else:
# pool without data or included in another pool
- formatted_name = f'{pool.name}'
+ formatted_name = ("" +
+ GLib.markup_escape_text(pool.name) +
+ '')
pool_name.set_markup(formatted_name)
pool_name.set_margin_left(20)
@@ -260,20 +264,20 @@ def __create_box(pool: PoolWrapper):
if pool.has_error:
error_desc = Gtk.Label(xalign=0)
- error_desc.set_markup("Error accessing pool data")
+ error_desc.set_text("Error accessing pool data")
error_desc.set_margin_left(40)
name_box.pack_start(error_desc, True, True, 0)
return name_box, percentage_box, usage_box
data_name = Gtk.Label(xalign=0)
- data_name.set_markup("data")
+ data_name.set_text("data")
data_name.set_margin_left(40)
name_box.pack_start(data_name, True, True, 0)
if pool.metadata_perc:
metadata_name = Gtk.Label(xalign=0)
- metadata_name.set_markup("metadata")
+ metadata_name.set_text("metadata")
metadata_name.set_margin_left(40)
name_box.pack_start(metadata_name, True, True, 0)
@@ -407,11 +411,13 @@ def set_icon_state(self, pool_warning=None, vm_warning=None):
self.icon.set_from_icon_name("dialog-warning")
text = _("Qubes Disk Space Monitor\n\nWARNING!")
if pool_warning:
- text += _('\nYou are running out of disk space.\n') + \
- ''.join(pool_warning)
+ text += GLib.markup_escape_text(
+ _('\nYou are running out of disk space.\n') +
+ ''.join(pool_warning))
if vm_warning:
- text += _('\nThe following qubes are running out of space: ') \
- + ', '.join([x.vm.name for x in vm_warning])
+ text += GLib.markup_escape_text(_(
+ '\nThe following qubes are running out of space: ')
+ + ', '.join([x.vm.name for x in vm_warning]))
self.icon.set_tooltip_markup(text)
else:
self.icon.set_from_icon_name("drive-harddisk")
@@ -465,7 +471,7 @@ def make_menu(self, _unused, _event):
@staticmethod
def make_title_item(text):
label = Gtk.Label(xalign=0)
- label.set_markup(_("{}").format(text))
+ label.set_markup("{}").format(GLib.markup_escape_text(text))
menu_item = Gtk.MenuItem()
menu_item.add(label)
menu_item.set_sensitive(False)
diff --git a/qui/tray/domains.py b/qui/tray/domains.py
index 1eae1310..654b0476 100644
--- a/qui/tray/domains.py
+++ b/qui/tray/domains.py
@@ -74,7 +74,7 @@ def show_error(title, text):
dialog = Gtk.MessageDialog(
None, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK)
dialog.set_title(title)
- dialog.set_markup(text)
+ dialog.set_markup(GLib.markup_escape_text(text))
dialog.connect("response", lambda *x: dialog.destroy())
GLib.idle_add(dialog.show)
@@ -284,12 +284,13 @@ class InternalInfoItem(Gtk.MenuItem):
def __init__(self):
super().__init__()
self.label = Gtk.Label(xalign=0)
- self.label.set_markup(_(
- 'Internal qube'))
- self.set_tooltip_text(
+ self.label.set_markup("" +
+ GLib.markup_escape_text(_("Internal qubes")) +
+ "")
+ self.set_tooltip_text(_(
'Internal qubes are used by the operating system. Do not modify'
' them or run programs in them unless you really '
- 'know what you are doing.')
+ 'know what you are doing.'))
self.add(self.label)
self.set_sensitive(False)
@@ -510,7 +511,9 @@ def update_state(self, state):
colormap = {'Paused': 'grey', 'Crashed': 'red', 'Transient': 'red'}
if state in colormap:
self.name.label.set_markup(
- f'{self.vm.name}')
+ "" +
+ GLib.markup_escape_text(self.vm.name) +
+ "")
else:
self.name.label.set_label(self.vm.name)
diff --git a/qui/tray/updates.py b/qui/tray/updates.py
index 8533dea3..dcda7d6a 100644
--- a/qui/tray/updates.py
+++ b/qui/tray/updates.py
@@ -14,7 +14,7 @@
import gi # isort:skip
gi.require_version('Gtk', '3.0') # isort:skip
-from gi.repository import Gtk, Gio # isort:skip
+from gi.repository import Gtk, Gio, GLib # isort:skip
import gbulb
gbulb.install()
@@ -28,7 +28,8 @@ class TextItem(Gtk.MenuItem):
def __init__(self, text):
super().__init__()
title_label = Gtk.Label()
- title_label.set_markup(text)
+ title_label.set_markup('' + GLib.markup_escape_text(_(text)) +
+ '')
title_label.set_halign(Gtk.Align.CENTER)
title_label.set_justify(Gtk.Justification.CENTER)
self.set_margin_left(10)
@@ -87,7 +88,7 @@ def setup_menu(self):
self.tray_menu.set_reserve_toggle_size(False)
if self.vms_needing_update:
- self.tray_menu.append(TextItem(_("Qube updates available!")))
+ self.tray_menu.append(TextItem("Qube updates available!"))
self.tray_menu.append(RunItem(
_("Updates for {} qubes are available!\n"
"Launch updater").format(
@@ -95,11 +96,13 @@ def setup_menu(self):
if self.obsolete_vms:
self.tray_menu.append(TextItem(
- _("Some qubes are no longer supported!")))
- obsolete_text = _("The following qubes are based on distributions "
- "that are no longer supported:\n")\
- + ", ".join([str(vm) for vm in self.obsolete_vms])\
- + _("\nInstall new templates with Template Manager")
+ "Some qubes are no longer supported!"))
+ obsolete_text = GLib.markup_escape_text(_(
+ "The following qubes are based on distributions "
+ "that are no longer supported:\n") +
+ ", ".join([str(vm) for vm in self.obsolete_vms])) \
+ + "\n" + GLib.markup_escape_text(
+ _("Install new templates with Template Manager")) + ""
self.tray_menu.append(
RunItem(obsolete_text, self.launch_template_manager))
diff --git a/qui/utils.py b/qui/utils.py
index ffbdf26b..d3536ed0 100644
--- a/qui/utils.py
+++ b/qui/utils.py
@@ -24,6 +24,7 @@
import sys
import traceback
from html import escape
+from typing import Union
from qubesadmin import exc
@@ -37,7 +38,7 @@
import gi # isort:skip
gi.require_version('Gtk', '3.0') # isort:skip
-from gi.repository import Gtk # isort:skip
+from gi.repository import Gtk, GLib # isort:skip
with importlib.resources.files('qui').joinpath('eol.json').open() as stream:
EOL_DATES = json.load(stream)
@@ -60,9 +61,9 @@ def run_asyncio_and_show_errors(loop, tasks, name, restart=True):
exit_code = 0
message = _("Whoops. A critical error in {} has occurred."
- " This is most likely a bug.").format(name)
+ " This is most likely a bug.").format(escape(name))
if restart:
- message += _(" {} will restart itself.").format(name)
+ message += _(" {} will restart itself.").format(escape(name))
for d in done: # pylint: disable=invalid-name
try:
@@ -76,12 +77,13 @@ def run_asyncio_and_show_errors(loop, tasks, name, restart=True):
exc_value_descr = escape(str(exc_value))
traceback_descr = escape(traceback.format_exc(limit=10))
exc_description = "\n{}: {}\n{}".format(
- exc_type.__name__, exc_value_descr, traceback_descr)
+ escape(exc_type.__name__), exc_value_descr, traceback_descr)
dialog.format_secondary_markup(exc_description)
dialog.run()
exit_code = 1
return exit_code
+
def check_update(vm) -> bool:
"""Return true if the given template/standalone vm is updated or not
updateable or skipped. default returns true"""
@@ -96,6 +98,20 @@ def check_update(vm) -> bool:
return True
return False
+def _escape_str(s: Union[str, float, int]) -> Union[str, float, int]:
+ # pylint: disable=unidiomatic-typecheck
+ if type(s) is str:
+ return GLib.markup_escape_text(s)
+ # pylint: disable=unidiomatic-typecheck
+ if type(s) in (float, int, bool):
+ return s
+ raise TypeError(f"Unsupported input type {type(s)}")
+
+def markup_format(s, *args, **kwargs) -> str:
+ escaped_args = [_escape_str(i) for i in args]
+ escaped_kwargs = {k: _escape_str(v) for k, v in kwargs.items()}
+ return s.format(*escaped_args, **escaped_kwargs)
+
def check_support(vm) -> bool:
"""Return true if the given template/standalone vm is still supported, by
default returns true"""