Як написати розширення для GNOME Shell: режим «Do Not Disturb»

Почалося все з переїзду на нову версію одного дистрибутива Linux, а там — скандально відомий GNOME Shell (GH для стислості), на Javascript. Ну ок, на JS так на JS, працює — і гаразд.

Одночасно з цим темп моєї роботи давно вже вимагав знайти нормальний поштовик, замість гальмує і жрущей тонни мегабайтів вкладки outlook.office.com у браузері. І ось знайшов, у наш час є кілька майже прекрасних кандидатур, одна біда — поштовик став діставати мене повідомленнями про нові листи — і звуком, і спливаючими написами.

Що робити? Рішення написати розширення “Не турбувати” прийшло не відразу, дуже не хотілося писати велосипед і/або загрузнути в розробці/код/тоннах помилок, але зважився, і ось хочу поділитися з Хабром своїм досвідом.1

 

 

Технічні вимоги

 

Хочеться мати одну велику кнопку, щоб вимкнути попередження і звуки на час за вибором: 20 хвилин, 40 хвилин, 1 год, 2 години, 4, 8 і 24 години.2 Ага, таймінг як в Slack.

На просторах extensions.gnome.org знайшлося розширення “Do Not Disturb Button”, яке послужило моделлю до написання свого розширення Do Not Disturb Time.

 

Кінцевий результат: Do Not Disturb Time

 

 

Встановити з сайту extensions.gnome.org.
Вихідні коди на github: ставимо зірочки, форкаем, пропонуємо поліпшення.

 

Як встановити розширення GH: інструкція

 

  1. Встановлюємо пакет chrome-gnome-shell, коннектор до браузеру, на прикладі Ubuntu:
    sudo apt install chrome-gnome-shell
  2. За посиланням встановлюємо браузерне розширення:
    • переходимо за посиланням Click here to install browser extension
    • в Ubuntu 18.04 у мене запрацювало в браузері Chrome/Chromium, у Fedora 28/29 — Firefox, і в Chromium-е
  3. Шукаємо потрібне розширення списку https://extensions.gnome.org: включаємо, вимикаємо, міняємо налаштування розширення.
  4. PROFIT!

 

Початок

 

Створимо розширення з нуля:

 

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

 

Файл extension.js у відповідній директорії є вхідною точкою в нашому додатку, в мінімальному виконанні він виглядає так:

 

function enable() {} // викликається при включенні; створюємо всі тут
function disable() {} // --||-- вимиканні; видаляємо все створене в enable()

 

Перший код

 

Для початку ми хочемо додати кнопку Status Menu праворуч зверху, як на скріншоті вище.

 

Отже, з чого б почати? О, почнемо з документації. У нас же є офіційна документація, всі справи. А от ні, офіційна документація дуже невелика і розрізнена, однак завдяки julio641742 і його неофіційною документації ми отримуємо те, що потрібно:

 

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

 

Цей код створює ключовий об’єкт dndButton класу PanelMenu.Button — це кнопка, спеціально призначена для панелі Status Menu. І ми її вставляємо в цю панель з допомогою функції Main.panel.addToStatusArea().3

 

Вставляємо пункти меню з прикрученими до них обробниками, приклад:

 

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

 dndButton.menu.addMenuItem(menuItem);

 

Спасибі тобі, julio641742, за документацію! Посилання:
https://github.com/julio641742/gnome-shell-extension-reference

 

Підсумковий працюючий код — за посиланням.

 

Особливості роботи GNOME Shell і Javascript

 

На дворі кінець 2018-го, і Node.js/V8 — основний інструмент для запуску Javascript-коду. Вся сучасна web-розробка тримається на “ноде”.

Читайте також  Регулярні вирази Python від простого до складного. Подробиці, приклади, ілюстрації, вправи

 

Але GNOME Shell і вся інфраструктура навколо нього використовує інший Javascript-движок, SpiderMonkey від Mozilla, і звідси випливає багато важливих відмінностей в роботі.

 

Імпорт модулів

 

На відміну від Node.js тут немає require(), і модного ES6-import-а — теж. Замість цього є спеціальний об’єкт заяву, звернення до атрибутів якого призводить до завантаження модуля:

 

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

 

В даному випадку ми завантажили модуль js/ui/panelMenu.js з бібліотеки GNOME Shell, в якому реалізований функціонал кнопки з спливаючим меню.

 

Так-так, всі кнопки на панелі сучасного десктопа Linux, використовує GNOME, написані на базі panelMenu.js. У тому числі: та сама права кнопка з індикаторами батареї, Wi-fi, гучністю звуку; перемикач мови введення en-ru.

 

Далі, є особливий атрибут imports.searchPath — це список шляхів (рядків), де будуть шукатися наші JS-модулі. Наприклад, ми виділили в окремий модуль timeUtils.js функціонал роботи з таймером і поклали його поруч з вхідною точкою нашого розширення, extension.js. Импортим timeUtils.js наступним чином:

 

