/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
/* exported init */
const { Clutter, GLib, GTop, Shell, St } = imports.gi;
const Signals = imports.signals;
const ExtensionUtils = imports.misc.extensionUtils;
const Main = imports.ui.main;
const MessageList = imports.ui.messageList;
const Tweener = imports.ui.tweener;
const Gettext = imports.gettext.domain('gnome-shell-extensions');
const _ = Gettext.gettext;
const INDICATOR_UPDATE_INTERVAL = 500;
const INDICATOR_NUM_GRID_LINES = 3;
const ITEM_LABEL_SHOW_TIME = 0.15;
const ITEM_LABEL_HIDE_TIME = 0.1;
const ITEM_HOVER_TIMEOUT = 300;
const Indicator = class {
constructor() {
this._initValues();
this._drawingArea = new St.DrawingArea();
this._drawingArea.connect('repaint', this._draw.bind(this));
this.actor = new St.Button({
style_class: 'message message-content extension-systemMonitor-indicator-area',
child: this._drawingArea,
x_expand: true,
x_fill: true,
y_fill: true,
can_focus: true
});
this.actor.connect('clicked', () => {
let app = Shell.AppSystem.get_default().lookup_app('gnome-system-monitor.desktop');
app.open_new_window(-1);
Main.overview.hide();
Main.panel.closeCalendar();
});
this.actor.connect('destroy', this._onDestroy.bind(this));
this._timeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
INDICATOR_UPDATE_INTERVAL,
() => {
this._updateValues();
this._drawingArea.queue_repaint();
return GLib.SOURCE_CONTINUE;
});
}
showLabel() {
if (this.label == null)
return;
this.label.opacity = 0;
this.label.show();
let [stageX, stageY] = this.actor.get_transformed_position();
let itemWidth = this.actor.allocation.x2 - this.actor.allocation.x1;
let labelWidth = this.label.width;
let xOffset = Math.floor((itemWidth - labelWidth) / 2);
let x = stageX + xOffset;
let node = this.label.get_theme_node();
let yOffset = node.get_length('-y-offset');
let y = stageY - this.label.get_height() - yOffset;
this.label.set_position(x, y);
this.label.get_parent().set_child_above_sibling(this.label, null);
Tweener.addTween(this.label, {
opacity: 255,
time: ITEM_LABEL_SHOW_TIME,
transition: 'easeOutQuad',
});
}
setLabelText(text) {
if (this.label == null)
this.label = new St.Label({
style_class: 'extension-systemMonitor-indicator-label'
});
this.label.set_text(text);
Main.layoutManager.addChrome(this.label);
this.label.hide();
}
hideLabel() {
Tweener.addTween(this.label, {
opacity: 0,
time: ITEM_LABEL_HIDE_TIME,
transition: 'easeOutQuad',
onComplete: () => this.label.hide()
});
}
/* MessageList.Message boilerplate */
canClose() {
return false;
}
clear() {
}
destroy() {
this.actor.destroy();
}
_onDestroy() {
GLib.source_remove(this._timeout);
if (this.label)
this.label.destroy();
}
_initValues() {
}
_updateValues() {
}
_draw(area) {
let [width, height] = area.get_surface_size();
let themeNode = this.actor.get_theme_node();
let cr = area.get_context();
//draw the background grid
let color = themeNode.get_color(this.gridColor);
let gridOffset = Math.floor(height / (INDICATOR_NUM_GRID_LINES + 1));
for (let i = 1; i <= INDICATOR_NUM_GRID_LINES; ++i) {
cr.moveTo(0, i * gridOffset + .5);
cr.lineTo(width, i * gridOffset + .5);
}
Clutter.cairo_set_source_color(cr, color);
cr.setLineWidth(1);
cr.setDash([4, 1], 0);
cr.stroke();
//draw the foreground
function makePath(values, reverse, nudge) {
if (nudge == null) {
nudge = 0;
}
//if we are going in reverse, we are completing the bottom of a chart, so use lineTo
if (reverse) {
cr.lineTo(values.length - 1, (1 - values[values.length - 1]) * height + nudge);
for (let k = values.length - 2; k >= 0; --k) {
cr.lineTo(k, (1 - values[k]) * height + nudge);
}
} else {
cr.moveTo(0, (1 - values[0]) * height + nudge);
for (let k = 1; k < values.length; ++k) {
cr.lineTo(k, (1 - values[k]) * height + nudge);
}
}
}
let renderStats = this.renderStats;
// Make sure we don't have more sample points than pixels
renderStats.map(k => {
let stat = this.stats[k];
if (stat.values.length > width) {
stat.values = stat.values.slice(stat.values.length - width, stat.values.length);
}
});
for (let i = 0; i < renderStats.length; ++i) {
let stat = this.stats[renderStats[i]];
// We outline at full opacity and fill with 40% opacity
let outlineColor = themeNode.get_color(stat.color);
let color = new Clutter.Color(outlineColor);
color.alpha = color.alpha * .4;
// Render the background between us and the next level
makePath(stat.values, false);
// If there is a process below us, render the cpu between us and it, otherwise,
// render to the bottom of the chart
if (i == renderStats.length - 1) {
cr.lineTo(stat.values.length - 1, height);
cr.lineTo(0, height);
cr.closePath();
} else {
let nextStat = this.stats[renderStats[i + 1]];
makePath(nextStat.values, true);
}
cr.closePath();
Clutter.cairo_set_source_color(cr, color);
cr.fill();
// Render the outline of this level
makePath(stat.values, false, .5);
Clutter.cairo_set_source_color(cr, outlineColor);
cr.setLineWidth(1.0);
cr.setDash([], 0);
cr.stroke();
}
}
};
Signals.addSignalMethods(Indicator.prototype); // For MessageList.Message compat
const CpuIndicator = class extends Indicator {
constructor() {
super();
this.gridColor = '-grid-color';
this.renderStats = ['cpu-user', 'cpu-sys', 'cpu-iowait'];
// Make sure renderStats is sorted as necessary for rendering
let renderStatOrder = {
'cpu-total': 0,
'cpu-user': 1,
'cpu-sys': 2,
'cpu-iowait': 3
};
this.renderStats = this.renderStats.sort((a, b) => {
return renderStatOrder[a] - renderStatOrder[b];
});
this.setLabelText(_('CPU'));
}
_initValues() {
this._prev = new GTop.glibtop_cpu;
GTop.glibtop_get_cpu(this._prev);
this.stats = {
'cpu-user': { color: '-cpu-user-color', values: [] },
'cpu-sys': { color: '-cpu-sys-color', values: [] },
'cpu-iowait': { color: '-cpu-iowait-color', values: [] },
'cpu-total': { color: '-cpu-total-color', values: [] }
};
}
_updateValues() {
let cpu = new GTop.glibtop_cpu;
let t = 0.0;
GTop.glibtop_get_cpu(cpu);
let total = cpu.total - this._prev.total;
let user = cpu.user - this._prev.user;
let sys = cpu.sys - this._prev.sys;
let iowait = cpu.iowait - this._prev.iowait;
let idle = cpu.idle - this._prev.idle;
t += iowait / total;
this.stats['cpu-iowait'].values.push(t);
t += sys / total;
this.stats['cpu-sys'].values.push(t);
t += user / total;
this.stats['cpu-user'].values.push(t);
this.stats['cpu-total'].values.push(1 - idle / total);
this._prev = cpu;
}
};
const MemoryIndicator = class extends Indicator {
constructor() {
super();
this.gridColor = '-grid-color';
this.renderStats = ['mem-user', 'mem-other', 'mem-cached'];
// Make sure renderStats is sorted as necessary for rendering
let renderStatOrder = { 'mem-cached': 0, 'mem-other': 1, 'mem-user': 2 };
this.renderStats = this.renderStats.sort((a, b) => {
return renderStatOrder[a] - renderStatOrder[b];
});
this.setLabelText(_('Memory'));
}
_initValues() {
this.mem = new GTop.glibtop_mem;
this.stats = {
'mem-user': { color: '-mem-user-color', values: [] },
'mem-other': { color: '-mem-other-color', values: [] },
'mem-cached': { color: '-mem-cached-color', values: [] }
};
}
_updateValues() {
GTop.glibtop_get_mem(this.mem);
let t = this.mem.user / this.mem.total;
this.stats['mem-user'].values.push(t);
t += (this.mem.used - this.mem.user - this.mem.cached) / this.mem.total;
this.stats['mem-other'].values.push(t);
t += this.mem.cached / this.mem.total;
this.stats['mem-cached'].values.push(t);
}
};
class SystemMonitorSection extends MessageList.MessageListSection {
constructor() {
super(_('System Monitor'));
}
_onTitleClicked() {
super._onTitleClicked();
let appSys = Shell.AppSystem.get_default();
let app = appSys.lookup_app('gnome-system-monitor.desktop');
if (app)
app.open_new_window(-1);
}
}
const INDICATORS = [CpuIndicator, MemoryIndicator];
class Extension {
constructor() {
ExtensionUtils.initTranslations();
this._showLabelTimeoutId = 0;
this._resetHoverTimeoutId = 0;
this._labelShowing = false;
}
enable() {
this._section = new SystemMonitorSection();
this._indicators = [];
for (let i = 0; i < INDICATORS.length; i++) {
let indicator = new (INDICATORS[i])();
indicator.actor.connect('notify::hover', () => {
this._onHover(indicator);
});
this._section.addMessage(indicator, false);
this._indicators.push(indicator);
}
Main.panel.statusArea.dateMenu._messageList._addSection(this._section);
this._section.actor.get_parent().set_child_at_index(this._section.actor, 0);
}
disable() {
this._indicators.forEach(i => i.destroy());
Main.panel.statusArea.dateMenu._messageList._removeSection(this._section);
}
_onHover(item) {
if (item.actor.get_hover()) {
if (this._showLabelTimeoutId)
return;
let timeout = this._labelShowing ? 0 : ITEM_HOVER_TIMEOUT;
this._showLabelTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
timeout,
() => {
this._labelShowing = true;
item.showLabel();
this._showLabelTimeoutId = 0;
return GLib.SOURCE_REMOVE;
});
if (this._resetHoverTimeoutId > 0) {
GLib.source_remove(this._resetHoverTimeoutId);
this._resetHoverTimeoutId = 0;
}
} else {
if (this._showLabelTimeoutId > 0)
GLib.source_remove(this._showLabelTimeoutId);
this._showLabelTimeoutId = 0;
item.hideLabel();
if (!this._labelShowing)
return;
this._resetHoverTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
ITEM_HOVER_TIMEOUT,
() => {
this._labelShowing = false;
return GLib.SOURCE_REMOVE;
});
}
}
}
function init() {
return new Extension();
}