554 lines
20 KiB
JavaScript
554 lines
20 KiB
JavaScript
const Gtk = imports.gi.Gtk;
|
|
const Gio = imports.gi.Gio;
|
|
const Gdk = imports.gi.Gdk;
|
|
const GLib = imports.gi.GLib;
|
|
const ByteArray = imports.byteArray;
|
|
const Config = imports.misc.config;
|
|
|
|
const Gettext = imports.gettext.domain('system-monitor');
|
|
|
|
let extension = imports.misc.extensionUtils.getCurrentExtension();
|
|
let convenience = extension.imports.convenience;
|
|
|
|
const _ = Gettext.gettext;
|
|
const N_ = function (e) {
|
|
return e;
|
|
};
|
|
|
|
let Schema;
|
|
|
|
const shellMajorVersion = parseInt(Config.PACKAGE_VERSION.split('.')[0]);
|
|
|
|
function init() {
|
|
convenience.initTranslations();
|
|
Schema = convenience.getSettings();
|
|
}
|
|
|
|
String.prototype.capitalize = function () {
|
|
return this.replace(/(^|\s)([a-z])/g, function (m, p1, p2) {
|
|
return p1 + p2.toUpperCase();
|
|
});
|
|
};
|
|
|
|
function color_to_hex(color) {
|
|
var output = N_('#%02x%02x%02x%02x').format(
|
|
255 * color.red,
|
|
255 * color.green,
|
|
255 * color.blue,
|
|
255 * color.alpha);
|
|
return output;
|
|
}
|
|
|
|
function parse_bytearray(bytearray) {
|
|
if (!ByteArray.toString(bytearray).match(/GjsModule byteArray/)) {
|
|
return ByteArray.toString(bytearray);
|
|
}
|
|
return bytearray
|
|
}
|
|
|
|
function check_sensors(sensor_type) {
|
|
const hwmon_path = '/sys/class/hwmon/';
|
|
const hwmon_dir = Gio.file_new_for_path(hwmon_path);
|
|
|
|
const sensor_files = [];
|
|
const sensor_labels = [];
|
|
|
|
function get_label_from(file) {
|
|
if (file.query_exists(null)) {
|
|
// load_contents (and even cat) fails with "Invalid argument" for some label files
|
|
try {
|
|
let [success, contents] = file.load_contents(null);
|
|
if (success) {
|
|
return String(parse_bytearray(contents)).split('\n')[0];
|
|
}
|
|
} catch (e) {
|
|
log('[System monitor] error loading label from file ' + file.get_path() + ': ' + e);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function add_sensors_from(chip_dir, chip_label) {
|
|
const chip_children = chip_dir.enumerate_children(
|
|
'standard::name,standard::type', Gio.FileQueryInfoFlags.NONE, null);
|
|
if (!chip_children) {
|
|
log('[System monitor] error enumerating children of chip ' + chip_dir.get_path());
|
|
return false;
|
|
}
|
|
|
|
const input_entry_regex = new RegExp('^' + sensor_type + '(\\d+)_input$');
|
|
let info;
|
|
let added = false;
|
|
while ((info = chip_children.next_file(null))) {
|
|
if (info.get_file_type() !== Gio.FileType.REGULAR) {
|
|
continue;
|
|
}
|
|
const matches = info.get_name().match(input_entry_regex);
|
|
if (!matches) {
|
|
continue;
|
|
}
|
|
const input_ordinal = matches[1];
|
|
const input = chip_children.get_child(info);
|
|
const input_label = get_label_from(chip_dir.get_child(sensor_type + input_ordinal + '_label'));
|
|
|
|
sensor_files.push(input.get_path());
|
|
sensor_labels.push(chip_label + ' - ' + (input_label || input_ordinal));
|
|
added = true;
|
|
}
|
|
return added;
|
|
}
|
|
|
|
const hwmon_children = hwmon_dir.enumerate_children(
|
|
'standard::name,standard::type', Gio.FileQueryInfoFlags.NONE, null);
|
|
if (!hwmon_children) {
|
|
log('[System monitor] error enumerating hwmon children');
|
|
return [[], []];
|
|
}
|
|
|
|
let info;
|
|
while ((info = hwmon_children.next_file(null))) {
|
|
if (info.get_file_type() !== Gio.FileType.DIRECTORY || !info.get_name().match(/^hwmon\d+$/)) {
|
|
continue;
|
|
}
|
|
const chip = hwmon_children.get_child(info);
|
|
const chip_label = get_label_from(chip.get_child('name')) || chip.get_basename();
|
|
|
|
if (!add_sensors_from(chip, chip_label)) {
|
|
// This is here to provide compatibility with previous code, but I can't find any
|
|
// information about sensors being stored in chip/device directory. Can we delete it?
|
|
const chip_device = chip.get_child('device');
|
|
if (chip_device.query_exists(null)) {
|
|
add_sensors_from(chip_device, chip_label);
|
|
}
|
|
}
|
|
}
|
|
return [sensor_files, sensor_labels];
|
|
}
|
|
|
|
/**
|
|
* @param args.hasBorder Whether the box has a border (true) or not
|
|
* @param args.horizontal Whether the box is horizontal (true)
|
|
* or vertical (false)
|
|
* @param args.shouldPack Determines whether a horizontal box should have
|
|
* uniform spacing for its children. Only applies to horizontal boxes
|
|
* @param args.spacing The amount of spacing for a given box
|
|
* @returns a new Box with settings specified by args
|
|
*/
|
|
function box(args = {}) {
|
|
const options = { };
|
|
|
|
if (typeof args.spacing !== 'undefined') {
|
|
options.spacing = args.spacing;
|
|
}
|
|
|
|
if (shellMajorVersion < 40) {
|
|
if (args.hasBorder) {
|
|
options.border_width = 10;
|
|
}
|
|
|
|
return args.horizontal ?
|
|
new Gtk.HBox(options) : new Gtk.VBox(options);
|
|
}
|
|
|
|
if (args.hasBorder) {
|
|
options.margin_top = 10;
|
|
options.margin_bottom = 10;
|
|
options.margin_start = 10;
|
|
options.margin_end = 10;
|
|
}
|
|
|
|
options.orientation = args.horizontal ?
|
|
Gtk.Orientation.HORIZONTAL : Gtk.Orientation.VERTICAL;
|
|
|
|
const aliasBox = new Gtk.Box(options);
|
|
|
|
if (args.shouldPack) {
|
|
aliasBox.set_homogeneous(true);
|
|
}
|
|
|
|
|
|
aliasBox.add = aliasBox.append;
|
|
aliasBox.pack_start = aliasBox.prepend;
|
|
// normally, this would be append; it is aliased to prepend because
|
|
// that appears to yield the same behavior as version < 40
|
|
aliasBox.pack_end = aliasBox.prepend;
|
|
|
|
return aliasBox;
|
|
}
|
|
|
|
const ColorSelect = class SystemMonitor_ColorSelect {
|
|
constructor(name) {
|
|
this.label = new Gtk.Label({label: name + _(':')});
|
|
this.picker = new Gtk.ColorButton();
|
|
this.actor = box({horizontal: true, spacing: 5});
|
|
this.actor.add(this.label);
|
|
this.actor.add(this.picker);
|
|
this.picker.set_use_alpha(true);
|
|
}
|
|
set_value(value) {
|
|
let color = new Gdk.RGBA();
|
|
|
|
if (Gtk.get_major_version() >= 4) {
|
|
// GDK did not support parsing hex colours with alpha before GTK 4.
|
|
color.parse(value);
|
|
} else {
|
|
// Use the Compat only when GTK 4 is not available,
|
|
// since it depends on the deprecated Clutter library.
|
|
let Compat = extension.imports.compat;
|
|
let clutterColor = Compat.color_from_string(value);
|
|
let ctemp = [clutterColor.red, clutterColor.green, clutterColor.blue, clutterColor.alpha / 255];
|
|
color.parse('rgba(' + ctemp.join(',') + ')');
|
|
}
|
|
|
|
this.picker.set_rgba(color);
|
|
}
|
|
}
|
|
|
|
const IntSelect = class SystemMonitor_IntSelect {
|
|
constructor(name) {
|
|
this.label = new Gtk.Label({label: name + _(':')});
|
|
this.spin = new Gtk.SpinButton();
|
|
this.actor = box({horizontal: true, shouldPack: true, });
|
|
this.actor.add(this.label);
|
|
this.actor.add(this.spin);
|
|
this.spin.set_numeric(true);
|
|
}
|
|
set_args(minv, maxv, incre, page) {
|
|
this.spin.set_range(minv, maxv);
|
|
this.spin.set_increments(incre, page);
|
|
}
|
|
set_value(value) {
|
|
this.spin.set_value(value);
|
|
}
|
|
}
|
|
|
|
const Select = class SystemMonitor_Select {
|
|
constructor(name) {
|
|
this.label = new Gtk.Label({label: name + _(':')});
|
|
// this.label.set_justify(Gtk.Justification.RIGHT);
|
|
this.selector = new Gtk.ComboBoxText();
|
|
this.actor = box({horizontal: true, shouldPack: true, spacing: 5});
|
|
this.actor.add(this.label);
|
|
this.actor.add(this.selector);
|
|
}
|
|
set_value(value) {
|
|
this.selector.set_active(value);
|
|
}
|
|
add(items) {
|
|
items.forEach((item) => {
|
|
this.selector.append_text(item);
|
|
})
|
|
}
|
|
}
|
|
|
|
function set_enum(combo, schema, name) {
|
|
Schema.set_enum(name, combo.get_active());
|
|
}
|
|
|
|
function set_color(color, schema, name) {
|
|
Schema.set_string(name, color_to_hex(color.get_rgba()))
|
|
}
|
|
|
|
function set_string(combo, schema, name, _slist) {
|
|
Schema.set_string(name, _slist[combo.get_active()]);
|
|
}
|
|
|
|
const SettingFrame = class SystemMonitor {
|
|
constructor(name, schema) {
|
|
this.schema = schema;
|
|
this.label = new Gtk.Label({label: name});
|
|
|
|
this.vbox = box({horizontal: false, shouldPack: true, spacing: 20});
|
|
this.hbox0 = box({horizontal: true, shouldPack: true, spacing: 20});
|
|
this.hbox1 = box({horizontal: true, shouldPack: true, spacing: 20});
|
|
this.hbox2 = box({horizontal: true, shouldPack: true, spacing: 20});
|
|
this.hbox3 = box({horizontal: true, shouldPack: true, spacing: 20});
|
|
|
|
if (shellMajorVersion < 40) {
|
|
this.frame = new Gtk.Frame({border_width: 10});
|
|
this.frame.add(this.vbox);
|
|
} else {
|
|
this.frame = new Gtk.Frame({
|
|
margin_top: 10,
|
|
margin_bottom: 10,
|
|
margin_start: 10,
|
|
margin_end: 10
|
|
});
|
|
this.frame.set_child(this.vbox);
|
|
}
|
|
|
|
|
|
if (shellMajorVersion < 40) {
|
|
this.vbox.pack_start(this.hbox0, true, false, 0);
|
|
this.vbox.pack_start(this.hbox1, true, false, 0);
|
|
this.vbox.pack_start(this.hbox2, true, false, 0);
|
|
this.vbox.pack_start(this.hbox3, true, false, 0);
|
|
} else {
|
|
this.vbox.append(this.hbox0);
|
|
this.vbox.append(this.hbox1);
|
|
this.vbox.append(this.hbox2);
|
|
this.vbox.append(this.hbox3);
|
|
}
|
|
}
|
|
|
|
/** Enforces child ordering of first 2 boxes by label */
|
|
_reorder() {
|
|
if (shellMajorVersion < 40) {
|
|
/** @return {string} label of/inside component */
|
|
const labelOf = el => {
|
|
if (el.get_children) {
|
|
return labelOf(el.get_children()[0]);
|
|
}
|
|
return el && el.label || '';
|
|
};
|
|
[this.hbox0, this.hbox1].forEach(hbox => {
|
|
hbox.get_children()
|
|
.sort((c1, c2) => labelOf(c1).localeCompare(labelOf(c2)))
|
|
.forEach((child, index) => hbox.reorder_child(child, index));
|
|
});
|
|
} else {
|
|
/** @return {string} label of/inside component */
|
|
const labelOf = el => {
|
|
if (el.get_label) {
|
|
return el.get_label();
|
|
}
|
|
return labelOf(el.get_first_child());
|
|
}
|
|
|
|
[this.hbox0, this.hbox1].forEach(hbox => {
|
|
const children = [];
|
|
let next = hbox.get_first_child();
|
|
|
|
while (next !== null) {
|
|
children.push(next);
|
|
next = next.get_next_sibling();
|
|
}
|
|
|
|
const sorted = children
|
|
.sort((c1, c2) => labelOf(c1).localeCompare(labelOf(c2)));
|
|
|
|
sorted
|
|
.forEach((child, index) => {
|
|
hbox.reorder_child_after(child, sorted[index - 1] || null);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
add(key) {
|
|
const configParent = key.substring(0, key.indexOf('-'));
|
|
const config = key.substring(configParent.length + 1);
|
|
|
|
// hbox0
|
|
if (config === 'display') {
|
|
let item = new Gtk.CheckButton({label: _('Display')});
|
|
this.hbox0.add(item);
|
|
Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
} else if (config === 'show-text') {
|
|
let item = new Gtk.CheckButton({label: _('Show Text')});
|
|
this.hbox0.add(item);
|
|
Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
} else if (config === 'show-menu') {
|
|
let item = new Gtk.CheckButton({label: _('Show In Menu')});
|
|
this.hbox0.add(item);
|
|
Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
// hbox1
|
|
} else if (config === 'refresh-time') {
|
|
let item = new IntSelect(_('Refresh Time'));
|
|
item.set_args(50, 100000, 1000, 5000);
|
|
this.hbox1.add(item.actor);
|
|
Schema.bind(key, item.spin, 'value', Gio.SettingsBindFlags.DEFAULT);
|
|
} else if (config === 'graph-width') {
|
|
let item = new IntSelect(_('Graph Width'));
|
|
item.set_args(1, 1000, 1, 10);
|
|
this.hbox1.add(item.actor);
|
|
Schema.bind(key, item.spin, 'value', Gio.SettingsBindFlags.DEFAULT);
|
|
} else if (config === 'style') {
|
|
let item = new Select(_('Display Style'));
|
|
item.add([_('digit'), _('graph'), _('both')]);
|
|
item.set_value(this.schema.get_enum(key));
|
|
this.hbox1.add(item.actor);
|
|
item.selector.connect('changed', function (style) {
|
|
set_enum(style, Schema, key);
|
|
});
|
|
// Schema.bind(key, item.selector, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
// hbox2
|
|
} else if (config.match(/-color$/)) {
|
|
let item = new ColorSelect(_(config.split('-')[0].capitalize()));
|
|
item.set_value(this.schema.get_string(key));
|
|
if (shellMajorVersion < 40) {
|
|
this.hbox2.pack_end(item.actor, true, false, 0);
|
|
} else {
|
|
this.hbox2.append(item.actor);
|
|
}
|
|
item.picker.connect('color-set', function (color) {
|
|
set_color(color, Schema, key);
|
|
});
|
|
} else if (config.match(/sensor/)) {
|
|
let sensor_type = configParent === 'fan' ? 'fan' : 'temp';
|
|
let [_slist, _strlist] = check_sensors(sensor_type);
|
|
let item = new Select(_('Sensor'));
|
|
if (_slist.length === 0) {
|
|
item.add([_('Please install lm-sensors')]);
|
|
} else if (_slist.length === 1) {
|
|
this.schema.set_string(key, _slist[0]);
|
|
}
|
|
item.add(_strlist);
|
|
try {
|
|
item.set_value(_slist.indexOf(this.schema.get_string(key)));
|
|
} catch (e) {
|
|
item.set_value(0);
|
|
}
|
|
// this.hbox3.add(item.actor);
|
|
if (configParent === 'fan') {
|
|
if (shellMajorVersion < 40) {
|
|
this.hbox2.pack_end(item.actor, true, false, 0);
|
|
} else {
|
|
this.hbox2.append(item.actor);
|
|
}
|
|
} else if (shellMajorVersion < 40) {
|
|
this.hbox2.pack_start(item.actor, true, false, 0);
|
|
} else {
|
|
this.hbox2.prepend(item.actor);
|
|
}
|
|
item.selector.connect('changed', function (combo) {
|
|
set_string(combo, Schema, key, _slist);
|
|
});
|
|
// hbox3
|
|
} else if (config === 'speed-in-bits') {
|
|
let item = new Gtk.CheckButton({label: _('Show network speed in bits')});
|
|
this.hbox3.add(item);
|
|
Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
} else if (config === 'individual-cores') {
|
|
let item = new Gtk.CheckButton({label: _('Display Individual Cores')});
|
|
this.hbox3.add(item);
|
|
Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
} else if (config === 'time') {
|
|
let item = new Gtk.CheckButton({label: _('Show Time Remaining')});
|
|
this.hbox3.add(item);
|
|
Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
} else if (config === 'hidesystem') {
|
|
let item = new Gtk.CheckButton({label: _('Hide System Icon')});
|
|
this.hbox3.add(item);
|
|
Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
} else if (config === 'usage-style') {
|
|
let item = new Select(_('Usage Style'));
|
|
item.add([_('pie'), _('bar'), _('none')]);
|
|
item.set_value(this.schema.get_enum(key));
|
|
if (shellMajorVersion < 40) {
|
|
this.hbox3.pack_end(item.actor, false, false, 20);
|
|
} else {
|
|
this.hbox3.append(item.actor);
|
|
}
|
|
|
|
item.selector.connect('changed', function (style) {
|
|
set_enum(style, Schema, key);
|
|
});
|
|
} else if (config === 'fahrenheit-unit') {
|
|
let item = new Gtk.CheckButton({label: _('Display temperature in Fahrenheit')});
|
|
this.hbox3.add(item);
|
|
Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
} else if (config === 'threshold') {
|
|
let item = new IntSelect(_('Temperature threshold (0 to disable)'));
|
|
item.set_args(0, 300, 5, 5);
|
|
this.hbox3.add(item.actor);
|
|
Schema.bind(key, item.spin, 'value', Gio.SettingsBindFlags.DEFAULT);
|
|
}
|
|
if (configParent.indexOf('gpu') !== -1 &&
|
|
config === 'display') {
|
|
let item = new Gtk.Label({label: _('** Only Nvidia GPUs supported so far **')});
|
|
this.hbox3.add(item);
|
|
}
|
|
this._reorder();
|
|
}
|
|
}
|
|
|
|
const App = class SystemMonitor_App {
|
|
constructor() {
|
|
let setting_items = ['cpu', 'memory', 'swap', 'net', 'disk', 'gpu', 'thermal', 'fan', 'freq', 'battery'];
|
|
let keys = Schema.list_keys();
|
|
|
|
this.items = [];
|
|
this.settings = [];
|
|
|
|
setting_items.forEach((setting) => {
|
|
this.settings[setting] = new SettingFrame(_(setting.capitalize()), Schema);
|
|
});
|
|
|
|
this.main_vbox = box({
|
|
hasBorder: true, horizontal: false, spacing: 10});
|
|
this.hbox1 = box({
|
|
hasBorder: true, horizontal: true, shouldPack: true, spacing: 20});
|
|
if (shellMajorVersion < 40) {
|
|
this.main_vbox.pack_start(this.hbox1, false, false, 0);
|
|
} else {
|
|
this.main_vbox.prepend(this.hbox1);
|
|
}
|
|
|
|
keys.forEach((key) => {
|
|
if (key === 'icon-display') {
|
|
let item = new Gtk.CheckButton({label: _('Display Icon')});
|
|
this.items.push(item)
|
|
this.hbox1.add(item)
|
|
Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
} else if (key === 'center-display') {
|
|
let item = new Gtk.CheckButton({label: _('Display in the Middle')})
|
|
this.items.push(item)
|
|
this.hbox1.add(item)
|
|
Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
} else if (key === 'compact-display') {
|
|
let item = new Gtk.CheckButton({label: _('Compact Display')})
|
|
this.items.push(item)
|
|
this.hbox1.add(item)
|
|
Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
} else if (key === 'show-tooltip') {
|
|
let item = new Gtk.CheckButton({label: _('Show tooltip')})
|
|
item.set_active(Schema.get_boolean(key))
|
|
this.items.push(item)
|
|
this.hbox1.add(item)
|
|
Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
} else if (key === 'move-clock') {
|
|
let item = new Gtk.CheckButton({label: _('Move the clock')})
|
|
this.items.push(item)
|
|
this.hbox1.add(item)
|
|
Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
} else if (key === 'background') {
|
|
let item = new ColorSelect(_('Background Color'))
|
|
item.set_value(Schema.get_string(key))
|
|
this.items.push(item)
|
|
if (shellMajorVersion < 40) {
|
|
this.hbox1.pack_start(item.actor, true, false, 0)
|
|
} else {
|
|
this.hbox1.prepend(item.actor)
|
|
}
|
|
item.picker.connect('color-set', function (color) {
|
|
set_color(color, Schema, key);
|
|
});
|
|
} else {
|
|
let sections = key.split('-');
|
|
if (setting_items.indexOf(sections[0]) >= 0) {
|
|
this.settings[sections[0]].add(key);
|
|
}
|
|
}
|
|
});
|
|
this.notebook = new Gtk.Notebook()
|
|
setting_items.forEach((setting) => {
|
|
this.notebook.append_page(this.settings[setting].frame, this.settings[setting].label)
|
|
if (shellMajorVersion < 40) {
|
|
this.main_vbox.show_all();
|
|
this.main_vbox.pack_start(this.notebook, true, true, 0)
|
|
} else {
|
|
this.main_vbox.append(this.notebook);
|
|
}
|
|
});
|
|
if (shellMajorVersion < 40) {
|
|
this.main_vbox.show_all();
|
|
}
|
|
}
|
|
}
|
|
|
|
function buildPrefsWidget() {
|
|
let widget = new App();
|
|
return widget.main_vbox;
|
|
}
|