|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org> |
| 4 | +## See the file COPYING for copying conditions. |
| 5 | + |
| 6 | +set -o errexit |
| 7 | +set -o nounset |
| 8 | +set -o errtrace |
| 9 | +set -o pipefail |
| 10 | + |
| 11 | +true "$0: START" |
| 12 | + |
| 13 | +xkb_variable_names=( |
| 14 | + 'XKB_DEFAULT_LAYOUT' |
| 15 | + 'XKB_DEFAULT_VARIANT' |
| 16 | + 'XKB_DEFAULT_OPTIONS' |
| 17 | +) |
| 18 | +do_persist='false' |
| 19 | + |
| 20 | +## Checks to see if all items in "check_str" are present in the output of a |
| 21 | +## command that lists valid items. |
| 22 | +is_layout_data_valid() { |
| 23 | + local valid_list_cmd check_str check_list check_item valid_item_list \ |
| 24 | + valid_item is_item_valid |
| 25 | + |
| 26 | + check_str="${1:-}" |
| 27 | + shift |
| 28 | + valid_list_cmd=( "$@" ) |
| 29 | + if [ "${#valid_list_cmd[@]}" = '0' ] || [ -z "${valid_list_cmd[0]}" ]; then\ |
| 30 | + return 1 |
| 31 | + fi |
| 32 | + if [ -z "${check_str}" ]; then return 1; fi |
| 33 | + |
| 34 | + readarray -t check_list < <(printf '%s\n' "${check_str}" | tr ',' '\n') |
| 35 | + readarray -t valid_item_list < <("${valid_list_cmd[@]}") |
| 36 | + |
| 37 | + for check_item in "${check_list[@]}"; do |
| 38 | + if [ -z "${check_item}" ]; then continue; fi |
| 39 | + is_item_valid='false' |
| 40 | + for valid_item in "${valid_item_list[@]}"; do |
| 41 | + if [ "${check_item}" = "${valid_item}" ]; then |
| 42 | + is_item_valid='true' |
| 43 | + break |
| 44 | + fi |
| 45 | + done |
| 46 | + if [ "${is_item_valid}" = 'false' ]; then |
| 47 | + return 1 |
| 48 | + fi |
| 49 | + done |
| 50 | + |
| 51 | + return 0 |
| 52 | +} |
| 53 | + |
| 54 | +## Sets the XKB layout(s), variant(s), and option(s) in labwc, either for just |
| 55 | +## this session or persistently. |
| 56 | +set_labwc_keymap() { |
| 57 | + local labwc_config_path labwc_config_bak_path labwc_env_file_lines \ |
| 58 | + did_replace_var var_idx conf_idx kb_layout_list kb_variant_list \ |
| 59 | + kb_idx |
| 60 | + |
| 61 | + labwc_config_path="${HOME}/.config/labwc/environment" |
| 62 | + labwc_config_bak_path='' |
| 63 | + labwc_env_file_lines=() |
| 64 | + args=( "$@" ) |
| 65 | + if [ "${#args[@]}" != '3' ]; then |
| 66 | + printf '%s\n' "$0: ERROR: Argument handling failure!" |
| 67 | + return 1 |
| 68 | + fi |
| 69 | + |
| 70 | + ## Ensure the user has no more than four keyboard layouts specified (this |
| 71 | + ## is the maximum number supported by XKB under X11 according to |
| 72 | + ## https://www.x.org/archive/X11R7.5/doc/input/XKB-Config.html, and it is |
| 73 | + ## the maximum number labwc appears to support). |
| 74 | + readarray -t kb_layout_list < <(printf '%s\n' "${args[0]}" | tr ',' '\n') |
| 75 | + if (( ${#kb_layout_list[@]} > 4 )); then |
| 76 | + printf '%s\n' "$0: ERROR: Too many keyboard layouts specified, must specify 4 or less!" |
| 77 | + return 1 |
| 78 | + fi |
| 79 | + |
| 80 | + ## Ensure the specified keyboard layout(s) are valid. |
| 81 | + if ! is_layout_data_valid "${args[0]}" \ |
| 82 | + localectl list-x11-keymap-layouts ; then |
| 83 | + printf '%s\n' "$0: ERROR: Specified keyboard layout(s) are not all valid!" |
| 84 | + printf '%s\n' "$0: INFO: Run 'localectl list-x11-keymap-layouts' to get a list of valid layouts." |
| 85 | + return 1 |
| 86 | + fi |
| 87 | + |
| 88 | + ## Ensure the specified keyboard layout variant(s) are valid. |
| 89 | + if [ -n "${args[1]:-}" ]; then |
| 90 | + readarray -t kb_variant_list < <(printf '%s\n' "${args[1]}" | tr ',' '\n') |
| 91 | + |
| 92 | + if (( ${#kb_layout_list[@]} < ${#kb_variant_list[@]} )); then |
| 93 | + printf '%s\n' "$0: ERROR: Insufficient number of keyboard layouts specified for number of variants!" |
| 94 | + return 1 |
| 95 | + fi |
| 96 | + |
| 97 | + for kb_idx in "${!kb_layout_list[@]}"; do |
| 98 | + if [ -z "${kb_variant_list[kb_idx]}" ]; then continue; fi |
| 99 | + if ! is_layout_data_valid "${kb_variant_list[kb_idx]}" \ |
| 100 | + localectl list-x11-keymap-variants "${kb_layout_list[kb_idx]}" ; then |
| 101 | + printf '%s\n' "$0: ERROR: Specified keyboard layout variant for layout ${kb_layout_list[kb_idx]} is not valid!" |
| 102 | + printf '%s\n' "$0: INFO: Run 'localectl list-x11-keymap-variants ${kb_layout_list[kb_idx]}' to get a list of valid layout variants for the ${kb_layout_list[kb_idx]} layout." |
| 103 | + return 1 |
| 104 | + fi |
| 105 | + done |
| 106 | + fi |
| 107 | + |
| 108 | + ## Ensure the specified keyboard option(s) are valid. |
| 109 | + if [ -n "${args[2]:-}" ]; then |
| 110 | + if ! is_layout_data_valid "${args[2]}" \ |
| 111 | + localectl list-x11-keymap-options ; then |
| 112 | + printf '%s\n' "$0: ERROR: Specified keyboard layout option(s) are not valid!" |
| 113 | + printf '%s\n' "$0: INFO: Run 'localectl list-x11-keymap-options' to get a list of valid layout options." |
| 114 | + return 1 |
| 115 | + fi |
| 116 | + fi |
| 117 | + |
| 118 | + ## Ensure the labwc configuration directory exists. |
| 119 | + mkdir -p "$(dirname "${labwc_config_path}")" || { |
| 120 | + printf '%s\n' "$0: ERROR: Cannot create labwc config directory!" |
| 121 | + return 1 |
| 122 | + } |
| 123 | + |
| 124 | + ## If labwc's environment config file exists, read it. |
| 125 | + if [ -f "${labwc_config_path}" ]; then |
| 126 | + if ! [ -r "${labwc_config_path}" ]; then |
| 127 | + printf '%s\n' "$0: ERROR: Cannot read existing labwc environment config!" |
| 128 | + return 1 |
| 129 | + fi |
| 130 | + |
| 131 | + readarray -t labwc_env_file_lines < "${labwc_config_path}" |
| 132 | + |
| 133 | + ## If we do not want the new configuration to be persistent, move the |
| 134 | + ## existing configuration to a temporary backup location. |
| 135 | + if [ "${do_persist}" = 'false' ]; then |
| 136 | + labwc_config_bak_path="$(mktemp)" |
| 137 | + mv "${labwc_config_path}" "${labwc_config_bak_path}" || { |
| 138 | + printf '%s\n' "$0: ERROR: Cannot move existing labwc environment config to backup location!" |
| 139 | + return 1 |
| 140 | + } |
| 141 | + fi |
| 142 | + fi |
| 143 | + |
| 144 | + ## Loop through the variables we want to set. For each variable in `args`, |
| 145 | + ## the variable's name is specified in the corresponding element in |
| 146 | + ## `xkb_variable_names`. |
| 147 | + for var_idx in "${!args[@]}"; do |
| 148 | + if (( var_idx >= 3 )); then break; fi |
| 149 | + |
| 150 | + ## Loop through the lines of the existing labwc environment configuration. |
| 151 | + ## Change already-set keyboard-layout-related variables to new values, |
| 152 | + ## append variables that we want to set but that aren't defined yet. |
| 153 | + ## Remove duplicate variable assignments so that the variables we set |
| 154 | + ## don't get overridden. |
| 155 | + did_replace_var='false' |
| 156 | + for conf_idx in "${!labwc_env_file_lines[@]}"; do |
| 157 | + ## Note that we use a capturing group around leading whitespace - this |
| 158 | + ## allows us to prepend ${BASH_REMATCH[1]} to the replacement line, |
| 159 | + ## preserving the indentation. |
| 160 | + if [[ "${labwc_env_file_lines[conf_idx]}" \ |
| 161 | + =~ ^([[:space:]]*)"${xkb_variable_names[var_idx]}=" ]]; then |
| 162 | + if [ "${did_replace_var}" = 'false' ]; then |
| 163 | + labwc_env_file_lines[conf_idx]="${BASH_REMATCH[1]}${xkb_variable_names[var_idx]}=${args[var_idx]}" |
| 164 | + did_replace_var='true' |
| 165 | + else |
| 166 | + unset "labwc_env_file_lines[${conf_idx}]" |
| 167 | + fi |
| 168 | + fi |
| 169 | + done |
| 170 | + |
| 171 | + ## Remove any holes from the `labwc_env_file_lines` array. |
| 172 | + labwc_env_file_lines=( "${labwc_env_file_lines[@]}" ) |
| 173 | + |
| 174 | + ## If we replaced a variable assignment in the configuration file, skip |
| 175 | + ## the rest of this loop iteration. |
| 176 | + if [ "${did_replace_var}" = 'true' ]; then |
| 177 | + continue |
| 178 | + fi |
| 179 | + |
| 180 | + ## Append the new environment variable to the configuration file's |
| 181 | + ## contents. |
| 182 | + labwc_env_file_lines+=( |
| 183 | + "${xkb_variable_names[var_idx]}=${args[var_idx]}" |
| 184 | + ) |
| 185 | + done |
| 186 | + |
| 187 | + ## Write the new config file contents and load them into labwc. |
| 188 | + printf '%s\n' "${labwc_env_file_lines[@]}" > "${labwc_config_path}" || { |
| 189 | + printf '%s\n' "$0: ERROR: Cannot write new labwc environment config!" |
| 190 | + return 1 |
| 191 | + } |
| 192 | + labwc --reconfigure \ |
| 193 | + || printf '%s\n' "$0: WARNING: labwc reconfiguration failed!" |
| 194 | + |
| 195 | + ## If we do not want to persist the new configuration, put the old |
| 196 | + ## configuration back (or just delete the new config file if there wasn't an |
| 197 | + ## old config file). |
| 198 | + if [ -n "${labwc_config_bak_path}" ]; then |
| 199 | + mv "${labwc_config_bak_path}" "${labwc_config_path}" || { |
| 200 | + printf '%s\n' "$0: ERROR: Cannot move backup labwc environment config to original location!" |
| 201 | + return 1 |
| 202 | + } |
| 203 | + elif [ "${do_persist}" = 'false' ]; then |
| 204 | + safe-rm "${labwc_config_path}" || { |
| 205 | + printf '%s\n' "$0: ERROR: Cannot remove temporary labwc environment config!" |
| 206 | + return 1 |
| 207 | + } |
| 208 | + fi |
| 209 | +} |
| 210 | + |
| 211 | +print_usage() { |
| 212 | + printf '%s\n' "$0: Set keyboard layout for labwc" |
| 213 | + printf '%s\n' "Usage: $0 [--help] [--persist] [layout [variant [option]]]" |
| 214 | + printf '%s\n' 'Options:' |
| 215 | + printf '%s\n' '--help Print this help message' |
| 216 | + printf '%s\n' '--persist Make layout change persistent' |
| 217 | + printf '\n' |
| 218 | + printf '%s\n' 'Examples:' |
| 219 | + printf '%s\n' ' set-labwc-keymap de' |
| 220 | + printf '%s\n' ' set-labwc-keymap --persist us colemak' |
| 221 | + printf '%s\n' ' set-labwc-keymap us,cz,de "" grp:alt_shift_toggle' |
| 222 | +} |
| 223 | + |
| 224 | +if [ "${1:-}" = '--persist' ]; then |
| 225 | + do_persist='true' |
| 226 | + shift |
| 227 | +fi |
| 228 | +if [ "${1:-}" = '--help' ] || [ "${1:-}" = '-h' ]; then |
| 229 | + print_usage |
| 230 | + exit 0 |
| 231 | +fi |
| 232 | + |
| 233 | +args=( "$@" ) |
| 234 | +## We must have at least one, but no more than three, arguments specifying the |
| 235 | +## keyboard layout(s). |
| 236 | +if [ "${#args[@]}" = '0' ] || [ -z "${args[0]:-}" ] \ |
| 237 | + || (( ${#args[@]} > 3 )); then |
| 238 | + print_usage |
| 239 | + exit 1 |
| 240 | +fi |
| 241 | + |
| 242 | +## If we have less than three arguments, populate the `args` array with empty |
| 243 | +## strings for the remaining arguments. This will make labwc unset the |
| 244 | +## corresponding XKB environment variables internally, allowing the user to |
| 245 | +## run something like `set-labwc-keymap us colemak`, then |
| 246 | +## `set-labwc-keymap us` and have their keyboard not stuck in the Colemak |
| 247 | +## layout after the second command. |
| 248 | +while (( ${#args[@]} < 3 )); do |
| 249 | + args+=( '' ) |
| 250 | +done |
| 251 | + |
| 252 | +set_labwc_keymap "${args[@]}" || exit |
| 253 | + |
| 254 | +true "$0: END" |
0 commit comments