Como escrever uma extensão para o Shell do GNOME: Modo Não perturbe

Tudo começou com a mudança para uma nova versão de uma distribuição Linux, e lá - o famoso GNOME Shell (GH, abreviado), em Javascript. Bem, ok, no JS e no JS, funciona - e tudo bem.


Ao mesmo tempo, o ritmo do meu trabalho há muito tempo exige encontrar uma mala direta, em vez das toneladas de megabytes de travagem e consumo outlook.office.com guia outlook.office.com no navegador. E agora descobri que, em nosso tempo, existem vários candidatos quase excelentes, um problema - a mala direta começou a me receber notificações de novas cartas - e som e inscrições pop-up.


O que fazer A decisão de escrever a extensão Não perturbe não veio imediatamente, eu realmente não queria escrever uma bicicleta e / ou ficar atolado no desenvolvimento / código / toneladas de erros, mas decidi e agora quero compartilhar minha experiência com Habr. 1



Requisitos técnicos


Eu quero ter um grande para desativar as notificações e sons para o horário de sua escolha: 20 minutos, 40 minutos, 1 hora, 2 horas, 4, 8 e 24 horas. 2 Sim, cronometrando como no Slack.


Nas extensões de extensions.gnome.org, havia uma extensão "Não perturbe o botão", que serviu de modelo para escrever sua extensão Não perturbe o tempo .


Resultado final: não perturbe o tempo


Não perturbe o tempo


Instale a partir de extensions.gnome.org .
Fontes no github : coloque estrelas, garfo, ofereça melhorias.


Como instalar a extensão GH: instruções


  1. Instale o pacote chrome-gnome-shell , um conector do navegador, usando o Ubuntu como exemplo:
    sudo apt install chrome-gnome-shell
  2. Usando o link, instale a extensão do navegador:
    • siga o link Clique aqui para instalar a extensão do navegador
    • no Ubuntu 18.04, funcionou para mim no navegador Chrome / Chromium, no Fedora 28/29 - no Firefox e no Chromium
  3. Estamos procurando a extensão desejada na lista https://extensions.gnome.org : ativar, desativar, alterar as configurações de extensão.
  4. LUCRO!

Iniciar


Crie uma extensão do zero:


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

O arquivo extension.js no diretório correspondente é o ponto de entrada em nosso aplicativo, em uma versão mínima semelhante a esta:


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

Primeiro código


Primeiro, queremos adicionar um botão ao Status Menu canto superior direito, como na imagem acima.


Então por onde começar? Oh, vamos começar com a documentação. Temos documentação oficial, tudo. Mas não, a documentação oficial é muito pequena e fragmentada, mas graças a julio641742 e sua documentação não oficial, conseguimos o que precisamos:


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

Esse código cria o objeto de chave dndButton classe dndButton - este é um botão especialmente projetado para o painel Menu Status. E nós o inserimos neste painel usando a função Main.panel.addToStatusArea (). 3


Insira itens de menu com manipuladores aparafusados ​​a eles, exemplo:


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

Obrigado, julio641742, pela documentação! Link:
https://github.com/julio641742/gnome-shell-extension-reference


O código final de trabalho está aqui .


Recursos de shell e Javascript do GNOME


Fora é o final de 2018 e o Node.js / V8 é a principal ferramenta para executar o código Javascript. Todo o desenvolvimento web moderno se baseia em um "nó".


Mas o Shell do GNOME e toda a infraestrutura ao seu redor usam um mecanismo Javascript diferente, o SpiderMonkey da Mozilla, e isso traz muitas diferenças importantes no desempenho.


Módulos de importação


Diferentemente do Node.js, não há require () aqui e também nenhuma importação ES6. Em vez disso, existe um objeto de importação especial, acessando os atributos que levam ao carregamento do módulo:


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

Nesse caso, baixamos o módulo js / ui / panelMenu.js da biblioteca de pacotes GNOME Shell, que implementa a funcionalidade de um botão com um menu pop-up.


Sim, todos os botões no painel de uma área de trabalho moderna do Linux usando o GNOME são baseados no panelMenu.js. Incluindo: o mesmo botão direito com indicadores de bateria, Wi-fi, volume de som; interruptor de idioma de entrada en-ru.


Em seguida, existe um atributo especial imports.searchPath - esta é uma lista de caminhos (linhas) nas quais nossos módulos JS serão pesquisados. Por exemplo, alocamos uma função de timer para um módulo timeUtils.js separado e a colocamos perto do ponto de entrada da nossa extensão extension.js. Importe timeUtils.js da seguinte maneira:


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

Registrando, Depurando Javascript


