diff --git a/README.md b/README.md index cc998c1..996b432 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,18 @@ -## quickshell-niri +a fork of the simple-bar for -This project contains examples of [Quickshell](https://quickshell.outfoxxed.me/) configuration for the [niri](https://github.com/YaLTeR/niri) Wayland compositor, using the [QML plugin for niri](https://github.com/imiric/qml-niri). +https://github.com/imiric/quickshell-niri +includes: + 1. volume widget + 2. system tray + 3. buttons intended to be used for launching specific programs -### Setup +requirements: +1. quickshell +2. https://github.com/imiric/qml-niri +3. nerd font symbols + on arch: sudo pacman -S ttf-nerd-fonts-symbols -1. Install Quickshell version >=0.2.1, and niri version >=25.08. +it looks like this: -2. [Install the QML plugin for niri.](https://github.com/imiric/qml-niri#installation) - -3. For specific widgets: - - Battery: [UPower](https://upower.freedesktop.org/) - -4. Fonts/icons: [Barlow](https://tribby.com/fonts/barlow/) - - -### Usage - -Clone this repository, and copy or symlink any of the examples into `~/.config/quickshell/`. E.g.: -```shell -git clone https://github.com/imiric/quickshell-niri.git -ln -s "$PWD/quickshell-niri/simple-bar" ~/.config/quickshell/niri-simple-bar -``` - -Then you can run Quickshell with the specific configuration: -```shell -quickshell --config niri-simple-bar -``` - -You can run this automatically when niri starts by adding this to your niri configuration file (`~/.config/niri/config.kdl`): -```kdl -spawn-at-startup "quickshell" "--config" "niri-simple-bar" -``` - - -### Examples - -#### [Simple bar](/simple-bar) - -![Simple bar](/assets/simple-bar.png) - -A plain bar with a workspace indicator and switcher, centered title of the currently focused window, and text battery and date/time widgets. - -This is a slightly modified version of [SimpleQuickshellBar](https://github.com/the-ink-serpent/SimpleQuickshellBar), but for niri instead of Hyprland. - - -## License - -[MIT](/LICENSE) +![screenshot of simple bar with goodies](https://github.com/tripleducky/quickshell-niri-bar-with-goodies/blob/main/Screenshot_20251024_211907.png) diff --git a/Screenshot_20251024_211907.png b/Screenshot_20251024_211907.png new file mode 100644 index 0000000..b7d262f Binary files /dev/null and b/Screenshot_20251024_211907.png differ diff --git a/assets/simple-bar.png b/assets/simple-bar.png deleted file mode 100644 index 65ea682..0000000 Binary files a/assets/simple-bar.png and /dev/null differ diff --git a/fancy-bar/config.json b/fancy-bar/config.json deleted file mode 100644 index 12e12ee..0000000 --- a/fancy-bar/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "theme": { - "font": { - "family": "Barlow Medium" - } - } -} diff --git a/fancy-bar/modules/bar/Bar.qml b/fancy-bar/modules/bar/Bar.qml deleted file mode 100644 index a692f38..0000000 --- a/fancy-bar/modules/bar/Bar.qml +++ /dev/null @@ -1,111 +0,0 @@ -import QtQuick -import QtQuick.Effects -import QtQuick.Layouts -import Quickshell -import Quickshell.Wayland -import qs.modules.common - -Item { - id: root - property int position: Types.Position.Top - property string color: "gray" - property int size: 30 - - WlrLayershell { - id: barShadow - implicitHeight: bar.height + 100 - color: "transparent" - layer: WlrLayer.Bottom - exclusiveZone: 0 - anchors: bar.anchors - - Rectangle { - color: barContent.color - anchors { - top: root.position === Types.Position.Top ? parent.top : undefined - bottom: root.position === Types.Position.Bottom ? parent.bottom : undefined - } - height: barContent.height - // The +40 here and the -20 shadowHorizontalOffset are to have the - // shadow extend all the way along the edge. Otherwise it would be - // slightly cut off at the left and right corners. - width: parent.width + 40 - - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - // The vertical offset makes the shadow slightly more prominent - shadowVerticalOffset: root.position === Types.Position.Top ? 5 : -5 - shadowHorizontalOffset: -20 - shadowBlur: 1 - blurMultiplier: 1.5 - shadowColor: "#C0000000" - } - } - } - - PanelWindow { - id: bar - implicitHeight: root.size - color: "transparent" - anchors { - top: root.position === Types.Position.Top - bottom: root.position === Types.Position.Bottom - left: true - right: true - } - - Rectangle { - id: barContent - anchors.fill: parent - color: root.color - - RowLayout { - id: leftLayout - anchors { - left: parent.left - leftMargin: 25 - } - Loader { active: Config.data.workspaces.enabled; sourceComponent: Workspaces {} } - } - - RowLayout { - id: centerLayout - - anchors { - left: leftLayout.right - leftMargin: 5 - verticalCenter: parent.verticalCenter - } - Loader { active: Config.data.focusedWindow.enabled; sourceComponent: FocusedWindow {} } - - } - - RowLayout { - id: rightLayout - - anchors { - verticalCenter: parent.verticalCenter - right: parent.right - rightMargin: 25 - } - spacing: 10 - Loader { - active: Config.data.battery.enabled - sourceComponent: Battery { - orientation: Types.stringToOrientation(Config.data.battery.orientation) - } - } - Loader { - active: Config.data.datetime.enabled - sourceComponent: DateTime { - size: Math.min( - root.size * Config.data.datetime.scale - root.size * 0.2, - root.size - ) - } - } - } - } - } -} diff --git a/fancy-bar/modules/bar/Battery.qml b/fancy-bar/modules/bar/Battery.qml deleted file mode 100644 index 55dccbc..0000000 --- a/fancy-bar/modules/bar/Battery.qml +++ /dev/null @@ -1,86 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects -import Quickshell -import qs.modules.common -import qs.modules.common.widgets -import qs.modules.icons -import qs.services - -MouseArea { - id: root - - property int orientation: Types.Orientation.Horizontal - property real size: 1 - - readonly property var chargeState: Battery.chargeState - readonly property bool isCharging: Battery.isCharging - readonly property bool isPluggedIn: Battery.isPluggedIn - readonly property real percentage: Battery.percentage - readonly property bool isLow: percentage <= Config.data.battery.low / 100 - readonly property bool isCritical: percentage <= Config.data.battery.critical / 100 - - implicitWidth: batteryProgress.implicitWidth - - hoverEnabled: true - - ProgressBarText { - id: batteryProgress - anchors { - left: icon.left - bottom: icon.bottom - } - valueBarWidth: icon.bodyWidth - valueBarHeight: icon.bodyHeight - value: percentage - text: Config.data.battery.showPercentage ? Math.round(value * 100) : "" - orientation: root.orientation - shimmer: isCharging - pulse: isCharging - highlightColor: (() => { - if (isCritical && !isCharging) { - return Config.data.theme.color.error; - } - if (isLow && !isCharging) { - return Config.data.theme.color.warning; - } - return Config.data.theme.color.ok; - })() - font.family: "Noto Sans" - font.pixelSize: 14 - textColor: Config.data.theme.color.foreground - - // Clip the progress bar within the borders of the battery icon body - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Item { - width: batteryProgress.width - height: batteryProgress.height - - Rectangle { - x: icon.borderWidth - y: icon.borderWidth - width: icon.bodyWidth - icon.borderWidth * 2 - height: icon.bodyHeight - icon.borderWidth * 2 - radius: icon.bodyRadius / 2 - } - } - } - } - - BatteryIcon { - id: icon - anchors.centerIn: parent - size: Math.min(Config.data.battery.scale * Config.data.theme.widget.size, - Config.data.bar.size) - iconColor: Config.data.theme.color.foreground - orientation: root.orientation - } - - /* - BatteryPopup { - id: batteryPopup - hoverTarget: root - } - */ -} diff --git a/fancy-bar/modules/bar/DateTime.qml b/fancy-bar/modules/bar/DateTime.qml deleted file mode 100644 index e8ebde6..0000000 --- a/fancy-bar/modules/bar/DateTime.qml +++ /dev/null @@ -1,116 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import qs.modules.common - -// DateTime component that displays time and date in a vertically stacked -// layout, with both blocks horizontally centered along the same axis. The width -// and height of the blocks is dynamically determined to avoid layout shifting -// with proportional fonts as digits change, and to make the shorter block -// slightly larger to compensate. -Item { - id: root - property real size: 1 - - SystemClock { - id: clock - precision: SystemClock.Seconds - } - - implicitWidth: contentItem.implicitWidth - implicitHeight: size - - // Fixed-width blocks used as width references for the actual time and date blocks. - // The reference isn't perfect, but should look good in most cases. - TextMetrics { - id: timeMetrics - font: timeBlock.font - text: Qt.formatDateTime(new Date(2000, 12, 30, 23, 59, 59), - Config.data.datetime.time.format || "hh:mm") - } - - TextMetrics { - id: dateMetrics - font: dateBlock.font - text: Qt.formatDateTime(new Date(2000, 12, 30), - Config.data.datetime.date.format || "yyyy-MM-dd") - } - - ColumnLayout { - id: contentItem - anchors.centerIn: parent - width: Math.max(timeBlock.Layout.preferredWidth, dateBlock.Layout.preferredWidth) - height: parent.height - spacing: gapSize - - Text { - id: timeBlock - Layout.alignment: Qt.AlignHCenter - Layout.preferredHeight: (root.size - gapSize) / 2 - Layout.preferredWidth: timeMetrics.advanceWidth - - text: Qt.formatDateTime(clock.date, Config.data.datetime.time.format || "hh:mm") - font.family: Config.data.datetime.font.family || Config.data.theme.font.family - font.pixelSize: baseFontSize * timeScale - font.weight: Config.data.datetime.font.weight - color: Config.data.theme.color.textMuted - visible: Config.data.datetime.time.enabled !== false - verticalAlignment: Text.AlignVCenter - } - - Text { - id: dateBlock - Layout.alignment: Qt.AlignHCenter - Layout.preferredHeight: (root.size - gapSize) / 2 - Layout.preferredWidth: dateMetrics.advanceWidth - - text: Qt.formatDateTime(clock.date, Config.data.datetime.date.format || "yyyy-MM-dd") - font.family: Config.data.datetime.font.family || Config.data.theme.font.family - font.pixelSize: baseFontSize * dateScale - font.weight: Config.data.datetime.font.weight - color: Config.data.theme.color.textMuted - visible: Config.data.datetime.date.enabled !== false - verticalAlignment: Text.AlignVCenter - } - } - - readonly property real baseFontSize: { - (timeBlock.visible && dateBlock.visible ? 0.5 : 1) - * size * Config.data.datetime.font.scale - } - readonly property real maxBlockHeight: size - gapSize - readonly property real gapSize: size * 0.1 - property real timeScale: 1 - property real dateScale: 1 - - Component.onCompleted: { - if (!timeBlock.visible || !dateBlock.visible) return; - - const timeWidth = timeBlock.contentWidth; - const dateWidth = dateBlock.contentWidth; - - // Give narrower block a boost - let targetTimeScale = dateWidth > timeWidth ? 1.3 : 1; - let targetDateScale = timeWidth > dateWidth ? 1.3 : 1; - - timeScale = targetTimeScale; - dateScale = targetDateScale; - - // Scale down if either block is too tall - if (timeBlock.contentHeight > maxBlockHeight) { - timeScale *= maxBlockHeight / timeBlock.contentHeight; - } - if (dateBlock.contentHeight > maxBlockHeight) { - dateScale *= maxBlockHeight / dateBlock.contentHeight; - } - - // Ensure scaled block doesn't exceed the widest block's width - const maxWidth = Math.max(timeWidth, dateWidth); - if (timeBlock.contentWidth > maxWidth) { - timeScale *= maxWidth / timeBlock.contentWidth; - } - if (dateBlock.contentWidth > maxWidth) { - dateScale *= maxWidth / dateBlock.contentWidth; - } - } -} diff --git a/fancy-bar/modules/bar/FocusedWindow.qml b/fancy-bar/modules/bar/FocusedWindow.qml deleted file mode 100644 index 81b8f77..0000000 --- a/fancy-bar/modules/bar/FocusedWindow.qml +++ /dev/null @@ -1,35 +0,0 @@ -import QtQuick -import qs.modules.common -import qs.services - -Row { - spacing: 5 - Image { - anchors.verticalCenter: parent.verticalCenter - source: Niri.focusedWindow?.iconPath ? "file://" + Niri.focusedWindow?.iconPath : "" - sourceSize.width: Config.data.focusedWindow.icon.scale * Config.data.theme.widget.size - sourceSize.height: Config.data.focusedWindow.icon.scale * Config.data.theme.widget.size - visible: Config.data.focusedWindow.icon.enabled && Niri.focusedWindow?.iconPath !== "" - smooth: true - } - - // Fallback for missing icons - Rectangle { - anchors.verticalCenter: parent.verticalCenter - width: Config.data.focusedWindow.icon.scale * Config.data.theme.widget.size - height: Config.data.focusedWindow.icon.scale * Config.data.theme.widget.size - color: "#CCC" - visible: Config.data.focusedWindow.icon.enabled && Niri.focusedWindow?.iconPath === "" - radius: 12 - } - - Text { - anchors.verticalCenter: parent.verticalCenter - text: Niri.focusedWindow?.title ?? "" - font.family: Config.data.focusedWindow.font.family || Config.data.theme.font.family - font.pixelSize: Config.data.focusedWindow.font.scale * Config.data.theme.font.size - font.weight: Config.data.focusedWindow.font.weight - color: Config.data.theme.color.text - visible: Config.data.focusedWindow.title.enabled - } -} diff --git a/fancy-bar/modules/bar/Workspaces.qml b/fancy-bar/modules/bar/Workspaces.qml deleted file mode 100644 index 3e4b8f3..0000000 --- a/fancy-bar/modules/bar/Workspaces.qml +++ /dev/null @@ -1,48 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import qs.modules.common -import qs.services - -Rectangle { - anchors.left: parent.left - color: Config.data.theme.color.background2 - height: 25 - width: 215 - bottomLeftRadius: 10 - bottomRightRadius: 10 - - Rectangle { - anchors { - verticalCenter: parent.verticalCenter - left: parent.left - right: parent.right - leftMargin: 10 - rightMargin: 10 - } - - RowLayout { - anchors { - verticalCenter: parent.verticalCenter - } - spacing: 5 - - Repeater { - model: Niri.workspaces - - Rectangle { - visible: index < 11 - width: Config.data.workspaces.icon.scale * Config.data.theme.widget.size - height: Config.data.workspaces.icon.scale * Config.data.theme.widget.size - radius: Config.data.workspaces.icon.scale * Config.data.theme.widget.size * Config.data.workspaces.icon.radius - color: model.isActive ? Config.data.theme.color.active : Config.data.theme.color.inactive - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Niri.focusWorkspaceById(model.id) - } - } - } - } - } -} diff --git a/fancy-bar/modules/common/Config.qml b/fancy-bar/modules/common/Config.qml deleted file mode 100644 index b151e1d..0000000 --- a/fancy-bar/modules/common/Config.qml +++ /dev/null @@ -1,104 +0,0 @@ -pragma Singleton -import Quickshell -import Quickshell.Io -import qs.modules.common - -Singleton { - property var data: adapter - - FileView { - path: Quickshell.shellPath("config.json") - watchChanges: true - onFileChanged: reload() - onAdapterUpdated: writeAdapter() - - JsonAdapter { - id: adapter - - // Global theme. Source of default and base values for all components. - property JsonObject theme: JsonObject { - property JsonObject color: JsonObject { - property string active: "#000000" - property string inactive: "#333333" - property string text: "#999999" - property string textMuted: "#777777" - property string foreground: "#999999" - property string background: "#222222" - property string background2: "#666666" - property string ok: "#1A7F39" - property string error: "#E5002E" - property string warning: "#E5BF00" - } - property JsonObject font: JsonObject { - property string family: "Sans" - // Size in pixels of all fonts. The actual size of fonts in - // individual components will be proportial to this value. - property real size: 14 - } - property JsonObject widget: JsonObject { - // Size in pixels of all widgets. The actual size of - // individual widgets will be proportial to this value. - property real size: 24 - } - } - - property JsonObject bar: JsonObject { - property string position: Types.positionToString(Types.Position.Top) - property int size: 30 - } - - property JsonObject focusedWindow: JsonObject { - property bool enabled: true - property JsonObject icon: JsonObject { - property bool enabled: true - property real scale: 1 - } - property JsonObject title: JsonObject { - property bool enabled: true - } - property JsonObject font: JsonObject { - property string family - property real scale: 1.2 - property int weight: 600 - } - } - - property JsonObject battery: JsonObject { - property bool enabled: true - property real scale: 1.5 - property int low: 20 - property int critical: 10 - property int suspend: 5 - property bool automaticSuspend: true - property bool showPercentage: true - property string orientation: Types.orientationToString(Types.Orientation.Horizontal) - } - - property JsonObject datetime: JsonObject { - property bool enabled: true - property real scale: 1 - property JsonObject time: JsonObject { - property bool enabled: true - property string format: "hh:mm" - } - property JsonObject date: JsonObject { - property bool enabled: true - property string format: "yyyy-MM-dd" - } - property JsonObject font: JsonObject { - property string family - property real scale: 1.1 - property int weight: 400 - } - } - - property JsonObject workspaces: JsonObject { - property bool enabled: true - property JsonObject icon: JsonObject { - property real scale: 0.6 - property real radius: 1 - } - } - } - } -} diff --git a/fancy-bar/modules/common/Types.qml b/fancy-bar/modules/common/Types.qml deleted file mode 100644 index 12131d3..0000000 --- a/fancy-bar/modules/common/Types.qml +++ /dev/null @@ -1,70 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - enum Orientation { - Horizontal, - Vertical - } - - enum Position { - Top, - Bottom - } - - /** - * I would prefer to use stdlib enum conversion functions: - * https://doc.qt.io/qt-6/qtqml-typesystem-enumerations.html - * But these aren't defined in Quickshell v0.2.1, for some reason, even - * though it does use Qt 6.10... - */ - function stringToOrientation(str) { - const normalized = str.toLowerCase(); - switch (normalized) { - case "horizontal": - return Types.Orientation.Horizontal; - case "vertical": - return Types.Orientation.Vertical; - default: - console.error("Error: invalid Orientation value:", str) - return -1; - } - } - - function orientationToString(value) { - switch (value) { - case Types.Orientation.Horizontal: - return "horizontal" - case Types.Orientation.Vertical: - return "vertical" - default: - console.error("Error: invalid Orientation value:", value) - return ""; - } - } - - function stringToPosition(str) { - const normalized = str.toLowerCase(); - switch (normalized) { - case "top": - return Types.Position.Top; - case "bottom": - return Types.Position.Bottom; - default: - console.error("Error: invalid Position value:", str) - return -1; - } - } - - function positionToString(value) { - switch (value) { - case Types.Position.Top: - return "top" - case Types.Position.Bottom: - return "bottom" - default: - console.error("Error: invalid Position value:", value) - return ""; - } - } -} diff --git a/fancy-bar/modules/common/utils/ColorUtils.qml b/fancy-bar/modules/common/utils/ColorUtils.qml deleted file mode 100644 index 795757e..0000000 --- a/fancy-bar/modules/common/utils/ColorUtils.qml +++ /dev/null @@ -1,130 +0,0 @@ -pragma Singleton -import Quickshell - -/* - Copied from https://github.com/end-4/dots-hyprland/blob/449df7f161e6435569bc7d9499b2e444dd8aa153/dots/.config/quickshell/ii/modules/common/functions/ColorUtils.qml -*/ -Singleton { - id: root - - /** - * Returns a color with the hue of color2 and the saturation, value, and alpha of color1. - * - * @param {string} color1 - The base color (any Qt.color-compatible string). - * @param {string} color2 - The color to take hue from. - * @returns {Qt.rgba} The resulting color. - */ - function withHueOf(color1, color2) { - var c1 = Qt.color(color1); - var c2 = Qt.color(color2); - - // Qt.color hsvHue/hsvSaturation/hsvValue/alpha return 0-1 - var hue = c2.hsvHue; - var sat = c1.hsvSaturation; - var val = c1.hsvValue; - var alpha = c1.a; - - return Qt.hsva(hue, sat, val, alpha); - } - - /** - * Returns a color with the saturation of color2 and the hue/value/alpha of color1. - * - * @param {string} color1 - The base color (any Qt.color-compatible string). - * @param {string} color2 - The color to take saturation from. - * @returns {Qt.rgba} The resulting color. - */ - function withSaturationOf(color1, color2) { - var c1 = Qt.color(color1); - var c2 = Qt.color(color2); - - var hue = c1.hsvHue; - var sat = c2.hsvSaturation; - var val = c1.hsvValue; - var alpha = c1.a; - - return Qt.hsva(hue, sat, val, alpha); - } - - /** - * Returns a color with the given lightness and the hue, saturation, and alpha of the input color (using HSL). - * - * @param {string} color - The base color (any Qt.color-compatible string). - * @param {number} lightness - The lightness value to use (0-1). - * @returns {Qt.rgba} The resulting color. - */ - function withLightness(color, lightness) { - var c = Qt.color(color); - return Qt.hsla(c.hslHue, c.hslSaturation, lightness, c.a); - } - - /** - * Returns a color with the lightness of color2 and the hue, saturation, and alpha of color1 (using HSL). - * - * @param {string} color1 - The base color (any Qt.color-compatible string). - * @param {string} color2 - The color to take lightness from. - * @returns {Qt.rgba} The resulting color. - */ - function withLightnessOf(color1, color2) { - var c2 = Qt.color(color2); - return colorWithLightness(color1, c2.hslLightness); - } - - /** - * Adapts color1 to the accent (hue and saturation) of color2 using HSL, keeping lightness and alpha from color1. - * - * @param {string} color1 - The base color (any Qt.color-compatible string). - * @param {string} color2 - The accent color. - * @returns {Qt.rgba} The resulting color. - */ - function adaptToAccent(color1, color2) { - var c1 = Qt.color(color1); - var c2 = Qt.color(color2); - - var hue = c2.hslHue; - var sat = c2.hslSaturation; - var light = c1.hslLightness; - var alpha = c1.a; - - return Qt.hsla(hue, sat, light, alpha); - } - - /** - * Mixes two colors by a given percentage. - * - * @param {string} color1 - The first color (any Qt.color-compatible string). - * @param {string} color2 - The second color. - * @param {number} percentage - The mix ratio (0-1). 1 = all color1, 0 = all color2. - * @returns {Qt.rgba} The resulting mixed color. - */ - function mix(color1, color2, percentage = 0.5) { - var c1 = Qt.color(color1); - var c2 = Qt.color(color2); - return Qt.rgba(percentage * c1.r + (1 - percentage) * c2.r, percentage * c1.g + (1 - percentage) * c2.g, percentage * c1.b + (1 - percentage) * c2.b, percentage * c1.a + (1 - percentage) * c2.a); - } - - /** - * Transparentizes a color by a given percentage. - * - * @param {string} color - The color (any Qt.color-compatible string). - * @param {number} percentage - The amount to transparentize (0-1). - * @returns {Qt.rgba} The resulting color. - */ - function transparentize(color, percentage = 1) { - var c = Qt.color(color); - return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage)); - } - - /** - * Sets the alpha channel of a color. - * - * @param {string} color - The base color (any Qt.color-compatible string). - * @param {number} alpha - The desired alpha (0-1). - * @returns {Qt.rgba} The resulting color with applied alpha. - */ - function applyAlpha(color, alpha) { - var c = Qt.color(color); - var a = Math.max(0, Math.min(1, alpha)); - return Qt.rgba(c.r, c.g, c.b, a); - } -} diff --git a/fancy-bar/modules/common/widgets/ProgressBarText.qml b/fancy-bar/modules/common/widgets/ProgressBarText.qml deleted file mode 100644 index 7c7b9f5..0000000 --- a/fancy-bar/modules/common/widgets/ProgressBarText.qml +++ /dev/null @@ -1,156 +0,0 @@ -import QtQuick -import QtQuick.Controls -import qs.modules.common -import qs.modules.common.utils - -/** - * A progress bar with optional text centered inside it. - * Partially based on https://github.com/end-4/dots-hyprland/blob/449df7f161e6435569bc7d9499b2e444dd8aa153/dots/.config/quickshell/ii/modules/common/widgets/ClippedProgressBar.qml - */ -ProgressBar { - id: root - property int orientation: Types.Orientation.Horizontal - property real valueBarWidth: 2 - property real valueBarHeight: 1 - property color highlightColor: "gray" - property color trackColor: ColorUtils.transparentize(highlightColor, 0.7) - property color textColor: "white" - property string text - property bool shimmer: false - property bool pulse: false - - font.weight: text.length > 2 ? Font.Medium : Font.DemiBold - - background: Item { - implicitHeight: valueBarHeight - implicitWidth: valueBarWidth - } - - contentItem: Rectangle { - id: contentItem - anchors.fill: parent - color: root.trackColor - - SequentialAnimation on color { - running: root.pulse - loops: Animation.Infinite - - ColorAnimation { - from: root.trackColor - to: { - var c = Qt.color(root.trackColor); - var boostedLight = Math.min(1.0, c.hslLightness + 0.2); - return Qt.hsla(c.hslHue, c.hslSaturation, boostedLight, c.a); - } - duration: 1000 - easing.type: Easing.InOutQuad - } - ColorAnimation { - from: { - var c = Qt.color(root.trackColor); - var boostedLight = Math.min(1.0, c.hslLightness + 0.2); - return Qt.hsla(c.hslHue, c.hslSaturation, boostedLight, c.a); - } - to: root.trackColor - duration: 1000 - easing.type: Easing.InOutQuad - } - } - - Rectangle { - id: progressFill - color: root.highlightColor - clip: true // ensure the shimmer is only visible inside progressFill - anchors { - top: parent.top - bottom: parent.bottom - left: parent.left - right: undefined - } - width: parent.width * root.visualPosition - height: parent.height - - states: State { - name: "vertical" - when: root.orientation === Types.Orientation.Vertical - AnchorChanges { - target: progressFill - anchors { - top: undefined - bottom: parent.bottom - left: parent.left - right: parent.right - } - } - PropertyChanges { - target: progressFill - width: parent.width - height: parent.height * root.visualPosition - } - } - - Rectangle { - id: shimmerOverlay - visible: root.shimmer - width: root.valueBarWidth - height: root.valueBarHeight - property real shimmerWidth: root.orientation === Types.Orientation.Vertical - ? root.valueBarHeight * 0.2 : root.valueBarWidth * 0.2 - x: root.orientation === Types.Orientation.Vertical ? 0 : -progressFill.x - y: root.orientation === Types.Orientation.Vertical ? -progressFill.y : 0 - opacity: 0.5 - - gradient: Gradient { - orientation: root.orientation === Types.Orientation.Vertical - ? Gradient.Vertical : Gradient.Horizontal - GradientStop { position: -0.3; color: "transparent" } - GradientStop { - position: Math.max(-0.3, - shimmerAnimation.position - shimmerOverlay.shimmerWidth - / (root.orientation === Types.Orientation.Vertical - ? root.valueBarHeight : root.valueBarWidth)) - color: "transparent" - } - GradientStop { position: shimmerAnimation.position; color: "white" } - GradientStop { - position: Math.min(1.3, - shimmerAnimation.position + shimmerOverlay.shimmerWidth - / (root.orientation === Types.Orientation.Vertical - ? root.valueBarHeight : root.valueBarWidth)) - color: "transparent" - } - GradientStop { position: 1.3; color: "transparent" } - } - - SequentialAnimation on x { - id: shimmerAnimation - property real position: 0.0 - running: root.shimmer - loops: Animation.Infinite - - NumberAnimation { - target: shimmerAnimation - property: "position" - from: root.orientation === Types.Orientation.Vertical ? 1.2 : -0.2 - to: root.orientation === Types.Orientation.Vertical ? 0.8 - value : value + 0.2 - duration: 3000 - easing.type: Easing.InOutExpo - } - } - } - } - } - - Text { - id: overlayText - font: root.font - text: root.text - color: textColor - opacity: 0.5 - width: root.width - height: root.height - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - style: Text.Outline - } -} diff --git a/fancy-bar/modules/icons/BatteryIcon.qml b/fancy-bar/modules/icons/BatteryIcon.qml deleted file mode 100644 index 8d97cab..0000000 --- a/fancy-bar/modules/icons/BatteryIcon.qml +++ /dev/null @@ -1,89 +0,0 @@ -import QtQuick -import qs.modules.common - -Item { - id: root - - property color iconColor: "white" - property int orientation: Types.Orientation.Horizontal - property real size: 1 - - readonly property real borderWidth: size * 0.05 - readonly property real bodyWidth: body.width - readonly property real bodyHeight: body.height - readonly property real bodyRadius: body.radius - - state: Types.orientationToString(orientation) - width: orientation === Types.Orientation.Horizontal ? size : size * 0.6 - height: orientation === Types.Orientation.Vertical ? size : size * 0.6 - - Rectangle { - id: body - color: "transparent" - border.color: root.iconColor - border.width: root.borderWidth - radius: root.size * 0.1 - } - - Rectangle { - id: nub - color: root.iconColor - radius: 0 - } - - states: [ - State { - name: "horizontal" - AnchorChanges { - target: body - anchors.left: parent.left - anchors.right: nub.left - anchors.verticalCenter: parent.verticalCenter - } - AnchorChanges { - target: nub - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - } - PropertyChanges { - target: body - width: root.width * 0.9 - height: root.height - } - PropertyChanges { - target: nub - width: root.size * 0.1 - height: root.size * 0.2 - topRightRadius: root.size * 0.1 - bottomRightRadius: root.size * 0.1 - } - }, - State { - name: "vertical" - AnchorChanges { - target: body - anchors.left: parent.left - anchors.right: parent.right - anchors.top: nub.bottom - anchors.bottom: parent.bottom - } - AnchorChanges { - target: nub - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - } - PropertyChanges { - target: body - width: root.width - height: root.height * 0.9 - } - PropertyChanges { - target: nub - width: root.size * 0.2 - height: root.size * 0.1 - topLeftRadius: root.size * 0.1 - topRightRadius: root.size * 0.1 - } - } - ] -} diff --git a/fancy-bar/services/Battery.qml b/fancy-bar/services/Battery.qml deleted file mode 100644 index aa0c96a..0000000 --- a/fancy-bar/services/Battery.qml +++ /dev/null @@ -1,56 +0,0 @@ -pragma Singleton - -import Quickshell -import Quickshell.Services.UPower -import QtQuick -import Quickshell.Io -import qs.services -import qs.modules.common - -Singleton { - property bool available: UPower.displayDevice.isLaptopBattery - property var chargeState: UPower.displayDevice.state - property bool isCharging: chargeState == UPowerDeviceState.Charging - property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge - property real percentage: UPower.displayDevice?.percentage ?? 1 - readonly property bool allowAutomaticSuspend: Config.data.battery.automaticSuspend - - property bool isLow: available && (percentage <= Config.data.battery.low / 100) - property bool isCritical: available && (percentage <= Config.data.battery.critical / 100) - property bool isSuspending: available && (percentage <= Config.data.battery.suspend / 100) - - property bool isLowAndNotCharging: isLow && !isCharging - property bool isCriticalAndNotCharging: isCritical && !isCharging - property bool isSuspendingAndNotCharging: allowAutomaticSuspend && isSuspending && !isCharging - - property real energyRate: UPower.displayDevice.changeRate - property real timeToEmpty: UPower.displayDevice.timeToEmpty - property real timeToFull: UPower.displayDevice.timeToFull - - onIsLowAndNotChargingChanged: { - if (available && isLowAndNotCharging) Quickshell.execDetached([ - "notify-send", - "Low battery", - "Consider plugging in your device", - "-u", "critical", - "-a", "Shell" - ]) - } - - onIsCriticalAndNotChargingChanged: { - if (available && isCriticalAndNotCharging) Quickshell.execDetached([ - "notify-send", - "Critically low battery", - "Please charge!\nAutomatic suspend triggers at %1".arg(Config.data.power.battery.suspend), - "-u", "critical", - "-a", "Shell" - ]); - - } - - onIsSuspendingAndNotChargingChanged: { - if (available && isSuspendingAndNotCharging) { - Quickshell.execDetached(["sh", "-c", `systemctl suspend || loginctl suspend`]); - } - } -} diff --git a/fancy-bar/services/Niri.qml b/fancy-bar/services/Niri.qml deleted file mode 100644 index 9fd639f..0000000 --- a/fancy-bar/services/Niri.qml +++ /dev/null @@ -1,14 +0,0 @@ -pragma Singleton -import QtQuick -import Niri 0.1 - -Niri { - id: niri - - Component.onCompleted: connect() - - onConnected: console.log("Connected to niri") - onErrorOccurred: function(error) { - console.error("Niri error:", error) - } -} diff --git a/fancy-bar/shell.qml b/fancy-bar/shell.qml deleted file mode 100644 index b2f552f..0000000 --- a/fancy-bar/shell.qml +++ /dev/null @@ -1,18 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Wayland -import "./modules/bar/" -import "./modules/common/" -import "./services/" - -ShellRoot{ - LazyLoader{ - active: true - component: Bar{ - position: Types.stringToPosition(Config.data.bar.position) - size: Config.data.bar.size - color: Config.data.theme.color.background - } - } -} diff --git a/simple-bar/README.md b/simple-bar/README.md new file mode 100644 index 0000000..336107f --- /dev/null +++ b/simple-bar/README.md @@ -0,0 +1,45 @@ +# simple-bar + +A minimal Quickshell bar with a system tray, time, battery, volume, and simple app launchers with single-instance focus. + +## Features +- Volume widget (PipeWire): mute on click, scroll to change, shows percent +- System tray: left/middle/right click actions, context menu displayed properly +- Time widget: 12-hour clock with date, implicit sizing for stable layout +- Battery widget (UPower): percentage with implicit sizing +- Launcher buttons: + - Nerd font icons + - Detects if an app is already open and focuses it instead of spawning + - If not open, launches detached via Quickshell.execDetached + +## Configure launchers +Edit `modules/bar/Bar.qml` and adjust the example loaders on the left side: + +``` +Loader { active: true; sourceComponent: LauncherButton { + icon: "" // nerd-font glyph + exec: "/usr/bin/alacritty" // command to start when not running + matchAppId: "alacritty" // used to detect existing windows + spawner: bar // use bar.spawn (execDetached) +}} +``` + +Tips: +- `matchAppId` is case-insensitive and matches substrings against multiple fields (Wayland toplevel appId and title; compositor fields like class/wmClass). +- If focusing doesn’t work at first, open the app and try a different token seen in its appId/class/title. Using the exact appId is usually best (e.g., `"Alacritty"`). +- Set `singleInstance: true` (default) to avoid duplicate instances when a match exists. + +## Spawn behavior +The bar prefers the documented Quickshell API: +- `Quickshell.execDetached(["sh", "-c", cmd])` to start processes detached +- Falls back to `niri.spawn(cmd)` if available + +No external helper is needed. + +## System tray +- Passes the bar window to tray menu display to satisfy platform menu requirements. +- Requires `//@ pragma UseQApplication` at the top of `shell.qml` (already set). + +## Notes +- Fonts: icons use "Symbols Nerd Font" and text uses "Barlow Medium"; adjust families to your installed fonts as needed. +- Layout: widgets use implicit sizing to prevent layout issues on startup. diff --git a/simple-bar/modules/bar/Bar.qml b/simple-bar/modules/bar/Bar.qml index 6102428..bd35f61 100644 --- a/simple-bar/modules/bar/Bar.qml +++ b/simple-bar/modules/bar/Bar.qml @@ -4,6 +4,23 @@ import Quickshell PanelWindow { id: bar + function spawn(cmd) { + try { + if (typeof Quickshell !== 'undefined' && typeof Quickshell.execDetached === 'function') { + // Use a shell to interpret the string command when necessary. + Quickshell.execDetached(["sh", "-c", cmd]); + return true; + } + // Fallback: attempt niri.spawn if provided by the compositor integration. + if (typeof niri !== 'undefined' && typeof niri.spawn === 'function') { + niri.spawn(cmd); + return true; + } + } catch (e) { + // ignore + } + return false; + } anchors { top: true left: true @@ -15,15 +32,22 @@ PanelWindow { Rectangle { anchors.fill: parent color: "#222222" - bottomLeftRadius: 20 - bottomRightRadius: 20 + bottomLeftRadius: 0 + bottomRightRadius: 0 + // left RowLayout { anchors { left: parent.left leftMargin: 25 } + spacing: 10 Loader { active: true; sourceComponent: Workspaces {} } + // Launchers: rely on Bar.spawn() -> Quickshell.execDetached to start commands detached. + Loader { active: true; sourceComponent: LauncherButton { icon: "󰖟"; exec: "/usr/bin/helium-browser"; matchAppId: "helium"; spawner: bar } } + Loader { active: true; sourceComponent: LauncherButton { icon: ""; exec: "/usr/bin/alacritty"; matchAppId: "alacritty"; spawner: bar } } + Loader { active: true; sourceComponent: LauncherButton { icon: ""; exec: "/usr/bin/dolphin"; matchAppId: "dolphin"; spawner: bar } } + Loader { active: true; sourceComponent: LauncherButton { icon: "󱩽"; exec: "/usr/bin/gedit"; matchAppId: "gedit"; spawner: bar } } } // center RowLayout { @@ -33,7 +57,7 @@ PanelWindow { } Text { - text: niri.focusedWindowTitle + text: (typeof niri !== 'undefined' && typeof niri.focusedWindowTitle === 'string') ? niri.focusedWindowTitle : "" font.family: "Barlow Medium" font.pixelSize: 16 color: "#999999" @@ -46,7 +70,9 @@ PanelWindow { right: parent.right rightMargin: 25 } - spacing: 10 + spacing: 20 + Loader { active: true; sourceComponent: Systray { window: bar } } + Loader { active: true; sourceComponent: Volume {} } Loader { active: true; sourceComponent: Power {} } Loader { active: true; sourceComponent: Time {} } } diff --git a/simple-bar/modules/bar/LauncherButton.qml b/simple-bar/modules/bar/LauncherButton.qml new file mode 100644 index 0000000..ef0896d --- /dev/null +++ b/simple-bar/modules/bar/LauncherButton.qml @@ -0,0 +1,160 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland as Wayland + +Item { + id: root + property alias icon: iconText.text + property string exec: "" // command to run when not open + property string matchAppId: "" // app id or wm-class to detect running windows + property int iconSize: 16 + property var spawner: undefined // optional object (e.g. the bar) with a spawn/spawnCommand method + // If true, never spawn a new instance when a matching window exists; instead just try to focus. + // If focusing fails, do nothing (prevents duplicate instances). + property bool singleInstance: true + + // Uniform cell sizing so icons align evenly regardless of glyph width + property int padding: 1 + property int cellWidth: iconSize + padding * 2 + width: cellWidth + height: Math.max(iconSize + padding * 2, iconText.implicitHeight + padding * 2) + Layout.preferredWidth: width + Layout.preferredHeight: height + + // Nerd-font icon text representing the app + Text { + id: iconText + text: "?" // replace with nerd font char via `icon` property + font.family: "Symbols Nerd Font" + font.pixelSize: root.iconSize + color: isOpen ? "#c4a912" : "#dac878" + anchors.centerIn: parent + } + + // Determine if a matching window is open by checking Wayland toplevels first, + // then compositor-specific collections (niri) as a fallback. + property bool isOpen: { + try { + var wins = listCandidates(); + for (var k=0;k 0) return out; + } + // Fallback: niri-provided windows + if (typeof niri !== 'undefined') { + if (niri.windows) return niri.windows; + if (niri.clients) return niri.clients; + if (niri.workspaces) { + for (var i=0;i 0) { + try { + // Prefer a provided spawner object (e.g. the bar) so we don't rely on environment globals + if (root.spawner && typeof root.spawner.spawn === 'function' && root.spawner.spawn(root.exec)) return; + if (typeof Quickshell !== 'undefined' && typeof Quickshell.execDetached === 'function') { + Quickshell.execDetached(["sh", "-c", root.exec]); + return; + } else if (typeof niri !== 'undefined' && typeof niri.spawn === 'function') { + niri.spawn(root.exec); + return; + } else { + // No spawn method available + } + } catch(e) { + // ignore + } + } + } + } +} diff --git a/simple-bar/modules/bar/Power.qml b/simple-bar/modules/bar/Power.qml index a91140b..48c5aa6 100644 --- a/simple-bar/modules/bar/Power.qml +++ b/simple-bar/modules/bar/Power.qml @@ -4,17 +4,31 @@ import Quickshell import Quickshell.Services.UPower Rectangle { - Text { - id: powerDisplay - anchors { - verticalCenter: parent.verticalCenter + // Keep background transparent so it doesn't show a white box behind the text + color: "transparent" + Row { + id: powerRow + anchors.verticalCenter: parent.verticalCenter + spacing: 6 + + Text { + id: powerIcon + text: "󰄌" + color: "#999999" + font.pixelSize: 16 + // Leave font.family unset so icon glyph can come from an icon/nerd font via fallback. } - text: Number(UPower.displayDevice.percentage * 100).toFixed(2) + "%" - color: "#999999" - font.family: "Barlow Medium" - font.pixelSize: 16 - Component.onCompleted: { - parent.width = powerDisplay.contentWidth + + Text { + id: powerPercent + text: Number(UPower.displayDevice.percentage * 100).toFixed(0) + "%" + color: "#999999" + font.family: "Barlow Medium" + font.pixelSize: 16 } } + + // Bind the Rectangle size to the row content so layout spacing is consistent + implicitWidth: powerRow.implicitWidth + implicitHeight: powerRow.implicitHeight } diff --git a/simple-bar/modules/bar/Systray.qml b/simple-bar/modules/bar/Systray.qml new file mode 100644 index 0000000..8107d2f --- /dev/null +++ b/simple-bar/modules/bar/Systray.qml @@ -0,0 +1,56 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Services.SystemTray + +// Minimal system tray row; shows icons and opens menus with the bar window context +RowLayout { + // The window the bar lives in; required for showing platform menus + required property var window + spacing: 8 + + Repeater { + model: SystemTray.items + + Item { + required property SystemTrayItem modelData + + implicitWidth: 20 + implicitHeight: 20 + + Image { + anchors.fill: parent + source: modelData.icon + fillMode: Image.PreserveAspectFit + smooth: true + } + + MouseArea { + id: mouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + hoverEnabled: true + + onClicked: function(mouse) { + if (mouse.button === Qt.LeftButton) { + modelData.activate(); + } else if (mouse.button === Qt.RightButton) { + if (modelData.menu && window) { + // Map local coordinates into window coordinates + var pos = mouseArea.mapToItem(window.contentItem, 0, mouseArea.height); + modelData.display(window, pos.x, pos.y); + } + } else if (mouse.button === Qt.MiddleButton) { + modelData.secondaryActivate(); + } + } + + onWheel: function(wheel) { modelData.scroll(wheel.angleDelta.y / 120, "vertical"); } + + ToolTip.visible: containsMouse && (modelData.tooltip?.title !== undefined && modelData.tooltip?.title !== "") + ToolTip.text: modelData.tooltip?.title ?? "" + } + } + } +} diff --git a/simple-bar/modules/bar/Time.qml b/simple-bar/modules/bar/Time.qml index 5ea2487..03172fe 100644 --- a/simple-bar/modules/bar/Time.qml +++ b/simple-bar/modules/bar/Time.qml @@ -3,6 +3,8 @@ import QtQuick.Layouts import Quickshell Rectangle { + // Make the background transparent so the parent bar color shows through + color: "transparent" SystemClock { id: clock precision: SystemClock.Seconds @@ -12,12 +14,25 @@ Rectangle { anchors { verticalCenter: parent.verticalCenter } - text: Qt.formatDateTime(clock.date, "hh:mm dd MMM, yyyy") - color: "#666666" + text: { + var d = clock.date; + if (!d) return ""; + var hours = d.getHours(); + var ampm = hours >= 12 ? "PM" : "AM"; + var h = hours % 12; + if (h === 0) h = 12; + var mins = d.getMinutes(); + var minsStr = mins < 10 ? "0" + mins : mins; + return h + ":" + minsStr + " " + ampm + " " + Qt.formatDate(d, "dd MMM, yyyy"); + } + color: "#c4a912" font.family: "Barlow Medium" font.pixelSize: 16 - Component.onCompleted: { - parent.width = timeBlock.contentWidth - } + // Use implicit sizing so the parent Rectangle's size follows the text + // Binding to contentWidth/contentHeight ensures the layout updates + // when fonts or the clock become available (avoids race at startup). } + + implicitWidth: timeBlock.contentWidth + implicitHeight: timeBlock.contentHeight } diff --git a/simple-bar/modules/bar/Volume.qml b/simple-bar/modules/bar/Volume.qml new file mode 100644 index 0000000..6ac354d --- /dev/null +++ b/simple-bar/modules/bar/Volume.qml @@ -0,0 +1,66 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pipewire + +Item { + PwObjectTracker { + objects: [Pipewire.defaultAudioSink] + } + + property var sink: Pipewire.defaultAudioSink + property real volume: sink?.audio?.volume ?? 0 + property bool muted: sink?.audio?.muted ?? false + + implicitWidth: volumeLayout.implicitWidth + implicitHeight: volumeLayout.implicitHeight + + RowLayout { + id: volumeLayout + anchors.fill: parent + spacing: 6 + + // Icon + Text { + text: muted ? "" : "󰕾" // Mute icon when muted, volume icon otherwise + color: "#999999" + font.family: "Symbols Nerd Font" + font.pixelSize: 16 + } + + // Percent text + Text { + text: { + var vol = Number(volume); + if (isNaN(vol) || !isFinite(vol)) { + return "0%"; + } + return Math.round(vol * 100) + "%"; + } + color: muted ? "#ff0000" : "#999999" + font.family: "Barlow Medium" + font.pixelSize: 16 + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (sink && sink.audio) { + sink.audio.muted = !sink.audio.muted; + } + } + + onWheel: function(wheel) { + if (sink && sink.audio) { + var delta = wheel.angleDelta.y / 120; // Standard wheel delta + var newVolume = volume + (delta * 0.05); // Change by 5% per scroll step + newVolume = Math.max(0, Math.min(1.5, newVolume)); // Clamp between 0 and 1 + sink.audio.volume = newVolume; + } + } + } +} \ No newline at end of file diff --git a/simple-bar/modules/bar/Workspaces.qml b/simple-bar/modules/bar/Workspaces.qml index f5d9ae9..512583d 100644 --- a/simple-bar/modules/bar/Workspaces.qml +++ b/simple-bar/modules/bar/Workspaces.qml @@ -34,7 +34,7 @@ Rectangle { width: 15 height: 15 radius: 10 - color: model.isActive ? "#000000" : "#333333" + color: model.isActive ? "#c4a912" : "#dac878" MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor diff --git a/simple-bar/shell.qml b/simple-bar/shell.qml index 4810544..af5b7b7 100644 --- a/simple-bar/shell.qml +++ b/simple-bar/shell.qml @@ -1,3 +1,5 @@ +//@ pragma UseQApplication + import QtQuick import QtQuick.Layouts import Quickshell