Tout a commencé avec le passage à une nouvelle version d'une distribution Linux, et là - le fameux GNOME Shell (GH pour faire court), en Javascript. Bon, ok, sur JS donc sur JS, ça marche - et ok.
Dans le même temps, le rythme de mon travail a longtemps exigé de trouver un expéditeur normal, au lieu des tonnes de mégaoctets de freinage et de mégaoctets de l'onglet outlook.office.com
dans le navigateur. Et maintenant, j'ai trouvé, à notre époque, plusieurs candidats presque excellents, un problème - l'expéditeur a commencé à me recevoir des notifications de nouvelles lettres - et des inscriptions sonores et pop-up.
Que faire La décision d'écrire l'extension Do Not Disturb n'est pas venue tout de suite, je ne voulais vraiment pas écrire un vélo et / ou m'enliser dans le développement / code / tonnes d'erreurs, mais j'ai décidé, et maintenant je veux partager mon expérience avec Habr. 1

Exigences techniques
Je veux avoir un gros bouton pour désactiver les notifications et les sons pour l'heure de votre choix: 20 minutes, 40 minutes, 1 heure, 2 heures, 4, 8 et 24 heures. 2 Ouais, le timing comme dans Slack.
Sur les extensions d'extensions.gnome.org, il y avait une extension "Do Not Disturb Button", qui a servi de modèle pour écrire son extension Do Not Disturb Time .

