#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2022 Karl Fogel # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # If you did not receive a copy of the GNU General Public License # along with this program, see . """Print any Gnome 3 dbus notifications to stdout. The purpose of this program is to enable you to see notifications after they've vanished from your screen. , As you know, Gnome always shows a notification at the moment your phone rings (or whatever), and the notification disappears right when you finally get a chance to glance at it. A typical way to run this is is from a shell init file, with output going to some place that you can then grab notifications from. E.g.: gnotifications > ~/.cache/gnotifications.out & """ import re import sys import datetime import subprocess def main(): with subprocess.Popen(["dbus-monitor", "interface='org.freedesktop.Notifications'"], stdout=subprocess.PIPE, bufsize=1, universal_newlines=True) as proc: # We're watching for multi-line beasts like this: # # method call time=1652471532.319641 sender=:1.463 -> destination=:1.43 serial=7 path=/org/freedesktop/Notifications; interface=org.freedesktop.Notifications; member=Notify # # string "notify-send" # # uint32 0 # # string "" # # string "fish" # # string "" # # array [ # # ] # # array [ # # dict entry( # # string "urgency" # # variant byte 1 # # ) # # dict entry( # # string "sender-pid" # # variant int64 204391 # # ) # # ] # # int32 -1 # # The trouble is, for some reason that message comes across # the dbus twice, with the same message and same timestamp, # so we have to check for that and ignore the second message. # Buncha flags for old-fashioned grunt parsing. seen_method = False last_timestamp = None this_timestamp = None last_message = None seen_array = False in_match_zone = False for line in proc.stdout: if in_match_zone: m = re.match("^ *string +\"(.+)\"$", line) if m is not None: this_message = m.group(1) if not ((last_timestamp is not None) and (this_timestamp == last_timestamp) and (this_message == last_message)): sys.stdout.write(f"{this_timestamp} UTC: {this_message}\n") sys.stdout.flush() last_message = this_message last_timestamp = this_timestamp else: # a repeat message with the same timestamp last_timestamp = None last_message = None if m := re.match("^method call time=(\\d+)\\.\\d+ .*; *member=Notify$", line): seen_method = True # See https://blog.ganssle.io/articles/2019/11/utcnow.html and # https://discuss.python.org/t/deprecating-utcnow-and-utcfromtimestamp/26221 utc_ts_tmp = datetime.datetime.fromtimestamp( int(m.group(1)), tz=datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S') if last_timestamp is not None and utc_ts_tmp == last_timestamp: this_timestamp = last_timestamp else: last_timestamp = None this_timestamp = utc_ts_tmp elif seen_method and re.match("^ *uint32 +0$", line): in_match_zone = True elif in_match_zone and re.match("^ +array \\[$", line): seen_method = False in_match_zone = False if __name__ == '__main__': main()