Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
.POSIX:
.PHONY: test test-golden test-integration apply fixtures lint
.PHONY: test test-golden test-integration test-maptool apply fixtures lint layout list-layouts

test: test-golden test-integration
BASE = ABC

test: test-golden test-integration test-maptool

test-golden:
tests/golden.sh

test-integration:
tests/integration.sh

test-maptool:
tests/maptool.sh

# generate a layout map from macOS layout data, e.g.:
# make layout LAYOUT=Ukrainian -> layouts/ukrainian.map
# make layout LAYOUT=Greek OUT=layouts/el.map
list-layouts:
python3 tools/dump_layout.py --list

layout:
@[ -n "$(LAYOUT)" ] || { echo 'usage: make layout LAYOUT=<name from list-layouts> [BASE=ABC] [OUT=layouts/<name>.map]'; exit 1; }
@out='$(OUT)'; \
[ -n "$$out" ] || out="layouts/$$(printf '%s' '$(LAYOUT)' | sed 's/.*\.//' | tr '[:upper:]' '[:lower:]').map"; \
python3 tools/dump_layout.py --map '$(BASE)' '$(LAYOUT)' > "$$out" && echo "wrote $$out"

# apply mirrors to the running tmux server (same as reloading the config)
apply:
scripts/mirror.sh
Expand All @@ -27,4 +44,4 @@ fixtures:
done

lint:
shellcheck langmap.tmux scripts/mirror.sh tests/golden.sh tests/integration.sh
shellcheck langmap.tmux scripts/mirror.sh tests/golden.sh tests/integration.sh tests/maptool.sh
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,33 @@ Reloading your config re-runs the plugin, which is the same thing.

## Adding a layout

On macOS the map file is generated straight from the system layout data
(the tool reads it via the Carbon framework, so this part is macOS only):

```sh
python3 tools/dump_layout.py --list
python3 tools/dump_layout.py com.apple.keylayout.ABC com.apple.keylayout.Ukrainian
make list-layouts # find your layout's name
make layout LAYOUT=Ukrainian # writes layouts/ukrainian.map
```

Then enable it:

```tmux
set -g @langmap-layouts 'ukrainian'
```

Turn the dump into `<us-char><TAB><ru-char>` lines (drop rows where both
columns match), save as `layouts/uk.map`, then set
`@langmap-layouts 'uk'`. See the headers of the shipped maps for the exact
format.
Short names expand to `com.apple.keylayout.<name>`; `BASE=` (default `ABC`)
and `OUT=` override the reference layout and the output path. Maps are
generated for the ANSI keyboard type; a few corner keys differ on ISO/JIS
keyboards, pass `--kbd-type native` to `tools/dump_layout.py` for your
machine's own type. On other systems write the file by hand:
`<us-char><TAB><target-char>` per line, both cases listed explicitly,
identity pairs omitted (see the shipped maps).

## Development

