#!/usr/bin/env bash
#
# localectl - control the system locale and keyboard layout
# Slackware implementation - writes files directly.
# localed picks up changes via inotify and updates D-Bus properties.
#
# Copyright 2026, GPLv2+

LANG_SH_FILE="/etc/profile.d/lang.sh"
RC_KEYMAP_FILE="/etc/rc.d/rc.keymap"
RC_KEYMAP_TOGGLE_FILE="/etc/rc.d/rc.keymap.toggle"
X11_KEYBOARD_FILE="/etc/X11/xorg.conf.d/00-keyboard.conf"

KNOWN_LOCALE_VARS=(
  LANG LANGUAGE
  LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY LC_MESSAGES
  LC_PAPER LC_NAME LC_ADDRESS LC_TELEPHONE LC_MEASUREMENT LC_IDENTIFICATION
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

die() {
  echo "localectl: $*" >&2
  exit 1
}

require_root() {
  [ "$EUID" -ne 0 ] && die "must be root to change locale/keyboard settings"
  return 0
}

locale_var_is_known() {
  local key="$1"
  local v
  for v in "${KNOWN_LOCALE_VARS[@]}"; do
    [ "$v" = "$key" ] && return 0
  done
  return 1
}

locale_value_is_safe() {
  # Allow alnum plus _ . - @ : ,
  [[ "$1" =~ ^[A-Za-z0-9_.@:,-]+$ ]]
}

# Read a KEY=value (or KEY="value") from a file; print the value.
kv_read() {
  local file="$1" key="$2"
  [ -f "$file" ] || return
  grep "^${key}=" "$file" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"'
}

# Atomically write or remove a KEY="value" entry in a KEY=value file.
# Passing an empty value removes the key entirely.
kv_set() {
  local file="$1" key="$2" value="$3"
  local tmp

  touch "$file" 2>/dev/null || die "cannot write $file"

  tmp=$(mktemp "${file}.XXXXXX") || die "cannot create temp file"

  grep -v "^${key}=" "$file" > "$tmp" 2>/dev/null || true

  if [ -n "$value" ]; then
    echo "${key}=\"${value}\"" >> "$tmp"
  fi

  mv "$tmp" "$file" || { rm -f "$tmp"; die "cannot update $file"; }
}

# ---------------------------------------------------------------------------
# Pager support
# ---------------------------------------------------------------------------

# Pipe stdout through $PAGER (default: less) unless --no-pager was given or
# stdout is not a terminal.  Usage: some_command | _run_pager
_run_pager() {
  if [ "$NO_PAGER" -eq 1 ] || [ ! -t 1 ]; then
    cat
  else
    "${PAGER:-less}" -FRX
  fi
}

# ---------------------------------------------------------------------------
# rc.keymap read/write helpers (Slackware-specific)
# ---------------------------------------------------------------------------

# Parse the loadkeys argument from a Slackware rc.keymap script.
# Skip comment lines and the [ -x /usr/bin/loadkeys ] test line
# (which contains "]"); on the actual invocation line, $NF is the keymap.
rcl_read_rc_keymap() {
  local file="$1"
  [ -f "$file" ] || return
  # The writer quotes the keymap: /usr/bin/loadkeys "us"
  # Extract $NF then strip surrounding double-quotes.
  awk '/loadkeys/ && !/^[[:space:]]*#/ && !/\]/ {
    tok = $NF
    gsub(/^"|"$/, "", tok)
    print tok
    exit
  }' "$file"
}

# Read the toggle keymap from the sidecar file.
rcl_read_rc_keymap_toggle() {
  local file="$1"
  [ -f "$file" ] || return
  head -1 "$file" 2>/dev/null | tr -d '\n'
}

keyboard_value_is_safe() {
  # Strict allowlist matching rcl_keyboard_value_is_safe() in rcl-locale.c:
  # alphanumeric plus the punctuation that appears in real keymap and XKB
  # identifiers.  Shell metacharacters are implicitly denied.
  [[ "$1" =~ ^[A-Za-z0-9._,:+:-]+$ ]]
}

