diff --git a/.eslintrc.yaml b/.eslintrc.yaml
deleted file mode 100644
index 627590e..0000000
--- a/.eslintrc.yaml
+++ /dev/null
@@ -1,252 +0,0 @@
----
-# SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
-#
-# The initial file content was copied from gnome shell extension repo and is a conbination of the linting rules under the direcory
-# https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/tree/main/lint
-env:
- es2021: true
-extends:
- - eslint:recommended
- - plugin:jsdoc/recommended-error
-plugins:
- - jsdoc
-rules:
- array-bracket-newline:
- - error
- - consistent
- array-bracket-spacing:
- - error
- - never
- array-callback-return: error
- arrow-parens:
- - error
- - as-needed
- arrow-spacing: error
- block-scoped-var: error
- block-spacing: error
- brace-style: error
- camelcase:
- - error
- - properties: never
- allow: [^vfunc_, ^on_]
- comma-dangle:
- - error
- - arrays: always-multiline
- objects: always-multiline
- functions: never
- comma-spacing:
- - error
- - before: false
- after: true
- comma-style:
- - error
- - last
- computed-property-spacing: error
- curly:
- - error
- dot-location:
- - error
- - property
- eol-last: error
- eqeqeq: error
- func-call-spacing: error
- func-name-matching: error
- func-style:
- - error
- - declaration
- - allowArrowFunctions: true
- indent:
- - error
- - 4
- - ignoredNodes:
- # Allow not indenting the body of GObject.registerClass, since in the
- # future it's intended to be a decorator
- - 'CallExpression[callee.object.name=GObject][callee.property.name=registerClass] > ClassExpression:first-child'
- # Allow dedenting chained member expressions
- MemberExpression: 'off'
- key-spacing:
- - error
- - beforeColon: false
- afterColon: true
- keyword-spacing:
- - error
- - before: true
- after: true
- linebreak-style:
- - error
- - unix
- lines-between-class-members: error
- max-nested-callbacks: error
- max-statements-per-line: error
- new-parens: error
- no-array-constructor: error
- no-await-in-loop: error
- no-caller: error
- no-constant-condition:
- - error
- - checkLoops: false
- no-div-regex: error
- no-empty:
- - error
- - allowEmptyCatch: true
- no-extra-bind: error
- no-extra-parens:
- - error
- - all
- - conditionalAssign: false
- nestedBinaryExpressions: false
- returnAssign: false
- no-implicit-coercion:
- - error
- - allow:
- - '!!'
- no-invalid-this: error
- no-iterator: error
- no-label-var: error
- no-lonely-if: error
- no-loop-func: error
- no-nested-ternary: error
- no-new-object: error
- no-new-wrappers: error
- no-octal-escape: error
- no-proto: error
- no-prototype-builtins: 'off'
- no-restricted-globals: [error, window]
- no-restricted-properties:
- - error
- - object: imports
- property: format
- message: Use template strings
- - object: pkg
- property: initFormat
- message: Use template strings
- - object: Lang
- property: copyProperties
- message: Use Object.assign()
- - object: Lang
- property: bind
- message: Use arrow notation or Function.prototype.bind()
- - object: Lang
- property: Class
- message: Use ES6 classes
- no-restricted-syntax:
- - error
- - selector: >-
- MethodDefinition[key.name="_init"] >
- FunctionExpression[params.length=1] >
- BlockStatement[body.length=1]
- CallExpression[arguments.length=1][callee.object.type="Super"][callee.property.name="_init"] >
- Identifier:first-child
- message: _init() that only calls super._init() is unnecessary
- - selector: >-
- MethodDefinition[key.name="_init"] >
- FunctionExpression[params.length=0] >
- BlockStatement[body.length=1]
- CallExpression[arguments.length=0][callee.object.type="Super"][callee.property.name="_init"]
- message: _init() that only calls super._init() is unnecessary
- - selector: BinaryExpression[operator="instanceof"][right.name="Array"]
- message: Use Array.isArray()
- no-return-assign: error
- no-return-await: error
- no-self-compare: error
- no-shadow: error
- no-shadow-restricted-names: error
- no-spaced-func: error
- no-tabs: error
- no-template-curly-in-string: error
- no-throw-literal: error
- no-trailing-spaces: error
- no-undef-init: error
- no-unneeded-ternary: error
- no-unused-expressions: error
- no-unused-vars:
- - error
- # Vars use a suffix _ instead of a prefix because of file-scope private vars
- - varsIgnorePattern: (^unused|_$)
- argsIgnorePattern: ^(unused|_)
- no-useless-call: error
- no-useless-computed-key: error
- no-useless-concat: error
- no-useless-constructor: error
- no-useless-rename: error
- no-useless-return: error
- no-whitespace-before-property: error
- no-with: error
- nonblock-statement-body-position:
- - error
- - below
- object-curly-newline:
- - error
- - consistent: true
- multiline: true
- object-curly-spacing: error
- object-shorthand: error
- operator-assignment: error
- operator-linebreak: error
- padded-blocks:
- - error
- - never
- # These may be a bit controversial, we can try them out and enable them later
- # prefer-const: error
- # prefer-destructuring: error
- prefer-numeric-literals: error
- prefer-promise-reject-errors: error
- prefer-rest-params: error
- prefer-spread: error
- prefer-template: error
- quotes:
- - error
- - double
- - avoidEscape: true
- require-await: error
- rest-spread-spacing: error
- semi:
- - error
- - always
- semi-spacing:
- - error
- - before: false
- after: true
- semi-style: error
- space-before-blocks: error
- space-before-function-paren:
- - error
- - named: never
- # for `function ()` and `async () =>`, preserve space around keywords
- anonymous: always
- asyncArrow: always
- space-in-parens: error
- space-infix-ops:
- - error
- - int32Hint: false
- space-unary-ops: error
- spaced-comment: error
- switch-colon-spacing: error
- symbol-description: error
- template-curly-spacing: error
- template-tag-spacing: error
- unicode-bom: error
- wrap-iife:
- - error
- - inside
- yield-star-spacing: error
- yoda: error
-settings:
- jsdoc:
- mode: typescript
-globals:
- ARGV: readonly
- Debugger: readonly
- GIRepositoryGType: readonly
- globalThis: readonly
- imports: readonly
- Intl: readonly
- log: readonly
- logError: readonly
- print: readonly
- printerr: readonly
- window: readonly
- TextEncoder: readonly
- TextDecoder: readonly
-parserOptions:
- ecmaVersion: 2022
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 80fcae1..fd4e178 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -4,10 +4,9 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: '18'
- - run: npm init @eslint/config
- - run: npm install eslint-plugin-jsdoc@latest --save-dev
- - run: npx eslint *.js modules/*.js
+ - run: npm install
+ - run: npm run lint
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index afb9fda..467663e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -21,4 +21,4 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
- gh release create ${GITHUB_REF#refs/tags/} --generate-notes
+ gh release create ${GITHUB_REF#refs/tags/} --generate-notes *${GITHUB_REF#refs/tags/}.zip
diff --git a/Makefile b/Makefile
index b1b0bad..e933a50 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@ EXTRA_SOURCES = \
--extra-source=classic.css \
--extra-source=modules
-build:
+build: lint
gnome-extensions pack -f $(EXTRA_SOURCES) src/
mv containers@royg.shell-extension.zip $(TARGET_FILE)
@@ -18,9 +18,12 @@ enable:
debug:
G_MESSAGES_DEBUG="GNOME Shell" dbus-run-session -- gnome-shell --nested --wayland
+lint:
+ npm run lint
+
all: \
install \
enable
-.PHONY: build debug enable install all
+.PHONY: build debug enable install all lint
diff --git a/README.md b/README.md
index 609f77b..1b49ba0 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,25 @@ You can install this extension directly from the [GNOME Extensions page](https:/
Alternatively, enable it via Extensions -> Toggle 'Containers'.
+## Preferences ⚙
+
+- __*extra-info*__
+ Show information about the container, see the screenshot
+- __*terminal*__
+ The terminal program to use when performing the `Show Logs`, `Watch Top`, `Open Shell`, and `Watch Statistics`
+
+ Tested values:
+ | value | notes |
+ | ----- | ----- |
+ |__*gnome-terminal --*__| default terminal program for all gnome version till 46, inclusive |
+ |__*ptyxis --*__| default terminal program for gnome version from 47 onward |
+ |__*kitty*__| no extra arguments needed |
+ |__*flatpak run app.studiodev.Ptyxis --*__| Some flatpak environments doesn't have podman on the path |
+ |__*flatpak run com.gexperts.Tilix -e*__| Some flatpak environments doesn't have podman on the path |
+
+ Note: The value for __*terminal*__ may not work for you. Please report any working value you have so in time we would have a proper list.
+ There is a system wide gsettings `org.gnome.desktop.default-applications.terminal exec` but depending on the value different args are needed, as seen from above.
+
## Development / Contributing 🤝
To contribute to the development of this extension:
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..ae6d69e
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,261 @@
+import jsdoc from "eslint-plugin-jsdoc";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import js from "@eslint/js";
+import { FlatCompat } from "@eslint/eslintrc";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+ recommendedConfig: js.configs.recommended,
+ allConfig: js.configs.all
+});
+
+export default [
+ ...compat.extends("eslint:recommended", "plugin:jsdoc/recommended-error"),
+ {
+ languageOptions: {
+ globals: {
+ ARGV: "readonly",
+ Debugger: "readonly",
+ GIRepositoryGType: "readonly",
+ globalThis: "readonly",
+ imports: "readonly",
+ Intl: "readonly",
+ console: "readonly",
+ print: "readonly",
+ printerr: "readonly",
+ window: "readonly",
+ TextEncoder: "readonly",
+ TextDecoder: "readonly",
+ },
+
+ ecmaVersion: 2022,
+ sourceType: "module",
+ },
+
+ settings: {
+ jsdoc: {
+ mode: "typescript",
+ },
+ },
+
+ rules: {
+ "no-console": "off",
+ "array-bracket-newline": ["error", "consistent"],
+ "array-bracket-spacing": ["error", "never"],
+ "array-callback-return": "error",
+ "arrow-parens": ["error", "as-needed"],
+ "arrow-spacing": "error",
+ "block-scoped-var": "error",
+ "block-spacing": "error",
+ "brace-style": "error",
+
+ camelcase: ["error", {
+ properties: "never",
+ allow: ["^vfunc_", "^on_"],
+ }],
+
+ "comma-dangle": ["error", {
+ arrays: "always-multiline",
+ objects: "always-multiline",
+ functions: "never",
+ }],
+
+ "comma-spacing": ["error", {
+ before: false,
+ after: true,
+ }],
+
+ "comma-style": ["error", "last"],
+ "computed-property-spacing": "error",
+ curly: ["error"],
+ "dot-location": ["error", "property"],
+ "eol-last": "error",
+ eqeqeq: "error",
+ "func-call-spacing": "error",
+ "func-name-matching": "error",
+
+ "func-style": ["error", "declaration", {
+ allowArrowFunctions: true,
+ }],
+
+ indent: ["error", 4, {
+ SwitchCase: 1,
+ MemberExpression: "off",
+ }],
+
+ "key-spacing": ["error", {
+ beforeColon: false,
+ afterColon: true,
+ }],
+
+ "keyword-spacing": ["error", {
+ before: true,
+ after: true,
+ }],
+
+ "linebreak-style": ["error", "unix"],
+ "lines-between-class-members": "error",
+ "max-nested-callbacks": "error",
+ "max-statements-per-line": "error",
+ "new-parens": "error",
+ "no-array-constructor": "error",
+ "no-await-in-loop": "error",
+ "no-caller": "error",
+
+ "no-constant-condition": ["error", {
+ checkLoops: false,
+ }],
+
+ "no-div-regex": "error",
+
+ "no-empty": ["error", {
+ allowEmptyCatch: true,
+ }],
+
+ "no-extra-bind": "error",
+
+ "no-extra-parens": ["error", "all", {
+ conditionalAssign: false,
+ nestedBinaryExpressions: false,
+ returnAssign: false,
+ }],
+
+ "no-implicit-coercion": ["error", {
+ allow: ["!!"],
+ }],
+
+ "no-invalid-this": "error",
+ "no-iterator": "error",
+ "no-label-var": "error",
+ "no-lonely-if": "error",
+ "no-loop-func": "error",
+ "no-nested-ternary": "error",
+ "no-new-object": "error",
+ "no-new-wrappers": "error",
+ "no-octal-escape": "error",
+ "no-proto": "error",
+ "no-prototype-builtins": "off",
+ "no-restricted-globals": ["error", "window"],
+
+ "no-restricted-properties": ["error", {
+ object: "imports",
+ property: "format",
+ message: "Use template strings",
+ }, {
+ object: "pkg",
+ property: "initFormat",
+ message: "Use template strings",
+ }, {
+ object: "Lang",
+ property: "copyProperties",
+ message: "Use Object.assign()",
+ }, {
+ object: "Lang",
+ property: "bind",
+ message: "Use arrow notation or Function.prototype.bind()",
+ }, {
+ object: "Lang",
+ property: "Class",
+ message: "Use ES6 classes",
+ }],
+
+ "no-restricted-syntax": ["error", {
+ selector: "MethodDefinition[key.name=\"_init\"] > FunctionExpression[params.length=1] > BlockStatement[body.length=1] CallExpression[arguments.length=1][callee.object.type=\"Super\"][callee.property.name=\"_init\"] > Identifier:first-child",
+ message: "_init() that only calls super._init() is unnecessary",
+ }, {
+ selector: "MethodDefinition[key.name=\"_init\"] > FunctionExpression[params.length=0] > BlockStatement[body.length=1] CallExpression[arguments.length=0][callee.object.type=\"Super\"][callee.property.name=\"_init\"]",
+ message: "_init() that only calls super._init() is unnecessary",
+ }, {
+ selector: "BinaryExpression[operator=\"instanceof\"][right.name=\"Array\"]",
+ message: "Use Array.isArray()",
+ }],
+
+ "no-return-assign": "error",
+ "no-return-await": "error",
+ "no-self-compare": "error",
+ "no-shadow": "error",
+ "no-shadow-restricted-names": "error",
+ "no-spaced-func": "error",
+ "no-tabs": "error",
+ "no-template-curly-in-string": "error",
+ "no-throw-literal": "error",
+ "no-trailing-spaces": "error",
+ "no-undef-init": "error",
+ "no-unneeded-ternary": "error",
+ "no-unused-expressions": "error",
+
+ "no-unused-vars": ["error", {
+ varsIgnorePattern: "(^unused|_$)",
+ argsIgnorePattern: "^(unused|_)",
+ }],
+
+ "no-useless-call": "error",
+ "no-useless-computed-key": "error",
+ "no-useless-concat": "error",
+ "no-useless-constructor": "error",
+ "no-useless-rename": "error",
+ "no-useless-return": "error",
+ "no-whitespace-before-property": "error",
+ "no-with": "error",
+ "nonblock-statement-body-position": ["error", "below"],
+
+ "object-curly-newline": ["error", {
+ consistent: true,
+ multiline: true,
+ }],
+
+ "object-curly-spacing": "error",
+ "object-shorthand": "error",
+ "operator-assignment": "error",
+ "operator-linebreak": "error",
+ "padded-blocks": ["error", "never"],
+ "prefer-numeric-literals": "error",
+ "prefer-promise-reject-errors": "error",
+ "prefer-rest-params": "error",
+ "prefer-spread": "error",
+ "prefer-template": "error",
+
+ quotes: ["error", "double", {
+ avoidEscape: true,
+ }],
+
+ "require-await": "error",
+ "rest-spread-spacing": "error",
+ semi: ["error", "always"],
+
+ "semi-spacing": ["error", {
+ before: false,
+ after: true,
+ }],
+
+ "semi-style": "error",
+ "space-before-blocks": "error",
+
+ "space-before-function-paren": ["error", {
+ named: "never",
+ anonymous: "always",
+ asyncArrow: "always",
+ }],
+
+ "space-in-parens": "error",
+
+ "space-infix-ops": ["error", {
+ int32Hint: false,
+ }],
+
+ "space-unary-ops": "error",
+ "spaced-comment": "error",
+ "switch-colon-spacing": "error",
+ "symbol-description": "error",
+ "template-curly-spacing": "error",
+ "template-tag-spacing": "error",
+ "unicode-bom": "error",
+ "wrap-iife": ["error", "inside"],
+ "yield-star-spacing": "error",
+ yoda: "error",
+ },
+ },
+];
diff --git a/package.json b/package.json
index 19f7b61..eb81235 100644
--- a/package.json
+++ b/package.json
@@ -3,10 +3,10 @@
"version": "0.1.0",
"description": "this package.json is solely to run eslint",
"scripts": {
- "lint": "eslint *.js modules/*.js"
+ "lint": "eslint src/*.js src/modules/*.js"
},
"devDependencies": {
- "eslint": "^8.39.0",
- "eslint-plugin-jsdoc": "^43.3.1"
+ "eslint": "^9.0.0",
+ "eslint-plugin-jsdoc": "^50.3.2"
}
}
diff --git a/src/extension.js b/src/extension.js
index 3581e2c..d2bd019 100644
--- a/src/extension.js
+++ b/src/extension.js
@@ -1,24 +1,23 @@
"use strict";
-import Clutter from 'gi://Clutter';
-import * as Main from 'resource:///org/gnome/shell/ui/main.js';
-import St from 'gi://St';
-import Gio from 'gi://Gio';
-import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
-import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
-import * as Dialog from 'resource:///org/gnome/shell/ui/dialog.js';
-import * as ModalDialog from 'resource:///org/gnome/shell/ui/modalDialog.js';
-import GObject from 'gi://GObject';
+import Clutter from "gi://Clutter";
+import * as Main from "resource:///org/gnome/shell/ui/main.js";
+import St from "gi://St";
+import Gio from "gi://Gio";
+import * as PanelMenu from "resource:///org/gnome/shell/ui/panelMenu.js";
+import * as PopupMenu from "resource:///org/gnome/shell/ui/popupMenu.js";
+import * as Dialog from "resource:///org/gnome/shell/ui/dialog.js";
+import * as ModalDialog from "resource:///org/gnome/shell/ui/modalDialog.js";
+import GObject from "gi://GObject";
-import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
+import {Extension} from "resource:///org/gnome/shell/extensions/extension.js";
-import * as Podman from './modules/podman.js';
+import * as Podman from "./modules/podman.js";
export default class ContainersExtension extends Extension {
/**
* enable is the entry point called by gnome-shell
*/
- // eslint-disable-next-line no-unused-vars
enable() {
console.log(`enabling ${this.uuid} extension`);
this._indicator = new PanelMenu.Button(0.0, this.metadata.name, false);
@@ -49,7 +48,6 @@ export default class ContainersExtension extends Extension {
/**
* disable is called when the main extension menu is closed
*/
- // eslint-disable-next-line no-unused-vars
disable() {
console.log("disabling containers extension");
this._indicator?.destroy();
@@ -58,8 +56,8 @@ export default class ContainersExtension extends Extension {
}
async _sync() {
- this.podmanListenCmd = await Podman.newEventsProcess((containerEvent) => {
- console.debug("container event for container " + containerEvent.name)
+ this.podmanListenCmd = await Podman.newEventsProcess(containerEvent => {
+ console.debug(`container event for container ${containerEvent.name}`);
this._renderMenu();
});
}
@@ -69,15 +67,15 @@ export default class ContainersExtension extends Extension {
const out = this.podmanListenCmd?.get_stdout_pipe();
await out.close_async(0, null, () => {});
await this.podmanListenCmd.force_exit();
- console.debug("podman events process status " + this.podmanListenCmd.get_status());
+ console.debug(`podman events process status ${this.podmanListenCmd.get_status()}`);
} catch (e) {
- console.error("cleaning up podman events subprocess failed" + e);
+ console.error(`cleaning up podman events subprocess failed ${e}`);
}
}
async _renderMenu() {
try {
- const containers = await Podman.getContainers();
+ const containers = await Podman.getContainers(this._settings);
console.debug(`found ${containers.length} containers`);
this.menu.removeAll();
@@ -101,7 +99,7 @@ export default class ContainersExtension extends Extension {
if (containers.length > 0) {
containers.forEach(container => {
console.debug(container.toString());
- this.menu.addMenuItem(new ContainerSubMenuItem(container, { extraInfo: this._settings.get_boolean("extra-info")}));
+ this.menu.addMenuItem(new ContainerSubMenuItem(container, {extraInfo: this._settings.get_boolean("extra-info")}));
});
} else {
this.menu.addMenuItem(new PopupMenu.PopupMenuItem("No containers detected"));
@@ -128,7 +126,7 @@ class ContainerSubMenuItem extends PopupMenu.PopupSubMenuMenuItem {
const actions = new PopupMenu.PopupBaseMenuItem({reactive: false, can_focus: false, style_class: "container-action-bar"});
actions.actor.set_x_expand(true);
actions.actor.set_x_align(Clutter.ActorAlign.END);
- //this.insert_child_at_index(actions, 2);
+ // this.insert_child_at_index(actions, 2);
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
const startBtn = createActionButton(() => container.start(), "media-playback-start-symbolic");
@@ -200,7 +198,7 @@ class ContainerSubMenuItem extends PopupMenu.PopupSubMenuMenuItem {
this.menu.addAction("Watch Statistics", () => container.stats());
this.menu.addAction("Copy Container Details", () => setClipboard(container.details()));
// the css nth- or last-of-type is probably not implemented in gjs
- this.menu.box.get_children().at(-1).add_style_class_name("last-container-menu-item");
+ this.menu.box.get_children().at(-1).add_style_class_name("last-container-menu-item");
}
}
diff --git a/src/metadata.json b/src/metadata.json
index 8fdfc0b..d16fa48 100644
--- a/src/metadata.json
+++ b/src/metadata.json
@@ -5,7 +5,8 @@
"uuid": "containers@royg",
"shell-version": [
"45",
- "46"
+ "46",
+ "47"
],
"settings-schema": "org.gnome.shell.extensions.containers"
}
diff --git a/src/modules/podman.js b/src/modules/podman.js
index 00e8b86..20a0fab 100644
--- a/src/modules/podman.js
+++ b/src/modules/podman.js
@@ -1,8 +1,8 @@
"use strict";
-import Gio from 'gi://Gio';
-import GLib from 'gi://GLib';
-import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import Gio from "gi://Gio";
+import GLib from "gi://GLib";
+import * as Main from "resource:///org/gnome/shell/ui/main.js";
const TERM_KEEP_ON_EXIT = true;
const TERM_CLOSE_ON_EXIT = false;
@@ -12,9 +12,12 @@ Gio._promisify(Gio.Subprocess.prototype,
let podmanVersion;
-/** @returns {Container[]} list of containers as reported by podman */
-// eslint-disable-next-line no-unused-vars
-export async function getContainers() {
+/**
+ * Get a list of containers
+ * @param {Gio.settings} settings - The extension settings
+ * @returns {Container[]} list of containers as reported by podman
+ */
+export async function getContainers(settings) {
if (podmanVersion === undefined) {
await discoverPodmanVersion();
}
@@ -35,14 +38,16 @@ export async function getContainers() {
const containers = [];
jsonContainers.forEach(e => {
- let c = new Container(e);
+ let c = new Container(settings, e);
containers.push(c);
});
return containers;
}
class Container {
- constructor(jsonContainer) {
+ // settings: the extension's Gio.settings
+ constructor(settings, jsonContainer) {
+ this.terminal = settings.get_string("terminal");
if (podmanVersion.newerOrEqualTo("2.0.3")) {
this.name = jsonContainer.Names[0];
this.id = jsonContainer.Id;
@@ -94,19 +99,19 @@ class Container {
logs() {
console.debug(`this state ${this.state} and is this === running ${this.state === "running"}`);
- runCommandInTerminal("podman logs -f", this.name, "", this.state === "running" ? TERM_CLOSE_ON_EXIT : TERM_KEEP_ON_EXIT);
+ runCommandInTerminal(this.terminal, "podman logs -f", this.name, "", this.state === "running" ? TERM_CLOSE_ON_EXIT : TERM_KEEP_ON_EXIT);
}
watchTop() {
- runCommandInTerminal("watch podman top", this.name, "");
+ runCommandInTerminal(this.terminal, "watch podman top", this.name, "");
}
shell() {
- runCommandInTerminal("podman exec -it", this.name, "/bin/sh");
+ runCommandInTerminal(this.terminal, "podman exec -it", this.name, "/bin/sh");
}
stats() {
- runCommandInTerminal("podman stats", this.name, "");
+ runCommandInTerminal(this.terminal, "podman stats", this.name, "");
}
async inspect() {
@@ -131,8 +136,8 @@ class Container {
`Status: ${this.status}`,
`Image: ${this.image}`,
`Created: ${this.createdAt}`,
- `Started: ${this.startedAt !== null ? this.startedAt : "never"}`
- ]
+ `Started: ${this.startedAt !== null ? this.startedAt : "never"}`,
+ ];
if (this.Command !== null) {
containerDetails.push(`Command: ${this.command}`);
}
@@ -149,8 +154,9 @@ class Container {
}
}
-/** discoverPodmanVersion fetches the podman version from cli */
-// eslint-disable-next-line no-unused-vars
+/**
+ * discoverPodmanVersion fetches the podman version from cli
+ */
async function discoverPodmanVersion() {
let versionJson;
@@ -209,8 +215,8 @@ class Version {
/**
* spawnCommandline runs a shell command and returns its output
- * @param {string} cmdline - the command line to spawn
- * @returns {string} - the command output
+ * @param {string} cmdline the command line to spawn
+ * @returns {string} the command output
* @throws
*/
export async function spawnCommandline(cmdline) {
@@ -228,9 +234,9 @@ export async function spawnCommandline(cmdline) {
/**
* runCommand runs a podman container command using the cli
- * @param {string} command the command verb
+ * @param {string} command the command verb
* @param {string} containerName is the contaier name
- * @returns {string} command output
+ * @returns {string} command output
*/
async function runCommand(command, containerName) {
const cmdline = `podman ${command} ${containerName}`;
@@ -253,19 +259,20 @@ async function runCommand(command, containerName) {
* runCommandInTerminal runs a podman container command using the cli
* and in gnome-terminal(unconfigurable atm) visible to users to present output.
* Useful for logs, top, and stats container-commands.
- * @param {string} command {string} the command verb
- * @param {string} containerName {string} is the contaier name
- * @param {...string} args to pass to the invocation
+ * @param {string} terminal the terminal program plus extra args if needed to execute in
+ * @param {string} command the podman verb
+ * @param {string} containerName is the container name
+ * @param {string[]} args extra args to pass to the podman invocation
* @param {boolean} keepOpenOnExit true means keep the terminal open when the command terminates
- * and/or when the output stream is closed. False means that if the logs can't be followed the terminal
- * just exits. For commands that are streaming like 'stats' this doesn't have and effect.
+ * and/or when the output stream is closed. False means that if the logs can't be followed the terminal
+ * just exits. For commands that are streaming like 'stats' this doesn't have an effect.
*/
-function runCommandInTerminal(command, containerName, args, keepOpenOnExit) {
+function runCommandInTerminal(terminal, command, containerName, args, keepOpenOnExit) {
let cmdline;
if (keepOpenOnExit) {
- cmdline = `gnome-terminal -- bash -c '${command} ${containerName} ${args};read i'`;
+ cmdline = `${terminal} bash -c '${command} ${containerName} ${args};read i'`;
} else {
- cmdline = `gnome-terminal -- ${command} ${containerName} ${args}`;
+ cmdline = `${terminal} ${command} ${containerName} ${args}`;
}
console.debug(`running command ${cmdline}`);
try {
@@ -273,53 +280,58 @@ function runCommandInTerminal(command, containerName, args, keepOpenOnExit) {
console.debug(`command on ${containerName} terminated successfully`);
} catch (e) {
const errMsg = `Error occurred when running ${command} on container ${containerName}`;
- Main.notify(errMsg);
- console.error(errMsg);
+ Main.notify(errMsg, e.message);
+ console.error(`${errMsg}: ${e.message}`);
}
}
+/**
+ * start listening to podman events in a separate process, each event is a line read.
+ * @param {Function} onEvent - run onEvent function on every line read
+ * @returns {Gio.Subprocess} process - The process handle
+ */
export async function newEventsProcess(onEvent) {
try {
const cmdline = "podman events --filter type=container --format '{\"name\": \"{{ .Name }}\"}'";
const [, argv] = GLib.shell_parse_argv(cmdline);
- const process = Gio.Subprocess.new(argv,
- Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
-
+ const process = Gio.Subprocess.new(argv, Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
const pipe = process.get_stdout_pipe();
await _read(pipe, onEvent);
return process;
-
} catch (e) {
console.error(e.message);
throw new Error("Error occurred when fetching containers");
}
}
+/**
+ * Read the input straem as a json a apply the onEvent function on it
+ * @param {Gio.inputStream} inputStream - Input stream of an array of json messages, where each entry is a single event on a container. See "man podman-events".
+ * @param {Function} onEvent - Function to apply on each container event
+ */
async function _read(inputStream, onEvent) {
- const content = await inputStream.read_bytes_async(4096, GLib.PRIORITY_DEFAULT, null, (source, result) => {
+ await inputStream.read_bytes_async(4096, GLib.PRIORITY_DEFAULT, null, (source, result) => {
const rawjson = new TextDecoder().decode(source.read_bytes_finish(result).toArray());
- console.debug("raw json answer " + rawjson);
+ console.debug(`raw json answer: ${rawjson}`);
if (rawjson === "") {
// no output is EOF, no need to continue processing
return;
}
const rawjsonArray = rawjson.split(/\n/);
- rawjsonArray.forEach( j => {
+ rawjsonArray.forEach(j => {
if (j !== "") {
try {
const containerEvent = JSON.parse(j);
- console.debug("firing callback on container event " + containerEvent);
+ console.debug(`firing callback on container event ${containerEvent}`);
onEvent(containerEvent);
} catch (e) {
- console.error("json parse error " + e);
+ console.error(`json parse error ${e}`);
}
}
});
if (!source.is_closed()) {
// keep reading
_read(source, onEvent);
- } else {
- return;
}
});
}
diff --git a/src/prefs.js b/src/prefs.js
index 17390ed..e9bf3c6 100644
--- a/src/prefs.js
+++ b/src/prefs.js
@@ -1,34 +1,43 @@
-import Gio from 'gi://Gio';
-import Adw from 'gi://Adw';
-
-import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
+import Gio from "gi://Gio";
+import Adw from "gi://Adw";
+import {ExtensionPreferences, gettext as _} from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
export default class ContainersPreferences extends ExtensionPreferences {
fillPreferencesWindow(window) {
- // Create a preferences page, with a single group
+ window._settings = this.getSettings();
+
const page = new Adw.PreferencesPage({
- title: _('General'),
- icon_name: 'dialog-information-symbolic',
+ title: _("General"),
+ icon_name: "dialog-information-symbolic",
});
window.add(page);
- const group = new Adw.PreferencesGroup({
- title: _('Appearance'),
- description: _('Configure the appearance of the extension'),
+ const appearanceGroup = new Adw.PreferencesGroup({
+ title: _("Appearance"),
+ description: _("Configure the appearance of the extension"),
});
- page.add(group);
+ page.add(appearanceGroup);
- // Create a new preferences row
- const row = new Adw.SwitchRow({
- title: _('Extra Info'),
- subtitle: _('Whether to show extra info of a container in name and the opened menu'),
+ const extraInfoRow = new Adw.SwitchRow({
+ title: _("Extra Info"),
+ subtitle: _("Whether to show extra info of a container in name and the opened menu"),
});
- group.add(row);
+ appearanceGroup.add(extraInfoRow);
+ window._settings.bind("extra-info", extraInfoRow, "active", Gio.SettingsBindFlags.DEFAULT);
- // Create a settings object and bind the row to the `extra-info` key
- window._settings = this.getSettings();
- window._settings.bind('extra-info', row, 'active',
- Gio.SettingsBindFlags.DEFAULT);
+ const behaviourGroup = new Adw.PreferencesGroup({
+ title: _("Behaviour"),
+ description: _("Configure the behaviour of the extension"),
+ });
+ page.add(behaviourGroup);
+
+ const terminalRow = new Adw.EntryRow({
+ title: _("Terminal program with arguments"),
+ show_apply_button: true, // Allows user to apply the input
+ });
+ behaviourGroup.add(terminalRow);
+ window._settings.bind("terminal", terminalRow, "text", Gio.SettingsBindFlags.DEFAULT);
}
}
+
diff --git a/src/schemas/org.gnome.shell.extensions.containers.gschema.xml b/src/schemas/org.gnome.shell.extensions.containers.gschema.xml
index f2bbfd7..af3d4cd 100644
--- a/src/schemas/org.gnome.shell.extensions.containers.gschema.xml
+++ b/src/schemas/org.gnome.shell.extensions.containers.gschema.xml
@@ -4,5 +4,9 @@
true
-
+
+ "gnome-terminal --"
+ The terminal program and arguments to use for running commands such as shell, logs, stats, and watch
+
+
diff --git a/src/stylesheet.css b/src/stylesheet.css
index b052735..858e6d7 100644
--- a/src/stylesheet.css
+++ b/src/stylesheet.css
@@ -62,6 +62,5 @@
.container-info {
font-size: 10px;
- font-variant: small-caps;
font-family: monospace;
}