11 Commits
v6 ... v15

7 changed files with 203 additions and 111 deletions

View File

@@ -7,6 +7,10 @@ This GNOME Shell Extension aims to display information to center box. Using DBUS
Installation Installation
------------ ------------
Install it from https://extensions.gnome.org/extension/2826/generic-monitor/
OR
Create a symbolic link from your _.local_ directory and enable extension Create a symbolic link from your _.local_ directory and enable extension
ln -s $PWD/generic-monitor@gnome-shell-extensions/ ~/.local/share/gnome-shell/extensions/ ln -s $PWD/generic-monitor@gnome-shell-extensions/ ~/.local/share/gnome-shell/extensions/
@@ -109,11 +113,11 @@ Signals can be :
Targets : Targets :
* signal : emit a signal to desktop application * signal : Emit a signal to desktop application
* delete : Delete item * delete : Delete item
* open-popup : Open the popup if there is one * open-popup : Open the popup if there is one
* close-popup : Close the popup if there is one * close-popup : Close the popup if there is one
* toggle-popup : Toggle (open/close) the popup if there is one * toggle-popup : Toggle (open/close) the popup if there is one
Signal names emit when action "signal" is specified : Signal names emit when action "signal" is specified :
@@ -142,11 +146,11 @@ Example
You can test it with command line : You can test it with command line :
gdbus call --session --dest org.gnome.Shell --object-path /com/soutade/GenericMonitor --method com.soutade.GenericMonitor.notify '{"group":"new","items":[{"name":"first","on-click":"toggle-popup","text":{"text":"Hello","style":"color:green"},"popup":{"items":[{"picture":{"path":"/tmp/cat2.jpg"}}]}}]}' gdbus call --session --dest org.gnome.Shell --object-path /com/soutade/GenericMonitor --method com.soutade.GenericMonitor.notify '{"group":"new","items":[{"name":"first","on-click":"toggle-popup","text":{"text":"Hello","style":"color:green"},"popup":{"items":[{"picture":{"path":"/tmp/cat.jpg"}}]}}]}'
gdbus call --session --dest org.gnome.Shell --object-path /com/soutade/GenericMonitor --method com.soutade.GenericMonitor.deleteGroups '{"groups":["new"]}' gdbus call --session --dest org.gnome.Shell --object-path /com/soutade/GenericMonitor --method com.soutade.GenericMonitor.deleteGroups '{"groups":["new"]}'
Python examples is available Python examples are available @ https://indefero.soutade.fr/p/genericmonitor/source/tree/master/examples
Development Development

View File

@@ -1,47 +1,47 @@
<interface name="com.soutade.GenericMonitor"> <interface name="com.soutade.GenericMonitor">
<!-- Functions --> <!-- Functions -->
<method name="notify"> <method name="notify">
<arg type="s" direction="in" /> <arg name="parameters" type="s" direction="in" />
</method> </method>
<method name="deleteItems"> <method name="deleteItems">
<arg type="s" direction="in" /> <arg name="items" type="s" direction="in" />
</method> </method>
<method name="deleteGroups"> <method name="deleteGroups">
<arg type="s" direction="in" /> <arg name="groups" type="s" direction="in" />
</method> </method>
<method name="openPopup"> <method name="openPopup">
<arg type="s" direction="in" /> <arg name="popup" type="s" direction="in" />
</method> </method>
<method name="closePopup"> <method name="closePopup">
<arg type="s" direction="in" /> <arg name="popup" type="s" direction="in" />
</method> </method>
<method name="togglePopup"> <method name="togglePopup">
<arg type="s" direction="in" /> <arg name="popup" type="s" direction="in" />
</method> </method>
<!-- Events --> <!-- Events -->
<signal name="onClick"> <signal name="onClick">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<signal name="onRightClick"> <signal name="onRightClick">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<signal name="onDblClick"> <signal name="onDblClick">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<signal name="onRightDblClick"> <signal name="onRightDblClick">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<signal name="onScrollUp"> <signal name="onScrollUp">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<signal name="onScrollDown"> <signal name="onScrollDown">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<signal name="onEnter"> <signal name="onEnter">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<signal name="onLeave"> <signal name="onLeave">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<!-- Activate/Deactivate signals --> <!-- Activate/Deactivate signals -->
<signal name="onActivate"> <signal name="onActivate">

85
examples/gmail.py Normal file
View File

