├── .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 | --------------------------------------------------------------------------------