linux-kernel-manager 0.1.2__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.
- linux_kernel_manager-0.1.2.dist-info/METADATA +271 -0
- linux_kernel_manager-0.1.2.dist-info/RECORD +45 -0
- linux_kernel_manager-0.1.2.dist-info/WHEEL +4 -0
- linux_kernel_manager-0.1.2.dist-info/entry_points.txt +3 -0
- linux_kernel_manager-0.1.2.dist-info/licenses/LICENSE +675 -0
- lkm/__init__.py +2 -0
- lkm/cli/__init__.py +0 -0
- lkm/cli/main.py +332 -0
- lkm/cli/output.py +57 -0
- lkm/core/__init__.py +0 -0
- lkm/core/backends/__init__.py +54 -0
- lkm/core/backends/apk.py +46 -0
- lkm/core/backends/apt.py +77 -0
- lkm/core/backends/base.py +81 -0
- lkm/core/backends/dnf.py +44 -0
- lkm/core/backends/nix.py +195 -0
- lkm/core/backends/pacman.py +78 -0
- lkm/core/backends/portage.py +107 -0
- lkm/core/backends/xbps.py +139 -0
- lkm/core/backends/zypper.py +44 -0
- lkm/core/kernel.py +95 -0
- lkm/core/manager.py +243 -0
- lkm/core/providers/__init__.py +62 -0
- lkm/core/providers/base.py +88 -0
- lkm/core/providers/distro.py +139 -0
- lkm/core/providers/gentoo.py +68 -0
- lkm/core/providers/liquorix.py +80 -0
- lkm/core/providers/lkf_build.py +355 -0
- lkm/core/providers/local_file.py +104 -0
- lkm/core/providers/mainline.py +106 -0
- lkm/core/providers/nixos.py +76 -0
- lkm/core/providers/void.py +69 -0
- lkm/core/providers/xanmod.py +81 -0
- lkm/core/system.py +235 -0
- lkm/gui/__init__.py +0 -0
- lkm/gui/app.py +94 -0
- lkm/gui/kernel_model.py +98 -0
- lkm/gui/main_window.py +385 -0
- lkm/gui/widgets/__init__.py +0 -0
- lkm/gui/widgets/gentoo_compile_dialog.py +132 -0
- lkm/gui/widgets/kernel_view.py +121 -0
- lkm/gui/widgets/lkf_build_dialog.py +370 -0
- lkm/gui/widgets/log_panel.py +78 -0
- lkm/gui/widgets/note_dialog.py +38 -0
- lkm/qt.py +122 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Void Linux provider — xbps package manager."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import platform
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
|
|
8
|
+
from lkm.core.kernel import KernelEntry, KernelFamily, KernelStatus, KernelVersion
|
|
9
|
+
from lkm.core.providers.base import KernelProvider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VoidProvider(KernelProvider):
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def id(self) -> str:
|
|
16
|
+
return "void"
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def display_name(self) -> str:
|
|
20
|
+
return "Void Linux"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def family(self) -> KernelFamily:
|
|
24
|
+
return KernelFamily.DISTRO
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def supported_arches(self) -> list[str]:
|
|
28
|
+
return ["*"]
|
|
29
|
+
|
|
30
|
+
def is_available(self) -> bool:
|
|
31
|
+
import shutil
|
|
32
|
+
return bool(shutil.which("xbps-install"))
|
|
33
|
+
|
|
34
|
+
def list(self, arch: str, refresh: bool = False) -> list[KernelEntry]:
|
|
35
|
+
running = platform.release()
|
|
36
|
+
if refresh:
|
|
37
|
+
# Consume and discard sync output
|
|
38
|
+
list(self._backend.sync())
|
|
39
|
+
|
|
40
|
+
names = self._backend.list_available_kernels()
|
|
41
|
+
entries = []
|
|
42
|
+
for name in names:
|
|
43
|
+
m = re.search(r"(\d+\.\d+(?:\.\d+)?)", name)
|
|
44
|
+
if not m:
|
|
45
|
+
continue
|
|
46
|
+
try:
|
|
47
|
+
ver = KernelVersion.parse(m.group(1))
|
|
48
|
+
except ValueError:
|
|
49
|
+
continue
|
|
50
|
+
installed = self._backend.is_installed(name)
|
|
51
|
+
if installed:
|
|
52
|
+
status = KernelStatus.RUNNING if m.group(1) in running else KernelStatus.INSTALLED
|
|
53
|
+
else:
|
|
54
|
+
status = KernelStatus.AVAILABLE
|
|
55
|
+
entries.append(KernelEntry(
|
|
56
|
+
version=ver,
|
|
57
|
+
family=self.family,
|
|
58
|
+
flavor=name,
|
|
59
|
+
arch=arch,
|
|
60
|
+
provider_id=self.id,
|
|
61
|
+
status=status,
|
|
62
|
+
))
|
|
63
|
+
return entries
|
|
64
|
+
|
|
65
|
+
def install(self, entry: KernelEntry) -> Iterator[str]:
|
|
66
|
+
yield from self._backend.install_packages([entry.flavor])
|
|
67
|
+
|
|
68
|
+
def remove(self, entry: KernelEntry, purge: bool = False) -> Iterator[str]:
|
|
69
|
+
yield from self._backend.remove_packages([entry.flavor], purge=purge)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""XanMod provider — x86_64 only, apt repository."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import shutil
|
|
5
|
+
import urllib.request
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
|
|
8
|
+
from lkm.core.kernel import KernelEntry, KernelFamily, KernelStatus, KernelVersion
|
|
9
|
+
from lkm.core.providers.base import KernelProvider
|
|
10
|
+
from lkm.core.system import system_info
|
|
11
|
+
|
|
12
|
+
_REPO_LINE = "deb [signed-by=/usr/share/keyrings/xanmod-archive-keyring.gpg] http://deb.xanmod.org releases main"
|
|
13
|
+
_KEY_URL = "https://dl.xanmod.org/archive.key"
|
|
14
|
+
_FLAVORS = ["xanmod1", "xanmod2", "xanmod3", "xanmod4", "edge", "lts", "rt"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class XanmodProvider(KernelProvider):
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def id(self) -> str:
|
|
21
|
+
return "xanmod"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def display_name(self) -> str:
|
|
25
|
+
return "XanMod"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def family(self) -> KernelFamily:
|
|
29
|
+
return KernelFamily.XANMOD
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def supported_arches(self) -> list[str]:
|
|
33
|
+
return ["amd64"]
|
|
34
|
+
|
|
35
|
+
def is_available(self) -> bool:
|
|
36
|
+
return bool(shutil.which("apt-get"))
|
|
37
|
+
|
|
38
|
+
def availability_reason(self) -> str:
|
|
39
|
+
return "XanMod requires apt (x86_64 only)."
|
|
40
|
+
|
|
41
|
+
def list(self, arch: str, refresh: bool = False) -> list[KernelEntry]:
|
|
42
|
+
running = system_info().running_kernel
|
|
43
|
+
try:
|
|
44
|
+
with urllib.request.urlopen(
|
|
45
|
+
"https://dl.xanmod.org/versions.json", timeout=10
|
|
46
|
+
) as resp:
|
|
47
|
+
import json
|
|
48
|
+
data = json.loads(resp.read())
|
|
49
|
+
except Exception:
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
entries = []
|
|
53
|
+
for item in data.get("kernels", []):
|
|
54
|
+
ver_str = item.get("version", "")
|
|
55
|
+
flavor = item.get("flavor", "xanmod1")
|
|
56
|
+
try:
|
|
57
|
+
ver = KernelVersion.parse(ver_str)
|
|
58
|
+
except ValueError:
|
|
59
|
+
continue
|
|
60
|
+
status = KernelStatus.RUNNING if ver_str in running else KernelStatus.AVAILABLE
|
|
61
|
+
entries.append(KernelEntry(
|
|
62
|
+
version=ver,
|
|
63
|
+
family=self.family,
|
|
64
|
+
flavor=flavor,
|
|
65
|
+
arch=arch,
|
|
66
|
+
provider_id=self.id,
|
|
67
|
+
status=status,
|
|
68
|
+
))
|
|
69
|
+
return entries
|
|
70
|
+
|
|
71
|
+
def install(self, entry: KernelEntry) -> Iterator[str]:
|
|
72
|
+
# Ensure the XanMod repo is present
|
|
73
|
+
yield "Adding XanMod repository...\n"
|
|
74
|
+
yield from self._backend.add_apt_repo(_REPO_LINE, _KEY_URL)
|
|
75
|
+
pkg = f"linux-xanmod-{entry.flavor}"
|
|
76
|
+
yield f"Installing {pkg}\n"
|
|
77
|
+
yield from self._backend.install_packages([pkg])
|
|
78
|
+
|
|
79
|
+
def remove(self, entry: KernelEntry, purge: bool = False) -> Iterator[str]:
|
|
80
|
+
pkg = f"linux-xanmod-{entry.flavor}"
|
|
81
|
+
yield from self._backend.remove_packages([pkg], purge=purge)
|
lkm/core/system.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System detection: distro, architecture, package manager, running kernel.
|
|
3
|
+
|
|
4
|
+
All detection is lazy and cached. Call system_info() to get a SystemInfo
|
|
5
|
+
snapshot; it is safe to call multiple times (returns the same object).
|
|
6
|
+
|
|
7
|
+
Extended from ukm to add NixOS and Void Linux (xbps) support.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import platform
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PackageManagerKind(Enum):
|
|
22
|
+
APT = "apt" # Debian / Ubuntu / Mint / etc.
|
|
23
|
+
PACMAN = "pacman" # Arch / Manjaro / EndeavourOS / CachyOS / etc.
|
|
24
|
+
DNF = "dnf" # Fedora / RHEL / AlmaLinux / Rocky / etc.
|
|
25
|
+
ZYPPER = "zypper" # openSUSE
|
|
26
|
+
APK = "apk" # Alpine
|
|
27
|
+
PORTAGE = "portage" # Gentoo
|
|
28
|
+
XBPS = "xbps" # Void Linux
|
|
29
|
+
NIX = "nix" # NixOS
|
|
30
|
+
UNKNOWN = "unknown"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DistroFamily(Enum):
|
|
34
|
+
DEBIAN = "debian"
|
|
35
|
+
ARCH = "arch"
|
|
36
|
+
FEDORA = "fedora"
|
|
37
|
+
SUSE = "suse"
|
|
38
|
+
ALPINE = "alpine"
|
|
39
|
+
GENTOO = "gentoo"
|
|
40
|
+
VOID = "void"
|
|
41
|
+
NIXOS = "nixos"
|
|
42
|
+
UNKNOWN = "unknown"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class DistroInfo:
|
|
47
|
+
id: str
|
|
48
|
+
id_like: list[str] = field(default_factory=list)
|
|
49
|
+
name: str = ""
|
|
50
|
+
version: str = ""
|
|
51
|
+
codename: str = ""
|
|
52
|
+
family: DistroFamily = DistroFamily.UNKNOWN
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class SystemInfo:
|
|
57
|
+
distro: DistroInfo
|
|
58
|
+
arch: str # normalised: amd64, arm64, armhf, riscv64, ppc64el, s390x, i386
|
|
59
|
+
arch_raw: str # as reported by uname -m
|
|
60
|
+
package_manager: PackageManagerKind
|
|
61
|
+
running_kernel: str # uname -r output
|
|
62
|
+
has_secure_boot: bool
|
|
63
|
+
has_pkexec: bool
|
|
64
|
+
has_sudo: bool
|
|
65
|
+
in_nix_shell: bool # True when inside nix-shell / nix develop
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Internal helpers
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def _read_os_release() -> dict[str, str]:
|
|
73
|
+
result: dict[str, str] = {}
|
|
74
|
+
for path in ("/etc/os-release", "/usr/lib/os-release"):
|
|
75
|
+
try:
|
|
76
|
+
with open(path) as f:
|
|
77
|
+
for line in f:
|
|
78
|
+
line = line.strip()
|
|
79
|
+
if "=" not in line or line.startswith("#"):
|
|
80
|
+
continue
|
|
81
|
+
k, _, v = line.partition("=")
|
|
82
|
+
result[k.strip()] = v.strip().strip('"')
|
|
83
|
+
break
|
|
84
|
+
except FileNotFoundError:
|
|
85
|
+
continue
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _detect_distro() -> DistroInfo:
|
|
90
|
+
data = _read_os_release()
|
|
91
|
+
distro_id = data.get("ID", "").lower()
|
|
92
|
+
id_like = [x.lower() for x in data.get("ID_LIKE", "").split()]
|
|
93
|
+
all_ids = {distro_id} | set(id_like)
|
|
94
|
+
|
|
95
|
+
if distro_id == "nixos" or "nixos" in all_ids:
|
|
96
|
+
family = DistroFamily.NIXOS
|
|
97
|
+
elif distro_id == "void" or "void" in all_ids:
|
|
98
|
+
family = DistroFamily.VOID
|
|
99
|
+
elif any(x in all_ids for x in (
|
|
100
|
+
"debian", "ubuntu", "linuxmint", "pop", "elementary",
|
|
101
|
+
"kali", "parrot", "devuan", "raspbian", "mx",
|
|
102
|
+
"antix", "zorin", "sparky", "bunsenlabs",
|
|
103
|
+
)):
|
|
104
|
+
family = DistroFamily.DEBIAN
|
|
105
|
+
elif any(x in all_ids for x in (
|
|
106
|
+
"arch", "manjaro", "endeavouros", "cachyos",
|
|
107
|
+
"artix", "garuda", "rebornos", "archcraft",
|
|
108
|
+
)):
|
|
109
|
+
family = DistroFamily.ARCH
|
|
110
|
+
elif any(x in all_ids for x in (
|
|
111
|
+
"fedora", "rhel", "centos", "almalinux",
|
|
112
|
+
"rocky", "nobara", "ultramarine", "oracle",
|
|
113
|
+
)):
|
|
114
|
+
family = DistroFamily.FEDORA
|
|
115
|
+
elif any(x in all_ids for x in ("opensuse", "suse", "sles")):
|
|
116
|
+
family = DistroFamily.SUSE
|
|
117
|
+
elif "alpine" in all_ids:
|
|
118
|
+
family = DistroFamily.ALPINE
|
|
119
|
+
elif "gentoo" in all_ids:
|
|
120
|
+
family = DistroFamily.GENTOO
|
|
121
|
+
else:
|
|
122
|
+
family = DistroFamily.UNKNOWN
|
|
123
|
+
|
|
124
|
+
return DistroInfo(
|
|
125
|
+
id=distro_id,
|
|
126
|
+
id_like=id_like,
|
|
127
|
+
name=data.get("NAME", distro_id),
|
|
128
|
+
version=data.get("VERSION_ID", ""),
|
|
129
|
+
codename=data.get("VERSION_CODENAME", ""),
|
|
130
|
+
family=family,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _normalise_arch(raw: str) -> str:
|
|
135
|
+
"""Map uname -m values to Debian-style arch names used throughout lkm."""
|
|
136
|
+
mapping = {
|
|
137
|
+
"x86_64": "amd64",
|
|
138
|
+
"aarch64": "arm64",
|
|
139
|
+
"armv7l": "armhf",
|
|
140
|
+
"armv6l": "armel",
|
|
141
|
+
"i686": "i386",
|
|
142
|
+
"i386": "i386",
|
|
143
|
+
"riscv64": "riscv64",
|
|
144
|
+
"ppc64le": "ppc64el",
|
|
145
|
+
"s390x": "s390x",
|
|
146
|
+
}
|
|
147
|
+
return mapping.get(raw, raw)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _detect_package_manager(family: DistroFamily) -> PackageManagerKind:
|
|
151
|
+
# Explicit binary detection takes priority over family inference so that
|
|
152
|
+
# distros with unusual setups (e.g. Arch with apt installed) still work.
|
|
153
|
+
checks = [
|
|
154
|
+
("apt-get", PackageManagerKind.APT),
|
|
155
|
+
("pacman", PackageManagerKind.PACMAN),
|
|
156
|
+
("dnf", PackageManagerKind.DNF),
|
|
157
|
+
("zypper", PackageManagerKind.ZYPPER),
|
|
158
|
+
("apk", PackageManagerKind.APK),
|
|
159
|
+
("emerge", PackageManagerKind.PORTAGE),
|
|
160
|
+
("xbps-install", PackageManagerKind.XBPS),
|
|
161
|
+
("nix-env", PackageManagerKind.NIX),
|
|
162
|
+
]
|
|
163
|
+
for cmd, kind in checks:
|
|
164
|
+
if shutil.which(cmd):
|
|
165
|
+
return kind
|
|
166
|
+
|
|
167
|
+
_family_map = {
|
|
168
|
+
DistroFamily.DEBIAN: PackageManagerKind.APT,
|
|
169
|
+
DistroFamily.ARCH: PackageManagerKind.PACMAN,
|
|
170
|
+
DistroFamily.FEDORA: PackageManagerKind.DNF,
|
|
171
|
+
DistroFamily.SUSE: PackageManagerKind.ZYPPER,
|
|
172
|
+
DistroFamily.ALPINE: PackageManagerKind.APK,
|
|
173
|
+
DistroFamily.GENTOO: PackageManagerKind.PORTAGE,
|
|
174
|
+
DistroFamily.VOID: PackageManagerKind.XBPS,
|
|
175
|
+
DistroFamily.NIXOS: PackageManagerKind.NIX,
|
|
176
|
+
}
|
|
177
|
+
return _family_map.get(family, PackageManagerKind.UNKNOWN)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _detect_secure_boot() -> bool:
|
|
181
|
+
if shutil.which("mokutil"):
|
|
182
|
+
try:
|
|
183
|
+
result = subprocess.run(
|
|
184
|
+
["mokutil", "--sb-state"],
|
|
185
|
+
capture_output=True, text=True, timeout=3,
|
|
186
|
+
)
|
|
187
|
+
return "enabled" in result.stdout.lower()
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
sb_var = Path(
|
|
191
|
+
"/sys/firmware/efi/efivars/"
|
|
192
|
+
"SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c"
|
|
193
|
+
)
|
|
194
|
+
if sb_var.exists():
|
|
195
|
+
try:
|
|
196
|
+
data = sb_var.read_bytes()
|
|
197
|
+
return len(data) >= 5 and data[4] == 1
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# Public API
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
@lru_cache(maxsize=1)
|
|
208
|
+
def system_info() -> SystemInfo:
|
|
209
|
+
"""Return a cached SystemInfo for the current machine."""
|
|
210
|
+
distro = _detect_distro()
|
|
211
|
+
arch_raw = platform.machine()
|
|
212
|
+
arch = _normalise_arch(arch_raw)
|
|
213
|
+
pm = _detect_package_manager(distro.family)
|
|
214
|
+
|
|
215
|
+
return SystemInfo(
|
|
216
|
+
distro=distro,
|
|
217
|
+
arch=arch,
|
|
218
|
+
arch_raw=arch_raw,
|
|
219
|
+
package_manager=pm,
|
|
220
|
+
running_kernel=platform.release(),
|
|
221
|
+
has_secure_boot=_detect_secure_boot(),
|
|
222
|
+
has_pkexec=bool(shutil.which("pkexec")),
|
|
223
|
+
has_sudo=bool(shutil.which("sudo")),
|
|
224
|
+
in_nix_shell=bool(os.environ.get("IN_NIX_SHELL") or os.environ.get("LKM_NIX_SHELL")),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def privilege_escalation_cmd() -> list[str]:
|
|
229
|
+
"""Return the best available privilege escalation prefix."""
|
|
230
|
+
info = system_info()
|
|
231
|
+
if info.has_pkexec:
|
|
232
|
+
return ["pkexec"]
|
|
233
|
+
if info.has_sudo:
|
|
234
|
+
return ["sudo"]
|
|
235
|
+
return []
|
lkm/gui/__init__.py
ADDED
|
File without changes
|
lkm/gui/app.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""QApplication entry point for lkm-gui."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from lkm.gui.main_window import MainWindow
|
|
7
|
+
from lkm.qt import QApplication
|
|
8
|
+
|
|
9
|
+
_STYLESHEET = """
|
|
10
|
+
QMainWindow, QDialog {
|
|
11
|
+
background-color: #1e1e2e;
|
|
12
|
+
color: #cdd6f4;
|
|
13
|
+
}
|
|
14
|
+
QTabWidget::pane {
|
|
15
|
+
border: 1px solid #45475a;
|
|
16
|
+
}
|
|
17
|
+
QTabBar::tab {
|
|
18
|
+
background: #313244;
|
|
19
|
+
color: #cdd6f4;
|
|
20
|
+
padding: 6px 14px;
|
|
21
|
+
border-radius: 4px 4px 0 0;
|
|
22
|
+
}
|
|
23
|
+
QTabBar::tab:selected {
|
|
24
|
+
background: #89b4fa;
|
|
25
|
+
color: #1e1e2e;
|
|
26
|
+
}
|
|
27
|
+
QTableView {
|
|
28
|
+
background-color: #181825;
|
|
29
|
+
alternate-background-color: #1e1e2e;
|
|
30
|
+
gridline-color: #313244;
|
|
31
|
+
color: #cdd6f4;
|
|
32
|
+
selection-background-color: #45475a;
|
|
33
|
+
}
|
|
34
|
+
QHeaderView::section {
|
|
35
|
+
background-color: #313244;
|
|
36
|
+
color: #cdd6f4;
|
|
37
|
+
padding: 4px;
|
|
38
|
+
border: none;
|
|
39
|
+
}
|
|
40
|
+
QToolBar {
|
|
41
|
+
background-color: #181825;
|
|
42
|
+
border-bottom: 1px solid #313244;
|
|
43
|
+
spacing: 4px;
|
|
44
|
+
}
|
|
45
|
+
QStatusBar {
|
|
46
|
+
background-color: #181825;
|
|
47
|
+
color: #a6adc8;
|
|
48
|
+
}
|
|
49
|
+
QPushButton {
|
|
50
|
+
background-color: #313244;
|
|
51
|
+
color: #cdd6f4;
|
|
52
|
+
border: 1px solid #45475a;
|
|
53
|
+
border-radius: 4px;
|
|
54
|
+
padding: 4px 12px;
|
|
55
|
+
}
|
|
56
|
+
QPushButton:hover { background-color: #45475a; }
|
|
57
|
+
QPushButton:pressed { background-color: #585b70; }
|
|
58
|
+
QPushButton:default { border-color: #89b4fa; }
|
|
59
|
+
QLineEdit, QComboBox, QTextEdit, QSpinBox {
|
|
60
|
+
background-color: #313244;
|
|
61
|
+
color: #cdd6f4;
|
|
62
|
+
border: 1px solid #45475a;
|
|
63
|
+
border-radius: 4px;
|
|
64
|
+
padding: 2px 6px;
|
|
65
|
+
}
|
|
66
|
+
QGroupBox {
|
|
67
|
+
border: 1px solid #45475a;
|
|
68
|
+
border-radius: 4px;
|
|
69
|
+
margin-top: 8px;
|
|
70
|
+
color: #a6adc8;
|
|
71
|
+
}
|
|
72
|
+
QGroupBox::title {
|
|
73
|
+
subcontrol-origin: margin;
|
|
74
|
+
left: 8px;
|
|
75
|
+
padding: 0 4px;
|
|
76
|
+
}
|
|
77
|
+
QCheckBox { color: #cdd6f4; }
|
|
78
|
+
QLabel { color: #cdd6f4; }
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main() -> None:
|
|
83
|
+
app = QApplication(sys.argv)
|
|
84
|
+
app.setApplicationName("lkm")
|
|
85
|
+
app.setApplicationVersion("0.1.0")
|
|
86
|
+
app.setStyleSheet(_STYLESHEET)
|
|
87
|
+
|
|
88
|
+
win = MainWindow()
|
|
89
|
+
win.show()
|
|
90
|
+
sys.exit(app.exec())
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
main()
|
lkm/gui/kernel_model.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""QAbstractTableModel wrapping a list of KernelEntry objects."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from lkm.core.kernel import KernelEntry, KernelStatus
|
|
5
|
+
from lkm.qt import (
|
|
6
|
+
QAbstractTableModel,
|
|
7
|
+
QColor,
|
|
8
|
+
QFont,
|
|
9
|
+
QModelIndex,
|
|
10
|
+
Qt,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
_COLUMNS = ["Version", "Family", "Flavor", "Arch", "Status", "Notes"]
|
|
14
|
+
_COL_VERSION = 0
|
|
15
|
+
_COL_FAMILY = 1
|
|
16
|
+
_COL_FLAVOR = 2
|
|
17
|
+
_COL_ARCH = 3
|
|
18
|
+
_COL_STATUS = 4
|
|
19
|
+
_COL_NOTES = 5
|
|
20
|
+
|
|
21
|
+
_STATUS_COLORS = {
|
|
22
|
+
KernelStatus.RUNNING: "#2ecc71",
|
|
23
|
+
KernelStatus.INSTALLED: "#3498db",
|
|
24
|
+
KernelStatus.HELD: "#e67e22",
|
|
25
|
+
KernelStatus.AVAILABLE: None,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class KernelTableModel(QAbstractTableModel):
|
|
30
|
+
|
|
31
|
+
def __init__(self, entries: list[KernelEntry] | None = None, parent=None) -> None:
|
|
32
|
+
super().__init__(parent)
|
|
33
|
+
self._entries: list[KernelEntry] = entries or []
|
|
34
|
+
|
|
35
|
+
def set_entries(self, entries: list[KernelEntry]) -> None:
|
|
36
|
+
self.beginResetModel()
|
|
37
|
+
self._entries = entries
|
|
38
|
+
self.endResetModel()
|
|
39
|
+
|
|
40
|
+
def entry_at(self, row: int) -> KernelEntry | None:
|
|
41
|
+
if 0 <= row < len(self._entries):
|
|
42
|
+
return self._entries[row]
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
# QAbstractTableModel interface
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
|
50
|
+
return len(self._entries)
|
|
51
|
+
|
|
52
|
+
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
|
53
|
+
return len(_COLUMNS)
|
|
54
|
+
|
|
55
|
+
def headerData(self, section: int, orientation, role=Qt.ItemDataRole.DisplayRole):
|
|
56
|
+
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
|
|
57
|
+
return _COLUMNS[section]
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def data(self, index: QModelIndex, role=Qt.ItemDataRole.DisplayRole):
|
|
61
|
+
if not index.isValid():
|
|
62
|
+
return None
|
|
63
|
+
entry = self._entries[index.row()]
|
|
64
|
+
col = index.column()
|
|
65
|
+
|
|
66
|
+
if role == Qt.ItemDataRole.DisplayRole:
|
|
67
|
+
return self._display(entry, col)
|
|
68
|
+
|
|
69
|
+
if role == Qt.ItemDataRole.ForegroundRole:
|
|
70
|
+
color = _STATUS_COLORS.get(entry.status)
|
|
71
|
+
if color:
|
|
72
|
+
return QColor(color)
|
|
73
|
+
|
|
74
|
+
if role == Qt.ItemDataRole.FontRole and entry.is_running:
|
|
75
|
+
f = QFont()
|
|
76
|
+
f.setBold(True)
|
|
77
|
+
return f
|
|
78
|
+
|
|
79
|
+
if role == Qt.ItemDataRole.UserRole:
|
|
80
|
+
return entry
|
|
81
|
+
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def _display(self, entry: KernelEntry, col: int) -> str:
|
|
85
|
+
if col == _COL_VERSION:
|
|
86
|
+
return str(entry.version)
|
|
87
|
+
if col == _COL_FAMILY:
|
|
88
|
+
return entry.family.value
|
|
89
|
+
if col == _COL_FLAVOR:
|
|
90
|
+
return entry.flavor
|
|
91
|
+
if col == _COL_ARCH:
|
|
92
|
+
return entry.arch
|
|
93
|
+
if col == _COL_STATUS:
|
|
94
|
+
s = entry.status.value
|
|
95
|
+
return s + " [held]" if entry.held else s
|
|
96
|
+
if col == _COL_NOTES:
|
|
97
|
+
return entry.notes[:60] if entry.notes else ""
|
|
98
|
+
return ""
|