Skip to content

Instantly share code, notes, and snippets.

@rgolangh
Created September 14, 2020 14:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rgolangh/fd8cfebc2f87dd39dee8d567742023aa to your computer and use it in GitHub Desktop.
Save rgolangh/fd8cfebc2f87dd39dee8d567742023aa to your computer and use it in GitHub Desktop.
Nesting PopupSubMenuMenuItem - doesn't work under gnome-shell 3.36
'use strict';
const Main = imports.ui.main;
const Config = imports.misc.config;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Util = imports.misc.util;
const St = imports.gi.St;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Lang = imports.lang;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const GObject = imports.gi.GObject;
const NA = _("n/a");
let containersMenu;
let debugEnabled = true;
let podmanVersion;
function debug(msg) {
if (debugEnabled) {
log(`gnome-shell-extensions-containers - [DEBUG] ${msg}`);
}
}
function info(msg) {
if (debugEnabled) {
log(`gnome-shell-extensions-containers - [INFO] ${msg}`);
}
}
function enable() {
info("enabling containers extension");
init();
containersMenu = new ContainersMenu();
debug(containersMenu);
containersMenu.renderMenu();
Main.panel.addToStatusArea('containers-menu', containersMenu);
}
function disable() {
info("disabling containers extension");
containersMenu.destroy();
}
function createIcon(name, styleClass) {
return new St.Icon({ icon_name: name, style_class: styleClass, icon_size: '14' });
}
var ContainersMenu = GObject.registerClass(
{
GTypeName: 'ContainersMenu'
},
class ContainersMenu extends PanelMenu.Button {
_init() {
super._init(0.0, "Containers");
this.menu.box.add_style_class_name('containers-extension-menu');
const hbox = new St.BoxLayout({ style_class: 'panel-status-menu-box' });
const gicon = Gio.icon_new_for_string(Me.path + "/podman-icon.png");
const icon = new St.Icon({ gicon: gicon, icon_size: '24' });
hbox.add_child(icon);
hbox.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM));
this.add_child(hbox);
this.connect('button_press_event', Lang.bind(this, () => {
if (this.menu.isOpen) {
this.menu.removeAll();
this.renderMenu();
}
}));
}
renderMenu() {
try {
const containers = getContainers();
const podContainers = new Map();
const pods = getPods();
info(`found ${containers.length} containers`);
if (containers.length == 0) {
this.menu.addMenuItem(new PopupMenu.PopupMenuItem(_("No containers detected")));
this.show();
return;
}
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem("Containers"));
containers.forEach(container => {
debug(container.toString());
const subMenu = new ContainerSubMenuMenuItem(container);
if (podmanVersion.newerOrEqualTo("2.0.3")) {
if (container.pod) {
if (!podContainers.has(container.pod)) {
podContainers.set(container.pod, []);
}
podContainers.get(container.pod).push(subMenu);
} else {
this.menu.addMenuItem(subMenu);
}
} else {
this.menu.addMenuItem(subMenu);
}
});
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem("Pods"));
pods.forEach(pod => {
const podMenu = new PodSubMenuMenuItem(pod);
podContainers.get(pod.id).forEach((containerSubMenu) => {
podMenu.menu.addMenuItem(containerSubMenu);
});
this.menu.addMenuItem(podMenu);
});
} catch (err) {
const errMsg = _("Error occurred when fetching containers");
this.menu.addMenuItem(new PopupMenu.PopupMenuItem(errMsg));
info(`${errMsg}: ${err} - ${err.stack}`);
}
this.show();
}
});
const runCommand = function (command, containerName) {
const cmdline = `podman ${command} ${containerName}`;
info(`running command ${cmdline}`);
const [res, out, err, status] = GLib.spawn_command_line_sync(cmdline);
if (status === 0) {
info(`command on ${containerName} terminated successfully`);
} else {
const errMsg = _(`Error occurred when running ${command} on container ${containerName}`);
Main.notify(errMsg);
info(errMsg);
info(err);
}
debug(out);
return out;
}
const runCommandInTerminal = function (command, entityName, args) {
const cmdline = `gnome-terminal -- ${command} ${entityName} ${args}`;
info(`running command ${cmdline}`);
const [res, out, err, status] = GLib.spawn_command_line_async(cmdline);
if (status === 0) {
info(`command on ${entityName} terminated successfully`);
} else {
const errMsg = _(`Error occurred when running ${command} on ${entityName}`);
Main.notify(errMsg);
info(errMsg);
info(err);
}
debug(out);
return out;
}
var PopupMenuItem = GObject.registerClass(
{
GTypeName: 'PopupMenuItem'
},
class extends PopupMenu.PopupMenuItem {
_init(label, value) {
if (value === undefined) {
super._init(label);
} else {
super._init(`${label}: ${value}`);
this.connect('button_press_event', Lang.bind(this, () => {
setClipboard(value);
}, false));
}
this.add_style_class_name("containers-extension-subMenuItem");
this.add_style_class_name(label.toLowerCase());
}
});
var ContainerMenuItem = GObject.registerClass(
{
GTypeName: 'ContainerMenuItem'
},
class extends PopupMenuItem {
_init(label, entityName, command) {
super._init(label);
this.entityName = entityName;
this.command = command;
this.connect('activate', Lang.bind(this, () => {
runCommand(this.command, this.entityName);
}));
}
});
var ContainerMenuWithOutputItem = GObject.registerClass(
{
GTypeName: 'ContainerMenuWithOutputItem'
},
class extends PopupMenuItem {
_init(containerName, command, outputHdndler) {
super._init(command);
this.containerName = containerName;
this.command = command;
this.connect('activate', Lang.bind(this, () => {
var out = runCommand(this.command, this.containerName);
outputHdndler(out);
}));
}
});
var EntityMenuItemWithTerminalAction = GObject.registerClass(
{
GTypeName: 'EntityMenuItemWithTerminalAction'
},
class extends PopupMenuItem {
_init(label, entityName, command, args) {
super._init(label);
this.entityName = entityName;
this.command = command;
this.args = args;
this.connect('activate', Lang.bind(this, () => {
runCommandInTerminal(this.command, this.entityName, this.args);
}));
}
});
var ContainerSubMenuMenuItem = GObject.registerClass(
{
GTypeName: 'ContainerSubMenuMenuItem'
},
class extends PopupMenu.PopupSubMenuMenuItem {
_init(container) {
super._init(container.name);
this.add_style_class_name("container");
this.menu.addMenuItem(new PopupMenuItem("Status", container.status));
this.menu.addMenuItem(new PopupMenuItem("Id", container.id));
this.menu.addMenuItem(new PopupMenuItem("Image", container.image));
this.menu.addMenuItem(new PopupMenuItem("Command", container.command));
this.menu.addMenuItem(new PopupMenuItem("Created", container.createdAt));
this.menu.addMenuItem(new PopupMenuItem("Ports", container.ports));
this.inspected = false;
// add more stats and info - inspect - SLOW
this.connect("button_press_event", Lang.bind(this, () => {
if (!this.inspected) {
inspect(container.name, this.menu);
this.inspected = true;
}
}));
switch (container.status.split(" ")[0]) {
case "Exited":
case "exited":
case "Created":
case "created":
case "configured":
case "stopped":
this.insert_child_at_index(createIcon('process-stop-symbolic', 'status-stopped'), 1);
const startMeunItem = new ContainerMenuItem("start", container.name, "container start");
startMeunItem.insert_child_at_index(createIcon('media-playback-start-symbolic', 'status-start'), 1);
this.menu.addMenuItem(startMeunItem);
const rmMenuItem = new ContainerMenuItem("remove", container.name, "container rm");
rmMenuItem.insert_child_at_index(createIcon('user-trash-symbolic', 'status-remove'), 1);
this.menu.addMenuItem(rmMenuItem);
break;
case "Up":
case "running":
this.menu.addMenuItem(new PopupMenuItem("Started", container.startedAt));
this.insert_child_at_index(createIcon('media-playback-start-symbolic', 'status-running'), 1);
const pauseMenuIten = new ContainerMenuItem("pause", container.name, "container pause");
pauseMenuIten.insert_child_at_index(createIcon('media-playback-pause-symbolic', 'status-stopped'), 1);
this.menu.addMenuItem(pauseMenuIten);
const stopMenuItem = new ContainerMenuItem("stop", container.name, "container stop");
stopMenuItem.insert_child_at_index(createIcon('process-stop-symbolic', 'status-stopped'), 1);
this.menu.addMenuItem(stopMenuItem);
const restartMenuItem = new ContainerMenuItem("restart", container.name, "container restart");
restartMenuItem.insert_child_at_index(createIcon('system-reboot-symbolic', 'status-restart'), 1);
this.menu.addMenuItem(restartMenuItem);
this.menu.addMenuItem(createTopMenuItem(container, "container"));
this.menu.addMenuItem(createShellMenuItem(container, "container"));
this.menu.addMenuItem(createStatsMenuItem(container, "container"));
break;
case "Paused":
case "paused":
this.insert_child_at_index(createIcon('media-playback-pause-symbolic', 'status-paused'), 1);
const unpauseMenuItem = new ContainerMenuItem("unpause", container.name, "container unpause");
unpauseMenuItem.insert_child_at_index(createIcon('media-playback-start-symbolic', 'status-start'), 1)
this.menu.addMenuItem(unpauseMenuItem);
break;
default:
this.insert_child_at_index(createIcon('action-unavailable-symbolic', 'status-undefined'), 1);
break;
}
// add log button
const logMenuItem = createLogMenuItem(container);
this.menu.addMenuItem(logMenuItem);
}
open() {
info("open container menu");
this.emit('open-state-changed', true);
}
close() {
info("close container menu");
this.emit('open-state-changed', false);
}
});
function setClipboard(text) {
St.Clipboard.get_default().set_text(St.ClipboardType.PRIMARY, text);
}
function inspect(container, menu) {
let out = runCommand("inspect --format json", container);
let inspect = JSON.parse(imports.byteArray.toString(out));
if (inspect.length > 0 && inspect[0].NetworkSettings != null) {
menu.addMenuItem(
new PopupMenuItem("IP Address", JSON.stringify(inspect[0].NetworkSettings.IPAddress)));
}
}
function createLogMenuItem(container) {
let i = new EntityMenuItemWithTerminalAction("logs", container.name, `podman container logs -f ${container.name}`, "");
i.insert_child_at_index(createIcon('document-open-symbolic.symbolic', 'action-logs'), 1)
return i
}
function createTopMenuItem(entity, entityType) {
const i = new EntityMenuItemWithTerminalAction("top", entity.name, `watch podman ${entityType} top`, "");
i.insert_child_at_index(createIcon('view-reveal-symbolic.symbolic', 'action-top'), 1);
return i;
}
function createShellMenuItem(entity, entityType) {
const i = new EntityMenuItemWithTerminalAction("sh", entity.name, `podman ${entityType} exec -it`, "/bin/sh");
i.insert_child_at_index(new St.Label({ style_class: 'action-sh', text: ">_" }), 1);
return i;
}
function createStatsMenuItem(entity, entityType) {
const i = new EntityMenuItemWithTerminalAction("stats", entity.name, `podman ${entityType} stats`, "");
i.insert_child_at_index(new St.Label({ style_class: 'action-stats', text: "%" }), 1);
return i;
}
var PodSubMenuMenuItem = GObject.registerClass(
{
GTypeName: 'PodSubMenuMenuItem'
},
class extends PopupMenu.PopupSubMenuMenuItem {
_init(pod) {
super._init(pod.name);
this.add_style_class_name("pod");
this.menu.addMenuItem(new PopupMenuItem("Status", pod.status));
this.menu.addMenuItem(new PopupMenuItem("Created", pod.created));
switch (pod.status.split(" ")[0]) {
case "Exited":
case "exited":
case "Created":
case "created":
case "configured":
case "stopped":
this.insert_child_at_index(createIcon('process-stop-symbolic', 'status-stopped'), 1);
const startMeunItem = new ContainerMenuItem("start", pod.name, "pod start");
startMeunItem.insert_child_at_index(createIcon('media-playback-start-symbolic', 'status-start'), 1);
this.menu.addMenuItem(startMeunItem);
const rmMenuItem = new ContainerMenuItem("remove", pod.name, "pod rm");
rmMenuItem.insert_child_at_index(createIcon('user-trash-symbolic', 'status-remove'), 1);
this.menu.addMenuItem(rmMenuItem);
break;
case "Up":
case "Running":
case "running":
this.menu.addMenuItem(new PopupMenuItem("Started", pod.created));
this.insert_child_at_index(createIcon('media-playback-start-symbolic', 'status-running'), 1);
const pauseMenuIten = new ContainerMenuItem("pause", pod.name, "pod pause");
pauseMenuIten.insert_child_at_index(createIcon('media-playback-pause-symbolic', 'status-stopped'), 1);
this.menu.addMenuItem(pauseMenuIten);
const stopMenuItem = new ContainerMenuItem("stop", pod.name, "pod stop");
stopMenuItem.insert_child_at_index(createIcon('process-stop-symbolic', 'status-stopped'), 1);
this.menu.addMenuItem(stopMenuItem);
const restartMenuItem = new ContainerMenuItem("restart", pod.name, "pod restart");
restartMenuItem.insert_child_at_index(createIcon('system-reboot-symbolic', 'status-restart'), 1);
this.menu.addMenuItem(restartMenuItem);
this.menu.addMenuItem(createTopMenuItem(pod, "pod"));
this.menu.addMenuItem(createStatsMenuItem(pod, "pod"));
break;
case "Paused":
case "paused":
this.insert_child_at_index(createIcon('media-playback-pause-symbolic', 'status-paused'), 1);
const unpauseMenuItem = new ContainerMenuItem("unpause", pod.name, "pod unpause");
unpauseMenuItem.insert_child_at_index(createIcon('media-playback-start-symbolic', 'status-start'), 1)
this.menu.addMenuItem(unpauseMenuItem);
break;
default:
this.insert_child_at_index(createIcon('action-unavailable-symbolic', 'status-undefined'), 1);
break;
}
}
});
function init() {
const [res, out, err, status] = GLib.spawn_command_line_sync("podman version --format json");
if (!res) {
info(`status: ${status}, error: ${err}`);
throw new Error(_("Error getting podman version"));
}
debug(out);
const versionJson = JSON.parse(imports.byteArray.toString(out));
if (versionJson.Client != null && versionJson.Client.Version != null) {
podmanVersion = new Version(versionJson.Client.Version);
}
if (versionJson == null) {
info("unable to set podman info, will fall back to syntax and output < 2.0.3");
}
debug(podmanVersion);
}
// return list of containers : Container[]
function getContainers() {
const [res, out, err, status] = GLib.spawn_command_line_sync("podman ps -a --format json");
if (!res) {
info(`status: ${status}, error: ${err}`);
throw new Error(_("Error occurred when fetching containers"));
}
debug(out);
const jsonContainers = JSON.parse(imports.byteArray.toString(out));
if (jsonContainers == null) {
return [];
}
const containers = [];
jsonContainers.forEach(e => {
let c = new Container(e);
containers.push(c);
});
return containers;
}
class Container {
constructor(jsonContainer) {
if (!jsonContainer) {
return;
}
if (podmanVersion.newerOrEqualTo("2.0.3")) {
this.name = jsonContainer.Names[0];
this.id = jsonContainer.Id;
this.state = jsonContainer.State;
this.status = jsonContainer.State;
this.createdAt = jsonContainer.CreatedAt;
this.isInfa = jsonContainer.IsInfra;
this.pod = jsonContainer.Pod;
} else {
this.name = jsonContainer.Names;
this.id = jsonContainer.ID;
this.state = jsonContainer.Status;
this.status = jsonContainer.Status;
this.createdAt = jsonContainer.Created;
}
this.image = jsonContainer.Image;
this.command = jsonContainer.Command;
this.startedAt = new Date(jsonContainer.StartedAt * 1000);
if (jsonContainer.Ports == null) {
this.ports = NA;
} else {
this.ports = jsonContainer.Ports.map(e => `host ${e.hostPort}/${e.protocol} -> pod ${e.containerPort}`);
}
}
toString() {
return `name: ${this.name}
id: ${this.id}
state: ${this.state}
status: ${this.status}
image: ${this.image}`;
}
}
// return list of pods : Pod[]
function getPods() {
const [res, out, err, status] = GLib.spawn_command_line_sync("podman pod ls --format json");
if (!res) {
info(`status: ${status}, error: ${err}`);
throw new Error(_("Error fetching pods"));
}
debug(out);
const jsonPods = JSON.parse(imports.byteArray.toString(out));
if (jsonPods == null) {
return [];
}
const pods = [];
jsonPods.forEach(e => {
let p = new Pod(e);
pods.push(p);
});
return pods;
}
class Pod {
constructor(jsonPod) {
this.name = jsonPod.Name;
this.id = jsonPod.Id;
this.status = jsonPod.Status;
const createdDate = Date.parse(jsonPod.Created);
if (isNaN(createdDate)) {
this.created = jsonPod.Created;
} else {
this.created = Util.formatTimeSpan(GLib.DateTime.new_from_unix_local(createdDate / 1000));
}
this.infraId = jsonPod.InfraId;
if (jsonPod.Containers) {
this.containers = new Map();
this.containers = jsonPod.Containers.map(e => {
const c = new Container();
c.id = e.Id;
c.name = e.Names[0];
c.status = e.Status;
this.containers.set(c.id, c);
});
} else {
this.ports = NA;
}
}
toString() {
return `name: ${this.name}
id: ${this.id}
status: ${this.status}
containers: ${this.containers}
`
}
}
class Version {
constructor(v) {
const splits = v.split(".")
this.major = splits[0];
this.minor = splits[1];
if (splits.length > 2) {
this.patch = splits[2];
}
}
newerOrEqualTo(v) {
return this.compare(new Version(v)) >= 0;
}
compare(other) {
debug(`compare ${this} with ${other}`);
if (this.major != other.major) {
return Math.sign(this.major - other.major);
}
if (this.minor != other.minor) {
return Math.sign(this.minor - other.minor);
}
if (this.patch != other.patch) {
if (this.patch == null) {
return -1;
}
return this.patch.localeCompare(other.patch);
}
return 0;
}
toString() {
return `major: ${this.major} minor: ${this.minor} patch: ${this.patch}`;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment