Skip to content

Commit 4cd6d26

Browse files
authored
Merge pull request #641 from blackteaextract/jsttxt
add (not-just-text): plugin that displays a short message in the bar
2 parents 7bc6060 + 78395fa commit 4cd6d26

24 files changed

Lines changed: 1194 additions & 0 deletions

not-just-text/BarWidget.qml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import QtQuick
2+
import Quickshell
3+
import qs.Commons
4+
import qs.Services.UI
5+
import qs.Widgets
6+
7+
Item {
8+
id: root
9+
10+
property var pluginApi: null
11+
property ShellScreen screen
12+
property string widgetId: ""
13+
property string section: ""
14+
property int sectionWidgetIndex: -1
15+
property int sectionWidgetsCount: 0
16+
17+
property var cfg: pluginApi?.pluginSettings || ({})
18+
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
19+
readonly property bool fortuneEnabled: cfg.fortuneEnabled ?? defaults.fortuneEnabled ?? false
20+
readonly property bool listEnabled: cfg.listEnabled ?? defaults.listEnabled ?? false
21+
property string displayText: fortuneEnabled
22+
? (pluginApi?.mainInstance?.fortuneText ?? "")
23+
: listEnabled
24+
? (pluginApi?.mainInstance?.listText ?? "")
25+
: (cfg.text ?? defaults.text ?? "")
26+
27+
readonly property string screenName: screen?.name ?? ""
28+
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
29+
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
30+
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
31+
32+
readonly property real contentWidth: isVertical ? capsuleHeight : label.implicitWidth + Style.marginL * 2
33+
readonly property real contentHeight: capsuleHeight
34+
35+
implicitWidth: contentWidth
36+
implicitHeight: contentHeight
37+
38+
Rectangle {
39+
id: visualCapsule
40+
x: Style.pixelAlignCenter(parent.width, width)
41+
y: Style.pixelAlignCenter(parent.height, height)
42+
width: root.contentWidth
43+
height: root.contentHeight
44+
color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor
45+
radius: Style.radiusL
46+
border.color: Style.capsuleBorderColor
47+
border.width: Style.capsuleBorderWidth
48+
49+
Text {
50+
id: label
51+
anchors.centerIn: parent
52+
text: root.displayText
53+
color: mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface
54+
font.pointSize: Style.barFontSize
55+
}
56+
}
57+
58+
NPopupContextMenu {
59+
id: contextMenu
60+
model: [
61+
{ "label": pluginApi?.tr("menu.settings"), "action": "settings", "icon": "settings" }
62+
]
63+
onTriggered: action => {
64+
contextMenu.close();
65+
PanelService.closeContextMenu(screen);
66+
if (action === "settings") {
67+
BarService.openPluginSettings(screen, pluginApi.manifest);
68+
}
69+
}
70+
}
71+
72+
MouseArea {
73+
id: mouseArea
74+
anchors.fill: parent
75+
hoverEnabled: true
76+
cursorShape: Qt.PointingHandCursor
77+
acceptedButtons: Qt.LeftButton | Qt.RightButton
78+
onClicked: mouse => {
79+
if (mouse.button === Qt.LeftButton) {
80+
BarService.openPluginSettings(screen, pluginApi.manifest);
81+
} else if (mouse.button === Qt.RightButton) {
82+
PanelService.showContextMenu(contextMenu, root, screen);
83+
}
84+
}
85+
}
86+
}

not-just-text/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 blackteaextract
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

