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
-
Copy the script into an empty file, save it as
windowlogger.py
and make it executable -
Test- run it in a terminal window, run it with the WM_CLASS as argument (I suppose
telegramdesktop
), so:/path/to/windowlogger telegramdesktop
-
See if the output in terminal is fine, see inside the logfile
~/.windowlog.txt
if all works as it should. -
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.