Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions system-monitor-graph@rcassani/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ Four instances of the Desklet in action.
This project has been inspired from other Desklets such as [Disk Space](https://cinnamon-spices.linuxmint.com/desklets/view/39), [CPU Load](https://cinnamon-spices.linuxmint.com/desklets/view/44), [Simple system monitor](https://cinnamon-spices.linuxmint.com/desklets/view/29), [Network usage monitor](https://cinnamon-spices.linuxmint.com/desklets/view/15), [Top](https://cinnamon-spices.linuxmint.com/desklets/view/41), and the [Rainmeter Win10 Widgets](https://win10widgets.com/).

## Features
### Variables to monitor (v2.0 - July 2025)
### Variables to monitor (v2.2 - July 2026)

| System variable | Description |
| ----------- | ----------- |
| CPU | CPU usage in % |
| CPU Temperature | CPU temperature C or F |
Comment thread
karthikmudaliarX marked this conversation as resolved.
| CPU Temperature | CPU temperature in C or F |
| CPU Fan | CPU fan speed in RPM and on/off status |
| RAM | Used RAM as % of total, and in GB |
| Swap | Used Swap space as % of total, and in GB |
| HDD | % of I/O activity, and free and total space in the filesystem (partition) indicated by the user |
Expand All @@ -27,6 +28,7 @@ This project has been inspired from other Desklets such as [Disk Space](https://

Each variable is calculated every `Refresh interval` seconds (Min. 1 s, Max. 60 s.), and the graph shows the last `Duration of the graph` period (Min. 30 s, Max. 60 min).
GPU graphs work for NVIDIA, discrete AMD and built-in AMD.
Fan monitoring uses CPU-labelled sensors and known laptop cooling drivers. It avoids guessing from unrelated unlabelled fans, and reports when an identifiable CPU fan is not detected.

### Customizable visual elements
<p align="center">
Expand Down Expand Up @@ -57,6 +59,11 @@ Another screenshot.
Screenshot with temperature and battery graphs.
</p>

<p align="center">
<img src="https://cinnamon-spices.linuxmint.com/git/desklets/system-monitor-graph@rcassani/screenshot5.png" width="700" align="middle"><br>
CPU fan speed history while the fan is running and stopped.
</p>

## TODO
- [x] Add network
- [x] Battery levels for main battery (laptop)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const KIB_TO_B = 1024; // 1 KiB = 1,024 B

const CPU_TEMP_MIN = 20; // Minimum CPU temperature
const CPU_TEMP_MAX = 100; // Maximum CPU temperature
const CPU_FANSPEED_MAX = 4000; // CPU fan graph full-scale speed
const CPU_FAN_RETRY_MIN = 5; // Initial retry delay in seconds
const CPU_FAN_RETRY_MAX = 300; // Maximum retry delay in seconds
const CPU_FAN_RETRY_MULTIPLIER = 2;

const UUID = "system-monitor-graph@rcassani";
const DESKLET_PATH = imports.ui.deskletManager.deskletMeta[UUID].path;
Expand Down Expand Up @@ -190,6 +194,10 @@ SystemMonitorGraph.prototype = {
// temperature values
this.cpu_temperature = NaN;
this.gpu_temperature = NaN;
// cpu fan values
this.cpu_fan_rpm = NaN;
Comment thread
karthikmudaliarX marked this conversation as resolved.
this.cpu_fan_file = "";
this.cpu_fan_discovery_complete = false;

// set colors
switch (this.type) {
Expand Down Expand Up @@ -232,6 +240,9 @@ SystemMonitorGraph.prototype = {
});
}
});
if (this.type == "cpu" && this.cpu_variable == "fan") {
this.rediscover_cpu_fan();
}
// find file for overall AMD GPU temperature
let gpu_path_temp = "/sys/class/drm/card" + this.gpu_id + "/device/hwmon/";
this.get_temperature_file_by_label(gpu_path_temp, 'edge', (result) => {
Expand Down Expand Up @@ -290,6 +301,26 @@ SystemMonitorGraph.prototype = {
text2 = Math.round(this.cpu_temperature * 9 / 5 + 32).toString() + "°F";
}
break;

case "fan":
this.get_cpu_fan_speed(this.cpu_fan_file);
value = isNaN(this.cpu_fan_rpm) ? 0 : this.cpu_fan_rpm / CPU_FANSPEED_MAX;
value = value < 0 ? 0 : value > 1 ? 1 : value;
text1 = _("CPU Fan");
text2_size = Math.max(1, text2_size - 1);
if (!isNaN(this.cpu_fan_rpm)) {
if (this.cpu_fan_rpm >= 1) {
text2 = this.cpu_fan_rpm + " " + _("RPM");
text3 = "🟢 " + _("On");
} else {
text3 = "🔴 " + _("Off");
}
} else if (this.cpu_fan_discovery_complete) {
text2 = _("Not detected");
} else {
text2 = _("Detecting...");
}
break;
}
break;

Expand Down Expand Up @@ -1142,6 +1173,75 @@ SystemMonitorGraph.prototype = {
});
},