not-just-text/Main.qml

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import QtQuick
2+
import Quickshell
3+
import Quickshell.Io
4+
import qs.Commons
5+
import qs.Services.UI
6+
7+
Item {
8+
id: root
9+
property var pluginApi: null
10+
11+
property var cfg: pluginApi?.pluginSettings || ({})
12+
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
13+
readonly property bool fortuneEnabled: cfg.fortuneEnabled ?? defaults.fortuneEnabled ?? false
14+
readonly property bool fortuneOffensive: cfg.fortuneOffensive ?? defaults.fortuneOffensive ?? false
15+
readonly property bool fortuneEqual: cfg.fortuneEqual ?? defaults.fortuneEqual ?? false
16+
readonly property string fortuneCategory: cfg.fortuneCategory ?? defaults.fortuneCategory ?? ""
17+
readonly property int fortuneMaxLength: cfg.fortuneMaxLength ?? defaults.fortuneMaxLength ?? 60
18+
readonly property bool listEnabled: cfg.listEnabled ?? defaults.listEnabled ?? false
19+
readonly property string textFile: cfg.textFile ?? defaults.textFile ?? ""
20+
readonly property bool refreshOnWallpaper: cfg.refreshOnWallpaper ?? defaults.refreshOnWallpaper ?? true
21+
22+
property string fortuneText: ""
23+
property int _retries: 0
24+
readonly property int _maxRetries: 10
25+
26+
property string listText: ""
27+
readonly property string _examplesPath: Qt.resolvedUrl("examples.txt").toString().replace(/^file:\/\//, "")
28+
readonly property string _activePath: textFile.trim().length > 0 ? textFile.trim() : _examplesPath
29+
30+
Component.onCompleted: {
31+
if (fortuneEnabled) triggerFortune();
32+
if (listEnabled) pickFromFile();
33+
}
34+
35+
onFortuneEnabledChanged: {
36+
if (fortuneEnabled) triggerFortune();
37+
}
38+
39+
onFortuneOffensiveChanged: {
40+
if (fortuneEnabled) triggerFortune();
41+
}
42+
43+
onFortuneEqualChanged: {
44+
if (fortuneEnabled) triggerFortune();
45+
}
46+
47+
onFortuneCategoryChanged: {
48+
if (fortuneEnabled) triggerFortune();
49+
}
50+
51+
onListEnabledChanged: {
52+
if (listEnabled) pickFromFile();
53+
}
54+
55+
onTextFileChanged: {
56+
if (listEnabled) pickFromFile();
57+
}
58+
59+
// Debounce timer — wallpaperChanged fires once per screen, wait for all to settle
60+
Timer {
61+
id: debounce
62+
interval: 300
63+
repeat: false
64+
onTriggered: {
65+
if (root.fortuneEnabled) triggerFortune();
66+
if (root.listEnabled) pickFromFile();
67+
}
68+
}
69+
70+
Connections {
71+
target: WallpaperService
72+
function onWallpaperChanged(screenName, path) {
73+
if (!root.refreshOnWallpaper) return;
74+
if (root.fortuneEnabled || root.listEnabled) debounce.restart();
75+
}
76+
}
77+
78+
Process {
79+
id: fortuneProcess
80+
command: {
81+
var cmd = ["fortune", "-s"];
82+
if (root.fortuneOffensive) cmd.push("-o");
83+
if (root.fortuneEqual) cmd.push("-e");
84+
if (root.fortuneCategory.trim().length > 0) cmd.push(root.fortuneCategory.trim());
85+
return cmd;
86+
}
87+
running: false
88+
stdout: StdioCollector {
89+
onStreamFinished: {
90+
var lines = text.trim().split('\n').filter(l => l.trim().length > 0);
91+
var valid = lines.length === 1 && lines[0].length <= root.fortuneMaxLength;
92+
if (valid) {
93+
root.fortuneText = lines[0].trim();
94+
root._retries = 0;
95+
} else if (root._retries < root._maxRetries) {
96+
root._retries++;
97+
root.triggerFortune();
98+
} else {
99+
Logger.w("NotJustText", "Gave up after", root._maxRetries, "retries finding a short single-line fortune");
100+
root.fortuneText = root.pluginApi?.tr("fortune.gaveUp");
101+
root._retries = 0;
102+
}
103+
}
104+
}
105+
onExited: (exitCode, exitStatus) => {
106+
if (exitCode === 127) {
107+
Logger.e("NotJustText", "fortune is not installed — install it to use fortune mode");
108+
root.fortuneText = root.pluginApi?.tr("fortune.notInstalled");
109+
} else if (exitCode !== 0) {
110+
Logger.w("NotJustText", "fortune exited with code", exitCode);
111+
}
112+
}
113+
}
114+
115+
function triggerFortune() {
116+
fortuneProcess.running = false;
117+
fortuneProcess.running = true;
118+
}
119+
120+
Process {
121+
id: textFileProcess
122+
command: ["cat", root._activePath]
123+
running: false
124+
stdout: StdioCollector {
125+
onStreamFinished: {
126+
var lines = text.split('\n').filter(l => l.trim().length > 0 && !l.trim().startsWith('# '));
127+
if (lines.length > 0) {
128+
root.listText = lines[Math.floor(Math.random() * lines.length)].trim();
129+
} else {
130+
root.listText = "";
131+
}
132+
}
133+
}
134+
onExited: (exitCode, exitStatus) => {
135+
if (exitCode !== 0) {
136+
Logger.w("NotJustText", "Could not read text file:", root._activePath);
137+
root.listText = "";
138+
}
139+
}
140+
}
141+
142+
function pickFromFile() {
143+
textFileProcess.running = false;
144+
textFileProcess.running = true;
145+
}
146+
}

not-just-text/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Not Just Text
2+
3+
A Noctalia bar widget that displays a short message — either custom text, a random quote from `fortune`, or a random entry from a text file you provide.
4+
5+
## Features
6+
7+
- **just text** — type anything, it shows up in the bar
8+
- **List mode** — picks a random line from a text file each time the wallpaper changes
9+
- **Fortune mode** — shows a random quote from `fortune -s` instead
10+
- **Wallpaper-triggered refresh** — when list or fortune mode is on, a new entry is picked each time the wallpaper changes (can be disabled to pick once per session)
11+
- **Fortune options** — optionally filter by category (e.g. `computers`), enable offensive quotes (`-o`), or equalise category probability (`-e`)
12+
- Click the widget to open settings directly
13+
14+
## List mode
15+
16+
Picks a random line from a plain text file — one entry per line. You can put anything in it: kaomoji, short phrases, quotes, whatever.
17+
18+
An `examples.txt` is included in the plugin directory as a starting point. Point the setting at any file you like; if no file is configured, `examples.txt` is used automatically.
19+
20+
Lines starting with `# ` (hash + space) and blank lines are ignored, so `#hashtag` style entries work fine.
21+
22+
## Fortune mode
23+
24+
Requires `fortune` to be installed.
25+
Quotes are filtered to single-line entries up to a configurable character limit (default: 60). If no suitable quote is found after 10 attempts, the widget displays `(╯°□°)╯︵ ┻━┻`.
26+
27+
## Settings
28+
29+
| Key | Type | Default | Description |
30+
|---|---|---|---|
31+
| `text` | string | `"Hello"` | Static text shown in the bar (just text mode) |
32+
| `fortuneEnabled` | bool | `false` | Enable fortune mode |
33+
| `fortuneCategory` | string | `""` | Limit fortune to a specific category (e.g. `computers`) |
34+
| `fortuneMaxLength` | int | `60` | Maximum character length a fortune quote may have |
35+
| `fortuneOffensive` | bool | `false` | Also draw from the offensive fortune database (`-o`) |
36+
| `fortuneEqual` | bool | `false` | Give all categories equal probability regardless of size (`-e`) |
37+
| `listEnabled` | bool | `false` | Enable list mode |
38+
| `textFile` | string | `""` | Path to a text file; falls back to bundled `examples.txt` if empty |
39+
| `refreshOnWallpaper` | bool | `true` | Pick a new entry when the wallpaper changes; disable to pick once per session |

not-just-text/Settings.qml

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import QtQuick
2+
import QtQuick.Layouts
3+
import qs.Commons
4+
import qs.Widgets
5+
6+
ColumnLayout {
7+
id: root
8+
9+
property var pluginApi: null
10+
property var cfg: pluginApi?.pluginSettings || ({})
11+
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
12+
13+
property string editText: cfg.text ?? defaults.text ?? ""
14+
property bool editFortuneEnabled: cfg.fortuneEnabled ?? defaults.fortuneEnabled ?? false
15+
property bool editFortuneOffensive: cfg.fortuneOffensive ?? defaults.fortuneOffensive ?? false
16+
property bool editFortuneEqual: cfg.fortuneEqual ?? defaults.fortuneEqual ?? false
17+
property string editFortuneCategory: cfg.fortuneCategory ?? defaults.fortuneCategory ?? ""
18+
property bool editListEnabled: cfg.listEnabled ?? defaults.listEnabled ?? false
19+
property string editTextFile: cfg.textFile ?? defaults.textFile ?? ""
20+
21+
spacing: Style.marginL
22+
23+
NToggle {
24+
Layout.fillWidth: true
25+
label: pluginApi?.tr("settings.fortune.label")
26+
description: pluginApi?.tr("settings.fortune.desc")
27+
checked: root.editFortuneEnabled
28+
onToggled: checked => {
29+
root.editFortuneEnabled = checked;
30+
if (checked) root.editListEnabled = false;
31+
}
32+
}
33+
34+
NToggle {
35+
Layout.fillWidth: true
36+
visible: !root.editFortuneEnabled
37+
label: pluginApi?.tr("settings.list.label")
38+
description: pluginApi?.tr("settings.list.desc")
39+
checked: root.editListEnabled
40+
onToggled: checked => root.editListEnabled = checked
41+
}
42+
43+
NTextInput {
44+
Layout.fillWidth: true
45+
visible: root.editListEnabled && !root.editFortuneEnabled
46+
label: pluginApi?.tr("settings.textFile.label")
47+
description: pluginApi?.tr("settings.textFile.desc")
48+
placeholderText: "~/.config/noctalia/plugins/not-just-text/examples.txt"
49+
text: root.editTextFile
50+
onTextChanged: root.editTextFile = text
51+
}
52+
53+
NTextInput {
54+
Layout.fillWidth: true
55+
visible: !root.editFortuneEnabled && !root.editListEnabled
56+
label: pluginApi?.tr("settings.text.label")
57+
description: pluginApi?.tr("settings.text.desc")
58+
text: root.editText
59+
onTextChanged: root.editText = text
60+
onAccepted: root.saveSettings()
61+
}
62+
63+
NTextInput {
64+
Layout.fillWidth: true
65+
visible: root.editFortuneEnabled
66+
label: pluginApi?.tr("settings.fortuneCategory.label")
67+
description: pluginApi?.tr("settings.fortuneCategory.desc")
68+
placeholderText: "computers"
69+
text: root.editFortuneCategory
70+
onTextChanged: root.editFortuneCategory = text
71+
}
72+
73+
NToggle {
74+
Layout.fillWidth: true
75+
visible: root.editFortuneEnabled
76+
label: pluginApi?.tr("settings.fortuneOffensive.label")
77+
description: pluginApi?.tr("settings.fortuneOffensive.desc")
78+
checked: root.editFortuneOffensive
79+
onToggled: checked => root.editFortuneOffensive = checked
80+
}
81+
82+
NToggle {
83+
Layout.fillWidth: true
84+
visible: root.editFortuneEnabled
85+
label: pluginApi?.tr("settings.fortuneEqual.label")
86+
description: pluginApi?.tr("settings.fortuneEqual.desc")
87+
checked: root.editFortuneEqual
88+
onToggled: checked => root.editFortuneEqual = checked
89+
}
90+
91+
function saveSettings() {
92+
if (!pluginApi) return;
93+
pluginApi.pluginSettings.text = root.editText;
94+
pluginApi.pluginSettings.fortuneEnabled = root.editFortuneEnabled;
95+
pluginApi.pluginSettings.fortuneOffensive = root.editFortuneOffensive;
96+
pluginApi.pluginSettings.fortuneEqual = root.editFortuneEqual;
97+
pluginApi.pluginSettings.fortuneCategory = root.editFortuneCategory;
98+
pluginApi.pluginSettings.listEnabled = root.editListEnabled;
99+
pluginApi.pluginSettings.textFile = root.editTextFile;
100+
pluginApi.saveSettings();
101+
}
102+
}

0 commit comments

Comments
 (0)