Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "Set custom shell command" menu item #68

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The following actions are available :
- Stop
- Restart
- Exec shell
- Set a custom shell command

## Screenshot

Expand Down
10 changes: 8 additions & 2 deletions src/docker.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Utils = Me.imports.src.utils;

var customShellCommandsToContainers = {};

/**
* Dictionary for Docker actions
* @readonly
Expand Down Expand Up @@ -75,8 +77,12 @@ const getDockerActionCommand = (dockerAction, 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;";
let customCommand = customShellCommandsToContainers[containerName];
customCommand = customCommand.split('"').join('\\\\\\"');
customCommand = customCommand ? ` -c "${customCommand}"` : ''

return "docker exec -it " + containerName + " /bin/bash" + customCommand + "; "
+ "if [ $? -ne 0 ]; then docker exec -it " + containerName + " /bin/sh" + customCommand + "; fi;";
case DockerActions.RESTART:
return "docker restart " + containerName;
case DockerActions.PAUSE:
Expand Down
24 changes: 24 additions & 0 deletions src/dockerSubMenuMenuItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const Me = ExtensionUtils.getCurrentExtension();
const DockerActions = Me.imports.src.docker.DockerActions;
const DockerMenuItem = Me.imports.src.dockerMenuItem;
const Utils = Me.imports.src.utils;
const Docker = Me.imports.src.docker;

/**
* Create a St.Icon
Expand Down Expand Up @@ -58,6 +59,10 @@ var DockerSubMenuMenuItem = class DockerSubMenuMenuItem extends PopupMenu.PopupS
_init(containerName, containerStatusMessage) {
super._init(containerName);

Utils.getCustomShellCommandsToContainersFromStorage((res) => {
Docker.customShellCommandsToContainers = res;
});

switch (getStatus(containerStatusMessage)) {
case "stopped":
this.actor.insert_child_at_index(createIcon('process-stop-symbolic', 'status-stopped'), 1);
Expand All @@ -70,6 +75,25 @@ var DockerSubMenuMenuItem = class DockerSubMenuMenuItem extends PopupMenu.PopupS
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));

const customShellCommandButton = new PopupMenu.PopupMenuItem('Set custom shell command');

this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this.menu.addMenuItem(customShellCommandButton);

customShellCommandButton.connect('activate', () => {
const textEntryProps = {
title: 'Set a custom shell command',
text: `Insert here a command to be executed when opening the \\"${containerName}\\" shell.`,
entryText: Docker.customShellCommandsToContainers[containerName],
};

Utils.openWindowTextEntry(textEntryProps, (customCommand) => {
Docker.customShellCommandsToContainers[containerName] = customCommand;
Utils.saveCustomShellCommandsToContainers(Docker.customShellCommandsToContainers);
});
});

break;
case "paused":
this.actor.insert_child_at_index(createIcon('media-playback-pause-symbolic', 'status-paused'), 1);
Expand Down
127 changes: 127 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

'use strict';

const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Config = imports.misc.config;

Expand All @@ -35,3 +36,129 @@ var isGnomeShellVersionLegacy = () => {
* @param {Function} callback The callback to call after fn
*/
var async = (fn, callback) => GLib.timeout_add(GLib.PRIORITY_DEFAULT, 0, () => callback(fn()));

/**
* Run a command asynchronously
* @param {string} commandLine Command to execute
* @param {(output: string) => void} callback The callback to call after shell return an output
*/
var spawnCommandLineAsync = (commandLine, callback) => {
function readOutput(stream, lineBuffer) {
stream.read_line_async(0, null, (stream, res) => {
try {
let line = stream.read_line_finish_utf8(res)[0];

if (line !== null) {
lineBuffer.push(line);
readOutput(stream, lineBuffer);
}
} catch (e) {
logError(e);
}
});
}

try {
let [, pid, stdin, stdout, stderr] = GLib.spawn_async_with_pipes(
null,
['/bin/sh', '-c', `${commandLine}`],
null,
GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD,
null
);

GLib.close(stdin);

let stdoutStream = new Gio.DataInputStream({
base_stream: new Gio.UnixInputStream({
fd: stdout,
close_fd: true
}),
close_base_stream: true
});

let stdoutLines = [];
readOutput(stdoutStream, stdoutLines);

let stderrStream = new Gio.DataInputStream({
base_stream: new Gio.UnixInputStream({
fd: stderr,
close_fd: true
}),
close_base_stream: true,
});

let stderrLines = [];
readOutput(stderrStream, stderrLines);

GLib.child_watch_add(GLib.PRIORITY_DEFAULT_IDLE, pid, (pid, status) => {
if (status === 0) {
callback(stdoutLines.join('\n'));
} else {
logError(new Error(stderrLines.join('\n')));
}

stdoutStream.close(null);
stderrStream.close(null);
GLib.spawn_close_pid(pid);
});
} catch(err) {
logError(err);
}
}

/**
* Creates/changes a file
* @param {string} filename Filename of the file that will be created/changed
* @param {string} content Content that will be written inside the file that will be created/changed
*/
var writeFile = (filename, content = '') => {
if (!filename) {
throw new Error('The filename is a required parameter');
}

content = content.split('"').join('\\\\\\"');

GLib.spawn_command_line_async(`/bin/sh -c "echo \\"${content}\\" > ${filename}"`);
}

/**
* Get custom shell commands to containers from storage
* @param {(customShellCommandsToContainers: {[containerName: string]: string}) => void} callback The callback to call after get the custom shell commands
*/
var getCustomShellCommandsToContainersFromStorage = (callback) => {
spawnCommandLineAsync(
'cat ~/.dockerIntegrationCustomShellCommandsToContainers.json',
(res) => {
const customShellCommandsToContainers = JSON.parse(res);
callback(customShellCommandsToContainers);
}
);
}

/**
* Save custom shell commands to containers in storage
* @param {{[containerName: string]: string}} customShellCommandsToContainers The custom shell commands to save in storage
*/
var saveCustomShellCommandsToContainers = (customShellCommandsToContainers) => {
writeFile(
'~/.dockerIntegrationCustomShellCommandsToContainers.json',
JSON.stringify(customShellCommandsToContainers)
);
}

/**
* Opens a window with an input
* @param {{title?: string, text?: string, entryText?: string}} textEntryProps The properties of the window elements
* @param {(inputValue: string) => void} callback The callback to call after the user filled the input
*/
var openWindowTextEntry = (textEntryProps, callback) => {
const { title = '', text = '', entryText = '' } = textEntryProps || {};

const script = 'zenity --entry ' +
`--title="${title}" ` +
`--text="${text}" ` +
`--entry-text "${entryText}"`;

spawnCommandLineAsync(script, callback);
}