# Write (or overwrite) a Slackware rc.keymap script.
rcl_write_rc_keymap() {
  local file="$1" keymap="$2"
  local dir tmp

  dir=$(dirname "$file")
  mkdir -p "$dir" || die "cannot create $dir"

  tmp=$(mktemp "${file}.XXXXXX") || die "cannot create temp file"

  if [ -n "$keymap" ]; then
    # Single-quoted heredoc: no shell expansion inside.
    # The validated keymap is appended via printf to prevent injection.
    {
      cat << 'HEREDOC'
#!/bin/bash
# Written by localectl - see localed(8)
# More keymaps are in /usr/share/kbd/keymaps
if [ -x /usr/bin/loadkeys ]; then
HEREDOC
      printf '  /usr/bin/loadkeys "%s"\n' "$keymap"
      echo 'fi'
    } > "$tmp"
  else
    cat > "$tmp" << 'HEREDOC'
#!/bin/bash
# Written by localectl - see localed(8)
# No keymap configured
HEREDOC
  fi

  chmod 755 "$tmp"
  mv "$tmp" "$file" || { rm -f "$tmp"; die "cannot update $file"; }
}

# Write (or remove) the toggle keymap sidecar file.
rcl_write_rc_keymap_toggle() {
  local file="$1" toggle="$2"

  if [ -z "$toggle" ]; then
    rm -f "$file"
    return
  fi

  local tmp
  tmp=$(mktemp "${file}.XXXXXX") || die "cannot create temp file"
  printf '%s\n' "$toggle" > "$tmp"
  mv "$tmp" "$file" || { rm -f "$tmp"; die "cannot update $file"; }
}

# ---------------------------------------------------------------------------
# Keyboard conversion table (console keymap <-> X11 layout)
# Mirrors the table in rcl-locale.c so localectl's direct-write path and the
# D-Bus daemon path produce the same conversions.
# ---------------------------------------------------------------------------

# _vc_to_x11 KEYMAP -> sets _X11_LAYOUT and _X11_VARIANT (or empty strings)
_vc_to_x11() {
  _X11_LAYOUT="" _X11_VARIANT=""
  case "$1" in
    us)          _X11_LAYOUT=us   _X11_VARIANT="" ;;
    uk)          _X11_LAYOUT=gb   _X11_VARIANT="" ;;
    de|de-latin1) _X11_LAYOUT=de  _X11_VARIANT="" ;;
    fr|fr-latin1) _X11_LAYOUT=fr  _X11_VARIANT="" ;;
    es)          _X11_LAYOUT=es   _X11_VARIANT="" ;;
    it)          _X11_LAYOUT=it   _X11_VARIANT="" ;;
    pt-latin1)   _X11_LAYOUT=pt   _X11_VARIANT="" ;;
    ru|ru4)      _X11_LAYOUT=ru   _X11_VARIANT="" ;;
    se-latin1)   _X11_LAYOUT=se   _X11_VARIANT="" ;;
    no-latin1)   _X11_LAYOUT=no   _X11_VARIANT="" ;;
    dk-latin1)   _X11_LAYOUT=dk   _X11_VARIANT="" ;;
    nl)          _X11_LAYOUT=nl   _X11_VARIANT="" ;;
    trq)         _X11_LAYOUT=tr   _X11_VARIANT="" ;;
    pl)          _X11_LAYOUT=pl   _X11_VARIANT="" ;;
    cz-lat2)     _X11_LAYOUT=cz   _X11_VARIANT="" ;;
    fi)          _X11_LAYOUT=fi   _X11_VARIANT="" ;;
    gr)          _X11_LAYOUT=gr   _X11_VARIANT="" ;;
    il)          _X11_LAYOUT=il   _X11_VARIANT="" ;;
    jp106)       _X11_LAYOUT=jp   _X11_VARIANT="" ;;
    ua)          _X11_LAYOUT=ua   _X11_VARIANT="" ;;
    be-latin1)   _X11_LAYOUT=be   _X11_VARIANT="" ;;
    ch)          _X11_LAYOUT=ch   _X11_VARIANT=de_nodeadkeys ;;
    ch-fr)       _X11_LAYOUT=ch   _X11_VARIANT=fr ;;
    *)           return 1 ;;
  esac
  return 0
}