Bem, como não temos o Node.js, temos nosso próprio log. Em vez de console.log (), várias funções de registro estão disponíveis no código, consulte gjs /../ global.cpp, static_funcs:


  • "log" = g_message ("JS LOG:" + message) - efetuando login no stderr, exemplo:

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

  • "logError" - registra a pilha de exceções:
    • o primeiro argumento necessário é uma exceção e, em seguida, uma vírgula separa o que você deseja
    • Por exemplo, se você precisar imprimir a pilha no lugar certo:

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

e isso vai chamar stderr em grande estilo:


 (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); - somente texto + "\ n" no stdout, sem prefixos e cores, diferente do log ()
  • "printerr" = g_printerr ("% s \ n", txt) - a diferença da impressão é a do stderr

Mas não há depurador para o SpiderMonkey pronto para uso (não foi em vão que eu escrevi meticulosamente acima de todas as ferramentas disponíveis para registro, use-o!). Se desejar, você pode tentar JSRDbg: um , dois .


Existe vida para o código JS fora do GNOME Shell?


Existe. Aplicativos completos, incluindo uma interface gráfica do usuário (GUI), podem ser escritos em Javascript! Você precisa executá-los usando o binar gjs, o iniciador de código JS-GTK, um exemplo de criação de uma janela da 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 

Mencionei acima a divisão do código em módulos e o carregamento do Javascript. A questão surge, mas como é o próprio módulo para determinar se é lançado como um módulo "principal" ou carregado a partir de outro módulo?


Python tem uma construção autêntica:


 if __name__ == "__main__": main() 

No Node.js - da mesma forma:


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

Não encontrei uma resposta oficial para esta pergunta para Gjs / GH, mas criei uma técnica que me apressei a compartilhar com o leitor (por que alguém leu “dosyudova”? Respeito!).


Portanto, o truque complicado é baseado na análise da pilha de chamadas atual, se consiste em 2 ou mais linhas, então não estamos no módulo main ():


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

Serviço de limpeza


Cada extensão do Shell do GNOME tem acesso a todos os objetos em todo o Shell do GNOME. Por exemplo, para exibir o número de notificações ainda não lidas, chegamos ao contêiner na Notification Area , localizada no centro acima, número 4 na imagem (clique na inscrição com a hora atual, é clicável na vida real, não aqui):


Não perturbe o tempo


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

Você pode descobrir quantas notificações não lidas se inscreve em eventos de adição e exclusão de notificações:


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

Isso é bom, mas às vezes o usuário pode decidir que não precisa mais da extensão X e clicar no botão para desativar a extensão. Para uma extensão, isso equivale a chamar a função disable (), e todos os esforços devem ser feitos para que a extensão desativada não quebre o GH de trabalho:


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

Nesse caso, além de excluir o botão em si, é necessário cancelar a inscrição nos eventos "ator adicionado" / "ator removido", por exemplo:


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

Se isso não for feito, o código dos manipuladores continuará sendo chamado no evento correspondente, tente atualizar o estado do botão de menu que ainda não existe e ... O GNOME Shell começará a falhar. Bem, sim, faremos isso, os usuários jurarão, as pedras voarão para os desenvolvedores do GNOME Shell e o GNOME em geral. A imagem real, Th.


Portanto, o GNOME Shell / Gjs é uma simbiose de dois sistemas, Glib / GTK e Javascript, e eles têm uma abordagem diferente para o gerenciamento de recursos. Glib / GTK requer a liberação explícita de seus recursos (botões, temporizadores, etc.). Se o objeto foi criado pelo mecanismo Javascript, agimos como de costume (não libere nada).


Como resultado, assim que nossa extensão estiver pronta e não "fluir", você poderá publicá-la com segurança em https://extensions.gnome.org .


Modo GnomeSession.PresenceStatus.BUSY e DBus.


Se você não se esqueceu, estamos fazendo a extensão "Não perturbe", que desliga a exibição de notificações para o usuário.


O GNOME já possui uma bandeira responsável por esse estado. Após o login do usuário, é criado o processo gnome-session, no qual esse sinalizador está localizado: este é o atributo GsmPresencePrivate.status, consulte as fontes do gnome-session, gnome-session / gsm-presence.c. Temos acesso a esse sinalizador através da interface DBus (como comunicação entre processos).


Não apenas nós, mas o próprio GH precisa de informações nessa bandeira para não mostrar notificações. Isso é fácil de encontrar na fonte GH:


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

Nesse caso, o método _onStatusChanged é um manipulador que responde a uma alteração de estado. Copiamos esse código para nós mesmos, adaptamos - descobrimos quatro notificações, houve um som.


Sem som / sem som


A maioria dos desktops Linux modernos são controlados pelo PulseAudio, trabalho notório autor do programa do notório Lennart Poettering. Até agora, eu não tinha conseguido escrever o código PulseAudio e fiquei feliz por ter a oportunidade de entender o PulseAudio em algum nível.


Como resultado, verificou-se que, para mudo / não mudo, um utilitário pactl é pactl , ou melhor, três comandos baseados nele:


  • "pactl info": encontre o default sink - qual saída de som, se houver várias, é o som padrão
  • "pactl list sinks": encontre o status mudo / não mudo do dispositivo correspondente
  • "pactl set-sink-mute% (defaultSink) s% (isMute) s": para ativar / desativar o som

Portanto, nossa tarefa é executar comandos / processos, ler sua saída stdout e procurar os valores desejados regularmente. Em suma, uma tarefa padrão.


No GNOME, a biblioteca principal glib é responsável pela criação de processos e existe uma excelente documentação . E é claro que ela está em C. E nós temos JS. Sabe-se que o pacote Gjs criou uma camada inteligente e "intuitiva" entre a C-API e o Javascript. Mas você ainda entende que precisa de exemplos e não pode ficar sem pesquisar no Google.


Como resultado, graças à excelente essência, obtemos o código de trabalho:


 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 } 

Salvando configurações no registro


Obviamente, não há registro no Linux. Aqui você não é o Windows. Existe uma melhor, chamada GSettings (esta é a API), várias opções de implementação estão ocultas por trás dela, por padrão o GNOME usa o Dconf. É assim que a estrutura da GUI se parece:


dconf-editor


- O que é melhor do que armazenar configurações em arquivos de texto sem formatação? - Pergunte aos usuários antigos e barbudos do Linux. O principal recurso do GSettings é que você pode assinar facilmente alterações nas configurações, por exemplo:


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

A única configuração até o momento em "Não perturbe" é a opção "áudio mudo", que permite ao usuário desligar ou não o som durante a "hora silenciosa" a pedido do usuário.


E um pouco clássico, GTK GUI


Para mostrar lindamente ao usuário as configurações de nossa extensão (e não entrar no registro com patas sujas), o GH nos oferece a gravação de um código da GUI e a inserção na função buildPrefsWidget () do arquivo prefs.js. Nesse caso, em frente à nossa extensão na lista de "Extensões instaladas" aqui , veremos um botão adicional "Configurar esta extensão", clicando no qual a nossa beleza aparecerá.


Vamos criar uma guia Sobre separada, porque sabe-se que sem o Ebout, desculpe, o programa não está completo.


De um modo geral, para criar uma interface gráfica clássica, o GTK possui toda uma gama de componentes básicos, , os quais você pode conferir a quantidade e a qualidade aqui .


Usaremos apenas alguns deles:


  • Gtk.Notebook é uma guia, como em um navegador
  • Gtk.VBox é um contêiner para estruturar verticalmente uma lista de widgets
  • Gtk.Label é um elemento básico, uma inscrição, com a capacidade de formatar 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 tela final:



Opcional


1. Modos de operação: suporte e operação

O trabalho do programador envolve 2 modos no meu caso:
1) no modo de suporte, quando você precisar responder rapidamente a eventos - correio, Slack, Skype e mais
2) no modo operacional, quando é vital reduzir as notificações por pelo menos 20 minutos, caso contrário, o foco é perdido e a produtividade final do trabalho é insignificante. Para isso, o modo Não perturbe é útil.


2. Como desligar o som

Pode parecer que um mudo completo, mudo, é demais. De fato, idealmente, você gostaria que o Slack / Skype fosse ouvido no modo Não perturbe, mas o restante dos sons (notificações reais) não é. Mas para isso eles precisam ser distinguidos de alguma forma. Obviamente, é possível criar uma API de som específica para notificações (e isso já existe), mas sempre existe um programa / programador que não usa essa funcionalidade. Um exemplo é o mailer do Mailspring: ele simplesmente reproduz sons através da tag de audio e eles não podem ser distinguidos de forma alguma, digamos, da fala em uma chamada do Slack.


3. PanelMenu.Button

PanelMenu.Button - este é o botão real no painel + menu pop-up, e você pode descobrir por si mesmo e criá-lo do zero, os dois serão apreciados pelos caras que estão feridos ! Eu estava buscando um resultado rápido e, portanto, copiei o código da documentação não oficial.


4. SetStatusRemote ()

Realmente inicie uma alteração de modo usando SetStatusRemote ().

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


All Articles