get_cpu_fan_file: function(callback) {
// Prefer explicitly labelled CPU fans, then known laptop cooling drivers.
// Do not guess from an unrelated, unlabelled hwmon fan.
let argv = ['/bin/sh', '-c', "for fan_file in $({ grep -s -i -l -d skip -E 'cpu|processor' /sys/class/hwmon/hwmon*/fan*_label | sed 's/_label/_input/'; grep -s -l -d skip -E '^(dell_smm|thinkpad)$' /sys/class/hwmon/hwmon*/name | sed 's#/name#/fan1_input#'; }); do [ -r \"$fan_file\" ] && { printf '%s' \"$fan_file\"; break; }; done"];
spawnAsyncWithOutput(argv, (success, output) => {
callback(success && output ? output.trim() : "");
});
},

schedule_cpu_fan_retry: function() {
if (this.cpu_fan_retry_delay == null) this.cpu_fan_retry_delay = CPU_FAN_RETRY_MIN;
this.cpu_fan_file = "";
this.cpu_fan_rpm = NaN;
this.cpu_fan_discovery_complete = true;
this.cpu_fan_next_discovery_time = GLib.get_monotonic_time()
+ (this.cpu_fan_retry_delay * GLib.USEC_PER_SEC);
this.cpu_fan_retry_delay = Math.min(
this.cpu_fan_retry_delay * CPU_FAN_RETRY_MULTIPLIER,
CPU_FAN_RETRY_MAX);
},

rediscover_cpu_fan: function() {
// Clear a stale hwmon path first. Missing or failed sensors are retried
// with bounded backoff instead of spawning another lookup every refresh.
if (this.cpu_fan_discovery_in_progress) return;
if (this.cpu_fan_retry_delay == null) this.cpu_fan_retry_delay = CPU_FAN_RETRY_MIN;
this.cpu_fan_discovery_in_progress = true;
this.cpu_fan_file = "";
this.cpu_fan_rpm = NaN;
this.cpu_fan_discovery_complete = false;
this.get_cpu_fan_file((result) => {
this.cpu_fan_discovery_in_progress = false;
if (result == "") {
this.schedule_cpu_fan_retry();
} else {
this.cpu_fan_file = result;
this.cpu_fan_discovery_complete = true;
this.cpu_fan_next_discovery_time = 0;
}
});
},

get_cpu_fan_speed: function(fan_file) {
if(fan_file == null || fan_file == "") {
if (this.cpu_fan_discovery_complete
&& GLib.get_monotonic_time() >= this.cpu_fan_next_discovery_time) {
this.rediscover_cpu_fan();
}
return;
}
Gio.file_new_for_path(fan_file).load_contents_async(null, (file, response) => {
try {
let [success, contents, tag] = file.load_contents_finish(response);
if (success) {
let rpm = parseInt(ByteArray.toString(contents).trim());
this.cpu_fan_rpm = isNaN(rpm) ? NaN : Math.max(0, rpm);
this.cpu_fan_retry_delay = CPU_FAN_RETRY_MIN;
this.cpu_fan_next_discovery_time = 0;
} else {
this.schedule_cpu_fan_retry();
}
GLib.free(contents);
} catch(error) {
this.schedule_cpu_fan_retry();
global.log('CPU fan speed file read error: ' + error.toString());
}
});
},

get_temperature_file_by_label: function(path, label, callback) {
// search for temperture file: 'path'/*/temp*_input associated to file path/*/temp*_label with content 'label'
let argv = ['/bin/sh', '-c', "grep -s -l -d skip '" + label + "' " + path + "*/temp*_label | sed 's/_label/_input/' | head -n 1"];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"uuid": "system-monitor-graph@rcassani",
"name": "System monitor graph",
"description": "Creates graphs for system variables.",
"version": "2.1",
"version": "2.2",
"prevent-decorations": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: https://github.com/linuxmint/cinnamon-spices-desklets/"
"issues\n"
"POT-Creation-Date: 2026-03-23 21:24-0400\n"
"Report-Msgid-Bugs-To: https://github.com/linuxmint/cinnamon-spices-desklets/issues\n"
"POT-Creation-Date: 2026-07-22 14:45+0530\n"
"PO-Revision-Date: 2025-09-29 04:41+0200\n"
"Last-Translator: Daniel <d3vf4n@tutanota.com>\n"
"Language-Team: \n"
Expand All @@ -20,104 +19,128 @@ msgstr ""
"X-Generator: Poedit 3.6\n"

#. settings-schema.json->type->options
#. desklet.js:275
#. desklet.js:288
msgid "CPU"
msgstr "CPU"

#. desklet.js:284
#. desklet.js:297
msgid "CPU Temperature"
msgstr ""

#. desklet.js:301 desklet.js:319 desklet.js:339 desklet.js:380 desklet.js:684
#. desklet.js:309
msgid "CPU Fan"
msgstr ""

#. desklet.js:313
msgid "RPM"
msgstr ""

#. desklet.js:314
msgid "On"
msgstr ""

#. desklet.js:316
msgid "Off"
msgstr ""

#. desklet.js:319
msgid "Not detected"
msgstr ""

#. desklet.js:321
msgid "Detecting..."
msgstr ""

#. desklet.js:334 desklet.js:352 desklet.js:372 desklet.js:413 desklet.js:717
msgid "GB"
msgstr "GB"

#. desklet.js:304 desklet.js:322 desklet.js:342 desklet.js:383 desklet.js:698
#. desklet.js:337 desklet.js:355 desklet.js:375 desklet.js:416 desklet.js:731
msgid "GiB"
msgstr "GiB"

#. settings-schema.json->type->options
#. desklet.js:306
#. desklet.js:339
msgid "RAM"
msgstr "RAM"

#. settings-schema.json->type->options
#. desklet.js:324
#. desklet.js:357
msgid "Swap"
msgstr "Swap"

#. desklet.js:347
#. desklet.js:380
msgid "free of"
msgstr "lliure de"

#. desklet.js:363
#. desklet.js:396
msgid "GPU Usage"
msgstr "Ús de GPU"

#. desklet.js:385
#. desklet.js:418
msgid "GPU Memory"
msgstr "Memòria de la GPU"

#. desklet.js:401
#. desklet.js:434
#, fuzzy
msgid "GPU Temperature"
msgstr "Fabricant de la GPU"

#. settings-schema.json->type->options
#. desklet.js:415
#. desklet.js:448
msgid "Network"
msgstr "Xarxa"

#. settings-schema.json->type->options
#. desklet.js:428
#. desklet.js:461
msgid "Battery"
msgstr ""

#. desklet.js:431
#. desklet.js:464
msgid "Fully charged"
msgstr ""

#. desklet.js:432
#. desklet.js:465
msgid "Not charging"
msgstr ""

#. desklet.js:434
#. desklet.js:467
msgid " hrs"
msgstr ""

#. desklet.js:655 desklet.js:678
#. desklet.js:688 desklet.js:711
msgid "b"
msgstr "b"

#. desklet.js:657 desklet.js:692 desklet.js:706
#. desklet.js:690 desklet.js:725 desklet.js:739
msgid "B"
msgstr "B"

#. desklet.js:670
#. desklet.js:703
msgid "Gb"
msgstr "Gb"

#. desklet.js:673
#. desklet.js:706
msgid "Mb"
msgstr "Mb"

#. desklet.js:676
#. desklet.js:709
msgid "Kb"
msgstr "Kb"

#. desklet.js:687
#. desklet.js:720
msgid "MB"
msgstr "MB"

#. desklet.js:690
#. desklet.js:723
msgid "KB"
msgstr "KB"

#. desklet.js:701
#. desklet.js:734
msgid "MiB"
msgstr "MiB"

#. desklet.js:704
#. desklet.js:737
msgid "KiB"
msgstr "KiB"

Expand Down Expand Up @@ -173,6 +196,10 @@ msgstr "Ús"
msgid "Temperature"
msgstr ""

#. settings-schema.json->cpu-variable->options
msgid "Fan Speed"
msgstr ""

#. settings-schema.json->temperature-units-cpu->options
#. settings-schema.json->temperature-units-gpu->options
msgid "°C"
Expand Down Expand Up @@ -252,10 +279,11 @@ msgstr "Interfície de xarxa a monitorar"

#. settings-schema.json->network-interface->tooltip
msgid ""
"Leave empty to monitor all interfaces combined. Examples: eth0, wlan0, enp0s3"
"Leave empty to monitor all interfaces combined. Examples: eth0, wlan0, "
"enp0s3"
msgstr ""
"Deixeu-ho en blanc per a monitorar totes les interfícies. Per exemple: eth0, "
"wlan0, enp0s3"
"Deixeu-ho en blanc per a monitorar totes les interfícies. Per exemple: eth0,"
" wlan0, enp0s3"

#. settings-schema.json->battery-name->description
#, fuzzy
Expand Down
Loading
Loading