@@ -0,0 +1,85 @@
#
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Setup : https://developers.google.com/gmail/api/quickstart/python
# Need to have credentials.json were you call main script
#
# File imported from https://github.com/googleworkspace/python-samples/blob/main/gmail/quickstart/quickstart.py
#
from __future__ import print_function
import os.path
import json
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
# If modifying these scopes, delete the file token.json.
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']
creds = None
def _initCreds():
global creds
if creds: return
# The file token.json stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists('token.json'):
creds = Credentials.from_authorized_user_file('./token.json', SCOPES)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.json', 'w') as token:
data = {}
data['refresh_token'] = creds.refresh_token
data['client_id'] = creds.client_id
data['client_secret'] = creds.client_secret
data['token_uri'] = creds.token_uri
data['id_token'] = creds.id_token
token.write(json.dumps(data))
def getUnreadMails():
"""
Get number of unread threads (that may contain multiple messages)
"""
_initCreds()
service = build('gmail', 'v1', credentials=creds)
pageToken = ''
threads = set()
while True:
results = service.users().messages().list(userId='me', labelIds=['UNREAD'],\
includeSpamTrash=False, pageToken=pageToken)\
.execute()
if not 'messages' in results.keys(): continue
threads = threads.union(set([k['threadId'] for k in results['messages']]))
# Loop over all result pages (100 results per page by default)
pageToken = results.get('nextPageToken', '')
if not pageToken: break
return len(threads)

View File

@@ -23,11 +23,11 @@ import time
import requests import requests
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
import xml.dom.minidom import xml.dom.minidom
import getpass
from threading import Thread from threading import Thread
from signal import signal, SIGINT from signal import signal, SIGINT
import sys import sys
from genericmonitor import * from genericmonitor import *
from gmail import getUnreadMails
PURPLE_CONV_UPDATE_UNSEEN = 4 PURPLE_CONV_UPDATE_UNSEEN = 4
PURPLE_MESSAGE_SEND = 0 PURPLE_MESSAGE_SEND = 0
@@ -59,31 +59,23 @@ class PidginConversation:
class EventThread(Thread,GenericMonitor): class EventThread(Thread,GenericMonitor):
SLEEP_TIME = 30 SLEEP_TIME = 30
MAIL_ADDRESS='XXX@gmail.com'
def stop(self): def stop(self):
self._stopLoop = True self._stopLoop = True
self.stopMainLoop() self.stopMainLoop()
def _getMail(self): def _getMail(self):
address = "https://mail.google.com/mail/feed/atom"
auth = HTTPBasicAuth(self.MAIL_ADDRESS, self._mail_password)
req = requests.get(address, auth=auth)
text = '' text = ''
style = '' style = ''
if req.status_code == requests.codes.ok: try:
dom = xml.dom.minidom.parseString(req.text) nb_messages = getUnreadMails()
try: if nb_messages == 1:
nb_messages = int(dom.getElementsByTagName('fullcount')[0].firstChild.nodeValue) text = '1 msg'
if nb_messages == 1: elif nb_messages > 1:
text = '1 msg' text = '%d msgs' % (nb_messages)
elif nb_messages > 1: style = 'color:white'
text = '%d msgs' % (nb_messages) except Exception as e:
style = 'color:white' text = str(e)
except Exception as e:
text = str(e)
else:
text = 'Mail error %d' % (req.status_code)
self.mailWidget.setText(text) self.mailWidget.setText(text)
self.mailWidget.setStyle(style) self.mailWidget.setStyle(style)
@@ -103,8 +95,6 @@ class EventThread(Thread,GenericMonitor):
self.add_signal_receiver(self.pidginMessageWrote, 'WroteChatMsg', 'im.pidgin.purple.PurpleInterface') self.add_signal_receiver(self.pidginMessageWrote, 'WroteChatMsg', 'im.pidgin.purple.PurpleInterface')
self.add_signal_receiver(self.pidginConversationUpdated, 'ConversationUpdated', 'im.pidgin.purple.PurpleInterface') self.add_signal_receiver(self.pidginConversationUpdated, 'ConversationUpdated', 'im.pidgin.purple.PurpleInterface')
self._mail_password = getpass.getpass('Enter password for address %s: ' % (self.MAIL_ADDRESS))
self.mailWidget = GenericMonitorTextWidget('') self.mailWidget = GenericMonitorTextWidget('')
mailItem = GenericMonitorItem('mail', [self.mailWidget]) mailItem = GenericMonitorItem('mail', [self.mailWidget])
self.mailGroup = GenericMonitorGroup('Mail', [mailItem]) self.mailGroup = GenericMonitorGroup('Mail', [mailItem])

View File