# _x11_to_vc LAYOUT -> sets _VC_KEYMAP (or empty string)
_x11_to_vc() {
  _VC_KEYMAP=""
  # Use only the first layout in a comma-separated list
  local layout="${1%%,*}"
  case "$layout" in
    us) _VC_KEYMAP=us       ;;
    gb) _VC_KEYMAP=uk       ;;
    de) _VC_KEYMAP=de       ;;
    fr) _VC_KEYMAP=fr       ;;
    es) _VC_KEYMAP=es       ;;
    it) _VC_KEYMAP=it       ;;
    pt) _VC_KEYMAP=pt-latin1 ;;
    ru) _VC_KEYMAP=ru       ;;
    se) _VC_KEYMAP=se-latin1 ;;
    no) _VC_KEYMAP=no-latin1 ;;
    dk) _VC_KEYMAP=dk-latin1 ;;
    nl) _VC_KEYMAP=nl       ;;
    tr) _VC_KEYMAP=trq      ;;
    pl) _VC_KEYMAP=pl       ;;
    cz) _VC_KEYMAP=cz-lat2  ;;
    fi) _VC_KEYMAP=fi       ;;
    gr) _VC_KEYMAP=gr       ;;
    il) _VC_KEYMAP=il       ;;
    jp) _VC_KEYMAP=jp106    ;;
    ua) _VC_KEYMAP=ua       ;;
    be) _VC_KEYMAP=be-latin1 ;;
    ch) _VC_KEYMAP=ch       ;;
    *)  return 1 ;;
  esac
  return 0
}

# Read a single locale variable from lang.sh (export KEY="value" format).
rcl_lang_sh_get() {
  local file="$1" key="$2"
  [ -f "$file" ] || return
  awk -v key="$key" '
    $0 ~ "^[[:space:]]*export[[:space:]]+" key "=" {
      val = $0
      sub(/^[^=]*=/, "", val)
      gsub(/^"|"$/, "", val)
      print val
      exit
    }
  ' "$file"
}

# Read a single XkbOption value from the X11 keyboard config file.
rcl_x11_read_opt() {
  local key="$1"
  [ -f "$X11_KEYBOARD_FILE" ] || return
  awk -v key="$key" '
    $0 ~ "Option[[:space:]]+\"" key "\"" {
      # extract the second quoted string on the line
      n = split($0, a, "\"")
      print a[4]
      exit
    }
  ' "$X11_KEYBOARD_FILE"
}

# ---------------------------------------------------------------------------
# status
# ---------------------------------------------------------------------------

# Print one status line with the label right-aligned in a 16-char field.
# Usage: _status_line "Label" "value"
_status_line() {
  printf "%16s: %s\n" "$1" "$2"
}

cmd_status() {
  # ── Locale ──────────────────────────────────────────────────────────────
  local found=0 first=1 v val indent="                  "
  for v in "${KNOWN_LOCALE_VARS[@]}"; do
    val=$(rcl_lang_sh_get "$LANG_SH_FILE" "$v")
    [ -z "$val" ] && continue
    if [ "$first" -eq 1 ]; then
      _status_line "System Locale" "$v=$val"
      first=0
    else
      # continuation lines aligned to value column (18 spaces)
      printf "%s%s=%s\n" "$indent" "$v" "$val"
    fi
    found=1
  done
  [ "$found" -eq 0 ] && _status_line "System Locale" "n/a"

  # ── Console keyboard ────────────────────────────────────────────────────
  local vc_keymap toggle
  vc_keymap=$(rcl_read_rc_keymap "$RC_KEYMAP_FILE")
  toggle=$(rcl_read_rc_keymap_toggle "$RC_KEYMAP_TOGGLE_FILE")
  _status_line "VC Keymap" "${vc_keymap:-(unset)}"
  [ -n "$toggle" ] && _status_line "VC Keymap Tgl" "$toggle"

  # ── X11 keyboard ────────────────────────────────────────────────────────
  local layout model variant options
  layout=$(rcl_x11_read_opt XkbLayout)
  model=$(rcl_x11_read_opt XkbModel)
  variant=$(rcl_x11_read_opt XkbVariant)
  options=$(rcl_x11_read_opt XkbOptions)

  _status_line "X11 Layout"  "${layout:-(unset)}"
  # Show Model/Variant/Options whenever a layout is configured, even if empty
  if [ -n "$layout" ]; then
    _status_line "X11 Model"   "${model:--}"
    _status_line "X11 Variant" "${variant:--}"
    _status_line "X11 Options" "${options:--}"
  fi
}

