From 28766bb3942546cca0a65754aa7eade509768aef Mon Sep 17 00:00:00 2001 From: Timothy Feierabend Date: Wed, 1 Apr 2026 15:09:18 -0500 Subject: [PATCH 1/4] thunderbolt: re-work module initialization re-worked settings module init to simplify the startup checks. the main goal is to use d-bus for all checks. the current solution performs 3 checks: 1) Checks for whether bolt is installed by checking if `boltctl` exists on the file system. 2) Checks for whether the system has thunderbolt by querying sysfs. 3) Checks if the bolt daemon is alive. This new implementation performs 2 checks: 1) Checks if `org.freedesktop.bolt` is in the D-Bus activitable list. if bolt is installed, it will appear in this list. This replaces check number 1 above. Since the whole settings module runs off d-bus, this is a more accurate check. 2) If bolt is available, we initialize the Manager proxy, and get the list of domains. Per the documentation, there will be 1 domain/device pair with the same uuid per thunderbolt controller in the system. This replaces check number 2 above. `boltd` knows how to examine sysfs, no need to re-invent the wheel, lets just ask it if it sees thunderbolt support on the system. The check for number 3 is removed because if `org.freedesktop.bolt` isn't running, it will automatically start when we start interacting with it. systems that do have thunderbolt, but don't have any devices connected via thunderbolt, won't automatically start the service until a device connects. --- .../modules/cs_thunderbolt.py | 134 ++++++++++-------- 1 file changed, 75 insertions(+), 59 deletions(-) diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py index 12ce8602ff..a4148bd845 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py +++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py @@ -26,7 +26,7 @@ def build_detail_row(key, value): else: labelValue = Gtk.Label(label=str(value)) labelValue.set_selectable(True) - labelValue.set_line_wrap(True) + labelValue.set_line_wrap(True) row.pack_end(labelValue, False, False, 0) return row @@ -53,6 +53,9 @@ def __init__(self, name, object_path, interface_name): self.on_property_changed = None self.on_property_invalidated = None + # List of signals that the proxy is connected to + self._sig_handles = [] + # Get the initial properties for this DBus Proxy var = Gio.DBusConnection.call_sync( Gio.bus_get_sync(Gio.BusType.SYSTEM), @@ -82,17 +85,23 @@ def __init__(self, name, object_path, interface_name): None) # Register for future changes to properties - self._proxy.connect("g-properties-changed", self._on_g_properties_changed) + self.signal_connect("g-properties-changed", self._on_g_properties_changed) + + def signal_connect(self, name, callback): + self._sig_handles.append(self._proxy.connect(name, callback)) + + def dispose(self): + for handle in self._sig_handles: + self._proxy.disconnect(handle) + self._sig_handles.clear() + self._proxy = None def _on_g_properties_changed(self, proxy, changed, invalidated): for key, value in changed.unpack().items(): setattr(self.props, key, value) if self.on_property_changed: self.on_property_changed(key, value) - for key in invalidated: - delattr(self.props, key) - if self.on_property_invalidated: - self.on_property_invalidated(key) + # do nothing with invalidated at this time class BoltManagerProxy(DBusProxy): @@ -107,9 +116,9 @@ def __init__(self): # Callbacks self.on_device_added = None self.on_device_removed = None - + # Connect to g-signal for event handling - self._proxy.connect('g-signal', self._on_g_signal) + self.signal_connect('g-signal', self._on_g_signal) def _on_g_signal(self, proxy, sender, signal, parameters): if signal == "DeviceAdded" and self.on_device_added: @@ -119,6 +128,9 @@ def _on_g_signal(self, proxy, sender, signal, parameters): (obj_path,) = parameters.unpack() self.on_device_removed(obj_path) + def list_domains(self): + return self._proxy.ListDomains() + def list_devices(self): return self._proxy.ListDevices() @@ -148,7 +160,7 @@ def __init__(self, bolt_manager, bolt_device): self.bolt_device = bolt_device self.bolt_device.on_property_changed = lambda k, v: self.refresh() super().__init__("{0} {1}".format(bolt_device.props.Vendor, bolt_device.props.Name)) - + widget = SettingsWidget() self.status_label = SettingsLabel() widget.pack_start(self.status_label, False, False, 0) @@ -165,7 +177,7 @@ def __init__(self, bolt_manager, bolt_device): button_box.set_layout(Gtk.ButtonBoxStyle.EXPAND) widget.pack_end(button_box, False, False, 0) self.add_row(widget) - + list_box = Gtk.ListBox() list_box.set_selection_mode(Gtk.SelectionMode.NONE) list_box.set_header_func(self.update_header) @@ -244,23 +256,16 @@ class Module: def __init__(self, content_box): keywords = _("thunderbolt, usb, docking, station, hub, dock") - sidePage = SidePage("Thunderbolt", "cs-thunderbolt", keywords, content_box, - module=self) + sidePage = SidePage("Thunderbolt", "cs-thunderbolt", keywords, content_box, module=self) self.sidePage = sidePage self.bolt_manager = None self.bolt_devices = dict() - def on_module_selected(self, check_again=False): - # Check if thunderbolt is present - thunderbolt_present = os.path.isdir("/sys/bus/thunderbolt") - # Check if bolt is installed by finding 'boltctl' - bolt_installed = GLib.find_program_in_path("boltctl") - # Check if bolt is available on DBus - boltd_alive = self.test_daemon_alive() + def on_module_selected(self): # Check if we've already been loaded + # This is set by the SidePage class that hosts this module if not self.loaded: - print("Loading Thunderbolt module") self.sidePage.stack = SettingsStack() @@ -300,30 +305,35 @@ def on_module_selected(self, check_again=False): self.sidePage.stack.add_named(page, "settings") page.set_spacing(10) - show_disabled = False - if not thunderbolt_present: - text = _("Thunderbolt or USB4 is not detected on your system.") + # Check that org.freedesktop.bolt is available on the system + if not self.is_bolt_available(): + text = _("'%s' D-Bus service is not available on your system.") % BOLT_BUS_NAME + self.disabled_label.set_markup(f"{text}") self.disabled_retry_button.set_visible(False) - show_disabled = True - elif not bolt_installed: - text = _("The 'bolt' package must be installed to manage Thunderbolt and USB4 devices.") - self.disabled_retry_button.set_visible(True) - self.disabled_retry_button.set_sensitive(True) - show_disabled = True - elif not boltd_alive: - text = _("The boltd service is not running.") - self.disabled_retry_button.set_visible(True) - self.disabled_retry_button.set_sensitive(True) - show_disabled = True - - if show_disabled: - self.reset() page = "disabled" + GLib.idle_add(self.set_initial_page, page) + return + + # Initialilze the bolt manager + if not self.bolt_manager: + self.bolt_manager = BoltManagerProxy() + self.bolt_manager.on_device_added = self.bolt_device_added + self.bolt_manager.on_device_removed = self.bolt_device_removed + + # Check if there are any domains + # If thunderbolt or usb4 is available, there will be 1 domain per controller + # Each domain has a corresponding device with the same uuid + if not self.bolt_manager.list_domains(): + text = _("Thunderbolt or USB4 is not detected on your system.") self.disabled_label.set_markup(f"{text}") - else: - self.setup() - page = self.page_name() + self.disabled_retry_button.set_visible(False) + page = "disabled" + GLib.idle_add(self.set_initial_page, page) + return + # Setup and display the page + self.setup() + page = self.page_name() GLib.idle_add(self.set_initial_page, page) def disable_retry_on_clicked(self, widget): @@ -333,18 +343,18 @@ def disable_retry_on_clicked(self, widget): def set_initial_page(self, page): self.sidePage.stack.set_visible_child_name(page) - def reset(self): - self.bolt_manager = None + def dispose_manager(self): + if self.bolt_manager: + self.bolt_manager.dispose() + self.bolt_manager = None + + def dispose_device_sections(self): for obj_path in list(self.bolt_devices.keys()): self.bolt_device_removed(obj_path, False) def setup(self): - if not self.bolt_manager: - self.bolt_manager = BoltManagerProxy() - self.bolt_manager.on_device_added = self.bolt_device_added - self.bolt_manager.on_device_removed = self.bolt_device_removed for obj_path in self.bolt_manager.list_devices(): - device = BoltDeviceProxy(obj_path) + device = BoltDeviceProxy(obj_path) # Skip the host device if device.props.Type == "host": continue @@ -367,7 +377,12 @@ def bolt_device_added(self, obj_path, change_page=True): def bolt_device_removed(self, obj_path, change_page=True): if obj_path in self.bolt_devices: section = self.bolt_devices[obj_path] + # Dispose of the bolt device proxy + section.bolt_device.dispose() + section.bolt_device = None + # Destroy the settigs section section.destroy() + # Finally - remove the settings section from the paths dict del self.bolt_devices[obj_path] if change_page: self.sidePage.stack.set_visible_child_name(self.page_name()) @@ -375,22 +390,23 @@ def bolt_device_removed(self, obj_path, change_page=True): def page_name(self): return "settings" if len(self.bolt_devices) > 0 else "empty" - def test_daemon_alive(self): + def is_bolt_available(self): try: - # Ping bolt to see if its available and running - Gio.DBusConnection.call_sync( + # bolt is an activatable d-bus service - ask d-bus if it knows about it + var = Gio.DBusConnection.call_sync( Gio.bus_get_sync(Gio.BusType.SYSTEM), - BOLT_BUS_NAME, - BOLT_OBJECT_PATH, - "org.freedesktop.DBus.Peer", - "Ping", + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "ListActivatableNames", None, None, 0, -1, - None) - return True - except GLib.Error as e: - # Bolt isn't installed or service is disabled + None + ) + (bus_names,) = var.unpack() + return BOLT_BUS_NAME in bus_names + except GLib.Error: pass - return False + return False \ No newline at end of file From c48605fca0bed625305a0404d8d8597acd6eed9f Mon Sep 17 00:00:00 2001 From: Timothy Feierabend Date: Sat, 4 Apr 2026 13:18:23 -0500 Subject: [PATCH 2/4] thunderbolt: refactor BoltManager/BoltDevice classes Use the `get_cached_property()` feature of DBusProxy object instead of re-inventing the wheel with the props object and setattr. Renamed DBusProxy to DBusObject since it is a wrapper class around a Gio.DBusProxy. Renamed BoltManagerProxy to BoltManager and BoltDeviceProxy to BoltDevice since they are convienence wrapper classes. Removed DBusProps empty class. Added DBusProperty descriptor class. This class is designed for use with the DBusObject class for easy access to properties. It is used to expose the various properties needed from the BoltDevice class. Changed host device check to call `org.freedesktop.DBus.Properties.Get()` method to get the device type, prevent un-necessary creation of a BoltDevice object. Renamed all properties and methods in the BoltManager and BoltDevice classes to match the DBus naming convention. --- .../modules/cs_thunderbolt.py | 195 ++++++++---------- 1 file changed, 89 insertions(+), 106 deletions(-) diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py index a4148bd845..36e40bde55 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py +++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py @@ -14,6 +14,8 @@ BOLT_BUS_NAME = "org.freedesktop.bolt" BOLT_OBJECT_PATH = "/org/freedesktop/bolt" +BOLT_MANAGER_IFACE = "org.freedesktop.bolt1.Manager" +BOLT_DEVICE_IFACE = "org.freedesktop.bolt1.Device" def build_detail_row(key, value): @@ -38,52 +40,38 @@ def format_generation(gen): raise ValueError("undefined thunderbolt generation") -class DBusProps: - pass +class DBusProperty: + """A read-only property for use with the DBusObject class.""" + def __init__(self, dbus_name): + self.dbus_name = dbus_name + def __get__(self, instance, owner=None): + if instance is None: + return self + variant = instance._proxy.get_cached_property(self.dbus_name) + return variant.unpack() if variant else None -class DBusProxy: - def __init__(self, name, object_path, interface_name): + +class DBusObject: + """A wrapper class around a DBusProxy instance.""" + def __init__(self, bus_name, object_path, interface_name): # Save the dbus info - self.name = name + self.bus_name = bus_name self.object_path = object_path self.interface_name = interface_name - - # Callback for when properties are changed - self.on_property_changed = None - self.on_property_invalidated = None - - # List of signals that the proxy is connected to - self._sig_handles = [] - - # Get the initial properties for this DBus Proxy - var = Gio.DBusConnection.call_sync( - Gio.bus_get_sync(Gio.BusType.SYSTEM), - name, - object_path, - "org.freedesktop.DBus.Properties", - "GetAll", - GLib.Variant("(s)", (interface_name,)), - None, - 0, - -1, - None) - - self.props = DBusProps() - (props,) = var.unpack() - for key, value in props.items(): - setattr(self.props, key, value) - # Main proxy for DBus Object self._proxy = Gio.DBusProxy.new_for_bus_sync( Gio.BusType.SYSTEM, Gio.DBusProxyFlags.NONE, None, - name, + bus_name, object_path, interface_name, None) - + # List of signals that self._proxy is connected to + self._sig_handles = [] + # Callback for when properties are changed + self.on_property_changed = None # Register for future changes to properties self.signal_connect("g-properties-changed", self._on_g_properties_changed) @@ -97,26 +85,18 @@ def dispose(self): self._proxy = None def _on_g_properties_changed(self, proxy, changed, invalidated): - for key, value in changed.unpack().items(): - setattr(self.props, key, value) - if self.on_property_changed: - self.on_property_changed(key, value) - # do nothing with invalidated at this time + if self.on_property_changed: + self.on_property_changed() -class BoltManagerProxy(DBusProxy): +class BoltManager(DBusObject): + """A DBusObject class for interacting with bolt's Manager interface.""" def __init__(self): # Perform parent initialization - super().__init__( - BOLT_BUS_NAME, - BOLT_OBJECT_PATH, - "org.freedesktop.bolt1.Manager" - ) - + super().__init__(BOLT_BUS_NAME, BOLT_OBJECT_PATH, BOLT_MANAGER_IFACE) # Callbacks self.on_device_added = None self.on_device_removed = None - # Connect to g-signal for event handling self.signal_connect('g-signal', self._on_g_signal) @@ -128,38 +108,44 @@ def _on_g_signal(self, proxy, sender, signal, parameters): (obj_path,) = parameters.unpack() self.on_device_removed(obj_path) - def list_domains(self): + def ListDomains(self): return self._proxy.ListDomains() - def list_devices(self): + def ListDevices(self): return self._proxy.ListDevices() - def enroll_device(self, uid): + def EnrollDevice(self, uid): self._proxy.EnrollDevice('(sss)', uid, 'auto', '') - def forget_device(self, uid): + def ForgetDevice(self, uid): self._proxy.ForgetDevice('(s)', uid) -class BoltDeviceProxy(DBusProxy): +class BoltDevice(DBusObject): + """A DBusObject class for interacting with bolt's Device interface.""" + + Vendor = DBusProperty("Vendor") + Name = DBusProperty("Name") + Generation = DBusProperty("Generation") + Type = DBusProperty("Type") + Uid = DBusProperty("Uid") + Status = DBusProperty("Status") + Stored = DBusProperty("Stored") + LinkSpeed = DBusProperty("LinkSpeed") + def __init__(self, obj_path): - super().__init__( - BOLT_BUS_NAME, - obj_path, - "org.freedesktop.bolt1.Device" - ) + super().__init__(BOLT_BUS_NAME, obj_path, BOLT_DEVICE_IFACE) - def authorize(self): + def Authorize(self): self._proxy.Authorize('(s)', 'auto') class BoltSection(SettingsSection): - def __init__(self, bolt_manager, bolt_device): self.bolt_manager = bolt_manager self.bolt_device = bolt_device - self.bolt_device.on_property_changed = lambda k, v: self.refresh() - super().__init__("{0} {1}".format(bolt_device.props.Vendor, bolt_device.props.Name)) + self.bolt_device.on_property_changed = lambda: self.refresh() + super().__init__("{0} {1}".format(bolt_device.Vendor, bolt_device.Name)) widget = SettingsWidget() self.status_label = SettingsLabel() @@ -167,7 +153,7 @@ def __init__(self, bolt_manager, bolt_device): self.details_btn = Gtk.ToggleButton(label=_("Details")) self.details_btn.connect("toggled", lambda w: self.details_revealer.set_reveal_child(w.get_active())) self.auth_btn = Gtk.Button(label=_("Authorize")) - self.auth_btn.connect("clicked", lambda w: self.bolt_device.authorize()) + self.auth_btn.connect("clicked", lambda w: self.bolt_device.Authorize()) self.trust_btn = Gtk.Button(label=_("Trust")) self.trust_btn.connect("clicked", self.on_trust_btn_clicked) button_box = Gtk.ButtonBox(orientation=Gtk.Orientation.HORIZONTAL) @@ -181,36 +167,34 @@ def __init__(self, bolt_manager, bolt_device): list_box = Gtk.ListBox() list_box.set_selection_mode(Gtk.SelectionMode.NONE) list_box.set_header_func(self.update_header) - generation = self.bolt_device.props.Generation + generation = self.bolt_device.Generation list_box.add(build_detail_row(_("Generation"), format_generation(generation))) self.details_bandwidth_label = Gtk.Label(label="-") list_box.add(build_detail_row(_("Bandwidth"), self.details_bandwidth_label)) - dev_type = self.bolt_device.props.Type + dev_type = self.bolt_device.Type list_box.add(build_detail_row(_("Type"), dev_type)) - uid = self.bolt_device.props.Uid + uid = self.bolt_device.Uid list_box.add(build_detail_row("Uid", uid)) self.details_revealer = self.add_reveal_row(list_box) self.refresh() def on_trust_btn_clicked(self, widget): - uid = self.bolt_device.props.Uid - stored = self.bolt_device.props.Stored + uid = self.bolt_device.Uid + stored = self.bolt_device.Stored if stored: - self.bolt_manager.forget_device(uid) + self.bolt_manager.ForgetDevice(uid) else: - self.bolt_manager.enroll_device(uid) + self.bolt_manager.EnrollDevice(uid) def update_header(self, row, before): if before: row.set_header(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)) def refresh(self): - status = self.bolt_device.props.Status - stored = self.bolt_device.props.Stored - link_speed = None - if hasattr(self.bolt_device.props, "LinkSpeed"): - link_speed = self.bolt_device.props.LinkSpeed + status = self.bolt_device.Status + stored = self.bolt_device.Stored + link_speed = self.bolt_device.LinkSpeed # Update the status label text = _("Disconnected") @@ -307,64 +291,65 @@ def on_module_selected(self): # Check that org.freedesktop.bolt is available on the system if not self.is_bolt_available(): - text = _("'%s' D-Bus service is not available on your system.") % BOLT_BUS_NAME + text = _("The %s service is missing or not activatable.") % BOLT_BUS_NAME self.disabled_label.set_markup(f"{text}") - self.disabled_retry_button.set_visible(False) + self.disabled_retry_button.set_visible(True) + self.disabled_retry_button.set_sensitive(True) page = "disabled" - GLib.idle_add(self.set_initial_page, page) + GLib.idle_add(self.set_page, page) return # Initialilze the bolt manager if not self.bolt_manager: - self.bolt_manager = BoltManagerProxy() + self.bolt_manager = BoltManager() self.bolt_manager.on_device_added = self.bolt_device_added self.bolt_manager.on_device_removed = self.bolt_device_removed # Check if there are any domains # If thunderbolt or usb4 is available, there will be 1 domain per controller # Each domain has a corresponding device with the same uuid - if not self.bolt_manager.list_domains(): + if not self.bolt_manager.ListDomains(): text = _("Thunderbolt or USB4 is not detected on your system.") self.disabled_label.set_markup(f"{text}") self.disabled_retry_button.set_visible(False) page = "disabled" - GLib.idle_add(self.set_initial_page, page) + GLib.idle_add(self.set_page, page) return # Setup and display the page self.setup() - page = self.page_name() - GLib.idle_add(self.set_initial_page, page) + GLib.idle_add(self.set_page, self.page_name()) def disable_retry_on_clicked(self, widget): self.disabled_retry_button.set_sensitive(False) - self.on_module_selected() + GLib.idle_add(self.on_module_selected) - def set_initial_page(self, page): + def set_page(self, page): self.sidePage.stack.set_visible_child_name(page) - def dispose_manager(self): - if self.bolt_manager: - self.bolt_manager.dispose() - self.bolt_manager = None - - def dispose_device_sections(self): - for obj_path in list(self.bolt_devices.keys()): - self.bolt_device_removed(obj_path, False) - def setup(self): - for obj_path in self.bolt_manager.list_devices(): - device = BoltDeviceProxy(obj_path) - # Skip the host device - if device.props.Type == "host": + for obj_path in self.bolt_manager.ListDevices(): + params = GLib.Variant("(ss)", (BOLT_DEVICE_IFACE, "Type")) + var = Gio.DBusConnection.call_sync( + Gio.bus_get_sync(Gio.BusType.SYSTEM), + BOLT_BUS_NAME, + obj_path, + "org.freedesktop.DBus.Properties", + "Get", + params, + GLib.VariantType("(v)"), + 0, + -1, + None) + (device_type,) = var.unpack() + if device_type == 'host': continue - # Add the device self.bolt_device_added(obj_path, False) def bolt_device_added(self, obj_path, change_page=True): if obj_path not in self.bolt_devices: # Build the section - device = BoltDeviceProxy(obj_path) + device = BoltDevice(obj_path) section = BoltSection(self.bolt_manager, device) section.show_all() # Add to the page @@ -372,7 +357,7 @@ def bolt_device_added(self, obj_path, change_page=True): page = self.sidePage.stack.get_child_by_name("settings") page.pack_start(section, False, False, 0) if change_page: - self.sidePage.stack.set_visible_child_name(self.page_name()) + GLib.idle_add(self.set_page, self.page_name()) def bolt_device_removed(self, obj_path, change_page=True): if obj_path in self.bolt_devices: @@ -385,14 +370,13 @@ def bolt_device_removed(self, obj_path, change_page=True): # Finally - remove the settings section from the paths dict del self.bolt_devices[obj_path] if change_page: - self.sidePage.stack.set_visible_child_name(self.page_name()) + GLib.idle_add(self.set_page, self.page_name()) def page_name(self): return "settings" if len(self.bolt_devices) > 0 else "empty" def is_bolt_available(self): try: - # bolt is an activatable d-bus service - ask d-bus if it knows about it var = Gio.DBusConnection.call_sync( Gio.bus_get_sync(Gio.BusType.SYSTEM), "org.freedesktop.DBus", @@ -400,13 +384,12 @@ def is_bolt_available(self): "org.freedesktop.DBus", "ListActivatableNames", None, - None, + GLib.VariantType("(as)"), 0, -1, - None - ) + None) (bus_names,) = var.unpack() return BOLT_BUS_NAME in bus_names - except GLib.Error: - pass + except Exception as e: + print(e) return False \ No newline at end of file From c2eae2ba8a4950a46de17e56bb640d8f120354c7 Mon Sep 17 00:00:00 2001 From: Timothy Feierabend Date: Sat, 4 Apr 2026 13:51:56 -0500 Subject: [PATCH 3/4] thunderbolt: removed os import os module no longer needed due to refactoring of module initialization --- .../share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py index 36e40bde55..67277fe485 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py +++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py @@ -1,7 +1,5 @@ #!/usr/bin/python3 -import os - import gi gi.require_version("Gtk", "3.0") gi.require_version("Gio", "2.0") From 4110305435ea37b7602ca667020a64ca068e43d9 Mon Sep 17 00:00:00 2001 From: Timothy Feierabend Date: Sat, 4 Apr 2026 14:20:44 -0500 Subject: [PATCH 4/4] thunderbolt: add missing newline from EOF --- .../share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py index 67277fe485..4377c57b30 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py +++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_thunderbolt.py @@ -390,4 +390,4 @@ def is_bolt_available(self): return BOLT_BUS_NAME in bus_names except Exception as e: print(e) - return False \ No newline at end of file + return False