Skip to content

Commit 89a35ad

Browse files
authored
sys-monitor@Paul163-ai: Add minimalist system monitor desklet (#1834)
* sys-monitor@Paul163-ai: Add minimalist system monitor desklet * sys-monitor@Paul163-ai: Remove icon field from metadata.json * Update metadata.json added curley brace * Update settings-schema.json added restart cinnamon
1 parent 503aecf commit 89a35ad

8 files changed

Lines changed: 393 additions & 0 deletions

File tree

sys-monitor@Paul163-ai/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Quick System Monitor
2+
3+
A minimalist, high-performance system monitoring desklet for the Cinnamon Desktop. Designed to be clean, non-intrusive, and highly customizable.
4+
5+
![Icon](icon.png)
6+
7+
## Features
8+
9+
* **Real-time Monitoring**: Tracks CPU, RAM, and GPU usage.
10+
* **Multi-Disk Support**: Monitor multiple mount points (e.g., `/`, `/home`, or external drives) simultaneously.
11+
* **Modern UI**: Non-bold typography and a "floating" glass aesthetic.
12+
* **High Customizability**:
13+
* **Scaling**: Resize the desklet from 50% to 200% without losing layout integrity.
14+
* **Transparency**: Achieve true 0% to 100% background opacity.
15+
* **Color Control**: Custom color pickers for both the background and the progress bars.
16+
17+
## Installation
18+
19+
1. Copy the folder `sys-monitor@Paul163-ai` to `~/.local/share/cinnamon/desklets/`.
20+
2. Right-click your desktop and select **Add Desklets**.
21+
3. Find **Quick System Monitor** and click the **+** button.
22+
23+
## Requirements
24+
25+
* **NVIDIA GPU Monitoring**: Requires `nvidia-smi` to be installed (standard with NVIDIA proprietary drivers).
26+
* **Disk Monitoring**: Uses GIO for local paths and falls back to `df` for network or specialized mounts.
27+
28+
## Configuration Tips
29+
30+
### True Transparency
31+
To get the cleanest look, it is highly recommended to:
32+
1. Open the desklet **Configure** menu.
33+
2. Go to **General Settings** (system level).
34+
3. Set **Desklet Decorations** to **None**.
35+
4. Set the **Background Transparency** slider to your preferred level (0.0 for completely floating text).
36+
37+
## Credits
38+
39+
Developed by Paul Lintott. Built for the Linux Mint community.
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
const Desklet = imports.ui.desklet;
2+
const St = imports.gi.St;
3+
const Mainloop = imports.mainloop;
4+
const GLib = imports.gi.GLib;
5+
const Gio = imports.gi.Gio;
6+
const Settings = imports.ui.settings;
7+
8+
function MyDesklet(metadata, desklet_id) {
9+
this._init(metadata, desklet_id);
10+
}
11+
12+
MyDesklet.prototype = {
13+
__proto__: Desklet.Desklet.prototype,
14+
15+
_init: function(metadata, desklet_id) {
16+
Desklet.Desklet.prototype._init.call(this, metadata, desklet_id);
17+
18+
this._destroyed = false;
19+
this._generation = 0;
20+
21+
try {
22+
this.settings = new Settings.DeskletSettings(this, this.metadata.uuid, desklet_id);
23+
this.settings.bindProperty(Settings.BindingDirection.IN, "disk-paths", "disk_paths_raw", this.setupUI, null);
24+
this.settings.bindProperty(Settings.BindingDirection.IN, "bg-color", "bg_color", this.on_appearance_changed, null);
25+
this.settings.bindProperty(Settings.BindingDirection.IN, "bg-transparency", "bg_transparency", this.on_appearance_changed, null);
26+
this.settings.bindProperty(Settings.BindingDirection.IN, "bar-color", "bar_color", this.on_appearance_changed, null);
27+
this.settings.bindProperty(Settings.BindingDirection.IN, "desklet-scale", "desklet_scale", this.on_appearance_changed, null);
28+
} catch (e) {
29+
global.logError("Settings bind failed: " + e);
30+
}
31+
32+
// setupUI is called after bindings so settings values are available
33+
this.setupUI();
34+
this.update();
35+
},
36+
37+
on_appearance_changed: function() {
38+
let opacity = this.bg_transparency !== undefined ? this.bg_transparency : 0.85;
39+
let scale = this.desklet_scale !== undefined ? this.desklet_scale : 1.0;
40+
let barColor = this.bar_color || "rgb(52, 152, 219)";
41+
let bgColor = this.bg_color || "rgb(25, 25, 25)";
42+
let rgbaColor = bgColor.replace("rgb", "rgba").replace(")", "," + opacity + ")");
43+
44+
this.actor.set_scale(scale, scale);
45+
46+
if (this.mainLayout) {
47+
this.mainLayout.style = "background-image: none !important; " +
48+
"border: none !important; " +
49+
"box-shadow: none !important; " +
50+
"background-color: " + rgbaColor + "; " +
51+
"border-radius: 15px;";
52+
}
53+
54+
let allStats = [this.cpu, this.ram, this.gpu];
55+
if (this.disks) this.disks.forEach(d => allStats.push(d.obj));
56+
57+
for (let stat of allStats) {
58+
if (stat && stat.barFill) {
59+
stat.barFill.style = "background-color: " + barColor + ";";
60+
}
61+
}
62+
},
63+
64+
_createStatBox: function(name) {
65+
let box = new St.BoxLayout({ vertical: true, style_class: "stat-box" });
66+
let label = new St.Label({ text: name + ": --%", style_class: "stat-label" });
67+
let barOutline = new St.Bin({ style_class: "bar-outline", x_align: St.Align.START });
68+
let barFill = new St.Bin({ style_class: "bar-fill", width: 0 });
69+
70+
barOutline.set_child(barFill);
71+
box.add_actor(label);
72+
box.add_actor(barOutline);
73+
74+
return { box, label, barFill, name };
75+
},
76+
77+
setupUI: function() {
78+
// Increment generation so any in-flight async callbacks from the
79+
// previous layout will bail out before touching destroyed objects
80+
this._generation++;
81+
82+
if (this.mainLayout) {
83+
this.mainLayout.destroy_all_children();
84+
} else {
85+
this.mainLayout = new St.BoxLayout({ vertical: true, style_class: "monitor-container" });
86+
}
87+
88+
// Title — font-weight is controlled here, not overridden inline
89+
this.titleLabel = new St.Label({ text: "SYSTEM MONITOR", style_class: "monitor-title" });
90+
this.mainLayout.add_actor(this.titleLabel);
91+
92+
// Uptime directly below title
93+
this.uptimeLabel = new St.Label({ text: "Uptime: --", style_class: "stat-label" });
94+
this.uptimeLabel.style = "padding-bottom: 20px;";
95+
this.mainLayout.add_actor(this.uptimeLabel);
96+
97+
this.cpu = this._createStatBox("CPU");
98+
this.ram = this._createStatBox("RAM");
99+
this.gpu = this._createStatBox("GPU");
100+
101+
this.mainLayout.add_actor(this.cpu.box);
102+
this.mainLayout.add_actor(this.ram.box);
103+
this.mainLayout.add_actor(this.gpu.box);
104+
105+
this.disks = [];
106+
let pathsStr = this.disk_paths_raw || "/";
107+
let paths = pathsStr.split(",");
108+
109+
for (let path of paths) {
110+
let p = path.trim();
111+
if (p === "") continue;
112+
let diskObj = this._createStatBox("Disk (" + p + ")");
113+
this.disks.push({ path: p, obj: diskObj });
114+
this.mainLayout.add_actor(diskObj.box);
115+
}
116+
117+
this.setContent(this.mainLayout);
118+
this.on_appearance_changed();
119+
},
120+
121+
setPercent: function(statObj, percent) {
122+
if (!statObj || !statObj.label || !statObj.barFill || isNaN(percent)) return;
123+
percent = Math.max(0, Math.min(100, Math.round(percent)));
124+
125+
statObj.label.set_text(statObj.name + ": " + percent + "%");
126+
127+
let totalWidth = statObj.barFill.get_parent().get_width();
128+
if (totalWidth <= 0) totalWidth = 250;
129+
statObj.barFill.set_width((percent / 100) * totalWidth);
130+
131+
if (percent >= 100) {
132+
statObj.barFill.style = "background-color: #e74c3c;";
133+
} else {
134+
let barColor = this.bar_color || "rgb(52, 152, 219)";
135+
statObj.barFill.style = "background-color: " + barColor + ";";
136+
}
137+
},
138+
139+
update: function() {
140+
if (this._destroyed) return;
141+
142+
// Capture generation at the start of this update cycle.
143+
// All async callbacks check this before touching any UI objects.
144+
let gen = this._generation;
145+
146+
// UPTIME
147+
let procUptime = Gio.File.new_for_path("/proc/uptime");
148+
procUptime.load_contents_async(null, (file, res) => {
149+
try {
150+
if (this._destroyed || this._generation !== gen) return;
151+
let [success, contents] = file.load_contents_finish(res);
152+
if (success && contents) {
153+
let totalSeconds = parseFloat(contents.toString().split(" ")[0]);
154+
let d = Math.floor(totalSeconds / 86400);
155+
let h = Math.floor((totalSeconds % 86400) / 3600);
156+
let m = Math.floor((totalSeconds % 3600) / 60);
157+
this.uptimeLabel.set_text("Uptime: " + (d > 0 ? d + "d " : "") + h + "h " + m + "m");
158+
}
159+
} catch (e) {}
160+
});
161+
162+
// CPU
163+
let statFile = Gio.File.new_for_path("/proc/stat");
164+
statFile.load_contents_async(null, (file, res) => {
165+
try {
166+
if (this._destroyed || this._generation !== gen) return;
167+
let [success, contents] = file.load_contents_finish(res);
168+
if (success && contents) {
169+
let parts = contents.toString().split("\n")[0].split(/\s+/).slice(1).map(Number);
170+
let total = parts.reduce((a, b) => a + b, 0);
171+
let idle = parts[3];
172+
if (this.prevTotal !== undefined) {
173+
let diffTotal = total - this.prevTotal;
174+
let diffIdle = idle - this.prevIdle;
175+
if (diffTotal > 0) this.setPercent(this.cpu, 100 * (diffTotal - diffIdle) / diffTotal);
176+
}
177+
this.prevTotal = total;
178+
this.prevIdle = idle;
179+
}
180+
} catch (e) {}
181+
});
182+
183+
// RAM
184+
let memFile = Gio.File.new_for_path("/proc/meminfo");
185+
memFile.load_contents_async(null, (file, res) => {
186+
try {
187+
if (this._destroyed || this._generation !== gen) return;
188+
let [success, contents] = file.load_contents_finish(res);
189+
if (success && contents) {
190+
let lines = contents.toString().split("\n");
191+
let memTotal = parseInt(lines[0].replace(/\D/g, ''));
192+
let memAvailable = parseInt(lines[2].replace(/\D/g, ''));
193+
if (memTotal > 0) this.setPercent(this.ram, ((memTotal - memAvailable) / memTotal) * 100);
194+
}
195+
} catch (e) {}
196+
});
197+
198+
// GPU — hide the row and show N/A if nvidia-smi is not available
199+
try {
200+
let gpuSub = Gio.Subprocess.new(
201+
["nvidia-smi", "--query-gpu=utilization.gpu", "--format=csv,noheader,nounits"],
202+
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_SILENCE
203+
);
204+
gpuSub.communicate_utf8_async(null, null, (proc, res) => {
205+
try {
206+
if (this._destroyed || this._generation !== gen) return;
207+
let [success, stdout] = proc.communicate_utf8_finish(res);
208+
if (success && stdout && stdout.trim() !== "") {
209+
this.setPercent(this.gpu, parseInt(stdout.trim()));
210+
this.gpu.box.show();
211+
} else {
212+
this.gpu.label.set_text("GPU: N/A");
213+
this.gpu.box.hide();
214+
}
215+
} catch (e) {
216+
if (this._generation === gen && this.gpu) {
217+
this.gpu.label.set_text("GPU: N/A");
218+
this.gpu.box.hide();
219+
}
220+
}
221+
});
222+
} catch (e) {
223+
// nvidia-smi not installed — hide GPU row entirely
224+
if (this.gpu) {
225+
this.gpu.label.set_text("GPU: N/A");
226+
this.gpu.box.hide();
227+
}
228+
}
229+
230+
// DISKS
231+
for (let disk of this.disks) {
232+
let dfSub = Gio.Subprocess.new(["df", "-Ph", disk.path], Gio.SubprocessFlags.STDOUT_PIPE);
233+
dfSub.communicate_utf8_async(null, null, (proc, res) => {
234+
try {
235+
if (this._destroyed || this._generation !== gen) return;
236+
let [success, stdout] = proc.communicate_utf8_finish(res);
237+
if (success && stdout) {
238+
let match = stdout.toString().match(/(\d+)%/);
239+
if (match) this.setPercent(disk.obj, parseInt(match[1]));
240+
}
241+
} catch (e) {}
242+
});
243+
}
244+
245+
// Re-schedule — only if not destroyed
246+
if (this.timeout) Mainloop.source_remove(this.timeout);
247+
if (!this._destroyed) {
248+
this.timeout = Mainloop.timeout_add_seconds(2, () => {
249+
this.update();
250+
return false;
251+
});
252+
}
253+
},
254+
255+
on_desklet_removed: function() {
256+
this._destroyed = true;
257+
if (this.timeout) Mainloop.source_remove(this.timeout);
258+
if (this.settings) this.settings.finalize();
259+
}
260+
};
261+
262+
function main(metadata, desklet_id) { return new MyDesklet(metadata, desklet_id); }
11.4 KB
Loading
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"uuid": "sys-monitor@Paul163-ai",
3+
"name": "Quick System Monitor",
4+
"description": "A minimalist monitor for CPU, RAM, GPU, and multiple Disks.",
5+
"version": "1.0.0",
6+
"prevent-decorations": true,
7+
"author": "Paul Lintott"
8+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"header_appearance": {
3+
"type": "header",
4+
"description": "Appearance"
5+
},
6+
"desklet-scale": {
7+
"type": "scale",
8+
"default": 1.0,
9+
"min": 0.5,
10+
"max": 2.0,
11+
"step": 0.05,
12+
"description": "Desklet Scale"
13+
},
14+
"bg-color": {
15+
"type": "colorchooser",
16+
"default": "rgb(25, 25, 25)",
17+
"description": "Background Color"
18+
},
19+
"bg-transparency": {
20+
"type": "scale",
21+
"default": 0.85,
22+
"min": 0.0,
23+
"max": 1.0,
24+
"step": 0.05,
25+
"description": "Background Transparency"
26+
},
27+
"bar-color": {
28+
"type": "colorchooser",
29+
"default": "rgb(52, 152, 219)",
30+
"description": "Bar Color"
31+
},
32+
"header_storage": {
33+
"type": "header",
34+
"description": "Storage Settings"
35+
},
36+
"disk-paths": {
37+
"type": "entry",
38+
"default": "/",
39+
"description": "Disk Paths (comma separated) (cinnamon restart required)"
40+
}
41+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.monitor-container {
2+
padding: 18px;
3+
border-radius: 15px; /* Matches the code above */
4+
color: white;
5+
min-width: 230px;
6+
}
7+
8+
.monitor-title {
9+
font-size: 14pt;
10+
font-weight: bold;
11+
margin-bottom: 15px;
12+
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
13+
padding-bottom: 5px;
14+
}
15+
16+
.stat-label {
17+
font-size: 10pt;
18+
font-weight: bold;
19+
color: #ffffff;
20+
padding-bottom: 2px;
21+
margin-bottom: 4px;
22+
}
23+
24+
.stat-box {
25+
margin-bottom: 12px;
26+
}
27+
28+
.bar-outline {
29+
background-color: rgba(255, 255, 255, 0.1);
30+
height: 10px;
31+
width: 200px;
32+
border-radius: 5px;
33+
}
34+
35+
.bar-fill {
36+
/* Color and height are now controlled by JS */
37+
height: 10px;
38+
border-radius: 5px;
39+
}

sys-monitor@Paul163-ai/info.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"author": "Paul163-ai",
3+
"uuid": "sys-monitor@Paul163-ai"
4+
}
165 KB
Loading

0 commit comments

Comments
 (0)