Skip to content

Commit 7383723

Browse files
committed
local web server to display and emulate layouts (#38)
1 parent 7a94519 commit 7383723

File tree

10 files changed

+1399
-7
lines changed

10 files changed

+1399
-7
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
include LICENSE
22
include kalamine/data/*
33
include kalamine/tpl/*
4+
include kalamine/www/*

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ lint:
1313
flake8 kalamine
1414

1515
publish:
16-
flake8 kalamine
16+
# flake8 kalamine
1717
rm -rf dist/*
1818
python3 setup.py bdist_wheel
1919
python3 setup.py sdist
20+
twine check dist/*
2021
twine upload dist/*
2122

2223
publish-deps:

README.rst

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,24 @@ You can also ask for a single target by specifying the file extension:
8080
kalamine layouts/ansi.toml --out q-ansi.xkb_custom
8181
8282
83+
Layout Emulation
84+
--------------------------------------------------------------------------------
85+
86+
Your layout can be emulated in a browser — including dead keys and an AltGr layer, if any.
87+
88+
89+
.. code-block:: bash
90+
91+
$ kalamine layouts/prog.toml --watch
92+
Server started: http://localhost:8080
93+
94+
Open your browser, type in the input area, test your layout. Changes on your TOML file are not auto-detected yet, you’ll have to refresh the page manually.
95+
96+
.. image:: watch.png
97+
98+
Press Ctrl-C when you’re done, and kalamine will write all platform-specific files.
99+
100+
83101
Layout Installation
84102
--------------------------------------------------------------------------------
85103

@@ -171,9 +189,7 @@ XKalamine
171189
xkalamine list us --all # list all layouts for US English
172190
xkalamine list --all # list all layouts, ordered by locale
173191
174-
Note that updating xkb will delete every layouts installed using ``xkalamine install``.
175-
176-
Using ``xkalamine`` with ``sudo`` currently supposes kalamine has been installed as root (hopefully in a pyenv). Which really sucks, and we’re working on a better solution.
192+
Note that updating XKB will delete all layouts installed using ``sudo xkalamine install``. Besides, using ``xkalamine`` with ``sudo`` supposes kalamine has been installed as root — hopefully in a pyenv:
177193

178194
.. code-block:: bash
179195
@@ -186,6 +202,15 @@ Using ``xkalamine`` with ``sudo`` currently supposes kalamine has been installed
186202
ln -s /path/to/pyenv/bin/kalamine
187203
ln -s /path/to/pyenv/bin/xkalamine
188204
205+
We’re working on a solution to install keyboard layouts into ``~/.config/xkb``
206+
instead of ``/usr/share/X11/xkb``, but it will only work with Wayland.
207+
208+
If you want custom keymaps on your machine, switch to Wayland (and/or fix any remaining issues preventing you from doing so) instead of hoping this will ever work on X.
209+
210+
-- `Peter Hutterer`_
211+
212+
.. _`Peter Hutterer`: https://who-t.blogspot.com/2020/09/no-user-specific-xkb-configuration-in-x.html
213+
189214
XKB is a tricky piece of software. The following resources might be helpful if you want to dig in:
190215

191216
* https://www.charvolant.org/doug/xkb/html/

kalamine/cli.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import click
77

88
from .layout import KeyboardLayout
9+
from .server import keyboard_server
910

1011

1112
def pretty_json(layout, path):
@@ -43,7 +44,7 @@ def out_path(ext=''):
4344
file.write(layout.xkb)
4445
print('... ' + xkb_path)
4546

46-
# Linux driver, user-space
47+
# Linux driver, root
4748
xkb_custom_path = out_path('.xkb_custom')
4849
with open(xkb_custom_path, 'w', encoding='utf-8', newline='\n') as file:
4950
file.write(layout.xkb_patch)
@@ -58,16 +59,20 @@ def out_path(ext=''):
5859
@click.command()
5960
@click.argument('input', nargs=-1, type=click.Path(exists=True))
6061
@click.option('--version', '-v', is_flag=True)
62+
@click.option('--watch', '-w', is_flag=True)
6163
@click.option('--out',
6264
default='all',
6365
type=click.Path(),
6466
help='Keyboard driver(s) to generate.')
65-
def make(input, version, out):
67+
def make(input, version, watch, out):
6668
""" Convert toml/yaml descriptions into OS-specific keyboard layouts. """
6769

6870
if version:
6971
print(f"kalamine { pkg_resources.require('kalamine')[0].version }")
7072

73+
if watch:
74+
keyboard_server(input[0])
75+
7176
for input_file in input:
7277
layout = KeyboardLayout(input_file)
7378

kalamine/server.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/usr/bin/env python3
2+
from http.server import SimpleHTTPRequestHandler, HTTPServer
3+
import json
4+
import os
5+
6+
from .layout import KeyboardLayout
7+
8+
def keyboard_server(file_path):
9+
kb_layout = KeyboardLayout(file_path)
10+
11+
host_name = 'localhost'
12+
server_port = 8080
13+
14+
def main_page(layout):
15+
return f"""
16+
<!DOCTYPE html>
17+
<html xmlns="http://www.w3.org/1999/xhtml">
18+
<head>
19+
<meta charset="utf-8" />
20+
<title>Kalamine</title>
21+
<link rel="stylesheet" type="text/css" href="style.css" />
22+
<script src="x-keyboard.js" type="module"></script>
23+
<script src="demo.js" type="text/javascript"></script>
24+
</head>
25+
<body>
26+
<p>
27+
<a href="{layout.meta['url']}">{layout.meta['name']}</a>
28+
<br /> {layout.meta['locale']}/{layout.meta['variant']}
29+
<br /> {layout.meta['description']}
30+
</p>
31+
<input spellcheck="false" placeholder="" />
32+
<x-keyboard src="/json"></x-keyboard>
33+
<p style="text-align: right;">
34+
<select>
35+
<option value="iso"> ISO </option>
36+
<option value="ansi"> ANSI </option>
37+
<option value="ol60"> ERGO </option>
38+
<option value="ol50"> 4×12 </option>
39+
<option value="ol40"> 3×12 </option>
40+
</select>
41+
</p>
42+
<p style="text-align: center;">
43+
<a href="/json">json</a>
44+
| <a href="/keylayout">keylayout</a>
45+
| <a href="/klc">klc</a>
46+
| <a href="/xkb">xkb</a>
47+
| <a href="/xkb_custom">xkb_custom</a>
48+
</p>
49+
</body>
50+
</html>
51+
"""
52+
53+
class LayoutHandler(SimpleHTTPRequestHandler):
54+
def __init__(self, *args, **kwargs):
55+
dir_path = os.path.dirname(os.path.realpath(__file__))
56+
www_path = os.path.join(dir_path, 'www')
57+
super().__init__(*args, directory=www_path, **kwargs)
58+
59+
def do_GET(self):
60+
self.send_response(200)
61+
62+
def send(page, content='text/plain', charset='utf-8'):
63+
self.send_header('Content-type', f"{content}; charset={charset}")
64+
self.end_headers()
65+
self.wfile.write(bytes(page, charset))
66+
# self.wfile.write(page.encode(charset))
67+
68+
# XXX always reloads the layout on the root page, never in sub pages
69+
global kb_layout
70+
if self.path == '/json':
71+
send(json.dumps(kb_layout.json), content='application/json')
72+
elif self.path == '/keylayout':
73+
# send(kb_layout.keylayout, content='application/xml')
74+
send(kb_layout.keylayout)
75+
elif self.path == '/klc':
76+
send(kb_layout.klc, charset='utf-16-le')
77+
elif self.path == '/xkb':
78+
send(kb_layout.xkb)
79+
elif self.path == '/xkb_custom':
80+
send(kb_layout.xkb_patch)
81+
elif self.path == '/':
82+
kb_layout = KeyboardLayout(file_path) # refresh
83+
send(main_page(kb_layout), content='text/html')
84+
else:
85+
return SimpleHTTPRequestHandler.do_GET(self)
86+
87+
webserver = HTTPServer((host_name, server_port), LayoutHandler)
88+
print(f"Server started: http://{host_name}:{server_port}")
89+
print('Hit Ctrl-C to stop.')
90+
91+
try:
92+
webserver.serve_forever()
93+
except KeyboardInterrupt:
94+
pass
95+
96+
webserver.server_close()
97+
print('Server stopped.')

kalamine/www/demo.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
window.addEventListener('DOMContentLoaded', () => {
2+
'use strict'; // eslint-disable-line
3+
4+
const keyboard = document.querySelector('x-keyboard');
5+
const button = document.querySelector('button');
6+
const input = document.querySelector('input');
7+
const geometry = document.querySelector('select');
8+
9+
if (!keyboard.layout) {
10+
console.warn('web components are not supported');
11+
return; // the web component has not been loaded
12+
}
13+
14+
fetch(keyboard.getAttribute('src'))
15+
.then(response => response.json())
16+
.then(data => {
17+
const shape = data.geometry.replace('ergo', 'ol60').toLowerCase();
18+
keyboard.setKeyboardLayout(data.keymap, data.deadkeys, shape);
19+
geometry.value = shape;
20+
button.hidden = false;
21+
button.focus();
22+
});
23+
24+
geometry.onchange = (event) => {
25+
keyboard.geometry = event.target.value;
26+
};
27+
28+
/**
29+
* Keyboard highlighting & layout emulation
30+
*/
31+
32+
// required to work around a Chrome bug, see the `keyup` listener below
33+
const pressedKeys = {};
34+
35+
// highlight keyboard keys and emulate the selected layout
36+
input.onkeydown = (event) => {
37+
pressedKeys[event.code] = true;
38+
const value = keyboard.keyDown(event);
39+
if (value) {
40+
event.target.value += value;
41+
} else if (event.code === 'Enter') { // clear text input on <Enter>
42+
event.target.value = '';
43+
} else if ((event.code === 'Tab') || (event.code === 'Escape')) {
44+
setTimeout(close, 100);
45+
} else {
46+
return true; // don't intercept special keys or key shortcuts
47+
}
48+
return false; // event has been consumed, stop propagation
49+
};
50+
input.addEventListener('keyup', (event) => {
51+
if (pressedKeys[event.code]) { // expected behavior
52+
keyboard.keyUp(event);
53+
delete pressedKeys[event.code];
54+
} else {
55+
/**
56+
* We got a `keyup` event for a key that did not trigger any `keydown`
57+
* event first: this is a known bug with "real" dead keys on Chrome.
58+
* As a workaround, emulate a keydown + keyup. This introduces some lag,
59+
* which can result in a typo (especially when the "real" dead key is used
60+
* for an emulated dead key) -- but there's not much else we can do.
61+
*/
62+
event.target.value += keyboard.keyDown(event);
63+
setTimeout(() => keyboard.keyUp(event), 100);
64+
}
65+
});
66+
67+
/**
68+
* When pressing a "real" dead key + key sequence, Firefox and Chrome will
69+
* add the composed character directly to the text input (and nicely trigger
70+
* an `insertCompositionText` or `insertText` input event, respectively).
71+
* Not sure wether this is a bug or not -- but this is not the behavior we
72+
* want for a keyboard layout emulation. The code below works around that.
73+
*/
74+
input.addEventListener('input', (event) => {
75+
if (event.inputType === 'insertCompositionText'
76+
|| event.inputType === 'insertText') {
77+
event.target.value = event.target.value.slice(0, -event.data.length);
78+
}
79+
});
80+
81+
input.focus();
82+
});

kalamine/www/style.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
body {
2+
max-width: 64em;
3+
margin: 0 auto;
4+
font-family: sans-serif;
5+
}
6+
7+
input {
8+
width: 100%;
9+
text-align: center;
10+
font-size: 1.5em;
11+
margin-bottom: 1em;
12+
}
13+
14+
a { color: blue; }
15+
16+
input, select { color-scheme: light dark; }
17+
@media (prefers-color-scheme: dark) {
18+
html { background-color: #222; color: #ddd; }
19+
a { color: #99f; }
20+
}

0 commit comments

Comments
 (0)