I want to show and tell about a small program that has benefited.
Once at work a friend wrote to me. Our dialogue took about the following:
- Hi, I'm learning blind typing. The fact is that on Linux there is no program that could help me. In general, maybe you can quickly write this?
So how to help a friend is a holy thing, and the task looked interesting, I agreed to help.
')
The result is this:

Who cares, details below
A warningI don’t claim to be a python guru, so the code (and I’m pretty sure of it) has a microscope nailing and other absurdities.
Together with a friend, we set the task:
The principle of its operation of the program is the following - after launching a window appears on the desktop with a schematic image of the keyboard. When pressing buttons on a real keyboard, on a schematic diagram, pressed buttons are pressed.
It helps to develop a reflex to look at the monitor while typing on the keyboard.
Main program requirements:
- Implement it very quickly;
- Show keystrokes in real time;
- "Switch layouts" in the window when switching the keyboard language;
- To configure the program through a text configuration file.
In the process of writing also added:
- The "sticking" mode of the last key pressed (it helps to figure out where to press with your fingers further);
- Work with shift key;
- The ability to mark the color of positions for the fingers on the keyboard;
- The ability to customize the font;
- Ability to customize the width of the buttons;
- Automatic hiding the contents of the window when you hover the mouse.
At the time of the program's appearance, there was already experience with Tkinter, working with several threads. Plus, by the nature of the activity you have to be a sysadmin, so working with the command line was not alien.
General description of the program internals:
To read the keys, use the bash line found in google, which allows you to read the keys pressed on the keyboard via the xinput utility. This method is selected for the sake of clause 1 of the requirements. The process of reading characters starts in a separate thread. Reading of the layout language is also implemented (again point 1). Information on the pressed buttons is issued in a queue. Working with the queue in the main window of the program is done by periodically calling the function periodicCall. Thus two streams are written to the queue, one stream is reading.
Program termination is performed in a peculiar way - through status variables in the streams.
Work with program settings
Program settings are loaded and stored in an instance of the ConfigManager class. Reading from the main text file settings is done using
ConfigParser . This module allows you to use a format similar to INI configuration files. The class constructor checks the existence of the configuration file located along the path "~ / .key_trainer / program.conf". If not, the program reads the program.conf file located in the current program folder.
Some codeimport os ... filename='program.conf' home = os.path.expanduser("~") if os.path.isfile(home+'/.key_trainer/'+filename): filename=home+'/.key_trainer/'+filename ...
ConfigParser is a great module. You can consider the names of all sections, and also consider the keys with their values ​​inside the sections as tuples. So, for example, the reading of section names, and keys in the section “KEYBOARD SETTINGS” is implemented.
Some more code from ConfigParser import RawParser ... myParser=RawConfigParser(allow_no_value=True) myParser.read(path_to_file)
In addition to the main configuration file, there is a second equally important one - “keyboard.conf”. It is used to customize the displayed buttons, namely the button code, the text on the button (with shifta and layouts), the position of the button. By removing / adding entries to this file, you can change the quantity and quality of buttons (and lines with buttons) in the main program window.
Format of entries in keyboard.confThe file contains entries in the form:
[button code]: "[lowercase character in English layout], [uppercase character in English layout], [lowercase character in Russian layout], [uppercase character in Russian layout]": [button line number], [button column number]
Here are a few entries for example:
24: “q, Q, D, Y”: 3.2
25: "W, W, C, C": 3.3
26: "e, E, Y, Y": 3.4
27: "r, r, k, k": 3.5
Reading characters from the keyboard
To read characters, the KeyboardStatus class is written, which takes the configuration class as an input parameter (see above). Inside this class is encapsulated a thread-safe queue
queue .
Reading characters from the keyboard is produced in two streams. Why two - because in practice it turned out to be easier. One stream reads the keyboard layout, the second one is pressed. Both threads are generated through
Thread , in each thread then the corresponding process of reading keys or layouts will be started through the
subprocess Popen . To read the process output stream,
subprocess.PIPE is used. As soon as the text arrives at the exit stream of the process, it is read, processed, and, if necessary, queued:
Code from subprocess import Popen from subprocess import PIPE import threading ... def doReadingKeys(self): self.myProcess=Popen('xinput list '+'|'+' grep -Po \'id=\K\d+(?=.*slave\s*keyboard)\' '+'|'+' xargs -P0 -n1 xinput test',shell=True,stdout=PIPE) while self.proc_started: symbol=self.myProcess.stdout.read(1) if symbol in press_release_dict: symbol_pressed=press_release_dict[symbol] while symbol!='\n': symbol=self.myProcess.stdout.read(1) if symbol.isdigit(): symbol_index=symbol_index*10+int(symbol) self.myQueue.put((symbol_index,symbol_pressed)) symbol_index=0 ... keysThread=threading.Thread(target=self.doReadingKeys) keysThread.start() ...
To terminate the thread, use the proc_started class variable. When the main program window is closed, it is set to False, the reading cycle is terminated, the key reading process is terminate through terminate, and then wait — to wait until the process has completed.
CommentThis approach has one drawback - unlocking (and hence further termination of the thread and process) of the read method, which will not occur inside the loop until something is considered from the output flow of the myProcess process. But in practice, problems did not arise because of this, since we often press buttons.
Graphical interface
In order to quickly make a graphical interface used
Tkinter . This module makes it easy to work with simple graphical interfaces (windows, buttons, checkboxes, etc.). The GuiManager window class at the input, among other parameters, accepts a configuration class. From it, the settings of the buttons are taken, then these buttons are created and added to the main program window.
Button add code from Tkinter import * import tkFont ... self.buttonFont=tkFont.Font(family=config.font_name,size=config.font_size) self.boldUnderscoredButtonFont=tkFont.Font(family=config.font_name,size=config.font_size,weight='bold',underline=1) for row_index in xrange(1,config.getNumOfRows()+1): self.gui_rows[int(row_index)]=Frame(master) self.gui_row_buttons[int(row_index)]=[] for button_num in xrange(1,config.getNumOfKeysInRow(row_index)+1): newButton=Button(self.gui_rows[int(row_index)]) if self.config.padx!=-1: newButton.config(padx=self.config.padx) if self.config.pady!=-1: newButton.config(pady=self.config.pady) if (row_index,int(button_num)) in config.key_pos_to_index: self.gui_all_buttons[config.key_pos_to_index[(row_index,int(button_num))]] = newButton self.gui_row_buttons[int(row_index)].append(newButton) newButton.pack(side=LEFT) self.gui_rows[int(row_index)].pack() self.reconfigure_text_on_buttons(config,shift_pressed=0,lang=0) ...
When adding buttons to a form, dictionaries are created along the way with keys of the line number and values ​​— a
Frame object in each of which buttons are placed. As can be seen from the code, the buttons are formed line by line, upon completion of the formation of the line the widget is put into the window using the pack () method.
Among other things, the processQueue function has been added to the class, which from the flow of the graphical interface gets tuples from the queue with events of pressed buttons and changes the appearance of the buttons - “presses” them, “switches layouts” and “presses” the shift button:
Processing the queue by the GUI def processQueue(self): while not self.queue.empty(): msg = self.queue.get(0) if msg[0] == -1:
The GuiManager class is encapsulated inside the ThreadedClient class, which accepts the Tkinter main thread as input and places a call to the queue parsing function every 20 milliseconds:
GuiManager Encapsulating Class class ThreadedClient: def __init__(self, master): self.master = master self.config=ConfigManager() self.keyTrainer=keyboardStatus(self.config) keyTrainer=self.keyTrainer master.protocol('WM_DELETE_WINDOW', self.kill_and_destroy) self.guiManager=GuiManager(master,self.config,keyTrainer.myQueue,keyTrainer) keyTrainer.begin_scan() self.running = 1 self.periodicCall() def kill_and_destroy(self): self.running = 0 self.keyTrainer.stop_scan() if self.config.debug: print "Stopping scan..." self.master.destroy() def periodicCall(self): self.guiManager.processQueue() if not self.running:
Some pictures
General view of the program window:

Left Alt key pressed:

Program window after reconfiguration:

When you hover the mouse cursor, the program window “leaves” under the title (colors that remain on a white background are video compression artifacts):

Pressing the shift key and switching the language:

Conclusion
What was the result? And it turned out a good program to help people learn to type blindly on the keyboard. Yes, she has flaws and inefficiencies, namely:
- Running processes from the side with bash commands for reading characters;
- Hard-coded languages ​​(only Russian and English);
- Square interface;
- Works on Ubuntu and Linux Mint (MATE), has not been tested on other distributions;
The code can be downloaded / viewed here:
Link to bitbucketThe program requires python 2.7 and Tkinter. To install the latter, run the command:
sudo apt-get install python-tk
The program is launched using the Start.sh script from the program directory.
Thanks for attention!
PS Received the question: how long did it take to write the program? Time was spent in the total amount of 6-8 hours, after the first three there was active testing and all details were finished.
UPD: removed try / except from queue processing by the GUI