Files
noctalia-shell/Modules/Panels/Settings/Tabs/About/VersionSubTab.qml
T
2026-01-10 23:14:32 -05:00

673 lines
19 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.Noctalia
import qs.Services.System
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
property string latestVersion: GitHubService.latestVersion
property string currentVersion: UpdateService.currentVersion
property string commitInfo: ""
readonly property bool isGitVersion: root.currentVersion.endsWith("-git")
// Update status: compare versions (strip -git suffix for comparison)
readonly property string installedBase: root.currentVersion.replace("-git", "")
readonly property bool updateAvailable: {
if (!root.latestVersion || !root.installedBase)
return false;
return root.latestVersion !== root.installedBase && !root.isGitVersion;
}
readonly property bool isUpToDate: {
if (!root.latestVersion || !root.installedBase)
return false;
return root.latestVersion === root.installedBase;
}
// System info properties
property var systemInfo: null
property bool systemInfoLoading: true
property bool systemInfoAvailable: true
spacing: Style.marginL
function getModule(type) {
if (!root.systemInfo)
return null;
return root.systemInfo.find(m => m.type === type);
}
function formatBytes(bytes) {
if (bytes === null || bytes === undefined)
return "N/A";
const gb = bytes / (1024 * 1024 * 1024);
return gb.toFixed(1) + " GB";
}
function copyInfoToClipboard() {
let info = "Noctalia Shell\n";
info += "==============\n";
info += "Latest version: " + root.latestVersion + "\n";
info += "Installed version: " + root.currentVersion + "\n";
if (root.isGitVersion && root.commitInfo) {
info += "Git commit: " + root.commitInfo + "\n";
}
info += "\nSystem Information\n";
info += "==================\n";
if (root.systemInfo) {
const os = root.getModule("OS");
const kernel = root.getModule("Kernel");
const title = root.getModule("Title");
const cpu = root.getModule("CPU");
const gpu = root.getModule("GPU");
const mem = root.getModule("Memory");
const wm = root.getModule("WM");
info += "OS: " + (os?.result?.prettyName || "N/A") + "\n";
info += "Kernel: " + (kernel?.result?.release || "N/A") + "\n";
info += "Host: " + (title?.result?.hostName || "N/A") + "\n";
info += "CPU: " + (cpu?.result?.cpu || "N/A") + "\n";
if (gpu?.result && Array.isArray(gpu.result) && gpu.result.length > 0) {
info += "GPU: " + gpu.result.map(g => g.name || "Unknown").join(", ") + "\n";
}
if (mem?.result) {
info += "Memory: " + root.formatBytes(mem.result.total) + "\n";
}
if (wm?.result) {
info += "WM: " + (wm.result.prettyName || wm.result.processName || "N/A") + "\n";
}
}
Quickshell.execDetached(["wl-copy", info]);
ToastService.showNotice(I18n.tr("panels.about.title"), I18n.tr("panels.about.info-copied"));
}
Component.onCompleted: {
fastfetchProcess.running = true;
Logger.d("VersionSubTab", "Current version:", root.currentVersion);
Logger.d("VersionSubTab", "Is git version:", root.isGitVersion);
// Only fetch commit info for -git versions
if (root.isGitVersion) {
// On NixOS, extract commit hash from the store path first
if (HostService.isNixOS) {
var shellDir = Quickshell.shellDir || "";
Logger.d("VersionSubTab", "Component.onCompleted - NixOS detected, shellDir:", shellDir);
if (shellDir) {
// Extract commit hash from path like: /nix/store/...-noctalia-shell-2025-11-30_225e6d3/share/noctalia-shell
// Pattern matches: noctalia-shell-YYYY-MM-DD_<commit_hash>
var match = shellDir.match(/noctalia-shell-\d{4}-\d{2}-\d{2}_([0-9a-f]{7,})/i);
if (match && match[1]) {
// Use first 7 characters of the commit hash
root.commitInfo = match[1].substring(0, 7);
Logger.d("VersionSubTab", "Component.onCompleted - Extracted commit from NixOS path:", root.commitInfo);
return;
} else {
Logger.d("VersionSubTab", "Component.onCompleted - Could not extract commit from NixOS path, trying fallback");
}
}
fetchGitCommit();
return;
} else {
// On non-NixOS systems, check for pacman first.
whichPacmanProcess.running = true;
return;
}
}
}
Timer {
id: gitFallbackTimer
interval: 500
running: false
onTriggered: {
if (!root.commitInfo) {
fetchGitCommit();
}
}
}
Process {
id: whichPacmanProcess
command: ["sh", "-c", "command -v pacman"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
Logger.d("VersionSubTab", "whichPacmanProcess - pacman found, starting query");
pacmanProcess.running = true;
gitFallbackTimer.start();
} else {
Logger.d("VersionSubTab", "whichPacmanProcess - pacman not found, falling back to git");
fetchGitCommit();
}
}
}
Process {
id: pacmanProcess
command: ["pacman", "-Q", "noctalia-shell-git"]
running: false
onStarted: {
gitFallbackTimer.stop();
}
onExited: function (exitCode) {
gitFallbackTimer.stop();
Logger.d("VersionSubTab", "pacmanProcess - Process exited with code:", exitCode);
if (exitCode === 0) {
var output = stdout.text.trim();
Logger.d("VersionSubTab", "pacmanProcess - Output:", output);
var match = output.match(/noctalia-shell-git\s+(.+)/);
if (match && match[1]) {
// For Arch packages, the version format might be like: 3.4.0.r112.g3f00bec8-1
// Extract just the commit hash part if it exists
var version = match[1];
var commitMatch = version.match(/\.g([0-9a-f]{7,})/i);
if (commitMatch && commitMatch[1]) {
// Show short hash (first 7 characters)
root.commitInfo = commitMatch[1].substring(0, 7);
Logger.d("VersionSubTab", "pacmanProcess - Set commitInfo from Arch package:", root.commitInfo);
return; // Successfully got commit hash from Arch package
} else {
// If no commit hash in version format, still try git repo
Logger.d("VersionSubTab", "pacmanProcess - No commit hash in version, trying git");
fetchGitCommit();
}
} else {
// Unexpected output format, try git
Logger.d("VersionSubTab", "pacmanProcess - Unexpected output format, trying git");
fetchGitCommit();
}
} else {
// If not on Arch, try to get git commit from repository
Logger.d("VersionSubTab", "pacmanProcess - Package not found, trying git");
fetchGitCommit();
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
function fetchGitCommit() {
var shellDir = Quickshell.shellDir || "";
Logger.d("VersionSubTab", "fetchGitCommit - shellDir:", shellDir);
if (!shellDir) {
Logger.d("VersionSubTab", "fetchGitCommit - Cannot determine shell directory, skipping git commit fetch");
return;
}
gitProcess.workingDirectory = shellDir;
gitProcess.running = true;
}
Process {
id: gitProcess
command: ["git", "rev-parse", "--short", "HEAD"]
running: false
onExited: function (exitCode) {
Logger.d("VersionSubTab", "gitProcess - Process exited with code:", exitCode);
if (exitCode === 0) {
var gitOutput = stdout.text.trim();
Logger.d("VersionSubTab", "gitProcess - gitOutput:", gitOutput);
if (gitOutput) {
root.commitInfo = gitOutput;
Logger.d("VersionSubTab", "gitProcess - Set commitInfo to:", root.commitInfo);
}
} else {
Logger.d("VersionSubTab", "gitProcess - Git command failed. Exit code:", exitCode);
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
Process {
id: fastfetchProcess
command: ["fastfetch", "--json"]
running: false
onExited: function (exitCode) {
root.systemInfoLoading = false;
if (exitCode === 0) {
try {
root.systemInfo = JSON.parse(stdout.text);
root.systemInfoAvailable = true;
} catch (e) {
Logger.w("VersionSubTab", "Failed to parse fastfetch JSON: " + e);
root.systemInfoAvailable = false;
}
} else {
root.systemInfoAvailable = false;
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
NHeader {
label: I18n.tr("panels.about.noctalia-title")
description: I18n.tr("panels.about.noctalia-desc")
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginXL
// Noctalia logo
Image {
source: "../../../../../Assets/noctalia.svg"
width: 128 * Style.uiScaleRatio
height: width
fillMode: Image.PreserveAspectFit
sourceSize.width: width
sourceSize.height: height
mipmap: true
smooth: true
Layout.alignment: Qt.AlignVCenter
}
// Versions
GridLayout {
columns: 2
rowSpacing: Style.marginXS
columnSpacing: Style.marginM
NText {
text: I18n.tr("panels.about.noctalia-latest-version")
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
}
NText {
text: root.latestVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
NText {
text: I18n.tr("panels.about.noctalia-installed-version")
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
}
RowLayout {
spacing: Style.marginS
NText {
text: root.currentVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
// Update status indicator
NIcon {
id: upToDateIcon
visible: root.isUpToDate
icon: "circle-check"
pointSize: Style.fontSizeM
color: Color.mPrimary
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(upToDateIcon, I18n.tr("panels.about.up-to-date"))
onExited: TooltipService.hide()
}
}
NIcon {
id: updateAvailableIcon
visible: root.updateAvailable
icon: "arrow-up-circle"
pointSize: Style.fontSizeS
color: Color.mPrimary
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(updateAvailableIcon, I18n.tr("panels.about.update-available"))
onExited: TooltipService.hide()
}
}
}
NText {
visible: root.isGitVersion
text: I18n.tr("panels.about.noctalia-git-commit")
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
}
// Clickable git commit
NText {
id: commitText
visible: root.isGitVersion
text: root.commitInfo || I18n.tr("common.loading")
color: root.commitInfo ? Color.mPrimary : Color.mOnSurface
pointSize: Style.fontSizeXS
font.underline: commitMouseArea.containsMouse && root.commitInfo
MouseArea {
id: commitMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: root.commitInfo ? Qt.PointingHandCursor : Qt.ArrowCursor
onEntered: {
if (root.commitInfo) {
TooltipService.show(commitText, I18n.tr("panels.about.view-commit"));
}
}
onExited: TooltipService.hide()
onClicked: {
if (root.commitInfo) {
Quickshell.execDetached(["xdg-open", "https://github.com/noctalia-dev/noctalia-shell/commit/" + root.commitInfo]);
}
}
}
}
}
}
// Action buttons row
RowLayout {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.marginM
Layout.bottomMargin: Style.marginM
spacing: Style.marginM
NButton {
icon: "sparkles"
text: I18n.tr("panels.about.changelog")
outlined: true
onClicked: {
var screen = PanelService.openedPanel?.screen || Quickshell.screens[0];
UpdateService.viewChangelog(screen);
}
}
NButton {
icon: "heart"
text: I18n.tr("panels.about.support")
outlined: true
onClicked: {
Quickshell.execDetached(["xdg-open", "https://ko-fi.com/lysec"]);
ToastService.showNotice(I18n.tr("panels.about.support"), I18n.tr("toast.kofi-opened"));
}
}
NButton {
icon: "copy"
text: I18n.tr("panels.about.copy-info")
outlined: true
onClicked: root.copyInfoToClipboard()
}
}
// System Information Section
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
}
NHeader {
label: I18n.tr("panels.about.system-title")
Layout.topMargin: Style.marginM
}
// Loading state
RowLayout {
visible: root.systemInfoLoading
Layout.fillWidth: true
spacing: Style.marginS
NBusyIndicator {
running: root.systemInfoLoading
}
NText {
text: I18n.tr("panels.about.system-loading")
color: Color.mOnSurfaceVariant
}
}
// Error state (fastfetch not installed)
ColumnLayout {
visible: !root.systemInfoLoading && !root.systemInfoAvailable
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: I18n.tr("panels.about.system-not-installed")
color: Color.mOnSurfaceVariant
}
NText {
text: I18n.tr("panels.about.system-install-hint")
color: Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeXS
}
}
// System info grid
GridLayout {
visible: !root.systemInfoLoading && root.systemInfoAvailable && root.systemInfo
Layout.fillWidth: true
columns: 2
rowSpacing: Style.marginXS
columnSpacing: Style.marginM
// OS
NText {
text: I18n.tr("panels.about.system-os")
color: Color.mOnSurfaceVariant
}
NText {
text: {
const os = root.getModule("OS");
return os?.result?.prettyName || "N/A";
}
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Kernel
NText {
text: I18n.tr("panels.about.system-kernel")
color: Color.mOnSurfaceVariant
}
NText {
text: {
const kernel = root.getModule("Kernel");
return kernel?.result?.release || "N/A";
}
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Host
NText {
text: I18n.tr("panels.about.system-host")
color: Color.mOnSurfaceVariant
}
NText {
text: {
const title = root.getModule("Title");
return title?.result?.hostName || "N/A";
}
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Uptime
NText {
text: I18n.tr("panels.about.system-uptime")
color: Color.mOnSurfaceVariant
}
NText {
text: {
const cmd = root.getModule("Command");
return cmd?.result || "N/A";
}
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// CPU
NText {
text: I18n.tr("panels.about.system-cpu")
color: Color.mOnSurfaceVariant
}
NText {
text: {
const cpu = root.getModule("CPU");
if (!cpu?.result)
return "N/A";
let cpuText = cpu.result.cpu || "N/A";
const cores = cpu.result.cores;
if (cores?.logical) {
cpuText += " (" + cores.logical + " threads)";
}
return cpuText;
}
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// GPU
NText {
text: I18n.tr("panels.about.system-gpu")
color: Color.mOnSurfaceVariant
}
NText {
text: {
const gpu = root.getModule("GPU");
if (!gpu?.result || !Array.isArray(gpu.result) || gpu.result.length === 0)
return "N/A";
return gpu.result.map(g => g.name || "Unknown").join(", ");
}
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Memory
NText {
text: I18n.tr("panels.about.system-memory")
color: Color.mOnSurfaceVariant
}
NText {
text: {
const mem = root.getModule("Memory");
if (!mem?.result)
return "N/A";
const used = root.formatBytes(mem.result.used);
const total = root.formatBytes(mem.result.total);
return used + " / " + total;
}
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Disk
NText {
text: I18n.tr("panels.about.system-disk")
color: Color.mOnSurfaceVariant
}
NText {
text: {
const disk = root.getModule("Disk");
if (!disk?.result || !Array.isArray(disk.result) || disk.result.length === 0)
return "N/A";
const rootDisk = disk.result.find(d => d.mountpoint === "/");
if (!rootDisk?.bytes)
return "N/A";
const used = root.formatBytes(rootDisk.bytes.used);
const total = root.formatBytes(rootDisk.bytes.total);
return used + " / " + total + " (" + rootDisk.filesystem + ")";
}
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// WM
NText {
text: I18n.tr("panels.about.system-wm")
color: Color.mOnSurfaceVariant
}
NText {
text: {
const wm = root.getModule("WM");
if (!wm?.result)
return "N/A";
let wmText = wm.result.prettyName || wm.result.processName || "N/A";
if (wm.result.protocolName) {
wmText += " (" + wm.result.protocolName + ")";
}
return wmText;
}
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Packages
NText {
text: I18n.tr("panels.about.system-packages")
color: Color.mOnSurfaceVariant
}
NText {
text: {
const pkg = root.getModule("Packages");
if (!pkg?.result)
return "N/A";
const result = pkg.result;
if (result.all) {
const managers = [];
if (result.rpm > 0)
managers.push("rpm: " + result.rpm);
if (result.pacman > 0)
managers.push("pacman: " + result.pacman);
if (result.dpkg > 0)
managers.push("dpkg: " + result.dpkg);
if (result.flatpakSystem > 0 || result.flatpakUser > 0) {
const flatpak = (result.flatpakSystem || 0) + (result.flatpakUser || 0);
managers.push("flatpak: " + flatpak);
}
if (result.snap > 0)
managers.push("snap: " + result.snap);
if (result.nixSystem > 0 || result.nixUser > 0 || result.nixDefault > 0) {
const nix = (result.nixSystem || 0) + (result.nixUser || 0) + (result.nixDefault || 0);
managers.push("nix: " + nix);
}
if (result.brew > 0)
managers.push("brew: " + result.brew);
if (managers.length > 0) {
return result.all + " (" + managers.join(", ") + ")";
}
return result.all.toString();
}
return "N/A";
}
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: Text.Wrap
}
}
}