The officially official Devuan Forum!

You are not logged in.

#1 2019-03-24 03:15:25

GNUser
Member
Registered: 2017-03-16
Posts: 482  

customizable Unicode international keyboard

I like using my own custom "hotstrings" for invoking Unicode characters (e.g., cx for ĉ, a' for á, c,, for ç) so I decided to create a python3 script that does this easily for me. Thought I'd post my solution here in case it is useful to anyone.

To use the script you need to install a few dependencies (some packages and two python3 modules). Run these two commands in a terminal to get all the dependencies:

$ sudo apt install python3 python3-pip x11-xserver-utils procps libnotify-bin xclip yad
$ pip3 install pynput Xlib --user

After that, all you need is the script and a config file.

Here is the script:

#!/usr/bin/env python3

# klavaro 2.0
# Copyright (c) 2019 Bruno "GNUser" Dantas <klavaro@dantas.airpost.net>
# This is free software, released under the terms of the ISC license:
# Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby
# granted, provided that the above copyright notice and this permission notice appear in all copies.
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE 
# FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 
# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 

from pynput import keyboard
from pynput import mouse
keyboard0 = keyboard.Controller()
mouse0 = mouse.Controller()
import os
import sys
import time

def log_it(keystroke):
	"""High level function that logs keystroke, checks if last keystrokes 
	match a hotsring/trigger, replaces trigger with corresponding Unicode character"""
	global mappings
	global last_strokes
	del last_strokes[0]
	last_strokes.append(keystroke)
	last_strokes_str = ''.join(last_strokes)
	#print("last strokes = " + last_strokes_str) # for debugging
	for trigger in mappings.keys():
		if last_strokes_str.endswith(trigger):
			character = mappings[trigger]
			backspaces = len(trigger)
			#print("replacing " + trigger + " with " + character) # for debugging
			while backspaces > 0:
				keyboard0.press(keyboard.Key.backspace)
				keyboard0.release(keyboard.Key.backspace)
				backspaces -= 1
			os.system('''xclip -o -selection clipboard | xclip -selection secondary''') # save clipboard contents to secondary
			os.system('''printf %s | xclip -selection clipboard''' % character) # feed unicode character to clipboard
			keyboard0.press(keyboard.Key.ctrl) # press Ctrl+v to paste from clipboard
			keyboard0.press('v')
			keyboard0.release('v')
			keyboard0.release(keyboard.Key.ctrl)
			time.sleep(0.5) # don't restore clipboard too quickly (some applications, e.g. qterminal, need a moment to react to Ctrl+v)
			os.system('''xclip -o -selection secondary | xclip -selection clipboard''') # restore clipboard contents from secondary

def on_press(key): 
	"""Keylogger runs this function on each keyboard event"""
	global capslock_in_effect
	output = str(key)
	if output == 'Key.caps_lock': # if capslock pressed, toggle capslock state
		capslock_in_effect = not capslock_in_effect
	if not output.startswith('Key'): # pynput calls special keys 'Key.foo'
		output = output[1:-1] # remove quotes around alphanumeric key names
		if capslock_in_effect:
			output = output.swapcase() 
			# .swapcase and not .upper because pressing Shift during CapsLock should log a lowercase letter
	if not output.startswith('Key.shift'): # log all keystrokes except shift (e.g., so that a~ is logged without an intervening Shift)
		if output.startswith('Key.'): # log special keystrokes as a space
			output=' '
		log_it(output)

def on_click(x, y, button, pressed):
	"""Keylogger runs this function on each mouse event"""
	button = str(button)
	if pressed and button == 'Button.left': # log left clicks as a space
		log_it(' ')

def start():
	# 1. Build mappings from klavaro.conf
	global mappings
	mappings = {}
	abspath=os.path.realpath(sys.argv[0])
	dirname=os.path.dirname(abspath)

	with open('%s/klavaro.conf' % dirname) as f:
		for line in f:
			(trigger, character) = line.split()
			mappings[trigger] = character

	# 2. Initialize an empty list to hold last few keystrokes
	global last_strokes
	last_strokes = [ '' ] * 5 
	
	# 3. Create taskbar icon
	os.system('''yad --notification --image=keyboard --text="Klavaro" --no-middle --menu="Exit!klavaro stop!application-exit" --listen & echo $! >/tmp/klavaro-icon-pid''')
	
	# 4. Get initial CapsLock state
	global capslock_in_effect
	exit_code = os.system('''xset q | grep -q 'Caps Lock:[[:space:]]*on' ''')
	if exit_code == 0:
		capslock_in_effect = True
	else:
		capslock_in_effect = False

	# 5. Finally, start keylogger
	with mouse.Listener(on_click=on_click) as listener:
		with keyboard.Listener(on_press=on_press) as listener:
			listener.join()
	
#####

def stop():
	os.system('''kill $(cat /tmp/klavaro-icon-pid); rm /tmp/klavaro-icon-pid''')
	os.system('''pkill -f klavaro''')

#####

if sys.argv[1] == "start":
	try:
		os.stat('/tmp/klavaro-icon-pid')
	except: 
		start()
	else: 
		os.system('''notify-send -i dialog-warning -t 2000 "Klavaro" "Klavaro is already running"''')
		sys.exit()

elif sys.argv[1] == "stop":
	stop()

Here is my config file, which lists my hotstrings and corresponding Unicode characters (modify* it to suit your needs):

A` À
a` à
A' Á
a' á
A^ Â
a^ â
A~ Ã
a~ ã
A:: Ä
a:: ä
E` È
e` è
E' É
e' é
E^ Ê
e^ ê
E~ Ẽ
e~ ẽ
E:: Ë
e:: ë
I` Ì
i` ì
I' Í
i' í
I^ Î
i^ î
I~ Ĩ
i~ ĩ
I:: Ï
i:: ï
O` Ò
o` ò
O' Ó
o' ó
O^ Ô
o^ ô
O~ Õ
o~ õ
O:: Ö
o:: ö
U` Ù
u` ù
U' Ú
u' ú
U^ Û
u^ û
U~ Ũ
u~ ũ
U:: Ü
u:: ü
C,, Ç
c,, ç
Cx Ĉ
CX Ĉ
cx ĉ
Gx Ĝ
GX Ĝ
gx ĝ
Hx Ĥ
HX Ĥ
hx ĥ
Jx Ĵ
JX Ĵ
jx ĵ
Sx Ŝ
SX Ŝ
sx ŝ
Ux Ŭ
UX Ŭ
ux ŭ

To use the script, save it as klavaro (it means "keyboard" in Esperanto), make it executable, and put it somewhere in your PATH. Save the config file as klavaro.conf and put it in the same directory as the script.

To start the script:

$ klavaro start &

To end the script:

$ klavaro stop

While the script is running, hotstrings will magically be converted to their corresponding Unicode character in any application that supports Control+v to paste from clipboard. (For the magic to happen in a terminal emulator, you may need to go to emulator's settings and change the paste shortcut from Shift+Control+v to Control+v.)

The script is tested and working (hard and on a daily basis) on Devuan ASCII and OpenBSD 6.4, but should work on any Unix-like OS. I love this script so much, I wish I had cooked it up years ago. Hopefully others will also find it useful smile

-------------------------------

How it works
Script waits for you to type one of the hotstrings. When you do, the script very quickly does the following:
1. Presses appropriate number of backspaces to erase the hotstring
2. Saves current clipboard** contents
3. Puts Unicode character in clipboard
4. Pastes Unicode character at cursor position
5. Restores clipboard contents

-------------------------------

Footnotes

* Adding new Unicode characters to klavaro.conf may seem like a "chicken and the egg" problem, but it isn't hard. First, find the code point for the character you want here (for example, the code for ĉ is 0109). Then, open klavaro.conf in a GUI text editor that supports the Shift+Control+u method of inserting Unicode characters (pluma, gedit, and geany support this, among many others). For my example, pressing Shift+Control+u, then typing 0109, then pressing Enter creates ĉ. Easy peasy!

** Using the clipboard was a workaround due to the fact that all the "typing" utilities I explored (xdotool, xvkbd, pynput) either had no Unicode support or only partial support. The X clipboard, on the other hand, can handle everything.

Last edited by GNUser (2019-03-25 12:34:30)

Offline

#2 2019-03-24 11:05:23

GNUser
Member
Registered: 2017-03-16
Posts: 482  

Re: customizable Unicode international keyboard

I forgot to mention that the scripts assumes you are using a UTF-8 locale. The script just won't work if it finds itself in a non-UTF-8 environment. Here is a quick tutorial on locales:

To see your current locale, run locale

To see locales that are ready to use on your machine, run locale -a

If you want to switch to a locale that is not ready to use on your machine, you have to generate it:
manually edit /etc/locale.gen and uncomment the locale you want, then run locale-gen as root

To change your locale, put the one you want in /etc/default/locale (for example, LANG=en_US.UTF-8) then reboot.

Last edited by GNUser (2019-03-24 11:19:14)

Offline

Board footer