diff --git a/.gitignore b/.gitignore index 18e7026..a70be5a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ gkeymapper gkeymapper-daemon *.o *.so +*.deb diff --git a/AGENTS.md b/AGENTS.md index 6ed7bc0..72fd084 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,9 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/` icon itself is compiled into the binary, so it never depends on these. - `make clean` — deletes the binaries and the generated `locale/` and `build/` trees. +- `make deb` — builds a `.deb` package (`gkeymapper__.deb`) via + `scripts/make-deb.sh`; on install it runs `systemctl --user daemon-reload` and + `systemctl --user enable --now gkeymapper-daemon` for the installing user. - No tests and no CI. Verify changes by building and running the app. ## i18n — easy to get wrong diff --git a/Makefile b/Makefile index 360bdbb..a21b0d8 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,8 @@ DAEMON_HEADERS := $(wildcard $(DAEMON_DIR)/*.h) $(wildcard $(SHARED_DIR)/*.h) GUI_RES := build/gresources.c GUI_RES_SRC := data/icons.gresource.xml PREFIX ?= /usr/local +DEB_VERSION ?= 0.1.0 +DEB_ARCH := $(shell dpkg --print-architecture 2>/dev/null || echo amd64) BINDIR := $(PREFIX)/bin LOCALEDIR := $(PREFIX)/share/locale ASSETS_DIR := $(PREFIX)/share/$(PACKAGE)/assets @@ -51,7 +53,7 @@ LANGS := $(patsubst po/%.po,%,$(PO_FILES)) MO_FILES := $(patsubst %,locale/%/LC_MESSAGES/$(PACKAGE).mo,$(LANGS)) I18N := $(CURDIR)/scripts/i18n.sh -.PHONY: all clean run install translations daemon pot i18n-check i18n-status +.PHONY: all clean run install translations daemon pot i18n-check i18n-status deb all: $(TARGET) translations $(DAEMON) @@ -94,6 +96,10 @@ i18n-status: run: $(TARGET) translations GKEYMAPPER_LOCALEDIR=$(CURDIR)/locale GKEYMAPPER_ASSETS_DIR=$(CURDIR)/assets ./$(TARGET) +deb: + $(CURDIR)/scripts/make-deb.sh $(DEB_VERSION) $(DEB_ARCH) + + clean: rm -f $(TARGET) $(DAEMON) rm -rf locale build diff --git a/README.md b/README.md index 98bf98d..ce42e9d 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,16 @@ with `make daemon`. The GUI has its icons (`assets/icon.png`, `assets/icon.svg`) compiled in (`data/icons.gresource.xml` → generated `build/gresources.c`), so the window icon works without an installed `assets/` directory. +A `.deb` package can be built with + +```bash +make deb +``` + +which produces `gkeymapper__.deb`. Installing it runs +`systemctl --user daemon-reload` and `systemctl --user enable --now gkeymapper-daemon` +for the installing user. + ## Rebuild translations ```bash @@ -296,6 +306,20 @@ so that a `command` action inherits `DISPLAY`/`WAYLAND_DISPLAY`. The unit does not run as root. `make install` reloads the udev rules for you; on a fresh machine replug the keyboard (or reboot) once so the ACL applies. +## After installation + +If the daemon is not running (the GUI footer says "Daemon: not reachable"): + +```bash +systemctl --user daemon-reload +systemctl --user enable --now gkeymapper-daemon +``` + +The daemon needs the user's session for `/dev/uinput` and for the `DISPLAY` a +`command` action inherits, so it runs as a **user** service, not a system +service. If the commands above fail, check that your user session has systemd +running (`systemctl --user status`). + ## Clean ```bash diff --git a/po/de.po b/po/de.po index 21a2e2f..208b96f 100644 --- a/po/de.po +++ b/po/de.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: gkeymapper 0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-10-18 20:18+0200\n" +"POT-Creation-Date: 2026-10-18 22:01+0200\n" "PO-Revision-Date: 2026-10-18 03:08+0200\n" "Last-Translator: \n" "Language-Team: German\n" @@ -131,6 +131,9 @@ msgstr "Fenster schließen" msgid "Daemon: not reachable" msgstr "Daemon: nicht erreichbar" +msgid "Run: systemctl --user enable --now gkeymapper-daemon" +msgstr "Ausführen: systemctl --user enable --now gkeymapper-daemon" + msgid "Daemon: running" msgstr "Daemon: läuft" diff --git a/po/en.po b/po/en.po index d74600a..84f209c 100644 --- a/po/en.po +++ b/po/en.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: gkeymapper 0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-10-18 20:18+0200\n" +"POT-Creation-Date: 2026-10-18 22:01+0200\n" "PO-Revision-Date: 2026-10-18 03:08+0200\n" "Last-Translator: \n" "Language-Team: English\n" @@ -128,6 +128,9 @@ msgstr "Close window" msgid "Daemon: not reachable" msgstr "Daemon: not reachable" +msgid "Run: systemctl --user enable --now gkeymapper-daemon" +msgstr "Run: systemctl --user enable --now gkeymapper-daemon" + msgid "Daemon: running" msgstr "Daemon: running" diff --git a/po/es.po b/po/es.po index d40814e..c747a74 100644 --- a/po/es.po +++ b/po/es.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: gkeymapper 0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-10-18 20:18+0200\n" +"POT-Creation-Date: 2026-10-18 22:01+0200\n" "PO-Revision-Date: 2026-10-18 03:08+0200\n" "Last-Translator: \n" "Language-Team: Spanish\n" @@ -128,6 +128,9 @@ msgstr "Cerrar ventana" msgid "Daemon: not reachable" msgstr "Demonio: no accesible" +msgid "Run: systemctl --user enable --now gkeymapper-daemon" +msgstr "Ejecutar: systemctl --user enable --now gkeymapper-daemon" + msgid "Daemon: running" msgstr "Demonio: en ejecución" diff --git a/po/fr.po b/po/fr.po index c14a447..6108a63 100644 --- a/po/fr.po +++ b/po/fr.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: gkeymapper 0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-10-18 20:18+0200\n" +"POT-Creation-Date: 2026-10-18 22:01+0200\n" "PO-Revision-Date: 2026-10-18 03:08+0200\n" "Last-Translator: \n" "Language-Team: French\n" @@ -131,6 +131,9 @@ msgstr "Fermer la fenêtre" msgid "Daemon: not reachable" msgstr "Démon : injoignable" +msgid "Run: systemctl --user enable --now gkeymapper-daemon" +msgstr "Exécuter : systemctl --user enable --now gkeymapper-daemon" + msgid "Daemon: running" msgstr "Démon : en cours d'exécution" diff --git a/po/gkeymapper.pot b/po/gkeymapper.pot index 87ec1d3..ec0df6f 100644 --- a/po/gkeymapper.pot +++ b/po/gkeymapper.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-10-18 20:18+0200\n" +"POT-Creation-Date: 2026-10-18 22:01+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: src/autostart.cpp:97 src/main.cpp:280 src/trayicon.cpp:371 +#: src/autostart.cpp:97 src/main.cpp:285 src/trayicon.cpp:371 msgid "G-Key mapper for the Logitech G910 (GTK4)." msgstr "" @@ -111,11 +111,11 @@ msgid "" "along." msgstr "" -#: src/help.cpp:67 src/help.cpp:86 src/main.cpp:635 +#: src/help.cpp:67 src/help.cpp:86 src/main.cpp:640 msgid "Help" msgstr "" -#: src/help.cpp:95 src/optionsdialog.cpp:177 +#: src/help.cpp:95 src/optionsdialog.cpp:178 msgid "Close window" msgstr "" @@ -123,158 +123,162 @@ msgstr "" msgid "Daemon: not reachable" msgstr "" -#: src/main.cpp:78 -msgid "Daemon: running" +#: src/main.cpp:80 +msgid "Run: systemctl --user enable --now gkeymapper-daemon" msgstr "" #: src/main.cpp:83 +msgid "Daemon: running" +msgstr "" + +#: src/main.cpp:88 msgid "Device: not found" msgstr "" -#: src/main.cpp:86 +#: src/main.cpp:91 #, c-format msgid "Device: %s" msgstr "" -#: src/main.cpp:91 src/optionsdialog.cpp:228 +#: src/main.cpp:96 src/optionsdialog.cpp:229 msgid "Configuration error" msgstr "" -#: src/main.cpp:96 +#: src/main.cpp:101 #, c-format msgid "Last key: %s" msgstr "" -#: src/main.cpp:139 +#: src/main.cpp:144 msgid "Choose file" msgstr "" -#: src/main.cpp:142 src/main.cpp:313 src/main.cpp:325 +#: src/main.cpp:147 src/main.cpp:318 src/main.cpp:330 msgid "_Cancel" msgstr "" -#: src/main.cpp:143 src/main.cpp:326 +#: src/main.cpp:148 src/main.cpp:331 msgid "_Open" msgstr "" -#: src/main.cpp:173 +#: src/main.cpp:178 msgid "Choose file..." msgstr "" -#: src/main.cpp:175 +#: src/main.cpp:180 msgid "e.g. /usr/bin/firefox or custom command" msgstr "" -#: src/main.cpp:177 src/main.cpp:181 src/main.cpp:726 +#: src/main.cpp:182 src/main.cpp:186 src/main.cpp:733 msgid "Capture" msgstr "" -#: src/main.cpp:179 +#: src/main.cpp:184 msgid "e.g. ctrl+w (held while the G key is held)" msgstr "" -#: src/main.cpp:183 src/main.cpp:719 +#: src/main.cpp:188 src/main.cpp:726 msgid "e.g. ctrl+c, ctrl+v" msgstr "" -#: src/main.cpp:198 +#: src/main.cpp:203 #, c-format msgid "Could not save %s.\n" msgstr "" -#: src/main.cpp:206 +#: src/main.cpp:211 msgid "Mappings saved to " msgstr "" -#: src/main.cpp:216 +#: src/main.cpp:221 msgid "Could not export mappings.\n" msgstr "" -#: src/main.cpp:277 src/main.cpp:623 src/trayicon.cpp:370 src/trayicon.cpp:500 +#: src/main.cpp:282 src/main.cpp:628 src/trayicon.cpp:370 src/trayicon.cpp:500 msgid "gKeyMapper" msgstr "" -#: src/main.cpp:310 +#: src/main.cpp:315 msgid "Export configuration" msgstr "" -#: src/main.cpp:314 +#: src/main.cpp:319 msgid "_Save" msgstr "" -#: src/main.cpp:322 +#: src/main.cpp:327 msgid "Import configuration" msgstr "" -#: src/main.cpp:608 +#: src/main.cpp:613 msgid "gKeyMapper – Logitech G910" msgstr "" -#: src/main.cpp:627 +#: src/main.cpp:632 msgid "Export configuration..." msgstr "" -#: src/main.cpp:628 +#: src/main.cpp:633 msgid "Import configuration..." msgstr "" -#: src/main.cpp:631 src/optionsdialog.cpp:83 +#: src/main.cpp:636 src/optionsdialog.cpp:83 msgid "Options" msgstr "" -#: src/main.cpp:636 +#: src/main.cpp:641 msgid "About" msgstr "" -#: src/main.cpp:642 +#: src/main.cpp:647 msgid "Quit" msgstr "" -#: src/main.cpp:656 +#: src/main.cpp:661 msgid "G1 to G9 of the Logitech G910 remapped to keys or commands" msgstr "" -#: src/main.cpp:677 +#: src/main.cpp:682 msgid "G-Key" msgstr "" -#: src/main.cpp:677 +#: src/main.cpp:682 msgid "Type" msgstr "" -#: src/main.cpp:677 +#: src/main.cpp:682 msgid "Target (Key/Command)" msgstr "" -#: src/main.cpp:677 +#: src/main.cpp:682 msgid "Action" msgstr "" -#: src/main.cpp:689 +#: src/main.cpp:696 msgid "Key" msgstr "" -#: src/main.cpp:689 +#: src/main.cpp:696 msgid "Command" msgstr "" -#: src/main.cpp:689 +#: src/main.cpp:696 msgid "Disabled" msgstr "" -#: src/main.cpp:690 +#: src/main.cpp:697 msgid "Key (hold)" msgstr "" -#: src/main.cpp:761 +#: src/main.cpp:769 msgid "Save mappings" msgstr "" -#: src/main.cpp:768 +#: src/main.cpp:776 msgid "Saved to ~/.config/gKeyMapper/mappings.conf" msgstr "" -#: src/main.cpp:779 +#: src/main.cpp:787 msgid "Daemon: checking..." msgstr "" @@ -301,7 +305,7 @@ msgid "" "G keys - the daemon does the work and is the only part that has to run." msgstr "" -#: src/optionsdialog.cpp:131 src/optionsdialog.cpp:149 +#: src/optionsdialog.cpp:131 src/optionsdialog.cpp:150 msgid "Language" msgstr "" @@ -309,65 +313,65 @@ msgstr "" msgid "System language" msgstr "" -#: src/optionsdialog.cpp:154 +#: src/optionsdialog.cpp:155 msgid "The interface switches right away, including the tray menu." msgstr "" -#: src/optionsdialog.cpp:157 +#: src/optionsdialog.cpp:158 msgid "Daemon status" msgstr "" -#: src/optionsdialog.cpp:170 +#: src/optionsdialog.cpp:171 msgid "Tray icon" msgstr "" -#: src/optionsdialog.cpp:171 +#: src/optionsdialog.cpp:172 msgid "State" msgstr "" -#: src/optionsdialog.cpp:172 +#: src/optionsdialog.cpp:173 msgid "Version" msgstr "" -#: src/optionsdialog.cpp:173 +#: src/optionsdialog.cpp:174 msgid "Device" msgstr "" -#: src/optionsdialog.cpp:174 +#: src/optionsdialog.cpp:175 msgid "Configuration file" msgstr "" -#: src/optionsdialog.cpp:175 +#: src/optionsdialog.cpp:176 msgid "Last key" msgstr "" -#: src/optionsdialog.cpp:216 +#: src/optionsdialog.cpp:217 msgid "" "Closing or minimizing the window hides it instead of quitting. The tray icon " "shows it again (Show) and quits the program (Close)." msgstr "" -#: src/optionsdialog.cpp:218 +#: src/optionsdialog.cpp:219 msgid "No tray is available on this desktop, so the window cannot be hidden." msgstr "" -#: src/optionsdialog.cpp:221 +#: src/optionsdialog.cpp:222 msgid "active" msgstr "" -#: src/optionsdialog.cpp:221 +#: src/optionsdialog.cpp:222 msgid "not available" msgstr "" -#: src/optionsdialog.cpp:222 +#: src/optionsdialog.cpp:223 msgid "running" msgstr "" -#: src/optionsdialog.cpp:222 +#: src/optionsdialog.cpp:223 msgid "not reachable" msgstr "" -#: src/optionsdialog.cpp:224 +#: src/optionsdialog.cpp:225 msgid "not found" msgstr "" diff --git a/scripts/make-deb.sh b/scripts/make-deb.sh new file mode 100755 index 0000000..3d7a6b4 --- /dev/null +++ b/scripts/make-deb.sh @@ -0,0 +1,129 @@ +#!/bin/sh +# Build a .deb package for gKeyMapper. +# Usage: scripts/make-deb.sh [version] [arch] +set -e + +VERSION="${1:-0.1.0}" +ARCH="${2:-$(dpkg --print-architecture 2>/dev/null || echo amd64)}" +PACKAGE="gkeymapper" +STAGING="build/deb" +DEB_NAME="${PACKAGE}_${VERSION}_${ARCH}.deb" + +# Build everything first. +make all translations + +rm -rf "$STAGING" +mkdir -p "$STAGING/DEBIAN" + +# Stage files with Debian layout (PREFIX=/usr). +make install DESTDIR="$STAGING" PREFIX=/usr + +# Calculate installed size (in KB) for the control file. +INSTALLED_SIZE=$(du -sk "$STAGING" | cut -f1) + +# Control file. +cat > "$STAGING/DEBIAN/control" < +Description: Remap G1-G9 keys of the Logitech G910 + A GTK4 GUI plus a background daemon for remapping the G1-G9 keys + of a Logitech G910 keyboard to keys or commands. +EOF + +# postinst: reload udev/systemd and enable the user daemon. +cat > "$STAGING/DEBIAN/postinst" <<'EOF' +#!/bin/sh +set -e +case "$1" in + configure) + if command -v udevadm >/dev/null 2>&1; then + udevadm control --reload-rules 2>/dev/null || true + udevadm trigger --subsystem-match=usb 2>/dev/null || true + udevadm trigger --subsystem-match=input 2>/dev/null || true + fi + + # Helper: run a command as a given user with the D-Bus session bus + # available. "systemctl --user" needs DBUS_SESSION_BUS_ADDRESS, which + # is not set in a plain "su -" login shell. + run_as_user() { + _user="$1"; shift + _uid=$(id -u "$_user" 2>/dev/null) || return 1 + _bus="unix:path=/run/user/$_uid/bus" + runuser -u "$_user" -- env DBUS_SESSION_BUS_ADDRESS="$_bus" "$@" 2>&1 + } + + _target_user="" + if [ -n "$SUDO_USER" ]; then + _target_user="$SUDO_USER" + fi + + if [ -n "$_target_user" ]; then + echo "gkeymapper: enabling daemon for user $_target_user ..." + if run_as_user "$_target_user" systemctl --user daemon-reload; then + echo " daemon-reload: OK" + else + echo " daemon-reload: FAILED (is the user session running?)" + fi + if run_as_user "$_target_user" systemctl --user enable --now gkeymapper-daemon; then + echo " enable --now gkeymapper-daemon: OK" + else + echo " enable --now gkeymapper-daemon: FAILED" + echo " -> run manually: systemctl --user enable --now gkeymapper-daemon" + fi + else + echo "gkeymapper: no installing user detected (SUDO_USER empty)." + echo " Enable the daemon manually with:" + echo " systemctl --user daemon-reload" + echo " systemctl --user enable --now gkeymapper-daemon" + for _user in $(loginctl list-users --no-legend 2>/dev/null | awk '{print $2}'); do + _uid=$(id -u "$_user" 2>/dev/null) || continue + echo " trying user $_user ..." + run_as_user "$_user" systemctl --user daemon-reload >/dev/null 2>&1 || true + run_as_user "$_user" systemctl --user enable --now gkeymapper-daemon >/dev/null 2>&1 || true + done + fi + ;; +esac +exit 0 +EOF + +chmod 755 "$STAGING/DEBIAN/postinst" + +# postrm: disable the user daemon on removal. +cat > "$STAGING/DEBIAN/postrm" <<'EOF' +#!/bin/sh +set -e +case "$1" in + remove|purge) + if [ -n "$SUDO_USER" ]; then + su - "$SUDO_USER" -c "systemctl --user disable --now gkeymapper-daemon" >/dev/null 2>&1 || true + su - "$SUDO_USER" -c "systemctl --user daemon-reload" >/dev/null 2>&1 || true + fi + ;; +esac +exit 0 +EOF +chmod 755 "$STAGING/DEBIAN/postrm" + +# Fix ownership (files must be owned by root in a .deb). +# Strip debug symbols from binaries. +strip "$STAGING/usr/bin/gkeymapper" "$STAGING/usr/bin/gkeymapper-daemon" 2>/dev/null || true + +# Remove generated cache files (they are regenerated on the target). +rm -f "$STAGING/usr/share/applications/mimeinfo.cache" +rm -f "$STAGING/usr/share/icons/hicolor/icon-theme.cache" +# Fix file permissions. +chmod 644 "$STAGING"/usr/share/applications/* 2>/dev/null || true +chmod 755 "$STAGING"/usr/bin/* 2>/dev/null || true + +# Build the .deb. +dpkg-deb --root-owner-group --build "$STAGING" "$DEB_NAME" + +echo "" +echo "Built: $DEB_NAME" diff --git a/src/main.cpp b/src/main.cpp index ec916cc..4cfc5fc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -74,7 +74,12 @@ static void refresh_footer() { std::string text; if (!daemonView.reachable) { text = _("Daemon: not reachable"); + // Hint for users who installed the package but did not (or could not) + // enable the daemon service during installation. + gtk_widget_set_tooltip_text(daemonLabel, + _("Run: systemctl --user enable --now gkeymapper-daemon")); } else { + gtk_widget_set_tooltip_text(daemonLabel, nullptr); text = _("Daemon: running"); text += " · "; if (daemonView.device.empty()) { @@ -685,14 +690,16 @@ void build_ui(GtkApplication* app, bool show) { // GTK draws the check mark of the selected entry right behind the text of a // dropdown row, so every entry carries a trailing space. The catalogs keep - // the bare words; the space is added here, where the model is built. + // the bare words; the space is added here, where the model is built. The + // strings are copied by gtk_drop_down_new_from_strings, so the array can be + // freed once every dropdown has been created. const char* typeOptions[] = { _("Key"), _("Command"), _("Disabled"), _("Key (hold)"), nullptr }; - g_autoptr(GtkStringList) typeModel = gtk_string_list_new(nullptr); + char** typeShown = g_new(char*, 5); for (int i = 0; typeOptions[i] != nullptr; ++i) { - g_autofree gchar* shown = g_strconcat(typeOptions[i], " ", nullptr); - gtk_string_list_append(typeModel, shown); + typeShown[i] = g_strconcat(typeOptions[i], " ", nullptr); } + typeShown[4] = nullptr; rows.resize(kNumKeys); for (int i = 0; i < kNumKeys; ++i) { @@ -706,7 +713,7 @@ void build_ui(GtkApplication* app, bool show) { rows[i].keyLabel = keyLabel; // Type dropdown (fixed width so rows don't jump between languages) - GtkWidget* drop = gtk_drop_down_new(G_LIST_MODEL(typeModel), nullptr); + GtkWidget* drop = gtk_drop_down_new_from_strings((const char* const*)typeShown); gtk_drop_down_set_selected(GTK_DROP_DOWN(drop), 0); gtk_widget_set_size_request(drop, 150, -1); gtk_grid_attach(GTK_GRID(grid), drop, 1, i + 1, 1, 1); @@ -731,6 +738,7 @@ void build_ui(GtkApplication* app, bool show) { update_row_state(&rows[i]); } + g_strfreev(typeShown); // Start from the rows the replaced instance had on screen, or - when this is // the only instance - from the saved mappings. Without either the rows are diff --git a/src/optionsdialog.cpp b/src/optionsdialog.cpp index 7c657ca..ff60bf9 100644 --- a/src/optionsdialog.cpp +++ b/src/optionsdialog.cpp @@ -136,12 +136,13 @@ void OptionsDialog::show(GtkWindow* parent, const OptionsState& state, ToggleFn // check mark of the selected entry right behind the text. const char* languageOptions[] = { _("System language"), "Deutsch", "English", "Español", "Français", nullptr }; - g_autoptr(GtkStringList) languageModel = gtk_string_list_new(nullptr); + char** languageShown = g_new(char*, 6); for (int i = 0; languageOptions[i] != nullptr; ++i) { - g_autofree gchar* shown = g_strconcat(languageOptions[i], " ", nullptr); - gtk_string_list_append(languageModel, shown); + languageShown[i] = g_strconcat(languageOptions[i], " ", nullptr); } - languageDrop_ = gtk_drop_down_new(G_LIST_MODEL(languageModel), nullptr); + languageShown[5] = nullptr; + languageDrop_ = gtk_drop_down_new_from_strings((const char* const*)languageShown); + g_strfreev(languageShown); // Right-aligned like the switches above: the GUI's own controls sit on one // line along the right edge, the text explains them on the left. gtk_widget_set_halign(languageDrop_, GTK_ALIGN_END);