/* This file is part of Arch Linux Updates Indicator Arch Linux Updates Indicator is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Arch Linux Updates Indicator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Arch Linux Updates Indicator. If not, see . Copyright 2016 Raphaël Rochet */ const Clutter = imports.gi.Clutter; const St = imports.gi.St; const GObject = imports.gi.GObject; const GLib = imports.gi.GLib; const Gio = imports.gi.Gio; const Gtk = imports.gi.Gtk; const Main = imports.ui.main; const Panel = imports.ui.panel; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; const MessageTray = imports.ui.messageTray; const Util = imports.misc.util; const ExtensionUtils = imports.misc.extensionUtils; const ExtensionManager = imports.ui.main.extensionManager; const Me = ExtensionUtils.getCurrentExtension(); const Format = imports.format; const Gettext = imports.gettext.domain('arch-update'); const _ = Gettext.gettext; /* Options */ let ALWAYS_VISIBLE = true; let USE_BUILDIN_ICONS = true; let SHOW_COUNT = true; let BOOT_WAIT = 15; // 15s let CHECK_INTERVAL = 60*60; // 1h let NOTIFY = false; let HOWMUCH = 0; let TRANSIENT = true; let UPDATE_CMD = "gnome-terminal -e 'sh -c \"sudo pacman -Syu ; echo Done - Press enter to exit; read _\" '"; let CHECK_CMD = "/usr/bin/checkupdates"; let MANAGER_CMD = ""; let PACMAN_DIR = "/var/lib/pacman/local"; let STRIP_VERSIONS = true; let AUTO_EXPAND_LIST = 0; /* Variables we want to keep when extension is disabled (eg during screen lock) */ let FIRST_BOOT = 1; let UPDATES_PENDING = -1; let UPDATES_LIST = []; function init() { String.prototype.format = Format.format; ExtensionUtils.initTranslations("arch-update"); } const ArchUpdateIndicator = GObject.registerClass( { _TimeoutId: null, _FirstTimeoutId: null, _updateProcess_sourceId: null, _updateProcess_stream: null, _updateProcess_pid: null, _updateList: [], }, class ArchUpdateIndicator extends PanelMenu.Button { _init() { super._init(0); this.updateIcon = new St.Icon({gicon: this._getCustIcon('arch-unknown-symbolic'), style_class: 'system-status-icon'}); let box = new St.BoxLayout({ vertical: false, style_class: 'panel-status-menu-box' }); this.label = new St.Label({ text: '', y_expand: true, y_align: Clutter.ActorAlign.CENTER }); box.add_child(this.updateIcon); box.add_child(this.label); this.add_child(box); // Prepare the special menu : a submenu for updates list that will look like a regular menu item when disabled // Scrollability will also be taken care of by the popupmenu this.menuExpander = new PopupMenu.PopupSubMenuMenuItem(''); this.menuExpander.menu.box.style_class = 'arch-updates-list'; // Other standard menu items let settingsMenuItem = new PopupMenu.PopupMenuItem(_('Settings')); this.updateNowMenuItem = new PopupMenu.PopupMenuItem(_('Update now')); this.managerMenuItem = new PopupMenu.PopupMenuItem(_('Open package manager')); // A special "Checking" menu item with a stop button this.checkingMenuItem = new PopupMenu.PopupBaseMenuItem( {reactive:false} ); let checkingLabel = new St.Label({ text: _('Checking') + " …" }); let cancelButton = new St.Button({ child: new St.Icon({ icon_name: 'process-stop-symbolic' }), style_class: 'system-menu-action arch-updates-menubutton', x_expand: true }); cancelButton.set_x_align(Clutter.ActorAlign.END); this.checkingMenuItem.actor.add_actor( checkingLabel ); this.checkingMenuItem.actor.add_actor( cancelButton ); // A little trick on "check now" menuitem to keep menu opened this.checkNowMenuItem = new PopupMenu.PopupMenuItem( _('Check now') ); this.checkNowMenuContainer = new PopupMenu.PopupMenuSection(); this.checkNowMenuContainer.actor.add_actor(this.checkNowMenuItem.actor); // Assemble all menu items into the popup menu this.menu.addMenuItem(this.menuExpander); this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); this.menu.addMenuItem(this.updateNowMenuItem); this.menu.addMenuItem(this.checkingMenuItem); this.menu.addMenuItem(this.checkNowMenuContainer); this.menu.addMenuItem(this.managerMenuItem); this.menu.addMenuItem(settingsMenuItem); // Bind some events this.menu.connect('open-state-changed', this._onMenuOpened.bind(this)); this.checkNowMenuItem.connect('activate', this._checkUpdates.bind(this)); cancelButton.connect('clicked', this._cancelCheck.bind(this)); settingsMenuItem.connect('activate', this._openSettings.bind(this)); this.updateNowMenuItem.connect('activate', this._updateNow.bind(this)); this.managerMenuItem.connect('activate', this._openManager.bind(this)); // Some initial status display this._showChecking(false); this._updateMenuExpander(false, _('Waiting first check')); // Restore previous updates list if any this._updateList = UPDATES_LIST; // Load settings this._settings = ExtensionUtils.getSettings('org.gnome.shell.extensions.arch-update'); this._settings.connect('changed', this._positionChanged.bind(this)); this._settingsChangedId = this._settings.connect('changed', this._applySettings.bind(this)); this._applySettings(); // Start monitoring external changes this._startFolderMonitor(); if (FIRST_BOOT) { // Schedule first check only if this is the first extension load // This won't be run again if extension is disabled/enabled (like when screen is locked) let that = this; this._FirstTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, BOOT_WAIT, function () { that._checkUpdates(); that._FirstTimeoutId = null; FIRST_BOOT = 0; return false; // Run once }); } } _getCustIcon(icon_name) { // I did not find a way to lookup icon via Gio, so use Gtk // I couldn't find why, but get_default is sometimes null, hence this additional test if (!USE_BUILDIN_ICONS && Gtk.IconTheme.get_default()) { if (Gtk.IconTheme.get_default().has_icon(icon_name)) { return Gio.icon_new_for_string( icon_name ); } } // Icon not available in theme, or user prefers built in icon return Gio.icon_new_for_string( Me.dir.get_child('icons').get_path() + "/" + icon_name + ".svg" ); } _positionChanged(){ this.container.get_parent().remove_actor(this.container); let boxes = { 0: Main.panel._leftBox, 1: Main.panel._centerBox, 2: Main.panel._rightBox }; let p = this._settings.get_int('position'); let i = this._settings.get_int('position-number'); boxes[p].insert_child_at_index(this.container, i); } _openSettings() { Gio.DBus.session.call( 'org.gnome.Shell.Extensions', '/org/gnome/Shell/Extensions', 'org.gnome.Shell.Extensions', 'OpenExtensionPrefs', new GLib.Variant('(ssa{sv})', [Me.uuid, '', {}]), null, Gio.DBusCallFlags.NONE, -1, null); } _openManager() { Util.spawnCommandLine(MANAGER_CMD); } _updateNow() { Util.spawnCommandLine(UPDATE_CMD); } _applySettings() { ALWAYS_VISIBLE = this._settings.get_boolean('always-visible'); USE_BUILDIN_ICONS = this._settings.get_boolean('use-buildin-icons'); SHOW_COUNT = this._settings.get_boolean('show-count'); BOOT_WAIT = this._settings.get_int('boot-wait'); CHECK_INTERVAL = 60 * this._settings.get_int('check-interval'); NOTIFY = this._settings.get_boolean('notify'); HOWMUCH = this._settings.get_int('howmuch'); TRANSIENT = this._settings.get_boolean('transient'); UPDATE_CMD = this._settings.get_string('update-cmd'); CHECK_CMD = this._settings.get_string('check-cmd'); MANAGER_CMD = this._settings.get_string('package-manager'); PACMAN_DIR = this._settings.get_string('pacman-dir'); STRIP_VERSIONS = this._settings.get_boolean('strip-versions'); AUTO_EXPAND_LIST = this._settings.get_int('auto-expand-list'); this.managerMenuItem.actor.visible = ( MANAGER_CMD != "" ); this._checkShowHide(); this._updateStatus(); let that = this; if (this._TimeoutId) GLib.source_remove(this._TimeoutId); this._TimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, CHECK_INTERVAL, function () { that._checkUpdates(); return true; }); } destroy() { this._settings.disconnect( this._settingsChangedId ); if (this._notifSource) { // Delete the notification source, which lay still have a notification shown this._notifSource.destroy(); this._notifSource = null; }; if (this.monitor) { // Stop spying on pacman local dir this.monitor.cancel(); this.monitor = null; } if (this._updateProcess_sourceId) { // We leave the checkupdate process end by itself but undef handles to avoid zombies GLib.source_remove(this._updateProcess_sourceId); this._updateProcess_sourceId = null; this._updateProcess_stream = null; } if (this._FirstTimeoutId) { GLib.source_remove(this._FirstTimeoutId); this._FirstTimeoutId = null; } if (this._TimeoutId) { GLib.source_remove(this._TimeoutId); this._TimeoutId = null; } super.destroy(); } _checkShowHide() { if ( UPDATES_PENDING == -3 ) { // Do not apply visibility change while checking for updates return; } if (!ALWAYS_VISIBLE && UPDATES_PENDING < 1) { this.visible = false; } else { this.visible = true; } this.label.visible = SHOW_COUNT && UPDATES_PENDING > 0; } _onMenuOpened() { // This event is fired when menu is shown or hidden // Only open the submenu if the menu is being opened and there is something to show this._checkAutoExpandList(); } _checkAutoExpandList() { if (this.menu.isOpen && UPDATES_PENDING > 0 && UPDATES_PENDING <= AUTO_EXPAND_LIST) { this.menuExpander.setSubmenuShown(true); } else { this.menuExpander.setSubmenuShown(false); } } _startFolderMonitor() { if (PACMAN_DIR) { this.pacman_dir = Gio.file_new_for_path(PACMAN_DIR); this.monitor = this.pacman_dir.monitor_directory(0, null); this.monitor.connect('changed', this._onFolderChanged.bind(this)); } } _onFolderChanged() { // Folder have changed ! Let's schedule a check in a few seconds let that = this; if (this._FirstTimeoutId) GLib.source_remove(this._FirstTimeoutId); this._FirstTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 5, function () { that._checkUpdates(); that._FirstTimeoutId = null; return false; }); } _showChecking(isChecking) { if (isChecking == true) { this.updateIcon.set_gicon( this._getCustIcon('arch-unknown-symbolic') ); this.checkNowMenuContainer.actor.visible = false; this.checkingMenuItem.actor.visible = true;; } else { this.checkNowMenuContainer.actor.visible = true;; this.checkingMenuItem.actor.visible = false;; } } _updateStatus(updatesCount) { updatesCount = typeof updatesCount === 'number' ? updatesCount : UPDATES_PENDING; if (updatesCount > 0) { // Updates pending this.updateIcon.set_gicon( this._getCustIcon('arch-updates-symbolic') ); this._updateMenuExpander( true, Gettext.ngettext( "%d update pending", "%d updates pending", updatesCount ).format(updatesCount) ); this.label.set_text(updatesCount.toString()); if (NOTIFY && UPDATES_PENDING < updatesCount) { if (HOWMUCH > 0) { let updateList = []; if (HOWMUCH > 1) { updateList = this._updateList; } else { // Keep only packets that was not in the previous notification updateList = this._updateList.filter(function(pkg) { return UPDATES_LIST.indexOf(pkg) < 0 }); } if (updateList.length > 0) { // Show notification only if there's new updates this._showNotification( Gettext.ngettext( "New Arch Linux Update", "New Arch Linux Updates", updateList.length ), updateList.join(', ') ); } } else { this._showNotification( Gettext.ngettext( "New Arch Linux Update", "New Arch Linux Updates", updatesCount ), Gettext.ngettext( "There is %d update pending", "There are %d updates pending", updatesCount ).format(updatesCount) ); } } // Store the new list UPDATES_LIST = this._updateList; } else { this.label.set_text(''); if (updatesCount == -1) { // Unknown this.updateIcon.set_gicon( this._getCustIcon('arch-unknown-symbolic') ); this._updateMenuExpander( false, '' ); } else if (updatesCount == -2) { // Error this.updateIcon.set_gicon( this._getCustIcon('arch-error-symbolic') ); if ( this.lastUnknowErrorString.indexOf("/usr/bin/checkupdates") > 0 ) { // We do a special change here due to checkupdates moved to pacman-contrib this._updateMenuExpander( false, _("Note : you have to install pacman-contrib to use the 'checkupdates' script.") ); } else { this._updateMenuExpander( false, _('Error') + "\n" + this.lastUnknowErrorString ); } } else { // Up to date this.updateIcon.set_gicon( this._getCustIcon('arch-uptodate-symbolic') ); this._updateMenuExpander( false, _('Up to date :)') ); UPDATES_LIST = []; // Reset stored list } } UPDATES_PENDING = updatesCount; this._checkAutoExpandList(); this._checkShowHide(); } _updateMenuExpander(enabled, label) { this.menuExpander.menu.box.destroy_all_children(); if (label == "") { // No text, hide the menuitem this.menuExpander.actor.visible = false; } else { // We make our expander look like a regular menu label if disabled this.menuExpander.actor.reactive = enabled; this.menuExpander._triangle.visible = enabled; this.menuExpander.label.set_text(label); this.menuExpander.actor.visible = true; if (enabled && this._updateList.length > 0) { this._updateList.forEach( item => { this.menuExpander.menu.box.add( new St.Label({ text: item }) ); } ); } } // 'Update now' visibility is linked so let's save a few lines and set it here this.updateNowMenuItem.actor.reactive = enabled; } _checkUpdates() { if(this._updateProcess_sourceId) { // A check is already running ! Maybe we should kill it and run another one ? return; } // Run asynchronously, to avoid shell freeze - even for a 1s check this._showChecking(true); try { // Parse check command line let [parseok, argvp] = GLib.shell_parse_argv( CHECK_CMD ); if (!parseok) { throw 'Parse error' }; let [res, pid, in_fd, out_fd, err_fd] = GLib.spawn_async_with_pipes(null, argvp, null, GLib.SpawnFlags.DO_NOT_REAP_CHILD, null); // Let's buffer the command's output - that's a input for us ! this._updateProcess_stream = new Gio.DataInputStream({ base_stream: new Gio.UnixInputStream({fd: out_fd}) }); // We will process the output at once when it's done this._updateProcess_sourceId = GLib.child_watch_add(0, pid, () => {this._checkUpdatesRead()} ); this._updateProcess_pid = pid; } catch (err) { this._showChecking(false); this.lastUnknowErrorString = err.message.toString(); this._updateStatus(-2); } } _cancelCheck() { if (this._updateProcess_pid == null) { return; }; Util.spawnCommandLine( "kill " + this._updateProcess_pid ); this._updateProcess_pid = null; // Prevent double kill this._checkUpdatesEnd(); } _checkUpdatesRead() { // Read the buffered output let updateList = []; let out, size; do { [out, size] = this._updateProcess_stream.read_line_utf8(null); if (out) updateList.push(out); } while (out); // If version numbers should be stripped, do it if (STRIP_VERSIONS == true) { updateList = updateList.map(function(p) { // Try to keep only what's before the first space var chunks = p.split(" ",2); return chunks[0]; }); } this._updateList = updateList; this._checkUpdatesEnd(); } _checkUpdatesEnd() { // Free resources this._updateProcess_stream.close(null); this._updateProcess_stream = null; GLib.source_remove(this._updateProcess_sourceId); this._updateProcess_sourceId = null; this._updateProcess_pid = null; // Update indicator this._showChecking(false); this._updateStatus(this._updateList.length); } _showNotification(title, message) { if (this._notifSource == null) { // We have to prepare this only once this._notifSource = new MessageTray.SystemNotificationSource(); this._notifSource.createIcon = function() { let gicon = Gio.icon_new_for_string( Me.dir.get_child('icons').get_path() + "/arch-lit-symbolic.svg" ); return new St.Icon({ gicon: gicon }); }; // Take care of note leaving unneeded sources this._notifSource.connect('destroy', ()=>{this._notifSource = null;}); Main.messageTray.add(this._notifSource); } let notification = null; // We do not want to have multiple notifications stacked // instead we will update previous if (this._notifSource.notifications.length == 0) { notification = new MessageTray.Notification(this._notifSource, title, message); notification.addAction( _('Update now') , ()=>{this._updateNow();} ); } else { notification = this._notifSource.notifications[0]; notification.update( title, message, { clear: true }); } notification.setTransient(TRANSIENT); this._notifSource.showNotification(notification); } }); let archupdateindicator; function enable() { archupdateindicator = new ArchUpdateIndicator(); Main.panel.addToStatusArea('ArchUpdateIndicator', archupdateindicator); archupdateindicator._positionChanged(); } function disable() { archupdateindicator.destroy(); }