diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c8b9b37 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# top-most EditorConfig file +root = true + +[**] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true diff --git a/docker.svg b/docker.svg index 4fd3e74..64d1e28 100644 --- a/docker.svg +++ b/docker.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/extension.js b/extension.js index ad18e58..8688f8c 100644 --- a/extension.js +++ b/extension.js @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + 'use strict'; const Main = imports.ui.main; diff --git a/metadata.json b/metadata.json index 4b3a0d1..ce55b1b 100644 --- a/metadata.json +++ b/metadata.json @@ -1,19 +1,20 @@ { - "uuid": "docker_status@gpouilloux", - "shell-version": [ - "3.14", - "3.16", - "3.18", - "3.20", - "3.21", - "3.22", - "3.24", - "3.26", - "3.28", - "3.30", - "3.32" - ], - "description": "A status menu for managing docker containers.", - "name": "Docker Integration", - "url": "https://github.com/gpouilloux/gnome-shell-extension-docker" -} \ No newline at end of file + "uuid": "docker_status@gpouilloux", + "shell-version": [ + "3.14", + "3.16", + "3.18", + "3.20", + "3.21", + "3.22", + "3.24", + "3.26", + "3.28", + "3.30", + "3.32", + "3.34" + ], + "description": "A status menu for managing docker containers.", + "name": "Docker Integration", + "url": "https://github.com/gpouilloux/gnome-shell-extension-docker" +} diff --git a/src/docker.js b/src/docker.js index 015402f..f3a2a58 100644 --- a/src/docker.js +++ b/src/docker.js @@ -21,12 +21,70 @@ const GLib = imports.gi.GLib; const Gio = imports.gi.Gio; -var dockerCommandsToLabels = { - start: 'Start', - stop: 'Stop', - pause: 'Pause', - unpause: 'Unpause', - rm: 'Remove' +/** + * Dictionary for Docker actions + * @readonly + * @type {{Object.}} + */ +var DockerActions = Object.freeze({ + START: { + label: "Start", + isInteractive: false + }, + REMOVE: { + label: "Remove", + isInteractive: false + }, + OPEN_SHELL: { + label: "Open shell", + isInteractive: true + }, + RESTART: { + label: "Restart", + isInteractive: false + }, + PAUSE: { + label: "Pause", + isInteractive: false + }, + STOP: { + label: "Stop", + isInteractive: false + }, + UNPAUSE: { + label: "Unpause", + isInteractive: false + }, +}); + +/** + * Return the command associated to the given Docker action + * @param {DockerActions} dockerAction The Docker action + * @param {String} containerName The name of the container on which to run the command + * @returns {String} The complete Docker command to run + * @throws {Error} + */ + +const getDockerActionCommand = (dockerAction, containerName) => { + switch (dockerAction) { + case DockerActions.START: + return "docker start " + containerName; + case DockerActions.REMOVE: + return "docker rm " + containerName; + case DockerActions.OPEN_SHELL: + return "docker exec -it " + containerName + " /bin/bash; " + + "if [ $? -ne 0 ]; then docker exec -it " + containerName + " /bin/sh; fi;"; + case DockerActions.RESTART: + return "docker restart " + containerName; + case DockerActions.PAUSE: + return "docker pause " + containerName; + case DockerActions.STOP: + return "docker stop " + containerName; + case DockerActions.UNPAUSE: + return "docker unpause " + containerName; + default: + throw new Error("Docker action not valid"); + } }; /** @@ -82,18 +140,49 @@ var getContainers = () => { }; /** - * Run a docker command - * @param {String} command The command to run - * @param {String} containerName The container + * Run the specified command in the background + * @param {String} dockerCommand The Docker command to run * @param {Function} callback A callback that takes the status, command, and stdErr */ -var runCommand = (command, containerName, callback) => { - const cmd = "docker " + command + " " + containerName; - async(() => { - const res = GLib.spawn_command_line_async(cmd); - return res; - }, (res) => callback(res)); -} +const runBackgroundCommand = (dockerCommand, callback) => { + async( + () => GLib.spawn_command_line_async(dockerCommand), + (res) => callback(res) + ); +}; + +/** + * Spawn a new terminal emulator and run the specified command within it + * @param {String} dockerCommand The Docker command to run + * @param {Function} callback A callback that takes the status, command, and stdErr + */ +const runInteractiveCommand = (dockerCommand, callback) => { + const defaultShell = GLib.getenv("SHELL"); + + const terminalCommand = "gnome-terminal -- " + + defaultShell + " -c '" + + dockerCommand + + "if [ $? -ne 0 ]; then " + defaultShell + "; fi'"; + + async( + () => GLib.spawn_command_line_async(terminalCommand), + (res) => callback(res) + ); +}; + +/** + * Run a Docker action + * @param {String} dockerAction The action to run + * @param {String} containerName The container + * @param {Function} callback A callback that takes the status, action, and stdErr + */ +var runAction = (dockerAction, containerName, callback) => { + const dockerCommand = getDockerActionCommand(dockerAction, containerName); + + dockerAction.isInteractive ? + runInteractiveCommand(dockerCommand, callback) + : runBackgroundCommand(dockerCommand, callback); +}; /** * Run a function in asynchronous mode using GLib diff --git a/src/dockerMenu.js b/src/dockerMenu.js index 6265cc7..7b20f38 100644 --- a/src/dockerMenu.js +++ b/src/dockerMenu.js @@ -27,73 +27,82 @@ const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Docker = Me.imports.src.docker; const DockerSubMenuMenuItem = Me.imports.src.dockerSubMenuMenuItem; +const Utils = Me.imports.src.utils; // Docker icon on status menu -var DockerMenu = class DockerMenu_DockerMenu extends PanelMenu.Button { + +var DockerMenu = class DockerMenu extends PanelMenu.Button { // Init the docker menu _init() { - super._init(0.0, _("Docker containers")); + super._init(0.0, _("Docker containers")); - const hbox = new St.BoxLayout({ style_class: "panel-status-menu-box" }); - const gicon = Gio.icon_new_for_string(Me.path + "/docker.svg"); - const dockerIcon = new St.Icon({ gicon: gicon, icon_size: "24" }); + const hbox = new St.BoxLayout({ style_class: "panel-status-menu-box" }); + const gicon = Gio.icon_new_for_string(Me.path + "/docker.svg"); + const dockerIcon = new St.Icon({ gicon: gicon, icon_size: "24" }); - hbox.add_child(dockerIcon); - this.actor.add_child(hbox); - this.actor.connect("button_press_event", this._refreshMenu.bind(this)); + hbox.add_child(dockerIcon); + this.actor.add_child(hbox); + this.actor.connect("button_press_event", this._refreshMenu.bind(this)); - this._renderMenu(); + this._renderMenu(); } // Refresh the menu everytime the user click on it // It allows to have up-to-date information on docker containers _refreshMenu() { - if (this.menu.isOpen) { - this.menu.removeAll(); - this._renderMenu(); - } + if (this.menu.isOpen) { + this.menu.removeAll(); + this._renderMenu(); + } } // Show docker menu icon only if installed and append docker containers _renderMenu() { - if (Docker.isDockerInstalled()) { - if (Docker.isDockerRunning()) { - this._feedMenu(); + if (Docker.isDockerInstalled()) { + if (Docker.isDockerRunning()) { + this._feedMenu(); + } else { + let errMsg = _("Docker daemon not started"); + this.menu.addMenuItem(new PopupMenu.PopupMenuItem(errMsg)); + log(errMsg); + } } else { - let errMsg = _("Docker daemon not started"); - this.menu.addMenuItem(new PopupMenu.PopupMenuItem(errMsg)); - log(errMsg); + let errMsg = _("Docker binary not found in PATH "); + this.menu.addMenuItem(new PopupMenu.PopupMenuItem(errMsg)); + log(errMsg); } - } else { - let errMsg = _("Docker binary not found in PATH "); - this.menu.addMenuItem(new PopupMenu.PopupMenuItem(errMsg)); - log(errMsg); - } - this.actor.show(); + this.actor.show(); } // Append containers to menu _feedMenu() { - try { - const containers = Docker.getContainers(); - if (containers.length > 0) { - containers.forEach(container => { - const subMenu = new DockerSubMenuMenuItem.DockerSubMenuMenuItem( - container.name, - container.status - ); - this.menu.addMenuItem(subMenu); - }); - } else { - this.menu.addMenuItem( - new PopupMenu.PopupMenuItem("No containers detected") - ); + try { + const containers = Docker.getContainers(); + if (containers.length > 0) { + containers.forEach(container => { + const subMenu = new DockerSubMenuMenuItem.DockerSubMenuMenuItem( + container.name, + container.status + ); + this.menu.addMenuItem(subMenu); + }); + } else { + this.menu.addMenuItem( + new PopupMenu.PopupMenuItem("No containers detected") + ); + } + } catch (err) { + const errMsg = "Error occurred when fetching containers"; + this.menu.addMenuItem(new PopupMenu.PopupMenuItem(errMsg)); + log(errMsg); + log(err); } - } catch (err) { - const errMsg = "Error occurred when fetching containers"; - this.menu.addMenuItem(new PopupMenu.PopupMenuItem(errMsg)); - log(errMsg); - log(err); - } } - } +}; + +if (!Utils.isGnomeShellVersionLegacy()) { + DockerMenu = GObject.registerClass( + { GTypeName: 'DockerMenu' }, + DockerMenu + ); +} diff --git a/src/dockerMenuItem.js b/src/dockerMenuItem.js index e9482db..0122816 100644 --- a/src/dockerMenuItem.js +++ b/src/dockerMenuItem.js @@ -18,36 +18,47 @@ 'use strict'; +const GObject = imports.gi.GObject; const PopupMenu = imports.ui.popupMenu; const Main = imports.ui.main; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Docker = Me.imports.src.docker; +const Utils = Me.imports.src.utils; // Docker actions for each container -var DockerMenuItem = class DockerMenu_DockerMenuItem extends PopupMenu.PopupMenuItem { - - constructor(containerName, dockerCommand) { - super(Docker.dockerCommandsToLabels[dockerCommand]); +var DockerMenuItem = class DockerMenuItem extends PopupMenu.PopupMenuItem { + _init(containerName, dockerAction) { + super._init(dockerAction.label); + this.containerName = containerName; - this.dockerCommand = dockerCommand; + this.dockerAction = dockerAction; this.connect('activate', this._dockerAction.bind(this)); } _dockerAction() { - Docker.runCommand(this.dockerCommand, this.containerName, (res) => { + Docker.runAction(this.dockerAction, this.containerName, (res) => { if (!!res) { - log("`" + this.dockerCommand + "` terminated successfully"); + log("Docker: `" + this.dockerAction.label + "` action terminated successfully"); } else { let errMsg = _( "Docker: Failed to '" + - this.dockerCommand + "' container '" + this.containerName + "'" + this.dockerAction + "' container '" + this.containerName + "'" ); Main.notify(errMsg); log(errMsg); } }); } -}; +} + + + +if (!Utils.isGnomeShellVersionLegacy()) { + DockerMenuItem = GObject.registerClass( + { GTypeName: 'DockerMenuItem' }, + DockerMenuItem + ); +} diff --git a/src/dockerSubMenuMenuItem.js b/src/dockerSubMenuMenuItem.js index 1138233..e06c19a 100644 --- a/src/dockerSubMenuMenuItem.js +++ b/src/dockerSubMenuMenuItem.js @@ -19,11 +19,14 @@ 'use strict'; const St = imports.gi.St; +const GObject = imports.gi.GObject; const Lang = imports.lang; const PopupMenu = imports.ui.popupMenu; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); +const DockerActions = Me.imports.src.docker.DockerActions; const DockerMenuItem = Me.imports.src.dockerMenuItem; +const Utils = Me.imports.src.utils; /** * Create a St.Icon @@ -50,25 +53,27 @@ const getStatus = (statusMessage) => { } // Menu entry representing a docker container -var DockerSubMenuMenuItem = class DockerMenu_DockerSubMenuMenuItem extends PopupMenu.PopupSubMenuMenuItem { +var DockerSubMenuMenuItem = class DockerSubMenuMenuItem extends PopupMenu.PopupSubMenuMenuItem { - constructor(containerName, containerStatusMessage) { - super(containerName); + _init(containerName, containerStatusMessage) { + super._init(containerName); switch (getStatus(containerStatusMessage)) { case "stopped": this.actor.insert_child_at_index(createIcon('process-stop-symbolic', 'status-stopped'), 1); - this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, "start")); - this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, "rm")); + this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, DockerActions.START)); + this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, DockerActions.REMOVE)); break; case "running": this.actor.insert_child_at_index(createIcon('system-run-symbolic', 'status-running'), 1); - this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, "pause")); - this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, "stop")); + this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, DockerActions.OPEN_SHELL)); + this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, DockerActions.RESTART)); + this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, DockerActions.PAUSE)); + this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, DockerActions.STOP)); break; case "paused": this.actor.insert_child_at_index(createIcon('media-playback-pause-symbolic', 'status-paused'), 1); - this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, "unpause")); + this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, DockerActions.UNPAUSE)); break; default: this.actor.insert_child_at_index(createIcon('action-unavailable-symbolic', 'status-undefined'), 1); @@ -76,3 +81,10 @@ var DockerSubMenuMenuItem = class DockerMenu_DockerSubMenuMenuItem extends Popup } } }; + +if (!Utils.isGnomeShellVersionLegacy()) { + DockerSubMenuMenuItem = GObject.registerClass( + { GTypeName: 'DockerSubMenuMenuItem' }, + DockerSubMenuMenuItem + ); +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..92fde47 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,29 @@ +/* + * Gnome3 Docker Menu Extension + * Copyright (C) 2017 Guillaume Pouilloux + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +'use strict'; + +const Config = imports.misc.config; + +var isGnomeShellVersionLegacy = () => { + const gnomeShellMajor = parseInt(Config.PACKAGE_VERSION.split('.')[0]); + const gnomeShellMinor = parseInt(Config.PACKAGE_VERSION.split('.')[1]); + + return gnomeShellMajor < 3 || + (gnomeShellMajor === 3 && gnomeShellMinor < 30); +};