How to monitor application events such as open, close, minimize in Ubuntu?

I would like to know if it is possible to monitor applications open/close/minimize events in Ubuntu. My initial goal is to monitor how many times I open telegram to check messages.

PS: I am using Ubuntu 20.04LTS (X11).


Solution 1:

Just for fun, since you are on X

Using python (or a bunch of other languages) We can use signals from Wnck.Screen and Wnck.Window to keep an eye on the creation and/or state change of windows. This includes maximizing and minimizing windows.

That's exactly what the script below does. Subsequently, it maintains a logfile, that will be updated if you create, minimize or unminimize a window of a specific WM_CLASS (and so application). You can find the WMCLASS of the targeted application by opening a terminal, type xprop WM_CLASS + Return, then click on the windowsubject ("telegramdesktop" for telegram or something like that).

Mind that I made the script reset the logfile after each (logging) session, else the logfile would become huge over time.

What's in the logfile?

The logfile (~/.windowlog.txt) will keep record of creation, closure and state-change of window(s) of the given WM_Class. Each time the window is unminimized, the counter adds one, so at the end of the day, you can see the activity:

found window: Telegram
state changed: visual(1)
state changed: minimized
state changed: visual(2)
state changed: minimized
state changed: visual(3)
window closed:Telegram
new window:Telegram
state changed: visual(4)
state changed: minimized
state changed: visual(5)

Note that the script is written with a single window for the application in mind, like the one in your question. To keep a more detailed record per window, processing the data would require more sophisticated coding.

The script

#!/usr/bin/env python3
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Wnck", "3.0")
from gi.repository import Gtk, Wnck
import os
import sys


class WatchWindow:

    def __init__(self, wmclass):
        self.visual = None
        self.count_visual = 0
        self.wnck_scr = Wnck.Screen.get_default()
        self.wmclass = wmclass
        self.logpath = os.environ["HOME"] + "/.windowlog.txt"
        self.run_watching()
        Gtk.main()

    def write_to_log(self, newline):
        with open(self.logpath, "a+") as logfile:
            logfile.write(newline + "\n")

    def readable_state(self, minimized):
        n = ""
        if not minimized:
            self.count_visual = self.count_visual + 1
            n = "(" + str(self.count_visual) + ")"
        return ["minimized", "visual"][[True, False].index(minimized)] + n
    
    def logstate(self, window, *args):
        old_state = self.visual
        new_state = window.is_minimized()
        # only register if minimized state really changed
        if old_state != new_state:
            self.visual = new_state
            message = "state changed: " + self.readable_state(self.visual) # log
            print(message)
            self.write_to_log(message)

    def log_new(self, screen, window):
        if  window.get_class_group_name().lower() == self.wmclass:
            message = "new window:" + window.get_name() # log new
            print(message)
            self.write_to_log(message)
            self.watch_window(window)
            self.logstate(window)

    def log_closed(self, screen, window):
        if  window.get_class_group_name().lower() == self.wmclass:
            name = window.get_name()
            self.visual = None
            print("window closed:", name) # log closed
    
    def watch_window(self, window, firstcall=False):
        if  window.get_class_group_name().lower() == self.wmclass:
            if firstcall:
                message = "found window:" + window.get_name()
                print(message) # log please
                self.write_to_log("found window: " + window.get_name())
                self.logstate(window)
            window.connect("state_changed", self.logstate)

    def run_watching(self):
        try:
            os.remove(self.logpath)
        except FileNotFoundError:
            pass
        self.wnck_scr.force_update()
        for w in self.wnck_scr.get_windows():
            self.watch_window(w, True)
        self.wnck_scr.connect("window-opened", self.log_new)
        self.wnck_scr.connect("window-closed", self.log_closed)

args = sys.argv[1:]
if not args:
    print("Insufficient arguments! We need a wm_class to watch...")
else:
    WatchWindow(args[0])

Set up

  1. Copy the script into an empty file, save it as windowlogger.py and make it executable

  2. Test- run it in a terminal window, run it with the WM_CLASS as argument (I suppose telegramdesktop), so:

    /path/to/windowlogger telegramdesktop
    
  3. See if the output in terminal is fine, see inside the logfile ~/.windowlog.txt if all works as it should.

  4. Add it to your startup applications if you like.

N.B

Possibly, you need to add one or more libraries, check the terminal output.

Logging window being active?

From a comment, I understand you consider the window as "used" (only) if it is the active window.
In that case we can make the script substantially simpler, since we only need to look at the active_window_changed signal. If we also log time usage (per usage / total use time), you can get clear insight how much time you spent, staring at the (any) telegram window. The logfile then looks like:

start_time:  woensdag, oktober 06 2021, 11:32:53
window activated (1)
window hidden or closed, was active: 0:00:04    total: 0:00:04
window activated (2)
window hidden or closed, was active: 0:00:06    total: 0:00:10
window activated (3)
window hidden or closed, was active: 0:00:12    total: 0:00:22
window activated (4)
window hidden or closed, was active: 0:00:07    total: 0:00:29

The script in that case:

#!/usr/bin/env python3
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Wnck", "3.0")
from gi.repository import Gtk, Wnck
import os
import sys
import time
import datetime

class WatchWindow:

    def __init__(self, wmclass):
        self.visual = False
        self.count_visual = 1
        self.wnck_scr = Wnck.Screen.get_default()
        self.wmclass = wmclass
        self.logpath = os.environ["HOME"] + "/.windowlog.txt"
        self.total_time = 0
        self.last_time = time.time()
        self.run_watching()
        Gtk.main()

    def write_to_log(self, newline):
        with open(self.logpath, "a+") as logfile:
            logfile.write(newline + "\n")

    def get_readable_time(self, elapsed):
        return str(datetime.timedelta(seconds=elapsed))

    def log_active(self, *args):
        try:
            # active_class can be None, e.g. on startup
            active_class = self.wnck_scr.get_active_window().get_class_group_name()
        except AttributeError:
            active_class = ""
        newvisual = active_class.lower() == self.wmclass.lower()
        oldvisual = self.visual
        currtime = time.time()
        if newvisual != oldvisual:
            if newvisual:
                self.last_time = currtime
                message = "window activated (" + str(self.count_visual) + ")"
                self.count_visual = self.count_visual + 1
            else:
                winactive_time = currtime - self.last_time
                self.last_time = currtime
                self.total_time = self.total_time + winactive_time
                message = "window hidden or closed, was active: " + \
                          self.get_readable_time(round(winactive_time)) +\
                          "\t" + "total: " +\
                          self.get_readable_time(round(self.total_time))
            self.write_to_log(message)
        self.visual = newvisual

    def run_watching(self):
        try:
            os.remove(self.logpath)
        except FileNotFoundError:
            pass
        time_stamp_message = "start_time: " + time.strftime(" %A, %B %d %Y, %H:%M:%S")
        self.write_to_log(time_stamp_message)
        self.wnck_scr.force_update()
        self.wnck_scr.connect("active-window-changed", self.log_active)
        self.log_active()

args = sys.argv[1:]
if not args:
    print("Insufficient arguments! We need a wm_class to watch...")
else:
    WatchWindow(args[0])

Setup is the same.