From 4acc02c91d137088661ae117846fd6bd5fa9db8f Mon Sep 17 00:00:00 2001 From: Roy Golan Date: Wed, 16 Oct 2024 10:06:39 +0300 Subject: [PATCH] Configurable terminal program (#58) * Add the extension zip into the release Signed-off-by: Roy Golan * Use font-variant normal for the container extra-info Signed-off-by: Roy Golan * Support configuring the terminal program The extension preferences has a new entry for the terminal program to use for running the commands in a terminal. The default value is "gnome-terminal --" which is the current behaviour. Here are terminal values that were tested and worked: "gnome-terminal --" "ptyxis --" "kitty" "flatpak run com.gexperts.Tilix -e" "flatpak run app.studiodev.Ptyxis --" Signed-off-by: Roy Golan * ci: lint action update - migrate to eslint 9 - migrate .eslint.yaml to eslint.config.mjs Signed-off-by: Roy Golan * Lint fixes Signed-off-by: Roy Golan * Fix Main.notify call to use 2 args Signed-off-by: Roy Golan * Support gnome 47 Signed-off-by: Roy Golan --------- Signed-off-by: Roy Golan --- .eslintrc.yaml | 252 ----------------- .github/workflows/main.yml | 9 +- .github/workflows/release.yml | 2 +- Makefile | 7 +- README.md | 19 ++ eslint.config.mjs | 261 ++++++++++++++++++ package.json | 6 +- src/extension.js | 40 ++- src/metadata.json | 3 +- src/modules/podman.js | 94 ++++--- src/prefs.js | 49 ++-- ...me.shell.extensions.containers.gschema.xml | 6 +- src/stylesheet.css | 1 - 13 files changed, 401 insertions(+), 348 deletions(-) delete mode 100644 .eslintrc.yaml create mode 100644 eslint.config.mjs 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; }