├── .gitignore
├── LICENSE
├── README.md
├── configs
├── 61-pixel-keyboard.hwdb
├── cros-pixel.conf
└── cros-sarien.conf
├── cros-keyboard-map.py
├── install.sh
└── local-overrides.quirks
/.gitignore:
--------------------------------------------------------------------------------
1 | cros.conf
2 | pkg.log
3 | keyd/
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2023, WeirdTreeThing
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its
16 | contributors may be used to endorse or promote products derived from
17 | this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Utility to generate keyd configurations for use on Chromebooks
2 |
3 | ## List of supported distributions
4 | - Alpine
5 | - Arch Linux
6 | - Chimera Linux
7 | - Debian
8 | - Fedora
9 | - openSUSE
10 | - Ubuntu
11 | - Void Linux
12 |
13 | ### Instructions
14 | 1. git clone https://github.com/WeirdTreeThing/cros-keyboard-map
15 | 2. cd cros-keyboard-map
16 | 3. ./install.sh
17 |
18 | Thanks to rvaiya for creating [keyd](https://github.com/rvaiya/keyd).
19 |
--------------------------------------------------------------------------------
/configs/61-pixel-keyboard.hwdb:
--------------------------------------------------------------------------------
1 | evdev:atkbd:dmi:bvn*:bvr*:bd*:svnGoogle:pn*:pvr*
2 | KEYBOARD_KEY_d8=leftmeta
3 |
4 |
--------------------------------------------------------------------------------
/configs/cros-pixel.conf:
--------------------------------------------------------------------------------
1 | [ids]
2 | # Pixelbook and Pixelbook Go use AT Keyboard (0001:0001)
3 | # Nocturne uses Google hammer (18d1:5030)
4 | 0001:0001
5 | 18d1:5030
6 |
7 | [main]
8 | f1 = back
9 | f2 = refresh
10 | f3 = f11
11 | f4 = scale
12 | f5 = brightnessdown
13 | f6 = brightnessup
14 | f7 = playpause
15 | f8 = mute
16 | f9 = volumedown
17 | f10 = volumeup
18 | f13 = f13
19 |
20 | [meta]
21 | f1 = f1
22 | f2 = f2
23 | f3 = f3
24 | f4 = f4
25 | f5 = f5
26 | f6 = f6
27 | f7 = f7
28 | f8 = f8
29 | f9 = f9
30 | f10 = f10
31 | f13 = f13
32 |
33 |
34 | [alt]
35 | backspace = delete
36 | f5 = kbdillumdown
37 | f6 = kbdillumup
38 |
39 | [control]
40 | f5 = print
41 |
42 |
43 | [control+alt]
44 | backspace = C-A-delete
45 |
--------------------------------------------------------------------------------
/configs/cros-sarien.conf:
--------------------------------------------------------------------------------
1 | # Config for Sarien and its variant, Arcada
2 | [ids]
3 | # AT keyboard is device 0001:0001
4 | 0001:0001
5 |
6 | [main]
7 | back = back
8 | refresh = refresh
9 | zoom = f11
10 | scale = scale
11 | print = print
12 | camera = brightnessdown
13 | prog1 = brightnessup
14 | mute = mute
15 | volumedown = volumedown
16 | volumeup = volumeup
17 | sleep = coffee
18 |
19 | [meta]
20 | back = f1
21 | refresh = f2
22 | zoom = f3
23 | scale = f4
24 | camera = f5
25 | prog1 = f6
26 | mute = f7
27 | volumedown = f8
28 | volumeup = f9
29 | switchvideomode = f12
30 |
31 |
32 | [alt]
33 | backspace = delete
34 | meta = capslock
35 | camera = kbdillumdown
36 | prog1 = kbdillumup
37 |
38 | [control+alt]
39 | backspace = C-A-delete
40 |
--------------------------------------------------------------------------------
/cros-keyboard-map.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import argparse
4 | import platform
5 |
6 | device_ids = {
7 | "k:0000:0000", # cros_ec keyboard
8 | "k:0001:0001", # AT keyboard
9 | "k:18d1:503c", # Google Inc. Hammer
10 | "k:18d1:5050", # Google Inc. Hammer
11 | "k:18d1:504c", # Google Inc. Hammer
12 | "k:18d1:5052", # Google Inc. Hammer
13 | "k:18d1:5057", # Google Inc. Hammer
14 | "k:18d1:505b", # Google Inc. Hammer
15 | "k:18d1:5030", # Google Inc. Hammer
16 | "k:18d1:503d", # Google Inc. Hammer
17 | "k:18d1:5044", # Google Inc. Hammer
18 | "k:18d1:5061", # Google Inc. Hammer
19 | "k:18d1:502b", # Google Inc. Hammer
20 | }
21 |
22 | vivaldi_keys = {
23 | "x86_64": {
24 | "90": "previoussong",
25 | "91": "zoom",
26 | "92": "scale",
27 | "93": "sysrq",
28 | "94": "brightnessdown",
29 | "95": "brightnessup",
30 | "97": "kbdillumdown",
31 | "98": "kbdillumup",
32 | "99": "nextsong",
33 | "9A": "playpause",
34 | "9B": "micmute",
35 | "9E": "kbdillumtoggle",
36 | "A0": "mute",
37 | "AE": "volumedown",
38 | "B0": "volumeup",
39 | "E9": "forward",
40 | "EA": "back",
41 | "E7": "refresh",
42 | },
43 | "arm": {
44 | "158": "back",
45 | "159": "forward",
46 | "173": "refresh",
47 | "372": "zoom",
48 | "120": "scale",
49 | "224": "brightnessdown",
50 | "225": "brightnessup",
51 | "113": "mute",
52 | "114": "volumedown",
53 | "115": "volumeup",
54 | "99" : "sysrq",
55 | }
56 | }
57 |
58 | def get_arch():
59 | return platform.uname().machine
60 |
61 | def get_ids_string(device_ids):
62 | return "\n".join(device_ids)
63 |
64 | def get_dt_layout():
65 | keys = []
66 | keycodes = []
67 |
68 | fdt = libfdt.Fdt(open("/sys/firmware/fdt", "rb").read())
69 | currentnode = fdt.first_subnode(0)
70 |
71 | while True:
72 | try:
73 | if fdt.get_name(currentnode) == "keyboard-controller":
74 | prop = fdt.getprop(currentnode, "linux,keymap")
75 | keys = prop.as_uint32_list()
76 | currentnode = fdt.next_node(currentnode, 10)[0]
77 | except:
78 | break
79 |
80 | if not keys:
81 | return ""
82 |
83 | for key in keys:
84 | keycode = str(key & 0xFFFF)
85 | if keycode in vivaldi_keys["arm"]:
86 | keycodes.append(keycode)
87 | return keycodes
88 |
89 | def get_physmap_data():
90 | if get_arch() == "x86_64":
91 | try:
92 | with open("/sys/bus/platform/devices/i8042/serio0/function_row_physmap", "r") as file:
93 | return file.read().strip().split()
94 | except FileNotFoundError:
95 | return ""
96 | else:
97 | return get_dt_layout()
98 |
99 | def get_functional_row(physmap, use_vivaldi, super_is_held, super_inverted):
100 | arch = get_arch()
101 | if arch != "x86_64":
102 | arch = "arm"
103 |
104 | i = 0
105 | result = ""
106 | for code in physmap:
107 | i += 1
108 | # Map zoom to f11 since most applications wont listen to zoom
109 | mapping = "f11" if vivaldi_keys[arch][code] == "zoom" \
110 | else vivaldi_keys[arch][code]
111 |
112 | match [super_is_held, use_vivaldi, super_inverted]:
113 | case [True, True, False] | [False, True, True]:
114 | result += f"{vivaldi_keys[arch][code]} = f{i}\n"
115 | case [True, False, False] | [False, False, True]:
116 | result += f"f{i} = f{i}\n"
117 | case [False, True, False] | [True, True, True]:
118 | result += f"{vivaldi_keys[arch][code]} = {mapping}\n"
119 | case [False, False, False] | [True, False, True]:
120 | result += f"f{i} = {mapping}\n"
121 |
122 | return result
123 |
124 | def get_keyd_config(physmap, inverted):
125 | config = f"""\
126 | [ids]
127 | {get_ids_string(device_ids)}
128 |
129 | [main]
130 | {get_functional_row(physmap, use_vivaldi=False, super_is_held=False, super_inverted=inverted)}
131 | {get_functional_row(physmap, use_vivaldi=True, super_is_held=False, super_inverted=inverted)}
132 | f13=coffee
133 | sleep=coffee
134 |
135 | [meta]
136 | {get_functional_row(physmap, use_vivaldi=False, super_is_held=True, super_inverted=inverted)}
137 | {get_functional_row(physmap, use_vivaldi=True, super_is_held=True, super_inverted=inverted)}
138 |
139 | [alt]
140 | backspace = delete
141 | brightnessdown = kbdillumdown
142 | brightnessup = kbdillumup
143 | f6 = kbdillumdown
144 | f7 = kbdillumup
145 |
146 | [control]
147 | f5 = sysrq
148 | scale = sysrq
149 |
150 | [altgr]
151 | backspace = delete
152 | left = home
153 | right = end
154 | up = pageup
155 | down = pagedown
156 |
157 | [control+alt]
158 | backspace = C-A-delete
159 | """
160 | return config
161 |
162 | def main():
163 | parser = argparse.ArgumentParser()
164 | parser.add_argument("-f", "--file", default="cros.conf", help="path to save config (default: cros.conf)")
165 | parser.add_argument("-i", "--inverted", action="store_true",
166 | help="use functional keys by default and media keys when super is held")
167 | args = vars(parser.parse_args())
168 |
169 |
170 |
171 | physmap = get_physmap_data()
172 | if not physmap:
173 | print("no function row mapping found, using default mapping")
174 | if get_arch() == "x86_64":
175 | physmap = ['EA', 'E9', 'E7', '91', '92', '94', '95', 'A0', 'AE', 'B0']
176 | else:
177 | physmap = ['158', '159', '173', '372', '120', '224', '225', '113', '114', '115']
178 |
179 | config = get_keyd_config(physmap, args["inverted"])
180 | with open(args["file"], "w") as conf:
181 | conf.write(config)
182 |
183 | if __name__ == "__main__":
184 | if get_arch() != "x86_64":
185 | import libfdt
186 | main()
187 |
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | # void, alpine, arch, and suse have packages
6 | # need to build on fedora (without terra) and debian/ubuntu
7 |
8 | ROOT=$(pwd)
9 |
10 | # fancy color
11 | printf "\033[94m"
12 |
13 | if [ -f /usr/bin/apt ]; then
14 | distro="deb"
15 | elif [ -f /usr/bin/zypper ]; then
16 | distro="suse"
17 | elif [ -f /usr/bin/pacman ]; then
18 | distro="arch"
19 | elif [ -f /usr/bin/dnf4 ]; then
20 | distro="fedora"
21 | elif [ -f /sbin/apk ]; then
22 | distro="alpine"
23 | elif [ -f /bin/xbps-install ]; then
24 | distro="void"
25 | elif grep 'ID=nixos' /etc/os-release &>> pkg.log; then
26 | echo "WARNING: This script will not install keyd on NixOS, but can install the configuration for you."
27 | printf "Continue? (y/N) "
28 | read -r NIXINSTALL
29 | [[ $NIXINSTALL =~ ^[Yy]$ ]] || exit 1
30 | distro="nixos"
31 | fi
32 |
33 | if which sudo &>/dev/null; then
34 | privesc="sudo"
35 | elif which doas &>/dev/null; then
36 | privesc="doas"
37 | elif which run0 &>/dev/null; then
38 | privesc="run0"
39 | fi
40 |
41 | echo "Installing, this may take some time...."
42 |
43 | # Fedora with the terra repo (Ultramarine) has keyd packaged
44 | [ "$distro" = "fedora" ] && dnf4 info keyd -y&>> pkg.log && FEDORA_HAS_KEYD=1
45 |
46 | if ! which keyd &>/dev/null && [ "$distro" != "nixos" ] ; then
47 | build_keyd=1
48 | # if keyd isnt installed
49 |
50 | # Debian-based distros and Fedora don't have keyd in the repos, ask the user to compile it from source.
51 | if [ "distro" = "fedora" ] && [ ! "$FEDORA_HAS_KEYD" = "1" ] || [ "$distro" = "deb" ]; then
52 | echo "This script can compile keyd for you or you can choose to get it from another source."
53 | printf "Compile keyd? (Y/n) "
54 | read -r COMPKEYD
55 | [[ $COMPKEYD =~ ^[Nn]$ ]] && build_keyd=0
56 | fi
57 |
58 | if [ "$build_keyd" = "1" ]; then
59 | echo "Installing keyd dependencies"
60 | case $distro in
61 | deb)
62 | $privesc apt install -y build-essential git &>> pkg.log
63 | ;;
64 | fedora)
65 | [ ! "$FEDORA_HAS_KEYD" = "1" ] && $privesc dnf4 install -y kernel-headers gcc make &>> pkg.log
66 | ;;
67 | esac
68 | fi
69 |
70 | if ( [ "distro" = "fedora" ] && [ ! "$FEDORA_HAS_KEYD" = "1" ] || [ "$distro" = "deb" ] ) && [ "$build_keyd" = "1" ]; then
71 | echo "Compiling keyd"
72 | git clone https://github.com/rvaiya/keyd &>> pkg.log
73 | cd keyd
74 | make &>> pkg.log
75 | $privesc make install
76 | cd ..
77 | else
78 | echo "Installing keyd"
79 | case $distro in
80 | suse)
81 | $privesc zypper --non-interactive install keyd &>> pkg.log
82 | ;;
83 | arch)
84 | $privesc pacman -S --noconfirm keyd &>> pkg.log
85 | ;;
86 | alpine)
87 | $privesc apk add --no-interactive keyd &>> pkg.log
88 | ;;
89 | void)
90 | $privesc xbps-install -S keyd -y &>> pkg.log
91 | ;;
92 | fedora)
93 | $privesc dnf4 install -y keyd &>> pkg.log
94 | ;;
95 | esac
96 | fi
97 | fi
98 |
99 | echo "Generating config"
100 | # Handle any special cases
101 | if (grep -E "^(Nocturne|Atlas|Eve)$" /sys/class/dmi/id/product_name &> /dev/null)
102 | then
103 | cp configs/cros-pixel.conf cros.conf
104 | $privesc mkdir -p /etc/udev/hwdb.d/
105 | $privesc cp configs/61-pixel-keyboard.hwdb /etc/udev/hwdb.d/
106 | $privesc udevadm hwdb --update
107 | $privesc udevadm trigger
108 | elif (grep -E "^(Sarien|Arcada)$" /sys/class/dmi/id/product_name &> /dev/null)
109 | then
110 | cp configs/cros-sarien.conf cros.conf
111 | else
112 | printf "By default, the top row keys will do their special function (brightness, volume, browser control, etc).\n"
113 | printf "Holding the search key will make the top row keys act like fn keys (f1, f2, f3, etc).\n"
114 | printf "Would you like to invert this? (y/N) "
115 | read -r INVERT
116 | if [ "$distro" == "nixos" ] && ! which python3 &>/dev/null; then
117 | [[ $INVERT =~ ^[Yy]$ ]] && nix-shell -p python3 --run "python3 cros-keyboard-map.py -i" ||
118 | nix-shell -p python3 --run "python3 cros-keyboard-map.py"
119 | else
120 | [[ $INVERT =~ ^[Yy]$ ]] && python3 cros-keyboard-map.py -i || python3 cros-keyboard-map.py
121 | fi
122 | fi
123 |
124 | echo "Installing config"
125 | $privesc mkdir -p /etc/keyd
126 | $privesc cp cros.conf /etc/keyd
127 |
128 | echo "Enabling keyd"
129 | case $distro in
130 | alpine)
131 | # Chimera uses apk like alpine but uses dinit instead of openrc
132 | if [ -f /usr/bin/dinitctl ]; then
133 | $privesc dinitctl start keyd
134 | $privesc dinitctl enable keyd
135 | else
136 | $privesc rc-update add keyd
137 | $privesc rc-service keyd restart
138 | fi
139 | ;;
140 | void)
141 | if [ -f /usr/bin/sv ]; then
142 | $privesc ln -s /etc/sv/keyd /var/service
143 | $privesc sv enable keyd
144 | $privesc sv start keyd
145 | else
146 | echo "This script can only be used for Void Linux using 'runit' init system. Other init system on Void Linux are currently unsupported."
147 | echo "I'M OUTTA HERE!"
148 | exit 1
149 | fi
150 | ;;
151 | *)
152 | $privesc systemctl enable keyd
153 | $privesc systemctl restart keyd
154 | ;;
155 | esac
156 |
157 | echo "Installing libinput configuration"
158 | $privesc mkdir -p /etc/libinput
159 | if [ -f /etc/libinput/local-overrides.quirks ]; then
160 | cat $ROOT/local-overrides.quirks | $privesc tee -a /etc/libinput/local-overrides.quirks > /dev/null
161 | else
162 | $privesc cp $ROOT/local-overrides.quirks /etc/libinput/local-overrides.quirks
163 | fi
164 |
165 | echo "Done"
166 | # reset color
167 | printf "\033[0m"
168 |
--------------------------------------------------------------------------------
/local-overrides.quirks:
--------------------------------------------------------------------------------
1 | [keyd virtual keyboard]
2 | MatchName=keyd virtual keyboard
3 | AttrKeyboardIntegration=internal
4 | ModelTabletModeNoSuspend=1
5 |
--------------------------------------------------------------------------------