# ---------------------------------------------------------------------------
# set-locale KEY=value [KEY=value ...]
# ---------------------------------------------------------------------------

cmd_set_locale() {
  require_root
  [ "$#" -eq 0 ] && die "set-locale requires at least one KEY=value argument"

  local pair key value
  for pair in "$@"; do
    key="${pair%%=*}"
    value="${pair#*=}"

    locale_var_is_known "$key" || die "unknown locale variable '$key'"

    if [ -n "$value" ] && ! locale_value_is_safe "$value"; then
      die "invalid value for $key: '$value'"
    fi
  done

  # All validated; apply using lang.sh-aware writer (export KEY="value" format).
  rcl_write_lang_sh "$@"
}

# Write locale variable assignments to /etc/profile.d/lang.sh.
# Preserves existing non-locale lines.  Accepts KEY=value pairs as arguments.
rcl_write_lang_sh() {
  local file="$LANG_SH_FILE"
  local dir tmp

  dir=$(dirname "$file")
  mkdir -p "$dir" || die "cannot create $dir"

  tmp=$(mktemp "${file}.XXXXXX") || die "cannot create temp file"

  # Build associative array of incoming updates
  declare -A updates
  local pair key value
  for pair in "$@"; do
    key="${pair%%=*}"
    value="${pair#*=}"
    updates["$key"]="$value"
  done

  {
    echo "# Written by localectl - see localed(8)"

    # Preserve non-locale lines (comments, unrecognised exports, etc.)
    if [ -f "$file" ]; then
      while IFS= read -r line; do
        stripped="${line#"${line%%[![:space:]]*}"}"  # lstrip
        # Drop old "Written by localectl" header
        [[ "$stripped" == *"Written by localectl"* ]] && continue
        # Drop blank lines (we'll re-emit at end)
        [ -z "$stripped" ] && continue
        # Drop known locale exports (rewritten below in canonical order)
        if [[ "$stripped" == export\ * ]]; then
          rest="${stripped#export }"
          rest="${rest#"${rest%%[![:space:]]*}"}"
          lkey="${rest%%=*}"
          locale_var_is_known "$lkey" && continue
        fi
        printf '%s\n' "$line"
      done < "$file"
    fi

    # Emit locale variables in canonical order
    local v cur_val new_val
    for v in "${KNOWN_LOCALE_VARS[@]}"; do
      if [[ -v updates["$v"] ]]; then
        new_val="${updates[$v]}"
      else
        # Preserve existing value for vars not being updated.
        # Validate it against the same safe-charset used by set-locale so a
        # manually-edited lang.sh cannot propagate unsafe values on the next write.
        cur_val=$(rcl_lang_sh_get "$file" "$v")
        if [ -n "$cur_val" ] && ! locale_value_is_safe "$cur_val"; then
          continue
        fi
        new_val="$cur_val"
      fi
      [ -n "$new_val" ] && printf 'export %s="%s"\n' "$v" "$new_val"
    done
  } > "$tmp"

  mv "$tmp" "$file" || { rm -f "$tmp"; die "cannot update $file"; }
}

# ---------------------------------------------------------------------------
# set-keymap KEYMAP [KEYMAP_TOGGLE]
# ---------------------------------------------------------------------------

cmd_set_keymap() {
  require_root

  local no_convert=0
  if [ "${1:-}" = "--no-convert" ]; then
    no_convert=1; shift
  fi

  [ "$#" -eq 0 ] && die "set-keymap requires a KEYMAP argument"

  local keymap="$1"
  local toggle="${2:-}"

  keyboard_value_is_safe "$keymap" || die "invalid keymap '$keymap'"
  [ -n "$toggle" ] && { keyboard_value_is_safe "$toggle" || die "invalid keymap toggle '$toggle'"; }

  rcl_write_rc_keymap "$RC_KEYMAP_FILE" "$keymap"
  rcl_write_rc_keymap_toggle "$RC_KEYMAP_TOGGLE_FILE" "$toggle"

  if [ "$no_convert" -eq 0 ] && [ -n "$keymap" ]; then
    if _vc_to_x11 "$keymap"; then
      rcl_write_x11_keyboard "$_X11_LAYOUT" "" "$_X11_VARIANT" ""
    fi
  fi
}

# Write /etc/X11/xorg.conf.d/00-keyboard.conf atomically.
rcl_write_x11_keyboard() {
  local layout="$1" model="${2:-}" variant="${3:-}" options="${4:-}"

  local dir
  dir=$(dirname "$X11_KEYBOARD_FILE")
  mkdir -p "$dir" || die "cannot create $dir"

  local tmp
  tmp=$(mktemp "${X11_KEYBOARD_FILE}.XXXXXX") || die "cannot create temp file"

  # Single-quoted heredoc: no shell expansion.  Values are written via
  # printf so shell metacharacters in layout/model/variant/options cannot
  # be interpreted even if keyboard_value_is_safe() is somehow bypassed.
  {
    cat << 'HEREDOC'
# Written by localectl - see localed(8)
Section "InputClass"
    Identifier "system-keyboard"
    MatchIsKeyboard "on"
HEREDOC
    printf '    Option "XkbLayout" "%s"\n'  "$layout"
    printf '    Option "XkbModel" "%s"\n'   "$model"
    printf '    Option "XkbVariant" "%s"\n' "$variant"
    printf '    Option "XkbOptions" "%s"\n' "$options"
    echo 'EndSection'
  } > "$tmp"

  mv "$tmp" "$X11_KEYBOARD_FILE" || { rm -f "$tmp"; die "cannot update $X11_KEYBOARD_FILE"; }
}

# ---------------------------------------------------------------------------
# set-x11-keymap LAYOUT [MODEL [VARIANT [OPTIONS]]]
# ---------------------------------------------------------------------------

cmd_set_x11_keymap() {
  require_root

  local no_convert=0
  if [ "${1:-}" = "--no-convert" ]; then
    no_convert=1; shift
  fi

  [ "$#" -eq 0 ] && die "set-x11-keymap requires a LAYOUT argument"

  local layout="$1"
  # Omitted args read the current value; explicit "" clears the field.
  local model variant options
  if [ "$#" -ge 2 ]; then model="$2";   else model=$(rcl_x11_read_opt XkbModel);   fi
  if [ "$#" -ge 3 ]; then variant="$3"; else variant=$(rcl_x11_read_opt XkbVariant); fi
  if [ "$#" -ge 4 ]; then options="$4"; else options=$(rcl_x11_read_opt XkbOptions); fi

  # Validate all fields; mirrors rcl_keyboard_value_is_safe() in rcl-locale.c
  local field fname
  for field_pair in "layout:$layout" "model:$model" "variant:$variant" "options:$options"; do
    fname="${field_pair%%:*}"
    field="${field_pair#*:}"
    if [ -n "$field" ] && ! keyboard_value_is_safe "$field"; then
      die "invalid X11 keyboard $fname value '$field'"
    fi
  done

  rcl_write_x11_keyboard "$layout" "$model" "$variant" "$options"

  if [ "$no_convert" -eq 0 ] && [ -n "$layout" ]; then
    if _x11_to_vc "$layout"; then
      rcl_write_rc_keymap "$RC_KEYMAP_FILE" "$_VC_KEYMAP"
    fi
  fi
}


# list-x11-keymap-models / layouts / variants / options
#
# All four parse /usr/share/X11/xkb/rules/base.lst which is shipped by the
# xkeyboard-config package.  The file has four sections introduced by a
# "! section" header line; entries are "  code   description" pairs.
#
# list-x11-keymap-variants accepts an optional LAYOUT argument: when given,
# only variants whose description starts with "LAYOUT:" are printed, matching
# upstream systemd localectl behaviour.
# ---------------------------------------------------------------------------

XKB_BASE_LST="/usr/share/X11/xkb/rules/base.lst"

# Internal helper: extract a named section from base.lst.
# Usage: _xkb_section SECTIONNAME [FILTER]
# Prints the code (first field) of every entry in the section, optionally
# filtered case-insensitively.
_xkb_section() {
  local section="$1" filter="${2:-}"

  if [ ! -f "$XKB_BASE_LST" ]; then
    die "$XKB_BASE_LST not found (is xkeyboard-config installed?)"
  fi

  awk -v section="$section" -v filter="$filter" '
    /^! /      { in_section = ($2 == section); next }
    /^[[:space:]]*$/ { next }
    in_section {
      code = $1
      if (code == "") next
      if (filter == "" || tolower(code) ~ tolower(filter) \
                       || tolower($0)   ~ tolower(filter))
        print code
    }
  ' "$XKB_BASE_LST" | sort -u
}

cmd_list_x11_keymap_models() {
  _xkb_section "model" "${1:-}" | _run_pager
}

cmd_list_x11_keymap_layouts() {
  _xkb_section "layout" "${1:-}" | _run_pager
}

cmd_list_x11_keymap_variants() {
  local layout="${1:-}" filter="${2:-}"

  if [ ! -f "$XKB_BASE_LST" ]; then
    die "$XKB_BASE_LST not found (is xkeyboard-config installed?)"
  fi

  # When a layout is given, restrict to variants whose description begins
  # with "LAYOUT:" (the format used in base.lst).  The filter arg (if any)
  # is then applied on top of that.
  awk -v layout="$layout" -v filter="$filter" '
    /^! /      { in_section = ($2 == "variant"); next }
    /^[[:space:]]*$/ { next }
    in_section {
      code = $1
      # desc is everything after the code and its trailing whitespace
      desc = substr($0, index($0, code) + length(code))
      sub(/^[[:space:]]+/, "", desc)
      if (code == "") next
      if (layout != "" && index(desc, layout ":") != 1) next
      if (filter != "" && tolower(code) !~ tolower(filter) \
                       && tolower(desc) !~ tolower(filter)) next
      print code
    }
  ' "$XKB_BASE_LST" | sort -u | _run_pager
}

cmd_list_x11_keymap_options() {
  local filter="${1:-}"

  if [ ! -f "$XKB_BASE_LST" ]; then
    die "$XKB_BASE_LST not found (is xkeyboard-config installed?)"
  fi

  # Options are "group:name" tokens; print the full token (both the group
  # header line and the individual options), matching upstream localectl.
  awk -v filter="$filter" '
    /^! /      { in_section = ($2 == "option"); next }
    /^[[:space:]]*$/ { next }
    in_section {
      code = $1
      if (code == "") next
      if (filter == "" || tolower(code) ~ tolower(filter) \
                       || tolower($0)   ~ tolower(filter))
        print code
    }
  ' "$XKB_BASE_LST" | sort -u
}

# ---------------------------------------------------------------------------
# list-keymaps
# ---------------------------------------------------------------------------

cmd_list_keymaps() {
  local filter="${1:-}"
  local kbddir="/usr/share/kbd/keymaps"

  if [ ! -d "$kbddir" ]; then
    die "keymap directory $kbddir not found (is kbd installed?)"
  fi

  # Strip path and extensions (.map.gz, .map) to get bare keymap names,
  # sort, deduplicate, then optionally filter.
  find "$kbddir" -type f \( -name "*.map.gz" -o -name "*.map" \) \
    | xargs -I{} basename {} \
    | sed -e 's/\.map\.gz$//' -e 's/\.map$//' \
    | sort -u \
    | if [ -n "$filter" ]; then grep -iF "$filter"; else cat; fi \
    | _run_pager
}

# ---------------------------------------------------------------------------
# list-locales
# ---------------------------------------------------------------------------

