From 4f9fd2b7c230a2c5abb7ffcd8e8dc24716d3d557 Mon Sep 17 00:00:00 2001 From: Josh Ashton Date: Wed, 4 Feb 2026 19:34:55 -0700 Subject: [PATCH] init commit --- .gitignore | 3 + Makefile | 36 ++++++++++ README.md | 54 +++++++++++++++ keyboard.ui | 84 +++++++++++++++++++++++ keymap.h | 40 +++++++++++ main.c | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++ resources.xml | 7 ++ style.css | 26 +++++++ 8 files changed, 436 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 keyboard.ui create mode 100644 keymap.h create mode 100644 main.c create mode 100644 resources.xml create mode 100644 style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f603704 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +osk +*.o +*.env diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8323e4f --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +# Variables for compiler and flags +CC = gcc +PKG_CONFIG = pkg-config +PACKAGES = gtk4 libevdev gtk4-layer-shell-0 + +# Fetch flags and libs using pkg-config +CFLAGS = $(shell $(PKG_CONFIG) --cflags $(PACKAGES)) +LIBS = $(shell $(PKG_CONFIG) --libs $(PACKAGES)) + +# Target executable name +TARGET = osk + +# Source files +SRC = main.c resources.c +OBJ = $(SRC:.c=.o) + +# Default rule: build the target +all: $(TARGET) + +# Rule to link the final executable +$(TARGET): resources.c $(OBJ) + $(CC) -o $(TARGET) $(OBJ) $(LIBS) + +# Rule to compile .c files to .o files +%.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +# Rule to generate resources.c from XML +resources.c: resources.xml + glib-compile-resources resources.xml --target=resources.c --generate-source + +# Clean up generated files +clean: + rm -f $(TARGET) $(OBJ) resources.c + +.PHONY: all clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..8650548 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# A Simple On-Screen Keyboard +`osk` is an On-Screen Keyboard written in C, using GTK bindings, and is meant for use on Linux / Wayland / Hyprland setups and the Spaces ecosystem, more to come soon on that. + +Currently, the default (and only) option is to toggle `osk` when `osk` is ran again. This keeps the keyboard in memory, keeping the launch nice and responsive. + +## Setup +There are a few steps necessary in order to have this OSK work. +First, check that you have the `uinput` kernel module loaded: +``` +lsmod | grep uinput + +# If there is no output, try this: +sudo modprobe uinput +``` + +Second, open `/etc/udev/rules.d/99-uinput.rules` in your text editor of choice and add this line to the file: +``` +KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput" +``` + +Then run the following command: +``` +sudo usermod -aG input username +``` + +Log out and back in, and you'll be all set. + +Alternatively, change the group to `uinput` in the `/etc/udev/rules.d/99-uinput.rules` file and in the `usermod` command, as the `input` group has access to other input devices, not just virtual input devices. + +## Build from source +Run the following commands: +``` +git clone https://github.com/weave-space/oskeyboard +cd oskeyboard +make +./osk +``` + +## Customization +There are two ways to customize `oskeyboard`: editing `keymap.h` and `styles.css`. Currently there is a limitation of custom layouts, as the number of rows and the number of keys per row are hardcoded in `main.c`. That will be remedied in the near future. + +Any changes to `keymap.h` or `styles.css` require you to recompile, simply run `make` to do so. + +## TODO +- [ ] Responsive design sizes +- [ ] CLI flags +- [ ] Man page +- [ ] Documentation +- [ ] In-depth testing +- [ ] Easier customization & custom layouts + +## Credits +- Google Gemini 3 (Explanation of docs and concepts.) +- https://docs.gtk.org/gtk4/index.html \ No newline at end of file diff --git a/keyboard.ui b/keyboard.ui new file mode 100644 index 0000000..55c8b36 --- /dev/null +++ b/keyboard.ui @@ -0,0 +1,84 @@ + + + + OSKeyboard + + + + 15 + center + center + + + + + + 0 + 0 + + 15 + + center + + + + + + + + + 1 + 0 + + 15 + + center + + + + + + + + + 2 + 0 + + 15 + + center + + + + + + + + + 3 + 0 + + 15 + + center + + + + + + + + + 4 + 0 + + 15 + + center + + + + + + + diff --git a/keymap.h b/keymap.h new file mode 100644 index 0000000..436eed8 --- /dev/null +++ b/keymap.h @@ -0,0 +1,40 @@ +#ifndef KEYMAP_H +#define KEYMAP_H + +#include + +typedef struct { +const char *label; +const char *shift_label; +int key_code; +} KeyMapping; + +static const KeyMapping key_map[] = { + /* Row 0 */ + {"`", "~", KEY_GRAVE}, {"1", "!", KEY_1}, {"2", "#", KEY_2}, {"3", "#", KEY_3}, {"4", "$", KEY_4}, + {"5", "%", KEY_5}, {"6", "^", KEY_6}, {"7", "&", KEY_7}, {"8", "*", KEY_8}, {"9", "(", KEY_9}, + {"0", ")", KEY_0}, {"-", "_", KEY_MINUS}, {"=", "+", KEY_EQUAL}, {"Backspace", "Delete", KEY_BACKSPACE}, + + /* Row 1 */ + {"Tab", "Tab", KEY_TAB}, {"q", "Q", KEY_Q}, {"w", "W", KEY_W}, {"e", "E", KEY_E}, {"r", "R", KEY_R}, + {"t", "T", KEY_T}, {"y", "Y", KEY_Y}, {"u", "U", KEY_U}, {"i", "I", KEY_I}, {"o", "O", KEY_O}, + {"p", "P", KEY_P}, {"[", "{", KEY_LEFTBRACE}, {"]", "}", KEY_RIGHTBRACE}, {"\\", "|", KEY_BACKSLASH}, + + /* Row 2 */ + {"Caps Lock", "Caps Lock", KEY_CAPSLOCK}, {"a", "A", KEY_A}, {"s", "S", KEY_S}, {"d", "D", KEY_D}, + {"f", "F", KEY_F}, {"g", "G", KEY_G}, {"h", "H", KEY_H}, {"j", "J", KEY_J}, {"k", "K", KEY_K}, + {"l", "L", KEY_L}, {";", ":", KEY_SEMICOLON}, {"'", "\"", KEY_APOSTROPHE}, {"Enter", "Enter", KEY_ENTER}, + + /* Row 3 */ + {"Shift", "Shift", KEY_LEFTSHIFT}, {"z", "Z", KEY_Z}, {"x", "X", KEY_X}, {"c", "C", KEY_C}, + {"v", "V", KEY_V}, {"b", "B", KEY_B}, {"n", "N", KEY_N}, {"m", "M", KEY_M}, {",", "<", KEY_COMMA}, + {".", ">", KEY_DOT}, {"/", "?", KEY_SLASH}, {"Shift", "Shift", KEY_RIGHTSHIFT}, + + /* Row 4 */ + {"Ctrl", "Ctrl", KEY_LEFTCTRL}, {"Super", "Super", KEY_LEFTMETA}, {"Alt", "Alt", KEY_LEFTALT}, + {"Space", "Space", KEY_SPACE}, {"Alt", "Alt", KEY_RIGHTALT}, {"Ctrl", "Ctrl", KEY_RIGHTCTRL} +}; + +#define KEY_MAP_SIZE (sizeof(key_map) / sizeof(KeyMapping)) + +#endif diff --git a/main.c b/main.c new file mode 100644 index 0000000..488f5b2 --- /dev/null +++ b/main.c @@ -0,0 +1,186 @@ +#include +#include +#include +#include +#include +#include +#include +#include "keymap.h" + +GtkWidget *window = NULL; +struct libevdev *dev; +struct libevdev_uinput *uidev; +bool shift_active = false; +GList *key_buttons = NULL; + +static void apply_css() { + GtkCssProvider *provider = gtk_css_provider_new(); + gtk_css_provider_load_from_resource(provider, "/org/spaces/oskeyboard/style.css"); + gtk_style_context_add_provider_for_display( + gdk_display_get_default(), + GTK_STYLE_PROVIDER(provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + ); + g_object_unref(provider); +} + +void press_key(int key_code) { + if (!uidev) return; + libevdev_uinput_write_event(uidev, EV_KEY, key_code, 1); + libevdev_uinput_write_event(uidev, EV_SYN, SYN_REPORT, 0); + libevdev_uinput_write_event(uidev, EV_KEY, key_code, 0); + libevdev_uinput_write_event(uidev, EV_SYN, SYN_REPORT, 0); +} + +static void update_labels() { + for (GList *l = key_buttons; l != NULL; l = l->next) { + GtkButton *btn = GTK_BUTTON(l->data); + const char *current = gtk_button_get_label(btn); + + for (int i = 0; i < KEY_MAP_SIZE; i++) { + if (shift_active && g_strcmp0(current, key_map[i].label) == 0) { + gtk_button_set_label(btn, key_map[i].shift_label); + break; + } else if (!shift_active && g_strcmp0(current, key_map[i].shift_label) == 0) { + gtk_button_set_label(btn, key_map[i].label); + break; + } + } + } +} + +static void on_key_clicked(GtkButton *button, gpointer user_data) { + const char *label = gtk_button_get_label(button); + + if (g_strcmp0(label, "Shift") == 0) { + shift_active = !shift_active; + update_labels(); + return; + } + + for (int i = 0; i < KEY_MAP_SIZE; i++) { + if (g_strcmp0(label, key_map[i].label) == 0 || g_strcmp0(label, key_map[i].shift_label) == 0) { + if (shift_active) { + libevdev_uinput_write_event(uidev, EV_KEY, KEY_LEFTSHIFT, 1); + libevdev_uinput_write_event(uidev, EV_SYN, SYN_REPORT, 0); + } + + press_key(key_map[i].key_code); + + if (shift_active) { + libevdev_uinput_write_event(uidev, EV_KEY, KEY_LEFTSHIFT, 0); + libevdev_uinput_write_event(uidev, EV_SYN, SYN_REPORT, 0); + shift_active = false; + update_labels(); + } + return; + } + } +} + +void add_key(GtkGrid *grid, const char *label, int col, int width) { + GtkWidget *button = gtk_button_new_with_label(label); + gtk_widget_set_hexpand(button, TRUE); + gtk_widget_set_halign(button, GTK_ALIGN_FILL); + gtk_widget_set_size_request(button, 94 * width, 94); + + g_signal_connect(button, "clicked", G_CALLBACK(on_key_clicked), NULL); + gtk_grid_attach(grid, button, col, 0, width, 1); + key_buttons = g_list_append(key_buttons, button); +} + +void init_evdev_keyboard() { + dev = libevdev_new(); + libevdev_set_name(dev, "OSKeyboard"); + libevdev_enable_event_type(dev, EV_KEY); + + for (int i = 0; i < KEY_MAP_SIZE; i++) { + libevdev_enable_event_code(dev, EV_KEY, key_map[i].key_code, NULL); + } + + int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK); + if (fd < 0) { + fprintf(stderr, "Failed to open /dev/uinput: %s\n", strerror(errno)); + return; + } + + int err = libevdev_uinput_create_from_device(dev, fd, &uidev); + if (err != 0) { + fprintf(stderr, "Failed to create uinput device: %s\n", strerror(-err)); + close(fd); + return; + } +} + +static void activate(GtkApplication *app, gpointer user_data) { + if (window) { + if (gtk_widget_get_visible(window)) { + gtk_widget_set_visible(window, FALSE); + } else { + gtk_window_present(GTK_WINDOW(window)); + } + return; + } + + apply_css(); + + GtkBuilder *builder = gtk_builder_new_from_resource("/org/spaces/oskeyboard/keyboard.ui"); + window = GTK_WIDGET(gtk_builder_get_object(builder, "main_window")); + + gtk_layer_init_for_window(GTK_WINDOW(window)); + gtk_layer_set_layer(GTK_WINDOW(window), GTK_LAYER_SHELL_EDGE_BOTTOM); + gtk_layer_auto_exclusive_zone_enable(GTK_WINDOW(window)); + gtk_layer_set_keyboard_mode(GTK_WINDOW(window), FALSE); + gtk_layer_set_anchor(GTK_WINDOW(window), GTK_LAYER_SHELL_EDGE_BOTTOM, TRUE); + gtk_layer_set_anchor(GTK_WINDOW(window), GTK_LAYER_SHELL_EDGE_LEFT, TRUE); + gtk_layer_set_anchor(GTK_WINDOW(window), GTK_LAYER_SHELL_EDGE_RIGHT, TRUE); + + gtk_window_set_application(GTK_WINDOW(window), app); + + GtkGrid *rows[] = { + GTK_GRID(gtk_builder_get_object(builder, "kbd_row_0")), + GTK_GRID(gtk_builder_get_object(builder, "kbd_row_1")), + GTK_GRID(gtk_builder_get_object(builder, "kbd_row_2")), + GTK_GRID(gtk_builder_get_object(builder, "kbd_row_3")), + GTK_GRID(gtk_builder_get_object(builder, "kbd_row_4")) + }; + + // Map keys from keymap.h to the UI grids + int current_key = 0; + int row_counts[] = {14, 14, 13, 12, 6}; // Matches keymap.h layout + + for (int r = 0; r < 5; r++) { + int current_col = 0; + for (int c = 0; c < row_counts[r]; c++) { + if (current_key >= KEY_MAP_SIZE) break; + + const char *label = key_map[current_key].label; + int width = 1; + + // Dynamic width logic + if (g_strcmp0(label, "Space") == 0) width = 7; + else if (g_strcmp0(label, "Backspace") == 0) width = 2; + else if (g_strcmp0(label, "Enter") == 0) width = 3; + else if (g_strcmp0(label, "Shift") == 0) width = 3; + else if (g_strcmp0(label, "Alt") == 0) width = 2; + else if (g_strcmp0(label, "Ctrl") == 0) width = 3; + else if (g_strcmp0(label, "Caps Lock") == 0) width = 2; + + add_key(rows[r], label, current_col, width); + current_col += width; + current_key++; + } + } + + init_evdev_keyboard(); + gtk_window_present(GTK_WINDOW(window)); + g_object_unref(builder); +} + +int main(int argc, char **argv) { + GtkApplication *app = gtk_application_new("org.spaces.oskeyboard", G_APPLICATION_DEFAULT_FLAGS); + g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); + int status = g_application_run(G_APPLICATION(app), argc, argv); + g_object_unref(app); + return status; +} diff --git a/resources.xml b/resources.xml new file mode 100644 index 0000000..f01ba4b --- /dev/null +++ b/resources.xml @@ -0,0 +1,7 @@ + + + + keyboard.ui + style.css + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..83bb1e9 --- /dev/null +++ b/style.css @@ -0,0 +1,26 @@ +* { + background-color: transparent; +} + +window { + margin-bottom: 25px; +} + +button { + background-color: black; + border: 3px solid white; + border-radius: 15px; + background-color: black; + margin: 0; + padding: 0; + + min-width: 94px; + min-height: 94px; +} + +button > * { + background-color: black; + color: white; + border-radius: 15px; + font-size: 20px; +}