-
Notifications
You must be signed in to change notification settings - Fork 1
Home
XKM is a GTK4/libadwaita graphical frontend for managing Linux kernels on Ubuntu-based systems. It wraps the system's apt package manager and dpkg tooling into a unified interface, letting users browse, install, remove, hold, and manage kernels across three families — XanMod, Liquorix, and standard mainline/Ubuntu kernels — without writing any terminal commands.
The application is a single Python 3 script. It uses the python-apt bindings to read the local package cache directly (no subprocess for reads), and pkexec to run privileged write operations like installs and removals.
XKM requires a small set of system packages, all available in the standard Ubuntu repositories.
On a typical Ubuntu desktop most of these are already installed, but on minimal/server installs you may need to install some manually.
-
python3— the runtime (Python 3.10 or newer) -
python3-apt— Python bindings for the apt package library (used to read the package cache) -
python3-gi— GObject introspection bindings (required for GTK and libadwaita) -
gir1.2-gtk-4.0— GTK4 introspection data -
gir1.2-adw-1— libadwaita introspection data -
libadwaita-1-0— the libadwaita runtime library -
policykit-1— providespkexecfor privilege escalation
(installed by default on all desktop Ubuntu flavors)
sudo apt update && sudo apt install \
python3 \
python3-apt \
python3-gi \
gir1.2-gtk-4.0 \
gir1.2-adw-1 \
libadwaita-1-0 \
policykit-1
<html><head><style type="text/css"><!--br {mso-data-placement:same-cell;}--></style></head><body>
Distro / Base | Version | Status | Notes
-- | -- | -- | --
Ubuntu | 22.04 LTS (Jammy) | ✓ Supported | All packages available
Ubuntu | 24.04 LTS (Noble) | ✓ Supported | All packages available
Ubuntu | 24.10 / 25.04 | ✓ Supported | —
Linux Mint | 21.x | ✓ Supported | Based on Ubuntu 22.04
Linux Mint | 22.x | ✓ Supported | Based on Ubuntu 24.04
Pop!_OS | 22.04 / 24.04 | ✓ Supported | —
Kubuntu / Xubuntu / Lubuntu | 22.04 or newer | ✓ Supported | —
elementary OS | 7.x | ✓ Should work | Based on Ubuntu 22.04
Ubuntu | 20.04 LTS (Focal) | ✗ Not supported | libadwaita not available
</body></html>
## Architecture
XKM is structured around three layers:
**Data layer** — the `apt` cache is opened via `python-apt` bindings inside a background thread. Packages are iterated, classified, and assembled into plain Python dicts. No direct subprocess calls are made for reading.
**Model layer** — classified packages are converted into `KernelRow` GObject instances and placed into `Gio.ListStore` stores (one each for XanMod, Liquorix, and meta-packages). Mainline versioned packages don't use a flat store — they live in a dict keyed by kernel version string (`_mainline_groups`), which drives the grouped card UI.
**View layer** — the UI is built with GTK4 and libadwaita. XanMod and Liquorix use `Gtk.ListView` with a `Gtk.FilterListModel` for efficient filtering. The Mainline tab uses a manually constructed scrollable box of cards, rebuilt on demand using a chunked `GLib.idle_add` scheduler to avoid freezing the interface.
The two layers communicate almost entirely through `GLib.idle_add`, which marshals results from background threads safely back onto the GTK main thread.
---
## Startup & Initialization
When the window first appears (on the `realize` signal), XKM immediately kicks off a background thread to load the package cache. While that runs, a "Loading package cache…" placeholder is shown in the Mainline tab so the interface is never blank or unresponsive.
A few seconds after the window appears, a separate background thread checks GitHub for a newer version of XKM. This is fire-and-forget — if it fails (no network, API down), it silently does nothing.
The periodic auto-refresh timer is also set up at this point, configured by the `auto_check_hours` config value. It calls `_reload_kernels_async` on a repeating schedule to keep the package list fresh.
---
## The Package Cache
XKM reads the system's apt package database directly using the `python-apt` library rather than shelling out to `apt list` or similar. This is significantly faster and avoids parsing human-readable text output.
The cache is opened via a custom `SilentCache` subclass that suppresses the stderr noise apt normally emits during cache initialization. It does this by temporarily redirecting file descriptor 2 to `/dev/null` for the duration of the `apt.Cache.__init__` call, then restoring the real stderr afterward. This is done at the file descriptor level (not just `sys.stderr`) because some of apt's output comes from native C code that bypasses Python's I/O layer entirely.
`apt_pkg` configuration is also set early in the startup sequence to enforce quiet mode and non-interactive behavior globally, preventing any apt subprocess from trying to prompt the user.
When the cache needs to be refreshed after an install or remove operation, `cache.open(None)` is called on the existing cache object rather than constructing a new one. This re-reads the database from disk and picks up any changes made by the completed operation.
---
## Kernel Classification
Every package in the cache is passed through a set of classification functions before any UI is built. These functions are pure string operations and run entirely in the background thread.
**`is_xanmod_name(name)`** checks for two forms of XanMod packages. Meta-packages (like `linux-xanmod-x64v3` or `linux-xanmod-edge`) start with known prefixes. Versioned packages (like `linux-image-6.18.3-x64v3-xanmod1`) start with `linux-image-` or `linux-headers-` and contain `xanmod` anywhere in the name. Both forms must be handled because the XanMod repository ships them differently.
**`is_liquorix_name(name)`** checks for the `linux-image-liquorix-` and `linux-headers-liquorix-` prefixes, which are consistent across all Liquorix packages.
**`is_generic_kernel_name(name)`** matches standard Ubuntu/mainline packages starting with `linux-image-`, `linux-headers-`, `linux-modules-`, and a few variant prefixes like `linux-image-unsigned-` and `linux-image-oem-`. Any package that matches XanMod or Liquorix is excluded first, so there's no overlap.
**`is_mainline_meta(name)`** identifies metapackages like `linux-generic` and `linux-lowlatency` — packages that track a kernel flavour without embedding a version number in the name. These are treated separately from versioned packages because they behave differently: installing one will always pull in the latest kernel, and they're never grouped by version number.
**`extract_kernel_version(name)`** pulls the numeric version string out of a package name using a compiled regex. For example, `linux-image-6.14.0-37-generic` yields `6.14.0-37`. This version string is the grouping key for the Mainline tab's card view.
**`pkg_category(name)`** assigns a human-readable category label (Image, Headers, Modules, Modules Extra, etc.) used to sort packages within a version card.
---
## The Three Kernel Families
**XanMod** kernels are built with aggressive compiler optimizations and patches aimed at desktop responsiveness and throughput. They come in CPU-specific builds (v1 through v4) and specialized variants like `edge` (latest upstream) and `lts` (long-term support). XanMod packages come from a third-party apt repository at `deb.xanmod.org` and require a separate signing key and source file.
**Liquorix** kernels focus on low-latency performance, particularly for audio work and gaming. They use a tuned scheduler configuration and are distributed via a Launchpad PPA (`ppa:damentz/liquorix`). Liquorix only ships two packages per release — an image and a headers package — making it simpler than XanMod in terms of package count.
**Mainline** kernels are the standard Ubuntu-provided kernels. Each kernel version ships multiple packages: the kernel image itself, headers for building external modules, and various `linux-modules-*` packages that contain drivers. Some versions also ship unsigned variants (`linux-image-unsigned-`) and GPU-specific module packages for NVIDIA. All of these are grouped together under a single version card in the UI.
---
## XanMod Flavors
XanMod's CPU-level builds are called flavors. XKM extracts the flavor from a package name by scanning for specific tokens:
- `x64v4`, `x64v3`, `x64v2`, `x64v1` — CPU optimization level embedded in the package name
- `-edge`, `-lts`, `-rt` — variant suffixes
- `-v4`, `-v3`, `-v2`, `-v1` — alternative form used in some meta-package names
The tokens are checked in order from most specific to least specific. For example, `x64v3` is checked before `-v3` so that a package containing `x64v3` doesn't accidentally match the shorter pattern and get misclassified.
Packages that contain `xanmod` but none of the above tokens are labeled `generic`.
The flavor filter dropdown on the XanMod tab uses a `Gtk.CustomFilter` rather than a `Gtk.StringFilter`. This is because filtering needs to check two things simultaneously — the text search query and the selected flavor — and `Gtk.StringFilter` can only match against a single property. The custom filter function is called by GTK for each row and returns `True` or `False` based on both conditions.
---
## GPU Detection & Package Filtering
At startup, XKM runs `lspci` and scans the output for GPU-related lines (VGA, 3D, Display controllers). It builds a set of detected vendors: `nvidia`, `amd`, `intel`.
This set is then used to filter Mainline packages. Packages matching patterns like `linux-modules-nvidia-*`, `linux-modules-amd-*`, and `linux-modules-intel-*` are marked as GPU-relevant only if the corresponding vendor was detected. If no NVIDIA GPU is found, all `linux-modules-nvidia-*` packages still appear in the UI but are visually dimmed, struck through, and have their checkboxes disabled so they can't be accidentally selected and installed.
This prevents a common mistake where users install GPU driver modules for hardware they don't have, which can cause module signing issues or bloat.
---
## The Mainline Grouped View
Unlike XanMod and Liquorix, which use GTK's built-in `Gtk.ListView` with a model/factory pattern, the Mainline tab is built manually as a scrollable vertical box of cards. Each card represents one kernel version and contains all of that version's packages grouped by category.
This approach was chosen because `Gtk.ListView` is designed for flat, homogeneous lists, and the grouped card layout with collapsible headers, tri-state checkboxes, and per-category sub-labels doesn't map naturally onto it. The trade-off is that the card view must be rebuilt from scratch whenever the data or search query changes.
To prevent this rebuild from freezing the UI, it uses a chunked scheduling approach. The rebuild function computes a list of "chunks" — one callable per card — and then dispatches them one at a time via `GLib.idle_add`. Each call to `GLib.idle_add` returns control to the GTK event loop before scheduling the next chunk, so the interface stays responsive while potentially dozens of cards are being constructed.
A `_rebuild_generation` counter is incremented every time a new rebuild is triggered. Each dispatched chunk checks whether its generation still matches the current one before doing any work. If a newer rebuild has been triggered (e.g. the user typed another character in the search box), all in-flight chunks from the previous rebuild abort immediately without touching the UI.
---
## Installing Kernels
Installation runs as a background thread that streams the output of `pkexec apt install -y <packages>` line by line, appending each line to the log view in real time via `GLib.idle_add`. This means the user can watch the download and installation progress as it happens.
Package names are validated against a strict regex (`[a-z0-9][a-z0-9+\-.]*`) before being passed to the install command. This is a safety measure: if a malformed or unexpected package name somehow ends up in the selection (due to a bug or a corrupt cache), it's rejected before it can be passed to a shell. The validation follows Debian's official package naming policy.
After installation completes, XKM automatically:
1. Runs `update-grub` to register the new kernel with the bootloader
2. Checks for new directories under `/usr/lib/modules/` to find the freshly installed kernel version
3. Runs DKMS to rebuild any third-party kernel modules for the new version
4. Prompts the user to reboot
If the **Auto-remove after install** checkbox is ticked, `_auto_remove_old_kernels` is also called immediately after installation finishes.
---
## Install + Hold
The Install + Hold action is a two-step sequence: install the selected packages, then immediately apply `apt-mark hold` to all of them. If the install fails, the hold step is skipped entirely. If the hold step fails after a successful install, the user is shown an error with the manual command needed to apply the hold themselves.
Held packages are excluded from `apt upgrade` and `apt dist-upgrade` automatically. They won't be upgraded, downgraded, or removed by apt unless the hold is explicitly released first. This makes Install + Hold the recommended approach when pinning a specific XanMod version for long-term stability.
---
## Removing Kernels
Before any removal begins, XKM checks whether any of the selected packages belong to the currently running kernel. If they do, the operation is blocked with an error dialog. Removing the active kernel would make the system unbootable on next restart.
The user is then asked to choose between **Remove** and **Purge**. Remove uninstalls the packages but leaves configuration files on disk. Purge removes everything including config files. XKM deliberately does not use `--auto-remove` with either option. While `--auto-remove` would clean up dependencies automatically, it can cascade to removing shared packages like `linux-firmware` or `linux-base` that other installed kernels still depend on, which is difficult to undo. Users who want dependency cleanup can run `apt autoremove` manually afterward.
After a successful removal, `update-grub` runs automatically to remove the deleted kernel from the bootloader menu.
---
## Holding & Unholding Kernels
Held packages are detected at data-load time by running `dpkg --get-selections` and collecting all packages with a `hold` status. This is done once per data load in a single subprocess call rather than querying each package individually, which would be significantly slower for large package lists.
Held packages display an orange **[Held]** badge in the UI. Their status field reads "Held" rather than "Installed".
The Hold and Unhold buttons call `pkexec apt-mark hold` and `pkexec apt-mark unhold` respectively, then reload the package list so the badges update to reflect the new state.
Auto-remove explicitly skips held packages. A held kernel will never be touched by the auto-remove logic regardless of its age or version.
---
## Auto-Remove
Auto-remove identifies old installed kernels and offers to remove them. The logic works as follows:
1. Collect all installed kernel rows across all stores and groups
2. Group them by version string
3. Sort versions newest-first using `apt_pkg.version_compare`
4. Build a "keep" set containing: the currently active kernel version, and the two most recent versions
5. Everything outside the keep set that isn't held is a candidate for removal
The user is shown a count of candidate packages and asked whether to Remove or Purge, using the same dialog and the same underlying command as manual removal.
---
## DKMS Handling
DKMS (Dynamic Kernel Module Support) is a framework that automatically recompiles out-of-tree kernel modules (like NVIDIA proprietary drivers) when a new kernel is installed. XKM triggers DKMS rebuilds automatically after any installation that adds new kernel module directories.
The process:
1. Before installation, XKM snapshots the contents of `/usr/lib/modules/`
2. After installation, it compares the directory listing to find newly added kernel versions
3. For each new kernel version, it runs `dkms autoinstall -k <version>`
4. After autoinstall, it queries `dkms status` to verify each module was built successfully
5. Any modules in a state other than `installed` or `built` are flagged in a warning summary
If DKMS reports failures, the user is shown a warning dialog explaining that the kernel was installed successfully but some driver modules may not work. They can choose to stay and review the log or reboot anyway.
The currently running kernel is excluded from DKMS processing — it's already built and running, so rebuilding it would be wasteful and potentially disruptive.
---
## Bootloader Updates
`update-grub` is run automatically after both installs and removals. While the kernel package's own post-install/post-remove scripts normally handle this, running it explicitly acts as a safety net for cases where those scripts fail silently or where the user has a non-standard GRUB configuration.
`update-grub` output is streamed into the current log session so the user can see what GRUB detected and registered. After removal, the grub update runs before the log session is formally closed, so all output lands in the same log file.
---
## Repository Management
On first launch, XKM checks whether the XanMod and Liquorix repositories are present on the system. For XanMod, it looks for the source file at `/etc/apt/sources.list.d/xanmod-kernel.list` and the keyring at `/usr/share/keyrings/xanmod-archive-keyring.gpg`, and also scans all source files for the `deb.xanmod.org` domain as a fallback. For Liquorix, it checks for the known PPA source files and also scans sources for `damentz/liquorix` and `liquorix.net`.
If either repository is missing, a dialog appears offering to add them. This check only runs once per session — a flag is set after the first check so the dialog doesn't reappear if the user dismisses it and navigates away.
**Adding XanMod** runs a bash script via `pkexec` that: creates the keyrings directory if needed, downloads and dearmors the GPG key, writes the source list entry, and runs `apt-get update`. All of this happens in a single pkexec invocation to minimize password prompts.
**Adding Liquorix** uses `add-apt-repository -y ppa:damentz/liquorix` followed by `apt update`. This relies on `add-apt-repository` being available, which it is on all standard Ubuntu desktop installations.
After either repository is added, the package list is reloaded so the newly available kernels appear immediately.
---
## Privilege Escalation
All write operations — installs, removals, holds, repository additions, DKMS builds, and grub updates — are run via `pkexec`. This is the standard PolicyKit-based privilege escalation tool on modern Ubuntu desktops. It prompts the user for their password through the desktop's authentication agent rather than in a terminal.
XKM tracks consecutive `pkexec` failures with a counter. If pkexec fails twice in a row, a yellow warning bar appears at the top of the window explaining that privilege escalation seems to be failing. This helps users in restricted environments (for example, systems without sudo rights, or locked-down enterprise configurations) understand why operations aren't working. The banner dismisses itself automatically if a subsequent pkexec call succeeds.
---
## Search & Filtering
The search bar applies to all three tabs simultaneously. For XanMod and Liquorix, filtering is handled by GTK's built-in filter model infrastructure. XanMod uses a `Gtk.CustomFilter` that checks both the text query and the selected flavor. Liquorix uses a `Gtk.StringFilter` that matches against the package's markup string (which includes the name, version, and status tags).
Both filter models connect directly to the search entry's `search-changed` signal. However, the signal is debounced: a 250ms timeout is set on each keystroke, and only when the user stops typing for 250ms does the actual filtering run. This prevents every intermediate keystroke from triggering a potentially expensive rebuild.
For the Mainline tab, filtering is done manually inside `_rebuild_mainline_ui`. Each version group and each meta-package is checked against the query string before a card is constructed for it. Groups with no matching packages are skipped entirely, so a search for `6.14` would only show cards for the 6.14.x version groups and hide everything else.
---
## Logging
Every privileged operation starts a log session. A log file is created in `~/.config/xanmod-kernel-manager/logs/` with a filename combining the operation type and a timestamp (e.g. `install_2025-01-15_14-32-01.log`).
All subprocess output is written to both the on-screen log view and the file simultaneously. The file handle is kept open for the duration of the operation and closed when the session ends, ensuring output is never lost even if the application crashes mid-operation.
The on-screen log auto-scrolls to the bottom as new lines arrive. It uses a `Gtk.TextBuffer` mark positioned at the end of the buffer, which GTK's scroll mechanism can track reliably even as new content is inserted.
The log view is hidden by default behind a **Show Details** toggle button. It expands with a slide animation using a `Gtk.Revealer`. XKM automatically shows the log panel when starting any operation, so the user doesn't have to manually reveal it to see what's happening.
---
## Configuration
XKM stores its configuration as a JSON file at `~/.config/xanmod-kernel-manager/config.json`. The config is loaded and saved using module-level functions (`load_config`, `save_config`) rather than instance methods, so any part of the codebase can access it without holding a reference to a specific object.
The config tracks:
- `auto_check_hours` — how often to automatically refresh the package list (default: every 6 hours)
- `auto_remove_after_install` — whether to automatically run auto-remove after a successful install (default: off)
- `win_size` — the last window size, restored on next launch
- `dark_mode` — whether the dark color scheme is active
The window size is saved when the window closes. The auto-remove checkbox state is also saved at close time. All other settings take effect immediately when changed.
If the config file doesn't exist or can't be parsed, all values fall back to the defaults defined in `DEFAULT_CONFIG`. Partial configs are supported — any missing keys are filled in from defaults at load time.
---
## Update Checker
A few seconds after the window opens, XKM fetches the latest release information from the GitHub API in a background thread. It compares the remote version tag against the current `APP_VERSION` string using a tuple comparison on the numeric version components.
If a newer version is available, a persistent toast notification appears at the bottom of the window with a button to open the release page. The toast has no timeout — it stays visible until dismissed. If no update is found, or if the network request fails for any reason, nothing happens and no error is shown.
Version strings are split on both dots and hyphens, and only numeric segments are compared. This means tags like `v2.1.0` and `2.1.0` are handled identically, and pre-release suffixes that aren't purely numeric are ignored rather than causing a crash.
---
## Version Comparison
XKM uses `apt_pkg.version_compare` for all kernel version sorting. This is the same comparison function apt itself uses, which correctly handles the complex versioning schemes Ubuntu kernels use (e.g. `6.14.0-37` vs `6.14.0-38`, or epoch-prefixed versions).
The comparison is wrapped in a small helper that falls back to a basic Python string comparison if `apt_pkg.version_compare` raises an exception, which can happen with malformed version strings.
Version sorting is used in three places: ordering packages within a store (active first, then installed, then available, then newest-first within each group), ordering version cards in the Mainline tab, and in the auto-remove logic to determine which versions are "old enough" to be candidates for removal.
---
## Security Considerations
**Package name validation** — before any package name is passed to an install or remove command, it's checked against a regex that only allows characters valid in Debian package names. This prevents a hypothetical injection where a malformed package name in the apt cache could smuggle shell metacharacters into a command.
**No shell=True** — all subprocess calls use list arguments rather than shell strings. Even though the commands go through `pkexec`, which itself invokes a shell for the `bash -c` cases, the XKM-controlled arguments never go through shell interpolation.
**No direct apt library writes** — all write operations (install, remove, hold) use subprocesses rather than the `python-apt` library's write interfaces. This is intentional: `python-apt`'s write path is less tested for edge cases and doesn't easily integrate with `pkexec`-style privilege escalation.
**Stderr suppression scope** — the stderr redirect used during cache initialization is scoped tightly using `try/finally` to guarantee the real stderr is always restored, even if an import or initialization raises an exception.
---
## Self-Tests
XKM includes a suite of unit tests that can be run without a display, a running desktop, or any apt packages installed. They cover the classification functions (`is_xanmod_name`, `is_liquorix_name`, `is_generic_kernel_name`), the flavor extractor, the kernel version regex, and the update checker's version comparison logic.
Run them with:
```bash
python3 XKM --testThe tests use Python's built-in unittest module and exit with code 0 on success or 1 on failure, making them suitable for use in CI pipelines. They are pure logic tests — no GTK, no apt cache, no filesystem access — so they run in any environment that has Python 3 available.