diff --git a/.gitignore b/.gitignore index 27736ea..d66fcab 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ dist/ node_modules/ yarn.lock package-lock.json -docs-lock.json \ No newline at end of file +docs-lock.json +src/schemas/gschemas.compiled +src/po/*~ \ No newline at end of file diff --git a/Makefile b/Makefile index c3f6316..7e03e73 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,56 @@ #!/usr/bin/make -f -.PHONY : build clean install uninstall -.DEFAULT_GOAL := help +.PHONY : build clean install uninstall open-prefs spawn-gnome-shell translations +.DEFAULT_GOAL := build UUID = runcat@kolesnikov.se +DIST_ARCHIVE = $(UUID).shell-extension.zip LOCAL = $(HOME)/.local/share/gnome-shell/extensions -js_sources = $(shell find src -maxdepth 1 -type f -name '*.js') +all_sources = $(shell find src -type f) -build: clean +js_sources = $(shell cd src && find . -maxdepth 1 -type f -name '*.js') + +translations_sources = src/panelMenuButton.js src/prefs.js +translations_sources += $(shell find src/resources/ui -maxdepth 1 -type f -name '*.ui') +translations = $(shell find src/po -maxdepth 1 -type f -name '*.po') + + +build: dist/$(DIST_ARCHIVE) + +dist: mkdir -p dist/ + +dist/$(DIST_ARCHIVE): dist $(all_sources) gnome-extensions pack -f src/ \ - $(addprefix --extra-source=../, $(js_sources)) \ + $(addprefix --extra-source=, $(js_sources)) \ + --extra-source=./dataProviders \ + --extra-source=./resources \ --extra-source=../assets \ --extra-source=../LICENSE \ + --podir=./po \ -o dist/ + +src/po/%.po: src/po/messages.pot + msgmerge --update $@ src/po/messages.pot + +src/po/messages.pot: $(translations_sources) + touch src/po/messages.pot && \ + xgettext \ + --package-name gnome-runcat-extension \ + --package-version 20 \ + --from-code=UTF-8 \ + --output=src/po/messages.pot \ + $^ + +translations: src/po/messages.pot $(translations) + clean: rm -rf dist install: uninstall build - gnome-extensions install dist/$(UUID).shell-extension.zip --force + gnome-extensions install dist/$(DIST_ARCHIVE) --force gnome-extensions enable $(UUID) || true echo "You need to restart GNOME Shell to apply changes" @@ -31,8 +61,8 @@ uninstall: open-prefs: gnome-extensions prefs $(UUID) -spawn-gnome-shell: install - env MUTTER_DEBUG_DUMMY_MODE_SPECS=1280x720 \ +spawn-gnome-shell: + env MUTTER_DEBUG_DUMMY_MODE_SPECS=1600x800 \ MUTTER_DEBUG_DUMMY_MONITOR_SCALES=1 \ dbus-run-session -- gnome-shell --nested --wayland diff --git a/README.md b/README.md index e98437e..46f6c5b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -RunCat for GNOME Shell Logo +RunCat for GNOME Shell Logo # RunCat for GNOME Shell @@ -15,7 +15,7 @@ This is the recommended method for installation, as it doesn't require the build [](https://extensions.gnome.org/extension/2986/runcat/) -### Manual installation +### Manual installation #### From source code If you want to install the extension from sources, clone [the RunCat repository](https://github.com/win0err/gnome-runcat), navigate to the cloned directory and run: @@ -24,23 +24,24 @@ $ make install ``` #### Release ZIP-archive -If you want to install the extension from release zip-archive, -download `runcat@kolesnikov.se.zip` from [the releases section](https://github.com/win0err/gnome-runcat/releases) and run: +If you want to install the extension from release zip-archive, +download `runcat@kolesnikov.se.shell-extension.zip` from [the releases section](https://github.com/win0err/gnome-runcat/releases) and run: ```bash -$ gnome-extensions install path/to/runcat@kolesnikov.se.zip --force +$ gnome-extensions install path/to/runcat@kolesnikov.se.shell-extension.zip --force ``` #### After installation: -1. Restart the GNOME Shell: +1. Restart the GNOME Shell: - ALT+F2 to open the command prompt, and enter r to restart the GNOME Shell; - or Log Out, then Log In, if GNOME Shell won't restart; -2. Enable the extension: +2. Enable the extension: - Open GNOME Tweaks → Extensions → RunCat → On; - or Run in terminal: `gnome-extensions enable runcat@kolesnikov.se`. ### Manage RunCat preferences -- Open GNOME Tweaks → Extensions → RunCat → ⚙️; +- Right-click on the extension button on the top bar → Settings; +- or Open GNOME Tweaks → Extensions → RunCat → ⚙️; - or Open [RunCat on GNOME Extensions portal](https://extensions.gnome.org/extension/2986/runcat/) → ⚙️; - or Manage directly in `dconf`: `dconf list /org/gnome/shell/extensions/runcat/`. diff --git a/package.json b/package.json index 0b547f7..78f66f4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "runcat", "private": true, "scripts": { - "generate:types": "ts-for-gir generate Gtk-4.0 Adw-1 -e gjs", + "generate:types": "ts-for-gir generate Gtk-4.0 Adw-1 Clutter-1.0 -e gjs", "lint": "eslint src/" }, "repository": "win0err/gnome-runcat", diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..92b8f0c --- /dev/null +++ b/src/constants.js @@ -0,0 +1,21 @@ +/* eslint-disable no-var, no-unused-vars */ + +var SCHEMA_PATH = 'org.gnome.shell.extensions.runcat'; +var LOG_ERROR_PREFIX = 'RuncatExtensionError'; + +const CHARACTER_AND_PERCENTAGE = 0; +const PERCENTAGE_ONLY = 1; +const CHARACTER_ONLY = 2; + +var PanelMenuButtonVisibility = { + [CHARACTER_AND_PERCENTAGE]: { character: true, percentage: true }, + [PERCENTAGE_ONLY]: { character: false, percentage: true }, + [CHARACTER_ONLY]: { character: true, percentage: false }, +}; + +var Settings = { + IDLE_THRESHOLD: 'idle-threshold', + DISPLAYING_ITEMS: 'displaying-items', +}; + +var SYSTEM_MONITOR_COMMAND = 'gnome-system-monitor -r'; \ No newline at end of file diff --git a/src/cpu.js b/src/cpu.js deleted file mode 100644 index c341bdf..0000000 --- a/src/cpu.js +++ /dev/null @@ -1,77 +0,0 @@ -const { Gio } = imports.gi; -const ByteArray = imports.byteArray; -const Config = imports.misc.config; - -const [major] = Config.PACKAGE_VERSION.split('.'); -const shellVersion = Number.parseInt(major, 10); - -// eslint-disable-next-line -var Cpu = class Cpu { - constructor() { - this.lastActive = 0; - this.lastTotal = 0; - - this.utilization = 0; - - this.procStatFile = Gio.File.new_for_path('/proc/stat'); - - this.refresh(); - } - - refresh() { - try { - const [success, contents] = this.procStatFile.load_contents(null); - if (!success) { - throw new Error('Can\'t load contents of stat file'); - } - - const procTextData = shellVersion >= 41 - ? new TextDecoder('utf-8').decode(contents) - : ByteArray.toString(contents); - - const cpuInfo = procTextData - .split('\n') - .shift() - .trim() - .split(/[\s]+/) - .map(n => parseInt(n, 10)); - - const [ - , // eslint-disable-line - user, - nice, - system, - idle, - iowait, - irq, // eslint-disable-line - softirq, - steal, - guest, // eslint-disable-line - ] = cpuInfo; - - const active = user + system + nice + softirq + steal; - const total = user + system + nice + softirq + steal + idle + iowait; - - const utilization = 100 * ((active - this.lastActive) / (total - this.lastTotal)); - - this.lastActive = active; - this.lastTotal = total; - - if (Number.isNaN(utilization)) { - const utilizationData = JSON.stringify({ - active, - lastActive: this.lastActive, - total, - lastTotal: this.lastTotal, - }); - throw new RangeError(`CPU utilization is NaN: ${utilizationData}`); - } - - this.utilization = utilization; - } catch (e) { - logError(e, 'RuncatExtensionError'); // eslint-disable-line no-undef - } - - return this.utilization; - } -}; \ No newline at end of file diff --git a/src/dataProviders/cpu.js b/src/dataProviders/cpu.js new file mode 100644 index 0000000..fc4f26b --- /dev/null +++ b/src/dataProviders/cpu.js @@ -0,0 +1,59 @@ +const { Gio } = imports.gi; +const Extension = imports.misc.extensionUtils.getCurrentExtension(); + +const { readFile } = Extension.imports.utils; + +// eslint-disable-next-line func-names, no-unused-vars, no-var +var createGenerator = async function* () { + const procStatFile = Gio.File.new_for_path('/proc/stat'); + + let prevActive = 0; + let prevTotal = 0; + + while (true) { + // eslint-disable-next-line no-await-in-loop + const procStatContents = await readFile(procStatFile); + + const cpuInfo = procStatContents + .split('\n')[0].trim() + .split(/[\s]+/) + .map(n => parseInt(n, 10)); + + const [ + , // eslint-disable-line + user, + nice, + system, + idle, + iowait, + irq, // eslint-disable-line + softirq, + steal, + guest, // eslint-disable-line + ] = cpuInfo; + + const active = user + system + nice + softirq + steal; + const total = user + system + nice + softirq + steal + idle + iowait; + + // eslint-disable-next-line object-curly-newline + const data = JSON.stringify({ total, active, prevTotal, prevActive }); + + let utilization = 100 * ((active - prevActive) / (total - prevTotal)); + if (Number.isNaN(utilization) || !Number.isFinite(utilization)) { + log(`cpu utilization is ${utilization}, data: ${data}`); + + utilization = 0; + } + + if (utilization > 100) { + log(`cpu utilization is ${utilization}, data: ${data}`); + + utilization = 100; + } + + prevActive = active; + prevTotal = total; + + yield utilization; + } +}; \ No newline at end of file diff --git a/src/extension.js b/src/extension.js index 5504117..20bbc53 100644 --- a/src/extension.js +++ b/src/extension.js @@ -1,7 +1,10 @@ 'use strict'; +imports.gi.versions.Gtk = '4.0'; + const Main = imports.ui.main; -const Extension = imports.misc.extensionUtils.getCurrentExtension(); +const ExtensionUtils = imports.misc.extensionUtils; +const Extension = ExtensionUtils.getCurrentExtension(); const { PanelMenuButton } = Extension.imports.panelMenuButton; @@ -22,5 +25,7 @@ class RunCatExtension { } function init() { + ExtensionUtils.initTranslations(Extension.metadata.uuid); + return new RunCatExtension(); } \ No newline at end of file diff --git a/src/iconProvider.js b/src/iconProvider.js deleted file mode 100644 index 7c4ad0b..0000000 --- a/src/iconProvider.js +++ /dev/null @@ -1,41 +0,0 @@ -const { Gio } = imports.gi; -const ExtensionUtils = imports.misc.extensionUtils; - -const Extension = ExtensionUtils.getCurrentExtension(); - -const getGIcon = (name, pack = 'cat') => Gio.icon_new_for_string( - `${Extension.path}/icons/${pack}/my-${name}-symbolic.svg`, -); - -// eslint-disable-next-line -var IconProvider = class IconProvider { - constructor(spritesCount = 5) { - this.spritesCount = spritesCount; - this.currentSprite = 0; - - this._sleeping = getGIcon('sleeping'); - - this.sprites = [...Array(spritesCount).keys()] - .map(i => getGIcon(`running-${i}`)); - } - - get sleeping() { - this.reset(); - - return this._sleeping; - } - - get nextSprite() { - this.currentSprite++; - - if (this.currentSprite === this.spritesCount) { - this.reset(); - } - - return this.sprites[this.currentSprite]; - } - - reset() { - this.currentSprite = 0; - } -}; \ No newline at end of file diff --git a/src/metadata.json b/src/metadata.json index 7830aa7..b442f4a 100644 --- a/src/metadata.json +++ b/src/metadata.json @@ -2,13 +2,12 @@ "name": "RunCat", "description": "The cat tells you the CPU usage by running speed", "uuid": "runcat@kolesnikov.se", + "gettext-domain": "runcat@kolesnikov.se", "shell-version": [ - "3.38", - "40", - "41", "42", "43" ], + "session-modes": ["user"], "url": "https://github.com/win0err/gnome-runcat", - "version": 19 + "version": 20 } diff --git a/src/panelMenuButton.js b/src/panelMenuButton.js index 33373c3..086ff4d 100644 --- a/src/panelMenuButton.js +++ b/src/panelMenuButton.js @@ -1,12 +1,47 @@ const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; +const { trySpawnCommandLine } = imports.misc.util; + const ExtensionUtils = imports.misc.extensionUtils; const Extension = ExtensionUtils.getCurrentExtension(); -const { St, Clutter, GObject } = imports.gi; +const { + Gio, + GObject, + Gtk, + GLib, +} = imports.gi; + +const _ = imports.gettext.domain(Extension.metadata.uuid).gettext; + +const { + SYSTEM_MONITOR_COMMAND, + SCHEMA_PATH, + PanelMenuButtonVisibility, + Settings, +} = Extension.imports.constants; +const { createGenerator: createCpuGenerator } = Extension.imports.dataProviders.cpu; + +const getGIcon = name => Gio.icon_new_for_string( + `${Extension.path}/resources/icons/cat/my-${name}-symbolic.svg`, +); + +// eslint-disable-next-line func-names +const spritesGenerator = function* () { + const SPRITES_COUNT = 5; + + const sprites = [...Array(SPRITES_COUNT).keys()] + .map(i => getGIcon(`active-${i}`)); + + let i; + while (true) { + for (i = 0; i < SPRITES_COUNT; i++) { + yield sprites[i]; + } + } +}; -const { Settings } = Extension.imports.settings; -const { Timer } = Extension.imports.timer; -const { Cpu } = Extension.imports.cpu; -const { IconProvider } = Extension.imports.iconProvider; +// y = 5000/sqrt(x+30) - 400 +const getAnimationInterval = cpuUtilization => Math.ceil(5000 / Math.sqrt(cpuUtilization + 30) - 400); // eslint-disable-next-line var PanelMenuButton = GObject.registerClass( @@ -15,131 +50,132 @@ var PanelMenuButton = GObject.registerClass( _init() { super._init(null, Extension.metadata.name); - this.cpu = new Cpu(); - this.iconProvider = new IconProvider(); + this.dataProviders = { + cpu: createCpuGenerator(), + }; - this.ui = new Map(); - this.timers = new Map(); + this.initSettingsListeners(); + this.initUi(); + this.initSources(); + } - this.currentSprite = 0; + initUi() { + this.ui = { + builder: Gtk.Builder.new(), + icons: { + idle: getGIcon('idle'), + runningGenerator: spritesGenerator(), + }, + }; + this.ui.builder.set_translation_domain(Extension.metadata.uuid); + + const itemsVisibility = PanelMenuButtonVisibility[this.settings.displayingItems]; + + this.ui.builder.add_from_file(`${Extension.path}/resources/ui/extension.ui`); + + const icon = this.ui.builder.get_object('icon'); + icon.set_property('gicon', this.ui.icons.idle); + if (!itemsVisibility.character) { + icon.hide(); + } + + const labelBox = this.ui.builder.get_object('labelBox'); + labelBox.add_child(this.ui.builder.get_object('label')); + if (!itemsVisibility.percentage) { + labelBox.hide(); + } + + const box = this.ui.builder.get_object('box'); + box.add_child(icon); + box.add_child(labelBox); - this._initSettings(); - this._initUi(); - this._initListeners(); - this._initTimers(); - } + this.add_child(box); - get animationInterval() { - const utilizationCoefficient = this.cpu.utilization > 100 ? 100 : this.cpu.utilization; + this.menu.addAction( + _('Open System Monitor'), + () => trySpawnCommandLine(SYSTEM_MONITOR_COMMAND), + ); + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this.menu.addAction(_('Settings'), () => ExtensionUtils.openPrefs()); + } - // y = 5000/sqrt(x+30) - 400 - return Math.ceil(5000 / Math.sqrt(utilizationCoefficient + 30) - 400); + destroyUi() { + this.ui.builder.get_object('icon').destroy(); + this.ui.builder.get_object('label').destroy(); + this.ui.builder.get_object('labelBox').destroy(); + this.ui.builder.get_object('box').destroy(); } - _initSettings() { - this.settings = new Settings(); + updateItemsVisibility() { + const itemsVisibility = PanelMenuButtonVisibility[this.settings.displayingItems]; + + const characterAction = itemsVisibility.character ? 'show' : 'hide'; + const percentageAction = itemsVisibility.percentage ? 'show' : 'hide'; - this.sleepingThreshold = this.settings.sleepingThreshold.get(); - this.isRunnerHidden = this.settings.hideRunner.get(); - this.isPercentageHidden = this.settings.hidePercentage.get(); + this.ui.builder.get_object('icon')[characterAction](); + this.ui.builder.get_object('labelBox')[percentageAction](); } - _initUi() { - const box = new St.BoxLayout({ - style_class: 'panel-status-menu-box runcat-menu', - }); + initSettingsListeners() { + this.gioSettings = ExtensionUtils.getSettings(SCHEMA_PATH); + this.settings = { + idleThreshold: this.gioSettings.get_int(Settings.IDLE_THRESHOLD), + displayingItems: this.gioSettings.get_enum(Settings.DISPLAYING_ITEMS), + }; - const icon = new St.Icon({ - style_class: 'system-status-icon runcat-menu__icon', - gicon: this.iconProvider.sleeping, - }); - this.ui.set('icon', icon); - this._manageUiElementVisibility('icon', this.isRunnerHidden); - - const labelBox = new St.BoxLayout({}); - this.ui.set('labelBox', labelBox); - - const label = new St.Label({ - style_class: 'runcat-menu__label', - y_expand: true, - y_align: Clutter.ActorAlign.CENTER, - x_align: Clutter.ActorAlign.FILL, - x_expand: true, + this.gioSettings.connect(`changed::${Settings.IDLE_THRESHOLD}`, () => { + this.settings.idleThreshold = this.gioSettings.get_int(Settings.IDLE_THRESHOLD); }); - this.ui.set('label', label); - labelBox.add_child(label); - this._manageUiElementVisibility('labelBox', this.isPercentageHidden); - box.add_child(icon); - box.add_child(labelBox); - this.ui.set('box', box); + this.gioSettings.connect(`changed::${Settings.DISPLAYING_ITEMS}`, () => { + this.settings.displayingItems = this.gioSettings.get_enum(Settings.DISPLAYING_ITEMS); - this.add_child(box); + const itemsVisibility = PanelMenuButtonVisibility[this.settings.displayingItems]; + + const characterAction = itemsVisibility.character ? 'show' : 'hide'; + const percentageAction = itemsVisibility.percentage ? 'show' : 'hide'; + + this.ui.builder.get_object('icon')[characterAction](); + this.ui.builder.get_object('labelBox')[percentageAction](); + }); } - _manageUiElementVisibility(elementName, isHidden) { - const action = isHidden ? 'hide' : 'show'; - this.ui.get(elementName)[action](); + async initSources() { + this.refreshDataSourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 3000, () => this.refreshData()); + await this.refreshData(); + + this.repaintUiSourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 0, () => this.repaintUi()); } - _initListeners() { - this.settings.hideRunner.addListener(() => { - this.isRunnerHidden = this.settings.hideRunner.get(); - this._manageUiElementVisibility('icon', this.isRunnerHidden); - }); + destroySources() { + GLib.source_remove(this.refreshDataSourceId); + GLib.source_remove(this.repaintUiSourceId); + } - this.settings.hidePercentage.addListener(() => { - this.isPercentageHidden = this.settings.hidePercentage.get(); - this._manageUiElementVisibility('labelBox', this.isPercentageHidden); - }); + async refreshData() { + const { value: cpu } = await this.dataProviders.cpu.next(); + this.data = { cpu }; - this.settings.sleepingThreshold.addListener(() => { - this.sleepingThreshold = this.settings.sleepingThreshold.get(); - }); + return GLib.SOURCE_CONTINUE; } - _initTimers() { - this.timers.set('cpu', new Timer(() => { - try { - this.cpu.refresh(); - } catch (e) { - logError(e, 'RuncatExtensionError'); // eslint-disable-line no-undef - } - }, 3000)); - - this.timers.set( - 'ui', - new Timer(() => { - try { - if (this.timers.has('ui')) { - this.timers.get('ui').interval = this.animationInterval; - } - - if (!this.isRunnerHidden) { - const isRunningSpriteShown = this.cpu.utilization > this.sleepingThreshold; - this.ui.get('icon').set_gicon( - isRunningSpriteShown ? this.iconProvider.nextSprite : this.iconProvider.sleeping, - ); - } - - if (!this.isPercentageHidden) { - const utilization = Math.ceil(this.cpu.utilization || 0); - this.ui.get('label').set_text(`${utilization}%`); - } - } catch (e) { - logError(e, 'RuncatExtensionError'); // eslint-disable-line no-undef - } - }, 250), - ); + repaintUi() { + const isRunningSpriteShown = this.data?.cpu > this.settings.idleThreshold; + const gicon = isRunningSpriteShown ? this.ui.icons.runningGenerator.next().value : this.ui.icons.idle; + + this.ui.builder.get_object('icon').set_gicon(gicon); + this.ui.builder.get_object('label').set_text(`${Math.round(this.data.cpu)}%`); + + const animationInterval = getAnimationInterval(this.data.cpu); + + this.repaintUiSourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, animationInterval, () => this.repaintUi()); + + return GLib.SOURCE_REMOVE; } destroy() { - this.timers.forEach(timer => timer.stop()); - this.ui.forEach(element => element.destroy()); - - this.settings.hideRunner.removeAllListeners(); - this.settings.hidePercentage.removeAllListeners(); - this.settings.sleepingThreshold.removeAllListeners(); + this.destroySources(); + this.destroyUi(); super.destroy(); } diff --git a/src/po/messages.pot b/src/po/messages.pot new file mode 100644 index 0000000..521d214 --- /dev/null +++ b/src/po/messages.pot @@ -0,0 +1,90 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gnome-runcat-extension package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: gnome-runcat-extension 20\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-10-01 15:52+0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/panelMenuButton.js:95 +msgid "Open System Monitor" +msgstr "" + +#: src/panelMenuButton.js:99 +msgid "Settings" +msgstr "" + +#: src/prefs.js:48 +msgid "RunCat Settings" +msgstr "" + +#: src/prefs.js:59 +msgid "Version" +msgstr "" + +#: src/resources/ui/preferences.ui:6 +msgid "General" +msgstr "" + +#: src/resources/ui/preferences.ui:10 +msgid "General Preferences" +msgstr "" + +#: src/resources/ui/preferences.ui:14 +msgid "Idle threshold" +msgstr "" + +#: src/resources/ui/preferences.ui:34 +msgid "Displaying items" +msgstr "" + +#: src/resources/ui/preferences.ui:38 +msgid "Character and percentage" +msgstr "" + +#: src/resources/ui/preferences.ui:39 +msgid "Percentage only" +msgstr "" + +#: src/resources/ui/preferences.ui:40 +msgid "Character only" +msgstr "" + +#: src/resources/ui/preferences.ui:51 +msgid "Reset preferences" +msgstr "" + +#: src/resources/ui/preferences.ui:52 +msgid "Reset RunCat preferences to defaults" +msgstr "" + +#: src/resources/ui/preferences.ui:65 +msgid "Reset" +msgstr "" + +#: src/resources/ui/preferences.ui:84 +msgid "The cat tells you the CPU usage by running speed" +msgstr "" + +#: src/resources/ui/preferences.ui:88 +msgid "Visit RunCat's GitHub page" +msgstr "" + +#: src/resources/ui/preferences.ui:98 +msgid "Visit Homepage" +msgstr "" + +#: src/resources/ui/preferences.ui:102 +msgid "About RunCat" +msgstr "" diff --git a/src/po/ru.po b/src/po/ru.po new file mode 100644 index 0000000..d872c6a --- /dev/null +++ b/src/po/ru.po @@ -0,0 +1,93 @@ +# Russian translation for the GNOME RunCat Extension. +# Copyright (C) 2022 Sergei Kolesnikov +# This file is distributed under the same license as the GNOME RunCat package. +# Sergei Kolesnikov , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: gnome-runcat-extension 20\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-10-01 15:50+0300\n" +"PO-Revision-Date: 2022-09-30 19:16+0300\n" +"Last-Translator: Sergei Kolesnikov \n" +"Language-Team: Russian\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" + +#: src/panelMenuButton.js:95 +msgid "Open System Monitor" +msgstr "Открыть Системный монитор" + +#: src/panelMenuButton.js:99 +msgid "Settings" +msgstr "Настройки" + +#: src/prefs.js:48 +msgid "RunCat Settings" +msgstr "Настройки RunCat" + +#: src/prefs.js:59 +msgid "Version" +msgstr "Версия" + +#: src/resources/ui/preferences.ui:6 +msgid "General" +msgstr "Общие" + +#: src/resources/ui/preferences.ui:10 +msgid "General Preferences" +msgstr "Общие настройки" + +#: src/resources/ui/preferences.ui:14 +msgid "Idle threshold" +msgstr "Порог бездействия" + +#: src/resources/ui/preferences.ui:34 +msgid "Displaying items" +msgstr "Отображаемые элементы" + +#: src/resources/ui/preferences.ui:38 +msgid "Character and percentage" +msgstr "Персонаж и проценты" + +#: src/resources/ui/preferences.ui:39 +msgid "Percentage only" +msgstr "Только проценты" + +#: src/resources/ui/preferences.ui:40 +msgid "Character only" +msgstr "Только персонаж" + +#: src/resources/ui/preferences.ui:51 +msgid "Reset preferences" +msgstr "Сбросить настройки" + +#: src/resources/ui/preferences.ui:52 +msgid "Reset RunCat preferences to defaults" +msgstr "Сбросить настройки RunCat к значениям по умолчанию" + +#: src/resources/ui/preferences.ui:65 +msgid "Reset" +msgstr "Сбрость" + +#: src/resources/ui/preferences.ui:84 +msgid "The cat tells you the CPU usage by running speed" +msgstr "" +"Котик, который показывает загрузку\n" +" процессора скоростью бега" + +#: src/resources/ui/preferences.ui:88 +msgid "Visit RunCat's GitHub page" +msgstr "Посетить страницу RunCat на GitHub" + +#: src/resources/ui/preferences.ui:98 +msgid "Visit Homepage" +msgstr "Открыть домашнюю страницу" + +#: src/resources/ui/preferences.ui:102 +msgid "About RunCat" +msgstr "О RunCat" diff --git a/src/prefs.js b/src/prefs.js index 431cf34..3ed90e2 100644 --- a/src/prefs.js +++ b/src/prefs.js @@ -1,220 +1,77 @@ -const { GObject, Gtk } = imports.gi; -const Extension = imports.misc.extensionUtils.getCurrentExtension(); - -const Config = imports.misc.config; -const [major] = Config.PACKAGE_VERSION.split('.'); -const shellVersion = Number.parseInt(major, 10); - -const isGtk4 = shellVersion >= 40; - -const { Settings } = Extension.imports.settings; - -const BaseComponent = isGtk4 ? Gtk.ScrolledWindow : Gtk.Box; - -const RuncatSettingsWidget = GObject.registerClass( - { GTypeName: 'RuncatSettingsWidget' }, - class RuncatSettingsWidget extends BaseComponent { - _init() { - if (isGtk4) { - super._init({ - hscrollbar_policy: Gtk.PolicyType.NEVER, - vexpand: true, - }); - } else { - super._init({ - orientation: Gtk.Orientation.VERTICAL, - border_width: 20, - spacing: 20, - }); - } - - this._settings = new Settings(); - if (isGtk4) { - this._box = new Gtk.Box({ - orientation: Gtk.Orientation.VERTICAL, - halign: Gtk.Align.CENTER, - spacing: 20, - margin_top: 20, - margin_bottom: 20, - margin_start: 20, - margin_end: 20, - }); - - this.set_child(this._box); - } - - this._initSleepingThreshold(); - this._initShowComboBox(); - this._initBottomButtons(); - - if (!isGtk4) { - this.show_all(); - } - } - - _initSleepingThreshold() { - const hbox = new Gtk.Box({ - orientation: Gtk.Orientation.HORIZONTAL, - spacing: 20, - }); - - const label = new Gtk.Label({ - label: 'Sleeping Threshold', - use_markup: true, - }); - - const scaleConfig = { - digits: 0, - adjustment: new Gtk.Adjustment({ - lower: 0, - upper: 100, - }), - hexpand: true, - halign: Gtk.Align.END, - }; - - let scale; - if (isGtk4) { - scale = new Gtk.Scale({ - ...scaleConfig, - draw_value: true, - }); - } else { - scale = new Gtk.HScale({ - ...scaleConfig, - value_pos: Gtk.PositionType.RIGHT, - }); - } - scale.set_size_request(400, 15); - scale.set_value(this._settings.sleepingThreshold.get()); - - this._settings.sleepingThreshold.addListener(() => { - const updatedValue = this._settings.sleepingThreshold.get(); - if (updatedValue !== scale.get_value()) { - scale.set_value(updatedValue); - } - }); - scale.connect('value-changed', () => { - const updatedValue = scale.get_value(); - if (updatedValue !== this._settings.sleepingThreshold.get()) { - this._settings.sleepingThreshold.set(scale.get_value()); - } - }); - this.connect('destroy', () => this._settings.sleepingThreshold.removeAllListeners()); - - if (isGtk4) { - hbox.append(label); - hbox.append(scale); - - this._box.append(hbox); - } else { - hbox.add(label); - hbox.add(scale); - - this.add(hbox); - } - } - - _initShowComboBox() { - const hbox = new Gtk.Box({ - orientation: Gtk.Orientation.HORIZONTAL, - }); - - const label = new Gtk.Label({ - label: 'Show', - use_markup: true, - margin_end: 20, - }); - - const combo = new Gtk.ComboBoxText({ - halign: Gtk.Align.END, - visible: true, - }); - - const options = ['Runner and percentage', 'Percentage only', 'Runner only']; - options.forEach(opt => combo.append(opt, opt)); - - combo.set_active(this._getActiveShowIndex()); - - this._settings.hideRunner.addListener(() => { - combo.set_active(this._getActiveShowIndex()); - }); - this._settings.hidePercentage.addListener(() => { - combo.set_active(this._getActiveShowIndex()); - }); - combo.connect('changed', widget => { - switch (widget.get_active()) { - case 0: // show runner & percentage - this._settings.hideRunner.set(false); - this._settings.hidePercentage.set(false); - break; - case 1: // show percentage only - this._settings.hideRunner.set(true); - this._settings.hidePercentage.set(false); - break; - case 2: // show runner only - this._settings.hideRunner.set(false); - this._settings.hidePercentage.set(true); - break; - default: - break; - } - }); - - this.connect('destroy', () => { - this._settings.hideRunner.removeAllListeners(); - this._settings.hidePercentage.removeAllListeners(); - }); - - if (isGtk4) { - hbox.append(label); - hbox.append(combo); - - this._box.append(hbox); - } else { - hbox.add(label); - hbox.pack_end(combo, false, false, 0); - - this.add(hbox); - } - } - - _getActiveShowIndex() { - const hideRunner = this._settings.hideRunner.get(); - const hidePercentage = this._settings.hidePercentage.get(); - - switch (true) { - case !hideRunner && !hidePercentage: - return 0; - case hideRunner && !hidePercentage: - return 1; - case !hideRunner && hidePercentage: - return 2; - default: - return 0; // default show both - } - } - - _initBottomButtons() { - const resetButton = new Gtk.Button({ label: 'Reset to default' }); - - resetButton.connect('clicked', () => { - this._settings.sleepingThreshold.set(0); - this._settings.hideRunner.set(false); - this._settings.hidePercentage.set(false); - }); - - if (isGtk4) { - this._box.append(resetButton); - } else { - this.pack_end(resetButton, false, false, 0); - } - } - }, -); - -function buildPrefsWidget() { - return new RuncatSettingsWidget(); +const { + Adw, + Gio, + Gdk, + Gtk, +} = imports.gi; + +const ExtensionUtils = imports.misc.extensionUtils; +const Extension = ExtensionUtils.getCurrentExtension(); + +const _ = imports.gettext.domain(Extension.metadata.uuid).gettext; + +const { SCHEMA_PATH, Settings } = Extension.imports.constants; +const { findWidgetByType } = Extension.imports.utils; + +// eslint-disable-next-line no-unused-vars +function fillPreferencesWindow(window) { + const settings = ExtensionUtils.getSettings(SCHEMA_PATH); + + const builder = Gtk.Builder.new(); + builder.set_translation_domain(Extension.metadata.uuid); + builder.add_from_file(`${Extension.path}/resources/ui/preferences.ui`); + + settings.bind( + Settings.IDLE_THRESHOLD, + builder.get_object(Settings.IDLE_THRESHOLD), + 'value', + Gio.SettingsBindFlags.DEFAULT, + ); + + const combo = builder.get_object(Settings.DISPLAYING_ITEMS); + combo.set_selected(settings.get_enum(Settings.DISPLAYING_ITEMS)); + combo.connect('notify::selected', widget => { + settings.set_enum(Settings.DISPLAYING_ITEMS, widget.selected); + }); + + builder.get_object('reset').connect('clicked', () => { + settings.reset(Settings.IDLE_THRESHOLD); + + settings.reset(Settings.DISPLAYING_ITEMS); + combo.set_selected(settings.get_enum(Settings.DISPLAYING_ITEMS)); + }); + + const page = builder.get_object('preferences-general'); + window.add(page); + + // eslint-disable-next-line no-param-reassign + window.title = _('RunCat Settings'); + + const homepageAction = Gio.SimpleAction.new('homepage', null); + homepageAction.connect('activate', () => Gtk.show_uri(window, Extension.metadata.url, Gdk.CURRENT_TIME)); + + const aboutAction = Gio.SimpleAction.new('about', null); + aboutAction.connect('activate', () => { + const logo = Gtk.Image.new_from_file(`${Extension.path}/resources/se.kolesnikov.runcat.svg`); + + const aboutDialog = builder.get_object('about-dialog'); + aboutDialog.set_property('logo', logo.get_paintable()); + const versionText = _('Version'); + aboutDialog.set_property('version', `${versionText} ${Extension.metadata.version}`); + aboutDialog.set_property('transient_for', window); + + aboutDialog.show(); + }); + + const group = Gio.SimpleActionGroup.new(); + group.add_action(homepageAction); + group.add_action(aboutAction); + + const menu = builder.get_object('menu-button'); + menu.insert_action_group('prefs', group); + + const header = findWidgetByType(window.get_content(), Adw.HeaderBar); + header.pack_end(menu); } function init() { diff --git a/src/icons/cat/my-running-0-symbolic.svg b/src/resources/icons/cat/my-active-0-symbolic.svg similarity index 100% rename from src/icons/cat/my-running-0-symbolic.svg rename to src/resources/icons/cat/my-active-0-symbolic.svg diff --git a/src/icons/cat/my-running-1-symbolic.svg b/src/resources/icons/cat/my-active-1-symbolic.svg similarity index 100% rename from src/icons/cat/my-running-1-symbolic.svg rename to src/resources/icons/cat/my-active-1-symbolic.svg diff --git a/src/icons/cat/my-running-2-symbolic.svg b/src/resources/icons/cat/my-active-2-symbolic.svg similarity index 100% rename from src/icons/cat/my-running-2-symbolic.svg rename to src/resources/icons/cat/my-active-2-symbolic.svg diff --git a/src/icons/cat/my-running-3-symbolic.svg b/src/resources/icons/cat/my-active-3-symbolic.svg similarity index 100% rename from src/icons/cat/my-running-3-symbolic.svg rename to src/resources/icons/cat/my-active-3-symbolic.svg diff --git a/src/icons/cat/my-running-4-symbolic.svg b/src/resources/icons/cat/my-active-4-symbolic.svg similarity index 100% rename from src/icons/cat/my-running-4-symbolic.svg rename to src/resources/icons/cat/my-active-4-symbolic.svg diff --git a/src/icons/cat/my-sleeping-symbolic.svg b/src/resources/icons/cat/my-idle-symbolic.svg similarity index 100% rename from src/icons/cat/my-sleeping-symbolic.svg rename to src/resources/icons/cat/my-idle-symbolic.svg diff --git a/assets/se.kolesnikov.runcat.svg b/src/resources/se.kolesnikov.runcat.svg similarity index 100% rename from assets/se.kolesnikov.runcat.svg rename to src/resources/se.kolesnikov.runcat.svg diff --git a/src/resources/ui/extension.ui b/src/resources/ui/extension.ui new file mode 100644 index 0000000..183a237 --- /dev/null +++ b/src/resources/ui/extension.ui @@ -0,0 +1,23 @@ + + + + + + panel-status-menu-box runcat-menu + + + + system-status-icon runcat-menu__icon + + + + ... + runcat-menu__label + True + True + fill + center + + + + \ No newline at end of file diff --git a/src/resources/ui/preferences.ui b/src/resources/ui/preferences.ui new file mode 100644 index 0000000..50bf420 --- /dev/null +++ b/src/resources/ui/preferences.ui @@ -0,0 +1,119 @@ + + + + + + General + + + + General Preferences + + + + Idle threshold + idle-threshold-scale + + + + center + true + 100px + true + right + horizontal + 0 + idle-threshold + + + + + + + + Displaying items + + + + Character and percentage + Percentage only + Character only + + + + + + + + + + + Reset preferences + Reset RunCat preferences to defaults + + + start + center + + + 20 + 20 + 6 + 6 + + + Reset + + + + + + + + + + + + + GNOME RunCat + Sergei Kolesnikov https://kolesnikov.se/ + Sergei Kolesnikov, Takuto Nakamura + The cat tells you the CPU usage by running speed + © 2020-2022 Sergei Kolesnikov + gpl-3-0 + https://github.com/win0err/gnome-runcat + Visit RunCat's GitHub page + False + True + True + True + + + +
+ + Visit Homepage + prefs.homepage + + + About RunCat + prefs.about + +
+
+ + + main-menu + open-menu-symbolic + + + + + 0 + 100 + 1 + +
\ No newline at end of file diff --git a/src/schemas/org.gnome.shell.extensions.runcat.gschema.xml b/src/schemas/org.gnome.shell.extensions.runcat.gschema.xml index bddcd97..e15598f 100644 --- a/src/schemas/org.gnome.shell.extensions.runcat.gschema.xml +++ b/src/schemas/org.gnome.shell.extensions.runcat.gschema.xml @@ -1,27 +1,26 @@ + + + + + + - + 0 - Sleeping Threshold + Idle threshold - - false - Hide Runner + + 'character-and-percentage' + Displaying items - - - false - Hide Percentage - - - diff --git a/src/settings.js b/src/settings.js deleted file mode 100644 index 07ce4df..0000000 --- a/src/settings.js +++ /dev/null @@ -1,109 +0,0 @@ -/* eslint-disable max-classes-per-file */ -const { Gio } = imports.gi; -const GioSSS = Gio.SettingsSchemaSource; -const ExtensionUtils = imports.misc.extensionUtils; -const Extension = ExtensionUtils.getCurrentExtension(); - -const SCHEMA_PATH = 'org.gnome.shell.extensions.runcat'; - -const valueTypes = { - INTEGER: 'int', - BOOLEAN: 'boolean', - STRING: 'string', - DOUBLE: 'double', -}; - -// eslint-disable-next-line -var Settings = class Settings { - constructor(schemaPath = SCHEMA_PATH) { - const schemaDir = Extension.dir.get_child('schemas'); - - let schemaSource = GioSSS.get_default(); - if (schemaDir.query_exists(null)) { - schemaSource = GioSSS.new_from_directory( - schemaDir.get_path(), - schemaSource, - false, - ); - } - - const schemaObj = schemaSource.lookup(schemaPath, true); - if (!schemaObj) { - throw new Error(`Schema ${schemaPath} could not be found for extension ${Extension.metadata.uuid}`); - } - - this._gioSettings = new Gio.Settings({ settings_schema: schemaObj }); - } - - get sleepingThreshold() { - if (!this._sleepingThreshold) { - // eslint-disable-next-line no-use-before-define - this._sleepingThreshold = new Value( - this._gioSettings, - 'sleeping-threshold', - valueTypes.INTEGER, - ); - } - - return this._sleepingThreshold; - } - - get hideRunner() { - if (!this._hideRunner) { - // eslint-disable-next-line no-use-before-define - this._hideRunner = new Value( - this._gioSettings, - 'hide-runner', - valueTypes.BOOLEAN, - ); - } - - return this._hideRunner; - } - - get hidePercentage() { - if (!this._hidePercentage) { - // eslint-disable-next-line no-use-before-define - this._hidePercentage = new Value( - this._gioSettings, - 'hide-percentage', - valueTypes.BOOLEAN, - ); - } - - return this._hidePercentage; - } -}; - -class Value { - constructor(gioSettings, key, type) { - this._gioSettings = gioSettings; - this._key = key; - this._type = type; - this._connectedCallbacks = []; - } - - set(v) { - return this._gioSettings[`set_${this._type}`](this._key, v); - } - - get() { - return this._gioSettings[`get_${this._type}`](this._key); - } - - addListener(fn) { - const id = this._gioSettings.connect(`changed::${this._key}`, fn); - this._connectedCallbacks = [...this._connectedCallbacks, id]; - return id; - } - - removeListener(id) { - this._gioSettings.disconnect(id); - this._connectedCallbacks = this._connectedCallbacks.filter(item => item !== id); - } - - removeAllListeners() { - this._connectedCallbacks.forEach(id => this._gioSettings.disconnect(id)); - this._connectedCallbacks = []; - } -} \ No newline at end of file diff --git a/src/timer.js b/src/timer.js deleted file mode 100644 index 0e43ca4..0000000 --- a/src/timer.js +++ /dev/null @@ -1,64 +0,0 @@ -const Main = imports.ui.main; -const Mainloop = imports.mainloop; - -const MAX_INTERVAL = 0xFFFFFFFF; - -// eslint-disable-next-line -var Timer = class Timer { - constructor(fn, interval = 1000, autostart = true) { - this.callback = fn; - this._interval = interval; - this.isStarted = false; - this.timeout = null; - - if (autostart) { - this.start(); - } - } - - get interval() { - return this._interval; - } - - set interval(newInterval) { - if (newInterval < 0 || newInterval > MAX_INTERVAL || Number.isNaN(newInterval)) { - throw new RangeError(`Interval ${newInterval} is out of range`); - } - - this._interval = newInterval; - } - - start() { - this.isStarted = true; - this._tick(); - } - - stop() { - this._clearTimeout(); - this.isStarted = false; - } - - _tick() { - this._clearTimeout(); - - const shouldTick = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter; - if (shouldTick) { - this.callback(); - } - - this._addTimeout(); - } - - _addTimeout() { - if (this.isStarted) { - this.timeout = Mainloop.timeout_add(this.interval, () => this._tick()); - } - } - - _clearTimeout() { - if (this.timeout) { - Mainloop.source_remove(this.timeout); - this.timeout = null; - } - } -}; \ No newline at end of file diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..1384877 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,31 @@ +// eslint-disable-next-line no-unused-vars, no-var +var readFile = gioFile => new Promise((resolve, reject) => { + gioFile.load_contents_async( + null, + (f, result) => { + try { + const contents = f.load_contents_finish(result)[1]; + const decodedContents = new TextDecoder('utf-8').decode(contents); + + resolve(decodedContents); + } catch (e) { + reject(e); + } + }, + ); +}); + +// modified version of desktop cube's helper +// https://github.com/Schneegans/Desktop-Cube/blob/main/prefs.js#L238 +// eslint-disable-next-line no-unused-vars, no-var +var findWidgetByType = (parent, type) => { + // eslint-disable-next-line no-restricted-syntax + for (const child of parent) { + if (child instanceof type) return child; + + const match = findWidgetByType(child, type); + if (match) return match; + } + + return null; +}; \ No newline at end of file