up su Gitea

This commit is contained in:
2026-04-19 17:07:18 +02:00
parent e78ce720bb
commit fe54b28378
298 changed files with 23460 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
{
"mPrimary": "#d5bbff",
"mOnPrimary": "#3d1c6f",
"mSecondary": "#cec2db",
"mOnSecondary": "#342d40",
"mTertiary": "#f1b7c3",
"mOnTertiary": "#4a252f",
"mError": "#ffb4ab",
"mOnError": "#690005",
"mSurface": "#141316",
"mOnSurface": "#e6e1e6",
"mSurfaceVariant": "#211f22",
"mOnSurfaceVariant": "#cbc4cf",
"mOutline": "#49454e",
"mShadow": "#000000",
"mHover": "#f1b7c3",
"mOnHover": "#4a252f"
}
@@ -0,0 +1,94 @@
{
"dark": {
"mPrimary": "#e8bcfb",
"mOnPrimary": "#8e0cc6",
"mSecondary": "#8e0cc6",
"mOnSecondary": "#e8bcfb",
"mTertiary": "#066f50",
"mOnTertiary": "#90f9d9",
"mError": "#e80e4f",
"mOnError": "#fee7ee",
"mSurface": "#300443",
"mOnSurface": "#fef7e7",
"mSurfaceVariant": "#50066f",
"mOnSurfaceVariant": "#e8bcfb",
"mOutline": "#8e0cc6",
"mShadow": "#110118",
"mHover": "#6f099a",
"mOnHover": "#fef7e7",
"terminal": {
"foreground": "#f7e7fe",
"background": "#110118",
"normal": {
"black": "#110118",
"red": "#e80e4f",
"green": "#cb62f8",
"yellow": "#e8a70e",
"blue": "#bc39f3",
"magenta": "#e80ebc",
"cyan": "#a70ee8",
"white": "#e8bcfb"
},
"bright": {
"black": "#310345",
"red": "#c73859",
"green": "#da8efa",
"yellow": "#f3bc39",
"blue": "#ca65f6",
"magenta": "#f339ce",
"cyan": "#bc39f3",
"white": "#f7e7fe"
},
"cursor": "#f7e7fe",
"cursorText": "#110118",
"selectionFg": "#f7e7fe",
"selectionBg": "#50066f"
}
},
"light": {
"mPrimary": "#8e0cc6",
"mOnPrimary": "#e8bcfb",
"mSecondary": "#ca65f6",
"mOnSecondary": "#a70ee8",
"mTertiary": "#bcfbe8",
"mOnTertiary": "#066f50",
"mError": "#f33971",
"mOnError": "#430417",
"mSurface": "#f7e7fe",
"mOnSurface": "#6f099a",
"mSurfaceVariant": "#e8bcfb",
"mOnSurfaceVariant": "#50066f",
"mOutline": "#ca65f6",
"mShadow": "#f7e7fe",
"mHover": "#d990f9",
"mOnHover": "#300443",
"terminal": {
"foreground": "#110118",
"background": "#f7e7fe",
"normal": {
"black": "#110118",
"red": "#e80e4f",
"green": "#cb62f8",
"yellow": "#e8a70e",
"blue": "#bc39f3",
"magenta": "#e80ebc",
"cyan": "#a70ee8",
"white": "#e8bcfb"
},
"bright": {
"black": "#310345",
"red": "#c73859",
"green": "#da8efa",
"yellow": "#f3bc39",
"blue": "#ca65f6",
"magenta": "#f339ce",
"cyan": "#bc39f3",
"white": "#f7e7fe"
},
"cursor": "#110118",
"cursorText": "#f7e7fe",
"selectionFg": "#50066f",
"selectionBg": "#e8bcfb"
}
}
}
+32
View File
@@ -0,0 +1,32 @@
{
"sources": [
{
"enabled": true,
"name": "Noctalia Plugins",
"url": "https://github.com/noctalia-dev/noctalia-plugins"
}
],
"states": {
"catwalk": {
"enabled": false,
"sourceUrl": "https://github.com/noctalia-dev/noctalia-plugins"
},
"fancy-audiovisualizer": {
"enabled": true,
"sourceUrl": "https://github.com/noctalia-dev/noctalia-plugins"
},
"file-search": {
"enabled": true,
"sourceUrl": "https://github.com/noctalia-dev/noctalia-plugins"
},
"kde-connect": {
"enabled": true,
"sourceUrl": "https://github.com/noctalia-dev/noctalia-plugins"
},
"slowbongo": {
"enabled": false,
"sourceUrl": "https://github.com/noctalia-dev/noctalia-plugins"
}
},
"version": 2
}
+193
View File
@@ -0,0 +1,193 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.UI
import qs.Widgets
import qs.Services.System
Item {
id: root
property var pluginApi: null
property ShellScreen screen
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
// Per-screen bar properties
readonly property string screenName: screen?.name ?? ""
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
property url currentIconSource
property string tooltipText: {
if (!pluginApi) return "";
return root.isRunning ? (pluginApi.tr("tooltip.running") || "Running") : (pluginApi.tr("tooltip.sleeping") || "Sleeping");
}
property string tooltipDirection: BarService.getTooltipDirection()
property bool enabled: true
property bool allowClickWhenDisabled: false
property bool hovering: false
property color colorBg: Color.mSurfaceVariant
property color colorFg: Color.mPrimary
property color colorBgHover: Color.mHover
property color colorFgHover: Color.mOnHover
property color colorBorder: Color.mOutline
property color colorBorderHover: Color.mOutline
property real customRadius: Style.radiusL
signal entered
signal exited
signal clicked
signal rightClicked
signal middleClicked
signal wheel(int angleDelta)
readonly property real contentWidth: barIsVertical ? capsuleHeight : Math.round(capsuleHeight + Style.marginXS * 2)
readonly property real contentHeight: capsuleHeight
implicitWidth: contentWidth
implicitHeight: contentHeight
// --- Catwalk Specific Logic ---
property int frameIndex: 0
property int idleFrameIndex: 0
readonly property bool isRunning: root.pluginApi?.mainInstance?.isRunning ?? false
readonly property var icons: root.pluginApi?.mainInstance?.icons || []
readonly property var idleIcons: root.pluginApi?.mainInstance?.idleIcons || []
readonly property real cpuUsage: root.pluginApi?.mainInstance?.cpuUsage ?? 0
function openPanel() {
if (pluginApi) {
var result = pluginApi.openPanel(root.screen);
Logger.i("Catwalk", "OpenPanel result:", result);
} else {
Logger.e("Catwalk", "PluginAPI is null");
}
}
function openExternalMonitor() {
Quickshell.execDetached(["sh", "-c", Settings.data.systemMonitor.externalMonitor]);
}
Timer {
interval: Math.max(30, 200 - root.cpuUsage * 1.7)
running: root.isRunning
repeat: true
onTriggered: {
root.frameIndex = (root.frameIndex + 1) % root.icons.length
}
}
Timer {
interval: 400
running: !root.isRunning
repeat: true
onTriggered: {
root.idleFrameIndex = (root.idleFrameIndex + 1) % root.idleIcons.length
}
}
currentIconSource: (root.icons && root.icons.length > 0 && root.idleIcons && root.idleIcons.length > 0)
? (root.isRunning
? Qt.resolvedUrl(root.icons[root.frameIndex % root.icons.length])
: Qt.resolvedUrl(root.idleIcons[root.idleFrameIndex % root.idleIcons.length]))
: ""
Rectangle {
id: visualCapsule
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
width: root.contentWidth
height: root.contentHeight
opacity: root.enabled ? Style.opacityFull : Style.opacityMedium
color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor
radius: Math.min((customRadius >= 0 ? customRadius : Style.iRadiusL), width / 2)
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
Image {
id: iconImage
source: root.currentIconSource
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
width: Style.toOdd(visualCapsule.width - Style.marginXS * 2)
height: width
// Render SVG at exact target size for crisp output
sourceSize: Qt.size(width, height)
fillMode: Image.PreserveAspectFit
smooth: true
mipmap: false
// This enables the "mask" behavior to recolor the icon
layer.enabled: true
layer.effect: MultiEffect {
colorization: 1.0
colorizationColor: Settings.data.colorSchemes.darkMode ? "white" : "black"
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
onEntered: {
root.hovering = true;
if (root.tooltipText) {
TooltipService.show(root, root.tooltipText, root.tooltipDirection);
}
root.entered();
}
onExited: {
root.hovering = false;
if (root.tooltipText) {
TooltipService.hide();
}
root.exited();
}
onClicked: function (mouse) {
if (root.tooltipText) {
TooltipService.hide();
}
Logger.i("Catwalk", "Clicked! API:", !!pluginApi, "Screen:", root.screen ? root.screen.name : "null");
if (!root.enabled && !root.allowClickWhenDisabled) {
return;
}
// Open Panel on left/right click
// Open external monitor on middle click
if (mouse.button === Qt.LeftButton) {
root.openPanel();
root.clicked();
} else if (mouse.button === Qt.RightButton) {
root.openPanel();
root.rightClicked();
} else if (mouse.button === Qt.MiddleButton) {
root.openExternalMonitor();
root.middleClicked();
}
}
onWheel: wheel => root.wheel(wheel.angleDelta.y)
}
}
@@ -0,0 +1,85 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Widgets
import qs.Services.System
DraggableDesktopWidget {
id: root
property var pluginApi: null
implicitWidth: 200
implicitHeight: 80
showBackground: !(root.pluginApi?.mainInstance?.hideBackground ?? false)
property int frameIndex: 0
readonly property var icons: root.pluginApi?.mainInstance?.icons || []
property int idleFrameIndex: 0
readonly property var idleIcons: root.pluginApi?.mainInstance?.idleIcons || []
readonly property bool isRunning: root.pluginApi?.mainInstance?.isRunning ?? false
readonly property real cpuUsage: root.pluginApi?.mainInstance?.cpuUsage ?? 0
Timer {
interval: Math.max(30, 200 - root.cpuUsage * 1.7)
running: root.isRunning
repeat: true
onTriggered: {
root.frameIndex = (root.frameIndex + 1) % root.icons.length
}
}
Timer {
interval: 400
running: !root.isRunning
repeat: true
onTriggered: {
root.idleFrameIndex = (root.idleFrameIndex + 1) % root.idleIcons.length
}
}
property url currentIconSource: (root.icons && root.icons.length > 0 && root.idleIcons && root.idleIcons.length > 0)
? (root.isRunning
? Qt.resolvedUrl(root.icons[root.frameIndex % root.icons.length])
: Qt.resolvedUrl(root.idleIcons[root.idleFrameIndex % root.idleIcons.length]))
: ""
RowLayout {
anchors.fill: parent
spacing: 5
Image {
id: iconImage
source: root.currentIconSource
Layout.fillHeight: true
Layout.preferredWidth: height
sourceSize.height: height
sourceSize.width: width
fillMode: Image.PreserveAspectFit
smooth: true
mipmap: false
layer.enabled: true
layer.effect: MultiEffect {
colorization: 1.0
colorizationColor: Settings.data.colorSchemes.darkMode ? "white" : "black"
}
}
Text {
text: Math.round(root.cpuUsage) + "%"
color: Settings.data.colorSchemes.darkMode ? "white" : "black"
font.bold: true
font.pixelSize: 40
Layout.alignment: Qt.AlignVCenter
}
}
}
+18
View File
@@ -0,0 +1,18 @@
import QtQuick
import Quickshell
import qs.Services.System
Item {
id: root
property var pluginApi: null
readonly property real minimumThreshold: pluginApi?.pluginSettings?.minimumThreshold || 10
readonly property bool hideBackground: pluginApi?.pluginSettings?.hideBackground ?? false
property real cpuUsage: SystemStatService.cpuUsage
readonly property bool isRunning: cpuUsage >= minimumThreshold
readonly property var icons: ["icons/my-active-0-symbolic.svg", "icons/my-active-1-symbolic.svg", "icons/my-active-2-symbolic.svg", "icons/my-active-3-symbolic.svg", "icons/my-active-4-symbolic.svg"]
readonly property var idleIcons: ["icons/my-idle-0-symbolic.svg", "icons/my-idle-1-symbolic.svg", "icons/my-idle-2-symbolic.svg", "icons/my-idle-3-symbolic.svg"]
}
+102
View File
@@ -0,0 +1,102 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import qs.Commons
import qs.Services.System
import qs.Widgets
Item {
id: root
property var pluginApi: null
// SmartPanel properties
readonly property var geometryPlaceholder: panelContainer
readonly property bool allowAttach: true
property real contentPreferredWidth: 300 * Style.uiScaleRatio
property real contentPreferredHeight: 300 * Style.uiScaleRatio
anchors.fill: parent
Rectangle {
id: panelContainer
anchors.fill: parent
color: "transparent"
Rectangle {
anchors.fill: parent
anchors.margins: Style.marginL
color: Color.mSurface
radius: Style.radiusL
border.color: Color.mOutline
border.width: Style.borderS
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginL
// Big Cat
Item {
id: bigCatItem
Layout.preferredWidth: 128 * Style.uiScaleRatio
Layout.preferredHeight: 128 * Style.uiScaleRatio
Layout.alignment: Qt.AlignHCenter
property int frameIndex: 0
readonly property bool isRunning: root.pluginApi?.mainInstance?.isRunning ?? false
readonly property var icons: root.pluginApi?.mainInstance?.icons || []
property int idleFrameIndex: 0
readonly property var idleIcons: root.pluginApi?.mainInstance?.idleIcons || []
readonly property real cpuUsage: root.pluginApi?.mainInstance?.cpuUsage ?? 0
Timer {
interval: Math.max(30, 200 - bigCatItem.cpuUsage * 1.7)
running: bigCatItem.isRunning
repeat: true
onTriggered: bigCatItem.frameIndex = (bigCatItem.frameIndex + 1) % bigCatItem.icons.length
}
Timer {
interval: 400
running: !bigCatItem.isRunning
repeat: true
onTriggered: bigCatItem.idleFrameIndex = (bigCatItem.idleFrameIndex + 1) % bigCatItem.idleIcons.length
}
Image {
id: bigCatImage
anchors.fill: parent
source: (bigCatItem.icons && bigCatItem.icons.length > 0 && bigCatItem.idleIcons && bigCatItem.idleIcons.length > 0)
? (bigCatItem.isRunning
? Qt.resolvedUrl(bigCatItem.icons[bigCatItem.frameIndex % bigCatItem.icons.length])
: Qt.resolvedUrl(bigCatItem.idleIcons[bigCatItem.idleFrameIndex % bigCatItem.idleIcons.length]))
: ""
fillMode: Image.PreserveAspectFit
smooth: true
mipmap: true
// This handles the programmatic coloring
layer.enabled: true
layer.effect: MultiEffect {
colorization: 1.0
colorizationColor: Settings.data.colorSchemes.darkMode ? "white" : "black"
}
}
}
// CPU Stats
Text {
Layout.alignment: Qt.AlignHCenter
text: (pluginApi?.tr("panel.cpuLabel") || "CPU: {usage}%").replace("{usage}", Math.round(root.pluginApi?.mainInstance?.cpuUsage ?? 0))
font.pointSize: Style.fontSizeXL
font.weight: Font.Bold
color: Settings.data.colorSchemes.darkMode ? "white" : "black"
}
}
}
}
}
+34
View File
@@ -0,0 +1,34 @@
# Catwalk Plugin for Noctalia
A cute animated cat for your Noctalia bar that reacts to your system's CPU usage.
## Features
- **Animated Cat**: The cat walks/runs on your bar based on CPU usage
- **CPU-Based Animation**:
- Below minimum threshold: Shows idle animation with "Zz" bubbles
- Above minimum threshold: Walks faster as CPU usage increases
- Speed scales continuously with CPU load
- **Popup Panel**: Click the cat to open a larger animated version with CPU stats
- **Theme Support**: Automatically switches between light/dark mode icons
- **Configurable Settings**: Adjust the minimum CPU threshold for running animation
## Installation
This plugin is part of the `noctalia-plugins` repository.
## Configuration
Access the plugin settings in Noctalia to configure:
- **Minimum CPU Threshold**: Set the CPU usage percentage (5-25%) above which the cat starts running. Below this, it stays idle with "Zz" animation.
## Usage
- The cat icon appears on your bar
- It automatically animates based on your CPU usage
- Click to open the CPU stats panel
## Requirements
- Noctalia 3.6.0 or later
+75
View File
@@ -0,0 +1,75 @@
import QtQuick
import QtQuick.Layouts
import qs.Widgets
import qs.Commons
ColumnLayout {
id: root
// Plugin API (injected by the settings dialog system)
property var pluginApi: null
// Local state - track changes before saving
property real valueMinimumThreshold: pluginApi?.mainInstance?.minimumThreshold ?? (pluginApi?.pluginSettings?.minimumThreshold || 10)
property bool valueHideBackground: pluginApi?.mainInstance?.hideBackground ?? (pluginApi?.pluginSettings?.hideBackground ?? false)
spacing: Style.marginM
Component.onCompleted: {
Logger.i("Catwalk", "Settings UI loaded");
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
NLabel {
label: pluginApi?.tr("settings.minimumThreshold.label") || "Minimum CPU Threshold"
description: pluginApi?.tr("settings.minimumThreshold.description") || "CPU usage must be above this percentage for the cat to start running"
}
NSlider {
id: thresholdSlider
from: 5
to: 25
value: root.valueMinimumThreshold
stepSize: 1
onValueChanged: {
root.valueMinimumThreshold = value
}
}
Text {
text: (pluginApi?.tr("settings.currentThreshold") || "Current threshold: {value}%").replace("{value}", thresholdSlider.value)
color: Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS
}
}
NToggle {
label: pluginApi?.tr("settings.hideBackground.label") || "Hide Background"
description: pluginApi?.tr("settings.hideBackground.description") || "Hide the background of the desktop widget"
checked: root.valueHideBackground
onToggled: function(checked) {
root.valueHideBackground = checked
}
}
// This function is called by the dialog to save settings
function saveSettings() {
if (!pluginApi) {
Logger.e("Catwalk", "Cannot save settings: pluginApi is null");
return;
}
// Update the plugin settings object
pluginApi.pluginSettings.minimumThreshold = root.valueMinimumThreshold;
pluginApi.pluginSettings.hideBackground = root.valueHideBackground;
// Save to disk
pluginApi.saveSettings();
Logger.i("Catwalk", "Settings saved successfully");
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "CPU: {usage}%",
"title": "Catwalk"
},
"settings": {
"currentThreshold": "Aktuelle Schwelle: {value}%",
"hideBackground": {
"description": "Verstecke den Hintergrund des Desktop-Widgets",
"label": "Hintergrund ausblenden"
},
"minimumThreshold": {
"description": "Die CPU-Auslastung muss über diesem Prozentsatz liegen, damit die Katze zu laufen beginnt",
"label": "Minimale CPU-Schwelle"
},
"title": "Catwalk-Einstellungen"
},
"tooltip": {
"running": "Katze läuft",
"sleeping": "Katze schläft"
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "CPU: {usage}%",
"title": "Catwalk"
},
"settings": {
"currentThreshold": "Current threshold: {value}%",
"hideBackground": {
"description": "Hide the background of the desktop widget",
"label": "Hide Background"
},
"minimumThreshold": {
"description": "CPU usage must be above this percentage for the cat to start running",
"label": "Minimum CPU Threshold"
},
"title": "Catwalk Settings"
},
"tooltip": {
"running": "Cat is running",
"sleeping": "Cat is sleeping"
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "CPU: {usage}%",
"title": "Catwalk"
},
"settings": {
"currentThreshold": "Umbral actual: {value}%",
"hideBackground": {
"description": "Ocultar el fondo del widget de escritorio",
"label": "Ocultar fondo"
},
"minimumThreshold": {
"description": "El uso de CPU debe estar por encima de este porcentaje para que el gato empiece a correr",
"label": "Umbral mínimo de CPU"
},
"title": "Configuración de Catwalk"
},
"tooltip": {
"running": "El gato está corriendo",
"sleeping": "El gato está durmiendo"
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "CPU : {usage}%",
"title": "Catwalk"
},
"settings": {
"currentThreshold": "Seuil actuel : {value}%",
"hideBackground": {
"description": "Masquer l'arrière-plan du widget de bureau",
"label": "Masquer l'arrière-plan"
},
"minimumThreshold": {
"description": "L'utilisation du CPU doit être supérieure à ce pourcentage pour que le chat commence à courir",
"label": "Seuil minimum CPU"
},
"title": "Paramètres Catwalk"
},
"tooltip": {
"running": "Le chat court",
"sleeping": "Le chat dort"
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "CPU: {usage}%",
"title": "Sétasáv"
},
"settings": {
"currentThreshold": "Aktuális küszöbérték: {value}%",
"hideBackground": {
"description": "Asztali widget hátterének elrejtése",
"label": "Háttér elrejtése"
},
"minimumThreshold": {
"description": "A processzorhasználatnak e fölött a százalék felett kell lennie, hogy a macska futni kezdjen",
"label": "Minimum CPU Küszöbérték"
},
"title": "Catwalk beállítások"
},
"tooltip": {
"running": "A Noctalia fut.",
"sleeping": "A macska alszik"
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "CPU: {usage}%",
"title": "Catwalk"
},
"settings": {
"currentThreshold": "Soglia attuale: {value}%",
"hideBackground": {
"description": "Nascondi lo sfondo del widget desktop",
"label": "Nascondi sfondo"
},
"minimumThreshold": {
"description": "L'utilizzo della CPU deve essere superiore a questa percentuale affinché il gatto inizi a correre",
"label": "Soglia minima CPU"
},
"title": "Impostazioni Catwalk"
},
"tooltip": {
"running": "Il gatto sta correndo",
"sleeping": "Il gatto sta dormendo"
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "CPU: {usage}%",
"title": "Catwalk"
},
"settings": {
"currentThreshold": "現在のしきい値: {value}%",
"hideBackground": {
"description": "デスクトップウィジェットの背景を隠す",
"label": "背景を隠す"
},
"minimumThreshold": {
"description": "猫が走り始めるにはCPU使用率がこの割合を超えている必要があります",
"label": "最小CPUしきい値"
},
"title": "Catwalk設定"
},
"tooltip": {
"running": "猫が走っています",
"sleeping": "猫が寝ています"
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "CPU: {usage}%",
"title": "Rêwînga pisîkan"
},
"settings": {
"currentThreshold": "Astana niha: {value}%",
"hideBackground": {
"description": "Paşxana wîceta sermaseyê veşêre",
"label": "Veşartina Paşxanê"
},
"minimumThreshold": {
"description": "Divê bikaranîna CPU ji vê rêjeyê bilindtir be da ku pisîk dest bi bezê bike",
"label": "Sînorê herî kêm ê CPU"
},
"title": "Mîhenên Catwalkê"
},
"tooltip": {
"running": "Pisîk direve",
"sleeping": "Pisîk radizê."
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "CPU: {usage}%",
"title": "Catwalk"
},
"settings": {
"currentThreshold": "Huidige drempelwaarde: {value}%",
"hideBackground": {
"description": "Verberg de achtergrond van de bureaubladwidget",
"label": "Achtergrond verbergen"
},
"minimumThreshold": {
"description": "CPU-gebruik moet boven dit percentage liggen voordat de kat begint te rennen.",
"label": "Minimale CPU-drempel"
},
"title": "Catwalk Instellingen"
},
"tooltip": {
"running": "Kat rent",
"sleeping": "Kat slaapt"
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "CPU: {usage}%",
"title": "Wybieg"
},
"settings": {
"currentThreshold": "Aktualny próg: {value}%",
"hideBackground": {
"description": "Ukryj tło widżetu pulpitu",
"label": "Ukryj tło"
},
"minimumThreshold": {
"description": "Użycie CPU musi przekraczać ten procent, aby kot zaczął biegać",
"label": "Minimalny próg użycia CPU"
},
"title": "Ustawienia Catwalk"
},
"tooltip": {
"running": "Kot jest uruchomiony",
"sleeping": "Kot śpi"
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "CPU: {usage}%",
"title": "Catwalk"
},
"settings": {
"currentThreshold": "Limite atual: {value}%",
"hideBackground": {
"description": "Ocultar o fundo do widget da área de trabalho",
"label": "Ocultar fundo"
},
"minimumThreshold": {
"description": "O uso da CPU deve estar acima desta porcentagem para o gato começar a correr",
"label": "Limite mínimo de CPU"
},
"title": "Configurações do Catwalk"
},
"tooltip": {
"running": "O gato está correndo",
"sleeping": "O gato está dormindo"
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "CPU: {usage}%",
"title": "Catwalk"
},
"settings": {
"currentThreshold": "Текущий порог: {value}%",
"hideBackground": {
"description": "Скрыть фон виджета рабочего стола",
"label": "Скрыть фон"
},
"minimumThreshold": {
"description": "Использование CPU должно быть выше этого процента, чтобы кот начал бежать",
"label": "Минимальный порог CPU"
},
"title": "Настройки Catwalk"
},
"tooltip": {
"running": "Кот бежит",
"sleeping": "Кот спит"
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "CPU: {usage}%",
"title": "Podyum"
},
"settings": {
"currentThreshold": "Mevcut eşik: {value}%",
"hideBackground": {
"description": "Masaüstü bileşeninin arka planını gizle",
"label": "Arka planı gizle"
},
"minimumThreshold": {
"description": "CPU kullanımı, kedinin koşmaya başlaması için bu yüzdeden yüksek olmalıdır.",
"label": "Minimum CPU Eşiği"
},
"title": "Podyum Ayarları"
},
"tooltip": {
"running": "Kedi koşuyor.",
"sleeping": "Kedi uyuyor."
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "ЦП: {usage}%",
"title": "Подіум"
},
"settings": {
"currentThreshold": "Поточний поріг: {value}%",
"hideBackground": {
"description": "Приховати фон віджета робочого столу",
"label": "Приховати фон"
},
"minimumThreshold": {
"description": "Використання ЦП має бути вище цього відсотка, щоб кіт почав бігати.",
"label": "Мінімальний поріг ЦП"
},
"title": "Налаштування подіуму"
},
"tooltip": {
"running": "Кіт біжить",
"sleeping": "Кіт спить"
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"panel": {
"cpuLabel": "CPU{usage}%",
"title": "猫步"
},
"settings": {
"currentThreshold": "当前阈值:{value}%",
"hideBackground": {
"description": "隐藏桌面小部件的背景",
"label": "隐藏背景"
},
"minimumThreshold": {
"description": "CPU 使用率必须高于此百分比,猫才能开始跑动。",
"label": "最低CPU阈值"
},
"title": "时装秀场布置"
},
"tooltip": {
"running": "猫在跑",
"sleeping": "猫在睡觉"
}
}
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
height="388"
width="388"
>
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#ffffff
}
</style>
</defs>
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title>RunCat: Frame 0</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<title>RunCat: Frame 0</title>
<path style="fill:currentColor"
d="m 321.24,116.28019 c -0.01,-13.464 1.661,-33.778997 -8.15,-35.162997 -9.811,-1.384 -25.074,26.578997 -25.074,26.578997 l -4.43,0.554 c 0,0 3.6,-26.025997 -8.86,-23.256997 -12.46,2.769 -27.687,41.810997 -27.687,41.810997 0,0 -5.538,4.429 -9.414,18.3 -3.876,13.871 -18.55,14.09 -42.915,11.045 -24.365,-3.045 -47.345,17.443 -47.345,17.443 -22.15,-0.831 -40.977,2.215 -55.374,7.752 -14.397,5.537 -32.117,23.811 -55.375,22.15 -23.258,-1.661 -34.332,2.492 -32.947,10.521 1.385,8.029 9.413,9.414 22.426,10.8 13.013,1.386 44.853,-7.476 65.342,-19.381 a 78.147,78.147 0 0 1 41.53,-10.143 c -2.768,24.19 6.646,21.218 5.538,45.029 -1.108,23.811 8.583,46.791 24.365,59.527 15.782,12.736 21.6,6.091 22.149,-1.661 0.549,-7.752 -15.781,-27.687 -16.335,-35.44 -0.554,-7.753 9.137,-1.384 10.244,0.554 1.107,1.938 16.889,17.166 29.9,20.489 13.011,3.323 7.2,-15.782 2.492,-27.134 -4.708,-11.352 3.6,-22.15 19.1,-24.641 15.5,-2.491 22.7,5.814 19.658,16.058 -3.042,10.244 -4.153,24.919 8.86,24.088 13.013,-0.831 34.332,-32.117 35.993,-50.668 1.661,-18.551 4.153,-28.794 16.059,-32.67 11.906,-3.876 33.5,-15.782 33.5,-37.932 0,-22.15 -23.25,-34.61 -23.25,-34.61 z m -46.722,1.107 c -1.592,6.091 -16.4,11.144 -16.4,11.144 0,0 1.873,-21.665 17.862,-26.28 a 43.462,43.462 0 0 1 -1.462,15.136 z m 16.182,39.385 c -5.433,0 -9.85,-5.237 -9.85,-11.673 0,-6.436 4.417,-11.671 9.85,-11.671 5.433,0 9.85,5.235 9.85,11.671 0,6.436 -4.418,11.673 -9.85,11.673 z m 21.311,-43.907 c -4.155,-2.006 -6.5,-3.074 -10.883,-3.517 0,0 2.808,-11.618997 11.345,-14.110997 a 36.68,36.68 0 0 1 -0.462,17.627997 z m 11.6,40.931 c -5.433,0 -9.85,-5.223 -9.85,-11.644 0,-6.421 4.419,-11.648 9.852,-11.648 5.433,0 9.847,5.225 9.847,11.646 0,6.421 -4.418,11.646 -9.847,11.646 z"
class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
height="388"
width="388"
viewBox="0 0 388 388">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#ffffff
}
</style>
</defs>
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title>RunCat: Frame 1</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<title>RunCat: Frame 1</title>
<path style="fill:currentColor"
d="m 327.054,197.34487 1.662,-4.153 c 35.162,-4.43 46.517,-27.359 44.671,-42.219 -1.938,-15.6 -17.815,-29.352 -23.629,-31.705 0,0 4.43,-34.885998 -5.261,-34.885998 -11.365,0 -26.856,26.578998 -26.856,26.578998 h -4.153 c 0,0 -0.277,-24.363998 -7.2,-24.086998 -14.939,0.6 -31.01,42.083998 -31.01,42.083998 -3.6,3.876 -4.43,13.29 -14.951,24.088 -10.521,10.798 -36.244,9.357 -56.733,9.08 -20.489,-0.277 -40.724,16.669 -40.724,16.669 -6.368,0 -24.088,-3.323 -39.316,-4.43 -15.228,-1.107 -47.345,0.831 -55.928,1.938 -8.583,1.107 -60.358,12.459 -57.312,23.811 3.046,11.352 24.364,4.153 50.113,0 25.749,-4.153 65.342,-4.43 74.756,-1.384 9.414,3.046 -1.385,20.488 -18.551,25.2 -17.166,4.712 -32.117,22.421 -39.592,30.452 -7.475,8.031 -14.4,23.257 1.938,24.088 16.338,0.831 32.117,-16.613 39.869,-19.381 7.752,-2.768 2.215,8.306 -0.554,15.227 -2.769,6.921 -6.368,28.518 4.984,29.349 11.352,0.831 28.8,-25.472 32.117,-30.179 3.317,-4.707 18,-11.352 21.319,-14.4 3.319,-3.048 12.183,-12.46 12.183,-12.46 0,0 32.394,-0.83 48.729,-3.876 16.335,-3.046 31.84,-15.5 40.146,-16.612 8.306,-1.112 19.935,7.2 22.981,9.414 3.046,2.214 25.749,15.781 37.1,18.55 11.351,2.769 12.46,-12.182 12.46,-12.182 0,0 9.967,4.983 15.5,-6.368 5.533,-11.351 -38.758,-38.207 -38.758,-38.207 z m -28.425,-72.171 c -2.492,3.415 -12,4.984 -12,4.984 0,0 6.46,-19.2 16.8,-23.35 1.753,9.414 -1.739,14.174 -4.8,18.366 z m 53.251,7.5 c 5.5,0 9.967,5.578 9.967,12.459 0,6.881 -4.462,12.459 -9.967,12.459 -5.505,0 -9.967,-5.578 -9.967,-12.459 0,-6.881 4.462,-12.451 9.967,-12.451 z m -11.014,-32.22 c 0.655,6.977 -2.275,14.593 -2.275,14.593 a 30.833,30.833 0 0 0 -8.971,-1.753 c 4.614,-9.183 7.863,-11.931 11.246,-12.84 z m -22.118,58.868 c -5.5,0 -9.967,-5.579 -9.967,-12.46 0,-6.881 4.462,-12.459 9.967,-12.459 5.505,0 9.968,5.578 9.968,12.459 0,6.881 -4.463,12.46 -9.968,12.46 z"
class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
height="388"
width="388"
viewBox="0 0 388 388">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#ffffff
}
</style>
</defs>
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title>RunCat: Frame 2</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<title>RunCat: Frame 2</title>
<path style="fill:currentColor"
d="m 382.705,182.19956 c 4.43,-19.381 -18,-37.654 -18,-37.654 0,0 6.645,-31.148 0,-33.779 -11.665,-4.617 -30.733,22.427 -30.733,22.427 l -4.707,-0.831 c 5.123,-32.531999 -5.676,-26.3 -18.965,-14.4 -13.289,11.9 -25.334,36.27 -33.917,44.3 -8.583,8.03 -18.827,5.26 -38.208,-3.887 -19.381,-9.147 -42.085,-6.081 -50.668,-1.374 -8.583,4.707 -15.777,1.941 -15.777,1.941 0,0 -24.642,-17.443 -32.394,-20.765 -7.752,-3.322 -28.518,-12.736 -53.99,-13.29 -25.472,-0.554 -46.792,9.967 -46.792,9.967 -10.521,9.137 1.385,16.889 12.737,14.4 11.352,-2.489 38.208,-6.092 56.758,-1.939 18.55,4.153 46.791,25.2 46.238,31.287 -0.553,6.087 -12.09,16.8 -25.2,19.658 -12.9,2.815 -48.175,29.071 -49.283,42.638 -1.108,13.567 14.4,14.4 29.626,6.645 15.226,-7.755 26.856,-1.384 26.856,-1.384 -14.12,13.843 -15.5,30.732 -3.6,33.224 11.9,2.492 22.149,-9.69 37.1,-23.257 14.951,-13.567 29.9,-19.935 53.437,-17.166 23.537,2.769 60.911,-0.554 60.911,-0.554 9.968,9.691 42.915,30.179 55.1,35.993 12.185,5.814 25.749,2.492 27.41,-3.6 1.661,-6.092 -16.612,-20.765 -14.951,-24.641 1.661,-3.876 19.461,3.225 26.411,-7.1 6.4,-9.509 -26.134,-24.46 -31.4,-25.844 -5.266,-1.384 -2.215,-5.814 -2.215,-5.814 17.451,-1.667 33.787,-5.82 38.216,-25.201 z m -18.39,-23.248 c 5.561,0 10.069,5.279 10.069,11.79 0,6.511 -4.508,11.79 -10.069,11.79 -5.561,0 -10.068,-5.278 -10.068,-11.79 0,-6.512 4.508,-11.79 10.068,-11.79 z m -4.728,-35.033 c 0,0 1.28,6.61 -5.019,15.47 a 48.782,48.782 0 0 0 -9.056,-3.089 c 0,0 8.26,-11.689 14.075,-12.381 z m -47.207,20.627 c -3.2,2.628 -10.936,3.184 -10.936,3.184 0,0 5.587,-16.156 19.084,-21.763 -0.277,8.583 -1.872,13.422 -8.148,18.579 z m 19.566,35.506 c -6.015,0 -10.89,-5.608 -10.89,-12.526 0,-6.918 4.875,-12.526 10.89,-12.526 6.015,0 10.89,5.608 10.89,12.526 0,6.918 -4.876,12.526 -10.89,12.526 z"
class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
height="388"
width="388"
viewBox="0 0 388 388">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#ffffff
}
</style>
</defs>
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title>RunCat: Frame 3</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<title>RunCat: Frame 3</title>
<path style="fill:currentColor"
d="m 364.709,159.58645 c 0,0 13.013,-29.833 4.983,-32.117 -15.7,-4.465 -32.344,14.823 -32.344,14.823 l -4.018,-0.887 c 0,0 3.415,-14.766 2.861,-21.688 -0.554,-6.922 -18.55,0.83 -28.795,9.413 -10.245,8.583 -29.9,36.271 -41.807,41.531 -11.907,5.26 -25.472,-13.013 -43.746,-26.026 -18.274,-13.013 -44.576,-2.492 -44.576,-2.492 -4.43,-1.384 -11.075,-13.566 -24.088,-26.3 C 140.166,103.10945 126.6,93.137455 96.7,88.153455 c -29.9,-4.984 -44.166,4.789 -42.5,11.157 1.666,6.367995 4.3,6.147995 38.352,9.331995 39.125,3.658 50.114,23.534 59.528,34.886 9.414,11.352 -3.046,22.7 -13.844,38.208 -10.798,15.508 -6.922,26.3 -4.984,39.316 1.938,13.016 13.567,16.335 15.5,35.162 1.933,18.827 6.645,25.2 18.551,21.043 11.636,-4.06 6.876,-43.539 6.655,-45.322 0.112,0.294 1.322,0.967 13,-4.515 13.567,-6.368 26.857,3.322 44.3,7.752 17.443,4.43 30.179,-0.277 34.609,9.691 8.9,20.018 23.257,45.13 40.7,53.99 17.443,8.86 20.765,-8.86 14.674,-16.059 -6.091,-7.199 -7.2,-19.1 -7.2,-19.1 34.885,4.984 43.191,-1.661 44.3,-10.244 1.109,-8.583 -17.72,-13.29 -26.857,-18 -9.137,-4.71 -2.768,-12.182 -2.768,-12.182 29.071,-3.323 45.868,-11.352 48.452,-32.394 2.143,-17.453 -12.459,-31.288 -12.459,-31.288 z m -62.3,-8.029 c 0,0 10.429,-15.874 22.242,-19.012 0,0 -0.554,11.352 -5.907,15.69 -6.296,5.105 -16.331,3.324 -16.331,3.324 z m 26.026,37.008 c -5.811,0 -10.521,-5.33 -10.521,-11.905 0,-6.575 4.71,-11.906 10.521,-11.906 5.811,0 10.521,5.331 10.521,11.906 0,6.575 -4.707,11.905 -10.517,11.905 z m 34.516,-49.744 c 0.018,3.471 -4.4,12.116 -6.959,13.62 a 16.906,16.906 0 0 0 -7.883,-4.3 c 1.756,-2.193 8.571,-9.345 14.846,-9.32 z m -2.857,55.638 c -5.084,0 -9.206,-4.913 -9.206,-10.974 0,-6.061 4.122,-10.973 9.206,-10.973 5.084,0 9.207,4.913 9.207,10.973 0,6.06 -4.122,10.974 -9.207,10.974 z"
class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
height="388"
width="388"
viewBox="0 0 388 388">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#ffffff
}
</style>
</defs>
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title>RunCat: Frame 4</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<title>RunCat: Frame 4</title>
<path style="fill:currentColor"
d="m 328.854,134.31128 c 0,0 9.967,-30.872 0.692,-34.194 -9.275,-3.322 -29.487,19.1 -29.487,19.1 l -4.983,-1.107 c 2.907,-25.057 0.277,-27.411 -14.536,-16.474 -14.813,10.937 -26.58,30.871 -29.625,34.055 -3.045,3.184 -5.953,14.951 -33.5,1.246 -27.547,-13.705 -44.714,-5.814 -60.635,-1.938 -15.921,3.876 -50.113,22.565 -68.941,29.763 -18.828,7.198 -37.931,4.154 -42.084,3.461 -4.153,-0.693 -23.4,-15.228 -30.594,-7.337 -7.194,7.891 0.692,16.336 8.583,22.7 7.891,6.364 27.272,9 43.745,8.444 16.473,-0.556 65.9,-18.273 65.9,-18.273 2.815,3.922 0.877,2.722 1.984,15.274 1.107,12.552 15.874,24 19.75,28.61 3.876,4.61 0.923,9.783 1.661,21.6 0.738,11.817 12,11.444 21.781,11.444 9.781,0 13.29,5.537 15.136,9.783 1.846,4.246 5.168,10.152 15.5,10.152 10.332,0 12.182,-4.061 14.951,-8.122 2.769,-4.061 12,-8.122 16.243,-7.752 4.243,0.37 7.014,21.042 11.259,28.24 4.245,7.198 19.75,11.26 22.519,9.045 2.769,-2.215 6.091,-3.323 1.292,-19.012 -4.799,-15.689 -1.292,-39.685 0.185,-47.253 1.477,-7.568 3.137,-18.458 30.271,-23.626 27.134,-5.168 40.608,-18.458 40.608,-36.178 0,-17.72 -17.675,-31.651 -17.675,-31.651 z m -49.468,-5.584 a 15.8,15.8 0 0 1 -11.905,3.921 c 0,0 7.752,-15.827 18.92,-18.412 -0.001,0.001 0.83,7.523 -7.015,14.491 z m 15.644,37.847 c -5.709,0 -10.337,-5.131 -10.337,-11.46 0,-6.329 4.628,-11.46 10.337,-11.46 5.709,0 10.336,5.131 10.336,11.46 0,6.329 -4.628,11.46 -10.336,11.46 z m 16.7,-41.908 c 0,0 7.16,-9.46 11.52,-10.429 0.277,5.446 -0.807,10.106 -4.453,13.8 a 26.262,26.262 0 0 0 -7.07,-3.371 z m 16.9,47.077 c -5.1,0 -9.229,-4.787 -9.229,-10.693 0,-5.906 4.132,-10.693 9.229,-10.693 5.097,0 9.229,4.788 9.229,10.693 0,5.905 -4.139,10.698 -9.236,10.698 z"
class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
height="388"
width="388">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#ffffff
}
</style>
</defs>
<path style="fill:currentColor"
d="M 365.652,223.08199 c 0,0 11.752,-20.337 9.131,-34.046 -1.842,-9.637 -31.391,18.589 -31.391,18.589 l -6.092,-1.557 c 0,0 8.033,-29.068 0.383,-30.981 -7.65,-1.913 -29.451,15.3 -41.308,30.981 0,0 -20.654,5.119 -29.068,31.437 -8.414,26.318 -45.515,-29.142 -58.52,-37.939 -13.005,-8.797 -28.685,-26.773 -70.376,-13.769 -41.690997,13.004 -54.688997,34.988 -54.693997,53.549 0,15.3 2.677,22.184 -1.53,28.686 0,0 -47.427,-4.972 -43.6,-43.6 3.827,-38.628 32.129,-32.511 36.718,-32.893 4.589,-0.382 14.152,-4.972 12.24,-16.447 -1.912,-11.475 -19.51,-16.832 -33.279,-11.477 -13.769,5.355 -43.6,19.124 -43.6,61.579 0,42.455 25.626,61.937 69.229,74.966 43.602997,13.029 104.413997,13.025 123.533997,13.025 19.12,0 125.453,2.27 132.72,-5.762 0,0 41.251,-12.68 41.251,-44.347 0.004,-25.239 -3.591,-31.012 -11.748,-39.991 z M 326.829,202.74698999999998 c 0,0 -5.915,-2.07 -14.787,0 0,0 10.942,-11.534 14.787,-11.534 3.845,0 0,11.534 0,11.534 z M 358.53700000000003,217.33299 c -1.774,-2.366 -6.323,-4.549 -6.323,-4.549 0,0 8.775,-7.195 9.366,-5.124 0.591,2.071 -3.042,9.673 -3.042,9.673 z"
class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
height="388"
width="388">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#ffffff
}
</style>
</defs>
<path style="fill:currentColor"
d="M 365.652,223.08199 c 0,0 11.752,-20.337 9.131,-34.046 -1.842,-9.637 -31.391,18.589 -31.391,18.589 l -6.092,-1.557 c 0,0 8.033,-29.068 0.383,-30.981 -7.65,-1.913 -29.451,15.3 -41.308,30.981 0,0 -20.654,5.119 -29.068,31.437 -8.414,26.318 -45.515,-29.142 -58.52,-37.939 -13.005,-8.797 -28.685,-26.773 -70.376,-13.769 -41.690997,13.004 -54.688997,34.988 -54.693997,53.549 0,15.3 2.677,22.184 -1.53,28.686 0,0 -47.427,-4.972 -43.6,-43.6 3.827,-38.628 32.129,-32.511 36.718,-32.893 4.589,-0.382 14.152,-4.972 12.24,-16.447 -1.912,-11.475 -19.51,-16.832 -33.279,-11.477 -13.769,5.355 -43.6,19.124 -43.6,61.579 0,42.455 25.626,61.937 69.229,74.966 43.602997,13.029 104.413997,13.025 123.533997,13.025 19.12,0 125.453,2.27 132.72,-5.762 0,0 41.251,-12.68 41.251,-44.347 0.004,-25.239 -3.591,-31.012 -11.748,-39.991 z M 326.829,202.74698999999998 c 0,0 -5.915,-2.07 -14.787,0 0,0 10.942,-11.534 14.787,-11.534 3.845,0 0,11.534 0,11.534 z M 358.53700000000003,217.33299 c -1.774,-2.366 -6.323,-4.549 -6.323,-4.549 0,0 8.775,-7.195 9.366,-5.124 0.591,2.071 -3.042,9.673 -3.042,9.673 z M 269.042,141.19398999999999 v -7.225 h 30.575 v 7.383 l -18.971,19.281 h 19.645 v 7.289 h -32.6 v -6.915 l 19.168,-19.811 z"
class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
height="388"
width="388">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#ffffff
}
</style>
</defs>
<path style="fill:currentColor"
d="M 365.652,223.08199 c 0,0 11.752,-20.337 9.131,-34.046 -1.842,-9.637 -31.391,18.589 -31.391,18.589 l -6.092,-1.557 c 0,0 8.033,-29.068 0.383,-30.981 -7.65,-1.913 -29.451,15.3 -41.308,30.981 0,0 -20.654,5.119 -29.068,31.437 -8.414,26.318 -45.515,-29.142 -58.52,-37.939 -13.005,-8.797 -28.685,-26.773 -70.376,-13.769 -41.690997,13.004 -54.688997,34.988 -54.693997,53.549 0,15.3 2.677,22.184 -1.53,28.686 0,0 -47.427,-4.972 -43.6,-43.6 3.827,-38.628 32.129,-32.511 36.718,-32.893 4.589,-0.382 14.152,-4.972 12.24,-16.447 -1.912,-11.475 -19.51,-16.832 -33.279,-11.477 -13.769,5.355 -43.6,19.124 -43.6,61.579 0,42.455 25.626,61.937 69.229,74.966 43.602997,13.029 104.413997,13.025 123.533997,13.025 19.12,0 125.453,2.27 132.72,-5.762 0,0 41.251,-12.68 41.251,-44.347 0.004,-25.239 -3.591,-31.012 -11.748,-39.991 z M 326.829,202.74698999999998 c 0,0 -5.915,-2.07 -14.787,0 0,0 10.942,-11.534 14.787,-11.534 3.845,0 0,11.534 0,11.534 z M 358.53700000000003,217.33299 c -1.774,-2.366 -6.323,-4.549 -6.323,-4.549 0,0 8.775,-7.195 9.366,-5.124 0.591,2.071 -3.042,9.673 -3.042,9.673 z M 269.042,141.19398999999999 v -7.225 h 30.575 v 7.383 l -18.971,19.281 h 19.645 v 7.289 h -32.6 v -6.915 l 19.168,-19.811 z M 218.504,85.393992 v -10.809 h 40.16 v 11.042 l -24.918,28.837998 h 25.8 v 10.9 h -42.812 v -10.341 l 25.172,-29.629998 z"
class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
height="388"
width="388">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#ffffff
}
</style>
</defs>
<path style="fill:currentColor"
d="M 365.652,223.08199 c 0,0 11.752,-20.337 9.131,-34.046 -1.842,-9.637 -31.391,18.589 -31.391,18.589 l -6.092,-1.557 c 0,0 8.033,-29.068 0.383,-30.981 -7.65,-1.913 -29.451,15.3 -41.308,30.981 0,0 -20.654,5.119 -29.068,31.437 -8.414,26.318 -45.515,-29.142 -58.52,-37.939 -13.005,-8.797 -28.685,-26.773 -70.376,-13.769 -41.690997,13.004 -54.688997,34.988 -54.693997,53.549 0,15.3 2.677,22.184 -1.53,28.686 0,0 -47.427,-4.972 -43.6,-43.6 3.827,-38.628 32.129,-32.511 36.718,-32.893 4.589,-0.382 14.152,-4.972 12.24,-16.447 -1.912,-11.475 -19.51,-16.832 -33.279,-11.477 -13.769,5.355 -43.6,19.124 -43.6,61.579 0,42.455 25.626,61.937 69.229,74.966 43.602997,13.029 104.413997,13.025 123.533997,13.025 19.12,0 125.453,2.27 132.72,-5.762 0,0 41.251,-12.68 41.251,-44.347 0.004,-25.239 -3.591,-31.012 -11.748,-39.991 z M 326.829,202.74698999999998 c 0,0 -5.915,-2.07 -14.787,0 0,0 10.942,-11.534 14.787,-11.534 3.845,0 0,11.534 0,11.534 z M 358.53700000000003,217.33299 c -1.774,-2.366 -6.323,-4.549 -6.323,-4.549 0,0 8.775,-7.195 9.366,-5.124 0.591,2.071 -3.042,9.673 -3.042,9.673 z M 218.504,85.393992 v -10.809 h 40.16 v 11.042 l -24.918,28.837998 h 25.8 v 10.9 h -42.812 v -10.341 l 25.172,-29.629998 z"
class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+32
View File
@@ -0,0 +1,32 @@
{
"id": "catwalk",
"name": "Catwalk",
"version": "1.1.7",
"minNoctaliaVersion": "3.6.0",
"author": "MannuVilasara",
"license": "MIT",
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
"description": "A cute animated cat for your bar.",
"tags": [
"Bar",
"Desktop",
"Panel",
"Fun"
],
"entryPoints": {
"main": "Main.qml",
"barWidget": "BarWidget.qml",
"desktopWidget": "DesktopWidget.qml",
"panel": "Panel.qml",
"settings": "Settings.qml"
},
"dependencies": {
"plugins": []
},
"metadata": {
"defaultSettings": {
"minimumThreshold": 10,
"hideBackground": false
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

+4
View File
@@ -0,0 +1,4 @@
{
"minimumThreshold": 5,
"hideBackground": false
}
@@ -0,0 +1,150 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Services.Media
DraggableDesktopWidget {
id: root
property var pluginApi: null
implicitWidth: Math.round(300 * widgetScale)
implicitHeight: Math.round(300 * widgetScale)
showBackground: false
// Scaled dimensions
readonly property int scaledRadiusL: Math.round(Style.radiusL * widgetScale)
// Settings from plugin
readonly property real sensitivity: widgetData?.sensitivity ?? pluginApi?.pluginSettings?.sensitivity ?? pluginApi?.manifest?.metadata?.defaultSettings?.sensitivity
readonly property real rotationSpeed: widgetData?.rotationSpeed ?? pluginApi?.pluginSettings?.rotationSpeed ?? pluginApi?.manifest?.metadata?.defaultSettings?.rotationSpeed
readonly property real barWidth: widgetData?.barWidth ?? pluginApi?.pluginSettings?.barWidth ?? pluginApi?.manifest?.metadata?.defaultSettings?.barWidth
readonly property real ringOpacity: widgetData?.ringOpacity ?? pluginApi?.pluginSettings?.ringOpacity ?? pluginApi?.manifest?.metadata?.defaultSettings?.ringOpacity
readonly property real bloomIntensity: widgetData?.bloomIntensity ?? pluginApi.pluginSettings?.bloomIntensity ?? pluginApi?.manifest?.metadata?.defaultSettings?.bloomIntensity
readonly property int visualizationMode: widgetData?.visualizationMode ?? pluginApi?.pluginSettings?.visualizationMode ?? pluginApi?.manifest?.metadata?.defaultSettings?.visualizationMode ?? 3
readonly property real waveThickness: widgetData?.waveThickness ?? pluginApi?.pluginSettings?.waveThickness ?? pluginApi?.manifest?.metadata?.defaultSettings?.waveThickness ?? 1.0
readonly property real innerDiameter: widgetData?.innerDiameter ?? pluginApi?.pluginSettings?.innerDiameter ?? pluginApi?.manifest?.metadata?.defaultSettings?.innerDiameter ?? 0.7
readonly property bool fadeWhenIdle: widgetData?.fadeWhenIdle ?? pluginApi?.pluginSettings?.fadeWhenIdle ?? false
readonly property bool useCustomColors: widgetData?.useCustomColors ?? pluginApi?.pluginSettings?.useCustomColors ?? false
readonly property color customPrimaryColor: widgetData?.customPrimaryColor ?? pluginApi?.pluginSettings?.customPrimaryColor ?? "#6750A4"
readonly property color customSecondaryColor: widgetData?.customSecondaryColor ?? pluginApi?.pluginSettings?.customSecondaryColor ?? "#625B71"
// Animation time for shader (0 to 3600, 1 hour cycle)
property real shaderTime: 0
NumberAnimation on shaderTime {
loops: Animation.Infinite
from: 0
to: 3600
duration: 3600000
running: !SpectrumService.isIdle
}
// Hidden canvas that encodes audio data as a 32x1 texture
Canvas {
id: audioCanvas
width: 32
height: 1
visible: false
onPaint: {
var ctx = getContext("2d");
var values = SpectrumService.values;
if (!values || values.length === 0) {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, 32, 1);
return;
}
for (var i = 0; i < 32; i++) {
var v = values[i] || 0;
// Encode amplitude in grayscale (R=G=B=amplitude)
var c = Math.floor(v * 255);
ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
ctx.fillRect(i, 0, 1, 1);
}
}
}
// Trigger canvas repaint when audio data changes
Connections {
target: SpectrumService
function onValuesChanged() {
if (!SpectrumService.isIdle) {
audioCanvas.requestPaint();
}
}
}
// Unique instance ID for SpectrumService registration
// This prevents the old widget's destruction from unregistering the new widget
readonly property string spectrumInstanceId: "plugin:fancy-audiovisualizer:" + Date.now() + Math.random()
// Register with SpectrumService when pluginApi becomes available
onPluginApiChanged: {
if (pluginApi) {
SpectrumService.registerComponent(spectrumInstanceId);
audioCanvas.requestPaint();
}
}
Component.onDestruction: {
SpectrumService.unregisterComponent(spectrumInstanceId);
}
// Audio texture source (outside ShaderEffect to avoid 'source' property warning)
ShaderEffectSource {
id: audioTextureSource
sourceItem: audioCanvas
live: true
hideSource: true
}
// The shader effect visualization
ShaderEffect {
id: visualizer
anchors.fill: parent
visible: pluginApi !== null
opacity: (root.fadeWhenIdle && SpectrumService.isIdle) ? 0 : 1
Behavior on opacity {
NumberAnimation { duration: 500; easing.type: Easing.InOutQuad }
}
// Audio texture - named 'source' to match ShaderEffectSource's property and avoid warning
property var source: audioTextureSource
// Uniforms passed to shader
property real time: root.shaderTime
property real itemWidth: visualizer.width
property real itemHeight: visualizer.height
property color primaryColor: root.useCustomColors ? root.customPrimaryColor : Color.mPrimary
property color secondaryColor: root.useCustomColors ? root.customSecondaryColor : Color.mSecondary
property real sensitivity: root.sensitivity
property real rotationSpeed: root.rotationSpeed
property real barWidth: root.barWidth
property real ringOpacity: root.ringOpacity
property real cornerRadius: scaledRadiusL
property real bloomIntensity: root.bloomIntensity
property real visualizationMode: root.visualizationMode
property real waveThickness: root.waveThickness
property real innerDiameter: root.innerDiameter
fragmentShader: pluginApi ? Qt.resolvedUrl(pluginApi.pluginDir + "/shaders/visualizer.frag.qsb") : ""
}
// Fallback when shader not loaded
Rectangle {
anchors.fill: parent
color: Color.mSurface
radius: scaledRadiusL
visible: !visualizer.visible || visualizer.fragmentShader === ""
Text {
anchors.centerIn: parent
text: "Loading..."
color: Color.mOnSurface
font.pointSize: Math.round(Style.fontSizeM * widgetScale)
}
}
}
@@ -0,0 +1,244 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
property var pluginApi: null
property var widgetSettings: null
property var screen: null
spacing: Style.marginM
// Local state for editing
property real valueSensitivity: widgetSettings?.data?.sensitivity ?? pluginApi?.pluginSettings?.sensitivity ?? 1.5
property real valueRotationSpeed: widgetSettings?.data?.rotationSpeed ?? pluginApi?.pluginSettings?.rotationSpeed ?? 0.5
property real valueBarWidth: widgetSettings?.data?.barWidth ?? pluginApi?.pluginSettings?.barWidth ?? 0.6
property real valueRingOpacity: widgetSettings?.data?.ringOpacity ?? pluginApi?.pluginSettings?.ringOpacity ?? 0.8
property real valueBloomIntensity: widgetSettings?.data?.bloomIntensity ?? pluginApi?.pluginSettings?.bloomIntensity ?? 0.5
property int valueVisualizationMode: widgetSettings?.data?.visualizationMode ?? pluginApi?.pluginSettings?.visualizationMode ?? 3
property real valueWaveThickness: widgetSettings?.data?.waveThickness ?? pluginApi?.pluginSettings?.waveThickness ?? 1.0
property real valueInnerDiameter: widgetSettings?.data?.innerDiameter ?? pluginApi?.pluginSettings?.innerDiameter ?? 0.7
property bool valueFadeWhenIdle: widgetSettings?.data?.fadeWhenIdle ?? pluginApi?.pluginSettings?.fadeWhenIdle ?? true
property bool valueUseCustomColors: widgetSettings?.data?.useCustomColors ?? pluginApi?.pluginSettings?.useCustomColors ?? false
property color valueCustomPrimaryColor: widgetSettings?.data?.customPrimaryColor ?? pluginApi?.pluginSettings?.customPrimaryColor ?? "#6750A4"
property color valueCustomSecondaryColor: widgetSettings?.data?.customSecondaryColor ?? pluginApi?.pluginSettings?.customSecondaryColor ?? "#625B71"
// Mode helpers
readonly property bool modeHasBars: valueVisualizationMode === 0 || valueVisualizationMode === 3 || valueVisualizationMode === 5
readonly property bool modeHasWave: valueVisualizationMode === 1 || valueVisualizationMode === 4 || valueVisualizationMode === 5
readonly property bool modeHasRings: valueVisualizationMode >= 2
NHeader {
label: pluginApi?.tr("settings.title") ?? "Visualizer Settings"
description: pluginApi?.tr("settings.description") ?? "Configure the audio visualizer appearance"
}
// Visualization mode selector
NComboBox {
Layout.fillWidth: true
label: pluginApi?.tr("settings.visualizationMode") ?? "Visualization Mode"
description: pluginApi?.tr("settings.visualizationMode-description") ?? "Choose visualization style"
model: [
{"key": "0", "name": pluginApi?.tr("settings.mode.bars") ?? "Bars"},
{"key": "1", "name": pluginApi?.tr("settings.mode.wave") ?? "Wave"},
{"key": "2", "name": pluginApi?.tr("settings.mode.rings") ?? "Rings"},
{"key": "3", "name": pluginApi?.tr("settings.mode.barsRings") ?? "Bars + Rings"},
{"key": "4", "name": pluginApi?.tr("settings.mode.waveRings") ?? "Wave + Rings"},
{"key": "5", "name": pluginApi?.tr("settings.mode.all") ?? "All"}
]
currentKey: String(root.valueVisualizationMode)
onSelected: key => {
root.valueVisualizationMode = parseInt(key);
root.saveSettings();
}
}
// Wave thickness slider (shown when mode includes wave)
NValueSlider {
Layout.fillWidth: true
visible: root.modeHasWave
label: pluginApi?.tr("settings.waveThickness") ?? "Wave Thickness"
value: root.valueWaveThickness
from: 0.3
to: 2.0
stepSize: 0.1
onMoved: value => {
root.valueWaveThickness = value;
root.saveSettings();
}
}
// Sensitivity slider
NValueSlider {
Layout.fillWidth: true
label: pluginApi?.tr("settings.sensitivity") ?? "Sensitivity"
value: root.valueSensitivity
from: 0.5
to: 3.0
stepSize: 0.1
onMoved: value => {
root.valueSensitivity = value;
root.saveSettings();
}
}
// Rotation speed slider
NValueSlider {
Layout.fillWidth: true
label: pluginApi?.tr("settings.rotationSpeed") ?? "Rotation Speed"
value: root.valueRotationSpeed
from: 0.0
to: 2.0
stepSize: 0.1
onMoved: value => {
root.valueRotationSpeed = value;
root.saveSettings();
}
}
// Bar width slider (shown when mode includes bars)
NValueSlider {
Layout.fillWidth: true
visible: root.modeHasBars
label: pluginApi?.tr("settings.barWidth") ?? "Bar Width"
value: root.valueBarWidth
from: 0.2
to: 1.0
stepSize: 0.1
onMoved: value => {
root.valueBarWidth = value;
root.saveSettings();
}
}
// Ring opacity slider (shown when mode includes rings)
NValueSlider {
Layout.fillWidth: true
visible: root.modeHasRings
label: pluginApi?.tr("settings.ringOpacity") ?? "Ring Opacity"
value: root.valueRingOpacity
from: 0.0
to: 1.0
stepSize: 0.1
onMoved: value => {
root.valueRingOpacity = value;
root.saveSettings();
}
}
// Base diameter slider
NValueSlider {
Layout.fillWidth: true
label: pluginApi?.tr("settings.innerDiameter") ?? "Inner Diameter"
value: root.valueInnerDiameter
from: 0
to: 1
stepSize: 0.05
onMoved: value => {
root.valueInnerDiameter = value;
root.saveSettings();
}
}
// Bloom intensity slider
NValueSlider {
Layout.fillWidth: true
label: pluginApi?.tr("settings.bloomIntensity") ?? "Bloom Intensity"
value: root.valueBloomIntensity
from: 0.0
to: 1.0
stepSize: 0.05
onMoved: value => {
root.valueBloomIntensity = value;
root.saveSettings();
}
}
// Fade when idle toggle
NToggle {
label: pluginApi?.tr("settings.fadeWhenIdle") ?? "Fade When Idle"
description: pluginApi?.tr("settings.fadeWhenIdle-description") ?? "Fade out visualizer when no audio is playing"
checked: root.valueFadeWhenIdle
onToggled: checked => {
root.valueFadeWhenIdle = checked;
root.saveSettings();
}
}
// Use custom colors toggle
NToggle {
label: pluginApi?.tr("settings.useCustomColors") ?? "Use Custom Colors"
description: pluginApi?.tr("settings.useCustomColors-description") ?? "Override theme colors with custom colors"
checked: root.valueUseCustomColors
onToggled: checked => {
root.valueUseCustomColors = checked;
root.saveSettings();
}
}
// Custom primary color picker
RowLayout {
Layout.fillWidth: true
visible: root.valueUseCustomColors
spacing: Style.marginM
NText {
text: pluginApi?.tr("settings.customPrimaryColor") ?? "Primary Color"
Layout.fillWidth: true
}
NColorPicker {
screen: Screen
selectedColor: root.valueCustomPrimaryColor
onColorSelected: color => {
root.valueCustomPrimaryColor = color;
root.saveSettings();
}
}
}
// Custom secondary color picker
RowLayout {
Layout.fillWidth: true
visible: root.valueUseCustomColors
spacing: Style.marginM
NText {
text: pluginApi?.tr("settings.customSecondaryColor") ?? "Secondary Color"
Layout.fillWidth: true
}
NColorPicker {
screen: Screen
selectedColor: root.valueCustomSecondaryColor
onColorSelected: color => {
root.valueCustomSecondaryColor = color;
root.saveSettings();
}
}
}
// Called when user clicks Apply/Save
function saveSettings() {
if (!widgetSettings)
return;
widgetSettings.data.sensitivity = root.valueSensitivity;
widgetSettings.data.rotationSpeed = root.valueRotationSpeed;
widgetSettings.data.barWidth = root.valueBarWidth;
widgetSettings.data.ringOpacity = root.valueRingOpacity;
widgetSettings.data.bloomIntensity = root.valueBloomIntensity;
widgetSettings.data.visualizationMode = root.valueVisualizationMode;
widgetSettings.data.waveThickness = root.valueWaveThickness;
widgetSettings.data.innerDiameter = root.valueInnerDiameter;
widgetSettings.data.fadeWhenIdle = root.valueFadeWhenIdle;
widgetSettings.data.useCustomColors = root.valueUseCustomColors;
widgetSettings.data.customPrimaryColor = root.valueCustomPrimaryColor.toString();
widgetSettings.data.customSecondaryColor = root.valueCustomSecondaryColor.toString();
widgetSettings.save();
}
}
@@ -0,0 +1,38 @@
# Fancy Audiovisualizer
A circular audio visualizer desktop widget for Noctalia Shell with shader-based rendering and multiple visualization modes.
## Features
- **Multiple Visualization Modes**: Bars, Wave, Rings, or combinations (Bars+Rings, Wave+Rings, All)
- **Shader-Based Rendering**: Smooth, GPU-accelerated visualization using custom fragment shaders
- **Theme Integration**: Automatically uses Noctalia theme colors, with optional custom color override
- **Configurable Appearance**: Adjust sensitivity, rotation speed, bar width, ring opacity, bloom intensity, and more
- **Idle Fade**: Optional fade-out when no audio is playing
## Installation
This plugin is part of the `noctalia-plugins` repository.
## Configuration
Access the plugin settings in Noctalia to configure the following options:
- **Visualization Mode**: Choose between Bars, Wave, Rings, Bars+Rings, Wave+Rings, or All
- **Wave Thickness**: Thickness of the wave visualization (when enabled)
- **Sensitivity**: Audio response sensitivity (0.5 - 3.0)
- **Rotation Speed**: Speed of visualization rotation (0.0 - 2.0)
- **Bar Width**: Width of audio bars (when enabled)
- **Ring Opacity**: Opacity of background rings (when enabled)
- **Inner Diameter**: Size of the inner empty area
- **Bloom Intensity**: Glow/bloom effect strength
- **Fade When Idle**: Fade out when no audio is playing
- **Custom Colors**: Override theme colors with custom primary and secondary colors
## Usage
Add the widget to your desktop via the Noctalia desktop widgets interface. The visualizer responds to system audio.
## Requirements
- Noctalia 3.7.2 or later
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Balkenbreite",
"bloomIntensity": "Blütenintensität",
"customPrimaryColor": "Primärfarbe",
"customSecondaryColor": "Sekundärfarbe",
"description": "Audiovisualisierungsdarstellung konfigurieren",
"fadeWhenIdle": "Ausblenden bei Inaktivität",
"fadeWhenIdle-description": "Visualisierer ausblenden, wenn keine Audioausgabe erfolgt",
"innerDiameter": "Innendurchmesser",
"mode": {
"all": "Alle",
"bars": "Bars",
"barsRings": "Barren + Ringe",
"rings": "Ringe",
"wave": "Welle",
"waveRings": "Welle + Ringe"
},
"ringOpacity": "Ring-Opazität",
"rotationSpeed": "Drehzahl",
"sensitivity": "Empfindlichkeit",
"title": "Visualisierungseinstellungen",
"useCustomColors": "Benutzerdefinierte Farben verwenden",
"useCustomColors-description": "Themenfarben mit benutzerdefinierten Farben überschreiben",
"visualizationMode": "Visualisierungsmodus",
"visualizationMode-description": "Visualisierungsstil wählen",
"waveThickness": "Wellendicke"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Bar Width",
"bloomIntensity": "Bloom Intensity",
"customPrimaryColor": "Primary Color",
"customSecondaryColor": "Secondary Color",
"description": "Configure the audio visualizer appearance",
"fadeWhenIdle": "Fade When Idle",
"fadeWhenIdle-description": "Fade out visualizer when no audio is playing",
"innerDiameter": "Inner Diameter",
"mode": {
"all": "All",
"bars": "Bars",
"barsRings": "Bars + Rings",
"rings": "Rings",
"wave": "Wave",
"waveRings": "Wave + Rings"
},
"ringOpacity": "Ring Opacity",
"rotationSpeed": "Rotation Speed",
"sensitivity": "Sensitivity",
"title": "Visualizer Settings",
"useCustomColors": "Use Custom Colors",
"useCustomColors-description": "Override theme colors with custom colors",
"visualizationMode": "Visualization Mode",
"visualizationMode-description": "Choose visualization style",
"waveThickness": "Wave Thickness"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Ancho de barra",
"bloomIntensity": "Intensidad de floración",
"customPrimaryColor": "Color primario",
"customSecondaryColor": "Color secundario",
"description": "Configurar la apariencia del visualizador de audio",
"fadeWhenIdle": "Desvanecer al estar inactivo",
"fadeWhenIdle-description": "Atenuar el visualizador cuando no se reproduce audio.",
"innerDiameter": "Diámetro interior",
"mode": {
"all": "Todo",
"bars": "Barras",
"barsRings": "Barras + Anillas",
"rings": "Anillos",
"wave": "Ola",
"waveRings": "Onda + Anillos"
},
"ringOpacity": "Opacidad del anillo",
"rotationSpeed": "Velocidad de rotación",
"sensitivity": "Sensibilidad",
"title": "Ajustes del Visualizador",
"useCustomColors": "Usar colores personalizados",
"useCustomColors-description": "Anular los colores del tema con colores personalizados",
"visualizationMode": "Modo de visualización",
"visualizationMode-description": "Elegir estilo de visualización",
"waveThickness": "Espesor de la onda"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Largeur de la barre",
"bloomIntensity": "Intensité de l'éclat",
"customPrimaryColor": "Couleur primaire",
"customSecondaryColor": "Couleur secondaire",
"description": "Configurer l'apparence du visualiseur audio",
"fadeWhenIdle": "Estomper en cas d'inactivité",
"fadeWhenIdle-description": "Estomper le visualiseur en l'absence de son.",
"innerDiameter": "Diamètre intérieur",
"mode": {
"all": "Tout",
"bars": "Barres",
"barsRings": "Barres + Anneaux",
"rings": "Anneaux",
"wave": "Vague",
"waveRings": "Onde + Anneaux"
},
"ringOpacity": "Opacité de l'anneau",
"rotationSpeed": "Vitesse de rotation",
"sensitivity": "Sensibilité",
"title": "Paramètres du visualiseur",
"useCustomColors": "Utiliser des couleurs personnalisées",
"useCustomColors-description": "Remplacer les couleurs du thème par des couleurs personnalisées",
"visualizationMode": "Mode de visualisation",
"visualizationMode-description": "Choisir le style de visualisation",
"waveThickness": "Épaisseur de la vague"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Sávszélesség",
"bloomIntensity": "Virágzás intenzitása",
"customPrimaryColor": "Elsődleges szín",
"customSecondaryColor": "Másodlagos szín",
"description": "Az audio vizualizáló megjelenésének beállítása",
"fadeWhenIdle": "Halványítás tétlenség esetén",
"fadeWhenIdle-description": "A vizualizáció elhalványítása, ha nincs hang lejátszva",
"innerDiameter": "Belső átmérő",
"mode": {
"all": "Összes",
"bars": "Sávok",
"barsRings": "Sávok + Gyűrűk",
"rings": "Gyűrűk",
"wave": "Hullám",
"waveRings": "Hullám + Gyűrűk"
},
"ringOpacity": "Gyűrű átlátszósága",
"rotationSpeed": "Forgási sebesség",
"sensitivity": "Érzékenység",
"title": "Vizualizációs beállítások",
"useCustomColors": "Egyéni színek használata",
"useCustomColors-description": "A téma színeinek felülírása egyéni színekkel",
"visualizationMode": "Megjelenítési mód",
"visualizationMode-description": "Válassz megjelenítési stílust",
"waveThickness": "Hullámvastagság"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Breite der Leiste",
"bloomIntensity": "Intensità Bloom",
"customPrimaryColor": "Ngjyra parësore",
"customSecondaryColor": "Ngjyra dytësore",
"description": "Konfigurieren Sie das Aussehen des Audio-Visualisierers.",
"fadeWhenIdle": "Verblassen im Leerlauf",
"fadeWhenIdle-description": "Vizualizátor eltüntetése, ha nincs hang lejátszva.",
"innerDiameter": "Innendurchmesser",
"mode": {
"all": "Kaikki",
"bars": "Pritličja",
"barsRings": "Stangen + Ringe",
"rings": "Hringar",
"wave": "Val",
"waveRings": "Welle + Ringe"
},
"ringOpacity": "Neprůhlednost prstence",
"rotationSpeed": "Schnelllaufdrehzahl",
"sensitivity": "Ndjeshmëri",
"title": "Mga Setting ng Visualizer",
"useCustomColors": "Përdorni Ngjyra të Personalizuara",
"useCustomColors-description": "Ngjyrat e personalizuara mbivendosin ngjyrat e temës",
"visualizationMode": "Módszer megjelenítése",
"visualizationMode-description": "Zgjidhni stilin e vizualizimit",
"waveThickness": "Onda-lodiera"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "バーの幅",
"bloomIntensity": "開花強度",
"customPrimaryColor": "原色",
"customSecondaryColor": "二次色",
"description": "オーディオビジュアライザーの外観を設定します。",
"fadeWhenIdle": "アイドル時にフェード",
"fadeWhenIdle-description": "音声が再生されていないときは、ビジュアライザーをフェードアウトする",
"innerDiameter": "内径",
"mode": {
"all": "すべて",
"bars": "バー",
"barsRings": "鉄棒と輪",
"rings": "リング",
"wave": "波",
"waveRings": "波紋 + リング"
},
"ringOpacity": "リングの不透明度",
"rotationSpeed": "回転速度",
"sensitivity": "感度",
"title": "ビジュアライザー設定",
"useCustomColors": "カスタムカラーを使用する",
"useCustomColors-description": "カスタムカラーでテーマの色をオーバーライドする",
"visualizationMode": "可視化モード",
"visualizationMode-description": "視覚化のスタイルを選択",
"waveThickness": "波厚"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Firehiya Barê",
"bloomIntensity": "Zehfîya Gurtî",
"customPrimaryColor": "Rengê Seretayî",
"customSecondaryColor": "Rengê Duyemîn",
"description": "Xuyakirina dîmenê bihîstwerî yê dengî",
"fadeWhenIdle": "Dema de bêdengiyê vemirîne",
"fadeWhenIdle-description": "Dîmenkerê dema ku tu deng lê nabe, hêdî hêdî winda bike",
"innerDiameter": "Dirêjbûna Hundir",
"mode": {
"all": "Hemû",
"bars": "Bars",
"barsRings": "Bars + Rings",
"rings": "Xelqet",
"wave": "Pêl",
"waveRings": "Pêl + Xelek"
},
"ringOpacity": "Ronahiya Zengil",
"rotationSpeed": "Leza Şewitandinê",
"sensitivity": "Hesasiyet",
"title": "Mîhengên Dîmenderê",
"useCustomColors": "Rengên Xweser Bikar Bîne",
"useCustomColors-description": "Rengên mijarê bi rengên xwerû biguherîne",
"visualizationMode": "Şêwaza Dîmenkirinê",
"visualizationMode-description": "Şêwaza dîtinê hilbijêre",
"waveThickness": "Qalinîya Pêlê"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Balkbreedte",
"bloomIntensity": "Bloei Intensiteit",
"customPrimaryColor": "Primaire kleur",
"customSecondaryColor": "Secundaire kleur",
"description": "Configureer de weergave van de audio visualisatie.",
"fadeWhenIdle": "Vervagen bij inactiviteit",
"fadeWhenIdle-description": "Visualiseerder laten vervagen wanneer er geen audio wordt afgespeeld.",
"innerDiameter": "Binnendiameter",
"mode": {
"all": "Alles",
"bars": "Bars",
"barsRings": "Rekstok + Ringen",
"rings": "Ringen",
"wave": "Golf",
"waveRings": "Golf + Ringen"
},
"ringOpacity": "Ringondoorzichtigheid",
"rotationSpeed": "Rotatiesnelheid",
"sensitivity": "Gevoeligheid",
"title": "Visualisatie-instellingen",
"useCustomColors": "Aangepaste kleuren gebruiken",
"useCustomColors-description": "Themakleuren overschrijven met aangepaste kleuren",
"visualizationMode": "Visualisatiemodus",
"visualizationMode-description": "Kies visualisatiestijl",
"waveThickness": "Golflaagdikte"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Szerokość paska",
"bloomIntensity": "Intensywność Rozkwitu",
"customPrimaryColor": "Kolor podstawowy",
"customSecondaryColor": "Kolor dodatkowy",
"description": "Skonfiguruj wygląd wizualizatora dźwięku",
"fadeWhenIdle": "Przyciemnij przy bezczynności",
"fadeWhenIdle-description": "Wycisz wizualizator, gdy nie jest odtwarzany dźwięk",
"innerDiameter": "Średnica wewnętrzna",
"mode": {
"all": "Wszystkie",
"bars": "Paski",
"barsRings": "Paski + Pierścienie",
"rings": "Pierścienie",
"wave": "Fala",
"waveRings": "Fala + Pierścienie"
},
"ringOpacity": "Krycie pierścienia",
"rotationSpeed": "Prędkość obrotu",
"sensitivity": "Czułość",
"title": "Ustawienia wizualizatora",
"useCustomColors": "Użyj własnych kolorów",
"useCustomColors-description": "Zastąp kolory motywu własnymi kolorami",
"visualizationMode": "Tryb wizualizacji",
"visualizationMode-description": "Wybierz styl wizualizacji",
"waveThickness": "Grubość fali"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Largura da Barra",
"bloomIntensity": "Intensidade do Bloom",
"customPrimaryColor": "Cor primária",
"customSecondaryColor": "Cor secundária",
"description": "Configurar a aparência do visualizador de áudio",
"fadeWhenIdle": "Desaparecer quando inativo",
"fadeWhenIdle-description": "Desvanecer o visualizador quando nenhum áudio estiver sendo reproduzido.",
"innerDiameter": "Diâmetro interno",
"mode": {
"all": "Tudo",
"bars": "Barras",
"barsRings": "Barras + Argolas",
"rings": "Anéis",
"wave": "Onda",
"waveRings": "Onda + Anéis"
},
"ringOpacity": "Opacidade do Anel",
"rotationSpeed": "Velocidade de rotação",
"sensitivity": "Sensibilidade",
"title": "Configurações do Visualizador",
"useCustomColors": "Usar Cores Personalizadas",
"useCustomColors-description": "Substituir as cores do tema por cores personalizadas",
"visualizationMode": "Modo de Visualização",
"visualizationMode-description": "Escolha o estilo de visualização",
"waveThickness": "Espessura da onda"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Ширина полосы",
"bloomIntensity": "Интенсивность свечения",
"customPrimaryColor": "Основной цвет",
"customSecondaryColor": "Вторичный цвет",
"description": "Настроить внешний вид визуализатора звука",
"fadeWhenIdle": "Затухать в режиме ожидания",
"fadeWhenIdle-description": "Затухать визуализатору при отсутствии воспроизведения аудио.",
"innerDiameter": "Внутренний диаметр",
"mode": {
"all": "Всё",
"bars": "Бары",
"barsRings": "Турники + Кольца",
"rings": "Кольца",
"wave": "Волна",
"waveRings": "Волна + Кольца"
},
"ringOpacity": "Прозрачность кольца",
"rotationSpeed": "Скорость вращения",
"sensitivity": "Чувствительность",
"title": "Настройки визуализатора",
"useCustomColors": "Использовать пользовательские цвета",
"useCustomColors-description": "Переопределить цвета темы пользовательскими цветами",
"visualizationMode": "Режим визуализации",
"visualizationMode-description": "Выберите стиль визуализации",
"waveThickness": "Толщина волны"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Çubuk Genişliği",
"bloomIntensity": "Çiçeklenme Yoğunluğu",
"customPrimaryColor": "Ana Renk",
"customSecondaryColor": "İkincil Renk",
"description": "Ses görselleştirici görünümünü yapılandır",
"fadeWhenIdle": "Boşta Kalınca Karart",
"fadeWhenIdle-description": "Ses çalmadığında görselleştiriciyi soldur.",
"innerDiameter": "İç Çap",
"mode": {
"all": "Tüm",
"bars": "Barlar",
"barsRings": "Barfiks Demiri + Halkalar",
"rings": "Yüzükler",
"wave": "Dalga",
"waveRings": "Dalga + Halkalar"
},
"ringOpacity": "Halka Opaklığı",
"rotationSpeed": "Dönme Hızı",
"sensitivity": "Hassasiyet",
"title": "Görselleştirici Ayarları",
"useCustomColors": "Özel Renkleri Kullan",
"useCustomColors-description": "Tema renklerini özel renklerle geçersiz kıl",
"visualizationMode": "Görselleştirme Modu",
"visualizationMode-description": "Görselleştirme stilini seçin",
"waveThickness": "Dalga Kalınlığı"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Ширина бруска",
"bloomIntensity": "Інтенсивність світіння",
"customPrimaryColor": "Основний колір",
"customSecondaryColor": "Вторинний колір",
"description": "Налаштуйте вигляд аудіовізуалізатора",
"fadeWhenIdle": "Згасати в режимі очікування",
"fadeWhenIdle-description": "Затемнювати візуалізатор, коли не відтворюється звук.",
"innerDiameter": "Внутрішній діаметр",
"mode": {
"all": "Все",
"bars": "Бари",
"barsRings": "Бруси + Кільця",
"rings": "Кільця",
"wave": "Хвиля",
"waveRings": "Хвиля + Кільця"
},
"ringOpacity": "Прозорість кільця",
"rotationSpeed": "Швидкість обертання",
"sensitivity": "Чутливість",
"title": "Налаштування візуалізатора",
"useCustomColors": "Використовувати власні кольори",
"useCustomColors-description": "Перевизначити кольори теми власними кольорами",
"visualizationMode": "Режим візуалізації",
"visualizationMode-description": "Виберіть стиль візуалізації",
"waveThickness": "Товщина хвилі"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Độ rộng thanh",
"bloomIntensity": "Cường độ hiệu ứng bloom",
"customPrimaryColor": "Màu chính tùy chỉnh",
"customSecondaryColor": "Màu phụ tùy chỉnh",
"description": "Cấu hình giao diện trình hiển thị âm thanh",
"fadeWhenIdle": "Làm mờ khi không hoạt động",
"fadeWhenIdle-description": "Làm mờ trình hiển thị khi không có âm thanh",
"innerDiameter": "Đường kính bên trong",
"mode": {
"all": "Tất cả",
"bars": "Thanh",
"barsRings": "Thanh + Vòng",
"rings": "Vòng",
"wave": "Sóng",
"waveRings": "Sóng + Vòng"
},
"ringOpacity": "Độ mờ vòng",
"rotationSpeed": "Tốc độ xoay",
"sensitivity": "Độ nhạy",
"title": "Cài đặt trình hiển thị",
"useCustomColors": "Dùng màu tùy chỉnh",
"useCustomColors-description": "Ghi đè màu giao diện bằng màu tùy chỉnh",
"visualizationMode": "Chế độ hiển thị",
"visualizationMode-description": "Chọn kiểu hiển thị",
"waveThickness": "Độ dày sóng"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "条宽",
"bloomIntensity": "光晕强度",
"customPrimaryColor": "原色",
"customSecondaryColor": "间色",
"description": "配置音频可视化效果外观",
"fadeWhenIdle": "空闲时淡出",
"fadeWhenIdle-description": "当没有音频播放时淡出可视化工具",
"innerDiameter": "内径",
"mode": {
"all": "全部",
"bars": "酒吧",
"barsRings": "单杠 + 吊环",
"rings": "戒指",
"wave": "波浪",
"waveRings": "波浪 + 环"
},
"ringOpacity": "环透明度",
"rotationSpeed": "转速",
"sensitivity": "敏感性",
"title": "可视化设置",
"useCustomColors": "使用自定义颜色",
"useCustomColors-description": "使用自定义颜色覆盖主题颜色",
"visualizationMode": "可视化模式",
"visualizationMode-description": "选择可视化风格",
"waveThickness": "波浪厚度"
}
}
@@ -0,0 +1,38 @@
{
"id": "fancy-audiovisualizer",
"name": "Fancy Audiovisualizer",
"version": "1.1.2",
"minNoctaliaVersion": "4.6.6",
"author": "Noctalia Team",
"official": true,
"license": "MIT",
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
"description": "Lemmy's fancy circular audio visualizer.",
"tags": [
"Desktop",
"Audio"
],
"entryPoints": {
"desktopWidget": "DesktopWidget.qml",
"desktopWidgetSettings": "DesktopWidgetSettings.qml"
},
"dependencies": {
"plugins": []
},
"metadata": {
"defaultSettings": {
"sensitivity": 1.5,
"rotationSpeed": 0.5,
"barWidth": 0.6,
"ringOpacity": 0.8,
"bloomIntensity": 0.5,
"visualizationMode": 3,
"waveThickness": 1.0,
"innerDiameter": 0.7,
"fadeWhenIdle": false,
"useCustomColors": false,
"customPrimaryColor": "#6750A4",
"customSecondaryColor": "#625B71"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

@@ -0,0 +1,559 @@
#version 450
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float time;
float itemWidth;
float itemHeight;
vec4 primaryColor;
vec4 secondaryColor;
float sensitivity;
float rotationSpeed;
float barWidth;
float ringOpacity;
float cornerRadius;
float bloomIntensity;
float visualizationMode; // 0=bars, 1=wave, 2=rings, 3=bars+rings, 4=wave+rings, 5=all
float waveThickness;
float innerDiameter;
} ubuf;
// Mode helper functions
bool hasRings() { return ubuf.visualizationMode >= 2.0; }
bool hasBars() { return ubuf.visualizationMode == 0.0 || ubuf.visualizationMode == 3.0 || ubuf.visualizationMode >= 5.0; }
bool hasWave() { return ubuf.visualizationMode == 1.0 || ubuf.visualizationMode == 4.0 || ubuf.visualizationMode >= 5.0; }
layout(binding = 1) uniform sampler2D source;
#define TWOPI 6.28318530718
#define PI 3.14159265359
#define NBARS 32
// Sample audio amplitude at normalized position (0.0-1.0)
float getAudio(float pos) {
return texture(source, vec2(clamp(pos, 0.0, 1.0), 0.5)).r;
}
// Smoothed audio sampling with interpolation
float smoothAudio(float pos) {
float idx = pos * float(NBARS - 1);
float frac = fract(idx);
float i0 = floor(idx) / float(NBARS - 1);
float i1 = ceil(idx) / float(NBARS - 1);
return mix(getAudio(i0), getAudio(i1), frac) * ubuf.sensitivity;
}
// Frequency band helpers
float getBass() { return smoothAudio(0.05); }
float getMid() { return smoothAudio(0.3); }
float getHighMid() { return smoothAudio(0.6); }
float getTreble() { return smoothAudio(0.9); }
// SDF for rounded rectangle
float roundedBoxSDF(vec2 center, vec2 size, float radius) {
vec2 q = abs(center) - size + radius;
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius;
}
// Compute polar wave visualization
vec4 computePolarWave(vec2 uv, float iTime, float bass, float mid, float highMid, float treble) {
float aspect = ubuf.itemWidth / ubuf.itemHeight;
vec2 centered = (uv - 0.5) * 2.0;
centered.x *= aspect;
float theta = atan(centered.y, centered.x);
float d = length(centered);
float innerRadius = ubuf.innerDiameter / 2.0;
float baseRadius = 0.35; // Fixed reference for outer extent
vec4 color = vec4(0.0);
// RING SYSTEM
if (hasRings()) {
// Center Waves
if (d < innerRadius * 0.6) {
float wave = mid * 0.8;
float ripple = sin(d * 25.0 + wave * 15.0 - iTime * 2.0);
if (ripple > 0.7) {
float intensity = clamp(mid * 0.6, 0.0, 0.4);
vec4 waveColor = ubuf.secondaryColor * intensity * ubuf.ringOpacity;
color = max(color, waveColor);
}
}
// Energy Ring
float energyRad = innerRadius * 0.65;
float energyThickness = 0.015 + clamp(highMid * 0.02, 0.0, 0.03);
if (d > energyRad - energyThickness && d < energyRad + energyThickness) {
float segmentAngle = theta * 8.0 + highMid * 3.0 + iTime;
if (mod(segmentAngle, 1.0) < 0.6) {
float alpha = clamp(highMid * 2.0, 0.3, 1.0) * ubuf.ringOpacity;
vec4 energyColor = mix(ubuf.primaryColor, ubuf.secondaryColor, 0.5) * alpha;
color = max(color, energyColor);
}
}
// Particle Ring
float particleRad = innerRadius * 0.75;
if (d > particleRad - 0.02 && d < particleRad + 0.02) {
float particleAngle = theta + treble * 2.0 + iTime * 0.5;
float particleSpacing = TWOPI / 16.0;
if (mod(particleAngle, particleSpacing) < 0.15) {
float brightness = 0.5 + clamp(treble * 1.5, 0.0, 0.5);
vec4 particleColor = ubuf.secondaryColor * brightness * ubuf.ringOpacity;
color = max(color, particleColor);
}
}
// Tech Grid Ring
float gridRad = innerRadius * 0.85;
if (d > gridRad - 0.012 && d < gridRad + 0.012) {
float gridAngle = theta + iTime * ubuf.rotationSpeed;
float gridDensity = 0.08 + clamp(mid * 0.05, 0.0, 0.1);
if (mod(gridAngle, 0.2) < gridDensity) {
vec4 gridColor = ubuf.primaryColor * 0.7 * ubuf.ringOpacity;
gridColor.rgb += vec3(0.1, 0.15, 0.2) * clamp(mid, 0.0, 0.8);
color = max(color, gridColor);
}
}
// Accent Ring
float accentRad = innerRadius * 0.92;
float pulse = clamp(bass * 0.08, 0.0, 0.05);
if (d > accentRad - pulse - 0.008 && d < accentRad + pulse + 0.015) {
float colorShift = clamp(bass * 0.5, 0.0, 1.0);
vec4 ringColor = mix(ubuf.secondaryColor * 0.7, ubuf.primaryColor, colorShift);
ringColor.a = ubuf.ringOpacity;
ringColor.rgb *= 1.0 + bass * 0.3;
color = max(color, ringColor);
}
// Outer Ring
float outerRad = innerRadius + bass * 0.05;
if (d > outerRad - 0.008 && d < outerRad + 0.008) {
vec4 outerColor = ubuf.primaryColor * ubuf.ringOpacity;
outerColor.rgb += vec3(0.2, 0.3, 0.4) * clamp(treble * 0.5, 0.0, 0.3);
outerColor.rgb *= 1.0 + bass * 0.4;
color = max(color, outerColor);
}
}
// POLAR WAVE - filled shape with mirrored bands (64 visual bands from 32 samples)
if (hasWave()) {
float adjustedTheta = theta + PI + iTime * ubuf.rotationSpeed * 0.2;
// Map angle to 0-1 range around the full circle
float normalizedAngle = mod(adjustedTheta, TWOPI) / TWOPI;
// Mirror: first half (0-0.5) maps to bands 0->31, second half (0.5-1) maps back 31->0
float mirroredPos = normalizedAngle < 0.5 ? normalizedAngle * 2.0 : (1.0 - normalizedAngle) * 2.0;
// Catmull-Rom spline interpolation for smooth curve through mirrored bands
float bandPos = mirroredPos * float(NBARS - 1);
float fband1 = floor(bandPos);
float fband0 = max(fband1 - 1.0, 0.0);
float fband2 = min(fband1 + 1.0, float(NBARS - 1));
float fband3 = min(fband1 + 2.0, float(NBARS - 1));
float t = fract(bandPos);
// Sample the 4 control points
float p0 = getAudio(fband0 / float(NBARS - 1)) * ubuf.sensitivity;
float p1 = getAudio(fband1 / float(NBARS - 1)) * ubuf.sensitivity;
float p2 = getAudio(fband2 / float(NBARS - 1)) * ubuf.sensitivity;
float p3 = getAudio(fband3 / float(NBARS - 1)) * ubuf.sensitivity;
// Catmull-Rom spline interpolation
float t2 = t * t;
float t3 = t2 * t;
float smoothedAudio = 0.5 * (
(2.0 * p1) +
(-p0 + p2) * t +
(2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
);
smoothedAudio = max(smoothedAudio, 0.0);
// Calculate wave radius at this angle
float waveDisplacement = smoothedAudio * 0.5;
float waveRadius = baseRadius + waveDisplacement; // Fixed outer extent
// Fill the entire area from inner to wave edge
if (d >= innerRadius && d <= waveRadius) {
float fillFactor = (d - innerRadius) / max(waveRadius - innerRadius, 0.001);
// Gradient from primary at base to accent at edge
vec3 fillColor = mix(ubuf.primaryColor.rgb * 0.8, ubuf.secondaryColor.rgb, fillFactor);
// Boost brightness with bass
fillColor *= 1.0 + bass * 0.3;
// Alpha: stronger near the edge, fades toward center
float fillAlpha = mix(0.4, 1.0, fillFactor) * ubuf.waveThickness;
fillAlpha = clamp(fillAlpha, 0.0, 1.0);
vec4 fill = vec4(fillColor, fillAlpha);
color = max(color, fill);
}
// Bright edge line at the wave boundary
float edgeThickness = ubuf.waveThickness * 0.025;
float distToEdge = abs(d - waveRadius);
if (distToEdge < edgeThickness) {
float edgeFactor = 1.0 - smoothstep(0.0, edgeThickness, distToEdge);
vec3 edgeColor = ubuf.secondaryColor.rgb * (1.2 + smoothedAudio * 0.5);
// Add highlight at peaks
if (smoothedAudio > 0.5) {
edgeColor += vec3(0.3, 0.4, 0.5) * (smoothedAudio - 0.5);
}
vec4 edge = vec4(edgeColor, edgeFactor);
color = max(color, edge);
}
}
return color;
}
// Compute visualization color at given UV coordinates
// Returns vec4 with RGB color and alpha
vec4 computeVisualization(vec2 uv, float iTime, float bass, float mid, float highMid, float treble) {
float aspect = ubuf.itemWidth / ubuf.itemHeight;
vec2 centered = (uv - 0.5) * 2.0;
centered.x *= aspect;
float theta = atan(centered.y, centered.x);
float d = length(centered);
float innerRadius = ubuf.innerDiameter / 2.0;
float baseRadius = 0.35; // Fixed reference for outer extent
vec4 color = vec4(0.0);
// RING SYSTEM
if (hasRings()) {
// Center Waves
if (d < innerRadius * 0.6) {
float wave = mid * 0.8;
float ripple = sin(d * 25.0 + wave * 15.0 - iTime * 2.0);
if (ripple > 0.7) {
float intensity = clamp(mid * 0.6, 0.0, 0.4);
vec4 waveColor = ubuf.secondaryColor * intensity * ubuf.ringOpacity;
color = max(color, waveColor);
}
}
// Energy Ring
float energyRad = innerRadius * 0.65;
float energyThickness = 0.015 + clamp(highMid * 0.02, 0.0, 0.03);
if (d > energyRad - energyThickness && d < energyRad + energyThickness) {
float segmentAngle = theta * 8.0 + highMid * 3.0 + iTime;
if (mod(segmentAngle, 1.0) < 0.6) {
float alpha = clamp(highMid * 2.0, 0.3, 1.0) * ubuf.ringOpacity;
vec4 energyColor = mix(ubuf.primaryColor, ubuf.secondaryColor, 0.5) * alpha;
color = max(color, energyColor);
}
}
// Particle Ring
float particleRad = innerRadius * 0.75;
if (d > particleRad - 0.02 && d < particleRad + 0.02) {
float particleAngle = theta + treble * 2.0 + iTime * 0.5;
float particleSpacing = TWOPI / 16.0;
if (mod(particleAngle, particleSpacing) < 0.15) {
float brightness = 0.5 + clamp(treble * 1.5, 0.0, 0.5);
vec4 particleColor = ubuf.secondaryColor * brightness * ubuf.ringOpacity;
color = max(color, particleColor);
}
}
// Tech Grid Ring
float gridRad = innerRadius * 0.85;
if (d > gridRad - 0.012 && d < gridRad + 0.012) {
float gridAngle = theta + iTime * ubuf.rotationSpeed;
float gridDensity = 0.08 + clamp(mid * 0.05, 0.0, 0.1);
if (mod(gridAngle, 0.2) < gridDensity) {
vec4 gridColor = ubuf.primaryColor * 0.7 * ubuf.ringOpacity;
gridColor.rgb += vec3(0.1, 0.15, 0.2) * clamp(mid, 0.0, 0.8);
color = max(color, gridColor);
}
}
// Accent Ring
float accentRad = innerRadius * 0.92;
float pulse = clamp(bass * 0.08, 0.0, 0.05);
if (d > accentRad - pulse - 0.008 && d < accentRad + pulse + 0.015) {
float colorShift = clamp(bass * 0.5, 0.0, 1.0);
vec4 ringColor = mix(ubuf.secondaryColor * 0.7, ubuf.primaryColor, colorShift);
ringColor.a = ubuf.ringOpacity;
ringColor.rgb *= 1.0 + bass * 0.3;
color = max(color, ringColor);
}
// Outer Ring
float outerRad = innerRadius + bass * 0.05;
if (d > outerRad - 0.008 && d < outerRad + 0.008) {
vec4 outerColor = ubuf.primaryColor * ubuf.ringOpacity;
outerColor.rgb += vec3(0.2, 0.3, 0.4) * clamp(treble * 0.5, 0.0, 0.3);
outerColor.rgb *= 1.0 + bass * 0.4;
color = max(color, outerColor);
}
}
// CIRCULAR AUDIO BARS (64 bars, mirrored from 32 audio samples)
if (hasBars() && d > innerRadius) {
// Double the visual bars by using NBARS * 2
float section = TWOPI / float(NBARS * 2);
float center = section / 2.0;
float adjustedTheta = theta + PI + iTime * ubuf.rotationSpeed * 0.2;
float m = mod(adjustedTheta, section);
float ym = d * sin(center - m);
float barW = ubuf.barWidth * 0.015;
if (abs(ym) < barW) {
// Calculate position in the circle (0.0 to 1.0)
float circlePos = mod(adjustedTheta, TWOPI) / TWOPI;
// Mirror: first half (0-0.5) maps to 0-1, second half (0.5-1) maps back 1-0
float mirroredPos = circlePos < 0.5 ? circlePos * 2.0 : (1.0 - circlePos) * 2.0;
float v = smoothAudio(mirroredPos);
float wave = sin(theta * 3.0 + mid * 5.0) * clamp(mid * 0.03, 0.0, 0.05);
v += wave;
v = max(v, 0.0);
float barStart = innerRadius;
float barEnd = baseRadius + v * 0.5; // Fixed outer extent
if (d >= barStart && d <= barEnd) {
float heightFactor = (d - barStart) / max(barEnd - barStart, 0.001);
vec3 bottomColor = ubuf.primaryColor.rgb * 0.6;
vec3 middleColor = ubuf.primaryColor.rgb;
vec3 topColor = ubuf.secondaryColor.rgb;
vec3 barColor;
if (heightFactor < 0.5) {
barColor = mix(bottomColor, middleColor, heightFactor * 2.0);
} else {
barColor = mix(middleColor, topColor, (heightFactor - 0.5) * 2.0);
}
barColor *= 1.0 + bass * 0.4;
if (heightFactor > 0.85) {
barColor += vec3(0.3, 0.4, 0.5) * clamp(treble * 0.8, 0.0, 0.5);
}
float edgeFactor = 1.0 - smoothstep(barW * 0.7, barW, abs(ym));
vec4 finalBarColor = vec4(barColor, edgeFactor);
color = max(color, finalBarColor);
}
}
}
return color;
}
void main() {
// ============================================
// DYNAMIC CONTENT SCALING
// ============================================
// Calculate max possible extent from current settings to guarantee
// nothing ever reaches the widget border.
//
// Max visualization radius in centered space [-1,1]:
// Bars/Wave: baseRadius(0.35) + sensitivity * 0.5
// Rings: innerDiameter/2 + sensitivity * 0.05 (always smaller)
//
// Bloom reach: exp(-dist * minDecayRate / bloomIntensity) decays to
// < 1/255 at dist ≈ bloomIntensity * 1.0 (from solving exp decay
// with worst-case amplitude * multiplier chain)
//
// contentScale maps the widget edge to ±contentScale in centered space,
// so setting it to maxTotalRadius ensures everything fits.
float maxContentRadius = 0.35 + ubuf.sensitivity * 0.5;
float maxBloomReach = ubuf.bloomIntensity * 1.0;
float maxTotalRadius = maxContentRadius + maxBloomReach;
float contentScale = max(maxTotalRadius * 1.05, 1.0); // 5% safety margin
vec2 uv = (qt_TexCoord0 - 0.5) * contentScale + 0.5;
// Convert linear time (0-3600) to smooth oscillation for seamless looping
// sin() ensures perfect continuity when QML wraps from 3600 back to 0
float iTime = sin(ubuf.time * TWOPI / 3600.0) * 1800.0 + 1800.0;
// Frequency analysis
float bass = getBass();
float mid = getMid();
float highMid = getHighMid();
float treble = getTreble();
// Get base visualization color based on mode
// Mode 0: bars only, Mode 1: wave only, Mode 2: rings only
// Mode 3: bars+rings, Mode 4: wave+rings, Mode 5: all
vec4 color;
if (hasWave() && !hasBars()) {
// Wave only or wave+rings (modes 1, 4)
color = computePolarWave(uv, iTime, bass, mid, highMid, treble);
} else if (hasBars() && !hasWave()) {
// Bars only or bars+rings (modes 0, 3)
color = computeVisualization(uv, iTime, bass, mid, highMid, treble);
} else if (hasWave() && hasBars()) {
// All mode (5) - combine both
vec4 barsColor = computeVisualization(uv, iTime, bass, mid, highMid, treble);
vec4 waveColor = computePolarWave(uv, iTime, bass, mid, highMid, treble);
color = max(barsColor, waveColor);
} else {
// Rings only (mode 2) - still need to call one of them for ring rendering
color = computeVisualization(uv, iTime, bass, mid, highMid, treble);
}
// ============================================
// BLOOM EFFECT - Glow based on distance to geometry
// ============================================
if (ubuf.bloomIntensity > 0.01 && color.a < 0.01) {
// Only apply bloom where there's no geometry (empty space)
// Find distance to nearest bright element
float aspect = ubuf.itemWidth / ubuf.itemHeight;
vec2 centered = (uv - 0.5) * 2.0;
centered.x *= aspect;
float d = length(centered);
float theta = atan(centered.y, centered.x);
float innerRadius = ubuf.innerDiameter / 2.0;
float baseRadius = 0.35; // Fixed reference for outer extent
float glowAmount = 0.0;
vec3 glowColor = vec3(0.0);
// Glow from rings (if enabled)
if (hasRings()) {
// Outer ring glow
float outerRad = innerRadius + bass * 0.05;
float ringDist = abs(d - outerRad);
float ringGlow = exp(-ringDist * 8.0 / ubuf.bloomIntensity) * (1.0 + bass * 0.5);
glowColor += ubuf.primaryColor.rgb * ringGlow;
glowAmount = max(glowAmount, ringGlow);
// Accent ring glow
float accentRad = innerRadius * 0.92;
float accentDist = abs(d - accentRad);
float accentGlow = exp(-accentDist * 10.0 / ubuf.bloomIntensity) * (0.7 + bass * 0.3);
glowColor += mix(ubuf.secondaryColor.rgb, ubuf.primaryColor.rgb, 0.5) * accentGlow;
glowAmount = max(glowAmount, accentGlow);
}
// Glow from visualization (bars or polar wave)
if ((hasBars() || hasWave()) && d > innerRadius * 0.8) {
float adjustedTheta = theta + PI + iTime * ubuf.rotationSpeed * 0.2;
float circlePos = mod(adjustedTheta, TWOPI) / TWOPI;
float mirroredPos = circlePos < 0.5 ? circlePos * 2.0 : (1.0 - circlePos) * 2.0;
float v = smoothAudio(mirroredPos);
if (hasWave()) {
// Polar wave bloom - Catmull-Rom spline with mirroring matching main render
float mirroredPos = circlePos < 0.5 ? circlePos * 2.0 : (1.0 - circlePos) * 2.0;
float bandPos = mirroredPos * float(NBARS - 1);
float fband1 = floor(bandPos);
float fband0 = max(fband1 - 1.0, 0.0);
float fband2 = min(fband1 + 1.0, float(NBARS - 1));
float fband3 = min(fband1 + 2.0, float(NBARS - 1));
float t = fract(bandPos);
float p0 = getAudio(fband0 / float(NBARS - 1)) * ubuf.sensitivity;
float p1 = getAudio(fband1 / float(NBARS - 1)) * ubuf.sensitivity;
float p2 = getAudio(fband2 / float(NBARS - 1)) * ubuf.sensitivity;
float p3 = getAudio(fband3 / float(NBARS - 1)) * ubuf.sensitivity;
float t2 = t * t;
float t3 = t2 * t;
float smoothedAudio = 0.5 * (
(2.0 * p1) +
(-p0 + p2) * t +
(2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
);
smoothedAudio = max(smoothedAudio, 0.0);
float waveRadius = baseRadius + smoothedAudio * 0.5;
// Glow from the filled area and edge
float distToWave = abs(d - waveRadius);
float waveGlow = exp(-distToWave * 8.0 / ubuf.bloomIntensity) * smoothedAudio * 2.5;
vec3 waveGlowColor = mix(ubuf.primaryColor.rgb, ubuf.secondaryColor.rgb, smoothedAudio);
glowColor += waveGlowColor * waveGlow;
glowAmount = max(glowAmount, waveGlow);
}
if (hasBars()) {
// Bars bloom
float section = TWOPI / float(NBARS * 2);
float m = mod(adjustedTheta, section);
float center = section / 2.0;
float barAngleDist = min(abs(m - center), section - abs(m - center));
float barEnd = baseRadius + v * 0.5; // Fixed outer extent
float radialDist = 0.0;
if (d < innerRadius) {
radialDist = innerRadius - d;
} else if (d > barEnd) {
radialDist = d - barEnd;
}
float totalDist = length(vec2(barAngleDist * d, radialDist));
float barGlow = exp(-totalDist * 15.0 / ubuf.bloomIntensity) * v * 2.0;
float heightFactor = clamp((d - innerRadius) / max(barEnd - innerRadius, 0.001), 0.0, 1.0);
vec3 barGlowColor = mix(ubuf.primaryColor.rgb, ubuf.secondaryColor.rgb, heightFactor);
glowColor += barGlowColor * barGlow;
glowAmount = max(glowAmount, barGlow);
}
}
// Apply bloom
float bloomMult = ubuf.bloomIntensity * (1.0 + bass * 0.5);
color.rgb = glowColor * bloomMult;
color.a = glowAmount * bloomMult * 0.6;
// Clamp to reasonable values
color.rgb = min(color.rgb, vec3(1.5));
color.a = min(color.a, 0.8);
}
// ============================================
// EDGE FADE - radial falloff that only affects the bloom zone
// ============================================
// Fade starts just past where main content ends (maxContentRadius)
// and reaches zero at the widget edge (contentScale) in centered space.
// This catches bloom tails without affecting the main visualization.
vec2 fromCenter = (qt_TexCoord0 - 0.5) * 2.0; // -1 to 1 in widget space
float edgeProximity = max(abs(fromCenter.x), abs(fromCenter.y)); // box distance
float fadeStart = maxContentRadius / contentScale; // where content ends in widget space
float edgeFade = 1.0 - smoothstep(fadeStart, 1.0, edgeProximity);
color *= edgeFade;
// ============================================
// CORNER MASKING
// ============================================
vec2 pixelPos = qt_TexCoord0 * vec2(ubuf.itemWidth, ubuf.itemHeight);
vec2 centerPos = pixelPos - vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5;
vec2 halfSize = vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5;
float dist = roundedBoxSDF(centerPos, halfSize, ubuf.cornerRadius);
float cornerMask = 1.0 - smoothstep(-1.0, 0.0, dist);
// Final output with premultiplied alpha
float finalAlpha = color.a * ubuf.qt_Opacity * cornerMask;
fragColor = vec4(color.rgb * finalAlpha, finalAlpha);
}
@@ -0,0 +1,448 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
Item {
id: root
property var pluginApi: null
// Provider metadata
property string name: pluginApi?.tr("provider.name")
property var launcher: null
property bool handleSearch: false
property string supportedLayouts: "list"
property bool supportsAutoPaste: false
// Search state
property var currentResults: []
property string currentQuery: ""
property bool searching: false
property int nextRequestId: 0
property int activeRequestId: 0
property int fileProcessRequestId: 0
property int dirProcessRequestId: 0
property int pendingProcessCount: 0
property bool currentRequestFailed: false
property var pendingResultsByType: ({ "files": [], "dirs": [] })
property string fdCommandPath: ""
property bool fdAvailable: false
// Settings shortcuts
property var cfg: pluginApi?.pluginSettings || ({})
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
property bool showHidden: cfg.showHidden ?? defaults.showHidden ?? false
property int maxResults: cfg.maxResults ?? defaults.maxResults ?? 0
property string fileOpener: cfg.fileOpener ?? defaults.fileOpener ?? "xdg-open"
property string fdCommand: cfg.fdCommand ?? defaults.fdCommand ?? "fd"
property string searchDirectory: cfg.searchDirectory ?? defaults.searchDirectory ?? "~"
Process {
id: fileSearchProcess
running: false
stdout: StdioCollector {
id: fileStdoutCollector
}
stderr: StdioCollector {
id: fileStderrCollector
}
onExited: function(exitCode) {
root.handleSearchProcessExit("files", fileProcessRequestId, exitCode, fileStdoutCollector.text, fileStderrCollector.text);
}
}
Process {
id: dirSearchProcess
running: false
stdout: StdioCollector {
id: dirStdoutCollector
}
stderr: StdioCollector {
id: dirStderrCollector
}
onExited: function(exitCode) {
root.handleSearchProcessExit("dirs", dirProcessRequestId, exitCode, dirStdoutCollector.text, dirStderrCollector.text);
}
}
// Debounce timer for search
Timer {
id: searchDebouncer
interval: 300
repeat: false
onTriggered: root.executeSearch(root.currentQuery)
}
function init() {
Logger.i("FileSearch", "Initializing plugin");
fdCommandPath = fdCommand;
fdAvailable = true;
Logger.i("FileSearch", "Using fd command:", fdCommandPath);
}
function handleCommand(searchText) {
return searchText.startsWith(">file");
}
function commands() {
return [{
"name": ">file",
"description": pluginApi?.tr("launcher.command.description"),
"icon": "file-search",
"isTablerIcon": true,
"isImage": false,
"onActivate": function() {
launcher.setSearchText(">file ");
}
}];
}
function getResults(searchText) {
if (!searchText.startsWith(">file")) {
return [];
}
if (!fdAvailable) {
return [{
"name": pluginApi?.tr("launcher.errors.fdNotFound.title"),
"description": pluginApi?.tr("launcher.errors.fdNotFound.description"),
"icon": "alert-circle",
"isTablerIcon": true,
"isImage": false,
"onActivate": function() {}
}];
}
var query = searchText.slice(5).trim();
if (query === "") {
return [{
"name": pluginApi?.tr("launcher.prompts.emptyQuery.title"),
"description": pluginApi?.tr("launcher.prompts.emptyQuery.description", { "root": displaySearchDirectory() }),
"icon": "file-search",
"isTablerIcon": true,
"isImage": false,
"onActivate": function() {}
}];
}
if (query !== currentQuery) {
currentQuery = query;
activeRequestId = 0;
currentRequestFailed = false;
searching = true;
searchDebouncer.restart();
return [{
"name": pluginApi?.tr("launcher.prompts.searching.title"),
"description": pluginApi?.tr("launcher.prompts.searching.description", { "query": query }),
"icon": "refresh",
"isTablerIcon": true,
"isImage": false,
"onActivate": function() {}
}];
}
if (searching) {
return [{
"name": pluginApi?.tr("launcher.prompts.searching.title"),
"description": pluginApi?.tr("launcher.prompts.searching.description", { "query": query }),
"icon": "refresh",
"isTablerIcon": true,
"isImage": false,
"onActivate": function() {}
}];
}
return currentResults;
}
function executeSearch(query) {
if (!fdAvailable || query === "") {
return;
}
Logger.d("FileSearch", "Executing search for:", query);
if (fileSearchProcess.running) {
fileSearchProcess.running = false;
}
if (dirSearchProcess.running) {
dirSearchProcess.running = false;
}
var expandedDir = expandHomePath(searchDirectory);
nextRequestId += 1;
var requestId = nextRequestId;
activeRequestId = requestId;
currentRequestFailed = false;
pendingProcessCount = 2;
pendingResultsByType = ({ "files": [], "dirs": [] });
var commonArgs = [];
if (showHidden) {
commonArgs.push("--hidden");
}
if (maxResults > 0) {
commonArgs.push("--max-results", maxResults.toString());
}
commonArgs.push("--base-directory", expandedDir);
commonArgs.push("--absolute-path");
commonArgs.push("--color", "never");
commonArgs.push(query);
var fileArgs = ["--type", "f"].concat(commonArgs);
var dirArgs = ["--type", "d"].concat(commonArgs);
fileProcessRequestId = requestId;
fileSearchProcess.command = [fdCommandPath].concat(fileArgs);
fileSearchProcess.running = true;
dirProcessRequestId = requestId;
dirSearchProcess.command = [fdCommandPath].concat(dirArgs);
dirSearchProcess.running = true;
Logger.d("FileSearch", "Running file command:", fdCommandPath, fileArgs.join(" "));
Logger.d("FileSearch", "Running dir command:", fdCommandPath, dirArgs.join(" "));
}
function handleSearchProcessExit(kind, requestId, exitCode, stdoutText, stderrText) {
if (requestId !== activeRequestId || currentRequestFailed) {
return;
}
if (exitCode !== 0) {
currentRequestFailed = true;
searching = false;
pendingProcessCount = 0;
Logger.e("FileSearch", "fd command failed with exit code:", exitCode);
Logger.e("FileSearch", "stderr:", stderrText);
currentResults = [{
"name": pluginApi?.tr("launcher.errors.fdNotFound.title"),
"description": pluginApi?.tr("launcher.errors.fdNotFound.description"),
"icon": "alert-circle",
"isTablerIcon": true,
"onActivate": function() {}
}];
if (launcher) {
launcher.updateResults();
}
return;
}
pendingResultsByType[kind] = parseRawPaths(stdoutText);
pendingProcessCount -= 1;
if (pendingProcessCount <= 0) {
finalizeSearchResults(requestId);
}
}
function finalizeSearchResults(requestId) {
if (requestId !== activeRequestId || currentRequestFailed) {
return;
}
var results = [];
for (var i = 0; i < pendingResultsByType.dirs.length; i++) {
results.push(formatFileEntry(pendingResultsByType.dirs[i], true));
}
for (var j = 0; j < pendingResultsByType.files.length; j++) {
results.push(formatFileEntry(pendingResultsByType.files[j], false));
}
results = sortResults(results, currentQuery);
if (maxResults > 0 && results.length > maxResults) {
results = results.slice(0, maxResults);
}
if (results.length === 0) {
results.push({
"name": pluginApi?.tr("launcher.prompts.noResults.title"),
"description": pluginApi?.tr("launcher.prompts.noResults.description", { "query": currentQuery }),
"icon": "file-off",
"isTablerIcon": true,
"isImage": false,
"onActivate": function() {}
});
searching = false;
currentResults = results;
if (launcher) {
launcher.updateResults();
}
return;
}
searching = false;
currentResults = results;
Logger.d("FileSearch", "Found", results.length, "results");
if (launcher) {
launcher.updateResults();
}
}
function parseRawPaths(output) {
var trimmed = output.trim();
if (trimmed === "") {
return [];
}
return trimmed.split("\n").filter(function(line) { return line.trim() !== ""; });
}
function expandHomePath(pathValue) {
var expandedPath = pathValue;
if (expandedPath.startsWith("~")) {
expandedPath = Quickshell.env("HOME") + expandedPath.substring(1);
}
return expandedPath;
}
function displaySearchDirectory() {
var expandedPath = expandHomePath(searchDirectory);
var homeDir = Quickshell.env("HOME");
if (expandedPath.startsWith(homeDir)) {
return "~" + expandedPath.slice(homeDir.length);
}
return expandedPath;
}
function sortResults(results, query) {
var queryLower = query.toLowerCase();
results.sort(function(a, b) {
var rankA = resultRank(a, queryLower);
var rankB = resultRank(b, queryLower);
if (rankA !== rankB) {
return rankA - rankB;
}
var nameA = (a.name || "").toLowerCase();
var nameB = (b.name || "").toLowerCase();
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return (a.description || "").length - (b.description || "").length;
});
return results;
}
function resultRank(result, queryLower) {
var name = (result.name || "").toLowerCase();
var description = (result.description || "").toLowerCase();
var fullPath = description + "/" + name;
if (name === queryLower) {
return 0;
}
if (name.startsWith(queryLower)) {
return 1;
}
if (name.indexOf(queryLower) !== -1) {
return 2;
}
if (fullPath.indexOf(queryLower) !== -1) {
return 3;
}
return 4;
}
function formatFileEntry(filePath, forcedIsDirectory) {
var normalizedPath = filePath;
while (normalizedPath.length > 1 && normalizedPath.endsWith("/")) {
normalizedPath = normalizedPath.slice(0, -1);
}
var isDirectory = (forcedIsDirectory !== undefined) ? forcedIsDirectory : normalizedPath !== filePath;
var parts = normalizedPath.split("/");
var filename = parts[parts.length - 1];
var parentPath = parts.slice(0, -1).join("/");
if (filename === "") {
filename = normalizedPath;
}
var homeDir = Quickshell.env("HOME");
if (parentPath.startsWith(homeDir)) {
parentPath = "~" + parentPath.slice(homeDir.length);
}
return {
"name": filename,
"description": parentPath,
"icon": isDirectory ? "folder" : getFileIcon(filename),
"isTablerIcon": true,
"isImage": false,
"singleLine": false,
"onActivate": function() {
root.openFile(normalizedPath);
}
};
}
function getFileIcon(filename) {
var ext = filename.split(".").pop().toLowerCase();
// Images
if (["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp", "ico"].indexOf(ext) !== -1) {
return "photo";
}
// Documents
if (["txt", "md", "pdf", "doc", "docx", "odt", "rtf"].indexOf(ext) !== -1) {
return "file-text";
}
// Code files
if (["js", "ts", "py", "java", "cpp", "c", "h", "qml", "rs", "go", "rb", "php", "html", "css", "json", "xml", "yaml", "yml"].indexOf(ext) !== -1) {
return "code";
}
// Archives
if (["zip", "tar", "gz", "bz2", "xz", "7z", "rar"].indexOf(ext) !== -1) {
return "file-zip";
}
// Audio
if (["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"].indexOf(ext) !== -1) {
return "music";
}
// Video
if (["mp4", "mkv", "avi", "mov", "wmv", "flv", "webm"].indexOf(ext) !== -1) {
return "video";
}
// Spreadsheets
if (["xls", "xlsx", "ods", "csv"].indexOf(ext) !== -1) {
return "table";
}
// Presentations
if (["ppt", "pptx", "odp"].indexOf(ext) !== -1) {
return "presentation";
}
// Default
return "file";
}
function openFile(filePath) {
Logger.i("FileSearch", "Opening file:", filePath);
Quickshell.execDetached([fileOpener, filePath]);
launcher.close();
}
}
+68
View File
@@ -0,0 +1,68 @@
import QtQuick
import Quickshell.Io
import qs.Commons
import qs.Services.UI
Item {
property var pluginApi: null
Component.onCompleted: {
if (pluginApi) {
Logger.i("FileSearch", "Plugin initialized");
}
}
IpcHandler {
target: "plugin:file-search"
// Toggle launcher in file search mode
function toggle() {
if (!pluginApi) return;
pluginApi.withCurrentScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
if (!launcherPanel) {
Logger.e("FileSearch", "Could not get launcher panel");
return;
}
var searchText = launcherPanel.searchText || "";
var isInFileMode = searchText.startsWith(">file");
if (!launcherPanel.isPanelOpen) {
// Launcher closed - open with file search
Logger.i("FileSearch", "Opening launcher in file search mode");
launcherPanel.open();
launcherPanel.setSearchText(">file ");
} else if (isInFileMode) {
// Already in file mode - close launcher
Logger.i("FileSearch", "Closing launcher (toggle off)");
launcherPanel.close();
} else {
// Launcher open but different mode - switch to file search
Logger.i("FileSearch", "Switching to file search mode");
launcherPanel.setSearchText(">file ");
}
});
}
// Open launcher with file search and specific query
function search(query: string) {
if (!pluginApi) return;
pluginApi.withCurrentScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
if (!launcherPanel) {
Logger.e("FileSearch", "Could not get launcher panel");
return;
}
var searchQuery = query || "";
Logger.i("FileSearch", "Opening launcher with search query:", searchQuery);
launcherPanel.open();
launcherPanel.setSearchText(">file " + searchQuery);
});
}
}
}
+25
View File
@@ -0,0 +1,25 @@
# File Search Plugin
File search from the launcher.
## Requirements
This plugin requires [fd](https://github.com/sharkdp/fd#installation) to be installed.
## Usage
**Access from launcher:**
Type `>file` in the Noctalia launcher to activate file search.
**Toggle file search:**
```bash
noctalia-shell ipc call plugin:file-search toggle
```
**Search with pre-filled query:**
```bash
noctalia-shell ipc call plugin:file-search search "eko"
```
+142
View File
@@ -0,0 +1,142 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
property var pluginApi: null
property var cfg: pluginApi?.pluginSettings || ({})
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
property bool valueShowHidden: cfg.showHidden ?? defaults.showHidden
property int valueMaxResults: cfg.maxResults ?? defaults.maxResults
property string valueFileOpener: cfg.fileOpener ?? defaults.fileOpener
property string valueFdCommand: cfg.fdCommand ?? defaults.fdCommand
property string valueSearchDirectory: cfg.searchDirectory ?? defaults.searchDirectory
spacing: Style.marginL
Component.onCompleted: {
Logger.d("FileSearch", "Settings UI loaded");
}
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
// Show Hidden Files Toggle
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: pluginApi?.tr("settings.showHidden.label")
font.pointSize: Style.fontSizeL
font.weight: Font.Medium
color: Color.mOnSurface
Layout.fillWidth: true
}
}
NToggle {
checked: root.valueShowHidden
onToggled: root.valueShowHidden = checked
}
}
// File Opener Input
NTextInput {
Layout.fillWidth: true
label: pluginApi?.tr("settings.fileOpener.label")
description: pluginApi?.tr("settings.fileOpener.description")
placeholderText: pluginApi?.tr("settings.fileOpener.placeholder")
text: root.valueFileOpener
onTextChanged: root.valueFileOpener = text
}
// Search Directory Input
NTextInput {
Layout.fillWidth: true
label: pluginApi?.tr("settings.searchDirectory.label")
description: pluginApi?.tr("settings.searchDirectory.description")
placeholderText: pluginApi?.tr("settings.searchDirectory.placeholder")
text: root.valueSearchDirectory
onTextChanged: root.valueSearchDirectory = text
}
// fd Command Path Input
NTextInput {
Layout.fillWidth: true
label: pluginApi?.tr("settings.fdCommand.label")
description: pluginApi?.tr("settings.fdCommand.description")
placeholderText: pluginApi?.tr("settings.fdCommand.placeholder")
text: root.valueFdCommand
onTextChanged: root.valueFdCommand = text
}
}
// Max Results Slider
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
RowLayout {
Layout.fillWidth: true
NText {
text: pluginApi?.tr("settings.maxResults.label")
font.pointSize: Style.fontSizeL
font.weight: Font.Medium
color: Color.mOnSurface
Layout.fillWidth: true
}
NText {
text: root.valueMaxResults === 0 ? pluginApi?.tr("settings.maxResults.unlimited") : root.valueMaxResults.toString()
font.pointSize: Style.fontSizeM
font.weight: Font.Medium
color: Color.mPrimary
}
}
NText {
text: pluginApi?.tr("settings.maxResults.description")
font.pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
NSlider {
Layout.fillWidth: true
from: 0
to: 200
stepSize: 10
value: root.valueMaxResults
onMoved: root.valueMaxResults = Math.round(value)
}
}
function saveSettings() {
if (!pluginApi) {
Logger.e("FileSearch", "Cannot save settings: pluginApi is null");
return;
}
pluginApi.pluginSettings.showHidden = root.valueShowHidden;
pluginApi.pluginSettings.maxResults = root.valueMaxResults;
pluginApi.pluginSettings.fileOpener = root.valueFileOpener;
pluginApi.pluginSettings.searchDirectory = root.valueSearchDirectory;
pluginApi.pluginSettings.fdCommand = root.valueFdCommand;
pluginApi.saveSettings();
Logger.d("FileSearch", "Settings saved successfully");
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"provider": {
"name": "File Search"
},
"launcher": {
"command": {
"description": "Search files and folders"
},
"errors": {
"fdNotFound": {
"title": "fd not found",
"description": "Please install fd to use file search"
}
},
"prompts": {
"emptyQuery": {
"title": "Type to search files and folders",
"description": "Start typing to search in {{root}}"
},
"searching": {
"title": "Searching...",
"description": "Looking for files and folders matching: {{query}}"
},
"noResults": {
"title": "No results found",
"description": "No files or folders matching '{{query}}'"
}
}
},
"settings": {
"showHidden": {
"label": "Include hidden files"
},
"fileOpener": {
"label": "File opener command",
"description": "Command used to open files and folders",
"placeholder": "xdg-open"
},
"searchDirectory": {
"label": "Search directory",
"description": "Directory to search for files and folders",
"placeholder": "~"
},
"fdCommand": {
"label": "fd command path",
"description": "Command name or full path",
"placeholder": "fd"
},
"maxResults": {
"label": "Maximum results",
"unlimited": "Unlimited",
"description": "Limit the number of search results displayed (set to 0 for unlimited)"
}
}
}
@@ -0,0 +1,31 @@
{
"id": "file-search",
"name": "File Search",
"version": "1.0.1",
"minNoctaliaVersion": "4.1.2",
"author": "ericbreh",
"license": "MIT",
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
"description": "File search from the launcher.",
"tags": [
"Launcher",
"Productivity"
],
"entryPoints": {
"main": "Main.qml",
"launcherProvider": "LauncherProvider.qml",
"settings": "Settings.qml"
},
"dependencies": {
"plugins": []
},
"metadata": {
"defaultSettings": {
"showHidden": false,
"maxResults": 0,
"fileOpener": "xdg-open",
"searchDirectory": "~",
"fdCommand": "fd"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 965 KiB

@@ -0,0 +1,53 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Settings
import qs.Services.Hardware
import qs.Services.UI
import qs.Widgets
import "./Services"
Item {
id: root
property var pluginApi: null
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
implicitWidth: pill.width
implicitHeight: pill.height
visible: !hideIfNoDeviceConnected ? true : KDEConnect.anyDevicesConnected;
opacity: (!hideIfNoDeviceConnected ? true : KDEConnect.anyDevicesConnected) ? 1.0 : 0.0;
property bool hideIfNoDeviceConnected: !(root.pluginApi?.mainInstance?.hideIfNoDeviceConnected ?? false)
BarPill {
id: pill
screen: root.screen
oppositeDirection: BarService.getPillDirection(root)
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
icon: KDEConnectUtils.getConnectionStateIcon(KDEConnect.mainDevice, KDEConnect.daemonAvailable)
autoHide: false // Important to be false so we can hover as long as we want
text: !KDEConnect.daemonAvailable || KDEConnect.mainDevice === null || KDEConnect.mainDevice.battery === -1 ? "" : (KDEConnect.mainDevice.battery + "%")
tooltipText: pluginApi?.tr("bar.tooltip")
onClicked: {
if (pluginApi) {
pluginApi.openPanel(root.screen, this);
}
}
}
}
@@ -0,0 +1,27 @@
import QtQuick
import Quickshell
import qs.Widgets
import "./Services"
NIconButtonHot {
property ShellScreen screen
property var pluginApi: null
function getTooltip(device) {
const batteryLabel = pluginApi?.tr("panel.card.battery") || "Battery";
const stateLabel = pluginApi?.tr("control_center.state-label") || "State";
const batteryLine = (device !== null && device.reachable && device.paired && device.battery !== -1) ? (batteryLabel + ": " + device.battery + "%\n") : "";
const stateKey = KDEConnectUtils.getConnectionStateKey(device, KDEConnect.daemonAvailable);
const stateValue = pluginApi?.tr(stateKey) || "Unknown";
const stateLine = stateLabel + ": " + stateValue;
return batteryLine + stateLine;
}
icon: KDEConnectUtils.getConnectionStateIcon(KDEConnect.mainDevice, KDEConnect.daemonAvailable)
tooltipText: getTooltip(KDEConnect.mainDevice)
onClicked: pluginApi?.togglePanel(screen, this)
}
+339
View File
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
+24
View File
@@ -0,0 +1,24 @@
import QtQuick
import Quickshell.Io
import qs.Services.UI
import qs.Commons
import "./Services"
Item {
property var pluginApi: null
onPluginApiChanged: {
KDEConnect.setMainDevice(pluginApi?.pluginSettings?.mainDeviceId || "")
}
IpcHandler {
target: "plugin:kde-connect"
function toggle() {
if (pluginApi) {
pluginApi.withCurrentScreen(screen => {
pluginApi.openPanel(screen);
});
}
}
}
}
+767
View File
@@ -0,0 +1,767 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.UI
import qs.Widgets
import "./Services"
import Quickshell
// Panel Component
Item {
id: root
// Plugin API (injected by PluginPanelSlot)
property var pluginApi: null
// SmartPanel
readonly property var geometryPlaceholder: panelContainer
property real contentPreferredWidth: 440 * Style.uiScaleRatio
property real contentPreferredHeight: 360 * Style.uiScaleRatio * Settings.data.ui.fontDefaultScale
readonly property bool allowAttach: true
property bool deviceSwitcherOpen: false
anchors.fill: parent
Component.onCompleted: {
if (pluginApi) {
Logger.i("KDEConnect", "Panel initialized");
}
}
Rectangle {
id: panelContainer
anchors.fill: parent
color: "transparent"
ColumnLayout {
id: deviceData
function getBatteryIcon(percentage, isCharging) {
if (percentage < 0) return "battery-exclamation"
if (isCharging) return "battery-charging-2"
if (percentage < 5) return "battery"
if (percentage < 25) return "battery-1"
if (percentage < 50) return "battery-2"
if (percentage < 75) return "battery-3"
return "battery-4"
}
function getCellularTypeIcon(type) {
switch (type) {
case "5G":
return "signal-5g"
case "LTE":
return "signal-4g"
case "HSPA":
return "signal-h"
case "UMTS":
return "signal-3g"
case "EDGE":
return "signal-e"
case "GPRS":
return "signal-g"
case "GSM":
return "signal-2g"
case "CDMA":
return "signal-3g"
case "CDMA2000":
return "signal-3g"
case "iDEN":
return "signal-2g"
default:
return "wave-square"
}
}
function getCellularStrengthIcon(strength) {
switch (strength) {
case 0:
return "antenna-bars-1"
case 1:
return "antenna-bars-2"
case 2:
return "antenna-bars-3"
case 3:
return "antenna-bars-4"
case 4:
return "antenna-bars-5"
default:
return "antenna-bars-off"
}
}
function getSignalStrengthText(strength) {
switch (strength) {
case 0:
return pluginApi?.tr("panel.signal.very-weak")
case 1:
return pluginApi?.tr("panel.signal.weak")
case 2:
return pluginApi?.tr("panel.signal.fair")
case 3:
return pluginApi?.tr("panel.signal.good")
case 4:
return pluginApi?.tr("panel.signal.excellent")
default:
return pluginApi?.tr("panel.unknown")
}
}
anchors {
fill: parent
margins: Style.marginL
}
spacing: Style.marginL
NBox {
id: headerBox
Layout.fillWidth: true
implicitHeight: headerRow.implicitHeight + (Style.marginXL)
RowLayout {
id: headerRow
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
NIcon {
icon: "device-mobile"
pointSize: Style.fontSizeXXL
color: Color.mPrimary
}
NText {
text: pluginApi?.tr("panel.title")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NIconButton {
readonly property bool multipleDevices: KDEConnect.devices.length > 1
icon: "swipe"
tooltipText: multipleDevices ? pluginApi?.tr("panel.other-devices") : ""
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
deviceSwitcherOpen = !deviceSwitcherOpen
}
enabled: KDEConnect.daemonAvailable && multipleDevices
opacity: multipleDevices ? 1.0 : 0.0
}
Item {
Layout.fillWidth: true
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("common.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
if (pluginApi)
pluginApi.withCurrentScreen(s => pluginApi.closePanel(s));
}
}
}
}
Loader {
Layout.fillWidth: true
Layout.fillHeight: true
active: true
sourceComponent: (KDEConnect.busctlCmd === null || KDEConnect.busctlCmd === "") ? busctlNotFoundCard :
(!KDEConnect.daemonAvailable) ? kdeConnectDaemonNotRunningCard :
(deviceSwitcherOpen) ? deviceSwitcherCard :
(KDEConnect.mainDevice !== null && !KDEConnect.mainDevice.reachable) ? deviceNotReachableCard :
(KDEConnect.mainDevice !== null && KDEConnect.mainDevice.paired) ? deviceConnectedCard :
(KDEConnect.mainDevice !== null && !KDEConnect.mainDevice.paired) ? noDevicePairedCard :
(KDEConnect.devices.length === 0) ? noDevicesAvailableCard :
null
}
Component {
id: deviceConnectedCard
Rectangle {
Layout.fillWidth: true
color: Color.mSurfaceVariant
radius: Style.radiusM
Component.onCompleted: {
root.contentPreferredHeight = headerBox.height + contentLayout.implicitHeight + (Style.marginL * 8)
}
Component.onDestruction: {
root.contentPreferredHeight = 360 * Style.uiScaleRatio * Settings.data.ui.fontDefaultScale
}
ColumnLayout {
id: contentLayout
anchors {
fill: parent
margins: Style.marginL
}
spacing: Style.marginL
RowLayout {
NText {
text: KDEConnect.mainDevice.name
pointSize: Style.fontSizeXXL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NFilePicker {
id: shareFilePicker
title: pluginApi?.tr("panel.send-file-picker")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*"]
onAccepted: paths => {
if (paths.length > 0) {
for (const path of paths) {
KDEConnect.shareFile(KDEConnect.mainDevice.id, path)
}
}
}
}
NIconButton {
icon: "device-mobile-search"
tooltipText: pluginApi?.tr("panel.browse-device")
onClicked: {
KDEConnect.browseFiles(KDEConnect.mainDevice.id)
}
}
NIconButton {
icon: "device-mobile-share"
tooltipText: pluginApi?.tr("panel.send-file")
onClicked: {
shareFilePicker.open()
}
}
NIconButton {
icon: "radar"
tooltipText: pluginApi?.tr("panel.find-device")
onClicked: {
KDEConnect.triggerFindMyPhone(KDEConnect.mainDevice.id)
}
}
}
// Device Status
Loader {
Layout.fillWidth: true
Layout.fillHeight: true
active: KDEConnect.mainDevice !== null
sourceComponent: deviceStatsWithPhone
}
}
Component {
id: deviceStatsWithPhone
RowLayout {
spacing: Style.marginM
Rectangle {
width: 100 * Style.uiScaleRatio
color: "transparent"
Layout.fillHeight: true
Layout.alignment: Qt.AlignCenter
PhoneDisplay {
Layout.alignment: Qt.AlignCenter
backgroundImage: ""
onClicked: KDEConnect.wakeUpDevice(KDEConnect.mainDevice.id)
}
}
Item {
width: Style.marginL
}
// Stats Grid
GridLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
columns: 1
rowSpacing: Style.marginL
// Battery Section
RowLayout {
spacing: Style.marginM
NIcon {
icon: deviceData.getBatteryIcon(KDEConnect.mainDevice.battery, KDEConnect.mainDevice.charging)
pointSize: Style.fontSizeXXXL
applyUiScale: true
color: Color.mOnSurface
}
ColumnLayout {
spacing: 2 * Style.uiScaleRatio
NText {
text: pluginApi?.tr("panel.card.battery")
pointSize: Style.fontSizeS
color: Color.mOnSurface
}
NText {
text: KDEConnect.mainDevice.battery < 0 ? pluginApi?.tr("panel.unknown") : (KDEConnect.mainDevice.battery + "%")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
}
}
}
// Network Type Section
RowLayout {
spacing: Style.marginM
NIcon {
icon: deviceData.getCellularTypeIcon(KDEConnect.mainDevice.cellularNetworkType)
pointSize: Style.fontSizeXXXL
applyUiScale: true
color: Color.mOnSurface
}
ColumnLayout {
spacing: 2 * Style.uiScaleRatio
NText {
text: pluginApi?.tr("panel.card.network")
pointSize: Style.fontSizeS
color: Color.mOnSurface
}
NText {
text: KDEConnect.mainDevice.cellularNetworkType || pluginApi?.tr("panel.unknown")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
}
}
}
// Signal Strength Section
RowLayout {
spacing: Style.marginM
NIcon {
icon: deviceData.getCellularStrengthIcon(KDEConnect.mainDevice.cellularNetworkStrength)
pointSize: Style.fontSizeXXXL
applyUiScale: true
color: Color.mOnSurface
}
ColumnLayout {
spacing: 2 * Style.uiScaleRatio
NText {
text: pluginApi?.tr("panel.card.signal-strength")
pointSize: Style.fontSizeS
color: Color.mOnSurface
}
NText {
text: deviceData.getSignalStrengthText(KDEConnect.mainDevice.cellularNetworkStrength)
pointSize: Style.fontSizeL
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
}
}
}
// Notifications Section
RowLayout {
spacing: Style.marginM
NIcon {
icon: "notification"
pointSize: Style.fontSizeXXXL
applyUiScale: true
color: Color.mOnSurface
}
ColumnLayout {
spacing: 2 * Style.uiScaleRatio
NText {
text: pluginApi?.tr("panel.card.notifications")
pointSize: Style.fontSizeS
color: Color.mOnSurface
}
NText {
text: KDEConnect.mainDevice.notificationIds.length
pointSize: Style.fontSizeL
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
}
}
}
}
}
}
}
}
Component {
id: noDevicePairedCard
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurfaceVariant
radius: Style.radiusM
ColumnLayout {
anchors {
fill: parent
margins: Style.marginL
}
spacing: Style.marginL
RowLayout {
NText {
text: KDEConnect.mainDevice.name
pointSize: Style.fontSizeXXL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "transparent"
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
NButton {
text: pluginApi?.tr("panel.pair")
Layout.alignment: Qt.AlignHCenter
enabled: !KDEConnect.mainDevice.pairRequested
onClicked: {
KDEConnect.requestPairing(KDEConnect.mainDevice.id)
KDEConnect.mainDevice.pairRequested = true
KDEConnect.refreshDevices()
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginM
NIcon {
icon: "key"
pointSize: Style.fontSizeXL
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
opacity: KDEConnect.mainDevice.pairRequested ? 1.0 : 0.0
}
NText {
text: KDEConnect.mainDevice.verificationKey
Layout.alignment: Qt.AlignHCenter
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
opacity: KDEConnect.mainDevice.pairRequested ? 1.0 : 0.0
}
}
NBusyIndicator {
Layout.alignment: Qt.AlignHCenter
opacity: KDEConnect.mainDevice.pairRequested ? 1.0 : 0.0
size: Style.baseWidgetSize * 0.5
running: KDEConnect.mainDevice.pairRequested
}
}
}
}
}
}
Component {
id: noDevicesAvailableCard
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurfaceVariant
radius: Style.radiusM
ColumnLayout {
id: emptyState
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
Item {
Layout.fillHeight: true
}
NIcon {
icon: "device-mobile-off"
pointSize: 48 * Style.uiScaleRatio
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {}
NText {
text: pluginApi?.tr("panel.kdeconnect-error.no-devices")
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
}
Item {
Layout.fillHeight: true
}
}
}
}
Component {
id: deviceNotReachableCard
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurfaceVariant
radius: Style.radiusM
ColumnLayout {
id: emptyState
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
Item {
Layout.fillHeight: true
}
NIcon {
icon: "device-mobile-off"
pointSize: 48 * Style.uiScaleRatio
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {}
NText {
text: pluginApi?.tr("panel.kdeconnect-error.device-unavailable")
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
}
Item {
}
NButton {
text: pluginApi?.tr("panel.unpair")
Layout.alignment: Qt.AlignHCenter
onClicked: {
KDEConnect.unpairDevice(KDEConnect.mainDevice.id)
}
}
Item {
Layout.fillHeight: true
}
}
}
}
Component {
id: busctlNotFoundCard
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurfaceVariant
radius: Style.radiusM
ColumnLayout {
id: emptyState
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
Item {
Layout.fillHeight: true
}
NIcon {
icon: "exclamation-circle"
pointSize: 48 * Style.uiScaleRatio
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {}
NText {
text: pluginApi?.tr("panel.busctl-error.unavailable-title")
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
NText {
text: pluginApi?.tr("panel.busctl-error.unavailable-desc")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Item {
Layout.fillHeight: true
}
}
}
}
Component {
id: kdeConnectDaemonNotRunningCard
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurfaceVariant
radius: Style.radiusM
ColumnLayout {
id: emptyState
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
Item {
Layout.fillHeight: true
}
NIcon {
icon: "exclamation-circle"
pointSize: 48 * Style.uiScaleRatio
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
Item {}
NText {
text: pluginApi?.tr("panel.kdeconnect-error.unavailable-title")
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
NText {
text: pluginApi?.tr("panel.kdeconnect-error.unavailable-desc")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Item {
Layout.fillHeight: true
}
}
}
}
Component {
id: deviceSwitcherCard
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurfaceVariant
radius: Style.radiusM
NScrollView{
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
contentWidth: parent.width
reserveScrollbarSpace: false
gradientColor: Color.mSurface
ColumnLayout {
id: emptyState
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
Repeater {
model: KDEConnect.devices
Layout.fillWidth: true
NButton {
required property var modelData
text: modelData.name
Layout.fillWidth: true
backgroundColor: modelData.id === KDEConnect.mainDevice.id ? Color.mSecondary : Color.mPrimary
onClicked: {
KDEConnect.setMainDevice(modelData.id);
deviceSwitcherOpen = false;
pluginApi.pluginSettings.mainDeviceId = modelData.id;
pluginApi.saveSettings();
}
}
}
Item {
Layout.fillHeight: true
}
}
}
}
}
}
}
}
@@ -0,0 +1,125 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import qs.Commons
import qs.Widgets
Rectangle {
id: phoneRoot
property string backgroundImage: "" // Path to background image
height: parent ? parent.height : 235
width: (height / 235) * 115
readonly property real scaleFactor: Math.min(width / 115, height / 235)
radius: 20 * scaleFactor
color: "#1c1c1e"
signal clicked;
MultiEffect {
source: phoneRect
anchors.fill: phoneRect
shadowEnabled: true
shadowBlur: phoneRect.scale > 0.97 ? 0.8 : 0.3
shadowVerticalOffset: phoneRect.scale > 0.97 ? 8 : 2
shadowColor: "#80000000"
Behavior on shadowBlur { NumberAnimation { duration: 100 } }
Behavior on shadowVerticalOffset { NumberAnimation { duration: 100 } }
}
RectangularShadow {
anchors.fill: phoneRect
radius: phoneRoot.radius
blur: 15
spread: 1
}
// Bezel/frame
Rectangle {
id: phoneRect
Behavior on scale {
NumberAnimation { duration: 100; easing.type: Easing.OutCubic }
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: phoneRect.scale = 1.02
onExited: phoneRect.scale = 1.0
onPressed: phoneRect.scale = 0.99
onReleased: phoneRect.scale = containsMouse ? 1.02 : 1.0
onClicked: phoneRoot.clicked();
}
anchors {
fill: parent
margins: 2 * phoneRoot.scaleFactor
}
radius: 18 * phoneRoot.scaleFactor
color: "black"
// Screen
Rectangle {
id: screen
anchors {
fill: parent
margins: 1 * phoneRoot.scaleFactor
}
radius: 17 * phoneRoot.scaleFactor
color: "black"
clip: true
// Background wallpaper
Image {
anchors.fill: parent
source: phoneRoot.backgroundImage
fillMode: Image.PreserveAspectCrop
visible: phoneRoot.backgroundImage !== ""
// Fallback gradient if no image
Rectangle {
anchors.fill: parent
visible: phoneRoot.backgroundImage === ""
gradient: Gradient {
GradientStop { position: 0.0; color: "#2c3e50" }
GradientStop { position: 1.0; color: "#34495e" }
}
}
}
// Dynamic Island
Rectangle {
id: dynamicIsland
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
topMargin: 6 * phoneRoot.scaleFactor
}
width: 48 * phoneRoot.scaleFactor
height: 10 * phoneRoot.scaleFactor
radius: 5 * phoneRoot.scaleFactor
color: "black"
}
// Home indicator (bottom gesture bar)
Rectangle {
anchors {
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
bottomMargin: 6 * phoneRoot.scaleFactor
}
width: 40 * phoneRoot.scaleFactor
height: 4 * phoneRoot.scaleFactor
radius: 2 * phoneRoot.scaleFactor
color: "white"
opacity: 0.4
}
}
}
}
+28
View File
@@ -0,0 +1,28 @@
# Noctalia KDE Connect
A Plugin integrating your mobile devices into a panel using KDEConnect
> [!IMPORTANT]
> Please submit any Pull Requests to https://github.com/WerWolv/noctalia-kde-connect and **NOT** to the noctalia-plugins repository!
## Features
- Support for multiple devices
- Panel to manage all devices
- Current battery charge and if the device is plugged in
- Mobile network connection state
- Number of notifications
- Wake up the device from the panel
- Browse files on the device
- Send files to the device
- Make the device ring
## Requirements
- `kdeconnectd` needs to be running which can be installed by setting up the official KDE Connect app
- In case it's not getting started by default, you might need to configure a systemd service for it
- Certain functionality will only work when enabling the right plugins on the device. Otherwise, they might not work or simply display "Unknown"
- The "Browse files" option mounts the device over SFTP using sshfs. Make sure you have `libfuse` and `sshfs` installed
- If clicking the button just opens the file browser without displaying anything, make sure you have the option enabled on your phone and that your file browser has permissions to access that path.
- If your file browser is sandboxed (e.g. when installed as a Flatpak or Snap), it's possible that it won't have access. Install it through the package manager instead
- On some systems kdeconnect's URL handler isn't configured properly and the button will instead just open your web browser
- In that case you can override the file handler by running `xdg-mime default org.kde.dolphin.desktop x-scheme-handler/kdeconnect`
@@ -0,0 +1,457 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
QtObject {
id: root
property list<var> devices: []
property bool daemonAvailable: false
property int pendingDeviceCount: 0
property list<var> pendingDevices: []
property var mainDevice: null
property string mainDeviceId: ""
property string busctlCmd: ""
property bool anyDevicesConnected: false;
onDevicesChanged: {
setMainDevice(root.mainDeviceId)
}
Component.onCompleted: {
checkDaemon();
}
// Check if KDE Connect daemon is available
function checkDaemon(): void {
detectBusctlProc.running = true;
}
// Refresh the list of devices
function refreshDevices(): void {
getDevicesProc.running = true;
}
function setMainDevice(deviceId: string): void {
root.mainDeviceId = deviceId;
updateMainDevice(false);
}
function updateMainDevice(checkReachable) {
let newMain;
if (checkReachable) {
newMain = devices.find((device) => device.id === root.mainDeviceId && device.reachable);
if (newMain === undefined)
newMain = devices.find((device) => device.reachable);
if (newMain === undefined)
newMain = devices.length === 0 ? null : devices[0];
} else {
newMain = devices.find((device) => device.id === root.mainDeviceId);
if (newMain === undefined)
newMain = devices.length === 0 ? null : devices[0];
}
if (root.mainDevice !== newMain) {
root.mainDevice = newMain;
}
anyDevicesConnected = devices.find((device) => device.reachable) !== undefined;
}
function triggerFindMyPhone(deviceId: string): void {
const proc = findMyPhoneComponent.createObject(root, { deviceId: deviceId });
proc.running = true;
}
function browseFiles(deviceId: string): void {
const proc = browseFilesComponent.createObject(root, { deviceId: deviceId });
proc.running = true;
}
// Share a file with a device
function shareFile(deviceId: string, filePath: string): void {
var proc = shareComponent.createObject(root, {
deviceId: deviceId,
filePath: filePath
});
proc.running = true;
}
function requestPairing(deviceId: string): void {
const proc = requestPairingComponent.createObject(root, { deviceId: deviceId });
proc.running = true;
}
function unpairDevice(deviceId: string): void {
const proc = unpairingComponent.createObject(root, { deviceId: deviceId });
proc.running = true;
}
function wakeUpDevice(deviceId: string): void {
const proc = wakeUpDeviceComponent.createObject(root, { deviceId: deviceId });
proc.running = true;
}
function busctlCall(obj, itf, method, params = []) {
let result = [ root.busctlCmd, "--user", "call", "--json=short", "org.kde.kdeconnect", obj, itf, method ];
return result.concat(params);
}
function busctlGet(obj, itf, prop) {
return [ root.busctlCmd, "--user", "get-property", "--json=short", "org.kde.kdeconnect", obj, itf, prop ];
}
function busctlData(text) {
if (text === "")
return "";
try {
let result = JSON.parse(text)?.data;
if (Array.isArray(result) && Array.isArray(result[0]))
return result[0]
else
return result;
} catch (e) {
Logger.e("KDEConnect", "Failed to parse busctl response: ", text)
return null;
}
}
property Process detectBusctlProc: Process {
command: ["which", "busctl"]
stdout: StdioCollector {
onStreamFinished: {
if (root.busctlCmd !== "") {
root.daemonCheckProc.running = true
return
}
let location = text.trim()
if (location !== "") {
root.busctlCmd = location
root.daemonCheckProc.running = true
Logger.i("KDEConnect", "Found busctl command:", location)
}
}
}
}
// Check daemon
property Process daemonCheckProc: Process {
command: [root.busctlCmd, "--user", "status", "org.kde.kdeconnect"]
onExited: (exitCode, exitStatus) => {
root.daemonAvailable = exitCode == 0;
if (root.daemonAvailable) {
forceOnNetworkChange.running = true;
} else {
root.devices = []
root.mainDevice = null
}
}
}
property Process forceOnNetworkChange: Process {
command: busctlCall("/modules/kdeconnect", "org.kde.kdeconnect.daemon", "forceOnNetworkChange")
stdout: StdioCollector {
onStreamFinished: {
getDevicesProc.running = true;
}
}
}
// Get device list
property Process getDevicesProc: Process {
command: busctlCall("/modules/kdeconnect", "org.kde.kdeconnect.daemon", "devices")
stdout: StdioCollector {
onStreamFinished: {
const deviceIds = busctlData(text);
root.pendingDevices = [];
root.pendingDeviceCount = deviceIds.length;
deviceIds.forEach(deviceId => {
const loader = deviceLoaderComponent.createObject(root, { deviceId: deviceId });
loader.start();
});
}
}
}
// Component that loads all info for a single device
property Component deviceLoaderComponent: Component {
QtObject {
id: loader
property string deviceId: ""
property var deviceData: ({
id: deviceId,
name: "",
reachable: false,
paired: false,
pairRequested: false,
verificationKey: "",
charging: false,
battery: -1,
cellularNetworkType: "",
cellularNetworkStrength: -1,
notificationIds: []
})
function start() {
nameProc.running = true
}
property Process nameProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "name")
stdout: StdioCollector {
onStreamFinished: {
loader.deviceData.name = busctlData(text);
reachableProc.running = true;
}
}
}
property Process reachableProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "isReachable")
stdout: StdioCollector {
onStreamFinished: {
loader.deviceData.reachable = busctlData(text);
pairingRequestedProc.running = true;
}
}
}
property Process pairingRequestedProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "isPairRequested")
stdout: StdioCollector {
onStreamFinished: {
loader.deviceData.pairRequested = busctlData(text);
verificationKeyProc.running = true;
}
}
}
property Process verificationKeyProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "verificationKey")
stdout: StdioCollector {
onStreamFinished: {
loader.deviceData.verificationKey = busctlData(text);
pairedProc.running = true;
}
}
}
property Process pairedProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId, "org.kde.kdeconnect.device", "isPaired")
stdout: StdioCollector {
onStreamFinished: {
loader.deviceData.paired = busctlData(text);
if (loader.deviceData.paired)
activeNotificationsProc.running = true;
else
finalize()
}
}
}
property Process activeNotificationsProc: Process {
command: busctlCall("/modules/kdeconnect/devices/" + loader.deviceId + "/notifications", "org.kde.kdeconnect.device.notifications", "activeNotifications");
stdout: StdioCollector {
onStreamFinished: {
let ids = busctlData(text);
loader.deviceData.notificationIds = ids
cellularNetworkTypeProc.running = true;
}
}
}
property Process cellularNetworkTypeProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId + "/connectivity_report", "org.kde.kdeconnect.device.connectivity_report", "cellularNetworkType")
stdout: StdioCollector {
onStreamFinished: {
loader.deviceData.cellularNetworkType = busctlData(text);
cellularNetworkStrengthProc.running = true;
}
}
}
property Process cellularNetworkStrengthProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId + "/connectivity_report", "org.kde.kdeconnect.device.connectivity_report", "cellularNetworkStrength")
stdout: StdioCollector {
onStreamFinished: {
const strength = busctlData(text);
loader.deviceData.cellularNetworkStrength = strength;
isChargingProc.running = true;
}
}
}
property Process isChargingProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId + "/battery", "org.kde.kdeconnect.device.battery", "isCharging")
stdout: StdioCollector {
onStreamFinished: {
loader.deviceData.charging = busctlData(text);
batteryProc.running = true;
}
}
}
property Process batteryProc: Process {
command: busctlGet("/modules/kdeconnect/devices/" + loader.deviceId + "/battery", "org.kde.kdeconnect.device.battery", "charge")
stdout: StdioCollector {
onStreamFinished: {
const charge = busctlData(text);
if (!isNaN(charge)) {
loader.deviceData.battery = charge;
}
finalize();
}
}
}
function finalize() {
root.pendingDevices = root.pendingDevices.concat([loader.deviceData]);
if (root.pendingDevices.length === root.pendingDeviceCount) {
let newDevices = root.pendingDevices
newDevices.sort((a, b) => a.name.localeCompare(b.name))
let prevMainDevice = root.devices.find((device) => device.id === root.mainDeviceId);
let newMainDevice = newDevices.find((device) => device.id === root.mainDeviceId);
let deviceNotReachableAnymore =
prevMainDevice === undefined ||
(
(prevMainDevice?.reachable ?? false) &&
!(newMainDevice?.reachable ?? false)
) ||
(
(prevMainDevice?.paired ?? false) &&
!(newMainDevice?.paired ?? false)
)
root.devices = newDevices
root.pendingDevices = []
updateMainDevice(deviceNotReachableAnymore);
}
loader.destroy();
}
}
}
// FindMyPhone component
property Component findMyPhoneComponent: Component {
Process {
id: proc
property string deviceId: ""
command: busctlCall("/modules/kdeconnect/devices/" + deviceId + "/findmyphone", "org.kde.kdeconnect.device.findmyphone", "ring")
stdout: StdioCollector {
onStreamFinished: proc.destroy()
}
}
}
// SFTP Browse component
property Component browseFilesComponent: Component {
Process {
id: mountProc
property string deviceId: ""
command: busctlCall("/modules/kdeconnect/devices/" + deviceId + "/sftp", "org.kde.kdeconnect.device.sftp", "mountAndWait")
stdout: StdioCollector {
onStreamFinished: rootDirProc.running = true
}
property Process rootDirProc: Process {
command: busctlCall("/modules/kdeconnect/devices/" + mountProc.deviceId + "/sftp", "org.kde.kdeconnect.device.sftp", "getDirectories")
stdout: StdioCollector {
onStreamFinished: {
const dirs = busctlData(text);
const path = Object.keys(dirs[0])[0];
if (!Qt.openUrlExternally("file://" + path)) {
Logger.e("KDEConnect", "Failed to open file manager for path:", path);
}
mountProc.destroy();
}
}
}
}
}
// Request Pairing Component
property Component requestPairingComponent: Component {
Process {
id: proc
property string deviceId: ""
command: busctlCall("/modules/kdeconnect/devices/" + deviceId, "org.kde.kdeconnect.device", "requestPairing")
stdout: StdioCollector {
onStreamFinished: proc.destroy()
}
}
}
// Unpairing Component
property Component unpairingComponent: Component {
Process {
id: proc
property string deviceId: ""
command: busctlCall("/modules/kdeconnect/devices/" + deviceId, "org.kde.kdeconnect.device", "unpair")
stdout: StdioCollector {
onStreamFinished: {
KDEConnect.refreshDevices()
proc.destroy()
}
}
}
}
// Wake up Device Component
property Component wakeUpDeviceComponent: Component {
Process {
id: proc
property string deviceId: ""
command: busctlCall("/modules/kdeconnect/devices/" + deviceId + "/remotecontrol", "org.kde.kdeconnect.device.remotecontrol", "sendCommand", [ "a{sv}", "1", "singleclick", "b", "true" ])
stdout: StdioCollector {
onStreamFinished: {
KDEConnect.refreshDevices()
proc.destroy()
}
}
}
}
// Share file component
property Component shareComponent: Component {
Process {
id: proc
property string deviceId: ""
property string filePath: ""
command: busctlCall("/modules/kdeconnect/devices/" + deviceId + "/share", "org.kde.kdeconnect.device.share", "shareUrl", [ "file://" + filePath ])
stdout: StdioCollector {
onStreamFinished: {
proc.destroy()
}
}
}
}
// Periodic refresh timer
property Timer refreshTimer: Timer {
interval: 5000
running: true
repeat: true
onTriggered: root.checkDaemon()
}
}
@@ -0,0 +1,40 @@
pragma Singleton
import QtQuick
QtObject {
function getConnectionStateIcon(device, daemonAvailable) {
if (!daemonAvailable)
return "exclamation-circle";
if (device === null || !device.reachable)
return "device-mobile-off";
if (device.battery >= 0 && device.battery < 10)
return "device-mobile-exclamation"
if (device.notificationIds.length > 0)
return "device-mobile-message";
else if (device.charging)
return "device-mobile-bolt";
else
return "device-mobile";
}
// Returns raw state keys for translation
function getConnectionStateKey(device, daemonAvailable) {
if (!daemonAvailable)
return "control_center.state.unavailable";
if (device === null)
return "control_center.state.no-device";
if (!device.reachable)
return "control_center.state.disconnected";
if (!device.paired)
return "control_center.state.not-paired";
return "control_center.state.connected";
}
}
@@ -0,0 +1,2 @@
singleton KDEConnect 1.0 KDEConnect.qml
singleton KDEConnectUtils 1.0 KDEConnectUtils.qml
+44
View File
@@ -0,0 +1,44 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
property var pluginApi: null
property var cfg: pluginApi?.pluginSettings || ({})
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
property bool hideIfNoDeviceConnected: pluginApi?.mainInstance?.hideIfNoDeviceConnected ?? (pluginApi?.pluginSettings?.hideIfNoDeviceConnected ?? false)
spacing: Style.marginL
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
NToggle {
label: pluginApi?.tr("settings.no-device-connected-hide.label")
description: pluginApi?.tr("settings.no-device-connected-hide.description")
checked: root.hideIfNoDeviceConnected
onToggled: function(checked) {
root.hideIfNoDeviceConnected = checked
}
}
}
function saveSettings() {
if (!pluginApi) {
Logger.e("KDEConnect", "Cannot save settings: pluginApi is null");
return;
}
pluginApi.pluginSettings.hideIfNoDeviceConnected = root.hideIfNoDeviceConnected;
pluginApi.saveSettings();
Logger.d("KDEConnect", "Settings saved successfully");
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"panel": {
"title": "Verbundene Geräte",
"signal": {
"very-weak": "Sehr schwach",
"weak": "Schwach",
"fair": "Okay",
"good": "Gut",
"excellent": "Ausgezeichnet"
},
"unknown": "Unbekannt",
"card": {
"battery": "Akkustand",
"network": "Netzwerk",
"signal-strength": "Signalstärke",
"notifications": "Benachrichtigungen"
},
"other-devices": "Andere Geräte",
"send-file-picker": "Datei zum Senden an Gerät auswählen",
"send-file": "Datei senden",
"browse-files": "Dateien auf Gerät durchsuchen",
"find-device": "Mein Gerät suchen",
"pair": "Mit Gerät koppeln",
"unpair": "Gerät entkoppeln",
"kdeconnect-error": {
"no-devices": "Kein Gerät mit KDE Connect gefunden",
"unavailable-title": "kdeconnectd scheint nicht zu laufen!",
"unavailable-desc": "Sicherstellen, dass die KDE Connect-Anwendung auf Ihrem System installiert ist und dass der kdeconnectd-Daemon gestartet wurde",
"device-unavailable": "Das Gerät ist derzeit nicht verfügbar."
},
"busctl-error": {
"unavailable-title": "busctl kann nicht gefunden werden!",
"unavailable-desc": "Stelle sicher, dass busctl (teil von systemd) auf deinem System installiert ist"
}
},
"bar": {
"tooltip": "Verbundene Geräte"
},
"control_center": {
"state-label": "Status",
"state": {
"connected": "Verbunden",
"disconnected": "Getrennt",
"unavailable": "Nicht verfügbar",
"no-device": "Kein Gerät",
"not-paired": "Nicht gekoppelt"
}
},
"settings": {
"no-device-connected-hide": {
"label": "Verstecken wenn nicht verbunden",
"description": "Verstecke den Knopf in der Leiste, wenn kein Gerät verbunden ist"
}
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"panel": {
"title": "Connected Devices",
"signal": {
"very-weak": "Very Weak",
"weak": "Weak",
"fair": "Fair",
"good": "Good",
"excellent": "Excellent"
},
"unknown": "Unknown",
"card": {
"battery": "Battery",
"network": "Network",
"signal-strength": "Signal Strength",
"notifications": "Notifications"
},
"other-devices": "Other Devices",
"send-file-picker": "Pick file to send to device",
"send-file": "Send File",
"browse-device": "Browse Device Files",
"find-device": "Find my Device",
"pair": "Pair with Device",
"unpair": "Unpair Device",
"kdeconnect-error": {
"no-devices": "No device running KDE Connect found",
"unavailable-title": "kdeconnectd does not seem to be running!",
"unavailable-desc": "Make sure you've installed the KDE Connect Application on your system and that it has started the kdeconnectd daemon",
"device-unavailable": "Device is currently unavailable"
},
"busctl-error": {
"unavailable-title": "busctl cannot be found!",
"unavailable-desc": "Make sure busctl (part of systemd) is installed on your system"
}
},
"bar": {
"tooltip": "Connected Devices"
},
"control_center": {
"state-label": "State",
"state": {
"connected": "Connected",
"disconnected": "Disconnected",
"unavailable": "Unavailable",
"no-device": "No device",
"not-paired": "Not paired"
}
},
"settings": {
"no-device-connected-hide": {
"label": "Hide if unavailable",
"description": "Hide the bar button entirely if no device is connected"
}
}
}
+44
View File
@@ -0,0 +1,44 @@
{
"panel": {
"title": "Appareils connectés",
"signal": {
"very-weak": "Très faible",
"weak": "Faible",
"fair": "Moyen",
"good": "Bon",
"excellent": "Excellent"
},
"unknown": "Inconnu",
"card": {
"battery": "Batterie",
"network": "Réseau",
"signal-strength": "Force du signal",
"notifications": "Notifications"
},
"other-devices": "Autres appareils",
"send-file-picker": "Choisir un fichier à envoyer à l'appareil",
"send-file": "Envoyer un fichier",
"find-device": "Trouver mon appareil",
"pair": "Coupler avec l'appareil",
"unpair": "Découpler l'appareil",
"kdeconnect-error": {
"no-devices": "Aucun appareil exécutant KDE Connect trouvé",
"unavailable-title": "kdeconnectd ne semble pas être en cours d'exécution !",
"unavailable-desc": "Assurez-vous d'avoir installé l'application KDE Connect sur votre système et qu'elle a démarré le démon kdeconnectd",
"device-unavailable": "L'appareil est actuellement indisponible"
}
},
"bar": {
"tooltip": "Appareils connectés"
},
"control_center": {
"state-label": "État",
"state": {
"connected": "Connecté",
"disconnected": "Déconnecté",
"unavailable": "Indisponible",
"no-device": "Aucun appareil",
"not-paired": "Non couplé"
}
}
}
+48
View File
@@ -0,0 +1,48 @@
{
"panel": {
"title": "Dispositivos Conectados",
"signal": {
"very-weak": "Muito Fraco",
"weak": "Fraco",
"fair": "Justo",
"good": "Bom",
"excellent": "Excelente"
},
"unknown": "Desconhecido",
"card": {
"battery": "Bateria",
"network": "Rede",
"signal-strength": "Intensidade do Signal",
"notifications": "Notificações"
},
"other-devices": "Outros Dispositivos",
"send-file-picker": "Selecione o arquivo para enviar ao dispositivo",
"send-file": "Enviar Arquivo",
"find-device": "Encontrar meu Dispositivo",
"pair": "Emparelhar meu Dispositivo",
"unpair": "Desemparelhar meu dispositivo",
"kdeconnect-error": {
"no-devices": "Nenhum dispositivo executando o KDE Connect encontrado",
"unavailable-title": "O kdeconnectd parece não estar em execução!",
"unavailable-desc": "Certifique-se de ter instalado o aplicativo KDE Connect em seu sistema e de que o daemon kdeconnectd esteja em execução",
"device-unavailable": "O dispositivo está indisponível no momento"
},
"busctl-error": {
"unavailable-title": "O busctl não foi encontrado!",
"unavailable-desc": "Certifique-se de que o busctl esteja instalado em seu sistema"
}
},
"bar": {
"tooltip": "Dispositivos conectados"
},
"control_center": {
"state-label": "Estado",
"state": {
"connected": "Conectado",
"disconnected": "Desconectado",
"unavailable": "Indisponível",
"no-device": "Nenhum dispositivo",
"not-paired": "Não pareado"
}
}
}
+49
View File
@@ -0,0 +1,49 @@
{
"panel": {
"title": "Подключённые устройства",
"signal": {
"very-weak": "Очень слабое",
"weak": "Слабое",
"fair": "Среднее",
"good": "Хорошее",
"excellent": "Отличное"
},
"unknown": "Неизвестно",
"card": {
"battery": "Батарея",
"network": "Сеть",
"signal-strength": "Качество сигнала",
"notifications": "Уведомления"
},
"other-devices": "Другие устройства",
"send-file-picker": "Выберите файл для отправки на устройство",
"send-file": "Отправить файл",
"browse-device": "Просмотреть файлы на устройстве",
"find-device": "Найти моё устройство",
"pair": "Сопрячь устройство",
"unpair": "Разорвать сопряжение",
"kdeconnect-error": {
"no-devices": "Устройства с KDE Connect не обнаружены",
"unavailable-title": "Похоже, kdeconnectd не запущен!",
"unavailable-desc": "Убедитесь, что KDE Connect установлен в системе и служба kdeconnectd работает.",
"device-unavailable": "Устройство сейчас недоступно"
},
"busctl-error": {
"unavailable-title": "Не удаётся найти busctl!",
"unavailable-desc": "Убедитесь, что busctl установлен в вашей системе"
}
},
"bar": {
"tooltip": "Подключённые устройства"
},
"control_center": {
"state-label": "Статус",
"state": {
"connected": "Подключено",
"disconnected": "Не подключено",
"unavailable": "Недоступно",
"no-device": "Нет устройства",
"not-paired": "Нет сопряжения"
}
}
}
@@ -0,0 +1,30 @@
{
"id": "kde-connect",
"name": "KDE Connect",
"version": "1.2.1",
"minNoctaliaVersion": "4.4.0",
"author": "WerWolv",
"official": false,
"license": "GPLv2",
"repository": "https://github.com/WerWolv/noctalia-kde-connect",
"description": "A Plugin integrating your mobile devices into a panel using KDEConnect",
"tags": [
"Bar",
"Panel",
"Utility",
"System"
],
"entryPoints": {
"main": "Main.qml",
"barWidget": "BarWidget.qml",
"controlCenterWidget": "ControlCenterWidget.qml",
"panel": "Panel.qml",
"settings": "Settings.qml"
},
"dependencies": {
"plugins": []
},
"metadata": {
"defaultSettings": {}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

+248
View File
@@ -0,0 +1,248 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
import qs.Services.UI
Item {
id: root
property var pluginApi: null
property ShellScreen screen
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
readonly property var mainInstance: pluginApi?.mainInstance
readonly property string screenName: screen?.name ?? ""
readonly property string resolvedBarPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: resolvedBarPosition === "left" || resolvedBarPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
// Settings tie-ins
readonly property real catSize: mainInstance?.catSize ?? 1.0
readonly property real catOffsetY: mainInstance?.catOffsetY ?? 0.0
readonly property real widthPadding: pluginApi?.pluginSettings?.widthPadding ?? 0.2
// Orientation-aware cat sizing (both driven by the catSize slider)
readonly property real catSizeHorizontal: catSize
readonly property real catSizeVertical: catSize * 0.50
readonly property real activeCatSize: isBarVertical ? catSizeVertical : catSizeHorizontal
// Glyph map: b = left paw up, d = left paw down, c = right paw up, a = right paw down, e+f = sleep, g+h = blink
readonly property var glyphMap: ["bc", "dc", "ba", "da"] // [idle, leftSlap, rightSlap, bothSlap]
readonly property string sleepGlyph: "ef"
readonly property string blinkGlyph: "gh"
readonly property int catState: mainInstance?.catState ?? 0
readonly property bool paused: mainInstance?.paused ?? false
readonly property bool waiting: mainInstance?.waiting ?? false
readonly property string catColorKey: mainInstance?.catColor ?? "default"
readonly property bool blinking: mainInstance?.blinking ?? false
readonly property bool showRainbowColor: mainInstance?.showRainbowColor ?? false
readonly property string rainbowColor: mainInstance?.currentRainbowColor ?? "#ff0000"
function resolveColor(key) {
switch (key) {
case "primary": return Color.mPrimary
case "secondary": return Color.mSecondary
case "tertiary": return Color.mTertiary
case "error": return Color.mError
default: return Color.mOnSurface
}
}
readonly property color resolvedCatColor: showRainbowColor ? rainbowColor : resolveColor(catColorKey)
// Sizing: capsule dimensions drive implicit size
readonly property real horizontalPadding: capsuleHeight * widthPadding
readonly property real contentWidth: isBarVertical
? capsuleHeight
: catText.implicitWidth + horizontalPadding + (paused ? pauseExpandAmount : 0)
readonly property real contentHeight: isBarVertical
? catText.implicitHeight + horizontalPadding + (paused ? pauseExpandAmount : 0)
: capsuleHeight
// Pause indicator expand/slide
readonly property real pauseIconSize: capsuleHeight * 0.45
readonly property real pauseExpandAmount: capsuleHeight * 0.8
readonly property real pauseSlideOffset: paused ? -pauseExpandAmount / 2 : 0
implicitWidth: contentWidth
implicitHeight: contentHeight
FontLoader {
id: bongoFont
source: pluginApi ? pluginApi.pluginDir + "/bongocat-Regular.otf" : ""
}
Rectangle {
id: visualCapsule
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
width: root.contentWidth
height: root.contentHeight
radius: Style.radiusL
Behavior on width {
NumberAnimation { duration: Style.animationNormal; easing.type: Easing.OutCubic }
}
Behavior on height {
NumberAnimation { duration: Style.animationNormal; easing.type: Easing.OutCubic }
}
color: mouseArea.containsMouse ? Color.mHover : (root.paused ? root.resolvedCatColor : Style.capsuleColor)
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
Behavior on color {
ColorAnimation { duration: Style.animationNormal; easing.type: Easing.OutCubic }
}
Text {
id: catText
anchors.centerIn: parent
anchors.horizontalCenterOffset: root.isBarVertical ? 0 : root.pauseSlideOffset
anchors.verticalCenterOffset: root.capsuleHeight * root.catOffsetY + (root.isBarVertical ? root.pauseSlideOffset : 0)
font.family: bongoFont.name
font.pixelSize: root.capsuleHeight * root.activeCatSize
font.weight: Font.Thin
color: mouseArea.containsMouse ? Color.mOnHover : (root.paused ? Color.mSurface : root.resolvedCatColor)
text: (root.paused || root.waiting) ? root.sleepGlyph : (root.blinking ? root.blinkGlyph : (root.glyphMap[root.catState] ?? "bc"))
Behavior on anchors.horizontalCenterOffset {
NumberAnimation { duration: Style.animationNormal; easing.type: Easing.OutCubic }
}
Behavior on anchors.verticalCenterOffset {
NumberAnimation { duration: Style.animationNormal; easing.type: Easing.OutCubic }
}
}
NIcon {
id: pauseIcon
icon: "player-pause-filled"
pointSize: root.pauseIconSize
applyUiScale: false
color: catText.color
opacity: root.paused ? 1 : 0
x: root.isBarVertical
? (parent.width - width) / 2
: parent.width - (root.pauseExpandAmount + width) / 2 - root.capsuleHeight * 0.20
y: root.isBarVertical
? parent.height - (root.pauseExpandAmount + height) / 2 - root.capsuleHeight * 0.10
: (parent.height - height) / 2
Behavior on opacity {
NumberAnimation { duration: Style.animationFast; easing.type: Easing.OutCubic }
}
}
Repeater {
id: zzzRepeater
property real catFontSize: catText.font.pixelSize
property color catFontColor: catText.color
property real catX: catText.x
property real catY: catText.y
property real catW: catText.width
property bool sleeping: root.paused || root.waiting
readonly property real baseScale: 0.28
readonly property real scaleStep: 0.07
readonly property real xOrigin: 0.55
readonly property real xSpacing: 0.18
readonly property real floatHeight: 0.7
readonly property int staggerDelay: 500
readonly property int floatDuration: 1800
readonly property int fadeInDuration: 300
readonly property int fadeOutDuration: 1500
model: 3
delegate: Text {
id: zItem
required property int index
text: "z"
font.pixelSize: zzzRepeater.catFontSize * (zzzRepeater.baseScale + index * zzzRepeater.scaleStep)
font.weight: Font.Bold
color: zzzRepeater.catFontColor
visible: zzzRepeater.sleeping
opacity: 0
x: zzzRepeater.catX + zzzRepeater.catW * zzzRepeater.xOrigin + index * zzzRepeater.catFontSize * zzzRepeater.xSpacing
y: zzzRepeater.catY
SequentialAnimation {
id: zAnim
running: zzzRepeater.sleeping
loops: Animation.Infinite
PauseAnimation { duration: zItem.index * zzzRepeater.staggerDelay }
ParallelAnimation {
NumberAnimation {
target: zItem; property: "y"
from: zzzRepeater.catY
to: zzzRepeater.catY - zzzRepeater.catFontSize * zzzRepeater.floatHeight
duration: zzzRepeater.floatDuration
easing.type: Easing.OutQuad
}
SequentialAnimation {
NumberAnimation {
target: zItem; property: "opacity"
from: 0; to: 1
duration: zzzRepeater.fadeInDuration
}
NumberAnimation {
target: zItem; property: "opacity"
from: 1; to: 0
duration: zzzRepeater.fadeOutDuration
easing.type: Easing.InQuad
}
}
}
}
onVisibleChanged: {
if (!visible) {
zAnim.stop();
opacity = 0;
y = zzzRepeater.catY;
}
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
PanelService.showContextMenu(contextMenu, root, screen);
} else if (root.mainInstance) {
root.mainInstance.paused = !root.mainInstance.paused;
}
}
}
NPopupContextMenu {
id: contextMenu
model: [{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
}]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openPluginSettings(screen, pluginApi.manifest);
}
}
}
}
+441
View File
@@ -0,0 +1,441 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.Media
import qs.Services.UI
Item {
id: root
// === EXTERNAL API ===
property var pluginApi: null
// === CORE STATE ===
property int catState: 0 // 0 = idle (both paws up), 1 = left slap, 2 = right slap, 3 = both slap
property bool leftWasLast: false // Track which paw slapped last to alternate
property bool paused: false
property bool waiting: false
property bool blinking: false
property int pendingCatState: 0 // State to transition to after reset delay
// === INSTANCE IDENTIFICATION ===
readonly property string spectrumInstanceId: "plugin:slowbongo:" + Date.now() + Math.random()
// === INPUT DEVICES (from settings) ===
readonly property var inputDevices: {
const saved = pluginApi?.pluginSettings?.inputDevices;
if (saved && saved.length > 0) return saved;
return [];
}
onPluginApiChanged: {
if (pluginApi) {
SpectrumService.registerComponent(spectrumInstanceId);
Logger.i("SlowBongo", "Registered with SpectrumService for audio detection");
}
}
Component.onDestruction: SpectrumService.unregisterComponent(spectrumInstanceId)
// === IPC CONTROL ===
IpcHandler {
target: "plugin:slowbongo"
function pause() {
root.paused = true
}
function resume() {
root.paused = false
}
function toggle() {
root.paused = !root.paused
}
}
// === SETTINGS ===
readonly property int idleTimeout: pluginApi?.pluginSettings?.idleTimeout ?? 250
readonly property int waitingTimeout: pluginApi?.pluginSettings?.waitingTimeout ?? 5000
readonly property string catColor: pluginApi?.pluginSettings?.catColor ?? "default"
readonly property real catSize: pluginApi?.pluginSettings?.catSize ?? 1.0
readonly property real catOffsetY: pluginApi?.pluginSettings?.catOffsetY ?? 0.0
readonly property bool raveMode: pluginApi?.pluginSettings?.raveMode ?? false
readonly property bool tappyMode: pluginApi?.pluginSettings?.tappyMode ?? false
readonly property bool useMprisFilter: pluginApi?.pluginSettings?.useMprisFilter ?? false
// === AUDIO REACTIVE STATE ===
readonly property bool anyMusicPlaying: !SpectrumService.isIdle
property int rainbowIndex: 0
readonly property var rainbowColors: ['#aa0000', '#b65c02', '#bb9c14', '#00a100', '#01019b', '#37005c', '#6a0196']
property real audioIntensity: 0
property real smoothedIntensity: 0
property real previousIntensity: 0
property real bassIntensity: 0
readonly property real beatThreshold: 0.07
readonly property real bigBeatThreshold: 0.67
readonly property real beatDeltaThreshold: 0.014 // Minimum sudden increase to count as beat
property bool isFlashing: false
// === COMPUTED MODE FLAGS ===
readonly property bool mprisAllowed: !useMprisFilter || MediaService.isPlaying
readonly property bool useTappyMode: tappyMode && anyMusicPlaying && mprisAllowed
readonly property string currentRainbowColor: rainbowColors[rainbowIndex]
readonly property bool useRaveColors: raveMode && anyMusicPlaying && mprisAllowed
readonly property bool showRainbowColor: useRaveColors && isFlashing
// === AUDIO REACTIVE CONNECTIONS ===
Connections {
target: SpectrumService
function onValuesChanged() {
if (root.paused) return;
if (!root.useRaveColors && !root.useTappyMode) return;
if (!SpectrumService.values || SpectrumService.values.length === 0) {
root.audioIntensity = 0;
return;
}
const subBassCount = Math.min(4, SpectrumService.values.length);
const bassCount = Math.min(8, SpectrumService.values.length);
const midCount = Math.min(16, SpectrumService.values.length);
let subBassSum = 0;
for (let i = 0; i < subBassCount; i++) {
subBassSum += SpectrumService.values[i] || 0;
}
let bassSum = 0;
for (let i = 0; i < bassCount; i++) {
bassSum += SpectrumService.values[i] || 0;
}
let midSum = 0;
for (let i = 8; i < midCount; i++) {
midSum += SpectrumService.values[i] || 0;
}
const subBassAvg = subBassSum / subBassCount;
const bassAvg = bassSum / bassCount;
const midAvg = midSum / Math.max(1, midCount - 8);
root.bassIntensity = subBassAvg;
root.audioIntensity = (bassAvg * 0.8) + (midAvg * 0.6);
const alpha = 0.75;
root.previousIntensity = root.smoothedIntensity;
root.smoothedIntensity = alpha * root.audioIntensity + (1 - alpha) * root.smoothedIntensity;
const intensityDelta = root.smoothedIntensity - root.previousIntensity;
const isBeat = (intensityDelta > root.beatDeltaThreshold && root.smoothedIntensity > root.beatThreshold * 0.5)
|| (root.smoothedIntensity > root.beatThreshold && intensityDelta > 0);
if (isBeat && !beatCooldownTimer.running) {
if (root.useRaveColors) {
root.rainbowIndex = (root.rainbowIndex + 1) % root.rainbowColors.length;
root.isFlashing = true;
flashTimer.restart();
}
if (root.useTappyMode) {
root.musicEvent(root.bassIntensity > root.bigBeatThreshold);
}
beatCooldownTimer.restart();
}
}
}
// === TIMERS ===
Timer {
id: beatCooldownTimer
interval: 70
repeat: false
}
Timer {
id: flashTimer
interval: 100
repeat: false
onTriggered: root.isFlashing = false
}
Timer {
id: stateResetTimer
interval: 40
repeat: false
onTriggered: root.catState = root.pendingCatState
}
// === MUSIC HANDLER ===
function musicEvent(isBigHit = false) {
if (root.paused) return;
root.waiting = false;
let targetState;
if (isBigHit) {
targetState = 3;
} else {
root.leftWasLast = !root.leftWasLast;
targetState = root.leftWasLast ? 1 : 2;
}
const needsReset = root.catState !== 0 && ((isBigHit && root.catState !== 3) || (!isBigHit && root.catState === 3));
if (needsReset) {
root.catState = 0;
root.pendingCatState = targetState;
stateResetTimer.restart();
} else {
root.catState = targetState;
}
idleTimer.restart();
waitingTimer.restart();
}
// === KEY PRESS HANDLER ===
function onKeyPress(isBigHit = false) {
if (root.paused) return;
root.waiting = false;
let targetState;
if (isBigHit) {
targetState = 3;
} else {
if (root.catState !== 0){
targetState = 3;
} else {
root.leftWasLast = !root.leftWasLast;
targetState = root.leftWasLast ? 1 : 2;
}
}
root.catState = targetState;
waitingTimer.restart();
}
function onKeyRelease(isBigHit = false) {
if (root.paused) return;
root.waiting = false;
let targetState;
if (isBigHit) {
targetState = 0;
} else {
if (root.catState === 3){
targetState = root.leftWasLast ? 1 : 2;
} else {
targetState = 0;
}
}
root.catState = targetState;
waitingTimer.restart();
}
function onKeyRepeat(isBigHit = false){
if (root.paused) return;
root.waiting = false;
let targetState;
if (root.catState !== 0) {
targetState = root.catState;
} else {
if (isBigHit){
targetState = 3;
} else {
targetState = root.leftWasLast ? 1 : 2;
}
}
root.catState = targetState;
waitingTimer.restart();
}
// === STATE CHANGE HANDLERS ===
onPausedChanged: {
if (root.paused) {
idleTimer.stop();
waitingTimer.stop();
root.waiting = false;
root.blinking = false;
root.catState = 0;
} else {
waitingTimer.restart();
}
}
onWaitingChanged: {
if (root.waiting) {
root.blinking = false;
}
}
// === IDLE & WAITING TIMERS ===
Timer {
id: idleTimer
interval: root.idleTimeout
repeat: false
onTriggered: root.catState = 0
}
Timer {
id: waitingTimer
interval: root.waitingTimeout
repeat: false
onTriggered: root.waiting = true
}
// === BLINK ANIMATION ===
Timer {
id: blinkIntervalTimer
interval: 6000 + Math.random() * 8000
repeat: true
running: !root.paused && !root.waiting
onTriggered: {
interval = 6000 + Math.random() * 8000;
if (Math.random() < 0.5) {
root.blinking = true;
blinkDurationTimer.start();
} else {
root.blinkFlutterCount = 0;
root.blinking = true;
flutterTimer.start();
}
}
}
property int blinkFlutterCount: 0
Timer {
id: blinkDurationTimer
interval: 450
repeat: false
onTriggered: root.blinking = false
}
Timer {
id: flutterTimer
interval: 120
repeat: false
onTriggered: {
root.blinkFlutterCount++;
root.blinking = !root.blinking;
if (root.blinkFlutterCount < 4) {
flutterTimer.start();
} else {
root.blinking = false;
}
}
}
// === INPUT DEVICE MONITORING ===
Repeater {
model: root.inputDevices
Item {
id: deviceMonitor
required property string modelData
property int retryCount: 0
property bool hasNotified: false
readonly property var retryIntervals: [30000, 90000, 300000] // 30s, 1:30, 5min
Process {
id: evtestProc
command: ["evtest", deviceMonitor.modelData]
running: true
stdout: SplitParser {
onRead: data => {
if (data.includes("EV_KEY")) {
// Detect spacebar/enter for double slap (both paws)
const isBigHit = data.includes("KEY_SPACE") || data.includes("KEY_ENTER");
// Key pressed
// Ignore BTN_TOOL_ events to avoid double events with touchpads
if (data.includes("value 1") && !data.includes("BTN_TOOL_")){
root.onKeyPress(isBigHit);
// Key released
} else if (data.includes("value 0")) {
root.onKeyRelease(isBigHit);
// Key repeat
} else if (data.includes("value 2")) {
root.onKeyRepeat(isBigHit);
}
}
}
}
stderr: StdioCollector {}
onRunningChanged: {
if (running) {
deviceMonitor.retryCount = 0;
deviceMonitor.hasNotified = false;
}
}
onExited: exitCode => {
Logger.w("Slow Bongo", "evtest (" + deviceMonitor.modelData + ") exited with code " + exitCode);
if (exitCode !== 0) {
deviceMonitor.retryCount++;
if (!deviceMonitor.hasNotified) {
ToastService.showWarning(
root.pluginApi?.tr("toast.evtest-error") ?? "SlowBongo",
root.pluginApi?.tr("toast.device-disconnected") ?? ("Device disconnected: " + deviceMonitor.modelData)
);
deviceMonitor.hasNotified = true;
}
if (deviceMonitor.retryCount <= deviceMonitor.retryIntervals.length) {
const interval = deviceMonitor.retryIntervals[deviceMonitor.retryCount - 1];
Logger.i("Slow Bongo", "Will retry in " + Math.floor(interval / 1000) + "s (attempt " + deviceMonitor.retryCount + "/" + deviceMonitor.retryIntervals.length + ")");
restartTimer.interval = interval;
restartTimer.start();
} else {
Logger.w("Slow Bongo", "Max retries reached for device: " + deviceMonitor.modelData + ". Giving up.");
ToastService.showInfo(
root.pluginApi?.tr("toast.device-gave-up") ?? "SlowBongo",
root.pluginApi?.tr("toast.device-gave-up-desc") ?? ("Stopped trying to reconnect to: " + deviceMonitor.modelData)
);
}
} else {
restartTimer.interval = deviceMonitor.retryIntervals[0];
restartTimer.start();
}
}
}
Timer {
id: restartTimer
repeat: false
onTriggered: deviceCheckProc.running = true
}
Process {
id: deviceCheckProc
command: ["test", "-e", deviceMonitor.modelData]
running: false
onExited: exitCode => {
if (exitCode === 0) {
Logger.i("Slow Bongo", "Device detected, restarting monitoring: " + deviceMonitor.modelData);
evtestProc.running = true;
} else if (deviceMonitor.retryCount < deviceMonitor.retryIntervals.length) {
deviceMonitor.retryCount++;;
const interval = deviceMonitor.retryIntervals[deviceMonitor.retryCount - 1];
Logger.i("Slow Bongo", "Device " + deviceMonitor.modelData + " not found, will retry in " + Math.floor(interval / 1000) + "s (attempt " + deviceMonitor.retryCount + "/" + deviceMonitor.retryIntervals.length + ")");
restartTimer.interval = interval;
restartTimer.start();
} else {
Logger.w("Slow Bongo", "Max retries reached for device: " + deviceMonitor.modelData + ". Giving up.");
}
}
}
}
}
}
+106
View File
@@ -0,0 +1,106 @@
# Slow Bongo
![Picture of a cute lil bongocatto](https://raw.githubusercontent.com/tuibird/slowbongo/refs/heads/main/slowbongo.png)
A bongo cat that sits in your bar and slaps when you type. This is very early days, there will be bugs.
## Features
- **Bar Widget**: Compact widget that fits seamlessly in your Noctalia bar
- **Keyboard Reactive**: Cat taps its paws in alternation when you type
- **Audio Reactive**: Optional rave mode and tappy mode that react to music
- **Easy pause**: Can quickly pause and un-pause reactivity with a single left click.
- **Customizable Appearance**: Choose from multiple color schemes and adjust size
- **Font-Based Animation**: Uses a bongo cat font for easy rendering
- **Bar Widget**: Compact widget that fits seamlessly in your Noctalia bar
## Installation
1. Navigate to the Noctalia settings plugins section.
2. Enter the sources sub-menu.
3. Add Slow Bongo as a custom repository.
```bash
https://github.com/tuibird/slowbongo.git
```
4. Open the Noctalia plugins store and enable **Slow Bongo**.
## Configuration
The plugin offers several customization options available in the settings panel:
### Input Devices
The plugin automatically detects keyboard input devices on first run. You can manually select which input devices to monitor from the settings panel.
### Colors
The colours are all pulled from your current Noctalia colourscheme.
### Rave Mode
When enabled, the cat changes colors to the beat when music is playing.
### Tappy Mode
When enabled, the cat taps along to the beat when music is playing instead of only reacting to keyboard input.
### Size and Position
- **Cat Size**: Scale the cat from 50% to 150% of default size
- **Vertical Position**: Fine-tune the cat's vertical alignment in the bar
## Requirements
### Essential
- **evtest**: Required for keyboard input detection
```bash
# Fedora/RHEL
sudo dnf install evtest
# Ubuntu/Debian
sudo apt install evtest
# Arch
sudo pacman -S evtest
```
- **Input group membership**: Your user must be in the `input` group to read keyboard events
```bash
sudo usermod -a -G input $USER
```
Restart for the group change to take effect.
## Troubleshooting
### Cat not responding to keyboard input
1. Check that `evtest` is installed:
```bash
which evtest
```
2. Verify you're in the `input` group:
```bash
id -nG | grep input
```
3. Make sure at least one input device is selected in the settings panel.
## Technical Details
- Uses `evtest` to monitor keyboard events from `/dev/input/event*` devices
- Integrates with Noctalia's SpectrumService for audio visualization
- Custom font file (`bongocatfont.woff`) contains the cat animations
- Alternates between left (1) and right (2) paw animations, returning to idle (0) after configurable timeout
## License
MIT
## Credits
- Thank you to [Kitgore](https://github.com/kitgore) for the inital bongo cat font
- Noctalia plugins for the amazing guides/examples
+455
View File
@@ -0,0 +1,455 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
property var pluginApi: null
// Requirement check states
property bool evtestInstalled: false
property bool inInputGroup: false
property string currentUser: ""
// Editable settings properties
property string editCatColor: {
let saved = pluginApi?.pluginSettings?.catColor
if (saved && saved.length > 0) return saved
return pluginApi?.manifest?.metadata?.defaultSettings?.catColor ?? "none"
}
property real editCatSize: {
let saved = pluginApi?.pluginSettings?.catSize
if (saved !== undefined && saved !== null) return saved
return pluginApi?.manifest?.metadata?.defaultSettings?.catSize ?? 1.0
}
property real editCatOffsetY: {
let saved = pluginApi?.pluginSettings?.catOffsetY
if (saved !== undefined && saved !== null) return saved
return pluginApi?.manifest?.metadata?.defaultSettings?.catOffsetY ?? 0.11
}
property var editInputDevices: {
let saved = pluginApi?.pluginSettings?.inputDevices
if (saved && saved.length > 0) return saved
let legacy = pluginApi?.pluginSettings?.inputDevice
?? pluginApi?.manifest?.metadata?.defaultSettings?.inputDevice
return legacy ? [legacy] : []
}
property bool editRaveMode: {
let saved = pluginApi?.pluginSettings?.raveMode
if (saved !== undefined && saved !== null) return saved
return pluginApi?.manifest?.metadata?.defaultSettings?.raveMode ?? false
}
property bool editTappyMode: {
let saved = pluginApi?.pluginSettings?.tappyMode
if (saved !== undefined && saved !== null) return saved
return pluginApi?.manifest?.metadata?.defaultSettings?.tappyMode ?? false
}
property bool editUseMprisFilter: {
let saved = pluginApi?.pluginSettings?.useMprisFilter
if (saved !== undefined && saved !== null) return saved
return pluginApi?.manifest?.metadata?.defaultSettings?.useMprisFilter ?? false
}
// Status colors (with fallback for theme compatibility)
readonly property color statusSuccessColor: Color.mPrimary
readonly property color statusErrorColor: Color.mError ?? "#c00202"
property var inputDevices: []
function isSelected(key) {
return root.editInputDevices.indexOf(key) >= 0
}
function toggleDevice(key) {
let list = root.editInputDevices.slice()
let idx = list.indexOf(key)
if (idx >= 0)
list.splice(idx, 1)
else
list.push(key)
root.editInputDevices = list
}
Component.onCompleted: {
evtestCheck.running = true
userCheck.running = true
byIdListProcess.running = true
}
Process {
id: evtestCheck
command: ["which", "evtest"]
stdout: StdioCollector {}
stderr: StdioCollector {}
onExited: function(exitCode, exitStatus) {
root.evtestInstalled = (exitCode == 0)
}
}
Process {
id: userCheck
command: ["id", "-un"]
stdout: SplitParser {
onRead: data => {
root.currentUser = data.trim()
}
}
stderr: StdioCollector {}
onExited: function(exitCode, exitStatus) {
if (exitCode == 0 && root.currentUser.length > 0)
groupCheck.running = true
}
}
Process {
id: groupCheck
command: ["sh", "-c", "id -nG '" + root.currentUser + "' | tr ' ' '\\n' | grep -qx input"]
stdout: StdioCollector {}
stderr: StdioCollector {}
onExited: function(exitCode, exitStatus) {
root.inInputGroup = (exitCode == 0)
}
}
// Try by-id first
Process {
id: byIdListProcess
command: ["sh", "-c", "[ -d /dev/input/by-id ] && for f in /dev/input/by-id/*-event-*; do [ -e \"$f\" ] && echo \"$(basename \"$f\")|$(readlink -f \"$f\")\"; done || true"]
stdout: SplitParser {
onRead: data => {
const line = data.trim()
if (line.length === 0) return
const parts = line.split("|")
if (parts.length !== 2) return
const name = parts[0]
const resolved = parts[1]
if (!resolved.startsWith("/dev/input/event")) return
const eventNum = resolved.replace(/.*\//, "")
let friendly = name
.replace(/^usb-/, "")
.replace(/-event-\w+$/, "")
.replace(/-if\d+$/, "")
.replace(/_/g, " ")
root.inputDevices = root.inputDevices.concat([{
key: resolved,
name: friendly,
eventDev: eventNum
}])
}
}
stderr: StdioCollector {}
onExited: function(exitCode, exitStatus) {
// Always try to get names from sysfs
sysfsListProcess.running = true
}
}
// Get device names from sysfs
Process {
id: sysfsListProcess
command: ["sh", "-c", "for f in /dev/input/event*; do [ -c \"$f\" ] && echo \"$f|$(cat /sys/class/input/$(basename $f)/device/name 2>/dev/null || basename $f)\"; done"]
running: false
stdout: SplitParser {
onRead: data => {
const line = data.trim()
if (line.length === 0) return
const parts = line.split("|")
if (parts.length !== 2) return
const device = parts[0]
const name = parts[1]
const eventNum = device.replace(/.*\//, "")
// Filter out non-keyboardy devices
const nameLower = name.toLowerCase()
const excludePatterns = [
/power button/i,
/sleep button/i,
/lid switch/i,
/video bus/i,
/audio/i,
/hdmi/i,
/speaker/i,
/headphone/i,
/mic\b/i
]
const shouldExclude = excludePatterns.some(pattern => pattern.test(name))
if (shouldExclude) return
// Check if we already have this device from by-id
const exists = root.inputDevices.some(d => d.key === device)
if (!exists) {
root.inputDevices = root.inputDevices.concat([{
key: device,
name: name,
eventDev: eventNum
}])
}
}
}
stderr: StdioCollector {}
}
// Requirements Section
Text {
text: pluginApi?.tr("settings.requirements")
color: Color.mOnSurface
font.pointSize: Style.fontSizeM
font.weight: Font.DemiBold
}
NBox {
Layout.fillWidth: true
implicitHeight: reqContent.implicitHeight + Style.marginM * 2
ColumnLayout {
id: reqContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginM
spacing: Style.marginS
RowLayout {
spacing: Style.marginS
NIcon {
icon: root.evtestInstalled ? "circle-check-filled" : "circle-x-filled"
color: root.evtestInstalled ? root.statusSuccessColor : root.statusErrorColor
pointSize: Style.fontSizeM
}
Text {
text: root.evtestInstalled
? pluginApi?.tr("settings.evtest-installed")
: pluginApi?.tr("settings.evtest-not-installed")
color: root.evtestInstalled ? root.statusSuccessColor : root.statusErrorColor
font.pointSize: Style.fontSizeM
}
}
RowLayout {
spacing: Style.marginS
NIcon {
icon: root.inInputGroup ? "circle-check-filled" : "circle-x-filled"
color: root.inInputGroup ? root.statusSuccessColor : root.statusErrorColor
pointSize: Style.fontSizeM
}
Text {
text: root.inInputGroup
? pluginApi?.tr("settings.in-input-group")
: pluginApi?.tr("settings.not-in-input-group")
color: root.inInputGroup ? root.statusSuccessColor : root.statusErrorColor
font.pointSize: Style.fontSizeM
}
}
}
}
NDivider {
Layout.fillWidth: true
}
// Widget Color
NColorChoice {
label: pluginApi?.tr("settings.colours")
currentKey: root.editCatColor
onSelected: key => { root.editCatColor = key; }
}
// Cat Size Section
NValueSlider {
Layout.fillWidth: true
label: pluginApi?.tr("settings.cat-size")
value: root.editCatSize
from: 0.5
to: 1.5
stepSize: 0.01
defaultValue: 1.0
showReset: true
text: Math.round(root.editCatSize * 100) + "%"
onMoved: value => root.editCatSize = value
}
// Vertical Position Section
NValueSlider {
Layout.fillWidth: true
label: pluginApi?.tr("settings.vertical-position")
value: root.editCatOffsetY
from: -0.39
to: 0.61
stepSize: 0.01
defaultValue: 0.11
showReset: true
text: { let v = Math.round(-(root.editCatOffsetY - 0.11) * 100) / 100; return (v > 0 ? "+" : "") + v.toFixed(2) }
onMoved: value => root.editCatOffsetY = value
}
// Rave Mode
NToggle {
label: pluginApi?.tr("settings.rave-mode")
description: pluginApi?.tr("settings.rave-mode-desc")
checked: root.editRaveMode
onToggled: checked => root.editRaveMode = checked
defaultValue: pluginApi?.manifest?.metadata?.defaultSettings?.raveMode ?? false
}
// Tappy Mode
NToggle {
label: pluginApi?.tr("settings.tappy-mode")
description: pluginApi?.tr("settings.tappy-mode-desc")
checked: root.editTappyMode
onToggled: checked => root.editTappyMode = checked
defaultValue: pluginApi?.manifest?.metadata?.defaultSettings?.tappyMode ?? false
}
// MPRIS Filtering
NToggle {
label: pluginApi?.tr("settings.mpris-filter")
description: pluginApi?.tr("settings.mpris-filter-desc")
checked: root.editUseMprisFilter
onToggled: checked => root.editUseMprisFilter = checked
defaultValue: pluginApi?.manifest?.metadata?.defaultSettings?.useMprisFilter ?? false
}
NDivider {
Layout.fillWidth: true
}
// Input Devices Section
Text {
text: pluginApi?.tr("settings.input-devices") || "Input Devices"
color: Color.mOnSurface
font.pointSize: Style.fontSizeM
font.weight: Font.DemiBold
}
NBox {
Layout.fillWidth: true
implicitHeight: devContent.implicitHeight + Style.marginM * 2
ColumnLayout {
id: devContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginM
spacing: Style.marginS
Repeater {
model: root.inputDevices
Rectangle {
required property var modelData
property bool isChecked: root.isSelected(modelData.key)
property bool isHovered: mouseArea.containsMouse
Layout.fillWidth: true
implicitHeight: rowContent.implicitHeight + Style.marginS * 2
radius: Style.iRadiusXS
color: isHovered ? Color.mSurfaceVariant : "transparent"
Behavior on color {
ColorAnimation { duration: Style.animationFast }
}
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: root.toggleDevice(modelData.key)
}
RowLayout {
id: rowContent
anchors.fill: parent
anchors.leftMargin: Style.marginS
anchors.rightMargin: Style.marginS
spacing: Style.marginM
Rectangle {
id: checkBox
implicitWidth: Math.round(Style.baseWidgetSize * 0.7)
implicitHeight: Math.round(Style.baseWidgetSize * 0.7)
radius: Style.iRadiusXS
color: parent.parent.isChecked ? Color.mPrimary : Color.mSurface
border.color: parent.parent.isHovered ? Color.mPrimary : Color.mOutline
border.width: Style.borderS
Behavior on color {
ColorAnimation { duration: Style.animationFast }
}
Behavior on border.color {
ColorAnimation { duration: Style.animationFast }
}
NIcon {
visible: parent.parent.parent.isChecked
anchors.centerIn: parent
anchors.horizontalCenterOffset: -1
icon: "check"
color: Color.mOnPrimary
pointSize: Math.max(Style.fontSizeXS, checkBox.implicitWidth * 0.5)
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Text {
text: parent.parent.parent.modelData.name
color: Color.mOnSurface
font.pointSize: Style.fontSizeM
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: parent.parent.parent.modelData.eventDev
color: Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS
visible: text !== ""
elide: Text.ElideRight
Layout.fillWidth: true
}
}
}
}
}
}
}
function saveSettings() {
if (!pluginApi) {
Logger.e("Slow Bongo", "Cannot save settings: pluginApi is null")
return
}
pluginApi.pluginSettings.inputDevices = root.editInputDevices
pluginApi.pluginSettings.catColor = root.editCatColor
pluginApi.pluginSettings.catSize = root.editCatSize
pluginApi.pluginSettings.catOffsetY = root.editCatOffsetY
pluginApi.pluginSettings.raveMode = root.editRaveMode
pluginApi.pluginSettings.tappyMode = root.editTappyMode
pluginApi.pluginSettings.useMprisFilter = root.editUseMprisFilter
pluginApi.saveSettings()
Logger.i("Slow Bongo", "Settings saved successfully")
}
}
Binary file not shown.
+36
View File
@@ -0,0 +1,36 @@
{
"description": "Eine Bongo-Katze, die in deiner Bar sitzt und klopft, wenn du tippst",
"settings": {
"requirements": "Voraussetzungen",
"input-devices": "Eingabegeräte",
"colours": "Farben",
"cat-size": "Katzengröße",
"vertical-position": "Vertikale Position",
"size-label": "Größe:",
"y-offset-label": "Y-Versatz:",
"rave-mode": "Rave Modus",
"rave-mode-desc": "Ändere die Farben im Takt der Musik, wenn Musik abgespielt wird",
"tappy-mode": "Tappy Modus",
"tappy-mode-desc": "Lass die Katze im Takt der Musik mit den Pfoten klopfen",
"mpris-filter": "MPRIS Filterung",
"mpris-filter-desc": "Reagiert nur auf Audio, wenn ein nicht auf der Blacklist stehender Mediaplayer abgespielt wird (verwendet die Noctalia-Shell Audio Blacklist)",
"evtest-installed": "evtest ist installiert",
"evtest-not-installed": "evtest ist nicht installiert",
"in-input-group": "Benutzer ist in der input Gruppe",
"not-in-input-group": "Benutzer ist nicht in der input Gruppe"
},
"colors": {
"default": "Standard",
"primary": "Primär",
"secondary": "Sekundär",
"tertiary": "Teritär"
},
"toast": {
"auto-detect-success": "SlowBongo",
"auto-detect-success-desc": "Automatisch erkannte Tastaturen",
"auto-detect-failed": "SlowBongo",
"auto-detect-failed-desc": "Tastaturen konnten nicht erkannt werden. Bitte manuell in den Einstellungen konfigurieren.",
"evtest-error": "SlowBongo",
"evtest-error-desc": "Die Tastaturüberwachung wurde unerwartet beendet. Neustart..."
}
}
+36
View File
@@ -0,0 +1,36 @@
{
"description": "A bongo cat that sits in your bar and slaps when you type.",
"settings": {
"requirements": "Requirements",
"input-devices": "Input Devices",
"colours": "Colours",
"cat-size": "Cat Size",
"vertical-position": "Vertical Position",
"size-label": "Size:",
"y-offset-label": "Y Offset:",
"rave-mode": "Rave Mode",
"rave-mode-desc": "Change colors to the beat when music is playing",
"tappy-mode": "Tappy Mode",
"tappy-mode-desc": "Make the cat tap along to the beat when music is playing",
"mpris-filter": "MPRIS Filtering",
"mpris-filter-desc": "Only react to audio when a non-blacklisted media player is playing (uses Noctalia Shell audio blacklist)",
"evtest-installed": "evtest is installed",
"evtest-not-installed": "evtest is not installed",
"in-input-group": "User is in the input group",
"not-in-input-group": "User is not in the input group"
},
"colors": {
"default": "Default",
"primary": "Primary",
"secondary": "Secondary",
"tertiary": "Tertiary"
},
"toast": {
"auto-detect-success": "SlowBongo",
"auto-detect-success-desc": "Auto-detected keyboard devices",
"auto-detect-failed": "SlowBongo",
"auto-detect-failed-desc": "Could not detect keyboard devices. Please configure manually in settings.",
"evtest-error": "SlowBongo",
"evtest-error-desc": "Keyboard monitoring stopped unexpectedly. Restarting..."
}
}
+34
View File
@@ -0,0 +1,34 @@
{
"id": "slowbongo",
"name": "Slow Bongo",
"version": "0.8.0",
"minNoctaliaVersion": "3.6.0",
"author": "tui",
"description": "A bongo cat that sits in your bar and slaps when you type.",
"license": "MIT",
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
"tags": [
"Bar",
"Audio",
"Fun"],
"entryPoints": {
"main": "Main.qml",
"barWidget": "BarWidget.qml",
"settings": "Settings.qml"
},
"dependencies": {
"plugins": []
},
"metadata": {
"defaultSettings": {
"idleTimeout": 150,
"waitingTimeout": 30000,
"catColor": "default",
"catSize": 1.0,
"catOffsetY": 0.0,
"raveMode": false,
"tappyMode": false,
"useMprisFilter": false
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

+13
View File
@@ -0,0 +1,13 @@
{
"idleTimeout": 150,
"waitingTimeout": 30000,
"catColor": "default",
"catSize": 1,
"catOffsetY": 0,
"raveMode": false,
"tappyMode": true,
"useMprisFilter": false,
"inputDevices": [
"/dev/input/event3"
]
}
+793
View File
@@ -0,0 +1,793 @@
{
"appLauncher": {
"autoPasteClipboard": false,
"clipboardWatchImageCommand": "wl-paste --type image --watch cliphist store",
"clipboardWatchTextCommand": "wl-paste --type text --watch cliphist store",
"clipboardWrapText": true,
"customLaunchPrefix": "",
"customLaunchPrefixEnabled": false,
"density": "default",
"enableClipPreview": true,
"enableClipboardChips": true,
"enableClipboardHistory": true,
"enableClipboardSmartIcons": true,
"enableSessionSearch": true,
"enableSettingsSearch": true,
"enableWindowsSearch": true,
"iconMode": "tabler",
"ignoreMouseInput": false,
"overviewLayer": false,
"pinnedApps": [
],
"position": "center",
"screenshotAnnotationTool": "",
"showCategories": true,
"showIconBackground": true,
"sortByMostUsed": true,
"terminalCommand": "kitty -e",
"viewMode": "list"
},
"audio": {
"mprisBlacklist": [
],
"preferredPlayer": "",
"spectrumFrameRate": 120,
"spectrumMirrored": true,
"visualizerType": "linear",
"volumeFeedback": false,
"volumeFeedbackSoundFile": "",
"volumeOverdrive": false,
"volumeStep": 5
},
"bar": {
"autoHideDelay": 500,
"autoShowDelay": 150,
"backgroundOpacity": 0.93,
"barType": "floating",
"capsuleColorKey": "none",
"capsuleOpacity": 1,
"contentPadding": 2,
"density": "default",
"displayMode": "always_visible",
"enableExclusionZoneInset": false,
"fontScale": 1,
"frameRadius": 12,
"frameThickness": 8,
"hideOnOverview": false,
"marginHorizontal": 4,
"marginVertical": 4,
"middleClickAction": "none",
"middleClickCommand": "",
"middleClickFollowMouse": false,
"monitors": [
],
"mouseWheelAction": "none",
"mouseWheelWrap": true,
"outerCorners": true,
"position": "top",
"reverseScroll": false,
"rightClickAction": "controlCenter",
"rightClickCommand": "",
"rightClickFollowMouse": true,
"screenOverrides": [
],
"showCapsule": false,
"showOnWorkspaceSwitch": true,
"showOutline": false,
"useSeparateOpacity": true,
"widgetSpacing": 6,
"widgets": {
"center": [
{
"compactMode": false,
"hideMode": "hidden",
"hideWhenIdle": false,
"id": "MediaMini",
"maxWidth": 200,
"panelShowAlbumArt": true,
"scrollingMode": "hover",
"showAlbumArt": false,
"showArtistFirst": false,
"showProgressRing": true,
"showVisualizer": true,
"textColor": "none",
"useFixedWidth": true,
"visualizerType": "linear"
}
],
"left": [
{
"colorizeDistroLogo": false,
"colorizeSystemIcon": "none",
"colorizeSystemText": "none",
"customIconPath": "",
"enableColorization": true,
"icon": "noctalia",
"id": "ControlCenter",
"useDistroLogo": true
},
{
"clockColor": "none",
"customFont": "Sans Serif",
"formatHorizontal": "HH:mm",
"formatVertical": "HH mm - dd MM",
"id": "Clock",
"tooltipFormat": "HH:mm ddd, MMM dd",
"useCustomFont": true
},
{
"characterCount": 2,
"colorizeIcons": false,
"emptyColor": "secondary",
"enableScrollWheel": true,
"focusedColor": "primary",
"followFocusedScreen": false,
"fontWeight": "bold",
"groupedBorderOpacity": 1,
"hideUnoccupied": true,
"iconScale": 0.8,
"id": "Workspace",
"labelMode": "none",
"occupiedColor": "secondary",
"pillSize": 0.6,
"showApplications": true,
"showApplicationsHover": false,
"showBadge": true,
"showLabelsOnlyWhenOccupied": true,
"unfocusedIconsOpacity": 1
},
{
"colorizeIcons": false,
"hideMode": "hidden",
"id": "ActiveWindow",
"maxWidth": 250,
"scrollingMode": "hover",
"showIcon": false,
"showText": true,
"textColor": "none",
"useFixedWidth": false
}
],
"right": [
{
"blacklist": [
],
"chevronColor": "none",
"colorizeIcons": false,
"drawerEnabled": false,
"hidePassive": false,
"id": "Tray",
"pinned": [
]
},
{
"compactMode": true,
"diskPath": "/",
"iconColor": "none",
"id": "SystemMonitor",
"showCpuCores": false,
"showCpuFreq": false,
"showCpuTemp": true,
"showCpuUsage": true,
"showDiskAvailable": false,
"showDiskUsage": false,
"showDiskUsageAsPercent": false,
"showGpuTemp": false,
"showLoadAverage": false,
"showMemoryAsPercent": false,
"showMemoryUsage": true,
"showNetworkStats": false,
"showSwapUsage": false,
"textColor": "none",
"useMonospaceFont": true,
"usePadding": false
},
{
"displayMode": "onhover",
"iconColor": "none",
"id": "Network",
"textColor": "none"
},
{
"displayMode": "onhover",
"iconColor": "none",
"id": "VPN",
"textColor": "none"
},
{
"displayMode": "onhover",
"iconColor": "none",
"id": "Volume",
"middleClickCommand": "pwvucontrol || pavucontrol",
"textColor": "none"
},
{
"applyToAllMonitors": false,
"displayMode": "onhover",
"iconColor": "none",
"id": "Brightness",
"textColor": "none"
},
{
"displayMode": "onhover",
"iconColor": "none",
"id": "Bluetooth",
"textColor": "none"
},
{
"deviceNativePath": "__default__",
"displayMode": "graphic-clean",
"hideIfIdle": false,
"hideIfNotDetected": true,
"id": "Battery",
"showNoctaliaPerformance": true,
"showPowerProfiles": true
},
{
"displayMode": "onhover",
"iconColor": "none",
"id": "KeyboardLayout",
"showIcon": false,
"textColor": "none"
},
{
"hideWhenZero": false,
"hideWhenZeroUnread": false,
"iconColor": "none",
"id": "NotificationHistory",
"showUnreadBadge": true,
"unreadBadgeColor": "primary"
},
{
"iconColor": "none",
"id": "SessionMenu"
}
]
}
},
"brightness": {
"backlightDeviceMappings": [
],
"brightnessStep": 5,
"enableDdcSupport": false,
"enforceMinimum": true
},
"calendar": {
"cards": [
{
"enabled": true,
"id": "calendar-header-card"
},
{
"enabled": true,
"id": "calendar-month-card"
},
{
"enabled": true,
"id": "weather-card"
}
]
},
"colorSchemes": {
"darkMode": true,
"generationMethod": "tonal-spot",
"manualSunrise": "06:30",
"manualSunset": "18:30",
"monitorForColors": "",
"predefinedScheme": "Noctalia (default)",
"schedulingMode": "off",
"syncGsettings": true,
"useWallpaperColors": true
},
"controlCenter": {
"cards": [
{
"enabled": true,
"id": "profile-card"
},
{
"enabled": true,
"id": "shortcuts-card"
},
{
"enabled": true,
"id": "audio-card"
},
{
"enabled": false,
"id": "brightness-card"
},
{
"enabled": true,
"id": "weather-card"
},
{
"enabled": true,
"id": "media-sysmon-card"
}
],
"diskPath": "/",
"position": "close_to_bar_button",
"shortcuts": {
"left": [
{
"id": "Network"
},
{
"id": "Bluetooth"
},
{
"id": "WallpaperSelector"
},
{
"id": "NoctaliaPerformance"
}
],
"right": [
{
"id": "Notifications"
},
{
"id": "PowerProfile"
},
{
"id": "KeepAwake"
},
{
"id": "NightLight"
}
]
}
},
"desktopWidgets": {
"enabled": false,
"gridSnap": false,
"gridSnapScale": false,
"monitorWidgets": [
{
"name": "eDP-1",
"widgets": [
{
"colorName": "primary",
"height": 72,
"hideWhenIdle": false,
"id": "AudioVisualizer",
"roundedCorners": true,
"scale": 1.116249091396632,
"showBackground": true,
"visualizerType": "linear",
"width": 320,
"x": 1078,
"y": 700
},
{
"clockColor": "none",
"clockStyle": "digital",
"customFont": "",
"format": "HH:mm\\nd MMMM yyyy",
"id": "Clock",
"roundedCorners": true,
"scale": 1,
"showBackground": true,
"useCustomFont": false,
"x": 696,
"y": 408
},
{
"defaultSettings": {
"barWidth": 0.6,
"bloomIntensity": 0.5,
"customPrimaryColor": "#6750A4",
"customSecondaryColor": "#625B71",
"fadeWhenIdle": false,
"innerDiameter": 0.7,
"ringOpacity": 0.8,
"rotationSpeed": 0.5,
"sensitivity": 1.5,
"useCustomColors": false,
"visualizationMode": 3,
"waveThickness": 1
},
"id": "plugin:fancy-audiovisualizer",
"scale": 1.462436786352546,
"showBackground": true,
"x": 33,
"y": 64
},
{
"diskPath": "/",
"id": "SystemStat",
"layout": "bottom",
"roundedCorners": true,
"scale": 1,
"showBackground": true,
"statType": "CPU",
"x": 1116,
"y": 198
}
]
}
],
"overviewEnabled": true
},
"dock": {
"animationSpeed": 1,
"backgroundOpacity": 1,
"colorizeIcons": false,
"deadOpacity": 0.6,
"displayMode": "auto_hide",
"dockType": "floating",
"enabled": true,
"floatingRatio": 1,
"groupApps": true,
"groupClickAction": "cycle",
"groupContextMenuMode": "extended",
"groupIndicatorStyle": "dots",
"inactiveIndicators": true,
"indicatorColor": "primary",
"indicatorOpacity": 0.6,
"indicatorThickness": 3,
"launcherIcon": "",
"launcherIconColor": "none",
"launcherPosition": "end",
"launcherUseDistroLogo": false,
"monitors": [
],
"onlySameOutput": true,
"pinnedApps": [
],
"pinnedStatic": false,
"position": "bottom",
"showDockIndicator": false,
"showLauncherIcon": true,
"sitOnFrame": false,
"size": 1
},
"general": {
"allowPanelsOnScreenWithoutBar": true,
"allowPasswordWithFprintd": true,
"animationDisabled": false,
"animationSpeed": 1,
"autoStartAuth": false,
"avatarImage": "/home/sinsa/.face",
"boxRadiusRatio": 1,
"clockFormat": "hh\\nmm",
"clockStyle": "custom",
"compactLockScreen": false,
"dimmerOpacity": 0.2,
"enableBlurBehind": true,
"enableLockScreenCountdown": true,
"enableLockScreenMediaControls": false,
"enableShadows": true,
"forceBlackScreenCorners": false,
"iRadiusRatio": 1,
"keybinds": {
"keyDown": [
"Down"
],
"keyEnter": [
"Return",
"Enter"
],
"keyEscape": [
"Esc"
],
"keyLeft": [
"Left"
],
"keyRemove": [
"Del"
],
"keyRight": [
"Right"
],
"keyUp": [
"Up"
]
},
"language": "",
"lockOnSuspend": true,
"lockScreenAnimations": true,
"lockScreenBlur": 0,
"lockScreenCountdownDuration": 10000,
"lockScreenMonitors": [
],
"lockScreenTint": 0,
"passwordChars": false,
"radiusRatio": 1,
"reverseScroll": false,
"scaleRatio": 1,
"screenRadiusRatio": 1,
"shadowDirection": "bottom_right",
"shadowOffsetX": 2,
"shadowOffsetY": 3,
"showChangelogOnStartup": true,
"showHibernateOnLockScreen": false,
"showScreenCorners": false,
"showSessionButtonsOnLockScreen": true,
"smoothScrollEnabled": true,
"telemetryEnabled": false
},
"hooks": {
"colorGeneration": "",
"darkModeChange": "",
"enabled": true,
"performanceModeDisabled": "",
"performanceModeEnabled": "",
"screenLock": "",
"screenUnlock": "",
"session": "",
"startup": "",
"wallpaperChange": ""
},
"idle": {
"customCommands": "[]",
"enabled": true,
"fadeDuration": 5,
"lockCommand": "",
"lockTimeout": 330,
"resumeLockCommand": "",
"resumeScreenOffCommand": "",
"resumeSuspendCommand": "",
"screenOffCommand": "",
"screenOffTimeout": 300,
"suspendCommand": "",
"suspendTimeout": 1800
},
"location": {
"analogClockInCalendar": false,
"autoLocate": false,
"firstDayOfWeek": -1,
"hideWeatherCityName": false,
"hideWeatherTimezone": false,
"name": "Padua",
"showCalendarEvents": true,
"showCalendarWeather": true,
"showWeekNumberInCalendar": false,
"use12hourFormat": false,
"useFahrenheit": false,
"weatherEnabled": true,
"weatherShowEffects": true,
"weatherTaliaMascotAlways": false
},
"network": {
"bluetoothAutoConnect": true,
"bluetoothDetailsViewMode": "grid",
"bluetoothHideUnnamedDevices": false,
"bluetoothRssiPollIntervalMs": 60000,
"bluetoothRssiPollingEnabled": false,
"disableDiscoverability": false,
"networkPanelView": "wifi",
"wifiDetailsViewMode": "grid"
},
"nightLight": {
"autoSchedule": true,
"dayTemp": "6500",
"enabled": false,
"forced": false,
"manualSunrise": "06:30",
"manualSunset": "18:30",
"nightTemp": "4000"
},
"noctaliaPerformance": {
"disableDesktopWidgets": true,
"disableWallpaper": true
},
"notifications": {
"backgroundOpacity": 1,
"clearDismissed": true,
"criticalUrgencyDuration": 15,
"density": "default",
"enableBatteryToast": true,
"enableKeyboardLayoutToast": true,
"enableMarkdown": false,
"enableMediaToast": false,
"enabled": true,
"location": "top_right",
"lowUrgencyDuration": 3,
"monitors": [
],
"normalUrgencyDuration": 8,
"overlayLayer": true,
"respectExpireTimeout": false,
"saveToHistory": {
"critical": true,
"low": true,
"normal": true
},
"sounds": {
"criticalSoundFile": "",
"enabled": false,
"excludedApps": "discord,firefox,chrome,chromium,edge",
"lowSoundFile": "",
"normalSoundFile": "",
"separateSounds": false,
"volume": 0.5
}
},
"osd": {
"autoHideMs": 2000,
"backgroundOpacity": 1,
"enabled": true,
"enabledTypes": [
0,
1,
2
],
"location": "top_right",
"monitors": [
],
"overlayLayer": true
},
"plugins": {
"autoUpdate": false,
"notifyUpdates": true
},
"sessionMenu": {
"countdownDuration": 10000,
"enableCountdown": true,
"largeButtonsLayout": "grid",
"largeButtonsStyle": true,
"position": "center",
"powerOptions": [
{
"action": "lock",
"command": "",
"countdownEnabled": true,
"enabled": true,
"keybind": "1"
},
{
"action": "suspend",
"command": "",
"countdownEnabled": true,
"enabled": true,
"keybind": "2"
},
{
"action": "hibernate",
"command": "",
"countdownEnabled": true,
"enabled": false,
"keybind": ""
},
{
"action": "logout",
"command": "",
"countdownEnabled": true,
"enabled": true,
"keybind": "3"
},
{
"action": "rebootToUefi",
"command": "",
"countdownEnabled": true,
"enabled": true,
"keybind": "4"
},
{
"action": "userspaceReboot",
"command": "",
"countdownEnabled": true,
"enabled": true,
"keybind": "5"
},
{
"action": "reboot",
"command": "",
"countdownEnabled": true,
"enabled": true,
"keybind": "6"
},
{
"action": "shutdown",
"command": "",
"countdownEnabled": true,
"enabled": true,
"keybind": "7"
}
],
"showHeader": true,
"showKeybinds": false
},
"settingsVersion": 59,
"systemMonitor": {
"batteryCriticalThreshold": 5,
"batteryWarningThreshold": 20,
"cpuCriticalThreshold": 90,
"cpuWarningThreshold": 80,
"criticalColor": "",
"diskAvailCriticalThreshold": 10,
"diskAvailWarningThreshold": 20,
"diskCriticalThreshold": 90,
"diskWarningThreshold": 80,
"enableDgpuMonitoring": false,
"externalMonitor": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor",
"gpuCriticalThreshold": 90,
"gpuWarningThreshold": 80,
"memCriticalThreshold": 90,
"memWarningThreshold": 80,
"swapCriticalThreshold": 90,
"swapWarningThreshold": 80,
"tempCriticalThreshold": 90,
"tempWarningThreshold": 80,
"useCustomColors": false,
"warningColor": ""
},
"templates": {
"activeTemplates": [
{
"enabled": true,
"id": "zenBrowser"
},
{
"enabled": true,
"id": "niri"
}
],
"enableUserTheming": false
},
"ui": {
"boxBorderEnabled": false,
"fontDefault": "Sans Serif",
"fontDefaultScale": 1,
"fontFixed": "monospace",
"fontFixedScale": 1,
"panelBackgroundOpacity": 0.93,
"panelsAttachedToBar": true,
"scrollbarAlwaysVisible": true,
"settingsPanelMode": "attached",
"settingsPanelSideBarCardStyle": false,
"tooltipsEnabled": true,
"translucentWidgets": false
},
"wallpaper": {
"automationEnabled": false,
"directory": "/home/sinsa/Pictures/wallpapers",
"enableMultiMonitorDirectories": false,
"enabled": true,
"favorites": [
],
"fillColor": "#000000",
"fillMode": "crop",
"hideWallpaperFilenames": false,
"linkLightAndDarkWallpapers": true,
"monitorDirectories": [
],
"overviewBlur": 0.4,
"overviewEnabled": true,
"overviewTint": 0.6,
"panelPosition": "center",
"randomIntervalSec": 300,
"setWallpaperOnAllMonitors": true,
"showHiddenFiles": false,
"skipStartupTransition": false,
"solidColor": "#1a1a2e",
"sortOrder": "name_desc",
"transitionDuration": 1500,
"transitionEdgeSmoothness": 0.05,
"transitionType": [
"fade",
"disc",
"stripes",
"wipe",
"pixelate",
"honeycomb"
],
"useOriginalImages": true,
"useSolidColor": false,
"useWallhaven": false,
"viewMode": "browse",
"wallhavenApiKey": "",
"wallhavenCategories": "111",
"wallhavenOrder": "desc",
"wallhavenPurity": "100",
"wallhavenQuery": "",
"wallhavenRatios": "",
"wallhavenResolutionHeight": "",
"wallhavenResolutionMode": "atleast",
"wallhavenResolutionWidth": "",
"wallhavenSorting": "relevance",
"wallpaperChangeMode": "random"
}
}