pax_global_header00006660000000000000000000000064142150121450014505gustar00rootroot0000000000000052 comment=9c4d78f2a2e4270dd605681c9eca8b0bc735fdbc swaykbdd-1.1/000077500000000000000000000000001421501214500131565ustar00rootroot00000000000000swaykbdd-1.1/.github/000077500000000000000000000000001421501214500145165ustar00rootroot00000000000000swaykbdd-1.1/.github/workflows/000077500000000000000000000000001421501214500165535ustar00rootroot00000000000000swaykbdd-1.1/.github/workflows/ci.yml000066400000000000000000000007401421501214500176720ustar00rootroot00000000000000name: CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: install dependencies run: > sudo apt install --no-install-recommends --yes build-essential meson pkg-config libjson-c-dev - name: meson run: meson ./build - name: ninja run: ninja -C ./build - name: check run: ./build/swaykbdd --version swaykbdd-1.1/LICENSE000066400000000000000000000021041421501214500141600ustar00rootroot00000000000000MIT License Copyright (c) 2020 Artem Senichev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. swaykbdd-1.1/README.md000066400000000000000000000007571421501214500144460ustar00rootroot00000000000000# Swaykbdd: per-window keyboard layout for Sway ![CI](https://github.com/artemsen/swaykbdd/workflows/CI/badge.svg) The _swaykbdd_ utility can be used to automatically change the keyboard layout on a per-window basis. ## Usage For automatic start add to the Sway config file the following command: `exec swaykbdd` ## Build and install ``` meson build ninja -C build sudo ninja -C build install ``` Arch users can install the program via [AUR](https://aur.archlinux.org/packages/swaykbdd). swaykbdd-1.1/debian/000077500000000000000000000000001421501214500144005ustar00rootroot00000000000000swaykbdd-1.1/debian/changelog000066400000000000000000000002451421501214500162530ustar00rootroot00000000000000swaykbdd (1.0-1) unstable; urgency=medium * Files for building debian packages -- Iskren Hadzhinedev Wed, 01 Dec 2021 00:23:59 +0200 swaykbdd-1.1/debian/control000066400000000000000000000012121421501214500157770ustar00rootroot00000000000000Source: swaykbdd Section: misc Priority: optional Maintainer: Iskren Hadzhinedev Build-Depends: debhelper-compat (= 13), libjson-c-dev (>= 0.15), meson (>= 0.56), ninja-build (>= 1.10) Standards-Version: 4.5.1 Homepage: https://github.com/artemsen/swaykbdd Vcs-Browser: https://github.com/artemsen/swaykbdd Vcs-Git: https://github.com/artemsen/swaykbdd.git Rules-Requires-Root: no Package: swaykbdd Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends} Description: Per-window keyboard layout management for sway The swaykbdd utility can be used to automatically change the keyboard layout on a per-window basis. swaykbdd-1.1/debian/copyright000066400000000000000000000043121421501214500163330ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: swaykbdd Upstream-Contact: Artem Senichev Source: https://github.com/artemsen/swaykbdd Files: * Copyright: 2020 Artem Senichev License: MIT MIT License . Copyright (c) 2020 Artem Senichev . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: . The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. . THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. . Files: debian/* Copyright: 2021 Iskren Hadzhinedev License: GPL-2+ This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. . This package is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . You should have received a copy of the GNU General Public License along with this program. If not, see . On Debian systems, the complete text of the GNU General Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". swaykbdd-1.1/debian/rules000077500000000000000000000000361421501214500154570ustar00rootroot00000000000000#!/usr/bin/make -f %: dh $@ swaykbdd-1.1/debian/source/000077500000000000000000000000001421501214500157005ustar00rootroot00000000000000swaykbdd-1.1/debian/source/format000066400000000000000000000000141421501214500171060ustar00rootroot000000000000003.0 (quilt) swaykbdd-1.1/meson.build000066400000000000000000000007471421501214500153300ustar00rootroot00000000000000# Rules for building with Meson project( 'swaykbdd', 'c', default_options: [ 'c_std=c99', 'warning_level=3', ], license: 'MIT', version: '1.1', ) add_project_arguments( [ '-DVERSION="' + meson.project_version() + '"', '-D_POSIX_C_SOURCE=200809' ], language: 'c', ) install_man('swaykbdd.1') executable( 'swaykbdd', [ 'src/layouts.c', 'src/main.c', 'src/sway.c', ], dependencies: [ dependency('json-c') ], install: true ) swaykbdd-1.1/src/000077500000000000000000000000001421501214500137455ustar00rootroot00000000000000swaykbdd-1.1/src/layouts.c000066400000000000000000000034521421501214500156150ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (C) 2020 Artem Senichev #include "layouts.h" #include /** State descriptor: window and its layout. */ struct state { int window; int layout; }; static struct state* states; static int states_sz; /** * Find state description for specified window. * @param[in] window window Id * @return pointer to the state descriptor or NULL if not found */ static struct state* find_state(int window) { for (int i = 0; i < states_sz; ++i) { struct state* entry = &states[i]; if (entry->window == window) { return entry; } } return NULL; } int get_layout(int window) { const struct state* entry = find_state(window); return entry ? entry->layout : -1; } void put_layout(int window, int layout) { // search for existing descriptor struct state* entry = find_state(window); if (entry) { entry->layout = layout; return; } // search for free descriptor for (int i = 0; i < states_sz; ++i) { struct state* entry = &states[i]; if (entry->window == -1) { entry->window = window; entry->layout = layout; return; } } // realloc const int old_sz = states_sz; states_sz += 8; states = realloc(states, states_sz * sizeof(struct state)); for (int i = old_sz; i < states_sz; ++i) { struct state* entry = &states[i]; if (i == old_sz) { entry->window = window; entry->layout = layout; } else { entry->window = -1; } } } void rm_layout(int window) { struct state* entry = find_state(window); if (entry) { // mark descriptor as free cell entry->window = -1; entry->layout = -1; } } swaykbdd-1.1/src/layouts.h000066400000000000000000000010301421501214500156100ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (C) 2020 Artem Senichev #pragma once /** * Get layout information for specified window. * @param[in] window window Id * @return layout index, -1 if not found */ int get_layout(int window); /** * Put layout information into storage. * @param[in] window window Id * @param[in] layout keyboard layout index */ void put_layout(int window, int layout); /** * Remove layout information from storage. * @param[in] window window Id */ void rm_layout(int window); swaykbdd-1.1/src/main.c000066400000000000000000000114671421501214500150460ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (C) 2020 Artem Senichev #include "layouts.h" #include "sway.h" #include #include #include #include // Default layout for new windows #define DEFAULT_LAYOUT 0 // Default ignored time between layout change and focus lost events #define DEFAULT_TIMEOUT 50 // Convert timespec to milliseconds #define TIMESPEC_MS(ts) (ts.tv_sec * 1000 + ts.tv_nsec / 1000000) /** Static context. */ struct context { int last_window; ///< Identifier of the last focused window int default_layout; ///< Default layout for new windows int current_layout; ///< Current layout index int switch_timeout; ///< Ignored time between layout change and focus lost struct timespec switch_timestamp; ///< Timestamp of the last layout change }; static struct context ctx = { .last_window = -1, .default_layout = DEFAULT_LAYOUT, .current_layout = -1, .switch_timeout = DEFAULT_TIMEOUT, }; /** Focus change handler. */ static int on_focus_change(int window) { int layout; // save current layout for previously focused window if (ctx.last_window != -1 && ctx.current_layout != -1) { if (ctx.switch_timeout == 0) { layout = ctx.current_layout; } else { // check for timeout unsigned long long elapsed; struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); elapsed = TIMESPEC_MS(ts) - TIMESPEC_MS(ctx.switch_timestamp); if (elapsed > (unsigned long long)ctx.switch_timeout) { layout = ctx.current_layout; } else { layout = -1; } } if (layout != -1) { put_layout(ctx.last_window, layout); } } // define layout for currently focused window layout = get_layout(window); if (layout == -1 && ctx.default_layout != -1) { layout = ctx.default_layout; // set default } if (layout == ctx.current_layout) { layout = -1; // already set } ctx.last_window = window; return layout; } /** Window close handler. */ static void on_window_close(int window) { rm_layout(window); if (window == ctx.last_window) { // reset last window id to prevent saving layout for the closed window ctx.last_window = -1; } } /** Keyboard layout change handler. */ static void on_layout_change(int layout) { ctx.current_layout = layout; clock_gettime(CLOCK_MONOTONIC, &ctx.switch_timestamp); } /** * Application entry point. */ int main(int argc, char* argv[]) { const struct option long_opts[] = { { "default", required_argument, NULL, 'd' }, { "timeout", required_argument, NULL, 't' }, { "version", no_argument, NULL, 'v' }, { "help", no_argument, NULL, 'h' }, { NULL, 0, NULL, 0 } }; const char* short_opts = "d:t:vh"; opterr = 0; // prevent native error messages // parse arguments int opt; while ((opt = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) { switch (opt) { case 'd': ctx.default_layout = atoi(optarg); if (ctx.default_layout < -1 || ctx.default_layout > 0xffff) { fprintf(stderr, "Invalid default layout: %s\n", argv[optind - 1]); return EXIT_FAILURE; } break; case 't': ctx.switch_timeout = atoi(optarg); if (ctx.switch_timeout < 0) { fprintf(stderr, "Invalid timeout value: %s\n", argv[optind - 1]); return EXIT_FAILURE; } break; case 'v': printf("swaykbdd version " VERSION ".\n"); return EXIT_SUCCESS; case 'h': printf("Keyboard layout switcher for Sway.\n"); printf("Usage: %s [OPTION]\n", argv[0]); printf(" -d, --default ID Default layout for new windows [%i]\n", DEFAULT_LAYOUT); printf(" -t, --timeout MS Delay between switching and saving layout [%i ms]\n", DEFAULT_TIMEOUT); printf(" -v, --version Print version info and exit\n"); printf(" -h, --help Print this help and exit\n"); return EXIT_SUCCESS; default: fprintf(stderr, "Invalid argument: %s\n", argv[optind - 1]); return EXIT_FAILURE; } } if (optind < argc) { fprintf(stderr, "Unexpected argument: %s\n", argv[optind]); return EXIT_FAILURE; } return sway_monitor(on_focus_change, on_window_close, on_layout_change); } swaykbdd-1.1/src/sway.c000066400000000000000000000171661421501214500151070ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (C) 2020 Artem Senichev #include "sway.h" #include #include #include #include #include #include #include #include /** IPC magic header value */ static const uint8_t ipc_magic[] = { 'i', '3', '-', 'i', 'p', 'c' }; /** IPC message types (used only) */ enum ipc_msg_type { IPC_COMMAND = 0, IPC_SUBSCRIBE = 2, }; /** IPC header */ struct __attribute__((__packed__)) ipc_header { uint8_t magic[sizeof(ipc_magic)]; uint32_t len; uint32_t type; }; /** * Read exactly specified number of bytes from socket. * @param[in] sock socket descriptor * @param[out] buf buffer for destination data * @param[in] len number of bytes to read * @return error code, 0 on success */ static int sock_read(int sock, void* buf, size_t len) { while (len) { const ssize_t rcv = recv(sock, buf, len, 0); if (rcv == 0) { fprintf(stderr, "IPC read error: no data\n"); return ENOMSG; } if (rcv == -1) { const int ec = errno; fprintf(stderr, "IPC read error: [%i] %s\n", ec, strerror(ec)); return ec; } len -= rcv; buf = ((uint8_t*)buf) + rcv; } return 0; } /** * Write data to the socket. * @param[in] sock socket descriptor * @param[in] buf buffer of data of send * @param[in] len number of bytes to write * @return error code, 0 on success */ static int sock_write(int sock, const void* buf, size_t len) { while (len) { const ssize_t rcv = write(sock, buf, len); if (rcv == -1) { const int ec = errno; fprintf(stderr, "IPC write error: [%i] %s\n", ec, strerror(ec)); return ec; } len -= rcv; buf = ((uint8_t*)buf) + rcv; } return 0; } /** * Read IPC message. * @param[in] sock socket descriptor * @return IPC response as json object, NULL on errors */ static struct json_object* ipc_read(int sock) { struct ipc_header hdr; if (sock_read(sock, &hdr, sizeof(hdr))) { return NULL; } char* raw = malloc(hdr.len + 1); if (!raw) { fprintf(stderr, "Not enough memory\n"); return NULL; } if (sock_read(sock, raw, hdr.len)) { free(raw); return NULL; } raw[hdr.len] = 0; struct json_object* response = json_tokener_parse(raw); if (!response) { fprintf(stderr, "Invalid IPC response\n"); } free(raw); return response; } /** * Write IPC message. * @param[in] sock socket descriptor * @param[in] type message type * @param[in] payload payload data * @return error code, 0 on success */ static int ipc_write(int sock, enum ipc_msg_type type, const char* payload) { struct ipc_header hdr; memcpy(hdr.magic, ipc_magic, sizeof(ipc_magic)); hdr.len = payload ? strlen(payload) : 0; hdr.type = type; int rc = sock_write(sock, &hdr, sizeof(hdr)); if (rc == 0 && hdr.len) { rc = sock_write(sock, payload, hdr.len); } return rc; } /** * Connect to Sway IPC. * @return socket descriptor (non-negative number) on successful completion, * otherwise error code (negative number) */ static int ipc_connect(void) { struct sockaddr_un sa; memset(&sa, 0, sizeof(sa)); const char* path = getenv("SWAYSOCK"); if (!path) { fprintf(stderr, "SWAYSOCK variable is not defined\n"); return -ENOENT; } size_t len = strlen(path); if (!len || len > sizeof(sa.sun_path)) { fprintf(stderr, "Invalid SWAYSOCK variable\n"); return -ENOENT; } const int sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock == -1) { const int ec = errno; fprintf(stderr, "Failed to create IPC socket: [%i] %s\n", ec, strerror(ec)); return -ec; } sa.sun_family = AF_UNIX; memcpy(sa.sun_path, path, len); len += sizeof(sa) - sizeof(sa.sun_path); if (connect(sock, (struct sockaddr*)&sa, len) == -1) { const int ec = errno; fprintf(stderr, "Failed to connect IPC socket: [%i] %s\n", ec, strerror(ec)); close(sock); return -ec; } return sock; } /** * Subscribe to Sway events. * @param[in] sock socket descriptor * @return error code, 0 on success */ static int ipc_subscribe(int sock) { const char* subscribe = "[ \"window\", \"input\" ]"; int rc = ipc_write(sock, IPC_SUBSCRIBE, subscribe); if (rc == 0) { struct json_object* response = ipc_read(sock); if (!response) { rc = EBADRQC; } else { struct json_object* val; if (!json_object_object_get_ex(response, "success", &val) || !json_object_get_boolean(val)) { fprintf(stderr, "Unable to subscribe\n"); rc = EBADRQC; } json_object_put(response); } } return rc; } /** * Set keyboard layout. * @param[in] sock socket descriptor * @param[in] layout keyboard layout index to set * @return error code, 0 on success */ static int ipc_change_layout(int sock, int layout) { char cmd[64]; snprintf(cmd, sizeof(cmd), "input * xkb_switch_layout %i", layout); return ipc_write(sock, IPC_COMMAND, cmd); } /** * Get container Id from event message. * @param[in] msg event message * @return container Id or -1 if not found */ static int container_id(struct json_object* msg) { struct json_object* cnt_node; if (json_object_object_get_ex(msg, "container", &cnt_node)) { struct json_object* id_node; if (json_object_object_get_ex(cnt_node, "id", &id_node)) { const int id = json_object_get_int(id_node); if (id != 0 || errno != EINVAL) { return id; } } } return -1; } /** * Get keyboard layout index from event message. * @param[in] msg event message * @return keyboard layout index or -1 if not found */ static int layout_index(struct json_object* msg) { struct json_object* input_node; if (json_object_object_get_ex(msg, "input", &input_node)) { struct json_object* index_node; if (json_object_object_get_ex(input_node, "xkb_active_layout_index", &index_node)) { const int idx = json_object_get_int(index_node); if (idx != 0 || errno != EINVAL) { return idx; } } } return -1; } int sway_monitor(on_focus fn_focus, on_close fn_close, on_layout fn_layout) { int rc; const int sock = ipc_connect(); if (sock < 0) { rc = -sock; goto error; } rc = ipc_subscribe(sock); if (rc) { goto error; } while (rc == 0) { struct json_object* msg = ipc_read(sock); if (!msg) { rc = EBADRQC; } else { struct json_object* event_node; if (json_object_object_get_ex(msg, "change", &event_node)) { const char* event_name = json_object_get_string(event_node); if (strcmp(event_name, "focus") == 0) { const int cid = container_id(msg); const int layout = fn_focus(cid); if (layout >= 0) { ipc_change_layout(sock, layout); } } else if (strcmp(event_name, "close") == 0) { fn_close(container_id(msg)); } else if (strcmp(event_name, "xkb_layout") == 0) { fn_layout(layout_index(msg)); } } json_object_put(msg); } } error: if (sock >= 0) { close(sock); } return rc; } swaykbdd-1.1/src/sway.h000066400000000000000000000017561421501214500151120ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (C) 2020 Artem Senichev #pragma once /** * Callback function: Window focus change handler. * @param[in] window identifier of currently focused window (container) * @return keyboard layout to set, -1 to leave current */ typedef int (*on_focus)(int window); /** * Callback function: Window close handler. * @param[in] window identifier of closed window (container) */ typedef void (*on_close)(int window); /** * Callback function: Keyboard layout change handler. * @param[in] layout current keyboard layout index */ typedef void (*on_layout)(int layout); /** * Connect to Sway IPC and start event monitoring. * Function never returns unless errors occurred. * @param[in] fn_focus event handler for focus change * @param[in] fn_close event handler for window close * @param[in] fn_layout event handler for layout change * @return error code */ int sway_monitor(on_focus fn_focus, on_close fn_close, on_layout fn_layout); swaykbdd-1.1/swaykbdd.1000066400000000000000000000023031421501214500150460ustar00rootroot00000000000000.\" XVI hexadecimal editor .\" Copyright (C) 2022 Artem Senichev .TH SWAYKBDD 1 2022-03-17 swaykbdd "Swaykbdd manual" .SH NAME swaykbdd \- keyboard layout switcher for Sway .SH SYNOPSIS swaykbdd [\fIOPTIONS\fR...] .SH DESCRIPTION The \fBswaykbdd\fR utility can be used to automatically change the keyboard layout on a per-window basis. .PP For automatic start add to the Sway config file the following command: `exec swaykbdd`. .SH OPTIONS .IP "\fB\-h\fR, \fB\-\-help\fR" Display help message. .IP "\fB\-v\fR, \fB\-\-version\fR" Display version information. .IP "\fB\-d\fR, \fB\-\-default\fR\fB=\fR\fIID\fR" Set the default keyboard layout for new windows. \fIID\fR is an index of layout, default is 0, special value -1 can be used to disable set the layout for new windows (currently active layout will be used instead). .IP "\fB\-t\fR, \fB\-\-timeout\fR\fB=\fR\fIMS\fR" Ignored time between layout change and focus lost events, in milliseconds. The default value is 50. .SH ENVIRONMENT .IP \fISWAYSOCK\fR Path to the socket file used for Sway IPC. .\" link to homepage .SH BUGS For suggestions, comments, bug reports etc. visit the .UR https://github.com/artemsen/swaykbdd project homepage .UE .