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
+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"
]
}