cmd_list_locales() {
  local filter="${1:-}"

  # locale(1) is part of glibc and always present on Slackware.
  # "locale -a" lists every locale the C library can use without generation.
  if ! command -v locale >/dev/null 2>&1; then
    die "locale(1) not found - cannot list available locales"
  fi

  if [ -n "$filter" ]; then
    locale -a 2>/dev/null | grep -iF "$filter"
  else
    locale -a 2>/dev/null
  fi | _run_pager
}

# ---------------------------------------------------------------------------
# usage
# ---------------------------------------------------------------------------

usage() {
  cat << EOF
Usage: localectl [OPTIONS] COMMAND [ARGUMENT...]

Commands:
  status                          Show current locale and keyboard settings
  list-keymaps [FILTER]           List available console keymaps (optional grep filter)
  list-locales [FILTER]           List available locales (optional grep filter)
  set-locale KEY=value [...]      Set system locale variables in lang.sh
  set-keymap [--no-convert] KEYMAP [TOGGLE]
                                  Set the virtual console keymap
  set-x11-keymap [--no-convert] LAYOUT [MODEL [VARIANT [OPTIONS]]]
                                  Set the X11 keyboard layout.
                                  Omitted args keep their current value;
                                  pass "" to explicitly clear a field.
  list-x11-keymap-models [FILTER]    List available X11 keyboard models
  list-x11-keymap-layouts [FILTER]   List available X11 keyboard layouts
  list-x11-keymap-variants [LAYOUT] [FILTER]
                                     List available X11 keyboard variants
  list-x11-keymap-options [FILTER]   List available X11 keyboard options

Options:
  --no-pager                      Do not pipe list output through a pager
  -h, --help                      Show this help and exit

Known locale variables:
  LANG LANGUAGE LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY
  LC_MESSAGES LC_PAPER LC_NAME LC_ADDRESS LC_TELEPHONE LC_MEASUREMENT
  LC_IDENTIFICATION

Examples:
  localectl list-keymaps
  localectl list-keymaps de
  localectl list-locales
  localectl list-locales UTF-8
  localectl list-locales en_US
  localectl set-locale LANG=en_US.UTF-8
  localectl set-locale LANG=en_US.UTF-8 LC_TIME=en_GB.UTF-8
  localectl set-keymap us
  localectl set-x11-keymap cz,us pc104 ,dvorak grp:win_space_toggle
  localectl set-x11-keymap cz,us pc104      # keeps existing variant and options
  localectl set-x11-keymap cz,us "" "" grp:alt_shift_toggle  # clear model/variant, set options
  localectl set-x11-keymap de pc105 nodeadkeys
  localectl set-x11-keymap us pc105 "" compose:ralt
  localectl list-x11-keymap-models
  localectl list-x11-keymap-layouts
  localectl list-x11-keymap-variants us
  localectl list-x11-keymap-options compose
EOF
}

# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------

# Global flags (consumed before the command)
NO_PAGER=0
while true; do
  case "${1:-}" in
    --no-pager) NO_PAGER=1; shift ;;
    *)          break ;;
  esac
done

case "${1:-status}" in
  status)                 shift; cmd_status "$@" ;;
  list-keymaps)           shift; cmd_list_keymaps "$@" ;;
  list-locales)           shift; cmd_list_locales "$@" ;;
  set-locale)             shift; cmd_set_locale "$@" ;;
  set-keymap)             shift; cmd_set_keymap "$@" ;;
  set-x11-keymap)              shift; cmd_set_x11_keymap "$@" ;;
  list-x11-keymap-models)      shift; cmd_list_x11_keymap_models "$@" ;;
  list-x11-keymap-layouts)     shift; cmd_list_x11_keymap_layouts "$@" ;;
  list-x11-keymap-variants)    shift; cmd_list_x11_keymap_variants "$@" ;;
  list-x11-keymap-options)     shift; cmd_list_x11_keymap_options "$@" ;;
  -h|--help)              usage ;;
  *)
    echo "localectl: unknown command '$1'" >&2
    usage >&2
    exit 1
    ;;
esac
