xwintrack Footswitch X Window tracker
Version 1.4.0
A python 3 Xwindow tool.
Keytracker, mousetracker, movement tracker
Allows tracking of keys, mouse buttons and mouse position in a selected
X window or globally.
Performs many of xdotool's functions
Other than Xlib requires: xkbcomp to build keyboard dictionaries
It performs no logging of its own, it tracks and reports what is occurring,
allowing the programmer to do want they will with the information.
Originally for use with the transcription tool footswitch2,
to track transcription entry in LibreOffice, to maintain a
timestamped copy of the text.
This enables the transcriber to search for when a phrase was uttered
within the audio and jump to that position.
Mouse and movement tracking retained and enhanced, solely because it may be
useful to others.
Inspiration from:
pyxhook Copyright (C) 2008 Tim Alexander <dragonfyre13@gmail.com>
pyxhook extensively modified by Daniel Folkinshteyn <nanotube@users.sf.net>
A hat tip to python-xlib/examples/record_demo.py Copyright (C) 2000-2002 Peter Liljenberg
Functionality of xdotool Copyright (c) 2007, 2008, 2009: Jordan Sissel.
#
xwintrack Copyright (C) 2020 James Healey <rolfofsaxony@gmx.com>
#
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.
#
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
xwintrack developed and Tested with python-xlib version 0.20-3
Tested solely on Linux (Mint/Ubuntu and Manjaro) python 3.6.9
Enhancements:
Most code re-written, leaving core pieces from the Xlib examples in place
and following the structure of pyxhook
Use event state bitwise tests to determine keyboard and mouse modifiers.
Cope with Num_Lock accommodating numeric pad keys.
Cope with Caps_Lock.
Allow for Alt Gr options and other modifiers and combinations
Key Action Down, Held, Up
Mouse Action Down, Up, Scroll
Mouse co-ordinates retrieved from the mouse button event
Mouse event reports if a modifier key was active
Mouse event reports if another mouse button was active
Mouse event reports combination of mouse button and Key modifiers
providing a powerful tool for user written Mouse based HotKey functions
Ability to track events for a single window, rather than all windows
Limit reporting to Currently open Windows or allow All
Limit overheads by allowing selection of event types:
keys only, mouse actions, keys and mouse buttons, movement only, all events
Ability to specify/limit key characters reported on
350 x speed up of key symbol look-up
Named threads
Call event function with lambdas for *args or **kwargs
Numerous example programs
NB: If logging key characters into a file or database, ensure that
you test for returned 'Null' characters, which can cause problems.
Test for the event.Char variable equal to '\x00' and discard it
(One entire day wasted discovering why an sqlite3 Db record
was stored as a Blob instead of Text - Null characters!)
Stand-alone Functions:
General Window Queries/Actions
Find a window, based on its title
Find a window, based on its class/program
Find a window, based on text anywhere in Title/class or Id
Find or Wait for a window to open if not currently available
Get a list of current windows
Get details of a specific window by specified Id
Select a window from the desktop
Change the cursor to a green dotbox to signify something (like Select Window)
Change the cursor back to a normal pointer
Raise a specific window
Test if a window is still open
Get pointer position
Get screen geometry
Get active modifiers
Move pointer
Drag pointer
Relative pointer move
Press mouse button
Release mouse button
Mouse button press & release
Double Click mouse button
Mouse button drag
Key entry with/without modifiers
Text entry
Text entry from file
Run a program
TrackManager Usage:
There are only 3 classes,
TrackManager, the main class to instantiate
trackkeyevent, which returns details of a key event
trackmouseevent, which returns details of a mouse event.
Create one or more Trackmanager's and then declare the event/s callback function/s
you are interested in.
There are 5 functions that can be declared.
KeyDown : The function to execute when a key is pressed.
Returns the trackkeyevent class.
KeyUp : The function to execute when a key is released.
Returns the trackkeyevent class.
MouseButtonDown :
The function to execute when a mouse button is pressed.
Returns the trackmouseevent class.
MouseButtonUp :
The function to execute when a mouse button is released.
Returns the trackmouseevent class.
MouseMovement :
The function to execute when a mouse moves.
Returns the trackmouseevent class.
The callback is your function to interpret the event and perform whatever is necessary.
The callback can be a lambda function, if you need to pass other variables.
You can define the lambda function to use *args (see example XwinExAdv.py) or **kwargs
(see example XwinExAdv2.py)
tm = xwintrack.TrackManager(winid=0,restricted_to_characters=[],action_required="all",tm_name="")
tm.KeyDown = kbevent
Start tracking
tm.start()
Cancel tracking
if tm.is_alive():
tm.cancel()
The TrackManager has a variable IsRunning and a function is_running()
This can be tested periodically to test if the watched window is still open
and the trackmanager still active.
If you are not incorporating this code into a Gui, you will need to establish some
form of loop, to keep this all running. Again see examples.
If you are running Gui, test the is_running() function periodically,
in case you need to run the cancel() function and perform a join() operation
for example:
if self.monitor_listener:
if not self.monitor_listener.is_running():
self.monitor_listener.cancel()
self.monitor_listener.join(0.1)
self.monitor_listener = False
The underlying Xlib thread has the usual "is_alive" function.
class TrackManager(threading.Thread):
""" Instantiate the class and hand it a function
An event is returned when it occurs
The track*event class that is returned, where * is key, or mouse, is as follows:
key event : Window, WindowName, ProcName, Key, Ascii, Char, ScanCode, TextMessage
Window Handle: Window id number
Window Name: Window Text Name
Proc Name: Name of process using window
Key Pressed: Key
Ascii Value: Ascii Value
Char: Character if applicable
ScanCode: ScanCode of key (just in case)
Modifier: Numeric value indicating Active modifiers
Action: Numeric value indicating Down (0), Held (1), Up (2)
TextMessage: Description of event
mouse event : Window, WindowName, ProcName, Position, Button, Modifier, TextMessage
Window Handle: Window id number
Window Name: Window Text Name
Proc Name: Name of process using window
Position: X, Y Position of mouse pointer
Button: Button number
Modifier: Numeric value indicating Active modifiers
Action: Numeric value indicating Down (0), Up (2)
TextMessage: Description of event
Functions: note Down and Up are used to avoid confusion with internal X Press and X Release
KeyDown : The function to execute when a key is pressed.
Returns the trackkeyevent class.
KeyUp : The function to execute when a key is released.
Returns the trackkeyevent class.
MouseButtonDown :
The function to execute when a mouse button is pressed.
Returns the trackmouseevent class.
MouseButtonUp :
The function to execute when a mouse button is released.
Returns the trackmouseevent class.
MouseMovement :
The function to execute when a mouse moves.
Returns the trackmouseevent class.
Optional parameters:
winid : The Xwindow id that you wish to track, ignoring all input
from others.
The default is 0, which listens to All device input
The source is identifiable via event Window, WindowName, or ProcName
restricted_to_characters :
A list [] of keyboard characters that you wish reported, ignoring
characters not in the list
A full set would be:
[' ', '!', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '¡', '¢', '£', '¤', '¥', '¦', '§', '¨', '©', 'ª', '«', '¬', '®', '¯', '°', '±', '²', '³', '´', 'µ', '¶', '·', '¸', '¹', 'º', '»', '¼', '½', '¾', '¿', 'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß', 'à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ð', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', '÷', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ', '€']
A more specialised value might be ['.','?','!'] i.e. punctuation
which marks the end of a sentence.
An empty list is the default, which sends back everything.
action_required : Default "keys"
If you only require limited event types, receiving notifications for
events you don't want is a waste of resources.
Use this parameter to define the events reported.
"keys" - keys only
"mouse" - mouse buttons only
"km" - keys and mouse buttons only
"move" - mouse pointer only
"all" - all device events, keys, mouse buttons and pointer movement
"select" - mouse button press only.
If tracking a single window, Mouse motion is reported anywhere on the display
when the "watched" window has Focus
Mouse motion reports not only the mouse position but also which, if any,
mouse buttons are held down, allowing for Drag options to be calculated.
The "select" option is used internally, it populates 2 variables within TrackManager:
select_button and select_win, they denote the mouse button pressed and standard
dictionary of the window attributes that the Click happened on.
If you choose to use "select" directly rather than the xwindowselect() function,
you decide which Button press means what.
sub_windows : Default False
Set to True, Sub Windows opened within a "watched" window (see winid), will also
send Key and Mouse events.
tm_name : An identifying name for this occurrence of TrackManager,
: in case you are running more than one.
: Otherwise the thread is identified by its number e.g. Thread-2
Modifiers:
Modifiers are accumlative, so if shift and ctrl are depressed when another key
is pressed, the modifier value will be the sum of two values.
i.e.
# 0 - standard # no Modifier
# 1 - Shift
# 4 - Ctrl
# 5 - Ctrl + Shift
# 8 - Alt_Left
# 64 - Super_L and Super_R (Win/Menu keys)
# 128 - Alt_Right
# 129 - Alt_R + Shift
# ...
Note: While caps_lock and num_lock are available and xwintrack uses them
internally, it does not specifically report them.
It gives more modifiers to check for little benefit.
With key release (Up) X reports a modifier key as its own modifier.
xwintrack deducts it in an attempt at sanity.
Modifiers for mouse buttons:
# 0 - standard # no Modifier
# 1 - Shift
# 4 - Ctrl
# 5 - Ctrl + Shift
# 8 - Alt_Left
# 9 - Alt_Left + Shift
# 12 - Alt_Left + Ctrl
# 13 - Alt_Left + Shift + Ctrl
# 64 - Super (L & R)
# 128 - Alt-R
# 256 - Button 1
# 257 - Button 1 + Shift
# ...
# 512 - Button 2
# 1024 - Button 3
# ...
With button release (Up) X reports the button as its own modifier.
xwintrack deducts it in an attempt at sanity.
Button numbers:
1 - Left
2 - Middle
3 - Right
4 - Wheel Up (forward)
5 - Wheel Down (backward)
check the wheel action for pressed or released
0 - pressed
2 - released
Key Actions:
0 - pressed
1 - held
2 - released
See the file Key_names.txt for all available names of keys.
Identifying a Window to watch:
xwintrack
can be used to identify the window you want to watch by setting it up as a one off
to receive a mouse click (function "xwindowselect")
or any of the "find" functions.
(See example programs)
xdotool
can also be used, either requesting a mouse click
(xdotool selectwindow)
or by searching for a named open window
(xdotool search --name "LibreOffice Writer") for example.
Again, see examples.
Check the example programs for usage.
Stand-alone Functions:
Designed to replicate key xdotool functions
* Findclass(window=None, search="", found = []):
''' Find windows matching a class name / program
The window and found variables are populated automatically and should not be set.
Call with search="class I want"
Input : class name
Returns : a list of dictionaries
"handle": Window Id
"class": Program name (class),
"name": window title,
"state": window state, 1 - Visible, 3 - Minimised
'''
*
* Findname(window=None, search="", found = []):
''' Find windows matching a Window name - the search variable
The window and found variables are populated automatically and should not be set.
Call with search="name I want"
Input : window name
Returns : a list of dictionaries
"handle": Window Id
"class": Program name (class),
"name": window title,
"state": window state, 1 - Visible, 3 - Minimised
'''
*
* Find(window=None, search="", found = []):
''' Generic Find windows matching the search variable to something in name, class or id
The window and found variables are populated automatically and should not be set.
Call with search="name I want"
Input : window name
Returns : a list of dictionaries
"handle": Window Id
"class": Program name (class),
"name": window title,
"state": window state, 1 - Visible, 3 - Minimised
'''
*
* SyncFind(search="", wait=120, progress=True, x=0, y=0):
''' A generic Find windows matching the search variable somewhere, that waits for a valid window
Call with search="thing I want" and a maximum time to wait in seconds
Fails with empty list if nothing found with time period
Input : search text
: wait - wait limit in seconds - default 120
: progress - True or False
: True opens a progress window
: x - horizontal position of the progress window
: y - vertical position of the progress window
Returns : a list of dictionaries
"handle": Window Id
"class": Program name (class),
"name": window title,
"state": window state, 1 - Visible, 3 - Minimised
'''
*
xwindowexist(id):
''' Test window exists
When a program like libreoffice has multiple windows open
If the window you are monitoring closes, it isn't reported as dying.
This test allows you to test from your program via a timer, if the
specific window you're working with is still available
Input : Id
Returns : True / False
'''
*
xwindowget(id):
''' return the window
Input : Id
Returns : window or None
'''
*
xwindowquery(id):
''' Get details of a window given its Id number
Input : Id
Returns : a dictionary
"name": window title,
"class": Program name (class),
"handle": window Id,
"state": window state, 1 - Visible, 3 - Minimised
"pid": Process Id
'''
*
xwindowraise(id):
''' Raise given window to focus
Input : Id
Returns : True or False
'''
*
xwindowlist():
''' Get list of windows
Returns : generator of dictionaries
"name": window title,
"class": Program name (class),
"handle": window Id,
"state": window state, 1 - Visible, 3 - Minimised
"pid": Process Id
'''
*
xcursorselect():
''' Change the cursor to a green dotbox to indicate a selection is required
Beware - there is no way back to the original themed cursor - see xcursornormal()
'''
*
xcursornormal():
''' Change the cursor back to White left_ptr to indicate selection finished
Sadly under python there appears to be no way to identify the existing
cursor image, so we're left with returning to a standard X cursor, rather
than the Themed one
'''
*
xwindowselect(wait=120):
''' Allows you to select a visible window with a Left Click
A right or Middle Click cancels the function
Input : wait= n (optional) seconds to wait for a click or timeout (Default 120)
Returns : a single dictionary of the selected window or None
"handle": Window Id
"class": Program name (class),
"name": window title,
"state": window state, 1 - Visible, 3 - Minimised
'''
*
# Window input functions i.e. xwintrack.function()
*
pointerpos():
''' Returns current pointer position - X, Y , Width of Screen, Height of Screen
'''
*
getgeometry():
''' Returns Width, Height of Screen
'''
*
getmodifiers():
''' Returns a single value which represents any and all
current active modifiers
'''
*
pointermove(x, y):
''' Moves the mouse pointer to X, Y
'''
*
pointerdrag(x, y):
''' Move mouse pointer to position x, y, in increments
'''
*
pointerrelmove(x, y):
''' Mouse pointer move position relative to current position
x = +/- from current x
y = +/- from current y
'''
*
typetext(win=None, text="", delay=0.01, raise_window=False, clearmods=False, file=False):
''' Enters the text string into the given window
Input:
win: Window target for text
text: The text to enter
delay: delay in seconds, int or float
raise_window: True or False, put the window on top
clearmods: True/False, Clear any current modifiers and then re-instate them
Shift|Caps_Lock|Control|Alt|Num_Lock
file: True/False - If True the "text" variable is a file name
input will be read from the file.
Note: if you need inverted commas or quotes in the text wrap it in triple quotes
use e.g.text='''your "text" here'''
'''
*
keypress(win=None, key="", delay=0, raise_window=False, mods=[]):
''' Enters key into the given window
Input:
win: Window target for text
key: The key to enter - a character or its name
e.g. a,A,~,space,comma,(, parenleft, F8, etc
delay: delay in seconds, int or float
raise_window: True or False, put the window on top
mods: Modifiers to be applied to the key
[Shift,Control,Alt,AltGr]
A value of 0 - no modifier
A value of 1 - apply modifier
Beware of using modifiers when the character is constructed with modifiers
'''
*
buttonpress(button):
''' Press mouse button
'''
*
buttonrelease(button):
''' Release mouse button
'''
*
buttonclick(button):
''' Press mouse button, then Release button
'''
*
buttondclick(button):
''' Double Click mouse button
'''
*
buttondrag(button,x,y):
''' Press mouse button
Drag to position x,y
Release mouse button
'''
*
exec(command):
''' Execute a command (Linux)
expects a [list] e.g. ['firefox -search "charles V"']
returns the process number of the new process
Restricted to commands that don't require communication.
i.e. Don't expect a list of files from ['ls']
'''
*
*
xwin_keymap.py
This code is run as needed i.e. if the file $HOME/.config/Xwintrack/xwintrack_keys.py'
is missing
*
buildkeys():
''' Builds the $HOME/.config/Xwintrack/xwintrack_keys.py dictionaries
These dictionaries are used to ascertain the keycodes and symbols used with
the active keyboard.
Specifically for the typetext and keypress functions
The code_to_key dict links the X key mnemonic with the key code.
e.g. '9': '<ESC>', '10': '<AE01>', '11': '<AE02>'
The key_mods dict links the mnemonic with the character in each modifier state
mnemonic [Normal, Shift, Alt_R, Shift+Alt_R]
e.g. '<AE05>': ['5', 'percent', 'onehalf', 'threeeighths']
The char_to_key dict links the character name with the mnemonic
e.g. '1': '<AE01>', 'exclam': '<AE01>', 'bar': '<LSGT>'
The char_to_symbolname dict links the printed character to the character name
i.e. ',': 'comma', '©': 'copyright', '¤': 'currency', 'd': 'd'
The char_to_symbolname is derived from latin1 if you wish to use another set
change the import statement or add another.
when using the 'typetext' function:
char -> char_to_symbolname
symbolname -> to char_to_key picks up the mnemonic
mnemonic -> to key_mods picks up the modifiers
Usage is, get the keycode from X, or the character to emulate, lookup the mnemonic,
get the key_mods to work out which modifiers are required to replicate the given
key as a KeyPress.
How I wish, I never started down this track of replicating some of the
xdotool functionality?
The idea is that it will build dictionaries relevant to the current keyboard.
If that changes, delete the existing $HOME/.config/Xwintrack/xwintrack_keys.py file and
a new one will be built.
The location for the file $HOME/.config/Xwintrack/xwintrack_keys.py is to ensure it is user
specific and because, as it is built as needed, it must reside somewhere that can
be written to.
'''