```sh
make test # golden (awk transform vs fixtures) + integration (isolated tmux server)
# + maptool (shipped maps vs live macOS layout data; skipped elsewhere)
make apply # apply mirrors to the running tmux server
make fixtures # re-capture test fixtures from a clean tmux server
make lint # shellcheck over the shell entry points
Expand Down
95 changes: 46 additions & 49 deletions layouts/ru-mac.map
Original file line number Diff line number Diff line change
@@ -1,80 +1,77 @@
## Russian - Apple (com.apple.keylayout.Russian) vs US ANSI (com.apple.keylayout.ABC).
## Generated with tools/dump_layout.py on macOS (Darwin 25.3), verified 2026-07-14.
## Format: <us-char><TAB><ru-char>. One pair per line. Identity pairs omitted.
## com.apple.keylayout.Russian vs com.apple.keylayout.ABC, keyboard type 40 (40 = ANSI).
## Generated with tools/dump_layout.py --map. Format: <base-char><TAB><target-char>, identity pairs omitted.
## Comment lines start with "##". A line starting with a single "#" is data.
## Differs from ru-pc: shifted digit row (%, :, comma, dot, ; on Shift+4..8),
## "ё" on the backslash key, "/" and "?" unchanged.
` ]
~ [
@ "
# №
$ %
% :
^ ,
& .
* ;
q й
w ц
e у
r к
t е
y н
u г
i ш
o щ
p з
a ф
s ы
d в
f а
g п
h р
j о
k л
l д
z я
x ч
c с
v м
b и
n т
m ь
Q Й
w ц
W Ц
e у
E У
r к
R К
t е
T Е
y н
Y Н
u г
U Г
i ш
I Ш
o щ
O Щ
p з
P З
[ х
{ Х
] ъ
} Ъ
\ ё
| Ё
a ф
A Ф
s ы
S Ы
d в
D В
f а
F А
g п
G П
h р
H Р
j о
J О
k л
K Л
l д
L Д
; ж
: Ж
' э
" Э
z я
Z Я
x ч
X Ч
c с
C С
v м
V М
b и
B И
n т
N Т
m ь
M Ь
` ]
~ [
@ "
# №
$ %
% :
^ ,
& .
* ;
[ х
{ Х
] ъ
} Ъ
\ ё
| Ё
; ж
: Ж
' э
" Э
, б
< Б
. ю
Expand Down
90 changes: 44 additions & 46 deletions layouts/ru-pc.map
Original file line number Diff line number Diff line change
@@ -1,76 +1,74 @@
## Russian - PC (com.apple.keylayout.RussianWin) vs US ANSI (com.apple.keylayout.ABC).
## Generated with tools/dump_layout.py on macOS (Darwin 25.3), verified 2026-07-14.
## Format: <us-char><TAB><ru-char>. One pair per line. Identity pairs omitted.
## Comment lines start with "##". A line starting with a single "#" is data
## (the "#" key maps to "№").
## com.apple.keylayout.RussianWin vs com.apple.keylayout.ABC, keyboard type 40 (40 = ANSI).
## Generated with tools/dump_layout.py --map. Format: <base-char><TAB><target-char>, identity pairs omitted.
## Comment lines start with "##". A line starting with a single "#" is data.
` ё
~ Ë
@ "
# №
$ ;
^ :
& ?
q й
w ц
e у
r к
t е
y н
u г
i ш
o щ
p з
a ф
s ы
d в
f а
g п
h р
j о
k л
l д
z я
x ч
c с
v м
b и
n т
m ь
Q Й
w ц
W Ц
e у
E У
r к
R К
t е
T Е
y н
Y Н
u г
U Г
i ш
I Ш
o щ
O Щ
p з
P З
[ х
{ Х
] ъ
} Ъ
| /
a ф
A Ф
s ы
S Ы
d в
D В
f а
F А
g п
G П
h р
H Р
j о
J О
k л
K Л
l д
L Д
; ж
: Ж
' э
" Э
z я
Z Я
x ч
X Ч
c с
C С
v м
V М
b и
B И
n т
N Т
m ь
M Ь
` ]
~ [
@ "
# №
$ ;
^ :
& ?
[ х
{ Х
] ъ
} Ъ
| /
; ж
: Ж
' э
" Э
, б
< Б
. ю
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/prefix.expected
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ bind-key -T prefix 'ч' confirm-before -p "kill-pane #P? (y/n)" kill-pane
bind-key -T prefix 'я' resize-pane -Z
bind-key -T prefix 'Х' swap-pane -U
bind-key -T prefix 'Ъ' swap-pane -D
bind-key -T prefix 'Ë' show-messages
bind-key -T prefix 'M-т' next-window -a
bind-key -T prefix 'M-щ' rotate-window -D
bind-key -T prefix 'M-з' previous-window -a
Expand Down
32 changes: 32 additions & 0 deletions tests/maptool.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/sh
# Verify that dump_layout.py --map regenerates the shipped layout maps from
# the live macOS layout data. Guards both the tool and the committed maps.
# Ordering differs (tool emits in keycode order), so compare sorted data lines.
set -eu

TESTS_DIR=$(cd "$(dirname "$0")" && pwd)
ROOT=$(dirname "$TESTS_DIR")

if [ "$(uname)" != "Darwin" ]; then
echo "skip: maptool (needs macOS keyboard layout data)"
exit 0
fi

check() {
gen=$(mktemp "${TMPDIR:-/tmp}/langmap-maptool.XXXXXX")
shipped=$(mktemp "${TMPDIR:-/tmp}/langmap-maptool.XXXXXX")
python3 "$ROOT/tools/dump_layout.py" --map ABC "$2" \
| grep -v '^##' | LC_ALL=C sort > "$gen"
grep -v '^##' "$ROOT/layouts/$1.map" | LC_ALL=C sort > "$shipped"
if diff -u "$shipped" "$gen"; then
echo "ok: maptool $1"
else
echo "FAIL: maptool $1 (generated map differs from shipped)"
rm -f "$gen" "$shipped"
exit 1
fi
rm -f "$gen" "$shipped"
}

check ru-pc RussianWin
check ru-mac Russian
Loading
Loading