Merge pull request #44 from JPratama7/main

feat: Environment variables, taskbar, and IPC enhancements
This commit is contained in:
Lysec 2025-07-27 18:54:56 +02:00 committed by GitHub
commit 5080c648ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 328 additions and 27 deletions

View file

@ -63,6 +63,10 @@ Scope {
Media {
anchors.verticalCenter: parent.verticalCenter
}
Taskbar {
anchors.verticalCenter: parent.verticalCenter
}
}
ActiveWindow {

View file

@ -61,17 +61,17 @@ Item {
id: batteryTooltip
text: {
let lines = [];
if (isReady) {
lines.push(charging ? "Charging" : "Discharging");
lines.push(Math.round(percent) + "%");
if (battery.changeRate !== undefined)
lines.push("Rate: " + battery.changeRate.toFixed(2) + " W");
if (battery.timeToEmpty > 0)
lines.push("Time left: " + Math.floor(battery.timeToEmpty / 60) + " min");
if (battery.timeToFull > 0)
lines.push("Time to full: " + Math.floor(battery.timeToFull / 60) + " min");
if (battery.healthPercentage !== undefined)
lines.push("Health: " + Math.round(battery.healthPercentage) + "%");
if (batteryWidget.isReady) {
lines.push(batteryWidget.charging ? "Charging" : "Discharging");
lines.push(Math.round(batteryWidget.percent) + "%");
if (batteryWidget.battery.changeRate !== undefined)
lines.push("Rate: " + batteryWidget.battery.changeRate.toFixed(2) + " W");
if (batteryWidget.battery.timeToEmpty > 0)
lines.push("Time left: " + Math.floor(batteryWidget.battery.timeToEmpty / 60) + " min");
if (batteryWidget.battery.timeToFull > 0)
lines.push("Time to full: " + Math.floor(batteryWidget.battery.timeToFull / 60) + " min");
if (batteryWidget.battery.healthPercentage !== undefined)
lines.push("Health: " + Math.round(batteryWidget.battery.healthPercentage) + "%");
}
return lines.join("\n");
}

185
Bar/Modules/Taskbar.qml Normal file
View file

@ -0,0 +1,185 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Settings
import qs.Components
Item {
id: taskbar
width: runningAppsRow.width
height: Settings.settings.taskbarIconSize
function getAppIcon(toplevel: Toplevel): string {
if (!toplevel)
return "";
// Try different icon resolution strategies
let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true);
if (!icon) {
icon = Quickshell.iconPath(toplevel.appId, true);
}
if (!icon) {
icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true);
}
if (!icon) {
icon = Quickshell.iconPath(toplevel.title, true);
}
if (!icon) {
icon = Quickshell.iconPath("application-x-executable", true);
}
return icon || "";
}
Row {
id: runningAppsRow
spacing: 8
height: parent.height
Repeater {
model: ToplevelManager ? ToplevelManager.toplevels : null
delegate: Rectangle {
id: appButton
width: Settings.settings.taskbarIconSize
height: Settings.settings.taskbarIconSize
radius: Math.max(4, Settings.settings.taskbarIconSize * 0.25)
color: isActive ? Theme.accentPrimary : (hovered ? Theme.surfaceVariant : "transparent")
border.color: isActive ? Qt.darker(Theme.accentPrimary, 1.2) : "transparent"
border.width: 1
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
property bool hovered: mouseArea.containsMouse
property string appId: modelData ? modelData.appId : ""
property string appTitle: modelData ? modelData.title : ""
Behavior on color {
ColorAnimation {
duration: 150
}
}
Behavior on border.color {
ColorAnimation {
duration: 150
}
}
// App icon
IconImage {
id: appIcon
width: Math.max(12, Settings.settings.taskbarIconSize * 0.625) // 62.5% of button size (20/32 = 0.625)
height: Math.max(12, Settings.settings.taskbarIconSize * 0.625)
anchors.centerIn: parent
source: getAppIcon(modelData)
smooth: true
// Fallback to first letter if no icon
visible: source.toString() !== ""
}
// Fallback text if no icon available
Text {
anchors.centerIn: parent
visible: !appIcon.visible
text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?"
font.family: Theme.fontFamily
font.pixelSize: Math.max(10, Settings.settings.taskbarIconSize * 0.4375) // 43.75% of button size (14/32 = 0.4375)
font.bold: true
color: appButton.isActive ? Theme.onAccent : Theme.textPrimary
}
// Tooltip
ToolTip {
id: tooltip
visible: mouseArea.containsMouse && !mouseArea.pressed
delay: 800
text: appTitle || appId
background: Rectangle {
color: Theme.backgroundPrimary
border.color: Theme.outline
border.width: 1
radius: 8
}
contentItem: Text {
text: tooltip.text
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeCaption
color: Theme.textPrimary
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: function(mouse) {
console.log("[Taskbar] Clicked on", appButton.appId, "- Active:", appButton.isActive);
if (mouse.button === Qt.MiddleButton) {
console.log("[Taskbar] Middle-clicked on", appButton.appId);
// Example: Close the window with middle click
if (modelData && modelData.close) {
modelData.close();
} else {
console.log("[Taskbar] No close method available for:", modelData);
}
}
if (mouse.button === Qt.LeftButton) {
// Left click: Focus/activate the window
if (modelData && modelData.activate) {
modelData.activate();
} else {
console.log("[Taskbar] No activate method available for:", modelData);
}
}
}
// Right-click for additional actions
onPressed: mouse => {
if (mouse.button === Qt.RightButton) {
console.log("[Taskbar] Right-clicked on", appButton.appId);
// Example actions you can add:
// 1. Close window
// if (modelData && modelData.close) {
// modelData.close();
// }
// 2. Minimize window
// if (modelData && modelData.minimize) {
// modelData.minimize();
// }
// 3. Show context menu (needs Menu component)
// contextMenu.popup();
}
}
}
// Active indicator dot
Rectangle {
visible: isActive
width: 4
height: 4
radius: 2
color: Theme.onAccent
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: -6
}
}
}
}
}

View file

@ -1,11 +1,25 @@
import Quickshell.Io
import "./IdleInhibitor.qml"
IpcHandler {
property var appLauncherPanel
property var lockScreen
property IdleInhibitor idleInhibitor
property var notificationPopup
target: "globalIPC"
function toggleIdleInhibitor(): void {
idleInhibitor.toggle()
}
function toggleNotificationPopup(): void {
console.log("[IPC] NotificationPopup toggle() called")
notificationPopup.togglePopup();
}
// Toggle Applauncher visibility
function toggleLauncher(): void {
if (!appLauncherPanel) {

43
Helpers/IdleInhibitor.qml Normal file
View file

@ -0,0 +1,43 @@
import Quickshell.Io
Process {
id: idleRoot
// Example: systemd-inhibit to prevent idle/sleep
command: ["systemd-inhibit", "--what=idle:sleep", "--who=noctalia", "--why=User requested", "sleep", "infinity"]
// Keep process running in background
property bool isRunning: running
onStarted: {
console.log("[IdleInhibitor] Process started - idle inhibited")
}
onExited: function(exitCode, exitStatus) {
console.log("[IdleInhibitor] Process finished:", exitCode)
}
// Control functions
function start() {
if (!running) {
console.log("[IdleInhibitor] Starting idle inhibitor...")
running = true
}
}
function stop() {
if (running) {
// Force stop the process by setting running to false
running = false
console.log("[IdleInhibitor] Stopping idle inhibitor...")
}
}
function toggle() {
if (running) {
stop()
} else {
start()
}
}
}

View file

@ -153,14 +153,23 @@ To make the weather widget, wallpaper manager and record button work you will ha
qs ipc call globalIPC toggleLauncher
```
### Toggle Lockscreen:
### Toggle Notification Popup:
```
qs ipc call globalIPC toggleLock
qs ipc call globalIPC toggleNotificationPopup
```
You can keybind it however you want in your niri setup.
### Toggle Idle Inhibitor:
```
qs ipc call globalIPC toggleIdleInhibitor
```
### Toggle Fullscreen:
```
qs ipc call globalIPC toggleFullscreen
```
</details>
---

View file

@ -7,8 +7,9 @@ import qs.Services
Singleton {
property string shellName: "Noctalia"
property string settingsDir: (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/"
property string settingsFile: settingsDir + "Settings.json"
property string settingsDir: Quickshell.env("NOCTALIA_SETTINGS_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/"
property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (settingsDir + "Settings.json")
property string themeFile: Quickshell.env("NOCTALIA_THEME_FILE") || (settingsDir + "Theme.json")
property var settings: settingAdapter
Item {
@ -60,6 +61,8 @@ Singleton {
property bool reverseDayMonth: false
property bool use12HourClock: false
property bool dimPanels: true
property real fontSizeMultiplier: 1.0 // Font size multiplier (1.0 = normal, 1.2 = 20% larger, 0.8 = 20% smaller)
property int taskbarIconSize: 24 // Taskbar icon button size in pixels (default: 32, smaller: 24, larger: 40)
property var pinnedExecs: [] // Added for AppLauncher pinned apps
}
}

View file

@ -15,13 +15,13 @@ Singleton {
// FileView to load theme data from JSON file
FileView {
id: themeFile
path: Settings.settingsDir + "Theme.json"
path: Settings.themeFile
watchChanges: true
onFileChanged: reload()
onAdapterUpdated: writeAdapter()
onLoadFailed: function(error) {
if (error.includes("No such file")) {
themeData = {}
if (error.toString().includes("No such file") || error === 2) {
// File doesn't exist, create it with default values
writeAdapter()
}
}
@ -102,9 +102,13 @@ Singleton {
// Font Properties
property string fontFamily: "Roboto" // Family for all text
property int fontSizeHeader: 32 // Headers and titles
property int fontSizeBody: 16 // Body text and general content
property int fontSizeSmall: 14 // Small text like clock, labels
property int fontSizeCaption: 12 // Captions and fine print
// Font size multiplier - adjust this in Settings.json to scale all fonts
property real fontSizeMultiplier: Settings.settings.fontSizeMultiplier || 1.0
// Base font sizes (multiplied by fontSizeMultiplier)
property int fontSizeHeader: Math.round(32 * fontSizeMultiplier) // Headers and titles
property int fontSizeBody: Math.round(16 * fontSizeMultiplier) // Body text and general content
property int fontSizeSmall: Math.round(14 * fontSizeMultiplier) // Small text like clock, labels
property int fontSizeCaption: Math.round(12 * fontSizeMultiplier) // Captions and fine print
}

View file

@ -7,12 +7,20 @@ import qs.Components
Item {
id: root
width: 22; height: 22
property bool isSilence: false
// Process for executing CLI commands
Process {
id: rightClickProcess
command: ["qs","ipc", "call", "globalIPC", "toggleNotificationPopup"]
}
// Bell icon/button
Item {
id: bell
width: 22; height: 22
Text {
id: bellText
anchors.centerIn: parent
text: notificationHistoryWin.hasUnread ? "notifications_unread" : "notifications"
font.family: mouseAreaBell.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
@ -25,7 +33,20 @@ Item {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: notificationHistoryWin.visible = !notificationHistoryWin.visible
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse): void {
if (mouse.button === Qt.RightButton) {
root.isSilence = !root.isSilence;
rightClickProcess.running = true;
bellText.text = root.isSilence ? "notifications_off" : "notifications"
}
if (mouse.button === Qt.LeftButton){
notificationHistoryWin.visible = !notificationHistoryWin.visible
return;
}
}
onEntered: notificationTooltip.tooltipVisible = true
onExited: notificationTooltip.tooltipVisible = false
}

View file

@ -8,11 +8,12 @@ PanelWindow {
implicitWidth: 350
implicitHeight: notificationColumn.implicitHeight
color: "transparent"
visible: notificationModel.count > 0
visible: notificationsVisible && notificationModel.count > 0
screen: Quickshell.primaryScreen !== undefined ? Quickshell.primaryScreen : null
focusable: false
property bool barVisible: true
property bool notificationsVisible: true
anchors.top: true
anchors.right: true
@ -26,6 +27,12 @@ PanelWindow {
property int maxVisible: 5
property int spacing: 5
function togglePopup(): void {
console.log("[NotificationPopup] Current state: " + notificationsVisible);
notificationsVisible = !notificationsVisible;
console.log("[NotificationPopup] New state: " + notificationsVisible);
}
function addNotification(notification) {
notificationModel.insert(0, {
id: notification.id,

View file

@ -11,6 +11,9 @@ import qs.Widgets.Notification
import qs.Settings
import qs.Helpers
import "./Helpers/IdleInhibitor.qml"
import "./Helpers/IPCHandlers.qml"
Scope {
id: root
@ -50,12 +53,18 @@ Scope {
}
}
IdleInhibitor {
id: idleInhibitor
}
NotificationServer {
id: notificationServer
onNotification: function (notification) {
console.log("Notification received:", notification.appName);
notification.tracked = true;
notificationPopup.addNotification(notification);
if (notificationPopup.notificationsVisible) {
notificationPopup.addNotification(notification);
}
if (notificationHistoryWin) {
notificationHistoryWin.addToHistory({
id: notification.id,
@ -89,6 +98,8 @@ Scope {
IPCHandlers {
appLauncherPanel: appLauncherPanel
lockScreen: lockScreen
idleInhibitor: idleInhibitor
notificationPopup: notificationPopup
}
Connections {