// отримуємо шлях до нашого розширення, десь у ~/.local/share/gnome-shell/extensions/<your-extension>/
const Me = imports.misc.extensionUtils.getCurrentExtension();
// вставляємо новий шлях в початок списку
imports.searchPath.unshift(Me.path);
// власне імпорт
const timeUtils = imports.timeUtils;

 

Логування, налагодження Javascript

 

Ну раз у нас не Node.js, то і логування у нас своє. Замість console.log() в коді доступні кілька функцій логування, див. gjs/../global.cpp, static_funcs:

 

  • “log” = g_message(“JS LOG:” + message) — логування у stderr, приклад:

 

$ cat helloWorld.js 
log("hello, world");

$ gjs helloWorld.js 
Gjs-Message: 17:20:21.048: JS LOG: hello, world

 

  • “logError” — логирует стек винятки:
    • перший обов’язковий аргумент — виняток, потім через кому — що хочеш
    • приклад, якщо потрібно роздрукувати стек в потрібному місці:

 

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

 

і це намалює у 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(“%sn”, txt); — тільки текст + “n” stdout, без префіксів і забарвлення, на відміну від log()
  • “printerr” = g_printerr(“%sn”, txt) — відмінність від print в тому, що у stderr

 

А ось налагоджувач для SpiderMonkey з коробки немає (не дарма ж я ретельно виписав вище всі доступні інструменти для логування, користуйтеся!). При бажанні можна спробувати JSRDbg: раз, два.

 

А чи є життя для JS-коду поза GNOME Shell?

 

Є. Повнофункціональні додатки, включаючи графічний інтерфейс (GUI), можна писати на Javascript! Запускати їх потрібно з допомогою бинаря gjs, пускальщика JS-GTK-коду, приклад створення 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 

 

Вище я згадав про розділення коду на модулі і завантаження їх з Javascript. Виникає питання, а як в самому модулі визначити, запущений він як “main”-модуль, або завантажений з іншого модуля?

 

В Python-е є автентична конструкція:

 

if __name__ == "__main__":
 main()

 

В Node.js — аналогічно:

 

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

 

Офіційної відповіді на це питання для Gjs/GH я не знайшов, але придумав такий прийом, яким поспішаю поділитися з читачем (а че, хтось дочитав “досюдова”? респект!).

 

Отже, хитрий прийом заснований на аналізі поточного стека викликів, якщо він складається з 2х і більше рядків — значить ми не в main()-модулі:

 

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

 

Прибирання за собою

 

Кожне розширення GNOME Shell має доступ до всіх об’єктів всього GNOME Shell. Наприклад, щоб відобразити кількість непрочитаних повідомлень ще, доберемося до контейнера з ними в Notification Area, розташованого по центру зверху, номер 4 на картинці (натисніть на напис з поточним часом, вона кликабельна в реалі, не тут):

Читайте також  Інформаційна безпека банківських безготівкових платежів. Частина 1 — Економічні основи

 

 

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

 

Можна дізнатися, скільки їх, непрочитаних повідомлень, підписатися на події додавання та видалення повідомлень:

 

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

 

Це чудово, але користувач, буває, може вирішити, що розширення X йому більше не потрібно, і натисне кнопку вимкнути розширення. Для розширення це рівносильно викликом функції disable(), і треба докласти всіх зусиль, щоб виключене розширення не поламало працює GH:

 

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

 

У цьому випадку, крім того, що видаляємо саму кнопку, потрібно відписатись від подій “actor-added”/”actor-removed”, приклад:

 

var signal = unseenlist.connect("actor-added", () => {
log("added!");
});

function disable() {
dndButton.destroy();
unseenlist.disconnect(signal);
}

 

Якщо цього не зробити, то код обробників буде продовжувати викликатися на відповідної події, намагатися оновлювати стан неіснуючої вже кнопки з менюшкой і… GNOME Shell почне глючити. Ну так, напакостим ми, лаятися будуть користувачі, камені полетять у розробників GNOME GNOME Shell і в цілому. Реальна картина, че.

 

Отже, GNOME Shell/Gjs являє собою симбіоз двох систем, Glib/GTK і Javascript, і у них різний підхід до управління ресурсами. Glib/GTK вимагає явного звільнення своїх ресурсів (кнопок, таймерів та іншого). Якщо об’єкт створений движок Javascript-а, то діємо як завжди (нічого не звільняємо).

 

У результаті, як тільки наше розширення готове, і не “тече”, можна сміливо публікувати його на https://extensions.gnome.org.

 

Режим GnomeSession.PresenceStatus.BUSY і DBus.

 

Якщо ви ще не забули, ми робимо розширення “Do Not Disturb”, яке вимикає показ повідомлень користувачу.

 

У GNOME вже є прапор, який відповідає за цей стан. Після логіну користувача створюється процес gnome-session, в якому цей прапор і знаходиться: це атрибут GsmPresencePrivate.status, див. вихідні gnome-session, gnome-session/gsm-presence.c. Доступ до цього прапору отримуємо через DBus-інтерфейс (таке межпроцессное взаємодія).

 

