extension.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. import Clutter from 'gi://Clutter';
  2. import Gio from 'gi://Gio';
  3. import GLib from 'gi://GLib';
  4. import GObject from 'gi://GObject';
  5. import St from 'gi://St'
  6. import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
  7. import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
  8. import * as Main from 'resource:///org/gnome/shell/ui/main.js';
  9. import * as Util from 'resource:///org/gnome/shell/misc/util.js';
  10. import * as Sensors from './sensors.js';
  11. import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
  12. import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js';
  13. import * as Values from './values.js';
  14. import * as Config from 'resource:///org/gnome/shell/misc/config.js';
  15. import * as MenuItem from './menuItem.js';
  16. let vitalsMenu;
  17. var VitalsMenuButton = GObject.registerClass({
  18. GTypeName: 'VitalsMenuButton',
  19. }, class VitalsMenuButton extends PanelMenu.Button {
  20. _init(extensionObject) {
  21. super._init(Clutter.ActorAlign.FILL);
  22. this._extensionObject = extensionObject;
  23. this._settings = extensionObject.getSettings();
  24. this._sensorIcons = {
  25. 'temperature' : { 'icon': 'temperature-symbolic.svg' },
  26. 'voltage' : { 'icon': 'voltage-symbolic.svg' },
  27. 'fan' : { 'icon': 'fan-symbolic.svg' },
  28. 'memory' : { 'icon': 'memory-symbolic.svg' },
  29. 'processor' : { 'icon': 'cpu-symbolic.svg' },
  30. 'system' : { 'icon': 'system-symbolic.svg' },
  31. 'network' : { 'icon': 'network-symbolic.svg',
  32. 'icon-rx': 'network-download-symbolic.svg',
  33. 'icon-tx': 'network-upload-symbolic.svg' },
  34. 'storage' : { 'icon': 'storage-symbolic.svg' },
  35. 'battery' : { 'icon': 'battery-symbolic.svg' },
  36. 'gpu' : { 'icon': 'gpu-symbolic.svg' }
  37. }
  38. // list with the prefixes for the according themes, the index of each
  39. // item must match the index on the combo box
  40. this._sensorsIconPathPrefix = ['/icons/original/', '/icons/gnome/'];
  41. this._warnings = [];
  42. this._sensorMenuItems = {};
  43. this._hotLabels = {};
  44. this._hotIcons = {};
  45. this._groups = {};
  46. this._widths = {};
  47. this._numGpus = 1;
  48. this._newGpuDetected = false;
  49. this._newGpuDetectedCount = 0;
  50. this._last_query = new Date().getTime();
  51. this._sensors = new Sensors.Sensors(this._settings, this._sensorIcons);
  52. this._values = new Values.Values(this._settings, this._sensorIcons);
  53. this._menuLayout = new St.BoxLayout({
  54. vertical: false,
  55. clip_to_allocation: true,
  56. x_align: Clutter.ActorAlign.START,
  57. y_align: Clutter.ActorAlign.CENTER,
  58. reactive: true,
  59. x_expand: true,
  60. pack_start: false
  61. });
  62. this._drawMenu();
  63. this.add_child(this._menuLayout);
  64. this._settingChangedSignals = [];
  65. this._refreshTimeoutId = null;
  66. this._addSettingChangedSignal('update-time', this._updateTimeChanged.bind(this));
  67. this._addSettingChangedSignal('position-in-panel', this._positionInPanelChanged.bind(this));
  68. this._addSettingChangedSignal('menu-centered', this._positionInPanelChanged.bind(this));
  69. this._addSettingChangedSignal('icon-style', this._iconStyleChanged.bind(this));
  70. let settings = [ 'use-higher-precision', 'alphabetize', 'hide-zeros', 'fixed-widths', 'hide-icons', 'unit', 'memory-measurement', 'include-public-ip', 'network-speed-format', 'storage-measurement', 'include-static-info', 'include-static-gpu-info' ];
  71. for (let setting of Object.values(settings))
  72. this._addSettingChangedSignal(setting, this._redrawMenu.bind(this));
  73. // add signals for show- preference based categories
  74. for (let sensor in this._sensorIcons)
  75. this._addSettingChangedSignal('show-' + sensor, this._showHideSensorsChanged.bind(this));
  76. this._initializeMenu();
  77. // start off with fresh sensors
  78. this._querySensors();
  79. // start monitoring sensors
  80. this._initializeTimer();
  81. }
  82. _initializeMenu() {
  83. // display sensor categories
  84. for (let sensor in this._sensorIcons) {
  85. // groups associated sensors under accordion menu
  86. if (sensor in this._groups) continue;
  87. //handle gpus separately.
  88. if (sensor === 'gpu') continue;
  89. this._initializeMenuGroup(sensor, sensor);
  90. }
  91. for (let i = 1; i <= this._numGpus; i++)
  92. this._initializeMenuGroup('gpu#' + i, 'gpu', (this._numGpus > 1 ? ' ' + i : ''));
  93. // add separator
  94. this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
  95. let item = new PopupMenu.PopupBaseMenuItem({
  96. reactive: false,
  97. style_class: 'vitals-menu-button-container'
  98. });
  99. let customButtonBox = new St.BoxLayout({
  100. style_class: 'vitals-button-box',
  101. vertical: false,
  102. clip_to_allocation: true,
  103. x_align: Clutter.ActorAlign.CENTER,
  104. y_align: Clutter.ActorAlign.CENTER,
  105. reactive: true,
  106. x_expand: true,
  107. pack_start: false
  108. });
  109. // custom round refresh button
  110. let refreshButton = this._createRoundButton('view-refresh-symbolic', _('Refresh'));
  111. refreshButton.connect('clicked', (self) => {
  112. // force refresh by clearing history
  113. this._sensors.resetHistory();
  114. this._values.resetHistory(this._numGpus);
  115. // make sure timer fires at next full interval
  116. this._updateTimeChanged();
  117. // refresh sensors now
  118. this._querySensors();
  119. });
  120. customButtonBox.add_child(refreshButton);
  121. // custom round monitor button
  122. let monitorButton = this._createRoundButton('org.gnome.SystemMonitor-symbolic', _('System Monitor'));
  123. monitorButton.connect('clicked', (self) => {
  124. this.menu._getTopMenu().close();
  125. Util.spawn(this._settings.get_string('monitor-cmd').split(" "));
  126. });
  127. customButtonBox.add_child(monitorButton);
  128. // custom round preferences button
  129. let prefsButton = this._createRoundButton('preferences-system-symbolic', _('Preferences'));
  130. prefsButton.connect('clicked', (self) => {
  131. this.menu._getTopMenu().close();
  132. this._extensionObject.openPreferences();
  133. });
  134. customButtonBox.add_child(prefsButton);
  135. // now add the buttons to the top bar
  136. item.actor.add_child(customButtonBox);
  137. // add buttons
  138. this.menu.addMenuItem(item);
  139. // query sensors on menu open
  140. this._menuStateChangeId = this.menu.connect('open-state-changed', (self, isMenuOpen) => {
  141. if (isMenuOpen) {
  142. // make sure timer fires at next full interval
  143. this._updateTimeChanged();
  144. // refresh sensors now
  145. this._querySensors();
  146. }
  147. });
  148. }
  149. _initializeMenuGroup(groupName, optionName, menuSuffix = '', position = -1) {
  150. this._groups[groupName] = new PopupMenu.PopupSubMenuMenuItem(_(this._ucFirst(groupName) + menuSuffix), true);
  151. this._groups[groupName].icon.gicon = Gio.icon_new_for_string(this._sensorIconPath(groupName));
  152. // hide menu items that user has requested to not include
  153. if (!this._settings.get_boolean('show-' + optionName))
  154. this._groups[groupName].actor.hide();
  155. if (!this._groups[groupName].status) {
  156. this._groups[groupName].status = this._defaultLabel();
  157. this._groups[groupName].actor.insert_child_at_index(this._groups[groupName].status, 4);
  158. this._groups[groupName].status.text = _('No Data');
  159. }
  160. if(position == -1) this.menu.addMenuItem(this._groups[groupName]);
  161. else this.menu.addMenuItem(this._groups[groupName], position);
  162. }
  163. _createRoundButton(iconName) {
  164. let button = new St.Button({
  165. style_class: 'message-list-clear-button button vitals-button-action'
  166. });
  167. button.child = new St.Icon({
  168. icon_name: iconName
  169. });
  170. return button;
  171. }
  172. _removeMissingHotSensors(hotSensors) {
  173. for (let i = hotSensors.length - 1; i >= 0; i--) {
  174. let sensor = hotSensors[i];
  175. // make sure default icon (if any) stays visible
  176. if (sensor == '_default_icon_') continue;
  177. // removes sensors that are no longer available
  178. if (!this._sensorMenuItems[sensor]) {
  179. hotSensors.splice(i, 1);
  180. this._removeHotLabel(sensor);
  181. this._removeHotIcon(sensor);
  182. }
  183. }
  184. return hotSensors;
  185. }
  186. _saveHotSensors(hotSensors) {
  187. // removes any sensors that may not currently be available
  188. hotSensors = this._removeMissingHotSensors(hotSensors);
  189. this._settings.set_strv('hot-sensors', hotSensors.filter(
  190. function(item, pos) {
  191. return hotSensors.indexOf(item) == pos;
  192. }
  193. ));
  194. }
  195. _initializeTimer() {
  196. // used to query sensors and update display
  197. let update_time = this._settings.get_int('update-time');
  198. this._refreshTimeoutId = GLib.timeout_add_seconds(
  199. GLib.PRIORITY_DEFAULT,
  200. update_time,
  201. (self) => {
  202. // only update menu if we have hot sensors
  203. if (Object.values(this._hotLabels).length > 0)
  204. this._querySensors();
  205. // keep the timer running
  206. return GLib.SOURCE_CONTINUE;
  207. }
  208. );
  209. }
  210. _createHotItem(key, value) {
  211. let icon = this._defaultIcon(key);
  212. this._hotIcons[key] = icon;
  213. this._menuLayout.add_child(icon)
  214. // don't add a label when no sensors are in the panel
  215. if (key == '_default_icon_') return;
  216. let label = new St.Label({
  217. style_class: 'vitals-panel-label',
  218. text: (value)?value:'\u2026', // ...
  219. y_expand: true,
  220. y_align: Clutter.ActorAlign.CENTER
  221. });
  222. // attempt to prevent ellipsizes
  223. label.get_clutter_text().ellipsize = 0;
  224. // keep track of label for removal later
  225. this._hotLabels[key] = label;
  226. // prevent "called on the widget" "which is not in the stage" errors by adding before width below
  227. this._menuLayout.add_child(label);
  228. // support for fixed widths #55, save label (text) width
  229. this._widths[key] = label.width;
  230. }
  231. _showHideSensorsChanged(self, sensor) {
  232. this._sensors.resetHistory();
  233. const sensorName = sensor.substr(5);
  234. if(sensorName === 'gpu') {
  235. for(let i = 1; i <= this._numGpus; i++)
  236. this._groups[sensorName + '#' + i].visible = this._settings.get_boolean(sensor);
  237. } else
  238. this._groups[sensorName].visible = this._settings.get_boolean(sensor);
  239. }
  240. _positionInPanelChanged() {
  241. this.container.get_parent().remove_child(this.container);
  242. let position = this._positionInPanel();
  243. // allows easily addressable boxes
  244. let boxes = {
  245. left: Main.panel._leftBox,
  246. center: Main.panel._centerBox,
  247. right: Main.panel._rightBox
  248. };
  249. // update position when changed from preferences
  250. boxes[position[0]].insert_child_at_index(this.container, position[1]);
  251. }
  252. _redrawDetailsMenuIcons() {
  253. // updates the icons on the 'details' menu, the one
  254. // you have to click to appear
  255. this._sensors.resetHistory();
  256. for (const sensor in this._sensorIcons) {
  257. if (sensor == "gpu") continue;
  258. this._groups[sensor].icon.gicon = Gio.icon_new_for_string(this._sensorIconPath(sensor));
  259. }
  260. // gpu's are indexed differently, handle them here
  261. const gpuKeys = Object.keys(this._groups).filter(key => key.startsWith("gpu#"));
  262. gpuKeys.forEach((gpuKey) => {
  263. this._groups[gpuKey].icon.gicon = Gio.icon_new_for_string(this._sensorIconPath("gpu"));
  264. });
  265. }
  266. _iconStyleChanged() {
  267. this._redrawDetailsMenuIcons();
  268. this._redrawMenu();
  269. }
  270. _removeHotLabel(key) {
  271. if (key in this._hotLabels) {
  272. let label = this._hotLabels[key];
  273. delete this._hotLabels[key];
  274. // make sure set_label is not called on non existent actor
  275. label.destroy();
  276. }
  277. }
  278. _removeHotLabels() {
  279. for (let key in this._hotLabels)
  280. this._removeHotLabel(key);
  281. }
  282. _removeHotIcon(key) {
  283. if (key in this._hotIcons) {
  284. this._hotIcons[key].destroy();
  285. delete this._hotIcons[key];
  286. }
  287. }
  288. _removeHotIcons() {
  289. for (let key in this._hotIcons)
  290. this._removeHotIcon(key);
  291. }
  292. _redrawMenu() {
  293. this._removeHotIcons();
  294. this._removeHotLabels();
  295. for (let key in this._sensorMenuItems) {
  296. if (key.includes('-group')) continue;
  297. this._sensorMenuItems[key].destroy();
  298. delete this._sensorMenuItems[key];
  299. }
  300. this._drawMenu();
  301. this._sensors.resetHistory();
  302. this._values.resetHistory(this._numGpus);
  303. this._querySensors();
  304. }
  305. _drawMenu() {
  306. // grab list of selected menubar icons
  307. let hotSensors = this._settings.get_strv('hot-sensors');
  308. for (let key of Object.values(hotSensors)) {
  309. // fixes issue #225 which started when _max_ was moved to the end
  310. if (key == '__max_network-download__') key = '__network-rx_max__';
  311. if (key == '__max_network-upload__') key = '__network-tx_max__';
  312. this._createHotItem(key);
  313. }
  314. }
  315. _destroyTimer() {
  316. // invalidate and reinitialize timer
  317. if (this._refreshTimeoutId != null) {
  318. GLib.Source.remove(this._refreshTimeoutId);
  319. this._refreshTimeoutId = null;
  320. }
  321. }
  322. _updateTimeChanged() {
  323. this._destroyTimer();
  324. this._initializeTimer();
  325. }
  326. _addSettingChangedSignal(key, callback) {
  327. this._settingChangedSignals.push(this._settings.connect('changed::' + key, callback));
  328. }
  329. _updateDisplay(label, value, type, key) {
  330. // update sensor value in menubar
  331. if (this._hotLabels[key]) {
  332. this._hotLabels[key].set_text(value);
  333. // support for fixed widths #55
  334. if (this._settings.get_boolean('fixed-widths')) {
  335. // grab text box width and see if new text is wider than old text
  336. let width2 = this._hotLabels[key].get_clutter_text().width;
  337. if (width2 > this._widths[key]) {
  338. this._hotLabels[key].set_width(width2);
  339. this._widths[key] = width2;
  340. }
  341. }
  342. }
  343. if(key === "_gpu#1_domain_number_")
  344. console.error('UPDATING: ', key);
  345. // have we added this sensor before?
  346. let item = this._sensorMenuItems[key];
  347. if (item) {
  348. // update sensor value in the group
  349. item.value = value;
  350. } else if (type.includes('-group')) {
  351. // update text next to group header
  352. let group = type.split('-')[0];
  353. if (this._groups[group]) {
  354. this._groups[group].status.text = value;
  355. this._sensorMenuItems[type] = this._groups[group];
  356. }
  357. } else {
  358. // add item to group for the first time
  359. let sensor = { 'label': label, 'value': value, 'type': type }
  360. this._appendMenuItem(sensor, key);
  361. }
  362. }
  363. _appendMenuItem(sensor, key) {
  364. let split = sensor.type.split('-');
  365. let type = split[0];
  366. let icon = (split.length == 2)?'icon-' + split[1]:'icon';
  367. let gicon = Gio.icon_new_for_string(this._sensorIconPath(type, icon));
  368. let item = new MenuItem.MenuItem(gicon, key, sensor.label, sensor.value, this._hotLabels[key]);
  369. item.connect('toggle', (self) => {
  370. let hotSensors = this._settings.get_strv('hot-sensors');
  371. if (self.checked) {
  372. // add selected sensor to panel
  373. hotSensors.push(self.key);
  374. this._createHotItem(self.key, self.value);
  375. } else {
  376. // remove selected sensor from panel
  377. hotSensors.splice(hotSensors.indexOf(self.key), 1);
  378. this._removeHotLabel(self.key);
  379. this._removeHotIcon(self.key);
  380. }
  381. if (hotSensors.length <= 0) {
  382. // add generic icon to panel when no sensors are selected
  383. hotSensors.push('_default_icon_');
  384. this._createHotItem('_default_icon_');
  385. } else {
  386. let defIconPos = hotSensors.indexOf('_default_icon_');
  387. if (defIconPos >= 0) {
  388. // remove generic icon from panel when sensors are selected
  389. hotSensors.splice(defIconPos, 1);
  390. this._removeHotIcon('_default_icon_');
  391. }
  392. }
  393. // this code is called asynchronously - make sure to save it for next round
  394. this._saveHotSensors(hotSensors);
  395. });
  396. this._sensorMenuItems[key] = item;
  397. let i = Object.keys(this._sensorMenuItems[key]).length;
  398. // alphabetize the sensors for these categories
  399. if (this._settings.get_boolean('alphabetize')) {
  400. let menuItems = this._groups[type].menu._getMenuItems();
  401. for (i = 0; i < menuItems.length; i++)
  402. // use natural sort order for system load, etc
  403. if (menuItems[i].label.localeCompare(item.label, undefined, { numeric: true, sensitivity: 'base' }) > 0)
  404. break;
  405. }
  406. this._groups[type].menu.addMenuItem(item, i);
  407. }
  408. _defaultLabel() {
  409. return new St.Label({
  410. y_expand: true,
  411. y_align: Clutter.ActorAlign.CENTER
  412. });
  413. }
  414. _defaultIcon(key) {
  415. let split = key.replaceAll('_', ' ').trim().split(' ')[0].split('-');
  416. let type = split[0];
  417. let icon = new St.Icon({
  418. style_class: 'system-status-icon vitals-panel-icon-' + type,
  419. reactive: true
  420. });
  421. // second condition prevents crash due to issue #225, which started when _max_ was moved to the end
  422. // don't use the default system icon if the type is a gpu; use the universal gpu icon instead
  423. if (type == 'default' || (!(type in this._sensorIcons) && !type.startsWith('gpu'))) {
  424. icon.gicon = Gio.icon_new_for_string(this._sensorIconPath('system'));
  425. } else if (!this._settings.get_boolean('hide-icons')) { // support for hide icons #80
  426. let iconObj = (split.length == 2)?'icon-' + split[1]:'icon';
  427. icon.gicon = Gio.icon_new_for_string(this._sensorIconPath(type, iconObj));
  428. }
  429. return icon;
  430. }
  431. _sensorIconPath(sensor, icon = 'icon') {
  432. // If the sensor is a numbered gpu, use the gpu icon. Otherwise use whatever icon associated with the sensor name.
  433. let sensorKey = sensor;
  434. if(sensor.startsWith('gpu')) sensorKey = 'gpu';
  435. const iconPathPrefixIndex = this._settings.get_int('icon-style');
  436. return this._extensionObject.path + this._sensorsIconPathPrefix[iconPathPrefixIndex] + this._sensorIcons[sensorKey][icon];
  437. }
  438. _ucFirst(string) {
  439. if(string.startsWith('gpu')) return 'Graphics';
  440. return string.charAt(0).toUpperCase() + string.slice(1);
  441. }
  442. _positionInPanel() {
  443. let alignment = '';
  444. let gravity = 0;
  445. let arrow_pos = 0;
  446. switch (this._settings.get_int('position-in-panel')) {
  447. case 0: // left
  448. alignment = 'left';
  449. gravity = -1;
  450. arrow_pos = 1;
  451. break;
  452. case 1: // center
  453. alignment = 'center';
  454. gravity = -1;
  455. arrow_pos = 0.5;
  456. break;
  457. case 2: // right
  458. alignment = 'right';
  459. gravity = 0;
  460. arrow_pos = 0;
  461. break;
  462. case 3: // far left
  463. alignment = 'left';
  464. gravity = 0;
  465. arrow_pos = 1;
  466. break;
  467. case 4: // far right
  468. alignment = 'right';
  469. gravity = -1;
  470. arrow_pos = 0;
  471. break;
  472. }
  473. let centered = this._settings.get_boolean('menu-centered')
  474. if (centered) arrow_pos = 0.5;
  475. // set arrow position when initializing and moving vitals
  476. this.menu._arrowAlignment = arrow_pos;
  477. return [alignment, gravity];
  478. }
  479. _querySensors() {
  480. // figure out last run time
  481. let now = new Date().getTime();
  482. let dwell = (now - this._last_query) / 1000;
  483. this._last_query = now;
  484. this._sensors.query((label, value, type, format) => {
  485. const typeKey = type.replace('-group', '');
  486. let key = '_' + typeKey + '_' + label.replace(' ', '_').toLowerCase() + '_';
  487. // if a sensor is disabled, gray it out
  488. if (key in this._sensorMenuItems) {
  489. this._sensorMenuItems[key].setSensitive((value!='disabled'));
  490. // don't continue below, last known value is shown
  491. if (value == 'disabled') return;
  492. }
  493. // add/initialize any gpu groups that we haven't added yet
  494. if(typeKey.startsWith('gpu') && typeKey !== 'gpu#1') {
  495. const split = typeKey.split('#');
  496. if(split.length == 2 && this._numGpus < parseInt(split[1])) {
  497. // occasionally two lines from nvidia-smi will be read at once
  498. // so we only actually update the number of gpus if we have recieved multiple lines at least 3 times in a row
  499. // i.e. we make sure that mutiple queries have detected a new gpu back-to-back
  500. if(this._newGpuDetectedCount < 2) {
  501. this._newGpuDetected = true;
  502. return;
  503. }
  504. this._numGpus = parseInt(split[1]);
  505. this._newGpuDetectedCount = 0;
  506. this._newGpuDetected = false;
  507. // change label for gpu 1 from "Graphics" to "Graphics 1" since we have multiple gpus now
  508. this._groups['gpu#1'].label.text = this._ucFirst('gpu#1') + ' 1';
  509. for(let i = 2; i <= this._numGpus; i++)
  510. if(!('gpu#' + i in this._groups))
  511. this._initializeMenuGroup('gpu#' + i, 'gpu', ' ' + i, Object.keys(this._groups).length);
  512. }
  513. }
  514. let items = this._values.returnIfDifferent(dwell, label, value, type, format, key);
  515. for (let item of Object.values(items))
  516. this._updateDisplay(_(item[0]), item[1], item[2], item[3]);
  517. }, dwell);
  518. //if a new gpu has been detected during the last query, then increment the amount of times we've detected a new gpu
  519. if(this._newGpuDetected) this._newGpuDetectedCount++;
  520. else this._newGpuDetectedCount = 0;
  521. this._newGpuDetected = false;
  522. if (this._warnings.length > 0) {
  523. this._notify('Vitals', this._warnings.join("\n"), 'folder-symbolic');
  524. this._warnings = [];
  525. }
  526. }
  527. _notify(msg, details, icon) {
  528. let source = new MessageTray.Source('MyApp Information', icon);
  529. Main.messageTray.add(source);
  530. let notification = new MessageTray.Notification(source, msg, details);
  531. notification.setTransient(true);
  532. source.notify(notification);
  533. }
  534. destroy() {
  535. this._destroyTimer();
  536. this._sensors.destroy();
  537. for (let signal of Object.values(this._settingChangedSignals))
  538. this._settings.disconnect(signal);
  539. super.destroy();
  540. }
  541. });
  542. export default class VitalsExtension extends Extension {
  543. enable() {
  544. vitalsMenu = new VitalsMenuButton(this);
  545. let position = vitalsMenu._positionInPanel();
  546. Main.panel.addToStatusArea('vitalsMenu', vitalsMenu, position[1], position[0]);
  547. }
  548. disable() {
  549. vitalsMenu.destroy();
  550. vitalsMenu = null;
  551. }
  552. }