-
-
Save rgolangh/fd8cfebc2f87dd39dee8d567742023aa to your computer and use it in GitHub Desktop.
Nesting PopupSubMenuMenuItem - doesn't work under gnome-shell 3.36
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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