Не тільки нам, але і самому GH потрібна інформація про це прапор, щоб не показувати попередження. Це досить легко шукається в исходниках GH:

 

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

 

В даному випадку метод _onStatusChanged є обробник, що реагує на зміну стану. Копіюємо цей код до себе, адаптуємо — з повідомленнями разобрались4, залишився звук.

 

Виключення/включення звуку

 

Більшість сучасних Linux-десктопів управляється PulseAudio, відоме поділився програма за авторством відомого Lennart Poettering. Досі у мене не доходили руки пошерстити код PulseAudio, і я був радий цій можливості розібратися в PulseAudio на деякому рівні.

 

У підсумку виявилося, що для mute/unmute достатньо однієї утиліти pactl, а точніше трьох команд на її основі:

 

  • “pactl info”: дізнатися default sink — який звуковий вихід, якщо їх кілька, подається звук за замовчуванням
  • “pactl list sinks”: дізнатися стан mute/unmute відповідного пристрою
  • “pactl set-sink-mute %(defaultSink)s %(isMute)s”: для власне mute/unmute

 

Отже, наше завдання полягає в запуску команд/процесів, читанні їх виводу (stdout і пошуку потрібних значень за регулярці. Коротше, стандартна завдання.

 

У GNOME за створення процесів відповідає базова бібліотека glib, і є відмінна документація по ній. І звичайно вона на C. А у нас JS. Відомо, що пакет Gjs зробив розумну, “інтуїтивно-зрозумілу” прошарок між З-API і Javascript. Але все одно розумієш, що потрібні приклади і без гугления не обійтися.

 

У підсумку, завдяки прекрасному gist-у отримуємо працюючий код:

 

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
}

 

Читайте також  Деякі завдання шкільної математики

Збереження налаштувань в реєстрі

 

Не, звичайно реєстру в Linux-е ні. Тут вам не Windows. Є краще, називається GSettings (це API), за ним ховається кілька варіантів реалізації, за замовчуванням GNOME використовується Dconf. Ось так виглядає GUI-шка для нього:

 

 

— Чим це краще зберігання налаштувань в звичайних текстових файлах? — запитають олдові і бородаті користувачі Linux-а. Основна фішка GSettings в тому, що можна легко підписатися на зміни в налаштуванні, приклад:

 

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

 

Єдина поки налаштування в нашому “Do Not Disturb” — це опція “mute-audio”, яка дозволяє за бажанням користувача вимикати чи ні звук на час “тихої години”.

 

І трохи класики, GUI на GTK

 

Щоб красиво показати користувачеві налаштування нашого розширення (а не лізти брудними лапами в реєстр), GH пропонує нам написати GUI-код і покласти його в функцію buildPrefsWidget() файлу prefs.js. У цьому випадку навпроти нашого розширення списку “Installed Extensions” тут ми побачимо додаткову кнопку “Configure this extension”, після натискання якої наша краса і з’явиться.

 

Давайте створимо окрему вкладку About, адже відомо, що без “Эбаута”, пардон, програма не є повноцінною.

 

Взагалі кажучи, для побудови класичного графічного інтерфейсу у GTK є весь асортимент будівельних блоків, віджетів, зацінити кількість і якість яких можна тут.

 

Ми ж скористаємося лише кількома з них:

 

  • Gtk.Notebook — це вкладки, приблизно як в браузері
  • Gtk.VBox — це контейнер для вертикального структурування списку віджетів
  • Gtk.Label — це базовий елемент, напис, з можливістю 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;
}

 

Підсумковий скріншот:

 

 

Додатково

1. Режими роботи: підтримка і робота

Робота програміста передбачає 2 режиму в моєму випадку:
1) у режимі підтримки, коли потрібно швидко реагувати на події, пошта, Slack, Skype та інше
2) у режимі роботи, коли життєво необхідно вирубати повідомлення хоча б на 20 хвилин, інакше фокус втрачається і підсумкова продуктивність праці — низькою. Саме для цього корисний режим “Не турбувати”.

2. Як вимкнути звук

Може здатися, що повне вимикання звуку, mute, це занадто. Дійсно, адже в ідеалі хочеться, щоб в режимі “Не турбувати” дзвінки Slack/Skype було чути, а от інші звуки (реальних повідомлень) — немає. Але для цього їх потрібно розрізняти. Можна, звичайно, зробити звуковий API спеціально для повідомлень (і таке вже є), тільки от завжди знайдеться програма/програміст, який не задіює такий функціонал. Приклад — поштовик Mailspring: звуки він просто програє через тег audio, і їх ніяк не відрізниш, припустимо, від мовлення в дзвінку Slack-а.

3. PanelMenu.Button

PanelMenu.Button — це власне кнопка в панелі + спливаюче меню, і можна самому розібратися і створити з нуля, і те, і інше, пацани на районі оцінять! У мене ж була націленість на швидкий результат і тому я копипастнул код з неофіційною документацією.

4. SetStatusRemote()

Власне ініціювати зміну режиму потрібно з допомогою SetStatusRemote().

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *