Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
10 changes: 9 additions & 1 deletion system-monitor-graph@rcassani/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ This project has been inspired from other Desklets such as [Disk Space](https://
| 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 Speed | Fan speed in RPM, on/off status, and activity history when exposed by hwmon |
| 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,13 @@ 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.

<p align="center">
<img src="fan-speed-on.png" width="350" alt="CPU fan speed graph showing 522 RPM and On">
<img src="fan-speed-off.png" width="350" alt="CPU fan speed graph showing 0 RPM and Off"><br>
CPU fan speed history while the fan is running and stopped.
</p>
Comment thread
karthikmudaliarX marked this conversation as resolved.
Outdated

### Customizable visual elements
<p align="center">
Expand Down
Binary file added system-monitor-graph@rcassani/fan-speed-off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added system-monitor-graph@rcassani/fan-speed-on.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ SystemMonitorGraph.prototype = {
// temperature values
this.cpu_temperature = NaN;
this.gpu_temperature = NaN;
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 +235,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 @@ -283,13 +289,32 @@ SystemMonitorGraph.prototype = {
// scale CPU temperature based on [CPU_TEMP_MIN, CPU_TEMP_MAX]
value = 1.0 * (this.cpu_temperature - CPU_TEMP_MIN) / (CPU_TEMP_MAX - CPU_TEMP_MIN);
value = value < 0 ? 0 : value > 1 ? 1 : value;
text1 = _("CPU Temperature");
text1 = _("CPU Temp");
Comment thread
karthikmudaliarX marked this conversation as resolved.
Outdated
if (this.temperature_units_cpu == "C") {
text2 = this.cpu_temperature.toString() + "°C";
} else if (this.temperature_units_cpu == "F") {
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;
text1 = _("CPU Fan Speed");
Comment thread
claudiux marked this conversation as resolved.
Outdated
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";
Comment thread
karthikmudaliarX marked this conversation as resolved.
Outdated
text3 = "🟢 " + _("On");
} else {
text3 = "🔴 " + _("Off");
}
} else if (this.cpu_fan_discovery_complete) {
text2 = _("Not detected");
} else {
text2 = _("Detecting...");
}
break;
}
break;

Expand Down Expand Up @@ -487,6 +512,8 @@ SystemMonitorGraph.prototype = {
if (this.type === "network") {
// Draw dual-line network graph
this.draw_network_graph(ctx, unit_size, margin_up, graph_w, graph_h, graph_step, n_values, line_width);
} else if (this.type === "cpu" && this.cpu_variable === "fan") {
Comment thread
karthikmudaliarX marked this conversation as resolved.
Outdated
this.draw_cpu_fan_graph(ctx, unit_size, margin_up, graph_w, graph_h, graph_step, n_values, line_width);
} else {
// timeseries and area
ctx.setLineWidth(2 * line_width);
Expand Down Expand Up @@ -541,6 +568,30 @@ SystemMonitorGraph.prototype = {
this.canvas.set_size(desklet_w, desklet_h);
},

draw_cpu_fan_graph: function(ctx, unit_size, margin_up, graph_w, graph_h, graph_step, n_values, line_width) {
let fan_colors = this.parse_rgba_settings(this.line_color_cpu);
let valid_values = this.values.filter(v => !isNaN(v) && isFinite(v) && v >= 0);
let max_value = valid_values.length > 0 ? Math.max(...valid_values) : 0;
let fan_scale = Math.max(1000, Math.ceil((max_value * 1.1) / 500) * 500);
let scale_value = function(value) {
if (isNaN(value) || !isFinite(value) || value < 0) return 0;
return Math.max(0, Math.min(graph_h, (value / fan_scale) * graph_h));
};

ctx.setLineWidth(2 * line_width);
ctx.setSourceRGBA(fan_colors[0], fan_colors[1], fan_colors[2], 1);
ctx.moveTo(unit_size, margin_up + graph_h - scale_value(this.values[0]));
for (let i = 1; i < n_values; i++) {
ctx.lineTo(unit_size + (i * graph_step), margin_up + graph_h - scale_value(this.values[i]));
}
ctx.strokePreserve();
ctx.lineTo(unit_size + graph_w, margin_up + graph_h);
ctx.lineTo(unit_size, margin_up + graph_h);
ctx.closePath();
ctx.setSourceRGBA(fan_colors[0], fan_colors[1], fan_colors[2], 0.4);
ctx.fill();
},

draw_network_graph: function(ctx, unit_size, margin_up, graph_w, graph_h, graph_step, n_values, line_width) {
// Parse colors for download and upload
var down_colors = this.parse_rgba_settings(this.line_color_down);
Expand Down Expand Up @@ -1142,6 +1193,68 @@ 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 script = `
for label_file in /sys/class/hwmon/hwmon*/fan*_label; do
[ -r "$label_file" ] || continue
label=$(cat "$label_file" 2>/dev/null) || continue
case "$label" in
*CPU*|*cpu*|*Processor*|*processor*)
fan_file="\${label_file%_label}_input"
[ -r "$fan_file" ] && { printf '%s' "$fan_file"; exit; }
;;
esac
done
Comment thread
karthikmudaliarX marked this conversation as resolved.
Outdated
for name_file in /sys/class/hwmon/hwmon*/name; do
[ -r "$name_file" ] || continue
driver=$(cat "$name_file" 2>/dev/null) || continue
case "$driver" in
dell_smm|thinkpad)
hwmon_dir=\${name_file%/name}
fan_file="$hwmon_dir/fan1_input"
[ -r "$fan_file" ] && { printf '%s' "$fan_file"; exit; }
;;
esac
done
Comment thread
karthikmudaliarX marked this conversation as resolved.
Outdated
`;
spawnAsyncWithOutput(['/bin/sh', '-c', script], (success, output) => {
callback(success && output ? output.trim() : "");
});
},

rediscover_cpu_fan: function() {
// Clear a stale hwmon path first. If discovery finds nothing, the fan
// graph reports that no sensor was detected instead of retrying forever.
this.cpu_fan_file = "";
this.cpu_fan_rpm = NaN;
this.cpu_fan_discovery_complete = false;
this.get_cpu_fan_file((result) => {
this.cpu_fan_file = result;
this.cpu_fan_discovery_complete = true;
});
},

get_cpu_fan_speed: function(fan_file) {
if(fan_file == null || fan_file == "") 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);
} else {
this.rediscover_cpu_fan();
}
GLib.free(contents);
} catch(error) {
this.rediscover_cpu_fan();
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 @@ -8,7 +8,7 @@ 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"
"POT-Creation-Date: 2026-07-20 21:19+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 +20,124 @@ msgstr ""
"X-Generator: Poedit 3.6\n"

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

#. desklet.js:284
msgid "CPU Temperature"
#. desklet.js:292
msgid "CPU Temp"
msgstr ""

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

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

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

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

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

#. desklet.js:327 desklet.js:345 desklet.js:365 desklet.js:406 desklet.js:736
msgid "GB"
msgstr "GB"

#. desklet.js:304 desklet.js:322 desklet.js:342 desklet.js:383 desklet.js:698
#. desklet.js:330 desklet.js:348 desklet.js:368 desklet.js:409 desklet.js:750
msgid "GiB"
msgstr "GiB"

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

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

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

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

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

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

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

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

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

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

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

#. desklet.js:655 desklet.js:678
#. desklet.js:707 desklet.js:730
msgid "b"
msgstr "b"

#. desklet.js:657 desklet.js:692 desklet.js:706
#. desklet.js:709 desklet.js:744 desklet.js:758
msgid "B"
msgstr "B"

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

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

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

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

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

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

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

Expand Down Expand Up @@ -173,6 +193,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
Loading
Loading