Skip to content

Commit c8cc888

Browse files
WanderingBread0WanderingBread0claudiux
authored
Add Stoic Quote desklet (stoic-quote@orion) (#1828)
* Add Stoic Quote desklet (stoic-quote@orion) Daily Stoic quote desklet for Cinnamon. Displays one quote per day from 63 curated entries across seven authors (Zeno, Cleanthes, Chrysippus, Musonius Rufus, Epictetus, Marcus Aurelius, Seneca). Quote is deterministic by date hash so all users see the same quote. Manual advance button cycles through the corpus. Refreshes at midnight. * Fix: use load_contents_async, replace Lang.bind with arrow functions, escape Unicode literals * Add i18n: gettext setup in desklet.js and po/ template Wire up Gettext.bindtextdomain with the UUID and add a `_()` helper. Wrap the only translatable JS string ("Unknown" fallback author). Cinnamon picks up metadata.json and settings-schema.json strings automatically against the UUID domain. Generated po/stoic-quote@orion.pot with cinnamon-spices-makepot. * Make every displayed string translatable Convert quotes.json to quotes.js so xgettext extracts every quote text, author, and source. Each field is wrapped in _() at module load so translators can localize the entire data set via po/<lang>.po. desklet.js imports the quotes module via imports.searchPath instead of async-loading JSON. FALLBACK_QUOTE strings are also wrapped in _(). Regenerated po/stoic-quote@orion.pot — 132 translatable strings total (63 quotes + their authors and sources, settings-schema labels, metadata name/description, and the "Unknown" fallback). * Remove icon field in metadata.json --------- Co-authored-by: WanderingBread0 <the.craftsman415@passmail.net> Co-authored-by: claudiux <33965039+claudiux@users.noreply.github.com>
1 parent 377287e commit c8cc888

12 files changed

Lines changed: 1295 additions & 0 deletions

File tree

stoic-quote@orion/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Stoic Quote — Changelog
2+
3+
## 0.1.0
4+
- Initial release
5+
- 63 curated quotes from Zeno of Citium, Cleanthes, Chrysippus, Musonius Rufus, Epictetus, Marcus Aurelius, and Seneca
6+
- Deterministic daily quote selection based on date hash
7+
- Manual advance button cycles through quotes
8+
- Configurable width, source visibility, and refresh frequency

stoic-quote@orion/README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Stoic Quote — Cinnamon Desklet
2+
3+
A Linux Mint Cinnamon desklet that displays a daily Stoic quote drawn from seven philosophers across the school's full history:
4+
5+
| Author | Era | Primary sources |
6+
|---|---|---|
7+
| Zeno of Citium | ~334–262 BC | *Lives of the Eminent Philosophers* (D.L. 7) |
8+
| Cleanthes | ~330–230 BC | *Hymn to Zeus*, D.L. 7 |
9+
| Chrysippus | ~280–207 BC | Fragments, D.L. 7 |
10+
| Epictetus | ~50–135 AD | *Discourses*, *Enchiridion* |
11+
| Musonius Rufus | ~30–100 AD | *Lectures* |
12+
| Marcus Aurelius | 121–180 AD | *Meditations* |
13+
| Seneca | ~4 BC–65 AD | *Letters from a Stoic*, *On the Shortness of Life* |
14+
15+
## Features
16+
17+
- **63 curated quotes** across seven authors (Zeno, Cleanthes, Chrysippus, Epictetus, Musonius Rufus, Marcus Aurelius, Seneca), with source attribution
18+
- **Deterministic daily rotation** — the same quote is shown to all users on a given date (hash of YYYY-MM-DD mod corpus size); it does not change if Cinnamon restarts
19+
- **Midnight auto-refresh** — switches to the next day's quote at local midnight via a Mainloop timer
20+
- **Manual refresh button** — circular arrow in the corner reloads quotes.json and re-renders
21+
- **Live settings** — width, source visibility, refresh mode, and button toggle all apply without a restart
22+
23+
## Settings
24+
25+
| Setting | Default | Description |
26+
|---|---|---|
27+
| Width | 380 px | Desklet width (300–500 px) |
28+
| Show source | On | Show book/work name below author |
29+
| Show refresh button | On | Show the ↺ refresh button |
30+
| Refresh frequency | Daily | Daily (midnight), Hourly, or Manual only |
31+
32+
## Local Installation
33+
34+
### 1. Copy the files
35+
36+
```bash
37+
mkdir -p ~/.local/share/cinnamon/desklets/stoic-quote@orion
38+
cp -r ~/stoic-quote@orion/* ~/.local/share/cinnamon/desklets/stoic-quote@orion/
39+
```
40+
41+
### 2. (Optional) Generate a PNG icon from the included SVG
42+
43+
If you have `rsvg-convert` (from `librsvg2-bin`):
44+
```bash
45+
rsvg-convert -w 48 -h 48 \
46+
~/.local/share/cinnamon/desklets/stoic-quote@orion/icon.svg \
47+
-o ~/.local/share/cinnamon/desklets/stoic-quote@orion/icon.png
48+
```
49+
50+
Or with ImageMagick:
51+
```bash
52+
convert -background none -resize 48x48 \
53+
~/.local/share/cinnamon/desklets/stoic-quote@orion/icon.svg \
54+
~/.local/share/cinnamon/desklets/stoic-quote@orion/icon.png
55+
```
56+
57+
### 3. Add the desklet to your desktop
58+
59+
- Right-click the desktop → **Desklets**
60+
- Find **Stoic Quote** in the list and click **+** to add it
61+
62+
### Reloading after edits
63+
64+
After editing files (e.g., updating quotes.json), reload without a full logout:
65+
66+
- **Right-click desktop → Troubleshoot → Restart Cinnamon**
67+
- Or from a terminal: `nohup cinnamon --replace > /dev/null 2>&1 &`
68+
- Or open **Looking Glass** (Alt+F2, type `lg`, Enter) and run:
69+
`imports.ui.main.loadTheme()` or remove and re-add the desklet
70+
71+
## Adding / editing quotes
72+
73+
`quotes.json` is a plain JSON array. Each entry:
74+
75+
```json
76+
{
77+
"text": "Quote text here.",
78+
"author": "Author Name",
79+
"source": "Work Title, Book/Letter N"
80+
}
81+
```
82+
83+
The desklet picks today's quote as `djb2_hash("YYYY-MM-DD") % len(quotes)`. Adding quotes at the end shifts all future dates; inserting in the middle shifts more. If you want stable date-to-quote mapping, only append.
84+
85+
## Submitting to Cinnamon Spices
86+
87+
1. Fork [linuxmint/cinnamon-spices-desklets](https://github.com/linuxmint/cinnamon-spices-desklets)
88+
2. Copy the `stoic-quote@orion/` folder into the repo root
89+
3. Add a `screenshot.png` (1920×1080 or a cropped portion showing the desklet)
90+
4. Ensure `icon.png` is 48×48 (convert from `icon.svg` above)
91+
5. Open a pull request — the Spices team reviews and merges
92+
93+
## Debug output
94+
95+
All log lines are prefixed `[stoic-quote]` and visible in **Looking Glass** (Alt+F2 → `lg` → Log tab).
96+
97+
## License
98+
99+
Public domain / CC0. Quote texts are historical works in the public domain.
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Stoic Quote Desklet — stoic-quote@orion
2+
3+
const Desklet = imports.ui.desklet;
4+
const St = imports.gi.St;
5+
const GLib = imports.gi.GLib;
6+
const Pango = imports.gi.Pango;
7+
const Mainloop = imports.mainloop;
8+
const Settings = imports.ui.settings;
9+
const Gettext = imports.gettext;
10+
11+
const UUID = "stoic-quote@orion";
12+
13+
Gettext.bindtextdomain(UUID, GLib.get_home_dir() + "/.local/share/locale");
14+
15+
function _(str) {
16+
return Gettext.dgettext(UUID, str);
17+
}
18+
19+
// Import the quotes module from the desklet directory. Each field in
20+
// quotes.js is wrapped in _() so xgettext extracts every quote and
21+
// translators can localize the entire data set via po/<lang>.po.
22+
let DESKLET_DIR = ".";
23+
for (let key in imports.ui.deskletManager.deskletMeta) {
24+
if (key === UUID) {
25+
DESKLET_DIR = imports.ui.deskletManager.deskletMeta[key].path;
26+
break;
27+
}
28+
}
29+
imports.searchPath.unshift(DESKLET_DIR);
30+
const QuotesModule = imports.quotes;
31+
32+
const FALLBACK_QUOTE = {
33+
text: _("The impediment to action advances action. What stands in the way becomes the way."),
34+
author: _("Marcus Aurelius"),
35+
source: _("Meditations, Book 5")
36+
};
37+
38+
// ── Helpers ───────────────────────────────────────────────────────────────────
39+
40+
function _getDateString() {
41+
let d = new Date();
42+
return d.getFullYear()
43+
+ "-" + String(d.getMonth() + 1).padStart(2, "0")
44+
+ "-" + String(d.getDate()).padStart(2, "0");
45+
}
46+
47+
// djb2 — same YYYY-MM-DD always yields the same index
48+
function _dateHash(str) {
49+
let h = 5381;
50+
for (let i = 0; i < str.length; i++) {
51+
h = (((h << 5) >>> 0) + h + str.charCodeAt(i)) >>> 0;
52+
}
53+
return h;
54+
}
55+
56+
// ── Desklet ───────────────────────────────────────────────────────────────────
57+
58+
function StoicQuoteDesklet(metadata, desklet_id) {
59+
this._init(metadata, desklet_id);
60+
}
61+
62+
StoicQuoteDesklet.prototype = {
63+
__proto__: Desklet.Desklet.prototype,
64+
65+
_init: function(metadata, desklet_id) {
66+
Desklet.Desklet.prototype._init.call(this, metadata, desklet_id);
67+
68+
this._quotes = (Array.isArray(QuotesModule.QUOTES) && QuotesModule.QUOTES.length)
69+
? QuotesModule.QUOTES
70+
: [FALLBACK_QUOTE];
71+
this._refreshTimer = null;
72+
this._manualOffset = 0;
73+
this._lastDate = null;
74+
75+
// Defaults — overwritten immediately by bindProperty
76+
this.showSource = true;
77+
this.deskletWidth = 380;
78+
this.showRefreshButton = true;
79+
this.refreshFrequency = "daily";
80+
81+
this._loadSettings(desklet_id);
82+
this._buildUI();
83+
this._showQuote();
84+
this._scheduleNextRefresh();
85+
},
86+
87+
// ── Settings ──────────────────────────────────────────────────────────────
88+
89+
_loadSettings: function(desklet_id) {
90+
this._settings = new Settings.DeskletSettings(this, UUID, desklet_id);
91+
this._settings.bindProperty(Settings.BindingDirection.IN, "show-source", "showSource", () => this._onSettingChanged(), null);
92+
this._settings.bindProperty(Settings.BindingDirection.IN, "desklet-width", "deskletWidth", () => this._onSettingChanged(), null);
93+
this._settings.bindProperty(Settings.BindingDirection.IN, "show-refresh-button", "showRefreshButton", () => this._onSettingChanged(), null);
94+
this._settings.bindProperty(Settings.BindingDirection.IN, "refresh-frequency", "refreshFrequency", () => this._onSettingChanged(), null);
95+
},
96+
97+
_onSettingChanged: function() {
98+
this._showQuote();
99+
this._scheduleNextRefresh();
100+
},
101+
102+
// ── UI construction ───────────────────────────────────────────────────────
103+
104+
_buildUI: function() {
105+
this._container = new St.BoxLayout({
106+
vertical: true,
107+
style_class: "stoic-desklet"
108+
});
109+
110+
this._quoteLabel = new St.Label({
111+
style_class: "stoic-quote-text",
112+
text: ""
113+
});
114+
this._quoteLabel.clutter_text.line_wrap = true;
115+
this._quoteLabel.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
116+
this._quoteLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
117+
118+
this._authorLabel = new St.Label({ style_class: "stoic-author", text: "" });
119+
this._sourceLabel = new St.Label({ style_class: "stoic-source", text: "" });
120+
121+
let authorBox = new St.BoxLayout({ vertical: true });
122+
authorBox.add_child(this._authorLabel);
123+
authorBox.add_child(this._sourceLabel);
124+
authorBox.x_expand = true;
125+
126+
this._refreshButton = new St.Button({
127+
style_class: "stoic-refresh-button",
128+
label: "↺"
129+
});
130+
this._refreshButton.connect("clicked", () => {
131+
this._manualOffset++;
132+
global.log("[stoic-quote] Manual advance → offset " + this._manualOffset);
133+
this._showQuote();
134+
});
135+
136+
let footer = new St.BoxLayout({ vertical: false, style_class: "stoic-footer" });
137+
footer.add_child(authorBox);
138+
footer.add_child(this._refreshButton);
139+
140+
this._container.add_child(this._quoteLabel);
141+
this._container.add_child(footer);
142+
143+
this.setContent(this._container);
144+
this._applyWidth();
145+
},
146+
147+
// ── Quote display ─────────────────────────────────────────────────────────
148+
149+
_showQuote: function() {
150+
let dateStr = _getDateString();
151+
if (dateStr !== this._lastDate) {
152+
this._manualOffset = 0;
153+
this._lastDate = dateStr;
154+
}
155+
156+
let base = _dateHash(dateStr) % this._quotes.length;
157+
let idx = (base + this._manualOffset) % this._quotes.length;
158+
let q = this._quotes[idx] || FALLBACK_QUOTE;
159+
160+
this._quoteLabel.set_text("\u201C" + (q.text || "") + "\u201D");
161+
this._authorLabel.set_text("\u2014 " + (q.author || _("Unknown")));
162+
this._sourceLabel.set_text(q.source || "");
163+
164+
this._sourceLabel.visible = this.showSource && !!q.source;
165+
this._refreshButton.visible = this.showRefreshButton;
166+
this._applyWidth();
167+
},
168+
169+
_applyWidth: function() {
170+
this._container.style = "width: " + (this.deskletWidth || 380) + "px;";
171+
},
172+
173+
// ── Refresh timer ─────────────────────────────────────────────────────────
174+
175+
_scheduleNextRefresh: function() {
176+
if (this._refreshTimer) {
177+
Mainloop.source_remove(this._refreshTimer);
178+
this._refreshTimer = null;
179+
}
180+
if (this.refreshFrequency === "manual") return;
181+
182+
let delaySec;
183+
if (this.refreshFrequency === "hourly") {
184+
delaySec = 3600;
185+
} else {
186+
let now = new Date();
187+
let midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0, 0);
188+
delaySec = Math.max(60, Math.ceil((midnight - now) / 1000));
189+
}
190+
191+
global.log("[stoic-quote] Next refresh in " + delaySec + "s (mode=" + this.refreshFrequency + ")");
192+
this._refreshTimer = Mainloop.timeout_add_seconds(delaySec, () => {
193+
global.log("[stoic-quote] Scheduled refresh fired");
194+
this._showQuote();
195+
this._scheduleNextRefresh();
196+
return false;
197+
});
198+
},
199+
200+
on_desklet_removed: function() {
201+
if (this._refreshTimer) {
202+
Mainloop.source_remove(this._refreshTimer);
203+
this._refreshTimer = null;
204+
}
205+
}
206+
};
207+
208+
// ── Entry point ───────────────────────────────────────────────────────────────
209+
210+
function main(metadata, desklet_id) {
211+
return new StoicQuoteDesklet(metadata, desklet_id);
212+
}
1.04 KB
Loading
Lines changed: 11 additions & 0 deletions
Loading
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"uuid": "stoic-quote@orion",
3+
"name": "Stoic Quote",
4+
"description": "Display a daily Stoic quote from Marcus Aurelius, Epictetus, and Seneca. Quote rotates at midnight and is deterministic by date.",
5+
"version": "0.1.0",
6+
"max-instances": "3",
7+
"author": "orion",
8+
"website": ""
9+
}

0 commit comments

Comments
 (0)