commit 4f9fd2b7c230a2c5abb7ffcd8e8dc24716d3d557 Author: Josh Ashton Date: Wed Feb 4 19:34:55 2026 -0700 init commit 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; +}