Commit 34711ae9 authored by Saswat's avatar Saswat

add makefile; scripts for packaging; code cleanup

parent 4970a206
Pipeline #1722 failed with stages
build/
data/
pkgs/
dist/
venv/
atvenv/
src/__pycache/
*.spec
VENV = atvenv
PYTHON = $(VENV)/bin/python3
PIP = $(VENV)/bin/pip
APP_NAME = "ActivityTracker"
VERSION = "1.0"
ARCH = "amd64"
PKG_NAME = "$(APP_NAME)_$(VERSION)_$(ARCH)"
PKGS_UTILS=\
scripts/activitytrackerd.service\
scripts/activitytrackerd.postinst\
scripts/activitytrackerd.prerm\
scripts/control\
UTILS=\
src/utils.py\
src/dbhandler.py\
DAEMON=\
src/trackingdaemon.py\
src/eventhandler.py\
GUI=\
src/plotter.py\
src/gui.py\
all: venv pkg
@echo Started Virtual Environment
@echo Built deb package in pkg/$(PKG_NAME).deb
venv: $(VENV)/bin/activate
@echo Virtual Environment is Set
# $(PYTHON) src/tracking_deamon.py &
# $(PYTHON) src/gui.py
$(VENV)/bin/activate: requirements.txt
@echo Creating Virtual Environment
python3 -m venv $(VENV)
$(PIP) install -r requirements.txt
pkg: dist/ActivityTracker dist/ActivityTrackerd $(PKGS_UTILS) appdata/config.json
@echo Building Package
mkdir -p pkgs/$(PKG_NAME)/etc/$(APP_NAME)/
cp appdata/config.json pkgs/$(PKG_NAME)/etc/$(APP_NAME)/
mkdir -p pkgs/$(PKG_NAME)/DEBIAN/
cp scripts/control pkgs/$(PKG_NAME)/DEBIAN/
mkdir -p pkgs/$(PKG_NAME)/var/lib/dpkg/info/
cp scripts/activitytrackerd.postinst pkgs/$(PKG_NAME)/var/lib/dpkg/info/
cp scripts/activitytrackerd.prerm pkgs/$(PKG_NAME)/var/lib/dpkg/info/
mkdir -p pkgs/$(PKG_NAME)/usr/bin/
cp dist/activitytracker pkgs/$(PKG_NAME)/usr/bin/
cp dist/activitytrackerd pkgs/$(PKG_NAME)/usr/bin/
dpkg-deb --build --root-owner-group pkgs/$(PKG_NAME)
dist/activitytracker: $(GUI) $(UTILS)
pyinstaller --onefile --hidden-import "babel.numbers" -n activitytracker src/gui.py
dist/activitytrackerd: $(DAEMON) $(UTILS)
pyinstaller --onefile --hidden-import "babel.numbers" -n activitytrackerd src/trackingdaemon.py
clean: clean-pkg clean-venv
@echo Clean completed
clean-pkg:
@echo Cleaning Package related folders
rm -rf build
rm -rf dist
rm -rf pkgs
rm -rf *.spec
rm -rf src/__pycache__
clean-venv:
@echo Removing Virtual Environment
rm -rf $(VENV)
{
"INTERVAL": 300,
"DATABASE_FILE": "data/activity_data.db",
"LOG_FILE": "log/deamon.log"
"DATABASE_FILE": "/home/saswat/.ActivityTracker/activity_data.db"
}
\ No newline at end of file
matplotlib==3.5.1
numpy==1.21.5
tkcalendar==1.6.1
Xlib==0.21
pyinstaller==5.6.2
sudo systemctl daemon-reload
sudo systemctl stop activitytrackerd.service
sudo systemctl disable activitytrackerd.service
sudo systemctl enable activitytrackerd.service
sudo systemctl start activitytrackerd.service
sudo systemctl status activitytrackerd.service
#!/bin/sh -e
#DEBHELPER#
set -x
systemctl daemon-reload
systemctl enable activitytrackerd.service
systemctl start activitytrackerd.service
set +x
\ No newline at end of file
#!/bin/sh -e
systemctl daemon-reload
systemctl stop activitytrackerd.service
systemctl disable activitytrackerd.service
#DEBHELPER#
\ No newline at end of file
[Unit]
Description=A utility to track your daily activities in the desktop.
[Service]
Type=simple
ExecStart=/usr/bin/ActivityTrackerd
[Install]
WantedBy=multi-user.target
Package: ActivityTracker
Version: 1.0
Architecture: amd64
Maintainer: Saswat Meher, Suraj Munjani <dummymail@gmail.com>
Description: A utility to track your daily activities in the desktop.
This program helps in keep track of which windows are being opened through it's deamon process Activitytrackerd and provides a gui interface to visualise those activities through ActivityTracker.
import sqlite3
from utils import load_config
from datetime import datetime, timedelta
#signal.signal(signal.SIGINT, handler)
import os
class DBHandler:
"""
This class contains utilites to interact with the activity database.
Both the deamon and application can use this class for reading and writing into the databse.
"""
def __init__(self, db_path) -> None:
"""
Init method that initialises a connection with the databases, provided the path in the argument.
Also if the file doesnot exist it will create the file.
If the table doesnot exist it will also create a table names 'ACTIVITY'
"""
self.conn = sqlite3.connect(db_path)
self.check_activity_table()
return
def check_activity_table(self):
"""
This method checks if a table named 'ACTIVITY' exist in our database.
If not then it will create a new table with the name 'ACTIVITY'.
"""
listOfTables = self.conn.execute(
'''SELECT name FROM sqlite_master WHERE type='table'
AND name='ACTIVITY';''').fetchall()
......@@ -23,10 +35,16 @@ class DBHandler:
return
def close_connection(self):
"""
Method to close the connection with database.
"""
self.conn.close()
return
def print_db(self):
"""
A helper function to print items in the database. Used for debugging purpose.
"""
print("All data in activity table")
cursor = self.conn.execute("SELECT * from ACTIVITY ")
for row in cursor:
......@@ -34,22 +52,31 @@ class DBHandler:
return
def write_interval(self, interval_start, app_name, duration):
"""
Write an entry for the amount of time an application was active in an interval.
"""
query = f"INSERT INTO activity VALUES ({interval_start}, '{app_name}', {duration})"
#print(query)
self.conn.execute(query)
self.conn.commit()
def read_week_data(self, date:datetime, app_name=""):
def read_week_data(self, date, app_name=""):
"""
Provided a date instance return all data present corresponding the whole week
for the date.
"""
print(date)
date = datetime(date.year, date.month, date.day, 0, 0, 0)
start = date - timedelta(days=date.weekday())
end = start + timedelta(days=6)
print(start)
print(end)
end = start + timedelta(days=7)
#print(start)
#print(end)
start = start.timestamp()
end = end.timestamp()
if app_name:
query = f'''SELECT * from ACTIVITY where DATETIME >= {start} and DATETIME <= {end} and APPID == "{app_name}"'''
query = f'''SELECT * from ACTIVITY where DATETIME >= {start} and DATETIME < {end} and APPID == "{app_name}"'''
else:
query = f"SELECT * from ACTIVITY where DATETIME >= {start} and DATETIME <= {end}"
query = f"SELECT * from ACTIVITY where DATETIME >= {start} and DATETIME < {end}"
cursor = self.conn.cursor()
data = cursor.execute(query).fetchall()
return data
\ No newline at end of file
......@@ -3,22 +3,39 @@ from dbhandler import DBHandler
from utils import extract_app_name
class Interval:
"""
Class for storing durations for each application active during an interval
that will get dumped to database at once.
"""
def __init__(self, interval, db:DBHandler) -> None:
"""
Init method to initialise the database, start of the interval and variables needed
during interval.
"""
self.__proc_duration = dict() # dict that stores processes duration in an interval
self.duration = int(interval) # Interval duration
self.db = db
self.db = db # Database class
self.start = int(self.__get_current_time() / self.duration) * self.duration # start of the interval
self.remaining_time = self.duration
return
def get_remaining_time(self):
"""
Returns the time remaining in an interval.
"""
return self.duration - self.exec
def __get_current_time(self):
"""
A wrapper method to get current time.
"""
return int(time.time())
def add_process(self, title:str, exec_time:int):
"""
Add a process and its active time in the interval.
"""
self.remaining_time -= exec_time
if title in self.__proc_duration:
self.__proc_duration[title] += exec_time
......@@ -26,12 +43,20 @@ class Interval:
self.__proc_duration[title] = exec_time
def dump_interval(self, incr_interval=False):
"""
When an interval gets completed it dumps all the active time for all process into
the database.
"""
for title in self.__proc_duration:
self.db.write_interval(self.start, title, self.__proc_duration[title])
self.reset(incr_interval)
return
def reset(self, incr_interval=False):
"""
This method helps in resenting the values present in the interval object for the
next interval.
"""
self.__proc_duration.clear()
if incr_interval:
self.start += self.duration
......@@ -40,16 +65,35 @@ class Interval:
self.remaining_time = self.duration
class EventHandler:
"""
Whenever a event happens i.e. change in the name of current active window, this class handle
that change.
"""
def __init__(self, interval, db_file) -> None:
"""
Initialise an interval class to hold durations for that interval.
"""
self.__prev_event_time = self.__get_current_time() # store current time as prev time to use later
self.__interval = Interval(interval, DBHandler(db_file))
self.__prev_proc = ""
return
def __get_current_time(self):
"""
A wrapper method to get current time.
"""
return int(time.time())
def handle_change(self, title:str) -> None:
"""
This method gets called whenever a change/event occurs.
Use __prev_proc that contains name of the process that was runnning previously.
Tries to assign that value to the interval.
If the event occurs a long after the end time the interval. It fill ups the prev interval
and then dumps the interval, then increament the interval.
Do this until it reaches to current interval.
Stores the current window name as __prev_proc
"""
curr_event_time = self.__get_current_time()
last_proc_exec_time = curr_event_time - self.__prev_event_time
......@@ -64,6 +108,9 @@ class EventHandler:
return
def handle_termination(self):
"""
A utility to terminate the event handling process graciously.
"""
self.handle_change("")
self.__interval.dump_interval()
#self.__interval.db.print_db()
......
from tkinter import *
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from plotter import ActivityPlotter
import plotter
from datetime import datetime, timedelta
from tkinter.ttk import *
from tkcalendar import DateEntry
class ActivityTracker:
"""
This class stores all the variables needed for the gui.
Also updates graphs and details when the date is changed.
"""
def __init__(self) -> None:
self.root = Tk()
self.plotter = ActivityPlotter()
self.root = Tk() # A tikinter object to create the root of the window
self.plotter = plotter.ActivityPlotter()
self.root.title('Activity Tracker')
width = self.root.winfo_screenwidth()
......@@ -17,14 +21,18 @@ class ActivityTracker:
self.root.geometry("%dx%d"%(width,height))
self.screen_time_text = "1 hr 30 min"
self.today = datetime.now()
self.curr_date = datetime.now()
self.today = datetime.now().date()
self.curr_date = datetime.now().date()
self.date_text = self.curr_date.strftime('%d, %b %Y')
self.__fetch_curr_date_figs()
self.__initialise_struct()
return
def __initialise_struct(self):
"""
Initialise the basic structure of the window.
Use grid method to place various structures like label and buttons.
"""
self.activity_label = Label(self.root, text = "Activity tracking for: ", font=('Arial',20))
self.activity_label.grid(row = 0, column = 0, columnspan=1, sticky='nesw', padx=20, pady=20 )
......@@ -43,7 +51,7 @@ class ActivityTracker:
self.cal = DateEntry(self.root, year=self.curr_date.year, month = self.curr_date.month, day=self.curr_date.day, font="Arial 15")
self.cal.grid(row=1, column=1, sticky='e', padx=20, pady=20, )
self.reload_button = Button(self.root, text ="Reload", command = self.reload_date)
self.reload_button = Button(self.root, text ="Go", command = self.reload_date)
self.reload_button.grid(row=1, column=2, sticky='W')
self.next_day_button = Button(self.root, text =">", command = self.get_next_date)
......@@ -57,7 +65,9 @@ class ActivityTracker:
def __refresh_graphs(self):
self.figure1.savefig('weekly.png')
"""
This method refreshes all three graphs provided the __fetch_curr_date_figs() is called before this method.
"""
self.weekly = FigureCanvasTkAgg(self.figure1, self.root)
self.weekly.get_tk_widget().grid(row=2, column=0, columnspan=4, rowspan=3, sticky='N', padx=20, pady=20 )
......@@ -69,6 +79,9 @@ class ActivityTracker:
def __refresh_labels(self):
"""
Refresh the labels for date and Total time spent.
"""
self.date_text = self.curr_date.strftime('%d, %b %Y')
self.date_label.config(text=self.date_text)
self.screen_time.config(text=self.screen_time_text)
......@@ -76,6 +89,11 @@ class ActivityTracker:
return
def __refresh_app(self):
"""
Perform refresh for various labels of text and grpahs once a button is pressed.
"""
# disable next date if current date is today.
if self.curr_date >= self.today:
self.next_day_button.config(state=DISABLED)
else:
......@@ -87,25 +105,39 @@ class ActivityTracker:
def __fetch_curr_date_figs(self):
"""
Fetch all the figures from the plotter object with the new updated time.
"""
self.figure1, self.figure2, self.figure3, self.screen_time_text = self.plotter.get_figs(self.curr_date)
return
def get_prev_date(self):
"""
A wrapper method to update the app with day as prev day.
"""
self.curr_date = self.curr_date - timedelta(days=1)
self.__refresh_app()
return
def get_next_date(self):
"""
A wrapper method to update the app with day as next day.
"""
self.curr_date = self.curr_date + timedelta(days=1)
self.__refresh_app()
return
def reload_date(self):
new_date = self.cal.get_date()
self.curr_date = datetime.combine(new_date, datetime.now().time())
"""
A wrapper method to update the app with day set buy the calender.
"""
self.curr_date = self.cal.get_date()
self.__refresh_app()
return
if __name__ == '__main__':
"""
Execution starts from here, Creating a ActivityTracker object, then it is set on mainloop().
"""
tracker = ActivityTracker()
tracker.root.mainloop()
\ No newline at end of file
from utils import *
from dbhandler import DBHandler
import dbhandler
import utils
import numpy as np
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
class ActivityPlotter:
"""
Class to help plotting grpahs in GUI.
"""
def __init__(self) -> None:
config = load_config()
"""
Init database handler and various constant variables
"""
config = utils.load_config()
plt.rcParams.update({'font.size': 16})
self.db = DBHandler(config['DATABASE_FILE'])
self.db = dbhandler.DBHandler(config['DATABASE_FILE'])
print(config['DATABASE_FILE'])
self.week_days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
self.hours = ['12am', '3am', '6am', '9am', '12pm', '3pm', '6pm', '9pm', '12am']
#self.curr_week = datetime.fromtimestamp(0)
def __init_dataholders(self):
"""
Initialise Variables that will contains the data for the graph axises.
"""
self.perday = np.zeros(7, dtype=float)
self.perhour = np.zeros(24, dtype= float)
self.perapp = dict()
return
def __load_weekly_data(self):
"""
This method fetch data from database handler. Process those dataset to get details
regarding activity done in a week, a day, per applciation basis.
"""
self.week_data = self.db.read_week_data(self.curr_date)
self.__init_dataholders()
......@@ -29,7 +41,7 @@ class ActivityPlotter:
event_timestamp = datetime.fromtimestamp(event[0])
event_date = event_timestamp.date()
event_day = event_date.weekday()
if event_date == self.curr_date.date():
if event_date == self.curr_date:
self.perhour[event_timestamp.hour] += event[2]
if event[1] in self.perapp:
self.perapp[event[1]] += event[2]
......@@ -38,8 +50,15 @@ class ActivityPlotter:
self.perday[event_day] += event[2]
self.perapp = dict(sorted(self.perapp.items(), key=lambda x:x[1],reverse=True))
#print(self.perday)
#print(self.perhour)
#print(self.perapp)
def __quantise_time(self, time_list, qtype):
"""
Quantise the time from second into a more human readable unit.
If max time is more than 2hr than represent in hrs, else represent in mins.
"""
if len(time_list) == 0:
if qtype == 0:
return np.zeros(7), "mins"
......@@ -58,7 +77,11 @@ class ActivityPlotter:
return time_list, time_format
def __get_week_plot(self):
week_day = self.curr_date.date().weekday()
"""
Plot graph for activities done in a week on per day basis.
Highlight the current day inside the graph.
"""
week_day = self.curr_date.weekday()
figure = plt.Figure(figsize=(15, 5), dpi=70)
ax = figure.add_subplot(111)
......@@ -72,14 +95,16 @@ class ActivityPlotter:
ax.grid()
figure.tight_layout()
curr_date_time = self.perday[week_day]
print(self.perday)
screen_time_text = str(int(curr_date_time/3600)) + " hr " + str(int((curr_date_time%3600)/60)) + " min"
print(screen_time_text)
return figure, screen_time_text
def __get_day_plot(self):
"""
Plot graph for activities done in a day on per hour basis.
Show time in X axis and amount of screen time in y axis.
"""
figure = plt.Figure(figsize=(15, 5), dpi=70)
ax = figure.add_subplot(111)
......@@ -95,11 +120,16 @@ class ActivityPlotter:
return figure
def __get_app_plot(self):
"""
Plot a Horizontal bar for different applications ran on the day.
At max Show only top 10 applications for the day.
"""
figure = plt.Figure(figsize=(10, 10), dpi=70)
ax = figure.add_subplot(111)
perapp_list = np.array(list(self.perapp.values())[::-1], dtype=float)
perapp_list = np.array(list(self.perapp.values()), dtype=float)
q_perapp, time_format = self.__quantise_time(perapp_list.copy(), 2)
ax.barh(list(self.perapp.keys())[::-1], q_perapp)
perapp_names = list(self.perapp.keys())
ax.barh(perapp_names[min(9,len(perapp_names)-1)::-1], q_perapp[min(9,len(perapp_names)-1)::-1])
ax.set_xlabel(f"Time spent in {time_format}")
ax.set_ylabel("Applications")
......@@ -108,7 +138,11 @@ class ActivityPlotter:
figure.tight_layout()
return figure
def get_figs(self, timestamp:datetime):
def get_figs(self, timestamp):
"""
Return all three plots i.e. Activity in a week, Activity in a day,
Activity per application on that day. Also returns the total screen time on the day.
"""
self.curr_date = timestamp
self.__load_weekly_data()
......@@ -117,14 +151,3 @@ class ActivityPlotter:
app_fig = self.__get_app_plot()
return week_fig, day_fig, app_fig, screen_time_text
"""
plotter = ActivityPlotter()
fig1, fig2, fig3 = plotter.get_figs(datetime.now())
fig1.savefig('weekly.png')
fig2.savefig('daily.png')
fig3.savefig('perapp.png')
"""
\ No newline at end of file
#!/bin/sh
DISPLAY=":1" /usr/bin/python3 /home/saswat/Projects/cs699/src/trackingdaemon.py
from contextlib import contextmanager
from typing import Any, Dict, Optional, Tuple, Union
from Xlib import X
from Xlib.display import Display
from Xlib.error import XError
from Xlib.error import BadWindow
from Xlib.xobject.drawable import Window
import os
import signal
import sys
import eventhandler
import utils
event_handler = None
@contextmanager
def create_window_obj(disp, window_id):
"""
Create a window object using window id to get its names later.
"""
new_window_obj = None
if window_id:
try:
new_window_obj = disp.create_resource_object('window', window_id)
except XError:
pass
yield new_window_obj
class EventFetcher:
def __init__(self) -> None:
"""
Initialise the EvenFetcher object that will keep track of the change in the
active window.
"""
# Connect to X11 server and get its root.
self.disp = Display()
self.root = self.disp.screen().root
# Set the event mask to only detect the change in the property of the window.
self.root.change_attributes(event_mask=X.PropertyChangeMask)
# Some variable to store the prev window and current window details.
self.curr_window_title = ""
self.prev_window_id = 0
self.prev_window_name = ""
# Prepare the property names we use so they can be fed into X11 APIs
self.NET_ACTIVE_WINDOW = self.disp.intern_atom('_NET_ACTIVE_WINDOW')
self.NAME_ATOMS = [self.disp.intern_atom('_NET_WM_NAME'), self.disp.intern_atom('WM_NAME')]
# Do a run for the first time.
self.__fetch_current_windowid()
self.__update_window_name()
event_handler.handle_change(self.prev_window_name)
def __fetch_current_windowid(self):
"""
Fetch current window ID. Unset event mask of previous window and set event mask of current window mask.
"""
result = self.root.get_full_property(self.NET_ACTIVE_WINDOW, X.AnyPropertyType)
if result:
window_id = result.value[0]
win_switched = (window_id != self.prev_window_id)
if win_switched:
with create_window_obj(self.disp, self.prev_window_id) as prev_win:
if prev_win != None:
prev_win.change_attributes(event_mask=X.NoEventMask)
self.prev_window_id = window_id
with create_window_obj(self.disp, self.prev_window_id) as curr_win:
if curr_win != None:
curr_win.change_attributes(event_mask=X.PropertyChangeMask)
return win_switched
else:
return False
def __get_window_title(self, window_object):
"""
Get the title from the window object.
"""
for atom in self.NAME_ATOMS:
try:
window_title = window_object.get_full_property(atom, 0)
except BadWindow:
print("Handle Bad Window Error")
else:
if window_title:
win_name = window_title.value
if isinstance(win_name, bytes):
win_name = win_name.decode('utf8', 'replace')
return win_name
else:
return "<unnamed window>"
return ""
def __update_window_name(self):
"""
Find the window name for the given window id.
If this is a new window name then update the previous window name and
return true else false.
"""
if self.prev_window_id:
is_changed = False
with create_window_obj(self.disp, self.prev_window_id) as windowobj:
if windowobj:
try:
curr_window_title = self.__get_window_title(windowobj)
except XError:
pass
else:
is_changed = (curr_window_title != self.prev_window_name)
self.prev_window_name = curr_window_title
return is_changed
else:
self.prev_window_id = None
return self.prev_window_name, True
def handle_xevent(self):
"""
Wait for an window property change event to occur.
"""
event = self.disp.next_event()
if event.type == X.PropertyNotify:
is_changed = False
# If the event is change in the active window.
if event.atom == self.NET_ACTIVE_WINDOW:
if self.__fetch_current_windowid():
self.__update_window_name()
is_changed = True
# If the event is change in the name for the current active window.
elif event.atom in self.NAME_ATOMS:
is_changed = is_changed or self.__update_window_name()
# If adesired change is detected then handle the change.
if is_changed:
event_handler.handle_change(self.prev_window_name)
else:
return
def signal_handler(sig, frame):
"""
Signal handler for termination of the service.
"""
print('Got signal', sig)
event_handler.handle_termination()
sys.exit(0)
def init_eventhandler():
"""
Initialise the EventHandler to handle a change in the name of the window
"""
global event_handler
config = utils.load_config()
event_handler = eventhandler.EventHandler(config["INTERVAL"], config["DATABASE_FILE"])
return event_handler
if __name__ == '__main__':
"""
Main method that starts the process that starts detecting changes in the active window using EventFetcher.
Handle any changes using EventHandler class.
"""
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
print("Current deamon pid:", os.getpid())
event_handler = init_eventhandler()
event_fetcher = EventFetcher()
while True:
event_fetcher.handle_xevent()
\ No newline at end of file
from curses.ascii import isalnum
import json
import os
def extract_app_name(title):
"""
Extract application name from the title of the window.
i.e. title = " - Youtube - Brave"
app = "Brave"
"""
if not title:
return ""
full_title_list = title.split()
i = len(full_title_list)-1
app_name = ""
while i >= 0 and len(full_title_list[i]) != 1 and full_title_list[i].isalnum():
app_name = full_title_list[i] + " " + app_name
i -= 1
return app_name
def load_config():
"""
Load the config file. This contains the interval time, Location for database, etc.
"""
local_file = "../appdata/config.json"
global_file = "/etc/ActivityTracker/config.json"
if os.path.exists(global_file):
f = open(global_file)
config = json.load(f)
f.close()
print("Using global config")
elif os.path.exists(local_file):
f = open(local_file)
config = json.load(f)
f.close()
print("Using dev config")
else:
config = dict()
config['INTERVAL'] = 300
deafult_db_file_folder = os.path.expanduser('~') + "/.ActivityTracker/"
if os.path.exists(deafult_db_file_folder) == False:
os.mkdir(deafult_db_file_folder)
config['DATABASE_FILE'] = deafult_db_file_folder + "activity_data.db"
print("Using default config")
print(config)
return config
try:
import tkinter as tk
from tkinter import ttk
except ImportError:
import Tkinter as tk
import ttk
from tkcalendar import Calendar, DateEntry
def example1():
def print_sel():
print(cal.selection_get())
top = tk.Toplevel(root)
cal = Calendar(top, font="Arial 14", selectmode='day', locale='en_US',
cursor="hand1", year=2018, month=2, day=5)
cal.pack(fill="both", expand=True)
ttk.Button(top, text="ok", command=print_sel).pack()
def example2():
top = tk.Toplevel(root)
cal = Calendar(top, selectmode='none')
date = cal.datetime.today() + cal.timedelta(days=2)
cal.calevent_create(date, 'Hello World', 'message')
cal.calevent_create(date, 'Reminder 2', 'reminder')
cal.calevent_create(date + cal.timedelta(days=-2), 'Reminder 1', 'reminder')
cal.calevent_create(date + cal.timedelta(days=3), 'Message', 'message')
cal.tag_config('reminder', background='red', foreground='yellow')
cal.pack(fill="both", expand=True)
ttk.Label(top, text="Hover over the events.").pack()
def example3():
top = tk.Toplevel(root)
ttk.Label(top, text='Choose date').pack(padx=10, pady=10)
cal = DateEntry(top, width=12, background='darkblue',
foreground='white', borderwidth=2, year=2010)
cal.pack(padx=10, pady=10)
root = tk.Tk()
ttk.Button(root, text='Calendar', command=example1).pack(padx=10, pady=10)
ttk.Button(root, text='Calendar with events', command=example2).pack(padx=10, pady=10)
ttk.Button(root, text='DateEntry', command=example3).pack(padx=10, pady=10)
root.mainloop()
\ No newline at end of file
from utils import *
from contextlib import contextmanager
from typing import Any, Dict, Optional, Tuple, Union # noqa
import os
from eventhandler import EventHandler
from Xlib import X
from Xlib.display import Display
from Xlib.error import XError
from Xlib.xobject.drawable import Window
from Xlib.protocol.rq import Event
import signal
import sys
win_title = ""
# Connect to the X server and get the root window
disp = Display()
root = disp.screen().root
# Prepare the property names we use so they can be fed into X11 APIs
NET_ACTIVE_WINDOW = disp.intern_atom('_NET_ACTIVE_WINDOW')
NET_WM_NAME = disp.intern_atom('_NET_WM_NAME') # UTF-8
WM_NAME = disp.intern_atom('WM_NAME') # Legacy encoding
last_seen = {'xid': None, 'title': None} # type: Dict[str, Any]
@contextmanager
def window_obj(win_id: Optional[int]) -> Window:
window_obj = None
if win_id:
try:
window_obj = disp.create_resource_object('window', win_id)
except XError:
pass
yield window_obj
def get_active_window() -> Tuple[Optional[int], bool]:
response = root.get_full_property(NET_ACTIVE_WINDOW,
X.AnyPropertyType)
if not response:
return None, False
win_id = response.value[0]
focus_changed = (win_id != last_seen['xid'])
if focus_changed:
with window_obj(last_seen['xid']) as old_win:
if old_win:
old_win.change_attributes(event_mask=X.NoEventMask)
last_seen['xid'] = win_id
with window_obj(win_id) as new_win:
if new_win:
new_win.change_attributes(event_mask=X.PropertyChangeMask)
return win_id, focus_changed
def _get_window_name_inner(win_obj: Window) -> str:
for atom in (NET_WM_NAME, WM_NAME):
try:
window_name = win_obj.get_full_property(atom, 0)
except UnicodeDecodeError:
title = "<could not decode characters>"
else:
if window_name:
win_name = window_name.value
if isinstance(win_name, bytes):
win_name = win_name.decode('latin1', 'replace')
return win_name
else:
title = "<unnamed window>"
return "{} (XID: {})".format(title, win_obj.id)
def get_window_name(win_id: Optional[int]) -> Tuple[Optional[str], bool]:
"""Look up the window name for a given X11 window ID"""
if not win_id:
last_seen['title'] = None
return last_seen['title'], True
title_changed = False
with window_obj(win_id) as wobj:
if wobj:
try:
win_title = _get_window_name_inner(wobj)
except XError:
pass
else:
title_changed = (win_title != last_seen['title'])
last_seen['title'] = win_title
return last_seen['title'], title_changed
def handle_xevent(event_handler, event):
if event.type != X.PropertyNotify:
return
changed = False
if event.atom == NET_ACTIVE_WINDOW:
if get_active_window()[1]:
get_window_name(last_seen['xid'])
changed = True
elif event.atom in (NET_WM_NAME, WM_NAME):
changed = changed or get_window_name(last_seen['xid'])[1]
if changed:
handle_change(event_handler, last_seen)
def handle_change(event_handler, new_state: dict):
event_handler.handle_change(new_state['title'])
return
def signal_handler(sig, frame):
print('Got signal', sig)
event_handler.handle_termination()
sys.exit(0)
def init_eventhandler():
config = load_config()
event_handler = EventHandler(config["INTERVAL"], config["DATABASE_FILE"])
return event_handler
if __name__ == '__main__':
signal.signal(signal.SIGINT, signal_handler)
print("Current deamon pid:", os.getpid())
event_handler = init_eventhandler()
root.change_attributes(event_mask=X.PropertyChangeMask)
get_window_name(get_active_window()[0])
handle_change(event_handler, last_seen)
while True:
handle_xevent(event_handler, disp.next_event())
\ No newline at end of file
from curses.ascii import isalnum
import json
def extract_app_name(title):
if not title:
return ""
full_title_list = title.split()
i = len(full_title_list)-1
app_name = ""
while i >= 0 and len(full_title_list[i]) != 1 and full_title_list[i].isalnum():
app_name = full_title_list[i] + " " + app_name
i -= 1
return app_name
def load_config():
f = open('config.json')
config = json.load(f)
f.close()
return config
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment