Cómo escribir una extensión para GNOME Shell: modo No molestar

Todo comenzó con el cambio a una nueva versión de una distribución de Linux, y allí, el infame Shell de GNOME (GH para abreviar), en Javascript. Bueno, está bien, en JS, así que en JS, funciona, y está bien.


Al mismo tiempo, el ritmo de mi trabajo siempre ha exigido encontrar un anuncio publicitario normal, en lugar de las toneladas de megabytes de la pestaña outlook.office.com en el navegador. Y ahora descubrí que, en nuestro tiempo, hay varios candidatos casi excelentes, un problema: el remitente comenzó a recibirme notificaciones de nuevas letras, sonido e inscripciones emergentes.


Que hacer La decisión de escribir la extensión No molestar no vino de inmediato, realmente no quería escribir una bicicleta y / o quedarme atrapado en el desarrollo / código / toneladas de errores, pero decidí, y ahora quiero compartir mi experiencia con Habr. 1



Requisitos técnicos


Quiero tener uno grande botón para desactivar las notificaciones y los sonidos durante el tiempo que elija: 20 minutos, 40 minutos, 1 hora, 2 horas, 4, 8 y 24 horas. 2 Sí, el tiempo como en Slack.


En las extensiones de extensiones.gnome.org había una extensión "No molestar botón", que sirvió como modelo para escribir su extensión No molestar tiempo .


Resultado final: No molestar tiempo


No molestar el tiempo


Instalar desde extensiones.gnome.org .
Fuentes en github : poner estrellas, tenedor, ofrecer mejoras.


Cómo instalar la extensión GH: instrucciones


  1. Instale el paquete chrome-gnome-shell , un conector de navegador, usando Ubuntu como ejemplo:
    sudo apt install chrome-gnome-shell
  2. Usando el enlace, instale la extensión del navegador:
    • siga el enlace Haga clic aquí para instalar la extensión del navegador
    • en Ubuntu 18.04, funcionó para mí en el navegador Chrome / Chromium, en Fedora 28/29, tanto en Firefox como en Chromium
  3. Estamos buscando la extensión deseada en la lista https://extensions.gnome.org : habilitar, deshabilitar, cambiar la configuración de la extensión.
  4. BENEFICIOS!

Inicio


Crea una extensión desde cero:


 $ 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|$' 

El archivo extension.js en el directorio correspondiente es el punto de entrada en nuestra aplicación, en una versión mínima se ve así:


 function enable() {} //   ;    function disable() {} // --||-- ;     enable() 

Primer código


Primero, queremos agregar un botón al Status Menu esquina superior derecha, como en la captura de pantalla anterior.


Entonces, ¿por dónde empezar? Oh, comencemos con la documentación. Tenemos documentación oficial, todas las cosas. Pero no, la documentación oficial es muy pequeña y fragmentada, pero gracias a julio641742 y su documentación no oficial, obtenemos lo que necesitamos:


  // 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); 

Este código crea el objeto clave dndButton de la clase dndButton : este es un botón especialmente diseñado para el panel del menú de estado. Y lo insertamos en este panel usando la función Main.panel.addToStatusArea (). 3


Inserte elementos de menú con controladores atornillados a ellos, por ejemplo:


  let menuItem = new PopupMenu.PopupMenuItem("hello, world!"); menuItem.connect("activate", (menuItem, event) => { log("hello, world!"); }); dndButton.menu.addMenuItem(menuItem); 

¡Gracias, julio641742, por la documentación! Enlace:
https://github.com/julio641742/gnome-shell-extension-reference


El código de trabajo final está aquí .


Funciones de GNOME Shell y Javascript


Afuera está a finales de 2018, y Node.js / V8 es la herramienta principal para ejecutar código Javascript. Todo el desarrollo web moderno se basa en un "nodo".


Pero GNOME Shell y toda la infraestructura a su alrededor utilizan un motor Javascript diferente, Mozilla's SpiderMonkey, y esto trae muchas diferencias importantes en el rendimiento.


Importar módulos


A diferencia de Node.js, no hay require () aquí, y tampoco hay importación de ES6 elegante. En cambio, hay un objeto especial de importación, el acceso a los atributos de los cuales conduce a la carga del módulo:


  //const PanelMenu = require("ui/panelMenu"); const PanelMenu = imports.ui.panelMenu; 

En este caso, descargamos el módulo js / ui / panelMenu.js de la biblioteca del paquete GNOME Shell, que implementa la funcionalidad de un botón con un menú emergente.


Sí, todos los botones en el panel de un escritorio moderno de Linux que usa GNOME están basados ​​en panelMenu.js. Incluyendo: el mismo botón derecho con indicadores de batería, Wi-Fi, volumen de sonido; cambio de idioma de entrada en-ru.


A continuación, hay un atributo especial imports.searchPath : esta es una lista de rutas (líneas) donde se buscarán nuestros módulos JS. Por ejemplo, asignamos una función de temporizador a un módulo timeUtils.js separado y lo colocamos cerca del punto de entrada de nuestra extensión, extension.js. Importe timeUtils.js de la siguiente manera:


 //     , -  ~/.local/share/gnome-shell/extensions/<your-extension>/ const Me = imports.misc.extensionUtils.getCurrentExtension(); //       imports.searchPath.unshift(Me.path); //   const timeUtils = imports.timeUtils; 

Registro, depuración de Javascript


Bueno, como no tenemos Node.js, tenemos nuestro propio registro. En lugar de console.log (), hay varias funciones de registro disponibles en el código, consulte gjs /../ global.cpp, static_funcs:


  • "log" = g_message ("JS LOG:" + mensaje) - inicio de sesión en stderr, ejemplo:

 $ cat helloWorld.js log("hello, world"); $ gjs helloWorld.js Gjs-Message: 17:20:21.048: JS LOG: hello, world 

  • "logError": registra la pila de excepciones:
    • el primer argumento requerido es una excepción, luego una coma separa lo que quieres
    • ejemplo, si necesita imprimir la pila en el lugar correcto:

 try { throw new Error('bum!'); } catch(e) { logError(e, "what a fuck"); } 

y esto dibujará en estilo stderr:


 (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); - solo texto + "\ n" en stdout, sin prefijos y colores, a diferencia de log ()
  • "printerr" = g_printerr ("% s \ n", txt): la diferencia con respecto a la impresión es que en stderr

Pero no hay un depurador para SpiderMonkey listo para usar (no fue en vano que escribí minuciosamente sobre todas las herramientas disponibles para iniciar sesión, ¡úselas!). Si lo desea, puede probar JSRDbg: uno , dos .


¿Hay vida para el código JS fuera de GNOME Shell?


Hay ¡Las aplicaciones con todas las funciones, incluida una interfaz gráfica de usuario (GUI), se pueden escribir en Javascript! Debe ejecutarlos utilizando el binar gjs, el iniciador de código JS-GTK, un ejemplo de creación de una ventana 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 

Arriba, mencioné dividir el código en módulos y cargarlos desde Javascript. Surge la pregunta, pero ¿cómo determinar en el propio módulo si se inicia como módulo "principal" o si se carga desde otro módulo?


Python tiene una construcción auténtica:


 if __name__ == "__main__": main() 

En Node.js, de manera similar:


 if (require.main === module) { main(); } 

No encontré una respuesta oficial a esta pregunta para Gjs / GH, pero se me ocurrió una técnica que me apresuro a compartir con el lector (¿por qué alguien leyó "dosyudova"? ¡Respeto!).


Entonces, el truco complicado se basa en el análisis de la pila de llamadas actual, si consta de 2 o más líneas, entonces no estamos en el módulo main ():


 if ( new Error().stack.split(/\r\n|\r|\n/g).filter(line => line.length > 0) .length == 1 ) { main(); } 

Limpieza


Cada extensión de GNOME Shell tiene acceso a todos los objetos en todo el GNOME Shell. Por ejemplo, para mostrar el número de notificaciones aún no leídas, llegamos al contenedor con ellas en el Notification Area , ubicada en el centro arriba, número 4 en la imagen (haga clic en la inscripción con la hora actual, se puede hacer clic en la vida real, no aquí):


No molestar el tiempo


  let unseenlist = Main.panel.statusArea.dateMenu._messageList._notificationSection._list; 

Puede averiguar cuántas notificaciones no leídas se suscriben a eventos de agregar y eliminar notificaciones:


 let number = unseenlist.get_n_children(); unseenlist.connect("actor-added", () => { log("added!"); }); unseenlist.connect("actor-removed", () => { log("removed!"); }); 

Esto está bien, pero el usuario a veces puede decidir que ya no necesita la extensión X y hace clic en el botón para deshabilitar la extensión. Para una extensión, esto es equivalente a llamar a la función disable (), y se debe hacer todo lo posible para que la extensión desactivada no rompa el GH en funcionamiento:


 function disable() { dndButton.destroy(); } 

En este caso, además de eliminar el botón en sí, debe darse de baja de los eventos "actor agregado" / "actor eliminado", por ejemplo:


 var signal = unseenlist.connect("actor-added", () => { log("added!"); }); function disable() { dndButton.destroy(); unseenlist.disconnect(signal); } 

Si esto no se hace, se continuará llamando al código de los controladores en el evento correspondiente, intente actualizar el estado del botón de menú que aún no existe y ... GNOME Shell comenzará a fallar. Bueno, sí, lo haremos, los usuarios jurarán, las piedras volarán hacia los desarrolladores de GNOME Shell y GNOME en general. La imagen real, Th.


Entonces GNOME Shell / Gjs es una simbiosis de dos sistemas, Glib / GTK y Javascript, y tienen un enfoque diferente para la gestión de recursos. Glib / GTK requiere la liberación explícita de sus recursos (botones, temporizadores, etc.). Si el objeto fue creado por el motor Javascript, entonces actuamos como de costumbre (no publiques nada).


Como resultado, tan pronto como nuestra extensión esté lista y no "fluya", puede publicarla con seguridad en https://extensions.gnome.org .


GnomeSession.PresenceStatus.BUSY y modo DBus.


Si no lo ha olvidado, estamos haciendo la extensión "No molestar", que desactiva la visualización de notificaciones al usuario.


GNOME ya tiene una bandera responsable de este estado. Después del inicio de sesión del usuario, se crea el proceso gnome-session, en el que se encuentra este indicador: este es el atributo GsmPresencePrivate.status, consulte las fuentes de gnome-session, gnome-session / gsm -ence.c. Tenemos acceso a este indicador a través de la interfaz DBus (como la comunicación entre procesos).


No solo nosotros, sino que GH necesita información sobre esta bandera para no mostrar notificaciones. Esto es bastante fácil de encontrar en la fuente de GH:


 this._presence = new GnomeSession.Presence((proxy, error) => { this._onStatusChanged(proxy.status); }); ... this._presence.connectSignal('StatusChanged', (proxy, senderName, [status]) => { this._onStatusChanged(status); }); 

En este caso, el método _onStatusChanged es un controlador que responde a un cambio de estado. Copiamos este código a nosotros mismos, lo adaptamos: descubrimos 4 notificaciones, hubo un sonido.


Silenciar / activar


La mayoría de los escritorios modernos de Linux están controlados por PulseAudio, trabajo notorio autoría del programa del famoso poeta Lennart. Hasta ahora, no había llegado a escribir el código de PulseAudio, y me alegré de tener la oportunidad de comprender PulseAudio en algún nivel.


Como resultado, resultó que para mute / unmute, una utilidad pactl es pactl , o más bien tres comandos basados ​​en ella:


  • "información de pactl": descubra el default sink : qué salida de sonido, si hay varias, es el sonido predeterminado
  • "sumideros de la lista de pactl": descubra el estado de silencio / silencio del dispositivo correspondiente
  • "pactl set-sink-mute% (defaultSink) s% (isMute) s": para silenciar / activar el silencio

Por lo tanto, nuestra tarea es ejecutar comandos / procesos, leer su salida estándar y buscar los valores deseados de forma regular. En resumen, una tarea estándar.


En GNOME, la biblioteca central de glib es responsable de crear procesos, y hay una excelente documentación al respecto. Y, por supuesto, ella está en C. Y tenemos a JS. Se sabe que el paquete Gjs creó una capa inteligente e "intuitiva" entre el C-API y Javascript. Pero aún comprende que necesita ejemplos y que no puede hacerlo sin buscar en Google.


Como resultado, gracias a la excelente esencia, obtenemos un código de trabajo:


 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 } 

Guardar ajustes en el registro


No, por supuesto, no hay registro en Linux. Aquí no eres Windows. Hay uno mejor, llamado GSettings (esta es la API), varias opciones de implementación están ocultas detrás, por defecto GNOME usa Dconf. Así es como se ve el marco de la GUI:


dconf-editor


- ¿Qué es mejor que almacenar configuraciones en archivos de texto sin formato? - Pregunte a los viejos y barbudos usuarios de Linux. La característica principal de GSettings es que puede suscribirse fácilmente a los cambios en la configuración, por ejemplo:


 const Gio = imports.gi.Gio; settings = new Gio.Settings({ settings_schema: schemaObj }); settings.connect("changed::mute-audio", function() { log("I see, you changed it!"); }); 

La única configuración hasta ahora en nuestro "No molestar" es la opción "silenciar audio", que permite al usuario apagar el sonido o no durante la "hora de silencio" a petición del usuario.


Y un poco de clásico, GTK GUI


Con el fin de mostrar bellamente al usuario la configuración de nuestra extensión (y no ingresar al registro con patas sucias), GH nos ofrece escribir un código GUI y ponerlo en la función buildPrefsWidget () del archivo prefs.js. En este caso, frente a nuestra extensión en la lista de "Extensiones instaladas", aquí veremos un botón adicional "Configurar esta extensión", haciendo clic en el que aparecerá nuestra belleza.


Creemos una pestaña Acerca de separada, porque se sabe que sin Ebout, lo siento, el programa no está completo.


En términos generales, para construir una interfaz gráfica clásica, GTK tiene una amplia gama de bloques de construcción, , de los cuales puede verificar la cantidad y calidad aquí .


Utilizaremos solo algunos de ellos:


  • Gtk.Notebook es una pestaña, como en un navegador
  • Gtk.VBox es un contenedor para estructurar verticalmente una lista de widgets
  • Gtk.Label es un elemento básico, una inscripción, con la capacidad de formatear 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; } 

Captura de pantalla final:



Opcional


1. Modos de operación: soporte y operación

El trabajo del programador implica 2 modos en mi caso:
1) en modo de soporte, cuando necesita responder rápidamente a eventos: correo, Slack, Skype y más
2) en el modo operativo, cuando es vital reducir las notificaciones durante al menos 20 minutos, de lo contrario se pierde el foco y la productividad laboral final es insignificante. Para esto, el modo No molestar es útil.


2. Cómo apagar el sonido

Puede parecer que un silencio total, silencio, es demasiado. De hecho, de hecho, idealmente, le gustaría que las llamadas de Slack / Skype se escuchen en el modo No molestar, pero el resto de sonidos (notificaciones reales) no lo son. Pero para esto necesitan ser distinguidos de alguna manera. Por supuesto, puede crear una API sólida específicamente para notificaciones (y esto ya existe), solo que siempre hay un programa / programador que no utiliza dicha funcionalidad. Un ejemplo es el correo de Mailspring: simplemente reproduce sonidos a través de la etiqueta de audio , y no se pueden distinguir de ninguna manera, por ejemplo, del habla en una llamada de Slack.


3. PanelMenu.Button

PanelMenu.Button: este es el botón real en el panel + menú emergente, y puede resolverlo usted mismo y crearlo desde cero, ¡ambos serán apreciados por los chicos de la herida ! Estaba buscando un resultado rápido y, por lo tanto, copié el código de la documentación no oficial.


4. SetStatusRemote ()

Realmente inicie un cambio de modo usando SetStatusRemote ().

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


All Articles