Installez depuis extensions.gnome.org .
Sources sur github : mettre des étoiles, fork, proposer des améliorations.
Comment installer l'extension GH: instructions
- Installez le package chrome-gnome-shell , un connecteur de navigateur, en utilisant Ubuntu comme exemple:
sudo apt install chrome-gnome-shell
- À l'aide du lien, installez l'extension du navigateur:
- suivez le lien Cliquez ici pour installer l'extension du navigateur
- dans Ubuntu 18.04, cela a fonctionné pour moi dans le navigateur Chrome / Chromium, dans Fedora 28/29 - à la fois dans Firefox et dans Chromium
- Nous recherchons l'extension souhaitée dans la liste https://extensions.gnome.org : activer, désactiver, modifier les paramètres d'extension.
- PROFIT!
Commencer
Créez une extension à partir de zéro:
$ gnome-shell-extension-tool --create-extension Name: Do Not Disturb Time Description: Disables notifications and sound for a period Uuid: dnd@catbo.net Created extension in '~/.local/share/gnome-shell/extensions/dnd@catbo.net'
Le fichier extension.js
dans le répertoire correspondant est le point d'entrée de notre application, dans une version minimale il ressemble à ceci:
function enable() {}
Premier code
Tout d'abord, nous voulons ajouter un bouton au Status Menu
haut à droite, comme dans la capture d'écran ci-dessus.
Alors par où commencer? Oh, commençons par la documentation. Nous avons une documentation officielle, toutes choses. Mais non, la documentation officielle est très petite et fragmentée, mais grâce à julio641742
et Ă sa documentation non officielle, nous obtenons ce dont nous avons besoin:
Ce code crée l'objet clé dndButton
de la classe dndButton
- il s'agit d'un bouton spécialement conçu pour le panneau Menu d'état. Et nous l'insérons dans ce panneau à l'aide de la fonction Main.panel.addToStatusArea (). 3
Insérez des éléments de menu avec des gestionnaires boulonnés, par exemple:
let menuItem = new PopupMenu.PopupMenuItem("hello, world!"); menuItem.connect("activate", (menuItem, event) => { log("hello, world!"); }); dndButton.menu.addMenuItem(menuItem);
Merci julio641742 pour la documentation! Lien:
https://github.com/julio641742/gnome-shell-extension-reference
Le code de travail final est ici .
Fonctions GNOME Shell et Javascript
Outside est la fin de 2018, et Node.js / V8 est le principal outil pour exécuter du code Javascript. Tout développement Web moderne repose sur un «nœud».
Mais le shell GNOME et toute l'infrastructure qui l'entoure utilisent un moteur Javascript différent, le SpiderMonkey de Mozilla, et cela apporte beaucoup de différences de performances importantes.
Importer des modules
Contrairement à Node.js, il n'y a pas de require () ici, et aucune importation ES6 sophistiquée non plus. Au lieu de cela, il existe un objet spécial d'importation, dont l'accès aux attributs conduit au chargement du module:
//const PanelMenu = require("ui/panelMenu"); const PanelMenu = imports.ui.panelMenu;
Dans ce cas, nous avons téléchargé le module js / ui / panelMenu.js à partir de la bibliothèque de packages GNOME Shell, qui implémente la fonctionnalité d'un bouton avec un menu contextuel.
Oui, tous les boutons du panneau d'un bureau Linux moderne utilisant GNOME sont basés sur panelMenu.js. Comprenant: le même bouton droit avec indicateurs de batterie, Wi-Fi, volume sonore; commutateur de langue d'entrée en-ru.
Ensuite, il y a un attribut spécial imports.searchPath
- c'est une liste de chemins (lignes) où nos modules JS seront recherchés. Par exemple, nous avons alloué une fonction de temporisation à un module timeUtils.js distinct et l'avons placée près du point d'entrée de notre extension, extension.js. Importez timeUtils.js comme suit:
// , - ~/.local/share/gnome-shell/extensions/<your-extension>/ const Me = imports.misc.extensionUtils.getCurrentExtension(); // imports.searchPath.unshift(Me.path); // const timeUtils = imports.timeUtils;
Journalisation, débogage Javascript
Eh bien, puisque nous n'avons pas Node.js, nous avons notre propre journalisation. Au lieu de console.log (), plusieurs fonctions de journalisation sont disponibles dans le code, voir gjs /../ global.cpp, static_funcs:
- "log" = g_message ("JS LOG:" + message) - connexion dans stderr, exemple:
$ cat helloWorld.js log("hello, world"); $ gjs helloWorld.js Gjs-Message: 17:20:21.048: JS LOG: hello, world
- "logError" - enregistre la pile d'exceptions:
- le premier argument requis est une exception, puis une virgule sépare ce que vous voulez
- exemple, si vous devez imprimer la pile au bon endroit:
try { throw new Error('bum!'); } catch(e) { logError(e, "what a fuck"); }
et cela attirera stderr avec style:
(gjs:28674): Gjs-WARNING **: 13:39:46.951: JS ERROR: what a fuck: Error: bum! ggg@./gtk.js:5:15 ddd@./gtk.js:12:5 @./gtk.js:15:1
- "print" = g_print ("% s \ n", txt); - uniquement du texte + "\ n" dans stdout, sans préfixe ni coloration, contrairement à log ()
- "printerr" = g_printerr ("% s \ n", txt) - la différence par rapport à l'impression est que dans stderr
Mais il n'y a pas de débogueur pour SpiderMonkey prêt à l'emploi (ce n'est pas en vain que j'ai minutieusement écrit par dessus tous les outils disponibles pour la journalisation, utilisez-le!). Si vous le souhaitez, vous pouvez essayer JSRDbg: un , deux .
Y a-t-il de la vie pour le code JS en dehors de GNOME Shell?
Il y en a. Des applications complètes, y compris une interface utilisateur graphique (GUI), peuvent être écrites en Javascript! Vous devez les exécuter en utilisant le binar gjs, le lanceur de code JS-GTK, un exemple de création d'une fenêtre GUI:
$ which gjs /usr/bin/gjs $ dpkg --search /usr/bin/gjs gjs: /usr/bin/gjs $ cat gtk.js const Gtk = imports.gi.Gtk; Gtk.init(null); let win = new Gtk.Window(); win.connect("delete-event", () => { Gtk.main_quit(); }); win.show_all(); Gtk.main(); $ gjs gtk.js
Ci-dessus, j'ai mentionné la division du code en modules et leur chargement à partir de Javascript. La question se pose, mais comment dans le module lui-même déterminer s'il est lancé en tant que module "principal", ou chargé à partir d'un autre module?
Python a une construction authentique:
if __name__ == "__main__": main()
Dans Node.js - de mĂŞme:
if (require.main === module) { main(); }
Je n'ai pas trouvé de réponse officielle à cette question pour Gjs / GH, mais j'ai trouvé une telle technique que je m'empresse de partager avec le lecteur (pourquoi quelqu'un a-t-il lu «dosyudova»? Respect!).
Donc, l'astuce est basée sur l'analyse de la pile d'appels actuelle, si elle se compose de 2 lignes ou plus, alors nous ne sommes pas dans le module main ():
if ( new Error().stack.split(/\r\n|\r|\n/g).filter(line => line.length > 0) .length == 1 ) { main(); }
Ménage
Chaque extension du shell GNOME a accès à tous les objets de l'ensemble du shell GNOME. Par exemple, pour afficher le nombre de notifications encore non lues, nous arrivons au conteneur avec elles dans la Notification Area
, située au centre ci-dessus, numéro 4 dans l'image (cliquez sur l'inscription avec l'heure actuelle, elle est cliquable dans la vraie vie, pas ici):

let unseenlist = Main.panel.statusArea.dateMenu._messageList._notificationSection._list;
Vous pouvez connaître le nombre de notifications non lues, vous abonner aux événements d'ajout et de suppression de notifications:
let number = unseenlist.get_n_children(); unseenlist.connect("actor-added", () => { log("added!"); }); unseenlist.connect("actor-removed", () => { log("removed!"); });
C'est très bien, mais l'utilisateur peut parfois décider qu'il n'a plus besoin de l'extension X et clique sur le bouton pour désactiver l'extension. Pour une extension, cela équivaut à appeler la fonction disable (), et tous les efforts doivent être faits pour que l'extension désactivée ne casse pas la GH active:
function disable() { dndButton.destroy(); }
Dans ce cas, en plus de supprimer le bouton lui-même, vous devez vous désinscrire des événements "acteur ajouté" / "acteur supprimé", par exemple:
var signal = unseenlist.connect("actor-added", () => { log("added!"); }); function disable() { dndButton.destroy(); unseenlist.disconnect(signal); }
Si cela n'est pas fait, alors le code des gestionnaires continuera à être appelé sur l'événement correspondant, essayez de mettre à jour l'état du bouton de menu qui n'existe pas déjà et ... GNOME Shell commencera à échouer. Eh bien, oui, nous le ferons, les utilisateurs jureront, les pierres voleront aux développeurs de GNOME Shell et GNOME en général. La vraie image, Th.
Donc, GNOME Shell / Gjs est une symbiose de deux systèmes, Glib / GTK et Javascript, et ils ont une approche différente de la gestion des ressources. Glib / GTK nécessite la libération explicite de ses ressources (boutons, minuteries, etc.). Si l'objet a été créé par le moteur Javascript, alors nous agissons comme d'habitude (ne libérons rien).
Par conséquent, dès que notre extension est prête et ne "coule" pas, vous pouvez la publier en toute sécurité sur https://extensions.gnome.org .
Mode GnomeSession.PresenceStatus.BUSY et DBus.
Si vous ne l'avez pas oublié, nous faisons l'extension "Ne pas déranger", qui désactive l'affichage des notifications à l'utilisateur.
GNOME a déjà un drapeau responsable de cet état. Après la connexion de l'utilisateur, le processus gnome-session est créé, dans lequel se trouve cet indicateur: il s'agit de l'attribut GsmPresencePrivate.status, voir les sources de gnome-session, gnome-session / gsm-presence.c. Nous avons accès à cet indicateur via l'interface DBus (telle la communication interprocessus).
Non seulement nous, mais GH lui-mĂŞme a besoin d'informations sur ce drapeau afin de ne pas afficher de notifications. C'est assez facile Ă trouver dans la source GH:
this._presence = new GnomeSession.Presence((proxy, error) => { this._onStatusChanged(proxy.status); }); ... this._presence.connectSignal('StatusChanged', (proxy, senderName, [status]) => { this._onStatusChanged(status); });
Dans ce cas, la méthode _onStatusChanged est un gestionnaire qui répond à un changement d'état. Nous copions ce code pour nous-mêmes, nous l'adaptons - nous avons compris 4 notifications, il y avait un son.
Activer / désactiver le son
La plupart des bureaux Linux modernes sont contrôlés par PulseAudio, travail notoire auteur du programme du célèbre Lennart Poettering. Jusqu'à présent, je n'ai pas réussi à écrire du code PulseAudio, et j'étais heureux d'avoir l'opportunité de comprendre PulseAudio à un certain niveau.
En conséquence, il s'est avéré que pour pactl
/ désactiver le son, un utilitaire pactl
, ou plutôt trois commandes basées sur celui-ci:
- "pactl info": découvrez le récepteur par
default sink
- quelle sortie sonore, s'il y en a plusieurs, est le son par défaut - "pactl list sinks": découvrez l'état muet / unmute du périphérique correspondant
- "pactl set-sink-mute% (defaultSink) s% (isMute) s": pour activer / désactiver le son lui-même
Notre tâche consiste donc à exécuter des commandes / processus, à lire leur sortie standard et à rechercher régulièrement les valeurs souhaitées. Bref, une tâche standard.
Chez GNOME, la bibliothèque de base glib est responsable de la création de processus, et il existe une excellente documentation à ce sujet. Et bien sûr, elle est en C. Et nous avons JS. Il est connu que le package Gjs a créé une couche intelligente et "intuitive" entre la C-API et Javascript. Mais vous comprenez toujours que vous avez besoin d'exemples et vous ne pouvez pas vous passer de googler.
En conséquence, grâce à l'excellent contenu, nous obtenons un code de travail:
let resList = GLib.spawn_command_line_sync(cmd);
Enregistrement des paramètres dans le registre
Non, bien sûr, il n'y a pas de registre sous Linux. Ici, vous n'êtes pas Windows. Il y en a une meilleure, appelée GSettings (c'est l'API), plusieurs options d'implémentation sont cachées derrière, par défaut GNOME utilise Dconf. Voici à quoi ressemble le framework GUI:

- Quoi de mieux que de stocker des paramètres dans des fichiers en texte brut? - Demandez aux utilisateurs Linux vieux et barbus. La principale caractéristique de GSettings est que vous pouvez facilement vous abonner aux modifications des paramètres, par exemple:
const Gio = imports.gi.Gio; settings = new Gio.Settings({ settings_schema: schemaObj }); settings.connect("changed::mute-audio", function() { log("I see, you changed it!"); });
Le seul réglage jusqu'à présent dans notre "Ne pas déranger" est l'option "muet-audio", qui permet à l'utilisateur de couper le son ou non pendant la durée de "l'heure calme" à la demande de l'utilisateur.
Et un peu classique, GTK GUI
Afin de montrer magnifiquement à l'utilisateur les paramètres de notre extension (et de ne pas entrer dans le registre avec des pattes sales), GH nous propose d'écrire un code GUI et de le mettre dans la fonction buildPrefsWidget () du fichier prefs.js. Dans ce cas, en face de notre extension dans la liste des "Extensions installées" ici nous verrons un bouton supplémentaire "Configurer cette extension", en cliquant sur lequel notre beauté apparaîtra.
Créons un onglet À propos séparé, car il est connu que sans Ebout, désolé, le programme n'est pas terminé.
De manière générale, pour construire une interface graphique classique, GTK a toute une gamme de blocs de construction, des
, dont vous pouvez vérifier la quantité et la qualité ici .
Nous n'en utiliserons que quelques-uns:
- Gtk.Notebook est un onglet, comme dans un navigateur
- Gtk.VBox est un conteneur pour structurer verticalement une liste de widgets
- Gtk.Label est un élément de base, une inscription, avec la possibilité de formater HTML
function buildPrefsWidget() {
Capture d'écran finale:

En option
1. Modes de fonctionnement: support et fonctionnementLe travail du programmeur implique 2 modes dans mon cas:
1) en mode support, lorsque vous devez répondre rapidement à des événements - mail, Slack, Skype et plus
2) en mode de fonctionnement, lorsqu'il est essentiel de réduire les notifications pendant au moins 20 minutes, sinon la concentration est perdue et la productivité finale du travail est négligeable. Pour cela, le mode Ne pas déranger est utile.
2. Comment désactiver le sonIl peut sembler qu'un muet complet, muet, c'est trop. En effet, en fait, idéalement, vous aimeriez que Slack / Skype soit entendu en mode Ne pas déranger, mais le reste des sons (notifications réelles) ne le sont pas. Mais pour cela, ils doivent être distingués d'une manière ou d'une autre. Vous pouvez bien sûr créer une API sonore spécifiquement pour les notifications (et cela existe déjà ), seulement il y a toujours un programme / programmeur qui n'utilise pas une telle fonctionnalité. Un exemple est le mailer Mailspring: il lit simplement les sons via la balise audio
, et ils ne peuvent être distingués en aucune façon, par exemple, de la parole dans un appel Slack.
3. PanelMenu.ButtonPanelMenu.Button - c'est le bouton réel dans le menu panneau + pop-up, et vous pouvez le découvrir vous-même et le créer à partir de zéro, les deux seront appréciés par les gars de la plaie ! Je visais un résultat rapide et j'ai donc copié le code de la documentation non officielle.
4. SetStatusRemote ()Initialisez réellement un changement de mode à l'aide de SetStatusRemote ().