Comment écrire une extension pour GNOME Shell: mode Ne pas déranger

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 .


Résultat final: Ne pas déranger le temps


Ne dérangez pas le temps


Installez depuis extensions.gnome.org .
Sources sur github : mettre des étoiles, fork, proposer des améliorations.


Comment installer l'extension GH: instructions


  1. Installez le package chrome-gnome-shell , un connecteur de navigateur, en utilisant Ubuntu comme exemple:
    sudo apt install chrome-gnome-shell
  2. À 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
  3. Nous recherchons l'extension souhaitée dans la liste https://extensions.gnome.org : activer, désactiver, modifier les paramètres d'extension.
  4. 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' #  gnome-shell Alt+F2, r, Enter #    https://extensions.gnome.org/local/      #   Gnome Shell - , gnome-shell   systemd,   journalctl -f /usr/bin/gnome-shell #    ,       gnome-shell journalctl -f /usr/bin/gnome-shell | grep -E 'dnd|$' 

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() {} //   ;    function disable() {} // --||-- ;     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:


  // 1 -    (1 - , 0 - , 0.5 -  ) // true,     let dndButton = new PanelMenu.Button(1, "DoNotDisturb", false); // `right` -      (left/center/right) Main.panel.addToStatusArea("DoNotDisturbRole", dndButton, 0, "right"); let box = new St.BoxLayout(); dndButton.actor.add_child(box); let icon = new St.Icon({ style_class: "system-status-icon" }); icon.set_gicon(Gio.icon_new_for_string("/tmp/bell_normal.svg")); box.add(icon); 

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):


Ne dérangez pas le temps


  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); // res = true/false, /   // status = int,    // out/err = ,  stdout/stderr  let [res, out, err, status] = resList; if (res != true || status != 0) { print("not ok!"); } else { // do something useful } 

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:


dconf-editor


- 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() { //    Gtk.Notebook    GUI let notebook = new Gtk.Notebook(); ... //  About,    VBox c   10 , //   margin/padding   let aboutBox = new Gtk.VBox({ border_width: 10 }); //     About notebook.append_page( aboutBox, new Gtk.Label({label: "About"}), ); //        , //      ,    (expand) aboutBox.pack_start( new Gtk.Label({ label: "<b>Do Not Disturb Time</b>", use_markup: true, }), true, // expand true, // fill 0, ); ... notebook.show_all(); return notebook; } 

Capture d'écran finale:



En option


1. Modes de fonctionnement: support et fonctionnement

Le 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 son

Il 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.Button

PanelMenu.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 ().

Source: https://habr.com/ru/post/fr428187/


All Articles