@@ -43,20 +43,17 @@ class PicturePopup(GenericMonitor):
self.runMainLoop() self.runMainLoop()
def display_next_img(self): def display_next_img(self):
filedata = urllib.request.urlopen('https://source.unsplash.com/random') filedata = urllib.request.urlopen('https://picsum.photos/500/500')
# Get redirected URL without parameters
url = filedata.url.split('?')[0]
filedata = urllib.request.urlopen(url + '?fit=max&width=500&height=500')
datatowrite = filedata.read() datatowrite = filedata.read()
with open('/tmp/cat2.jpg', 'wb') as f: with open('/tmp/cat2.jpg', 'wb') as f:
f.write(datatowrite) f.write(datatowrite)
widget = GenericMonitorTextWidget('#%d' % self.imgs_idx, 'color:purple') widget = GenericMonitorTextWidget('#%d' % self.imgs_idx, 'color:purple')
url_widget = GenericMonitorTextWidget(url, 'color:white;font-weight:bold', signals={'on-click':'signal'}) # No name here url_widget = GenericMonitorTextWidget('random_pic', 'color:white;font-weight:bold', signals={'on-click':'signal'}) # No name here
picture_widget = GenericMonitorPictureWidget('/tmp/cat2.jpg', name='NestedWidget', signals={'on-click':'signal'}) picture_widget = GenericMonitorPictureWidget('/tmp/cat2.jpg', name='NestedWidget', signals={'on-click':'signal'})
popup = GenericMonitorPopup([url_widget, picture_widget]) popup = GenericMonitorPopup([url_widget, picture_widget])
signals = { signals = {
'on-click':'toggle-popup', 'on-click':'toggle-popup',
# Could also use this behavior # Could also use this behavior [bugged since GNOME 42]
# 'on-enter':'open-popup', # 'on-enter':'open-popup',
# 'on-leave':'close-popup', # 'on-leave':'close-popup',
'on-dblclick':'signal', 'on-dblclick':'signal',

View File

@@ -24,22 +24,22 @@
https://github.com/bananenfisch/RecentItems/blob/master/extension.js https://github.com/bananenfisch/RecentItems/blob/master/extension.js
https://github.com/julio641742/gnome-shell-extension-reference/blob/master/tutorials/POPUPMENU-EXTENSION.md https://github.com/julio641742/gnome-shell-extension-reference/blob/master/tutorials/POPUPMENU-EXTENSION.md
https://gjs-docs.gnome.org/st10~1.0_api/st.widget https://gjs-docs.gnome.org/st10~1.0_api/st.widget
https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/master/js/ui/panelMenu.js https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/panelMenu.js
https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/master/js/ui/popupMenu.js https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/popupMenu.js
https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/master/js/ui/panel.js https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/panel.js
*/ */
const St = imports.gi.St; import * as Extension from 'resource:///org/gnome/shell/extensions/extension.js';
const Gio = imports.gi.Gio; import St from 'gi://St';
const GLib = imports.gi.GLib import Gio from 'gi://Gio';
const Main = imports.ui.main; import GLib from 'gi://GLib';
const Mainloop = imports.mainloop; import Clutter from 'gi://Clutter';
const Clutter = imports.gi.Clutter; import GObject from 'gi://GObject';
const PanelMenu = imports.ui.panelMenu; import Pixbuf from 'gi://GdkPixbuf';
const PopupMenu = imports.ui.popupMenu; import Cogl from 'gi://Cogl';
const GObject = imports.gi.GObject; import * as Main from 'resource:///org/gnome/shell/ui/main.js';
const Pixbuf = imports.gi.GdkPixbuf; import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
const Cogl = imports.gi.Cogl; import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
function hashGet(hash, key, defaultValue) { function hashGet(hash, key, defaultValue) {
@@ -50,22 +50,23 @@ function hashGet(hash, key, defaultValue) {
} }
function log(message) { function log(message) {
global.log('[GenericMontior]', message); console.error('[GenericMontior]', message);
} }
class SignalMgt { class SignalMgt {
constructor(item, name, group, dbus, menu) { constructor(item, name, group, dbus, buttonMenu) {
this.name = name; this.name = name;
this.group = group; this.group = group;
this.fullname = this.name + '@' + this.group; this.fullname = this.name + '@' + this.group;
this.dbus = dbus; this.dbus = dbus;
this.menu = menu; this.buttonMenu = buttonMenu;
this.signals = new WeakMap(); this.signals = new WeakMap();
this.widgets = new Array(); this.widgets = new Array();
this.timeouts = new Array(); this.timeouts = new Array();
this.menuOpen = false;
this.nbClicks = 0; this.nbClicks = 0;
this.button = -1; this.button = -1;
@@ -78,7 +79,7 @@ class SignalMgt {
this.onScroll = hashGet(item, 'on-scroll', ''); this.onScroll = hashGet(item, 'on-scroll', '');
} }
destructor() { destroy() {
for(let widgetIdx in this.widgets) for(let widgetIdx in this.widgets)
this.disconnectWidgetSignals(this.widgets[widgetIdx]); this.disconnectWidgetSignals(this.widgets[widgetIdx]);
for(let timeoutIdx in this.timeouts) for(let timeoutIdx in this.timeouts)
@@ -118,25 +119,42 @@ class SignalMgt {
this.signals.set(widget, null); this.signals.set(widget, null);
} }
toggleMenu() {
if (this.menuOpen)
{
this.buttonMenu.menu.close();
this.menuOpen = false;
}
else
{
this.buttonMenu.menu.open(true);
this.menuOpen = true;
}
}
_manageEventAction(action, signalName) { _manageEventAction(action, signalName) {
if (action === 'open-popup') if (action === 'open-popup')
this.menu.open(true); {
this.buttonMenu.menu.open(true);
this.menuOpen = true;
}
else if (action === 'close-popup') else if (action === 'close-popup')
this.menu.close(); {
this.buttonMenu.menu.close();
this.menuOpen = false;
}
else if (action === 'toggle-popup') else if (action === 'toggle-popup')
this.menu.toggle(); {
else if (action == 'delete') this.toggleMenu();
}
else if (action === 'delete')
this.dbus.deleteItem(this, this.group); this.dbus.deleteItem(this, this.group);
else if (action === 'signal') else if (action === 'signal')
this.dbus.emitSignal(signalName, this.fullname); this.dbus.emitSignal(signalName, this.fullname);
return Clutter.EVENT_PROPAGATE; return Clutter.EVENT_STOP;
} }
_manageLeaveEvent() {
this._manageEventAction(this.onLeave);
}
_doClickCallback() { _doClickCallback() {
let right = ''; let right = '';
let nbClicks = ''; let nbClicks = '';
@@ -171,8 +189,9 @@ class SignalMgt {
this.button = event.get_button(); this.button = event.get_button();
this.nbClicks = 1; this.nbClicks = 1;
let sourceId = Mainloop.timeout_add(this.dbus.ClutterSettings['double-click-time'], let sourceId = GLib.timeout_add(GLib.G_PRIORITY_DEFAULT,
this._doClickCallback.bind(this)); this.dbus.ClutterSettings['double-click-time'],
this._doClickCallback.bind(this));
this.timeouts.push(sourceId); this.timeouts.push(sourceId);
} }
@@ -210,7 +229,7 @@ class MyPopupMenuItem extends PopupMenu.PopupBaseMenuItem {
this.box.set_vertical(true); this.box.set_vertical(true);
for (let widgetIndex in widgets) for (let widgetIndex in widgets)
this.box.add(widgets[widgetIndex]); this.box.add_child(widgets[widgetIndex]);
this.add_child(this.box); this.add_child(this.box);
} }
@@ -229,7 +248,8 @@ class MonitorWidget extends PanelMenu.Button {
this.group = group; this.group = group;
this.fullname = this.name + '@' + this.group; this.fullname = this.name + '@' + this.group;
this.dbus = dbus; this.dbus = dbus;
this.signalManager = new SignalMgt(item, this.name, group, dbus, this.menu); this.menuItem = null;
this.signalManager = new SignalMgt(item, this.name, group, dbus, this);
this.popup_signals = null; this.popup_signals = null;
this.popup_widgets = null; this.popup_widgets = null;
@@ -374,6 +394,7 @@ class MonitorWidget extends PanelMenu.Button {
destroy() { destroy() {
this.menu.close(); this.menu.close();
this.signalManager.destroy();
super.destroy(); super.destroy();
} }
@@ -414,7 +435,7 @@ class MonitorWidget extends PanelMenu.Button {
const name = hashGet(nestedItem, 'name', ''); const name = hashGet(nestedItem, 'name', '');
this.popup_signals[widget] = new SignalMgt(nestedItem, name, this.popup_signals[widget] = new SignalMgt(nestedItem, name,
this.fullname, this.dbus, this.fullname, this.dbus,
this.menu); this);
this.popup_signals[widget].connectWidgetSignals(widget); this.popup_signals[widget].connectWidgetSignals(widget);
this.popup_widgets.push(widget); this.popup_widgets.push(widget);
} }
@@ -536,14 +557,12 @@ class MonitorWidget extends PanelMenu.Button {
// From https://github.com/ubuntu/gnome-shell-extension-appindicator/blob/master/interfaces.js // From https://github.com/ubuntu/gnome-shell-extension-appindicator/blob/master/interfaces.js
// loads a xml file into an in-memory string // loads a xml file into an in-memory string
function loadInterfaceXml(filename) { function loadInterfaceXml(extension, filename) {
const extension = imports.misc.extensionUtils.getCurrentExtension();
const interfacesDir = extension.dir.get_child('.'); const interfacesDir = extension.dir.get_child('.');
const file = interfacesDir.get_child(filename); const file = interfacesDir.get_child(filename);
let [result, contents] = imports.gi.GLib.file_get_contents(file.get_path()); let [result, contents] = GLib.file_get_contents(file.get_path());
if (result) { if (result) {
// HACK: The "" + trick is important as hell because file_get_contents returns // HACK: The "" + trick is important as hell because file_get_contents returns
@@ -552,7 +571,10 @@ function loadInterfaceXml(filename) {
// is no `XML` on very recent SpiderMonkey releases (or, if SpiderMonkey is old enough, // is no `XML` on very recent SpiderMonkey releases (or, if SpiderMonkey is old enough,
// will spit out a TypeError soon). // will spit out a TypeError soon).
if (contents instanceof Uint8Array) if (contents instanceof Uint8Array)
contents = imports.byteArray.toString(contents); {
const decoder = new TextDecoder();
contents = decoder.decode(contents);
}
const res = `<node>${contents}</node>`; const res = `<node>${contents}</node>`;
return res; return res;
} else { } else {
@@ -561,11 +583,11 @@ function loadInterfaceXml(filename) {
} }
class GenericMonitorDBUS { class GenericMonitorDBUS {
constructor() { constructor(extension) {
this.monitor_groups = {}; this.monitor_groups = {};
this.actor_clicked = {}; this.actor_clicked = {};
this.ClutterSettings = Clutter.Settings.get_default(); this.ClutterSettings = Clutter.Settings.get_default();
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(loadInterfaceXml('dbus.xml'), this); this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(loadInterfaceXml(extension, 'dbus.xml'), this);
this._dbusImpl.export(Gio.DBus.session, '/com/soutade/GenericMonitor'); this._dbusImpl.export(Gio.DBus.session, '/com/soutade/GenericMonitor');
this._dbusImpl.emit_signal('onActivate', null); this._dbusImpl.emit_signal('onActivate', null);
} }
@@ -768,7 +790,7 @@ class GenericMonitorDBUS {
monitorWidget.togglePopup(); monitorWidget.togglePopup();
} }
destructor() { destroy() {
this._dbusImpl.emit_signal('onDeactivate', null); this._dbusImpl.emit_signal('onDeactivate', null);
for (let groupIndex in this.monitor_groups) { for (let groupIndex in this.monitor_groups) {
const group = this.monitor_groups[groupIndex]; const group = this.monitor_groups[groupIndex];
@@ -780,26 +802,21 @@ class GenericMonitorDBUS {
} }
} }
class Extension { export default class GenericMonitorExtension extends Extension.Extension {
constructor(...args) {
super(...args);
this.textDBusService = null;
}
enable() { enable() {
this.textDBusService = new GenericMonitorDBUS(); this.textDBusService = new GenericMonitorDBUS(this);
} }
disable() { disable() {
this.textDBusService.destructor(); if (this.textDBusService !== null) {
delete this.textDBusService; this.textDBusService.destroy();
delete this.textDBusService;
this.textDBusService = null;
}
} }
} }
const extension = new Extension();
function init() {
}
function enable() {
extension.enable();
}
function disable() {
extension.disable();
}

View File

@@ -2,12 +2,11 @@
"uuid": "generic-monitor@gnome-shell-extensions", "uuid": "generic-monitor@gnome-shell-extensions",
"name": "Generic Monitor", "name": "Generic Monitor",
"description": "Display text & icon on systray using DBUS", "description": "Display text & icon on systray using DBUS",
"version": "6", "version": "15",
"shell-version": [ "shell-version": [
"41", "47",
"40", "46",
"3.38", "45"
"3.36"
], ],
"url": "http://indefero.soutade.fr/p/genericmonitor" "url": "https://forge.soutade.fr/soutade/GnomeShellGenericMonitor"
} }