From b30979988f34202253810df3f8c7a59aab8b08ef Mon Sep 17 00:00:00 2001 From: cilginc <104676201+cilginc@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:14:17 +0300 Subject: [PATCH 01/24] feat: Use system-wide Escape key for exiting --- main.go | 2 +- wayland.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 2549a25..44f4ace 100644 --- a/main.go +++ b/main.go @@ -1168,7 +1168,7 @@ func main() { app.updateCursor(window) if key, state, hasKey := window.GetLastKey(); hasKey { - if state == 1 && key == 1 { + if state == 1 && key == 0xff1b { if !app.isExiting { app.isExiting = true app.exitStartTime = time.Now() diff --git a/wayland.go b/wayland.go index 9dac652..ee5cd3b 100644 --- a/wayland.go +++ b/wayland.go @@ -1,8 +1,8 @@ package main /* -#cgo pkg-config: wayland-client wayland-egl egl gl -#cgo LDFLAGS: -lwayland-client -lwayland-egl -lEGL -lGL +#cgo pkg-config: wayland-client wayland-egl egl gl xkbcommon +#cgo LDFLAGS: -lwayland-client -lwayland-egl -lEGL -lGL -lxkbcommon #cgo CFLAGS: -I. #include #include @@ -12,6 +12,8 @@ package main #include #include "wlr-layer-shell-client.h" #include "keyboard-shortcuts-inhibit-client.h" +#include +#include #include #include @@ -121,6 +123,9 @@ struct wl_keyboard *keyboard = NULL; struct zwp_keyboard_shortcuts_inhibit_manager_v1 *shortcuts_inhibit_manager = NULL; struct zwp_keyboard_shortcuts_inhibitor_v1 *shortcuts_inhibitor = NULL; struct zwlr_layer_surface_v1 *layer_surface_global = NULL; +struct xkb_context *xkb_context; +struct xkb_keymap *xkb_keymap; +struct xkb_state *xkb_state; int32_t width_global = 0; int32_t height_global = 0; @@ -300,7 +305,31 @@ static uint32_t last_key_state = 0; void keyboard_keymap(void *data, struct wl_keyboard *keyboard, uint32_t format, int32_t fd, uint32_t size) { + if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { + close(fd); + return; + } + + char *map_shm = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); + if (map_shm == MAP_FAILED) { + close(fd); + return; + } + + xkb_keymap = xkb_keymap_new_from_string(xkb_context, map_shm, + XKB_KEYMAP_FORMAT_TEXT_V1, + XKB_KEYMAP_COMPILE_NO_FLAGS); + munmap(map_shm, size); close(fd); + + if (!xkb_keymap) { + return; + } + + xkb_state = xkb_state_new(xkb_keymap); + if (!xkb_state) { + return; + } } void keyboard_enter(void *data, struct wl_keyboard *keyboard, uint32_t serial, @@ -313,13 +342,24 @@ void keyboard_leave(void *data, struct wl_keyboard *keyboard, uint32_t serial, void keyboard_key(void *data, struct wl_keyboard *keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { - last_key = key; - last_key_state = state; + if (xkb_state) { + xkb_keysym_t sym = xkb_state_key_get_one_sym(xkb_state, key + 8); + if (state == WL_KEYBOARD_KEY_STATE_PRESSED) { + last_key = sym; + last_key_state = 1; + } else { + last_key = 0; + last_key_state = 0; + } + } } void keyboard_modifiers(void *data, struct wl_keyboard *keyboard, uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { + if (xkb_state) { + xkb_state_update_mask(xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group); + } } void keyboard_repeat_info(void *data, struct wl_keyboard *keyboard, @@ -411,6 +451,11 @@ type WaylandWindow struct { func NewWaylandWindow() (*WaylandWindow, error) { w := &WaylandWindow{} + C.xkb_context = C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS) + if C.xkb_context == nil { + return nil, &WaylandError{"failed to create xkb context"} + } + w.display = C.wl_display_connect(nil) if w.display == nil { return nil, &WaylandError{"failed to connect to Wayland display"} From 6d3251d3cb135a0352974357114acff2cd14f5a5 Mon Sep 17 00:00:00 2001 From: cilginc <104676201+cilginc@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:45:44 +0300 Subject: [PATCH 02/24] chore: updated github project structure --- CODE_OF_CONDUCT.md => .github/CODE_OF_CONDUCT.md | 0 SECURITY.md => .github/SECURITY.md | 0 {assets => .github/assets}/demo.gif | Bin .gitignore | 2 +- README.md | 2 +- 5 files changed, 2 insertions(+), 2 deletions(-) rename CODE_OF_CONDUCT.md => .github/CODE_OF_CONDUCT.md (100%) rename SECURITY.md => .github/SECURITY.md (100%) rename {assets => .github/assets}/demo.gif (100%) diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md diff --git a/SECURITY.md b/.github/SECURITY.md similarity index 100% rename from SECURITY.md rename to .github/SECURITY.md diff --git a/assets/demo.gif b/.github/assets/demo.gif similarity index 100% rename from assets/demo.gif rename to .github/assets/demo.gif diff --git a/.gitignore b/.gitignore index ba6b563..ffa95da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -result/ +bin/ hexecute diff --git a/README.md b/README.md index a56766e..2e342a3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A gesture-based launcher for Wayland. Launch apps by casting spells! 🪄 -![Demo GIF](assets/demo.gif) +![Demo GIF](.github/assets/demo.gif) ## Installation From 6f4c65507a93083e94cce111e7fb981bf4e467f4 Mon Sep 17 00:00:00 2001 From: cilginc <104676201+cilginc@users.noreply.github.com> Date: Wed, 15 Oct 2025 21:07:48 +0300 Subject: [PATCH 03/24] fix: go module --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 54569a0..8772d7e 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module hexecute +module github.com/ThatOtherAndrew/Hexecute go 1.25.1 From d81576cc1823c9d7b8ba0a17d0b081f00a8ce988 Mon Sep 17 00:00:00 2001 From: cilginc <104676201+cilginc@users.noreply.github.com> Date: Wed, 15 Oct 2025 22:02:30 +0300 Subject: [PATCH 04/24] refactor: refactored wayland package --- main.go | 17 +- pkg/wayland/.clangd | 4 + .../keyboard-shortcuts-inhibit-client.h | 216 +++--- pkg/wayland/wayland.c | 416 ++++++++++++ pkg/wayland/wayland.go | 223 ++++++ pkg/wayland/wayland.h | 77 +++ .../wayland/wlr-layer-shell-client.h | 502 +++++++------- wayland.go | 635 ------------------ 8 files changed, 1112 insertions(+), 978 deletions(-) create mode 100644 pkg/wayland/.clangd rename keyboard-shortcuts-inhibit-client.h => pkg/wayland/keyboard-shortcuts-inhibit-client.h (62%) create mode 100644 pkg/wayland/wayland.c create mode 100644 pkg/wayland/wayland.go create mode 100644 pkg/wayland/wayland.h rename wlr-layer-shell-client.h => pkg/wayland/wlr-layer-shell-client.h (59%) delete mode 100644 wayland.go diff --git a/main.go b/main.go index 44f4ace..717ef8e 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "syscall" "time" + "github.com/ThatOtherAndrew/Hexecute/pkg/wayland" "github.com/go-gl/gl/v4.1-core/gl" ) @@ -641,7 +642,7 @@ func (a *App) updateParticles(dt float32) { } } -func (a *App) updateCursor(window *WaylandWindow) { +func (a *App) updateCursor(window *wayland.WaylandWindow) { x, y := window.GetCursorPos() fx, fy := float32(x), float32(y) @@ -681,7 +682,7 @@ func (a *App) updateCursor(window *WaylandWindow) { a.lastCursorY = fy } -func (a *App) draw(window *WaylandWindow) { +func (a *App) draw(window *wayland.WaylandWindow) { gl.Clear(gl.COLOR_BUFFER_BIT) currentTime := float32(time.Since(a.startTime).Seconds()) @@ -700,7 +701,7 @@ func (a *App) draw(window *WaylandWindow) { a.drawParticles(window) } -func (a *App) drawLine(window *WaylandWindow, baseThickness, baseAlpha, currentTime float32) { +func (a *App) drawLine(window *wayland.WaylandWindow, baseThickness, baseAlpha, currentTime float32) { if len(a.points) < 2 { return } @@ -789,7 +790,7 @@ func (a *App) drawLine(window *WaylandWindow, baseThickness, baseAlpha, currentT gl.BindVertexArray(0) } -func (a *App) drawParticles(window *WaylandWindow) { +func (a *App) drawParticles(window *wayland.WaylandWindow) { if len(a.particles) == 0 { return } @@ -813,7 +814,7 @@ func (a *App) drawParticles(window *WaylandWindow) { gl.BindVertexArray(0) } -func (a *App) drawBackground(currentTime float32, window *WaylandWindow) { +func (a *App) drawBackground(currentTime float32, window *wayland.WaylandWindow) { fadeDuration := float32(1.0) targetAlpha := float32(0.75) @@ -861,7 +862,7 @@ func (a *App) drawBackground(currentTime float32, window *WaylandWindow) { gl.BlendFunc(gl.SRC_ALPHA, gl.ONE) } -func (a *App) drawCursorGlow(window *WaylandWindow, cursorX, cursorY, currentTime float32) { +func (a *App) drawCursorGlow(window *wayland.WaylandWindow, cursorX, cursorY, currentTime float32) { width, height := window.GetSize() growDuration := float32(1.2) @@ -1016,7 +1017,7 @@ func executeCommand(command string) error { return cmd.Start() } -func (a *App) recognizeAndExecute(window *WaylandWindow, x, y float32) { +func (a *App) recognizeAndExecute(window *wayland.WaylandWindow, x, y float32) { if len(a.points) < 5 { log.Println("Gesture too short, ignoring") return @@ -1119,7 +1120,7 @@ func main() { return } - window, err := NewWaylandWindow() + window, err := wayland.NewWaylandWindow() if err != nil { log.Fatal("Failed to create Wayland window:", err) } diff --git a/pkg/wayland/.clangd b/pkg/wayland/.clangd new file mode 100644 index 0000000..0da6147 --- /dev/null +++ b/pkg/wayland/.clangd @@ -0,0 +1,4 @@ +Diagnostics: + Suppress: + - keyword_as_parameter + - expected_expression diff --git a/keyboard-shortcuts-inhibit-client.h b/pkg/wayland/keyboard-shortcuts-inhibit-client.h similarity index 62% rename from keyboard-shortcuts-inhibit-client.h rename to pkg/wayland/keyboard-shortcuts-inhibit-client.h index 7564fcc..637805e 100644 --- a/keyboard-shortcuts-inhibit-client.h +++ b/pkg/wayland/keyboard-shortcuts-inhibit-client.h @@ -3,17 +3,18 @@ #ifndef KEYBOARD_SHORTCUTS_INHIBIT_UNSTABLE_V1_CLIENT_PROTOCOL_H #define KEYBOARD_SHORTCUTS_INHIBIT_UNSTABLE_V1_CLIENT_PROTOCOL_H -#include -#include #include "wayland-client.h" +#include +#include -#ifdef __cplusplus +#ifdef __cplusplus extern "C" { #endif /** - * @page page_keyboard_shortcuts_inhibit_unstable_v1 The keyboard_shortcuts_inhibit_unstable_v1 protocol - * Protocol for inhibiting the compositor keyboard shortcuts + * @page page_keyboard_shortcuts_inhibit_unstable_v1 The + * keyboard_shortcuts_inhibit_unstable_v1 protocol Protocol for inhibiting the + * compositor keyboard shortcuts * * @section page_desc_keyboard_shortcuts_inhibit_unstable_v1 Description * @@ -33,8 +34,10 @@ extern "C" { * reset. * * @section page_ifaces_keyboard_shortcuts_inhibit_unstable_v1 Interfaces - * - @subpage page_iface_zwp_keyboard_shortcuts_inhibit_manager_v1 - context object for keyboard grab_manager - * - @subpage page_iface_zwp_keyboard_shortcuts_inhibitor_v1 - context object for keyboard shortcuts inhibitor + * - @subpage page_iface_zwp_keyboard_shortcuts_inhibit_manager_v1 - context + * object for keyboard grab_manager + * - @subpage page_iface_zwp_keyboard_shortcuts_inhibitor_v1 - context object + * for keyboard shortcuts inhibitor * @section page_copyright_keyboard_shortcuts_inhibit_unstable_v1 Copyright *
  *
@@ -68,24 +71,29 @@ struct zwp_keyboard_shortcuts_inhibitor_v1;
 #ifndef ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_INTERFACE
 #define ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_INTERFACE
 /**
- * @page page_iface_zwp_keyboard_shortcuts_inhibit_manager_v1 zwp_keyboard_shortcuts_inhibit_manager_v1
- * @section page_iface_zwp_keyboard_shortcuts_inhibit_manager_v1_desc Description
+ * @page page_iface_zwp_keyboard_shortcuts_inhibit_manager_v1
+ * zwp_keyboard_shortcuts_inhibit_manager_v1
+ * @section page_iface_zwp_keyboard_shortcuts_inhibit_manager_v1_desc
+ * Description
  *
  * A global interface used for inhibiting the compositor keyboard shortcuts.
  * @section page_iface_zwp_keyboard_shortcuts_inhibit_manager_v1_api API
  * See @ref iface_zwp_keyboard_shortcuts_inhibit_manager_v1.
  */
 /**
- * @defgroup iface_zwp_keyboard_shortcuts_inhibit_manager_v1 The zwp_keyboard_shortcuts_inhibit_manager_v1 interface
+ * @defgroup iface_zwp_keyboard_shortcuts_inhibit_manager_v1 The
+ * zwp_keyboard_shortcuts_inhibit_manager_v1 interface
  *
  * A global interface used for inhibiting the compositor keyboard shortcuts.
  */
-extern const struct wl_interface zwp_keyboard_shortcuts_inhibit_manager_v1_interface;
+extern const struct wl_interface
+    zwp_keyboard_shortcuts_inhibit_manager_v1_interface;
 #endif
 #ifndef ZWP_KEYBOARD_SHORTCUTS_INHIBITOR_V1_INTERFACE
 #define ZWP_KEYBOARD_SHORTCUTS_INHIBITOR_V1_INTERFACE
 /**
- * @page page_iface_zwp_keyboard_shortcuts_inhibitor_v1 zwp_keyboard_shortcuts_inhibitor_v1
+ * @page page_iface_zwp_keyboard_shortcuts_inhibitor_v1
+ * zwp_keyboard_shortcuts_inhibitor_v1
  * @section page_iface_zwp_keyboard_shortcuts_inhibitor_v1_desc Description
  *
  * A keyboard shortcuts inhibitor instructs the compositor to ignore
@@ -125,7 +133,8 @@ extern const struct wl_interface zwp_keyboard_shortcuts_inhibit_manager_v1_inter
  * See @ref iface_zwp_keyboard_shortcuts_inhibitor_v1.
  */
 /**
- * @defgroup iface_zwp_keyboard_shortcuts_inhibitor_v1 The zwp_keyboard_shortcuts_inhibitor_v1 interface
+ * @defgroup iface_zwp_keyboard_shortcuts_inhibitor_v1 The
+ * zwp_keyboard_shortcuts_inhibitor_v1 interface
  *
  * A keyboard shortcuts inhibitor instructs the compositor to ignore
  * its own keyboard shortcuts when the associated surface has keyboard
@@ -167,17 +176,16 @@ extern const struct wl_interface zwp_keyboard_shortcuts_inhibitor_v1_interface;
 #ifndef ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_ERROR_ENUM
 #define ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_ERROR_ENUM
 enum zwp_keyboard_shortcuts_inhibit_manager_v1_error {
-	/**
-	 * the shortcuts are already inhibited for this surface
-	 */
-	ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_ERROR_ALREADY_INHIBITED = 0,
+  /**
+   * the shortcuts are already inhibited for this surface
+   */
+  ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_ERROR_ALREADY_INHIBITED = 0,
 };
 #endif /* ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_ERROR_ENUM */
 
 #define ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_DESTROY 0
 #define ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_INHIBIT_SHORTCUTS 1
 
-
 /**
  * @ingroup iface_zwp_keyboard_shortcuts_inhibit_manager_v1
  */
@@ -185,26 +193,31 @@ enum zwp_keyboard_shortcuts_inhibit_manager_v1_error {
 /**
  * @ingroup iface_zwp_keyboard_shortcuts_inhibit_manager_v1
  */
-#define ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_INHIBIT_SHORTCUTS_SINCE_VERSION 1
+#define ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_INHIBIT_SHORTCUTS_SINCE_VERSION \
+  1
 
 /** @ingroup iface_zwp_keyboard_shortcuts_inhibit_manager_v1 */
-static inline void
-zwp_keyboard_shortcuts_inhibit_manager_v1_set_user_data(struct zwp_keyboard_shortcuts_inhibit_manager_v1 *zwp_keyboard_shortcuts_inhibit_manager_v1, void *user_data)
-{
-	wl_proxy_set_user_data((struct wl_proxy *) zwp_keyboard_shortcuts_inhibit_manager_v1, user_data);
+static inline void zwp_keyboard_shortcuts_inhibit_manager_v1_set_user_data(
+    struct zwp_keyboard_shortcuts_inhibit_manager_v1
+        *zwp_keyboard_shortcuts_inhibit_manager_v1,
+    void *user_data) {
+  wl_proxy_set_user_data(
+      (struct wl_proxy *)zwp_keyboard_shortcuts_inhibit_manager_v1, user_data);
 }
 
 /** @ingroup iface_zwp_keyboard_shortcuts_inhibit_manager_v1 */
-static inline void *
-zwp_keyboard_shortcuts_inhibit_manager_v1_get_user_data(struct zwp_keyboard_shortcuts_inhibit_manager_v1 *zwp_keyboard_shortcuts_inhibit_manager_v1)
-{
-	return wl_proxy_get_user_data((struct wl_proxy *) zwp_keyboard_shortcuts_inhibit_manager_v1);
+static inline void *zwp_keyboard_shortcuts_inhibit_manager_v1_get_user_data(
+    struct zwp_keyboard_shortcuts_inhibit_manager_v1
+        *zwp_keyboard_shortcuts_inhibit_manager_v1) {
+  return wl_proxy_get_user_data(
+      (struct wl_proxy *)zwp_keyboard_shortcuts_inhibit_manager_v1);
 }
 
-static inline uint32_t
-zwp_keyboard_shortcuts_inhibit_manager_v1_get_version(struct zwp_keyboard_shortcuts_inhibit_manager_v1 *zwp_keyboard_shortcuts_inhibit_manager_v1)
-{
-	return wl_proxy_get_version((struct wl_proxy *) zwp_keyboard_shortcuts_inhibit_manager_v1);
+static inline uint32_t zwp_keyboard_shortcuts_inhibit_manager_v1_get_version(
+    struct zwp_keyboard_shortcuts_inhibit_manager_v1
+        *zwp_keyboard_shortcuts_inhibit_manager_v1) {
+  return wl_proxy_get_version(
+      (struct wl_proxy *)zwp_keyboard_shortcuts_inhibit_manager_v1);
 }
 
 /**
@@ -212,11 +225,15 @@ zwp_keyboard_shortcuts_inhibit_manager_v1_get_version(struct zwp_keyboard_shortc
  *
  * Destroy the keyboard shortcuts inhibitor manager.
  */
-static inline void
-zwp_keyboard_shortcuts_inhibit_manager_v1_destroy(struct zwp_keyboard_shortcuts_inhibit_manager_v1 *zwp_keyboard_shortcuts_inhibit_manager_v1)
-{
-	wl_proxy_marshal_flags((struct wl_proxy *) zwp_keyboard_shortcuts_inhibit_manager_v1,
-			 ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_keyboard_shortcuts_inhibit_manager_v1), WL_MARSHAL_FLAG_DESTROY);
+static inline void zwp_keyboard_shortcuts_inhibit_manager_v1_destroy(
+    struct zwp_keyboard_shortcuts_inhibit_manager_v1
+        *zwp_keyboard_shortcuts_inhibit_manager_v1) {
+  wl_proxy_marshal_flags(
+      (struct wl_proxy *)zwp_keyboard_shortcuts_inhibit_manager_v1,
+      ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_DESTROY, NULL,
+      wl_proxy_get_version(
+          (struct wl_proxy *)zwp_keyboard_shortcuts_inhibit_manager_v1),
+      WL_MARSHAL_FLAG_DESTROY);
 }
 
 /**
@@ -229,14 +246,21 @@ zwp_keyboard_shortcuts_inhibit_manager_v1_destroy(struct zwp_keyboard_shortcuts_
  * a protocol error "already_inhibited" is raised by the compositor.
  */
 static inline struct zwp_keyboard_shortcuts_inhibitor_v1 *
-zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(struct zwp_keyboard_shortcuts_inhibit_manager_v1 *zwp_keyboard_shortcuts_inhibit_manager_v1, struct wl_surface *surface, struct wl_seat *seat)
-{
-	struct wl_proxy *id;
+zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(
+    struct zwp_keyboard_shortcuts_inhibit_manager_v1
+        *zwp_keyboard_shortcuts_inhibit_manager_v1,
+    struct wl_surface *surface, struct wl_seat *seat) {
+  struct wl_proxy *id;
 
-	id = wl_proxy_marshal_flags((struct wl_proxy *) zwp_keyboard_shortcuts_inhibit_manager_v1,
-			 ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_INHIBIT_SHORTCUTS, &zwp_keyboard_shortcuts_inhibitor_v1_interface, wl_proxy_get_version((struct wl_proxy *) zwp_keyboard_shortcuts_inhibit_manager_v1), 0, NULL, surface, seat);
+  id = wl_proxy_marshal_flags(
+      (struct wl_proxy *)zwp_keyboard_shortcuts_inhibit_manager_v1,
+      ZWP_KEYBOARD_SHORTCUTS_INHIBIT_MANAGER_V1_INHIBIT_SHORTCUTS,
+      &zwp_keyboard_shortcuts_inhibitor_v1_interface,
+      wl_proxy_get_version(
+          (struct wl_proxy *)zwp_keyboard_shortcuts_inhibit_manager_v1),
+      0, NULL, surface, seat);
 
-	return (struct zwp_keyboard_shortcuts_inhibitor_v1 *) id;
+  return (struct zwp_keyboard_shortcuts_inhibitor_v1 *)id;
 }
 
 /**
@@ -244,42 +268,44 @@ zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(struct zwp_keyboard_
  * @struct zwp_keyboard_shortcuts_inhibitor_v1_listener
  */
 struct zwp_keyboard_shortcuts_inhibitor_v1_listener {
-	/**
-	 * shortcuts are inhibited
-	 *
-	 * This event indicates that the shortcut inhibitor is active.
-	 *
-	 * The compositor sends this event every time compositor shortcuts
-	 * are inhibited on behalf of the surface. When active, the client
-	 * may receive input events normally reserved by the compositor
-	 * (see zwp_keyboard_shortcuts_inhibitor_v1).
-	 *
-	 * This occurs typically when the initial request
-	 * "inhibit_shortcuts" first becomes active or when the user
-	 * instructs the compositor to re-enable and existing shortcuts
-	 * inhibitor using any mechanism offered by the compositor.
-	 */
-	void (*active)(void *data,
-		       struct zwp_keyboard_shortcuts_inhibitor_v1 *zwp_keyboard_shortcuts_inhibitor_v1);
-	/**
-	 * shortcuts are restored
-	 *
-	 * This event indicates that the shortcuts inhibitor is inactive,
-	 * normal shortcuts processing is restored by the compositor.
-	 */
-	void (*inactive)(void *data,
-			 struct zwp_keyboard_shortcuts_inhibitor_v1 *zwp_keyboard_shortcuts_inhibitor_v1);
+  /**
+   * shortcuts are inhibited
+   *
+   * This event indicates that the shortcut inhibitor is active.
+   *
+   * The compositor sends this event every time compositor shortcuts
+   * are inhibited on behalf of the surface. When active, the client
+   * may receive input events normally reserved by the compositor
+   * (see zwp_keyboard_shortcuts_inhibitor_v1).
+   *
+   * This occurs typically when the initial request
+   * "inhibit_shortcuts" first becomes active or when the user
+   * instructs the compositor to re-enable and existing shortcuts
+   * inhibitor using any mechanism offered by the compositor.
+   */
+  void (*active)(void *data, struct zwp_keyboard_shortcuts_inhibitor_v1
+                                 *zwp_keyboard_shortcuts_inhibitor_v1);
+  /**
+   * shortcuts are restored
+   *
+   * This event indicates that the shortcuts inhibitor is inactive,
+   * normal shortcuts processing is restored by the compositor.
+   */
+  void (*inactive)(void *data, struct zwp_keyboard_shortcuts_inhibitor_v1
+                                   *zwp_keyboard_shortcuts_inhibitor_v1);
 };
 
 /**
  * @ingroup iface_zwp_keyboard_shortcuts_inhibitor_v1
  */
-static inline int
-zwp_keyboard_shortcuts_inhibitor_v1_add_listener(struct zwp_keyboard_shortcuts_inhibitor_v1 *zwp_keyboard_shortcuts_inhibitor_v1,
-						 const struct zwp_keyboard_shortcuts_inhibitor_v1_listener *listener, void *data)
-{
-	return wl_proxy_add_listener((struct wl_proxy *) zwp_keyboard_shortcuts_inhibitor_v1,
-				     (void (**)(void)) listener, data);
+static inline int zwp_keyboard_shortcuts_inhibitor_v1_add_listener(
+    struct zwp_keyboard_shortcuts_inhibitor_v1
+        *zwp_keyboard_shortcuts_inhibitor_v1,
+    const struct zwp_keyboard_shortcuts_inhibitor_v1_listener *listener,
+    void *data) {
+  return wl_proxy_add_listener(
+      (struct wl_proxy *)zwp_keyboard_shortcuts_inhibitor_v1,
+      (void (**)(void))listener, data);
 }
 
 #define ZWP_KEYBOARD_SHORTCUTS_INHIBITOR_V1_DESTROY 0
@@ -299,23 +325,27 @@ zwp_keyboard_shortcuts_inhibitor_v1_add_listener(struct zwp_keyboard_shortcuts_i
 #define ZWP_KEYBOARD_SHORTCUTS_INHIBITOR_V1_DESTROY_SINCE_VERSION 1
 
 /** @ingroup iface_zwp_keyboard_shortcuts_inhibitor_v1 */
-static inline void
-zwp_keyboard_shortcuts_inhibitor_v1_set_user_data(struct zwp_keyboard_shortcuts_inhibitor_v1 *zwp_keyboard_shortcuts_inhibitor_v1, void *user_data)
-{
-	wl_proxy_set_user_data((struct wl_proxy *) zwp_keyboard_shortcuts_inhibitor_v1, user_data);
+static inline void zwp_keyboard_shortcuts_inhibitor_v1_set_user_data(
+    struct zwp_keyboard_shortcuts_inhibitor_v1
+        *zwp_keyboard_shortcuts_inhibitor_v1,
+    void *user_data) {
+  wl_proxy_set_user_data((struct wl_proxy *)zwp_keyboard_shortcuts_inhibitor_v1,
+                         user_data);
 }
 
 /** @ingroup iface_zwp_keyboard_shortcuts_inhibitor_v1 */
-static inline void *
-zwp_keyboard_shortcuts_inhibitor_v1_get_user_data(struct zwp_keyboard_shortcuts_inhibitor_v1 *zwp_keyboard_shortcuts_inhibitor_v1)
-{
-	return wl_proxy_get_user_data((struct wl_proxy *) zwp_keyboard_shortcuts_inhibitor_v1);
+static inline void *zwp_keyboard_shortcuts_inhibitor_v1_get_user_data(
+    struct zwp_keyboard_shortcuts_inhibitor_v1
+        *zwp_keyboard_shortcuts_inhibitor_v1) {
+  return wl_proxy_get_user_data(
+      (struct wl_proxy *)zwp_keyboard_shortcuts_inhibitor_v1);
 }
 
-static inline uint32_t
-zwp_keyboard_shortcuts_inhibitor_v1_get_version(struct zwp_keyboard_shortcuts_inhibitor_v1 *zwp_keyboard_shortcuts_inhibitor_v1)
-{
-	return wl_proxy_get_version((struct wl_proxy *) zwp_keyboard_shortcuts_inhibitor_v1);
+static inline uint32_t zwp_keyboard_shortcuts_inhibitor_v1_get_version(
+    struct zwp_keyboard_shortcuts_inhibitor_v1
+        *zwp_keyboard_shortcuts_inhibitor_v1) {
+  return wl_proxy_get_version(
+      (struct wl_proxy *)zwp_keyboard_shortcuts_inhibitor_v1);
 }
 
 /**
@@ -323,14 +353,18 @@ zwp_keyboard_shortcuts_inhibitor_v1_get_version(struct zwp_keyboard_shortcuts_in
  *
  * Remove the keyboard shortcuts inhibitor from the associated wl_surface.
  */
-static inline void
-zwp_keyboard_shortcuts_inhibitor_v1_destroy(struct zwp_keyboard_shortcuts_inhibitor_v1 *zwp_keyboard_shortcuts_inhibitor_v1)
-{
-	wl_proxy_marshal_flags((struct wl_proxy *) zwp_keyboard_shortcuts_inhibitor_v1,
-			 ZWP_KEYBOARD_SHORTCUTS_INHIBITOR_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_keyboard_shortcuts_inhibitor_v1), WL_MARSHAL_FLAG_DESTROY);
+static inline void zwp_keyboard_shortcuts_inhibitor_v1_destroy(
+    struct zwp_keyboard_shortcuts_inhibitor_v1
+        *zwp_keyboard_shortcuts_inhibitor_v1) {
+  wl_proxy_marshal_flags(
+      (struct wl_proxy *)zwp_keyboard_shortcuts_inhibitor_v1,
+      ZWP_KEYBOARD_SHORTCUTS_INHIBITOR_V1_DESTROY, NULL,
+      wl_proxy_get_version(
+          (struct wl_proxy *)zwp_keyboard_shortcuts_inhibitor_v1),
+      WL_MARSHAL_FLAG_DESTROY);
 }
 
-#ifdef  __cplusplus
+#ifdef __cplusplus
 }
 #endif
 
diff --git a/pkg/wayland/wayland.c b/pkg/wayland/wayland.c
new file mode 100644
index 0000000..93b07ad
--- /dev/null
+++ b/pkg/wayland/wayland.c
@@ -0,0 +1,416 @@
+#include "wayland.h"
+#include "keyboard-shortcuts-inhibit-client.h"
+#include "wlr-layer-shell-client.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#ifndef __has_attribute
+#define __has_attribute(x) 0
+#endif
+
+#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
+#define WL_PRIVATE __attribute__((visibility("hidden")))
+#else
+#define WL_PRIVATE
+#endif
+
+extern const struct wl_interface wl_output_interface;
+extern const struct wl_interface wl_surface_interface;
+extern const struct wl_interface zwlr_layer_surface_v1_interface;
+
+static const struct wl_interface xdg_popup_interface = {
+    "xdg_popup", 0, 0, NULL, 0, NULL,
+};
+
+static const struct wl_interface *wlr_layer_shell_unstable_v1_types[] = {
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    &zwlr_layer_surface_v1_interface,
+    &wl_surface_interface,
+    &wl_output_interface,
+    NULL,
+    NULL,
+    &xdg_popup_interface,
+};
+
+static const struct wl_message zwlr_layer_shell_v1_requests[] = {
+    {"get_layer_surface", "no?ous", wlr_layer_shell_unstable_v1_types + 4},
+    {"destroy", "3", wlr_layer_shell_unstable_v1_types + 0},
+};
+
+WL_PRIVATE const struct wl_interface zwlr_layer_shell_v1_interface = {
+    "zwlr_layer_shell_v1", 4, 2, zwlr_layer_shell_v1_requests, 0, NULL,
+};
+
+static const struct wl_message zwlr_layer_surface_v1_requests[] = {
+    {"set_size", "uu", wlr_layer_shell_unstable_v1_types + 0},
+    {"set_anchor", "u", wlr_layer_shell_unstable_v1_types + 0},
+    {"set_exclusive_zone", "i", wlr_layer_shell_unstable_v1_types + 0},
+    {"set_margin", "iiii", wlr_layer_shell_unstable_v1_types + 0},
+    {"set_keyboard_interactivity", "u", wlr_layer_shell_unstable_v1_types + 0},
+    {"get_popup", "o", wlr_layer_shell_unstable_v1_types + 9},
+    {"ack_configure", "u", wlr_layer_shell_unstable_v1_types + 0},
+    {"destroy", "", wlr_layer_shell_unstable_v1_types + 0},
+    {"set_layer", "2u", wlr_layer_shell_unstable_v1_types + 0},
+};
+
+static const struct wl_message zwlr_layer_surface_v1_events[] = {
+    {"configure", "uuu", wlr_layer_shell_unstable_v1_types + 0},
+    {"closed", "", wlr_layer_shell_unstable_v1_types + 0},
+};
+
+WL_PRIVATE const struct wl_interface zwlr_layer_surface_v1_interface = {
+    "zwlr_layer_surface_v1",        4, 9,
+    zwlr_layer_surface_v1_requests, 2, zwlr_layer_surface_v1_events,
+};
+
+static const struct wl_interface
+    *keyboard_shortcuts_inhibit_unstable_v1_types[] = {
+        &zwp_keyboard_shortcuts_inhibitor_v1_interface,
+        &wl_surface_interface,
+        &wl_seat_interface,
+};
+
+static const struct wl_message
+    zwp_keyboard_shortcuts_inhibit_manager_v1_requests[] = {
+        {"destroy", "", keyboard_shortcuts_inhibit_unstable_v1_types + 0},
+        {"inhibit_shortcuts", "noo",
+         keyboard_shortcuts_inhibit_unstable_v1_types + 0},
+};
+
+WL_PRIVATE const struct wl_interface
+    zwp_keyboard_shortcuts_inhibit_manager_v1_interface = {
+        "zwp_keyboard_shortcuts_inhibit_manager_v1",        1, 2,
+        zwp_keyboard_shortcuts_inhibit_manager_v1_requests, 0, NULL,
+};
+
+static const struct wl_message zwp_keyboard_shortcuts_inhibitor_v1_requests[] =
+    {
+        {"destroy", "", keyboard_shortcuts_inhibit_unstable_v1_types + 0},
+};
+
+static const struct wl_message zwp_keyboard_shortcuts_inhibitor_v1_events[] = {
+    {"active", "", keyboard_shortcuts_inhibit_unstable_v1_types + 0},
+    {"inactive", "", keyboard_shortcuts_inhibit_unstable_v1_types + 0},
+};
+
+WL_PRIVATE const struct wl_interface
+    zwp_keyboard_shortcuts_inhibitor_v1_interface = {
+        "zwp_keyboard_shortcuts_inhibitor_v1",
+        1,
+        1,
+        zwp_keyboard_shortcuts_inhibitor_v1_requests,
+        2,
+        zwp_keyboard_shortcuts_inhibitor_v1_events,
+};
+
+struct wl_compositor *compositor = NULL;
+struct zwlr_layer_shell_v1 *layer_shell = NULL;
+struct wl_seat *seat = NULL;
+struct wl_pointer *pointer = NULL;
+struct wl_keyboard *keyboard = NULL;
+struct zwp_keyboard_shortcuts_inhibit_manager_v1 *shortcuts_inhibit_manager =
+    NULL;
+struct zwp_keyboard_shortcuts_inhibitor_v1 *shortcuts_inhibitor = NULL;
+struct zwlr_layer_surface_v1 *layer_surface_global = NULL;
+struct xkb_context *xkb_context;
+struct xkb_keymap *xkb_keymap;
+struct xkb_state *xkb_state;
+int32_t width_global = 0;
+int32_t height_global = 0;
+
+void layer_surface_configure(void *data, struct zwlr_layer_surface_v1 *surface,
+                             uint32_t serial, uint32_t width, uint32_t height) {
+  width_global = width;
+  height_global = height;
+  zwlr_layer_surface_v1_ack_configure(surface, serial);
+}
+
+void layer_surface_closed(void *data, struct zwlr_layer_surface_v1 *surface) {}
+
+static struct zwlr_layer_surface_v1_listener layer_surface_listener = {
+    .configure = layer_surface_configure,
+    .closed = layer_surface_closed,
+};
+
+// Forward declarations for seat
+void seat_capabilities(void *data, struct wl_seat *seat, uint32_t capabilities);
+void seat_name(void *data, struct wl_seat *seat, const char *name);
+
+static const struct wl_seat_listener seat_listener = {
+    .capabilities = seat_capabilities,
+    .name = seat_name,
+};
+
+void registry_global(void *data, struct wl_registry *registry, uint32_t name,
+                     const char *interface, uint32_t version) {
+  if (strcmp(interface, "wl_compositor") == 0) {
+    compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 4);
+  } else if (strcmp(interface, "zwlr_layer_shell_v1") == 0) {
+    layer_shell = (struct zwlr_layer_shell_v1 *)wl_registry_bind(
+        registry, name, &zwlr_layer_shell_v1_interface, 1);
+  } else if (strcmp(interface, "wl_seat") == 0) {
+    seat = wl_registry_bind(registry, name, &wl_seat_interface, 1);
+    wl_seat_add_listener(seat, &seat_listener, NULL);
+  } else if (strcmp(interface, "zwp_keyboard_shortcuts_inhibit_manager_v1") ==
+             0) {
+    shortcuts_inhibit_manager =
+        (struct zwp_keyboard_shortcuts_inhibit_manager_v1 *)wl_registry_bind(
+            registry, name,
+            &zwp_keyboard_shortcuts_inhibit_manager_v1_interface, 1);
+  }
+}
+
+void registry_global_remove(void *data, struct wl_registry *registry,
+                            uint32_t name) {}
+
+static const struct wl_registry_listener registry_listener = {
+    .global = registry_global,
+    .global_remove = registry_global_remove,
+};
+
+struct wl_registry *get_registry(struct wl_display *display) {
+  return wl_display_get_registry(display);
+}
+
+void add_registry_listener(struct wl_registry *registry) {
+  wl_registry_add_listener(registry, ®istry_listener, NULL);
+}
+
+struct wl_surface *surface_global = NULL;
+
+struct zwlr_layer_surface_v1 *create_layer_surface(struct wl_surface *surface) {
+  surface_global = surface;
+
+  layer_surface_global = zwlr_layer_shell_v1_get_layer_surface(
+      layer_shell, surface, NULL, ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY,
+      "hexecute");
+
+  zwlr_layer_surface_v1_set_anchor(layer_surface_global,
+                                   ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
+                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM |
+                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT |
+                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT);
+
+  zwlr_layer_surface_v1_set_exclusive_zone(layer_surface_global, -1);
+
+  zwlr_layer_surface_v1_set_keyboard_interactivity(
+      layer_surface_global,
+      ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE);
+
+  zwlr_layer_surface_v1_add_listener(layer_surface_global,
+                                     &layer_surface_listener, NULL);
+
+  wl_surface_commit(surface);
+
+  return layer_surface_global;
+}
+
+void set_input_region(int32_t width, int32_t height) {
+  if (surface_global) {
+    struct wl_region *region = wl_compositor_create_region(compositor);
+    wl_region_add(region, 0, 0, width, height);
+    wl_surface_set_input_region(surface_global, region);
+    wl_region_destroy(region);
+    wl_surface_commit(surface_global);
+  }
+}
+
+void disable_all_input() {
+  if (shortcuts_inhibitor) {
+    zwp_keyboard_shortcuts_inhibitor_v1_destroy(shortcuts_inhibitor);
+    shortcuts_inhibitor = NULL;
+  }
+
+  if (layer_surface_global) {
+    zwlr_layer_surface_v1_set_keyboard_interactivity(
+        layer_surface_global,
+        ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE);
+  }
+
+  if (surface_global) {
+    struct wl_region *region = wl_compositor_create_region(compositor);
+    wl_surface_set_input_region(surface_global, region);
+    wl_region_destroy(region);
+    wl_surface_commit(surface_global);
+  }
+}
+
+static int button_state = 0;
+static double mouse_x = 0;
+static double mouse_y = 0;
+
+void pointer_enter(void *data, struct wl_pointer *pointer, uint32_t serial,
+                   struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y) {
+  mouse_x = wl_fixed_to_double(x);
+  mouse_y = wl_fixed_to_double(y);
+  wl_pointer_set_cursor(pointer, serial, NULL, 0, 0);
+}
+
+void pointer_leave(void *data, struct wl_pointer *pointer, uint32_t serial,
+                   struct wl_surface *surface) {}
+
+void pointer_motion(void *data, struct wl_pointer *pointer, uint32_t time,
+                    wl_fixed_t x, wl_fixed_t y) {
+  mouse_x = wl_fixed_to_double(x);
+  mouse_y = wl_fixed_to_double(y);
+}
+
+void pointer_button(void *data, struct wl_pointer *pointer, uint32_t serial,
+                    uint32_t time, uint32_t button, uint32_t state) {
+  if (button == 272) {
+    button_state = state;
+  }
+}
+
+void pointer_axis(void *data, struct wl_pointer *pointer, uint32_t time,
+                  uint32_t axis, wl_fixed_t value) {}
+
+void pointer_frame(void *data, struct wl_pointer *pointer) {}
+
+void pointer_axis_source(void *data, struct wl_pointer *pointer,
+                         uint32_t source) {}
+
+void pointer_axis_stop(void *data, struct wl_pointer *pointer, uint32_t time,
+                       uint32_t axis) {}
+
+void pointer_axis_discrete(void *data, struct wl_pointer *pointer,
+                           uint32_t axis, int32_t discrete) {}
+
+static const struct wl_pointer_listener pointer_listener = {
+    .enter = pointer_enter,
+    .leave = pointer_leave,
+    .motion = pointer_motion,
+    .button = pointer_button,
+    .axis = pointer_axis,
+    .frame = pointer_frame,
+    .axis_source = pointer_axis_source,
+    .axis_stop = pointer_axis_stop,
+    .axis_discrete = pointer_axis_discrete,
+};
+
+static uint32_t last_key = 0;
+static uint32_t last_key_state = 0;
+
+void keyboard_keymap(void *data, struct wl_keyboard *keyboard, uint32_t format,
+                     int32_t fd, uint32_t size) {
+  if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
+    close(fd);
+    return;
+  }
+
+  char *map_shm = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
+  if (map_shm == MAP_FAILED) {
+    close(fd);
+    return;
+  }
+
+  xkb_keymap = xkb_keymap_new_from_string(xkb_context, map_shm,
+                                          XKB_KEYMAP_FORMAT_TEXT_V1,
+                                          XKB_KEYMAP_COMPILE_NO_FLAGS);
+  munmap(map_shm, size);
+  close(fd);
+
+  if (!xkb_keymap) {
+    return;
+  }
+
+  xkb_state = xkb_state_new(xkb_keymap);
+  if (!xkb_state) {
+    return;
+  }
+}
+
+void keyboard_enter(void *data, struct wl_keyboard *keyboard, uint32_t serial,
+                    struct wl_surface *surface, struct wl_array *keys) {}
+
+void keyboard_leave(void *data, struct wl_keyboard *keyboard, uint32_t serial,
+                    struct wl_surface *surface) {}
+
+void keyboard_key(void *data, struct wl_keyboard *keyboard, uint32_t serial,
+                  uint32_t time, uint32_t key, uint32_t state) {
+  if (xkb_state) {
+    xkb_keysym_t sym = xkb_state_key_get_one_sym(xkb_state, key + 8);
+    if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
+      last_key = sym;
+      last_key_state = 1;
+    } else {
+      last_key = 0;
+      last_key_state = 0;
+    }
+  }
+}
+
+void keyboard_modifiers(void *data, struct wl_keyboard *keyboard,
+                        uint32_t serial, uint32_t mods_depressed,
+                        uint32_t mods_latched, uint32_t mods_locked,
+                        uint32_t group) {
+  if (xkb_state) {
+    xkb_state_update_mask(xkb_state, mods_depressed, mods_latched, mods_locked,
+                          0, 0, group);
+  }
+}
+
+void keyboard_repeat_info(void *data, struct wl_keyboard *keyboard,
+                          int32_t rate, int32_t delay) {}
+
+static const struct wl_keyboard_listener keyboard_listener = {
+    .keymap = keyboard_keymap,
+    .enter = keyboard_enter,
+    .leave = keyboard_leave,
+    .key = keyboard_key,
+    .modifiers = keyboard_modifiers,
+    .repeat_info = keyboard_repeat_info,
+};
+
+// Seat listener
+void seat_capabilities(void *data, struct wl_seat *seat,
+                       uint32_t capabilities) {
+  if (capabilities & WL_SEAT_CAPABILITY_POINTER) {
+    pointer = wl_seat_get_pointer(seat);
+    wl_pointer_add_listener(pointer, &pointer_listener, NULL);
+  }
+
+  if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) {
+    keyboard = wl_seat_get_keyboard(seat);
+    wl_keyboard_add_listener(keyboard, &keyboard_listener, NULL);
+
+    if (shortcuts_inhibit_manager && surface_global && !shortcuts_inhibitor) {
+      shortcuts_inhibitor =
+          zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(
+              shortcuts_inhibit_manager, surface_global, seat);
+    }
+  }
+}
+
+void seat_name(void *data, struct wl_seat *seat, const char *name) {}
+
+int get_button_state() { return button_state; }
+
+void get_mouse_pos(double *x, double *y) {
+  *x = mouse_x;
+  *y = mouse_y;
+}
+
+void get_dimensions(int32_t *w, int32_t *h) {
+  *w = width_global;
+  *h = height_global;
+}
+
+uint32_t get_last_key() { return last_key; }
+
+uint32_t get_last_key_state() { return last_key_state; }
+
+void clear_last_key() {
+  last_key = 0;
+  last_key_state = 0;
+}
+
+EGLNativeWindowType native_window(struct wl_egl_window *egl_window) {
+  return (EGLNativeWindowType)egl_window;
+}
diff --git a/pkg/wayland/wayland.go b/pkg/wayland/wayland.go
new file mode 100644
index 0000000..cf52f8f
--- /dev/null
+++ b/pkg/wayland/wayland.go
@@ -0,0 +1,223 @@
+package wayland
+
+/*
+#cgo pkg-config: wayland-client wayland-egl egl gl xkbcommon
+#cgo LDFLAGS: -lwayland-client -lwayland-egl -lEGL -lGL -lxkbcommon
+#cgo CFLAGS: -I.
+#include "wayland.h"
+*/
+import "C"
+import (
+	"fmt"
+)
+
+type WaylandError struct {
+	msg string
+}
+
+func (e *WaylandError) Error() string {
+	return e.msg
+}
+
+type WaylandWindow struct {
+	display       *C.struct_wl_display
+	registry      *C.struct_wl_registry
+	surface       *C.struct_wl_surface
+	layerSurface  *C.struct_zwlr_layer_surface_v1
+	eglWindow     *C.struct_wl_egl_window
+	eglDisplay    C.EGLDisplay
+	eglContext    C.EGLContext
+	eglSurface    C.EGLSurface
+	width, height int32
+}
+
+func NewWaylandWindow() (*WaylandWindow, error) {
+	w := &WaylandWindow{}
+
+	C.xkb_context = C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS)
+	if C.xkb_context == nil {
+		return nil, &WaylandError{"failed to create xkb context"}
+	}
+
+	w.display = C.wl_display_connect(nil)
+	if w.display == nil {
+		return nil, &WaylandError{"failed to connect to Wayland display"}
+	}
+
+	w.registry = C.get_registry(w.display)
+	C.add_registry_listener(w.registry)
+	C.wl_display_roundtrip(w.display)
+	if C.compositor == nil {
+		return nil, &WaylandError{"compositor not available"}
+	}
+	if C.layer_shell == nil {
+		return nil, &WaylandError{"layer shell not available"}
+	}
+
+	w.surface = C.wl_compositor_create_surface(C.compositor)
+	if w.surface == nil {
+		return nil, &WaylandError{"failed to create surface"}
+	}
+
+	w.layerSurface = C.create_layer_surface(w.surface)
+
+	C.wl_display_roundtrip(w.display)
+
+	var width, height C.int32_t
+	C.get_dimensions(&width, &height)
+	w.width = int32(width)
+	w.height = int32(height)
+
+	if w.width == 0 || w.height == 0 {
+		w.width = 1920
+		w.height = 1080
+	}
+
+	C.wl_display_roundtrip(w.display)
+
+	C.set_input_region(C.int32_t(w.width), C.int32_t(w.height))
+
+	if err := w.initEGL(); err != nil {
+		return nil, err
+	}
+
+	C.wl_surface_commit(w.surface)
+	C.wl_display_flush(w.display)
+
+	C.wl_display_roundtrip(w.display)
+	C.wl_display_roundtrip(w.display)
+	C.wl_display_flush(w.display)
+
+	return w, nil
+}
+
+func (w *WaylandWindow) initEGL() error {
+	w.eglWindow = C.wl_egl_window_create(w.surface, C.int(w.width), C.int(w.height))
+	if w.eglWindow == nil {
+		return fmt.Errorf("failed to create EGL window")
+	}
+
+	w.eglDisplay = C.eglGetDisplay(C.EGLNativeDisplayType(w.display))
+	if w.eglDisplay == C.EGLDisplay(C.EGL_NO_DISPLAY) {
+		return fmt.Errorf("failed to get EGL display")
+	}
+
+	var major, minor C.EGLint
+	if C.eglInitialize(w.eglDisplay, &major, &minor) == C.EGL_FALSE {
+		return fmt.Errorf("failed to initialize EGL")
+	}
+
+	configAttribs := []C.EGLint{
+		C.EGL_SURFACE_TYPE, C.EGL_WINDOW_BIT,
+		C.EGL_RED_SIZE, 8,
+		C.EGL_GREEN_SIZE, 8,
+		C.EGL_BLUE_SIZE, 8,
+		C.EGL_ALPHA_SIZE, 8,
+		C.EGL_RENDERABLE_TYPE, C.EGL_OPENGL_BIT,
+		C.EGL_NONE,
+	}
+
+	var config C.EGLConfig
+	var numConfigs C.EGLint
+	if C.eglChooseConfig(w.eglDisplay, &configAttribs[0], &config, 1, &numConfigs) == C.EGL_FALSE {
+		return fmt.Errorf("failed to choose EGL config")
+	}
+
+	C.eglBindAPI(C.EGL_OPENGL_API)
+	contextAttribs := []C.EGLint{
+		C.EGL_CONTEXT_MAJOR_VERSION, 4,
+		C.EGL_CONTEXT_MINOR_VERSION, 1,
+		C.EGL_CONTEXT_OPENGL_PROFILE_MASK, C.EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
+		C.EGL_NONE,
+	}
+
+	w.eglContext = C.eglCreateContext(w.eglDisplay, config, nil, &contextAttribs[0])
+	if w.eglContext == nil {
+		return fmt.Errorf("failed to create EGL context")
+	}
+
+	w.eglSurface = C.eglCreateWindowSurface(
+		w.eglDisplay,
+		config,
+		C.native_window(w.eglWindow),
+		nil,
+	)
+	if w.eglSurface == nil {
+		return fmt.Errorf("failed to create EGL surface")
+	}
+
+	if C.eglMakeCurrent(w.eglDisplay, w.eglSurface, w.eglSurface, w.eglContext) == C.EGL_FALSE {
+		return fmt.Errorf("failed to make EGL context current")
+	}
+
+	return nil
+}
+
+func (w *WaylandWindow) GetSize() (int, int) {
+	var width, height C.int32_t
+	C.get_dimensions(&width, &height)
+	if width > 0 && height > 0 {
+		w.width = int32(width)
+		w.height = int32(height)
+	}
+	return int(w.width), int(w.height)
+}
+
+func (w *WaylandWindow) ShouldClose() bool {
+	return false
+}
+
+func (w *WaylandWindow) SwapBuffers() {
+	C.eglSwapBuffers(w.eglDisplay, w.eglSurface)
+}
+
+func (w *WaylandWindow) PollEvents() {
+	C.wl_display_flush(w.display)
+	C.wl_display_dispatch_pending(w.display)
+}
+
+func (w *WaylandWindow) GetCursorPos() (float64, float64) {
+	var x, y C.double
+	C.get_mouse_pos(&x, &y)
+	return float64(x), float64(y)
+}
+
+func (w *WaylandWindow) GetMouseButton() bool {
+	state := C.get_button_state()
+	return state == 1
+}
+
+func (w *WaylandWindow) DisableInput() {
+	C.disable_all_input()
+}
+
+func (w *WaylandWindow) GetLastKey() (uint32, uint32, bool) {
+	key := uint32(C.get_last_key())
+	state := uint32(C.get_last_key_state())
+	return key, state, key != 0
+}
+
+func (w *WaylandWindow) ClearLastKey() {
+	C.clear_last_key()
+}
+
+func (w *WaylandWindow) Destroy() {
+	if w.eglContext != C.EGLContext(C.EGL_NO_CONTEXT) {
+		C.eglDestroyContext(w.eglDisplay, w.eglContext)
+	}
+	if w.eglSurface != C.EGLSurface(C.EGL_NO_SURFACE) {
+		C.eglDestroySurface(w.eglDisplay, w.eglSurface)
+	}
+	if w.eglWindow != nil {
+		C.wl_egl_window_destroy(w.eglWindow)
+	}
+	if w.eglDisplay != C.EGLDisplay(C.EGL_NO_DISPLAY) {
+		C.eglTerminate(w.eglDisplay)
+	}
+	if w.surface != nil {
+		C.wl_surface_destroy(w.surface)
+	}
+	if w.display != nil {
+		C.wl_display_disconnect(w.display)
+	}
+}
diff --git a/pkg/wayland/wayland.h b/pkg/wayland/wayland.h
new file mode 100644
index 0000000..4e685e9
--- /dev/null
+++ b/pkg/wayland/wayland.h
@@ -0,0 +1,77 @@
+#ifndef WAYLAND_H
+#define WAYLAND_H
+
+#include "wlr-layer-shell-client.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+
+void layer_surface_configure(void *data, struct zwlr_layer_surface_v1 *surface,
+                             uint32_t serial, uint32_t width, uint32_t height);
+void layer_surface_closed(void *data, struct zwlr_layer_surface_v1 *surface);
+void seat_capabilities(void *data, struct wl_seat *seat, uint32_t capabilities);
+void seat_name(void *data, struct wl_seat *seat, const char *name);
+void registry_global(void *data, struct wl_registry *registry, uint32_t name,
+                     const char *interface, uint32_t version);
+void registry_global_remove(void *data, struct wl_registry *registry,
+                            uint32_t name);
+struct wl_registry *get_registry(struct wl_display *display);
+void add_registry_listener(struct wl_registry *registry);
+struct zwlr_layer_surface_v1 *create_layer_surface(struct wl_surface *surface);
+void set_input_region(int32_t width, int32_t height);
+void disable_all_input();
+void pointer_enter(void *data, struct wl_pointer *pointer, uint32_t serial,
+                   struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y);
+void pointer_leave(void *data, struct wl_pointer *pointer, uint32_t serial,
+                   struct wl_surface *surface);
+void pointer_motion(void *data, struct wl_pointer *pointer, uint32_t time,
+                    wl_fixed_t x, wl_fixed_t y);
+void pointer_button(void *data, struct wl_pointer *pointer, uint32_t serial,
+                    uint32_t time, uint32_t button, uint32_t state);
+void pointer_axis(void *data, struct wl_pointer *pointer, uint32_t time,
+                  uint32_t axis, wl_fixed_t value);
+void pointer_frame(void *data, struct wl_pointer *pointer);
+void pointer_axis_source(void *data, struct wl_pointer *pointer,
+                         uint32_t source);
+void pointer_axis_stop(void *data, struct wl_pointer *pointer, uint32_t time,
+                       uint32_t axis);
+void pointer_axis_discrete(void *data, struct wl_pointer *pointer,
+                           uint32_t axis, int32_t discrete);
+void keyboard_keymap(void *data, struct wl_keyboard *keyboard, uint32_t format,
+                     int32_t fd, uint32_t size);
+void keyboard_enter(void *data, struct wl_keyboard *keyboard, uint32_t serial,
+                    struct wl_surface *surface, struct wl_array *keys);
+void keyboard_leave(void *data, struct wl_keyboard *keyboard, uint32_t serial,
+                    struct wl_surface *surface);
+void keyboard_key(void *data, struct wl_keyboard *keyboard, uint32_t serial,
+                  uint32_t time, uint32_t key, uint32_t state);
+void keyboard_modifiers(void *data, struct wl_keyboard *keyboard,
+                        uint32_t serial, uint32_t mods_depressed,
+                        uint32_t mods_latched, uint32_t mods_locked,
+                        uint32_t group);
+void keyboard_repeat_info(void *data, struct wl_keyboard *keyboard,
+                          int32_t rate, int32_t delay);
+int get_button_state();
+void get_mouse_pos(double *x, double *y);
+void get_dimensions(int32_t *w, int32_t *h);
+uint32_t get_last_key();
+uint32_t get_last_key_state();
+void clear_last_key();
+EGLNativeWindowType native_window(struct wl_egl_window *egl_window);
+
+extern struct wl_compositor *compositor;
+extern struct zwlr_layer_shell_v1 *layer_shell;
+extern struct wl_seat *seat;
+extern struct wl_pointer *pointer;
+extern struct wl_keyboard *keyboard;
+extern struct zwp_keyboard_shortcuts_inhibit_manager_v1
+    *shortcuts_inhibit_manager;
+extern struct zwp_keyboard_shortcuts_inhibitor_v1 *shortcuts_inhibitor;
+extern struct zwlr_layer_surface_v1 *layer_surface_global;
+extern struct xkb_context *xkb_context;
+extern struct xkb_keymap *xkb_keymap;
+extern struct xkb_state *xkb_state;
+
+#endif // WAYLAND_H
diff --git a/wlr-layer-shell-client.h b/pkg/wayland/wlr-layer-shell-client.h
similarity index 59%
rename from wlr-layer-shell-client.h
rename to pkg/wayland/wlr-layer-shell-client.h
index 847e201..231c34a 100644
--- a/wlr-layer-shell-client.h
+++ b/pkg/wayland/wlr-layer-shell-client.h
@@ -3,18 +3,20 @@
 #ifndef WLR_LAYER_SHELL_UNSTABLE_V1_CLIENT_PROTOCOL_H
 #define WLR_LAYER_SHELL_UNSTABLE_V1_CLIENT_PROTOCOL_H
 
-#include 
-#include 
 #include "wayland-client.h"
+#include 
+#include 
 
-#ifdef  __cplusplus
+#ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
- * @page page_wlr_layer_shell_unstable_v1 The wlr_layer_shell_unstable_v1 protocol
+ * @page page_wlr_layer_shell_unstable_v1 The wlr_layer_shell_unstable_v1
+ * protocol
  * @section page_ifaces_wlr_layer_shell_unstable_v1 Interfaces
- * - @subpage page_iface_zwlr_layer_shell_v1 - create surfaces that are layers of the desktop
+ * - @subpage page_iface_zwlr_layer_shell_v1 - create surfaces that are layers
+ * of the desktop
  * - @subpage page_iface_zwlr_layer_surface_v1 - layer metadata interface
  * @section page_copyright_wlr_layer_shell_unstable_v1 Copyright
  * 
@@ -127,18 +129,18 @@ extern const struct wl_interface zwlr_layer_surface_v1_interface;
 #ifndef ZWLR_LAYER_SHELL_V1_ERROR_ENUM
 #define ZWLR_LAYER_SHELL_V1_ERROR_ENUM
 enum zwlr_layer_shell_v1_error {
-	/**
-	 * wl_surface has another role
-	 */
-	ZWLR_LAYER_SHELL_V1_ERROR_ROLE = 0,
-	/**
-	 * layer value is invalid
-	 */
-	ZWLR_LAYER_SHELL_V1_ERROR_INVALID_LAYER = 1,
-	/**
-	 * wl_surface has a buffer attached or committed
-	 */
-	ZWLR_LAYER_SHELL_V1_ERROR_ALREADY_CONSTRUCTED = 2,
+  /**
+   * wl_surface has another role
+   */
+  ZWLR_LAYER_SHELL_V1_ERROR_ROLE = 0,
+  /**
+   * layer value is invalid
+   */
+  ZWLR_LAYER_SHELL_V1_ERROR_INVALID_LAYER = 1,
+  /**
+   * wl_surface has a buffer attached or committed
+   */
+  ZWLR_LAYER_SHELL_V1_ERROR_ALREADY_CONSTRUCTED = 2,
 };
 #endif /* ZWLR_LAYER_SHELL_V1_ERROR_ENUM */
 
@@ -156,17 +158,16 @@ enum zwlr_layer_shell_v1_error {
  * single layer is undefined.
  */
 enum zwlr_layer_shell_v1_layer {
-	ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND = 0,
-	ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM = 1,
-	ZWLR_LAYER_SHELL_V1_LAYER_TOP = 2,
-	ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY = 3,
+  ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND = 0,
+  ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM = 1,
+  ZWLR_LAYER_SHELL_V1_LAYER_TOP = 2,
+  ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY = 3,
 };
 #endif /* ZWLR_LAYER_SHELL_V1_LAYER_ENUM */
 
 #define ZWLR_LAYER_SHELL_V1_GET_LAYER_SURFACE 0
 #define ZWLR_LAYER_SHELL_V1_DESTROY 1
 
-
 /**
  * @ingroup iface_zwlr_layer_shell_v1
  */
@@ -177,23 +178,20 @@ enum zwlr_layer_shell_v1_layer {
 #define ZWLR_LAYER_SHELL_V1_DESTROY_SINCE_VERSION 3
 
 /** @ingroup iface_zwlr_layer_shell_v1 */
-static inline void
-zwlr_layer_shell_v1_set_user_data(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1, void *user_data)
-{
-	wl_proxy_set_user_data((struct wl_proxy *) zwlr_layer_shell_v1, user_data);
+static inline void zwlr_layer_shell_v1_set_user_data(
+    struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1, void *user_data) {
+  wl_proxy_set_user_data((struct wl_proxy *)zwlr_layer_shell_v1, user_data);
 }
 
 /** @ingroup iface_zwlr_layer_shell_v1 */
-static inline void *
-zwlr_layer_shell_v1_get_user_data(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1)
-{
-	return wl_proxy_get_user_data((struct wl_proxy *) zwlr_layer_shell_v1);
+static inline void *zwlr_layer_shell_v1_get_user_data(
+    struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1) {
+  return wl_proxy_get_user_data((struct wl_proxy *)zwlr_layer_shell_v1);
 }
 
-static inline uint32_t
-zwlr_layer_shell_v1_get_version(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1)
-{
-	return wl_proxy_get_version((struct wl_proxy *) zwlr_layer_shell_v1);
+static inline uint32_t zwlr_layer_shell_v1_get_version(
+    struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1) {
+  return wl_proxy_get_version((struct wl_proxy *)zwlr_layer_shell_v1);
 }
 
 /**
@@ -222,14 +220,18 @@ zwlr_layer_shell_v1_get_version(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1)
  * surface.
  */
 static inline struct zwlr_layer_surface_v1 *
-zwlr_layer_shell_v1_get_layer_surface(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1, struct wl_surface *surface, struct wl_output *output, uint32_t layer, const char *namespace)
-{
-	struct wl_proxy *id;
-
-	id = wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_shell_v1,
-			 ZWLR_LAYER_SHELL_V1_GET_LAYER_SURFACE, &zwlr_layer_surface_v1_interface, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_shell_v1), 0, NULL, surface, output, layer, namespace);
-
-	return (struct zwlr_layer_surface_v1 *) id;
+zwlr_layer_shell_v1_get_layer_surface(
+    struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1, struct wl_surface *surface,
+    struct wl_output *output, uint32_t layer, const char *namespace) {
+  struct wl_proxy *id;
+
+  id = wl_proxy_marshal_flags(
+      (struct wl_proxy *)zwlr_layer_shell_v1,
+      ZWLR_LAYER_SHELL_V1_GET_LAYER_SURFACE, &zwlr_layer_surface_v1_interface,
+      wl_proxy_get_version((struct wl_proxy *)zwlr_layer_shell_v1), 0, NULL,
+      surface, output, layer, namespace);
+
+  return (struct zwlr_layer_surface_v1 *)id;
 }
 
 /**
@@ -240,10 +242,11 @@ zwlr_layer_shell_v1_get_layer_surface(struct zwlr_layer_shell_v1 *zwlr_layer_she
  * are not affected.
  */
 static inline void
-zwlr_layer_shell_v1_destroy(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1)
-{
-	wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_shell_v1,
-			 ZWLR_LAYER_SHELL_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_shell_v1), WL_MARSHAL_FLAG_DESTROY);
+zwlr_layer_shell_v1_destroy(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1) {
+  wl_proxy_marshal_flags(
+      (struct wl_proxy *)zwlr_layer_shell_v1, ZWLR_LAYER_SHELL_V1_DESTROY, NULL,
+      wl_proxy_get_version((struct wl_proxy *)zwlr_layer_shell_v1),
+      WL_MARSHAL_FLAG_DESTROY);
 }
 
 #ifndef ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ENUM
@@ -259,66 +262,66 @@ zwlr_layer_shell_v1_destroy(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1)
  * keyboard focus.
  */
 enum zwlr_layer_surface_v1_keyboard_interactivity {
-	/**
-	 * no keyboard focus is possible
-	 *
-	 * This value indicates that this surface is not interested in
-	 * keyboard events and the compositor should never assign it the
-	 * keyboard focus.
-	 *
-	 * This is the default value, set for newly created layer shell
-	 * surfaces.
-	 *
-	 * This is useful for e.g. desktop widgets that display information
-	 * or only have interaction with non-keyboard input devices.
-	 */
-	ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE = 0,
-	/**
-	 * request exclusive keyboard focus
-	 *
-	 * Request exclusive keyboard focus if this surface is above the
-	 * shell surface layer.
-	 *
-	 * For the top and overlay layers, the seat will always give
-	 * exclusive keyboard focus to the top-most layer which has
-	 * keyboard interactivity set to exclusive. If this layer contains
-	 * multiple surfaces with keyboard interactivity set to exclusive,
-	 * the compositor determines the one receiving keyboard events in
-	 * an implementation- defined manner. In this case, no guarantee is
-	 * made when this surface will receive keyboard focus (if ever).
-	 *
-	 * For the bottom and background layers, the compositor is allowed
-	 * to use normal focus semantics.
-	 *
-	 * This setting is mainly intended for applications that need to
-	 * ensure they receive all keyboard events, such as a lock screen
-	 * or a password prompt.
-	 */
-	ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE = 1,
-	/**
-	 * request regular keyboard focus semantics
-	 *
-	 * This requests the compositor to allow this surface to be
-	 * focused and unfocused by the user in an implementation-defined
-	 * manner. The user should be able to unfocus this surface even
-	 * regardless of the layer it is on.
-	 *
-	 * Typically, the compositor will want to use its normal mechanism
-	 * to manage keyboard focus between layer shell surfaces with this
-	 * setting and regular toplevels on the desktop layer (e.g. click
-	 * to focus). Nevertheless, it is possible for a compositor to
-	 * require a special interaction to focus or unfocus layer shell
-	 * surfaces (e.g. requiring a click even if focus follows the mouse
-	 * normally, or providing a keybinding to switch focus between
-	 * layers).
-	 *
-	 * This setting is mainly intended for desktop shell components
-	 * (e.g. panels) that allow keyboard interaction. Using this option
-	 * can allow implementing a desktop shell that can be fully usable
-	 * without the mouse.
-	 * @since 4
-	 */
-	ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND = 2,
+  /**
+   * no keyboard focus is possible
+   *
+   * This value indicates that this surface is not interested in
+   * keyboard events and the compositor should never assign it the
+   * keyboard focus.
+   *
+   * This is the default value, set for newly created layer shell
+   * surfaces.
+   *
+   * This is useful for e.g. desktop widgets that display information
+   * or only have interaction with non-keyboard input devices.
+   */
+  ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE = 0,
+  /**
+   * request exclusive keyboard focus
+   *
+   * Request exclusive keyboard focus if this surface is above the
+   * shell surface layer.
+   *
+   * For the top and overlay layers, the seat will always give
+   * exclusive keyboard focus to the top-most layer which has
+   * keyboard interactivity set to exclusive. If this layer contains
+   * multiple surfaces with keyboard interactivity set to exclusive,
+   * the compositor determines the one receiving keyboard events in
+   * an implementation- defined manner. In this case, no guarantee is
+   * made when this surface will receive keyboard focus (if ever).
+   *
+   * For the bottom and background layers, the compositor is allowed
+   * to use normal focus semantics.
+   *
+   * This setting is mainly intended for applications that need to
+   * ensure they receive all keyboard events, such as a lock screen
+   * or a password prompt.
+   */
+  ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE = 1,
+  /**
+   * request regular keyboard focus semantics
+   *
+   * This requests the compositor to allow this surface to be
+   * focused and unfocused by the user in an implementation-defined
+   * manner. The user should be able to unfocus this surface even
+   * regardless of the layer it is on.
+   *
+   * Typically, the compositor will want to use its normal mechanism
+   * to manage keyboard focus between layer shell surfaces with this
+   * setting and regular toplevels on the desktop layer (e.g. click
+   * to focus). Nevertheless, it is possible for a compositor to
+   * require a special interaction to focus or unfocus layer shell
+   * surfaces (e.g. requiring a click even if focus follows the mouse
+   * normally, or providing a keybinding to switch focus between
+   * layers).
+   *
+   * This setting is mainly intended for desktop shell components
+   * (e.g. panels) that allow keyboard interaction. Using this option
+   * can allow implementing a desktop shell that can be fully usable
+   * without the mouse.
+   * @since 4
+   */
+  ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND = 2,
 };
 /**
  * @ingroup iface_zwlr_layer_surface_v1
@@ -329,44 +332,44 @@ enum zwlr_layer_surface_v1_keyboard_interactivity {
 #ifndef ZWLR_LAYER_SURFACE_V1_ERROR_ENUM
 #define ZWLR_LAYER_SURFACE_V1_ERROR_ENUM
 enum zwlr_layer_surface_v1_error {
-	/**
-	 * provided surface state is invalid
-	 */
-	ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_SURFACE_STATE = 0,
-	/**
-	 * size is invalid
-	 */
-	ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_SIZE = 1,
-	/**
-	 * anchor bitfield is invalid
-	 */
-	ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_ANCHOR = 2,
-	/**
-	 * keyboard interactivity is invalid
-	 */
-	ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_KEYBOARD_INTERACTIVITY = 3,
+  /**
+   * provided surface state is invalid
+   */
+  ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_SURFACE_STATE = 0,
+  /**
+   * size is invalid
+   */
+  ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_SIZE = 1,
+  /**
+   * anchor bitfield is invalid
+   */
+  ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_ANCHOR = 2,
+  /**
+   * keyboard interactivity is invalid
+   */
+  ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_KEYBOARD_INTERACTIVITY = 3,
 };
 #endif /* ZWLR_LAYER_SURFACE_V1_ERROR_ENUM */
 
 #ifndef ZWLR_LAYER_SURFACE_V1_ANCHOR_ENUM
 #define ZWLR_LAYER_SURFACE_V1_ANCHOR_ENUM
 enum zwlr_layer_surface_v1_anchor {
-	/**
-	 * the top edge of the anchor rectangle
-	 */
-	ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP = 1,
-	/**
-	 * the bottom edge of the anchor rectangle
-	 */
-	ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM = 2,
-	/**
-	 * the left edge of the anchor rectangle
-	 */
-	ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT = 4,
-	/**
-	 * the right edge of the anchor rectangle
-	 */
-	ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT = 8,
+  /**
+   * the top edge of the anchor rectangle
+   */
+  ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP = 1,
+  /**
+   * the bottom edge of the anchor rectangle
+   */
+  ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM = 2,
+  /**
+   * the left edge of the anchor rectangle
+   */
+  ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT = 4,
+  /**
+   * the right edge of the anchor rectangle
+   */
+  ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT = 8,
 };
 #endif /* ZWLR_LAYER_SURFACE_V1_ANCHOR_ENUM */
 
@@ -375,59 +378,56 @@ enum zwlr_layer_surface_v1_anchor {
  * @struct zwlr_layer_surface_v1_listener
  */
 struct zwlr_layer_surface_v1_listener {
-	/**
-	 * suggest a surface change
-	 *
-	 * The configure event asks the client to resize its surface.
-	 *
-	 * Clients should arrange their surface for the new states, and
-	 * then send an ack_configure request with the serial sent in this
-	 * configure event at some point before committing the new surface.
-	 *
-	 * The client is free to dismiss all but the last configure event
-	 * it received.
-	 *
-	 * The width and height arguments specify the size of the window in
-	 * surface-local coordinates.
-	 *
-	 * The size is a hint, in the sense that the client is free to
-	 * ignore it if it doesn't resize, pick a smaller size (to satisfy
-	 * aspect ratio or resize in steps of NxM pixels). If the client
-	 * picks a smaller size and is anchored to two opposite anchors
-	 * (e.g. 'top' and 'bottom'), the surface will be centered on this
-	 * axis.
-	 *
-	 * If the width or height arguments are zero, it means the client
-	 * should decide its own window dimension.
-	 */
-	void (*configure)(void *data,
-			  struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1,
-			  uint32_t serial,
-			  uint32_t width,
-			  uint32_t height);
-	/**
-	 * surface should be closed
-	 *
-	 * The closed event is sent by the compositor when the surface
-	 * will no longer be shown. The output may have been destroyed or
-	 * the user may have asked for it to be removed. Further changes to
-	 * the surface will be ignored. The client should destroy the
-	 * resource after receiving this event, and create a new surface if
-	 * they so choose.
-	 */
-	void (*closed)(void *data,
-		       struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1);
+  /**
+   * suggest a surface change
+   *
+   * The configure event asks the client to resize its surface.
+   *
+   * Clients should arrange their surface for the new states, and
+   * then send an ack_configure request with the serial sent in this
+   * configure event at some point before committing the new surface.
+   *
+   * The client is free to dismiss all but the last configure event
+   * it received.
+   *
+   * The width and height arguments specify the size of the window in
+   * surface-local coordinates.
+   *
+   * The size is a hint, in the sense that the client is free to
+   * ignore it if it doesn't resize, pick a smaller size (to satisfy
+   * aspect ratio or resize in steps of NxM pixels). If the client
+   * picks a smaller size and is anchored to two opposite anchors
+   * (e.g. 'top' and 'bottom'), the surface will be centered on this
+   * axis.
+   *
+   * If the width or height arguments are zero, it means the client
+   * should decide its own window dimension.
+   */
+  void (*configure)(void *data,
+                    struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1,
+                    uint32_t serial, uint32_t width, uint32_t height);
+  /**
+   * surface should be closed
+   *
+   * The closed event is sent by the compositor when the surface
+   * will no longer be shown. The output may have been destroyed or
+   * the user may have asked for it to be removed. Further changes to
+   * the surface will be ignored. The client should destroy the
+   * resource after receiving this event, and create a new surface if
+   * they so choose.
+   */
+  void (*closed)(void *data,
+                 struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1);
 };
 
 /**
  * @ingroup iface_zwlr_layer_surface_v1
  */
-static inline int
-zwlr_layer_surface_v1_add_listener(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1,
-				   const struct zwlr_layer_surface_v1_listener *listener, void *data)
-{
-	return wl_proxy_add_listener((struct wl_proxy *) zwlr_layer_surface_v1,
-				     (void (**)(void)) listener, data);
+static inline int zwlr_layer_surface_v1_add_listener(
+    struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1,
+    const struct zwlr_layer_surface_v1_listener *listener, void *data) {
+  return wl_proxy_add_listener((struct wl_proxy *)zwlr_layer_surface_v1,
+                               (void (**)(void))listener, data);
 }
 
 #define ZWLR_LAYER_SURFACE_V1_SET_SIZE 0
@@ -487,23 +487,20 @@ zwlr_layer_surface_v1_add_listener(struct zwlr_layer_surface_v1 *zwlr_layer_surf
 #define ZWLR_LAYER_SURFACE_V1_SET_LAYER_SINCE_VERSION 2
 
 /** @ingroup iface_zwlr_layer_surface_v1 */
-static inline void
-zwlr_layer_surface_v1_set_user_data(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, void *user_data)
-{
-	wl_proxy_set_user_data((struct wl_proxy *) zwlr_layer_surface_v1, user_data);
+static inline void zwlr_layer_surface_v1_set_user_data(
+    struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, void *user_data) {
+  wl_proxy_set_user_data((struct wl_proxy *)zwlr_layer_surface_v1, user_data);
 }
 
 /** @ingroup iface_zwlr_layer_surface_v1 */
-static inline void *
-zwlr_layer_surface_v1_get_user_data(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1)
-{
-	return wl_proxy_get_user_data((struct wl_proxy *) zwlr_layer_surface_v1);
+static inline void *zwlr_layer_surface_v1_get_user_data(
+    struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1) {
+  return wl_proxy_get_user_data((struct wl_proxy *)zwlr_layer_surface_v1);
 }
 
-static inline uint32_t
-zwlr_layer_surface_v1_get_version(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1)
-{
-	return wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1);
+static inline uint32_t zwlr_layer_surface_v1_get_version(
+    struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1) {
+  return wl_proxy_get_version((struct wl_proxy *)zwlr_layer_surface_v1);
 }
 
 /**
@@ -520,11 +517,13 @@ zwlr_layer_surface_v1_get_version(struct zwlr_layer_surface_v1 *zwlr_layer_surfa
  *
  * Size is double-buffered, see wl_surface.commit.
  */
-static inline void
-zwlr_layer_surface_v1_set_size(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t width, uint32_t height)
-{
-	wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1,
-			 ZWLR_LAYER_SURFACE_V1_SET_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, width, height);
+static inline void zwlr_layer_surface_v1_set_size(
+    struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t width,
+    uint32_t height) {
+  wl_proxy_marshal_flags(
+      (struct wl_proxy *)zwlr_layer_surface_v1, ZWLR_LAYER_SURFACE_V1_SET_SIZE,
+      NULL, wl_proxy_get_version((struct wl_proxy *)zwlr_layer_surface_v1), 0,
+      width, height);
 }
 
 /**
@@ -538,11 +537,13 @@ zwlr_layer_surface_v1_set_size(struct zwlr_layer_surface_v1 *zwlr_layer_surface_
  *
  * Anchor is double-buffered, see wl_surface.commit.
  */
-static inline void
-zwlr_layer_surface_v1_set_anchor(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t anchor)
-{
-	wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1,
-			 ZWLR_LAYER_SURFACE_V1_SET_ANCHOR, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, anchor);
+static inline void zwlr_layer_surface_v1_set_anchor(
+    struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t anchor) {
+  wl_proxy_marshal_flags(
+      (struct wl_proxy *)zwlr_layer_surface_v1,
+      ZWLR_LAYER_SURFACE_V1_SET_ANCHOR, NULL,
+      wl_proxy_get_version((struct wl_proxy *)zwlr_layer_surface_v1), 0,
+      anchor);
 }
 
 /**
@@ -581,11 +582,12 @@ zwlr_layer_surface_v1_set_anchor(struct zwlr_layer_surface_v1 *zwlr_layer_surfac
  *
  * Exclusive zone is double-buffered, see wl_surface.commit.
  */
-static inline void
-zwlr_layer_surface_v1_set_exclusive_zone(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, int32_t zone)
-{
-	wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1,
-			 ZWLR_LAYER_SURFACE_V1_SET_EXCLUSIVE_ZONE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, zone);
+static inline void zwlr_layer_surface_v1_set_exclusive_zone(
+    struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, int32_t zone) {
+  wl_proxy_marshal_flags(
+      (struct wl_proxy *)zwlr_layer_surface_v1,
+      ZWLR_LAYER_SURFACE_V1_SET_EXCLUSIVE_ZONE, NULL,
+      wl_proxy_get_version((struct wl_proxy *)zwlr_layer_surface_v1), 0, zone);
 }
 
 /**
@@ -599,11 +601,14 @@ zwlr_layer_surface_v1_set_exclusive_zone(struct zwlr_layer_surface_v1 *zwlr_laye
  *
  * Margin is double-buffered, see wl_surface.commit.
  */
-static inline void
-zwlr_layer_surface_v1_set_margin(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, int32_t top, int32_t right, int32_t bottom, int32_t left)
-{
-	wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1,
-			 ZWLR_LAYER_SURFACE_V1_SET_MARGIN, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, top, right, bottom, left);
+static inline void zwlr_layer_surface_v1_set_margin(
+    struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, int32_t top,
+    int32_t right, int32_t bottom, int32_t left) {
+  wl_proxy_marshal_flags(
+      (struct wl_proxy *)zwlr_layer_surface_v1,
+      ZWLR_LAYER_SURFACE_V1_SET_MARGIN, NULL,
+      wl_proxy_get_version((struct wl_proxy *)zwlr_layer_surface_v1), 0, top,
+      right, bottom, left);
 }
 
 /**
@@ -622,11 +627,14 @@ zwlr_layer_surface_v1_set_margin(struct zwlr_layer_surface_v1 *zwlr_layer_surfac
  *
  * Keyboard interactivity is double-buffered, see wl_surface.commit.
  */
-static inline void
-zwlr_layer_surface_v1_set_keyboard_interactivity(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t keyboard_interactivity)
-{
-	wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1,
-			 ZWLR_LAYER_SURFACE_V1_SET_KEYBOARD_INTERACTIVITY, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, keyboard_interactivity);
+static inline void zwlr_layer_surface_v1_set_keyboard_interactivity(
+    struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1,
+    uint32_t keyboard_interactivity) {
+  wl_proxy_marshal_flags(
+      (struct wl_proxy *)zwlr_layer_surface_v1,
+      ZWLR_LAYER_SURFACE_V1_SET_KEYBOARD_INTERACTIVITY, NULL,
+      wl_proxy_get_version((struct wl_proxy *)zwlr_layer_surface_v1), 0,
+      keyboard_interactivity);
 }
 
 /**
@@ -640,11 +648,13 @@ zwlr_layer_surface_v1_set_keyboard_interactivity(struct zwlr_layer_surface_v1 *z
  * See the documentation of xdg_popup for more details about what an
  * xdg_popup is and how it is used.
  */
-static inline void
-zwlr_layer_surface_v1_get_popup(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, struct xdg_popup *popup)
-{
-	wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1,
-			 ZWLR_LAYER_SURFACE_V1_GET_POPUP, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, popup);
+static inline void zwlr_layer_surface_v1_get_popup(
+    struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1,
+    struct xdg_popup *popup) {
+  wl_proxy_marshal_flags(
+      (struct wl_proxy *)zwlr_layer_surface_v1, ZWLR_LAYER_SURFACE_V1_GET_POPUP,
+      NULL, wl_proxy_get_version((struct wl_proxy *)zwlr_layer_surface_v1), 0,
+      popup);
 }
 
 /**
@@ -666,11 +676,13 @@ zwlr_layer_surface_v1_get_popup(struct zwlr_layer_surface_v1 *zwlr_layer_surface
  * only the last request sent before a commit indicates which configure
  * event the client really is responding to.
  */
-static inline void
-zwlr_layer_surface_v1_ack_configure(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t serial)
-{
-	wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1,
-			 ZWLR_LAYER_SURFACE_V1_ACK_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, serial);
+static inline void zwlr_layer_surface_v1_ack_configure(
+    struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t serial) {
+  wl_proxy_marshal_flags(
+      (struct wl_proxy *)zwlr_layer_surface_v1,
+      ZWLR_LAYER_SURFACE_V1_ACK_CONFIGURE, NULL,
+      wl_proxy_get_version((struct wl_proxy *)zwlr_layer_surface_v1), 0,
+      serial);
 }
 
 /**
@@ -678,11 +690,12 @@ zwlr_layer_surface_v1_ack_configure(struct zwlr_layer_surface_v1 *zwlr_layer_sur
  *
  * This request destroys the layer surface.
  */
-static inline void
-zwlr_layer_surface_v1_destroy(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1)
-{
-	wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1,
-			 ZWLR_LAYER_SURFACE_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), WL_MARSHAL_FLAG_DESTROY);
+static inline void zwlr_layer_surface_v1_destroy(
+    struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1) {
+  wl_proxy_marshal_flags(
+      (struct wl_proxy *)zwlr_layer_surface_v1, ZWLR_LAYER_SURFACE_V1_DESTROY,
+      NULL, wl_proxy_get_version((struct wl_proxy *)zwlr_layer_surface_v1),
+      WL_MARSHAL_FLAG_DESTROY);
 }
 
 /**
@@ -692,14 +705,15 @@ zwlr_layer_surface_v1_destroy(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v
  *
  * Layer is double-buffered, see wl_surface.commit.
  */
-static inline void
-zwlr_layer_surface_v1_set_layer(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t layer)
-{
-	wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1,
-			 ZWLR_LAYER_SURFACE_V1_SET_LAYER, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, layer);
+static inline void zwlr_layer_surface_v1_set_layer(
+    struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t layer) {
+  wl_proxy_marshal_flags(
+      (struct wl_proxy *)zwlr_layer_surface_v1, ZWLR_LAYER_SURFACE_V1_SET_LAYER,
+      NULL, wl_proxy_get_version((struct wl_proxy *)zwlr_layer_surface_v1), 0,
+      layer);
 }
 
-#ifdef  __cplusplus
+#ifdef __cplusplus
 }
 #endif
 
diff --git a/wayland.go b/wayland.go
deleted file mode 100644
index ee5cd3b..0000000
--- a/wayland.go
+++ /dev/null
@@ -1,635 +0,0 @@
-package main
-
-/*
-#cgo pkg-config: wayland-client wayland-egl egl gl xkbcommon
-#cgo LDFLAGS: -lwayland-client -lwayland-egl -lEGL -lGL -lxkbcommon
-#cgo CFLAGS: -I.
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include "wlr-layer-shell-client.h"
-#include "keyboard-shortcuts-inhibit-client.h"
-#include 
-#include 
-
-#include 
-#include 
-
-#ifndef __has_attribute
-# define __has_attribute(x) 0
-#endif
-
-#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
-#define WL_PRIVATE __attribute__ ((visibility("hidden")))
-#else
-#define WL_PRIVATE
-#endif
-
-extern const struct wl_interface wl_output_interface;
-extern const struct wl_interface wl_surface_interface;
-extern const struct wl_interface zwlr_layer_surface_v1_interface;
-
-static const struct wl_interface xdg_popup_interface = {
-	"xdg_popup", 0, 0, NULL, 0, NULL,
-};
-
-static const struct wl_interface *wlr_layer_shell_unstable_v1_types[] = {
-	NULL,
-	NULL,
-	NULL,
-	NULL,
-	&zwlr_layer_surface_v1_interface,
-	&wl_surface_interface,
-	&wl_output_interface,
-	NULL,
-	NULL,
-	&xdg_popup_interface,
-};
-
-static const struct wl_message zwlr_layer_shell_v1_requests[] = {
-	{ "get_layer_surface", "no?ous", wlr_layer_shell_unstable_v1_types + 4 },
-	{ "destroy", "3", wlr_layer_shell_unstable_v1_types + 0 },
-};
-
-WL_PRIVATE const struct wl_interface zwlr_layer_shell_v1_interface = {
-	"zwlr_layer_shell_v1", 4,
-	2, zwlr_layer_shell_v1_requests,
-	0, NULL,
-};
-
-static const struct wl_message zwlr_layer_surface_v1_requests[] = {
-	{ "set_size", "uu", wlr_layer_shell_unstable_v1_types + 0 },
-	{ "set_anchor", "u", wlr_layer_shell_unstable_v1_types + 0 },
-	{ "set_exclusive_zone", "i", wlr_layer_shell_unstable_v1_types + 0 },
-	{ "set_margin", "iiii", wlr_layer_shell_unstable_v1_types + 0 },
-	{ "set_keyboard_interactivity", "u", wlr_layer_shell_unstable_v1_types + 0 },
-	{ "get_popup", "o", wlr_layer_shell_unstable_v1_types + 9 },
-	{ "ack_configure", "u", wlr_layer_shell_unstable_v1_types + 0 },
-	{ "destroy", "", wlr_layer_shell_unstable_v1_types + 0 },
-	{ "set_layer", "2u", wlr_layer_shell_unstable_v1_types + 0 },
-};
-
-static const struct wl_message zwlr_layer_surface_v1_events[] = {
-	{ "configure", "uuu", wlr_layer_shell_unstable_v1_types + 0 },
-	{ "closed", "", wlr_layer_shell_unstable_v1_types + 0 },
-};
-
-WL_PRIVATE const struct wl_interface zwlr_layer_surface_v1_interface = {
-	"zwlr_layer_surface_v1", 4,
-	9, zwlr_layer_surface_v1_requests,
-	2, zwlr_layer_surface_v1_events,
-};
-
-static const struct wl_interface *keyboard_shortcuts_inhibit_unstable_v1_types[] = {
-	&zwp_keyboard_shortcuts_inhibitor_v1_interface,
-	&wl_surface_interface,
-	&wl_seat_interface,
-};
-
-static const struct wl_message zwp_keyboard_shortcuts_inhibit_manager_v1_requests[] = {
-	{ "destroy", "", keyboard_shortcuts_inhibit_unstable_v1_types + 0 },
-	{ "inhibit_shortcuts", "noo", keyboard_shortcuts_inhibit_unstable_v1_types + 0 },
-};
-
-WL_PRIVATE const struct wl_interface zwp_keyboard_shortcuts_inhibit_manager_v1_interface = {
-	"zwp_keyboard_shortcuts_inhibit_manager_v1", 1,
-	2, zwp_keyboard_shortcuts_inhibit_manager_v1_requests,
-	0, NULL,
-};
-
-static const struct wl_message zwp_keyboard_shortcuts_inhibitor_v1_requests[] = {
-	{ "destroy", "", keyboard_shortcuts_inhibit_unstable_v1_types + 0 },
-};
-
-static const struct wl_message zwp_keyboard_shortcuts_inhibitor_v1_events[] = {
-	{ "active", "", keyboard_shortcuts_inhibit_unstable_v1_types + 0 },
-	{ "inactive", "", keyboard_shortcuts_inhibit_unstable_v1_types + 0 },
-};
-
-WL_PRIVATE const struct wl_interface zwp_keyboard_shortcuts_inhibitor_v1_interface = {
-	"zwp_keyboard_shortcuts_inhibitor_v1", 1,
-	1, zwp_keyboard_shortcuts_inhibitor_v1_requests,
-	2, zwp_keyboard_shortcuts_inhibitor_v1_events,
-};
-
-struct wl_compositor *compositor = NULL;
-struct zwlr_layer_shell_v1 *layer_shell = NULL;
-struct wl_seat *seat = NULL;
-struct wl_pointer *pointer = NULL;
-struct wl_keyboard *keyboard = NULL;
-struct zwp_keyboard_shortcuts_inhibit_manager_v1 *shortcuts_inhibit_manager = NULL;
-struct zwp_keyboard_shortcuts_inhibitor_v1 *shortcuts_inhibitor = NULL;
-struct zwlr_layer_surface_v1 *layer_surface_global = NULL;
-struct xkb_context *xkb_context;
-struct xkb_keymap *xkb_keymap;
-struct xkb_state *xkb_state;
-int32_t width_global = 0;
-int32_t height_global = 0;
-
-void layer_surface_configure(void *data, struct zwlr_layer_surface_v1 *surface,
-                             uint32_t serial, uint32_t width, uint32_t height) {
-    width_global = width;
-    height_global = height;
-    zwlr_layer_surface_v1_ack_configure(surface, serial);
-}
-
-void layer_surface_closed(void *data, struct zwlr_layer_surface_v1 *surface) {
-}
-
-static struct zwlr_layer_surface_v1_listener layer_surface_listener = {
-    .configure = layer_surface_configure,
-    .closed = layer_surface_closed,
-};
-
-// Forward declarations for seat
-void seat_capabilities(void *data, struct wl_seat *seat, uint32_t capabilities);
-void seat_name(void *data, struct wl_seat *seat, const char *name);
-
-static const struct wl_seat_listener seat_listener = {
-    .capabilities = seat_capabilities,
-    .name = seat_name,
-};
-
-static void registry_global(void *data, struct wl_registry *registry,
-                           uint32_t name, const char *interface,
-                           uint32_t version) {
-    if (strcmp(interface, "wl_compositor") == 0) {
-        compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 4);
-    } else if (strcmp(interface, "zwlr_layer_shell_v1") == 0) {
-        layer_shell = (struct zwlr_layer_shell_v1 *)
-            wl_registry_bind(registry, name, &zwlr_layer_shell_v1_interface, 1);
-    } else if (strcmp(interface, "wl_seat") == 0) {
-        seat = wl_registry_bind(registry, name, &wl_seat_interface, 1);
-        wl_seat_add_listener(seat, &seat_listener, NULL);
-    } else if (strcmp(interface, "zwp_keyboard_shortcuts_inhibit_manager_v1") == 0) {
-        shortcuts_inhibit_manager = (struct zwp_keyboard_shortcuts_inhibit_manager_v1 *)
-            wl_registry_bind(registry, name, &zwp_keyboard_shortcuts_inhibit_manager_v1_interface, 1);
-    }
-}
-
-static void registry_global_remove(void *data, struct wl_registry *registry,
-                                   uint32_t name) {
-}
-
-static const struct wl_registry_listener registry_listener = {
-    .global = registry_global,
-    .global_remove = registry_global_remove,
-};
-
-struct wl_registry *get_registry(struct wl_display *display) {
-    return wl_display_get_registry(display);
-}
-
-void add_registry_listener(struct wl_registry *registry) {
-    wl_registry_add_listener(registry, ®istry_listener, NULL);
-}
-
-struct wl_surface *surface_global = NULL;
-
-struct zwlr_layer_surface_v1 *create_layer_surface(struct wl_surface *surface) {
-    surface_global = surface;
-
-    layer_surface_global =
-        zwlr_layer_shell_v1_get_layer_surface(
-            layer_shell, surface, NULL,
-            ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "hexecute");
-
-    zwlr_layer_surface_v1_set_anchor(layer_surface_global,
-        ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
-        ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM |
-        ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT |
-        ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT);
-
-    zwlr_layer_surface_v1_set_exclusive_zone(layer_surface_global, -1);
-
-    zwlr_layer_surface_v1_set_keyboard_interactivity(layer_surface_global,
-        ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE);
-
-    zwlr_layer_surface_v1_add_listener(layer_surface_global, &layer_surface_listener, NULL);
-
-    wl_surface_commit(surface);
-
-    return layer_surface_global;
-}
-
-void set_input_region(int32_t width, int32_t height) {
-    if (surface_global) {
-        struct wl_region *region = wl_compositor_create_region(compositor);
-        wl_region_add(region, 0, 0, width, height);
-        wl_surface_set_input_region(surface_global, region);
-        wl_region_destroy(region);
-        wl_surface_commit(surface_global);
-    }
-}
-
-void disable_all_input() {
-    if (shortcuts_inhibitor) {
-        zwp_keyboard_shortcuts_inhibitor_v1_destroy(shortcuts_inhibitor);
-        shortcuts_inhibitor = NULL;
-    }
-
-    if (layer_surface_global) {
-        zwlr_layer_surface_v1_set_keyboard_interactivity(layer_surface_global,
-            ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE);
-    }
-
-    if (surface_global) {
-        struct wl_region *region = wl_compositor_create_region(compositor);
-        wl_surface_set_input_region(surface_global, region);
-        wl_region_destroy(region);
-        wl_surface_commit(surface_global);
-    }
-}
-
-static int button_state = 0;
-static double mouse_x = 0;
-static double mouse_y = 0;
-
-void pointer_enter(void *data, struct wl_pointer *pointer, uint32_t serial,
-                  struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y) {
-    mouse_x = wl_fixed_to_double(x);
-    mouse_y = wl_fixed_to_double(y);
-    wl_pointer_set_cursor(pointer, serial, NULL, 0, 0);
-}
-
-void pointer_leave(void *data, struct wl_pointer *pointer, uint32_t serial,
-                  struct wl_surface *surface) {
-}
-
-void pointer_motion(void *data, struct wl_pointer *pointer, uint32_t time,
-                   wl_fixed_t x, wl_fixed_t y) {
-    mouse_x = wl_fixed_to_double(x);
-    mouse_y = wl_fixed_to_double(y);
-}
-
-void pointer_button(void *data, struct wl_pointer *pointer, uint32_t serial,
-                   uint32_t time, uint32_t button, uint32_t state) {
-    if (button == 272) {
-        button_state = state;
-    }
-}
-
-void pointer_axis(void *data, struct wl_pointer *pointer, uint32_t time,
-                 uint32_t axis, wl_fixed_t value) {
-}
-
-void pointer_frame(void *data, struct wl_pointer *pointer) {
-}
-
-void pointer_axis_source(void *data, struct wl_pointer *pointer, uint32_t source) {
-}
-
-void pointer_axis_stop(void *data, struct wl_pointer *pointer, uint32_t time, uint32_t axis) {
-}
-
-void pointer_axis_discrete(void *data, struct wl_pointer *pointer, uint32_t axis, int32_t discrete) {
-}
-
-static const struct wl_pointer_listener pointer_listener = {
-    .enter = pointer_enter,
-    .leave = pointer_leave,
-    .motion = pointer_motion,
-    .button = pointer_button,
-    .axis = pointer_axis,
-    .frame = pointer_frame,
-    .axis_source = pointer_axis_source,
-    .axis_stop = pointer_axis_stop,
-    .axis_discrete = pointer_axis_discrete,
-};
-
-static uint32_t last_key = 0;
-static uint32_t last_key_state = 0;
-
-void keyboard_keymap(void *data, struct wl_keyboard *keyboard, uint32_t format,
-                     int32_t fd, uint32_t size) {
-    if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
-        close(fd);
-        return;
-    }
-
-    char *map_shm = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
-    if (map_shm == MAP_FAILED) {
-        close(fd);
-        return;
-    }
-
-    xkb_keymap = xkb_keymap_new_from_string(xkb_context, map_shm,
-                                            XKB_KEYMAP_FORMAT_TEXT_V1,
-                                            XKB_KEYMAP_COMPILE_NO_FLAGS);
-    munmap(map_shm, size);
-    close(fd);
-
-    if (!xkb_keymap) {
-        return;
-    }
-
-    xkb_state = xkb_state_new(xkb_keymap);
-    if (!xkb_state) {
-        return;
-    }
-}
-
-void keyboard_enter(void *data, struct wl_keyboard *keyboard, uint32_t serial,
-                    struct wl_surface *surface, struct wl_array *keys) {
-}
-
-void keyboard_leave(void *data, struct wl_keyboard *keyboard, uint32_t serial,
-                    struct wl_surface *surface) {
-}
-
-void keyboard_key(void *data, struct wl_keyboard *keyboard, uint32_t serial,
-                  uint32_t time, uint32_t key, uint32_t state) {
-    if (xkb_state) {
-        xkb_keysym_t sym = xkb_state_key_get_one_sym(xkb_state, key + 8);
-        if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
-            last_key = sym;
-            last_key_state = 1;
-        } else {
-            last_key = 0;
-            last_key_state = 0;
-        }
-    }
-}
-
-void keyboard_modifiers(void *data, struct wl_keyboard *keyboard, uint32_t serial,
-                        uint32_t mods_depressed, uint32_t mods_latched,
-                        uint32_t mods_locked, uint32_t group) {
-    if (xkb_state) {
-        xkb_state_update_mask(xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group);
-    }
-}
-
-void keyboard_repeat_info(void *data, struct wl_keyboard *keyboard,
-                          int32_t rate, int32_t delay) {
-}
-
-static const struct wl_keyboard_listener keyboard_listener = {
-    .keymap = keyboard_keymap,
-    .enter = keyboard_enter,
-    .leave = keyboard_leave,
-    .key = keyboard_key,
-    .modifiers = keyboard_modifiers,
-    .repeat_info = keyboard_repeat_info,
-};
-
-// Seat listener
-void seat_capabilities(void *data, struct wl_seat *seat, uint32_t capabilities) {
-    if (capabilities & WL_SEAT_CAPABILITY_POINTER) {
-        pointer = wl_seat_get_pointer(seat);
-        wl_pointer_add_listener(pointer, &pointer_listener, NULL);
-    }
-
-    if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) {
-        keyboard = wl_seat_get_keyboard(seat);
-        wl_keyboard_add_listener(keyboard, &keyboard_listener, NULL);
-
-        if (shortcuts_inhibit_manager && surface_global && !shortcuts_inhibitor) {
-            shortcuts_inhibitor = zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(
-                shortcuts_inhibit_manager, surface_global, seat);
-        }
-    }
-}
-
-void seat_name(void *data, struct wl_seat *seat, const char *name) {
-}
-
-int get_button_state() {
-    return button_state;
-}
-
-void get_mouse_pos(double *x, double *y) {
-    *x = mouse_x;
-    *y = mouse_y;
-}
-
-void get_dimensions(int32_t *w, int32_t *h) {
-    *w = width_global;
-    *h = height_global;
-}
-
-uint32_t get_last_key() {
-    return last_key;
-}
-
-uint32_t get_last_key_state() {
-    return last_key_state;
-}
-
-void clear_last_key() {
-    last_key = 0;
-    last_key_state = 0;
-}
-*/
-import "C"
-import (
-	"fmt"
-)
-
-type WaylandError struct {
-	msg string
-}
-
-func (e *WaylandError) Error() string {
-	return e.msg
-}
-
-type WaylandWindow struct {
-	display       *C.struct_wl_display
-	registry      *C.struct_wl_registry
-	surface       *C.struct_wl_surface
-	layerSurface  *C.struct_zwlr_layer_surface_v1
-	eglWindow     *C.struct_wl_egl_window
-	eglDisplay    C.EGLDisplay
-	eglContext    C.EGLContext
-	eglSurface    C.EGLSurface
-	width, height int32
-}
-
-func NewWaylandWindow() (*WaylandWindow, error) {
-	w := &WaylandWindow{}
-
-    C.xkb_context = C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS)
-    if C.xkb_context == nil {
-        return nil, &WaylandError{"failed to create xkb context"}
-    }
-
-	w.display = C.wl_display_connect(nil)
-	if w.display == nil {
-		return nil, &WaylandError{"failed to connect to Wayland display"}
-	}
-
-	w.registry = C.get_registry(w.display)
-	C.add_registry_listener(w.registry)
-	C.wl_display_roundtrip(w.display)
-	if C.compositor == nil {
-		return nil, &WaylandError{"compositor not available"}
-	}
-	if C.layer_shell == nil {
-		return nil, &WaylandError{"layer shell not available"}
-	}
-
-	w.surface = C.wl_compositor_create_surface(C.compositor)
-	if w.surface == nil {
-		return nil, &WaylandError{"failed to create surface"}
-	}
-
-	w.layerSurface = C.create_layer_surface(w.surface)
-
-	C.wl_display_roundtrip(w.display)
-
-	var width, height C.int32_t
-	C.get_dimensions(&width, &height)
-	w.width = int32(width)
-	w.height = int32(height)
-
-	if w.width == 0 || w.height == 0 {
-		w.width = 1920
-		w.height = 1080
-	}
-
-	C.wl_display_roundtrip(w.display)
-
-	C.set_input_region(C.int32_t(w.width), C.int32_t(w.height))
-
-	if err := w.initEGL(); err != nil {
-		return nil, err
-	}
-
-	C.wl_surface_commit(w.surface)
-	C.wl_display_flush(w.display)
-
-	C.wl_display_roundtrip(w.display)
-	C.wl_display_roundtrip(w.display)
-	C.wl_display_flush(w.display)
-
-	return w, nil
-}
-
-func (w *WaylandWindow) initEGL() error {
-	w.eglWindow = C.wl_egl_window_create(w.surface, C.int(w.width), C.int(w.height))
-	if w.eglWindow == nil {
-		return fmt.Errorf("failed to create EGL window")
-	}
-
-	w.eglDisplay = C.eglGetDisplay(C.EGLNativeDisplayType(w.display))
-	if w.eglDisplay == C.EGLDisplay(C.EGL_NO_DISPLAY) {
-		return fmt.Errorf("failed to get EGL display")
-	}
-
-	var major, minor C.EGLint
-	if C.eglInitialize(w.eglDisplay, &major, &minor) == C.EGL_FALSE {
-		return fmt.Errorf("failed to initialize EGL")
-	}
-
-	configAttribs := []C.EGLint{
-		C.EGL_SURFACE_TYPE, C.EGL_WINDOW_BIT,
-		C.EGL_RED_SIZE, 8,
-		C.EGL_GREEN_SIZE, 8,
-		C.EGL_BLUE_SIZE, 8,
-		C.EGL_ALPHA_SIZE, 8,
-		C.EGL_RENDERABLE_TYPE, C.EGL_OPENGL_BIT,
-		C.EGL_NONE,
-	}
-
-	var config C.EGLConfig
-	var numConfigs C.EGLint
-	if C.eglChooseConfig(w.eglDisplay, &configAttribs[0], &config, 1, &numConfigs) == C.EGL_FALSE {
-		return fmt.Errorf("failed to choose EGL config")
-	}
-
-	C.eglBindAPI(C.EGL_OPENGL_API)
-	contextAttribs := []C.EGLint{
-		C.EGL_CONTEXT_MAJOR_VERSION, 4,
-		C.EGL_CONTEXT_MINOR_VERSION, 1,
-		C.EGL_CONTEXT_OPENGL_PROFILE_MASK, C.EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
-		C.EGL_NONE,
-	}
-
-	w.eglContext = C.eglCreateContext(w.eglDisplay, config, nil, &contextAttribs[0])
-	if w.eglContext == nil {
-		return fmt.Errorf("failed to create EGL context")
-	}
-
-	w.eglSurface = C.eglCreateWindowSurface(w.eglDisplay, config, C.EGLNativeWindowType(w.eglWindow), nil)
-	if w.eglSurface == nil {
-		return fmt.Errorf("failed to create EGL surface")
-	}
-
-	if C.eglMakeCurrent(w.eglDisplay, w.eglSurface, w.eglSurface, w.eglContext) == C.EGL_FALSE {
-		return fmt.Errorf("failed to make EGL context current")
-	}
-
-	return nil
-}
-
-func (w *WaylandWindow) GetSize() (int, int) {
-	var width, height C.int32_t
-	C.get_dimensions(&width, &height)
-	if width > 0 && height > 0 {
-		w.width = int32(width)
-		w.height = int32(height)
-	}
-	return int(w.width), int(w.height)
-}
-
-func (w *WaylandWindow) ShouldClose() bool {
-	return false
-}
-
-func (w *WaylandWindow) SwapBuffers() {
-	C.eglSwapBuffers(w.eglDisplay, w.eglSurface)
-}
-
-func (w *WaylandWindow) PollEvents() {
-	C.wl_display_flush(w.display)
-	C.wl_display_dispatch_pending(w.display)
-}
-
-func (w *WaylandWindow) GetCursorPos() (float64, float64) {
-	var x, y C.double
-	C.get_mouse_pos(&x, &y)
-	return float64(x), float64(y)
-}
-
-func (w *WaylandWindow) GetMouseButton() bool {
-	state := C.get_button_state()
-	return state == 1
-}
-
-func (w *WaylandWindow) DisableInput() {
-	C.disable_all_input()
-}
-
-func (w *WaylandWindow) GetLastKey() (uint32, uint32, bool) {
-	key := uint32(C.get_last_key())
-	state := uint32(C.get_last_key_state())
-	return key, state, key != 0
-}
-
-func (w *WaylandWindow) ClearLastKey() {
-	C.clear_last_key()
-}
-
-func (w *WaylandWindow) Destroy() {
-	if w.eglContext != C.EGLContext(C.EGL_NO_CONTEXT) {
-		C.eglDestroyContext(w.eglDisplay, w.eglContext)
-	}
-	if w.eglSurface != C.EGLSurface(C.EGL_NO_SURFACE) {
-		C.eglDestroySurface(w.eglDisplay, w.eglSurface)
-	}
-	if w.eglWindow != nil {
-		C.wl_egl_window_destroy(w.eglWindow)
-	}
-	if w.eglDisplay != C.EGLDisplay(C.EGL_NO_DISPLAY) {
-		C.eglTerminate(w.eglDisplay)
-	}
-	if w.surface != nil {
-		C.wl_surface_destroy(w.surface)
-	}
-	if w.display != nil {
-		C.wl_display_disconnect(w.display)
-	}
-}

From c53985b3073ff76ad8df1d6bcb4e4879676e4e22 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Wed, 15 Oct 2025 22:20:08 +0300
Subject: [PATCH 05/24] refactor: added models package

---
 internal/models/models.go |  22 ++++++++
 main.go                   | 116 +++++++++++++++++---------------------
 stroke.go                 |  14 ++++-
 3 files changed, 87 insertions(+), 65 deletions(-)
 create mode 100644 internal/models/models.go

diff --git a/internal/models/models.go b/internal/models/models.go
new file mode 100644
index 0000000..545020a
--- /dev/null
+++ b/internal/models/models.go
@@ -0,0 +1,22 @@
+package models
+
+import "time"
+
+type Point struct {
+	X, Y     float32
+	BornTime time.Time `json:"-"`
+}
+
+type Particle struct {
+	X, Y    float32
+	VX, VY  float32
+	Life    float32
+	MaxLife float32
+	Size    float32
+	Hue     float32
+}
+
+type GestureConfig struct {
+	Command   string    `json:"command"`
+	Templates [][]Point `json:"templates"`
+}
diff --git a/main.go b/main.go
index 717ef8e..0f769cf 100644
--- a/main.go
+++ b/main.go
@@ -13,56 +13,11 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/ThatOtherAndrew/Hexecute/internal/models"
 	"github.com/ThatOtherAndrew/Hexecute/pkg/wayland"
 	"github.com/go-gl/gl/v4.1-core/gl"
 )
 
-type Point struct {
-	X, Y     float32
-	BornTime time.Time `json:"-"`
-}
-
-type Particle struct {
-	X, Y    float32
-	VX, VY  float32
-	Life    float32
-	MaxLife float32
-	Size    float32
-	Hue     float32
-}
-
-type App struct {
-	points            []Point
-	particles         []Particle
-	isDrawing         bool
-	vao               uint32
-	vbo               uint32
-	program           uint32
-	particleVAO       uint32
-	particleVBO       uint32
-	particleProgram   uint32
-	bgVAO             uint32
-	bgVBO             uint32
-	bgProgram         uint32
-	cursorGlowVAO     uint32
-	cursorGlowVBO     uint32
-	cursorGlowProgram uint32
-	startTime         time.Time
-	lastCursorX       float32
-	lastCursorY       float32
-	cursorVelocity    float32
-	smoothVelocity    float32
-	smoothRotation    float32
-	smoothDrawing     float32
-	isExiting         bool
-	exitStartTime     time.Time
-	learnMode         bool
-	learnCommand      string
-	learnGestures     [][]Point
-	learnCount        int
-	savedGestures     []GestureConfig
-}
-
 const lineVertexShader = `
 #version 410 core
 layout (location = 0) in vec2 position;
@@ -343,6 +298,38 @@ func init() {
 	runtime.LockOSThread()
 }
 
+type App struct {
+	points            []models.Point
+	particles         []models.Particle
+	isDrawing         bool
+	vao               uint32
+	vbo               uint32
+	program           uint32
+	particleVAO       uint32
+	particleVBO       uint32
+	particleProgram   uint32
+	bgVAO             uint32
+	bgVBO             uint32
+	bgProgram         uint32
+	cursorGlowVAO     uint32
+	cursorGlowVBO     uint32
+	cursorGlowProgram uint32
+	startTime         time.Time
+	lastCursorX       float32
+	lastCursorY       float32
+	cursorVelocity    float32
+	smoothVelocity    float32
+	smoothRotation    float32
+	smoothDrawing     float32
+	isExiting         bool
+	exitStartTime     time.Time
+	learnMode         bool
+	learnCommand      string
+	learnGestures     [][]models.Point
+	learnCount        int
+	savedGestures     []models.GestureConfig
+}
+
 func (a *App) initGL() error {
 	if err := gl.Init(); err != nil {
 		return err
@@ -518,7 +505,12 @@ func (a *App) initGL() error {
 		-1.0, 1.0,
 		1.0, 1.0,
 	}
-	gl.BufferData(gl.ARRAY_BUFFER, len(glowQuadVertices)*4, gl.Ptr(glowQuadVertices), gl.STATIC_DRAW)
+	gl.BufferData(
+		gl.ARRAY_BUFFER,
+		len(glowQuadVertices)*4,
+		gl.Ptr(glowQuadVertices),
+		gl.STATIC_DRAW,
+	)
 
 	gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 2*4, nil)
 	gl.EnableVertexAttribArray(0)
@@ -553,7 +545,7 @@ func compileShader(source string, shaderType uint32) (uint32, error) {
 }
 
 func (a *App) addPoint(x, y float32) {
-	newPoint := Point{X: x, Y: y, BornTime: time.Now()}
+	newPoint := models.Point{X: x, Y: y, BornTime: time.Now()}
 
 	shouldAdd := false
 	if len(a.points) == 0 {
@@ -568,7 +560,7 @@ func (a *App) addPoint(x, y float32) {
 			for range 3 {
 				angle := rand.Float64() * 2 * math.Pi
 				speed := rand.Float32()*50 + 20
-				a.particles = append(a.particles, Particle{
+				a.particles = append(a.particles, models.Particle{
 					X:       x + (rand.Float32()-0.5)*10,
 					Y:       y + (rand.Float32()-0.5)*10,
 					VX:      float32(math.Cos(angle)) * speed,
@@ -596,7 +588,7 @@ func (a *App) spawnCursorSparkles(x, y float32) {
 	for range 3 {
 		angle := rand.Float64() * 2 * math.Pi
 		speed := rand.Float32()*80 + 40
-		a.particles = append(a.particles, Particle{
+		a.particles = append(a.particles, models.Particle{
 			X:       x + (rand.Float32()-0.5)*8,
 			Y:       y + (rand.Float32()-0.5)*8,
 			VX:      float32(math.Cos(angle)) * speed,
@@ -613,7 +605,7 @@ func (a *App) spawnExitWisps(x, y float32) {
 	for range 8 {
 		angle := rand.Float64() * 2 * math.Pi
 		speed := rand.Float32()*150 + 80
-		a.particles = append(a.particles, Particle{
+		a.particles = append(a.particles, models.Particle{
 			X:       x + (rand.Float32()-0.5)*30,
 			Y:       y + (rand.Float32()-0.5)*30,
 			VX:      float32(math.Cos(angle)) * speed,
@@ -701,7 +693,10 @@ func (a *App) draw(window *wayland.WaylandWindow) {
 	a.drawParticles(window)
 }
 
-func (a *App) drawLine(window *wayland.WaylandWindow, baseThickness, baseAlpha, currentTime float32) {
+func (a *App) drawLine(
+	window *wayland.WaylandWindow,
+	baseThickness, baseAlpha, currentTime float32,
+) {
 	if len(a.points) < 2 {
 		return
 	}
@@ -926,11 +921,6 @@ func (a *App) drawCursorGlow(window *wayland.WaylandWindow, cursorX, cursorY, cu
 	gl.BindVertexArray(0)
 }
 
-type GestureConfig struct {
-	Command   string    `json:"command"`
-	Templates [][]Point `json:"templates"`
-}
-
 func getConfigPath() (string, error) {
 	homeDir, err := os.UserHomeDir()
 	if err != nil {
@@ -943,7 +933,7 @@ func getConfigPath() (string, error) {
 	return filepath.Join(configDir, "gestures.json"), nil
 }
 
-func loadGestures() ([]GestureConfig, error) {
+func loadGestures() ([]models.GestureConfig, error) {
 	configFile, err := getConfigPath()
 	if err != nil {
 		return nil, err
@@ -952,12 +942,12 @@ func loadGestures() ([]GestureConfig, error) {
 	data, err := os.ReadFile(configFile)
 	if err != nil {
 		if os.IsNotExist(err) {
-			return []GestureConfig{}, nil
+			return []models.GestureConfig{}, nil
 		}
 		return nil, err
 	}
 
-	var gestures []GestureConfig
+	var gestures []models.GestureConfig
 	if err := json.Unmarshal(data, &gestures); err != nil {
 		return nil, err
 	}
@@ -965,18 +955,18 @@ func loadGestures() ([]GestureConfig, error) {
 	return gestures, nil
 }
 
-func saveGesture(command string, templates [][]Point) error {
+func saveGesture(command string, templates [][]models.Point) error {
 	configFile, err := getConfigPath()
 	if err != nil {
 		return err
 	}
 
-	var gestures []GestureConfig
+	var gestures []models.GestureConfig
 	if data, err := os.ReadFile(configFile); err == nil {
 		json.Unmarshal(data, &gestures)
 	}
 
-	newGesture := GestureConfig{
+	newGesture := models.GestureConfig{
 		Command:   command,
 		Templates: templates,
 	}
diff --git a/stroke.go b/stroke.go
index 2dc77b8..a61431e 100644
--- a/stroke.go
+++ b/stroke.go
@@ -2,10 +2,16 @@
 
 package main
 
-import "math"
+import (
+	"math"
+
+	"github.com/ThatOtherAndrew/Hexecute/internal/models"
+)
 
 // Step 1
 
+type Point = models.Point
+
 func resample(points []Point, n int) []Point {
 	I := pathLength(points) / float32(n-1)
 	D := float32(0)
@@ -110,7 +116,11 @@ func centroid(points []Point) Point {
 
 // Step 4
 
-func recognise(points []Point, templates [][]Point, size float64) (bestMatch int, bestScore float64) {
+func recognise(
+	points []Point,
+	templates [][]Point,
+	size float64,
+) (bestMatch int, bestScore float64) {
 	b := math.Inf(1)
 	for i, T := range templates {
 		d := distanceAtBestAngle(points, T, -math.Pi/4, math.Pi/4, math.Pi/90)

From ae494752568b4c2a91a8ea7b715561e7147d1f49 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Wed, 15 Oct 2025 22:45:08 +0300
Subject: [PATCH 06/24] refactor: added stroke package

---
 stroke.go => internal/stroke/stroke.go | 2 +-
 main.go                                | 7 ++++---
 2 files changed, 5 insertions(+), 4 deletions(-)
 rename stroke.go => internal/stroke/stroke.go (99%)

diff --git a/stroke.go b/internal/stroke/stroke.go
similarity index 99%
rename from stroke.go
rename to internal/stroke/stroke.go
index a61431e..730b186 100644
--- a/stroke.go
+++ b/internal/stroke/stroke.go
@@ -1,6 +1,6 @@
 // https://depts.washington.edu/acelab/proj/dollar/dollar.pdf
 
-package main
+package stroke
 
 import (
 	"math"
diff --git a/main.go b/main.go
index 0f769cf..3fdca32 100644
--- a/main.go
+++ b/main.go
@@ -14,6 +14,7 @@ import (
 	"time"
 
 	"github.com/ThatOtherAndrew/Hexecute/internal/models"
+	"github.com/ThatOtherAndrew/Hexecute/internal/stroke"
 	"github.com/ThatOtherAndrew/Hexecute/pkg/wayland"
 	"github.com/go-gl/gl/v4.1-core/gl"
 )
@@ -1013,13 +1014,13 @@ func (a *App) recognizeAndExecute(window *wayland.WaylandWindow, x, y float32) {
 		return
 	}
 
-	processed := ProcessStroke(a.points)
+	processed := stroke.ProcessStroke(a.points)
 
 	bestMatch := -1
 	bestScore := 0.0
 
 	for i, gesture := range a.savedGestures {
-		match, score := UnistrokeRecognise(processed, gesture.Templates)
+		match, score := stroke.UnistrokeRecognise(processed, gesture.Templates)
 		log.Printf("Gesture %d (%s): template %d, score %.3f", i, gesture.Command, match, score)
 
 		if score > bestScore {
@@ -1183,7 +1184,7 @@ func main() {
 			app.isDrawing = false
 
 			if app.learnMode && len(app.points) > 0 {
-				processed := ProcessStroke(app.points)
+				processed := stroke.ProcessStroke(app.points)
 				app.learnGestures = append(app.learnGestures, processed)
 				app.learnCount++
 				log.Printf("Captured gesture %d/3", app.learnCount)

From 38c9661b0b0fb3efdf149f0f13f289bf183f2a2a Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Wed, 15 Oct 2025 23:04:33 +0300
Subject: [PATCH 07/24] refactor: added shaders package

---
 internal/shaders/backgroundFragment.go |  19 ++
 internal/shaders/backgroundVertex.go   |  10 +
 internal/shaders/cursorGlowFragment.go | 133 ++++++++++
 internal/shaders/cursorGlowVertex.go   |  24 ++
 internal/shaders/lineFragment.go       |  26 ++
 internal/shaders/lineVertex.go         |  23 ++
 internal/shaders/pacticleVertex.go     |  24 ++
 internal/shaders/particleFragment.go   |  25 ++
 internal/shaders/shader.go             |  27 +++
 main.go                                | 322 ++-----------------------
 10 files changed, 329 insertions(+), 304 deletions(-)
 create mode 100644 internal/shaders/backgroundFragment.go
 create mode 100644 internal/shaders/backgroundVertex.go
 create mode 100644 internal/shaders/cursorGlowFragment.go
 create mode 100644 internal/shaders/cursorGlowVertex.go
 create mode 100644 internal/shaders/lineFragment.go
 create mode 100644 internal/shaders/lineVertex.go
 create mode 100644 internal/shaders/pacticleVertex.go
 create mode 100644 internal/shaders/particleFragment.go
 create mode 100644 internal/shaders/shader.go

diff --git a/internal/shaders/backgroundFragment.go b/internal/shaders/backgroundFragment.go
new file mode 100644
index 0000000..5ee004a
--- /dev/null
+++ b/internal/shaders/backgroundFragment.go
@@ -0,0 +1,19 @@
+package shaders
+
+const BackgroundFragmentShader = `
+#version 410 core
+out vec4 FragColor;
+
+uniform float alpha;
+uniform vec2 cursorPos;
+uniform vec2 resolution;
+
+void main() {
+	vec2 fragCoord = gl_FragCoord.xy;
+	float dist = length(fragCoord - cursorPos);
+	float glowFalloff = smoothstep(0.0, 300.0, dist);
+	float cursorTransparency = mix(0.3, 1.0, glowFalloff);
+
+	FragColor = vec4(0., 0., 0., alpha * cursorTransparency);
+}
+` + "\x00"
diff --git a/internal/shaders/backgroundVertex.go b/internal/shaders/backgroundVertex.go
new file mode 100644
index 0000000..5d4db7a
--- /dev/null
+++ b/internal/shaders/backgroundVertex.go
@@ -0,0 +1,10 @@
+package shaders
+
+const BackgroundVertexShader = `
+#version 410 core
+layout (location = 0) in vec2 position;
+
+void main() {
+	gl_Position = vec4(position, 0.0, 1.0);
+}
+` + "\x00"
diff --git a/internal/shaders/cursorGlowFragment.go b/internal/shaders/cursorGlowFragment.go
new file mode 100644
index 0000000..6500a85
--- /dev/null
+++ b/internal/shaders/cursorGlowFragment.go
@@ -0,0 +1,133 @@
+package shaders
+
+const CursorGlowFragmentShader = `
+#version 410 core
+in vec2 vTexCoord;
+out vec4 FragColor;
+
+uniform float time;
+uniform float velocity;
+uniform float isDrawing;
+uniform float exitProgress;
+
+vec3 hsv2rgb(vec3 c) {
+	vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+	vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+	return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+}
+
+float smin(float a, float b, float k) {
+	float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
+	return mix(b, a, h) - k * h * (1.0 - h);
+}
+
+float hash(vec2 p) {
+	p = fract(p * vec2(123.34, 456.21));
+	p += dot(p, p + 45.32);
+	return fract(p.x * p.y);
+}
+
+float noise(vec2 p) {
+	vec2 i = floor(p);
+	vec2 f = fract(p);
+	f = f * f * (3.0 - 2.0 * f);
+
+	float a = hash(i);
+	float b = hash(i + vec2(1.0, 0.0));
+	float c = hash(i + vec2(0.0, 1.0));
+	float d = hash(i + vec2(1.0, 1.0));
+
+	return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
+}
+
+float fbm(vec2 p) {
+	float value = 0.0;
+	float amplitude = 0.5;
+	float frequency = 1.0;
+
+	for(int i = 0; i < 4; i++) {
+		value += amplitude * noise(p * frequency);
+		frequency *= 2.0;
+		amplitude *= 0.5;
+	}
+	return value;
+}
+
+void main() {
+	vec2 coord = vTexCoord * 2.0 - 1.0;
+	float velocityNorm = clamp(velocity * 0.01, 0.0, 1.0);
+	float energy = mix(0.3, 1.0, velocityNorm) + isDrawing * 0.7;
+
+	float sdf = 1000.0;
+	float centralSize = mix(0.15, 0.35, velocityNorm) + isDrawing * 0.05;
+	float pulseSpeed = (3.0 + velocityNorm * 2.0) * (1.0 + isDrawing * 0.75);
+	float pulseAmount = (0.1 * energy + isDrawing * 0.075);
+	float pulse = sin(time * pulseSpeed) * pulseAmount + 0.9;
+	float centralDist = length(coord) - centralSize * pulse;
+	sdf = centralDist;
+
+	float numBlobsFloat = mix(5.0, 9.0, velocityNorm) + isDrawing * 1.0;
+	int numBlobs = int(numBlobsFloat);
+	float blobFade = fract(numBlobsFloat);
+
+	for(int i = 0; i < 10; i++) {
+		if(i > numBlobs) break;
+		if(i == numBlobs && blobFade < 0.01) break;
+		float baseRotation = time * 0.8;
+		float velocityRotation = time * velocityNorm * 0.4;
+		float angle = float(i) * 6.28 / float(numBlobs) + baseRotation + velocityRotation;
+
+		float baseRadius = mix(0.2, 0.5, velocityNorm) + isDrawing * 0.075;
+		float radiusVariation = sin(time * (1.5 + isDrawing * 0.5) + float(i) * 0.8) * mix(0.05, 0.15, velocityNorm);
+		float chaoticRadius = sin(time * 4.0 + float(i) * 2.1) * cos(time * 3.5 + float(i) * 1.7) * 0.003 * isDrawing;
+		float radius = baseRadius + radiusVariation + chaoticRadius;
+		vec2 orbPos = vec2(cos(angle), sin(angle)) * radius;
+
+		float baseBlobSize = mix(0.08, 0.18, velocityNorm) + isDrawing * 0.04;
+		float sizeVariation = sin(time * (2.5 + isDrawing * 1.0) + float(i) * 0.6) * mix(0.02, 0.05, velocityNorm);
+		float drawingGrowth = sin(time * 5.0 + float(i) * 1.3) * 0.03 * isDrawing;
+		float blobSize = baseBlobSize + sizeVariation + drawingGrowth;
+		float blobDist = length(coord - orbPos) - blobSize;
+
+		if(i == numBlobs) {
+			blobDist += (1.0 - blobFade) * 0.5;
+		}
+		float blendAmount = mix(0.15, 0.3, velocityNorm) + isDrawing * 0.075;
+		sdf = smin(sdf, blobDist, blendAmount);
+	}
+
+	float noiseZoom = 3.0 + isDrawing * 0.5;
+	vec2 noiseCoord = coord * noiseZoom;
+	noiseCoord += vec2(time * 0.3, time * 0.2);
+	float swirl = fbm(noiseCoord) * 2.0 - 1.0;
+
+	sdf += swirl * (0.1 * energy + exitProgress * 0.8);
+	float intensity = exp(-max(sdf, 0.0) * 4.0);
+	float outerGlow = exp(-max(sdf, 0.0) * 1.5) * 0.4 * energy;
+	float innerGlow = exp(-max(sdf, 0.0) * 8.0) * 0.8;
+
+	float totalIntensity = intensity + outerGlow + innerGlow;
+
+	totalIntensity *= smoothstep(1.0, 0.7, max(abs(coord.x), abs(coord.y)));
+
+	float hueSpeed = mix(0.2, 0.6, velocityNorm);
+	float hue = mod(time * hueSpeed + atan(coord.y, coord.x) / 6.28 + swirl * 0.3, 1.0);
+	vec3 mainColor = hsv2rgb(vec3(hue, mix(0.7, 0.75, velocityNorm), 1.0));
+	vec3 accentColor = hsv2rgb(vec3(mod(hue + 0.5, 1.0), 0.75, 1.2));
+	vec3 finalColor = mainColor * intensity;
+	finalColor += accentColor * innerGlow;
+	finalColor += mainColor * 0.5 * outerGlow;
+
+	float sparkle = smoothstep(0.85, 1.0, noise(coord * 20.0 + time * 5.0 * energy)) * totalIntensity * velocityNorm;
+	finalColor += sparkle;
+
+	float edge = smoothstep(0.05, -0.05, sdf) - smoothstep(0.15, 0.05, sdf);
+	finalColor += accentColor * edge * energy;
+
+	finalColor *= sin(time * (2.5 + isDrawing * 0.75)) * (0.1 + velocityNorm * 0.1 + isDrawing * 0.075) + 0.9;
+
+	float alpha = clamp(totalIntensity * mix(0.8, 1.3, velocityNorm), 0.0, 1.0) * (1.0 - exitProgress);
+
+	FragColor = vec4(finalColor, alpha * 0.95);
+}
+` + "\x00"
diff --git a/internal/shaders/cursorGlowVertex.go b/internal/shaders/cursorGlowVertex.go
new file mode 100644
index 0000000..2db01cc
--- /dev/null
+++ b/internal/shaders/cursorGlowVertex.go
@@ -0,0 +1,24 @@
+package shaders
+
+const CursorGlowVertexShader = `
+#version 410 core
+layout (location = 0) in vec2 position;
+
+uniform vec2 cursorPos;
+uniform vec2 resolution;
+uniform float glowSize;
+uniform float rotation;
+
+out vec2 vTexCoord;
+
+void main() {
+	float c = cos(rotation);
+	float s = sin(rotation);
+	vec2 rotatedPos = vec2(position.x * c - position.y * s, position.x * s + position.y * c);
+	vec2 worldPos = cursorPos + rotatedPos * glowSize;
+	vec2 normalized = (worldPos / resolution) * 2.0 - 1.0;
+	normalized.y = -normalized.y;
+	gl_Position = vec4(normalized, 0.0, 1.0);
+	vTexCoord = rotatedPos * 0.5 + 0.5;
+}
+` + "\x00"
diff --git a/internal/shaders/lineFragment.go b/internal/shaders/lineFragment.go
new file mode 100644
index 0000000..b764151
--- /dev/null
+++ b/internal/shaders/lineFragment.go
@@ -0,0 +1,26 @@
+package shaders
+
+const LineFragmentShader = `
+#version 410 core
+in float vAlpha;
+in vec2 vPosition;
+out vec4 FragColor;
+
+uniform float time;
+
+vec3 hsv2rgb(vec3 c) {
+	vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+	vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+	return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+}
+
+void main() {
+	float hue = mod(vPosition.x * 0.001 + vPosition.y * 0.001 + time * 0.5, 1.0);
+	vec3 color = hsv2rgb(vec3(hue, 0.8, 1.0));
+
+	float sparkle = sin(vPosition.x * 0.1 + time * 3.0) * sin(vPosition.y * 0.1 + time * 2.0);
+	sparkle = smoothstep(0.7, 1.0, sparkle) * 0.5;
+
+	FragColor = vec4(color * (1.0 + sparkle * 2.0), vAlpha);
+}
+` + "\x00"
diff --git a/internal/shaders/lineVertex.go b/internal/shaders/lineVertex.go
new file mode 100644
index 0000000..26c4aac
--- /dev/null
+++ b/internal/shaders/lineVertex.go
@@ -0,0 +1,23 @@
+package shaders
+
+const LineVertexShader = `
+#version 410 core
+layout (location = 0) in vec2 position;
+layout (location = 1) in vec2 offset;
+layout (location = 2) in float alpha;
+
+uniform vec2 resolution;
+uniform float thickness;
+
+out float vAlpha;
+out vec2 vPosition;
+
+void main() {
+	vec2 pos = position + offset * thickness;
+	vec2 normalized = (pos / resolution) * 2.0 - 1.0;
+	normalized.y = -normalized.y;
+	gl_Position = vec4(normalized, 0.0, 1.0);
+	vAlpha = alpha;
+	vPosition = pos;
+}
+` + "\x00"
diff --git a/internal/shaders/pacticleVertex.go b/internal/shaders/pacticleVertex.go
new file mode 100644
index 0000000..0599378
--- /dev/null
+++ b/internal/shaders/pacticleVertex.go
@@ -0,0 +1,24 @@
+package shaders
+
+const ParticleVertexShader = `
+#version 410 core
+layout (location = 0) in vec2 position;
+layout (location = 1) in float life;
+layout (location = 2) in float maxLife;
+layout (location = 3) in float size;
+layout (location = 4) in float hue;
+
+uniform vec2 resolution;
+
+out float vLife;
+out float vHue;
+
+void main() {
+	vec2 normalized = (position / resolution) * 2.0 - 1.0;
+	normalized.y = -normalized.y;
+	gl_Position = vec4(normalized, 0.0, 1.0);
+	gl_PointSize = size * (life / maxLife);
+	vLife = life / maxLife;
+	vHue = hue;
+}
+` + "\x00"
diff --git a/internal/shaders/particleFragment.go b/internal/shaders/particleFragment.go
new file mode 100644
index 0000000..def3b2a
--- /dev/null
+++ b/internal/shaders/particleFragment.go
@@ -0,0 +1,25 @@
+package shaders
+
+const ParticleFragmentShader = `
+#version 410 core
+in float vLife;
+in float vHue;
+out vec4 FragColor;
+
+vec3 hsv2rgb(vec3 c) {
+	vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+	vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+	return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+}
+
+void main() {
+	vec2 coord = gl_PointCoord - vec2(0.5);
+	float dist = length(coord);
+	if (dist > 0.5) discard;
+
+	float alpha = smoothstep(0.5, 0.2, dist) * vLife;
+	vec3 color = hsv2rgb(vec3(vHue, 0.9, 1.0)) * (1.0 + (1.0 - dist * 2.0) * 2.0);
+
+	FragColor = vec4(color, alpha * 0.8);
+}
+` + "\x00"
diff --git a/internal/shaders/shader.go b/internal/shaders/shader.go
new file mode 100644
index 0000000..27ea2fb
--- /dev/null
+++ b/internal/shaders/shader.go
@@ -0,0 +1,27 @@
+package shaders
+
+import (
+	"log"
+
+	"github.com/go-gl/gl/v2.1/gl"
+)
+
+func CompileShader(source string, shaderType uint32) (uint32, error) {
+	shader := gl.CreateShader(shaderType)
+	csources, free := gl.Strs(source)
+	gl.ShaderSource(shader, 1, csources, nil)
+	free()
+	gl.CompileShader(shader)
+
+	var status int32
+	gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
+	if status == gl.FALSE {
+		var logLength int32
+		gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)
+		logMsg := make([]byte, logLength)
+		gl.GetShaderInfoLog(shader, logLength, nil, &logMsg[0])
+		log.Fatalf("Failed to compile shader: %s", logMsg)
+	}
+
+	return shader, nil
+}
diff --git a/main.go b/main.go
index 3fdca32..b938372 100644
--- a/main.go
+++ b/main.go
@@ -14,287 +14,12 @@ import (
 	"time"
 
 	"github.com/ThatOtherAndrew/Hexecute/internal/models"
+	"github.com/ThatOtherAndrew/Hexecute/internal/shaders"
 	"github.com/ThatOtherAndrew/Hexecute/internal/stroke"
 	"github.com/ThatOtherAndrew/Hexecute/pkg/wayland"
 	"github.com/go-gl/gl/v4.1-core/gl"
 )
 
-const lineVertexShader = `
-#version 410 core
-layout (location = 0) in vec2 position;
-layout (location = 1) in vec2 offset;
-layout (location = 2) in float alpha;
-
-uniform vec2 resolution;
-uniform float thickness;
-
-out float vAlpha;
-out vec2 vPosition;
-
-void main() {
-	vec2 pos = position + offset * thickness;
-	vec2 normalized = (pos / resolution) * 2.0 - 1.0;
-	normalized.y = -normalized.y;
-	gl_Position = vec4(normalized, 0.0, 1.0);
-	vAlpha = alpha;
-	vPosition = pos;
-}
-` + "\x00"
-
-const lineFragmentShader = `
-#version 410 core
-in float vAlpha;
-in vec2 vPosition;
-out vec4 FragColor;
-
-uniform float time;
-
-vec3 hsv2rgb(vec3 c) {
-	vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
-	vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
-	return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
-}
-
-void main() {
-	float hue = mod(vPosition.x * 0.001 + vPosition.y * 0.001 + time * 0.5, 1.0);
-	vec3 color = hsv2rgb(vec3(hue, 0.8, 1.0));
-
-	float sparkle = sin(vPosition.x * 0.1 + time * 3.0) * sin(vPosition.y * 0.1 + time * 2.0);
-	sparkle = smoothstep(0.7, 1.0, sparkle) * 0.5;
-
-	FragColor = vec4(color * (1.0 + sparkle * 2.0), vAlpha);
-}
-` + "\x00"
-
-const particleVertexShader = `
-#version 410 core
-layout (location = 0) in vec2 position;
-layout (location = 1) in float life;
-layout (location = 2) in float maxLife;
-layout (location = 3) in float size;
-layout (location = 4) in float hue;
-
-uniform vec2 resolution;
-
-out float vLife;
-out float vHue;
-
-void main() {
-	vec2 normalized = (position / resolution) * 2.0 - 1.0;
-	normalized.y = -normalized.y;
-	gl_Position = vec4(normalized, 0.0, 1.0);
-	gl_PointSize = size * (life / maxLife);
-	vLife = life / maxLife;
-	vHue = hue;
-}
-` + "\x00"
-
-const particleFragmentShader = `
-#version 410 core
-in float vLife;
-in float vHue;
-out vec4 FragColor;
-
-vec3 hsv2rgb(vec3 c) {
-	vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
-	vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
-	return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
-}
-
-void main() {
-	vec2 coord = gl_PointCoord - vec2(0.5);
-	float dist = length(coord);
-	if (dist > 0.5) discard;
-
-	float alpha = smoothstep(0.5, 0.2, dist) * vLife;
-	vec3 color = hsv2rgb(vec3(vHue, 0.9, 1.0)) * (1.0 + (1.0 - dist * 2.0) * 2.0);
-
-	FragColor = vec4(color, alpha * 0.8);
-}
-` + "\x00"
-
-const backgroundVertexShader = `
-#version 410 core
-layout (location = 0) in vec2 position;
-
-void main() {
-	gl_Position = vec4(position, 0.0, 1.0);
-}
-` + "\x00"
-
-const backgroundFragmentShader = `
-#version 410 core
-out vec4 FragColor;
-
-uniform float alpha;
-uniform vec2 cursorPos;
-uniform vec2 resolution;
-
-void main() {
-	vec2 fragCoord = gl_FragCoord.xy;
-	float dist = length(fragCoord - cursorPos);
-	float glowFalloff = smoothstep(0.0, 300.0, dist);
-	float cursorTransparency = mix(0.3, 1.0, glowFalloff);
-
-	FragColor = vec4(0., 0., 0., alpha * cursorTransparency);
-}
-` + "\x00"
-
-const cursorGlowVertexShader = `
-#version 410 core
-layout (location = 0) in vec2 position;
-
-uniform vec2 cursorPos;
-uniform vec2 resolution;
-uniform float glowSize;
-uniform float rotation;
-
-out vec2 vTexCoord;
-
-void main() {
-	float c = cos(rotation);
-	float s = sin(rotation);
-	vec2 rotatedPos = vec2(position.x * c - position.y * s, position.x * s + position.y * c);
-	vec2 worldPos = cursorPos + rotatedPos * glowSize;
-	vec2 normalized = (worldPos / resolution) * 2.0 - 1.0;
-	normalized.y = -normalized.y;
-	gl_Position = vec4(normalized, 0.0, 1.0);
-	vTexCoord = rotatedPos * 0.5 + 0.5;
-}
-` + "\x00"
-
-const cursorGlowFragmentShader = `
-#version 410 core
-in vec2 vTexCoord;
-out vec4 FragColor;
-
-uniform float time;
-uniform float velocity;
-uniform float isDrawing;
-uniform float exitProgress;
-
-vec3 hsv2rgb(vec3 c) {
-	vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
-	vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
-	return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
-}
-
-float smin(float a, float b, float k) {
-	float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
-	return mix(b, a, h) - k * h * (1.0 - h);
-}
-
-float hash(vec2 p) {
-	p = fract(p * vec2(123.34, 456.21));
-	p += dot(p, p + 45.32);
-	return fract(p.x * p.y);
-}
-
-float noise(vec2 p) {
-	vec2 i = floor(p);
-	vec2 f = fract(p);
-	f = f * f * (3.0 - 2.0 * f);
-
-	float a = hash(i);
-	float b = hash(i + vec2(1.0, 0.0));
-	float c = hash(i + vec2(0.0, 1.0));
-	float d = hash(i + vec2(1.0, 1.0));
-
-	return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
-}
-
-float fbm(vec2 p) {
-	float value = 0.0;
-	float amplitude = 0.5;
-	float frequency = 1.0;
-
-	for(int i = 0; i < 4; i++) {
-		value += amplitude * noise(p * frequency);
-		frequency *= 2.0;
-		amplitude *= 0.5;
-	}
-	return value;
-}
-
-void main() {
-	vec2 coord = vTexCoord * 2.0 - 1.0;
-	float velocityNorm = clamp(velocity * 0.01, 0.0, 1.0);
-	float energy = mix(0.3, 1.0, velocityNorm) + isDrawing * 0.7;
-
-	float sdf = 1000.0;
-	float centralSize = mix(0.15, 0.35, velocityNorm) + isDrawing * 0.05;
-	float pulseSpeed = (3.0 + velocityNorm * 2.0) * (1.0 + isDrawing * 0.75);
-	float pulseAmount = (0.1 * energy + isDrawing * 0.075);
-	float pulse = sin(time * pulseSpeed) * pulseAmount + 0.9;
-	float centralDist = length(coord) - centralSize * pulse;
-	sdf = centralDist;
-
-	float numBlobsFloat = mix(5.0, 9.0, velocityNorm) + isDrawing * 1.0;
-	int numBlobs = int(numBlobsFloat);
-	float blobFade = fract(numBlobsFloat);
-
-	for(int i = 0; i < 10; i++) {
-		if(i > numBlobs) break;
-		if(i == numBlobs && blobFade < 0.01) break;
-		float baseRotation = time * 0.8;
-		float velocityRotation = time * velocityNorm * 0.4;
-		float angle = float(i) * 6.28 / float(numBlobs) + baseRotation + velocityRotation;
-
-		float baseRadius = mix(0.2, 0.5, velocityNorm) + isDrawing * 0.075;
-		float radiusVariation = sin(time * (1.5 + isDrawing * 0.5) + float(i) * 0.8) * mix(0.05, 0.15, velocityNorm);
-		float chaoticRadius = sin(time * 4.0 + float(i) * 2.1) * cos(time * 3.5 + float(i) * 1.7) * 0.003 * isDrawing;
-		float radius = baseRadius + radiusVariation + chaoticRadius;
-		vec2 orbPos = vec2(cos(angle), sin(angle)) * radius;
-
-		float baseBlobSize = mix(0.08, 0.18, velocityNorm) + isDrawing * 0.04;
-		float sizeVariation = sin(time * (2.5 + isDrawing * 1.0) + float(i) * 0.6) * mix(0.02, 0.05, velocityNorm);
-		float drawingGrowth = sin(time * 5.0 + float(i) * 1.3) * 0.03 * isDrawing;
-		float blobSize = baseBlobSize + sizeVariation + drawingGrowth;
-		float blobDist = length(coord - orbPos) - blobSize;
-
-		if(i == numBlobs) {
-			blobDist += (1.0 - blobFade) * 0.5;
-		}
-		float blendAmount = mix(0.15, 0.3, velocityNorm) + isDrawing * 0.075;
-		sdf = smin(sdf, blobDist, blendAmount);
-	}
-
-	float noiseZoom = 3.0 + isDrawing * 0.5;
-	vec2 noiseCoord = coord * noiseZoom;
-	noiseCoord += vec2(time * 0.3, time * 0.2);
-	float swirl = fbm(noiseCoord) * 2.0 - 1.0;
-
-	sdf += swirl * (0.1 * energy + exitProgress * 0.8);
-	float intensity = exp(-max(sdf, 0.0) * 4.0);
-	float outerGlow = exp(-max(sdf, 0.0) * 1.5) * 0.4 * energy;
-	float innerGlow = exp(-max(sdf, 0.0) * 8.0) * 0.8;
-
-	float totalIntensity = intensity + outerGlow + innerGlow;
-
-	totalIntensity *= smoothstep(1.0, 0.7, max(abs(coord.x), abs(coord.y)));
-
-	float hueSpeed = mix(0.2, 0.6, velocityNorm);
-	float hue = mod(time * hueSpeed + atan(coord.y, coord.x) / 6.28 + swirl * 0.3, 1.0);
-	vec3 mainColor = hsv2rgb(vec3(hue, mix(0.7, 0.75, velocityNorm), 1.0));
-	vec3 accentColor = hsv2rgb(vec3(mod(hue + 0.5, 1.0), 0.75, 1.2));
-	vec3 finalColor = mainColor * intensity;
-	finalColor += accentColor * innerGlow;
-	finalColor += mainColor * 0.5 * outerGlow;
-
-	float sparkle = smoothstep(0.85, 1.0, noise(coord * 20.0 + time * 5.0 * energy)) * totalIntensity * velocityNorm;
-	finalColor += sparkle;
-
-	float edge = smoothstep(0.05, -0.05, sdf) - smoothstep(0.15, 0.05, sdf);
-	finalColor += accentColor * edge * energy;
-
-	finalColor *= sin(time * (2.5 + isDrawing * 0.75)) * (0.1 + velocityNorm * 0.1 + isDrawing * 0.075) + 0.9;
-
-	float alpha = clamp(totalIntensity * mix(0.8, 1.3, velocityNorm), 0.0, 1.0) * (1.0 - exitProgress);
-
-	FragColor = vec4(finalColor, alpha * 0.95);
-}
-` + "\x00"
-
 func init() {
 	runtime.LockOSThread()
 }
@@ -336,11 +61,11 @@ func (a *App) initGL() error {
 		return err
 	}
 
-	vertShader, err := compileShader(lineVertexShader, gl.VERTEX_SHADER)
+	vertShader, err := shaders.CompileShader(shaders.LineVertexShader, gl.VERTEX_SHADER)
 	if err != nil {
 		return err
 	}
-	fragShader, err := compileShader(lineFragmentShader, gl.FRAGMENT_SHADER)
+	fragShader, err := shaders.CompileShader(shaders.LineFragmentShader, gl.FRAGMENT_SHADER)
 	if err != nil {
 		return err
 	}
@@ -363,11 +88,14 @@ func (a *App) initGL() error {
 	gl.DeleteShader(vertShader)
 	gl.DeleteShader(fragShader)
 
-	particleVertShader, err := compileShader(particleVertexShader, gl.VERTEX_SHADER)
+	particleVertShader, err := shaders.CompileShader(shaders.ParticleVertexShader, gl.VERTEX_SHADER)
 	if err != nil {
 		return err
 	}
-	particleFragShader, err := compileShader(particleFragmentShader, gl.FRAGMENT_SHADER)
+	particleFragShader, err := shaders.CompileShader(
+		shaders.ParticleFragmentShader,
+		gl.FRAGMENT_SHADER,
+	)
 	if err != nil {
 		return err
 	}
@@ -423,11 +151,11 @@ func (a *App) initGL() error {
 
 	gl.BindVertexArray(0)
 
-	bgVertShader, err := compileShader(backgroundVertexShader, gl.VERTEX_SHADER)
+	bgVertShader, err := shaders.CompileShader(shaders.BackgroundVertexShader, gl.VERTEX_SHADER)
 	if err != nil {
 		return err
 	}
-	bgFragShader, err := compileShader(backgroundFragmentShader, gl.FRAGMENT_SHADER)
+	bgFragShader, err := shaders.CompileShader(shaders.BackgroundFragmentShader, gl.FRAGMENT_SHADER)
 	if err != nil {
 		return err
 	}
@@ -468,11 +196,17 @@ func (a *App) initGL() error {
 
 	gl.BindVertexArray(0)
 
-	cursorGlowVertShader, err := compileShader(cursorGlowVertexShader, gl.VERTEX_SHADER)
+	cursorGlowVertShader, err := shaders.CompileShader(
+		shaders.CursorGlowVertexShader,
+		gl.VERTEX_SHADER,
+	)
 	if err != nil {
 		return err
 	}
-	cursorGlowFragShader, err := compileShader(cursorGlowFragmentShader, gl.FRAGMENT_SHADER)
+	cursorGlowFragShader, err := shaders.CompileShader(
+		shaders.CursorGlowFragmentShader,
+		gl.FRAGMENT_SHADER,
+	)
 	if err != nil {
 		return err
 	}
@@ -525,26 +259,6 @@ func (a *App) initGL() error {
 	return nil
 }
 
-func compileShader(source string, shaderType uint32) (uint32, error) {
-	shader := gl.CreateShader(shaderType)
-	csources, free := gl.Strs(source)
-	gl.ShaderSource(shader, 1, csources, nil)
-	free()
-	gl.CompileShader(shader)
-
-	var status int32
-	gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
-	if status == gl.FALSE {
-		var logLength int32
-		gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)
-		logMsg := make([]byte, logLength)
-		gl.GetShaderInfoLog(shader, logLength, nil, &logMsg[0])
-		log.Fatalf("Failed to compile shader: %s", logMsg)
-	}
-
-	return shader, nil
-}
-
 func (a *App) addPoint(x, y float32) {
 	newPoint := models.Point{X: x, Y: y, BornTime: time.Now()}
 

From 731daa536a1af323bff1186ebd42f9b655bb4a4e Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Wed, 15 Oct 2025 23:28:42 +0300
Subject: [PATCH 08/24] feat: better shader implementation

---
 ...groundFragment.go => background.frag.glsl} |  4 ---
 ...ckgroundVertex.go => background.vert.glsl} |  4 ---
 ...orGlowFragment.go => cursorGlow.frag.glsl} |  4 ---
 ...rsorGlowVertex.go => cursorGlow.vert.glsl} |  4 ---
 .../{lineFragment.go => line.frag.glsl}       |  4 ---
 .../shaders/{lineVertex.go => line.vert.glsl} |  4 ---
 ...particleFragment.go => particle.frag.glsl} |  4 ---
 .../{pacticleVertex.go => particle.vert.glsl} |  4 ---
 internal/shaders/paths.go                     | 10 ++++++
 internal/shaders/shader.go                    | 31 ++++++++++++++-----
 main.go                                       | 31 ++++++++++++-------
 11 files changed, 54 insertions(+), 50 deletions(-)
 rename internal/shaders/{backgroundFragment.go => background.frag.glsl} (85%)
 rename internal/shaders/{backgroundVertex.go => background.vert.glsl} (65%)
 rename internal/shaders/{cursorGlowFragment.go => cursorGlow.frag.glsl} (98%)
 rename internal/shaders/{cursorGlowVertex.go => cursorGlow.vert.glsl} (90%)
 rename internal/shaders/{lineFragment.go => line.frag.glsl} (91%)
 rename internal/shaders/{lineVertex.go => line.vert.glsl} (88%)
 rename internal/shaders/{particleFragment.go => particle.frag.glsl} (89%)
 rename internal/shaders/{pacticleVertex.go => particle.vert.glsl} (89%)
 create mode 100644 internal/shaders/paths.go

diff --git a/internal/shaders/backgroundFragment.go b/internal/shaders/background.frag.glsl
similarity index 85%
rename from internal/shaders/backgroundFragment.go
rename to internal/shaders/background.frag.glsl
index 5ee004a..2f4dc31 100644
--- a/internal/shaders/backgroundFragment.go
+++ b/internal/shaders/background.frag.glsl
@@ -1,6 +1,3 @@
-package shaders
-
-const BackgroundFragmentShader = `
 #version 410 core
 out vec4 FragColor;
 
@@ -16,4 +13,3 @@ void main() {
 
 	FragColor = vec4(0., 0., 0., alpha * cursorTransparency);
 }
-` + "\x00"
diff --git a/internal/shaders/backgroundVertex.go b/internal/shaders/background.vert.glsl
similarity index 65%
rename from internal/shaders/backgroundVertex.go
rename to internal/shaders/background.vert.glsl
index 5d4db7a..a48563d 100644
--- a/internal/shaders/backgroundVertex.go
+++ b/internal/shaders/background.vert.glsl
@@ -1,10 +1,6 @@
-package shaders
-
-const BackgroundVertexShader = `
 #version 410 core
 layout (location = 0) in vec2 position;
 
 void main() {
 	gl_Position = vec4(position, 0.0, 1.0);
 }
-` + "\x00"
diff --git a/internal/shaders/cursorGlowFragment.go b/internal/shaders/cursorGlow.frag.glsl
similarity index 98%
rename from internal/shaders/cursorGlowFragment.go
rename to internal/shaders/cursorGlow.frag.glsl
index 6500a85..edfb78a 100644
--- a/internal/shaders/cursorGlowFragment.go
+++ b/internal/shaders/cursorGlow.frag.glsl
@@ -1,6 +1,3 @@
-package shaders
-
-const CursorGlowFragmentShader = `
 #version 410 core
 in vec2 vTexCoord;
 out vec4 FragColor;
@@ -130,4 +127,3 @@ void main() {
 
 	FragColor = vec4(finalColor, alpha * 0.95);
 }
-` + "\x00"
diff --git a/internal/shaders/cursorGlowVertex.go b/internal/shaders/cursorGlow.vert.glsl
similarity index 90%
rename from internal/shaders/cursorGlowVertex.go
rename to internal/shaders/cursorGlow.vert.glsl
index 2db01cc..ac68929 100644
--- a/internal/shaders/cursorGlowVertex.go
+++ b/internal/shaders/cursorGlow.vert.glsl
@@ -1,6 +1,3 @@
-package shaders
-
-const CursorGlowVertexShader = `
 #version 410 core
 layout (location = 0) in vec2 position;
 
@@ -21,4 +18,3 @@ void main() {
 	gl_Position = vec4(normalized, 0.0, 1.0);
 	vTexCoord = rotatedPos * 0.5 + 0.5;
 }
-` + "\x00"
diff --git a/internal/shaders/lineFragment.go b/internal/shaders/line.frag.glsl
similarity index 91%
rename from internal/shaders/lineFragment.go
rename to internal/shaders/line.frag.glsl
index b764151..e0ce5a0 100644
--- a/internal/shaders/lineFragment.go
+++ b/internal/shaders/line.frag.glsl
@@ -1,6 +1,3 @@
-package shaders
-
-const LineFragmentShader = `
 #version 410 core
 in float vAlpha;
 in vec2 vPosition;
@@ -23,4 +20,3 @@ void main() {
 
 	FragColor = vec4(color * (1.0 + sparkle * 2.0), vAlpha);
 }
-` + "\x00"
diff --git a/internal/shaders/lineVertex.go b/internal/shaders/line.vert.glsl
similarity index 88%
rename from internal/shaders/lineVertex.go
rename to internal/shaders/line.vert.glsl
index 26c4aac..62cb8e0 100644
--- a/internal/shaders/lineVertex.go
+++ b/internal/shaders/line.vert.glsl
@@ -1,6 +1,3 @@
-package shaders
-
-const LineVertexShader = `
 #version 410 core
 layout (location = 0) in vec2 position;
 layout (location = 1) in vec2 offset;
@@ -20,4 +17,3 @@ void main() {
 	vAlpha = alpha;
 	vPosition = pos;
 }
-` + "\x00"
diff --git a/internal/shaders/particleFragment.go b/internal/shaders/particle.frag.glsl
similarity index 89%
rename from internal/shaders/particleFragment.go
rename to internal/shaders/particle.frag.glsl
index def3b2a..aef6f60 100644
--- a/internal/shaders/particleFragment.go
+++ b/internal/shaders/particle.frag.glsl
@@ -1,6 +1,3 @@
-package shaders
-
-const ParticleFragmentShader = `
 #version 410 core
 in float vLife;
 in float vHue;
@@ -22,4 +19,3 @@ void main() {
 
 	FragColor = vec4(color, alpha * 0.8);
 }
-` + "\x00"
diff --git a/internal/shaders/pacticleVertex.go b/internal/shaders/particle.vert.glsl
similarity index 89%
rename from internal/shaders/pacticleVertex.go
rename to internal/shaders/particle.vert.glsl
index 0599378..3d165aa 100644
--- a/internal/shaders/pacticleVertex.go
+++ b/internal/shaders/particle.vert.glsl
@@ -1,6 +1,3 @@
-package shaders
-
-const ParticleVertexShader = `
 #version 410 core
 layout (location = 0) in vec2 position;
 layout (location = 1) in float life;
@@ -21,4 +18,3 @@ void main() {
 	vLife = life / maxLife;
 	vHue = hue;
 }
-` + "\x00"
diff --git a/internal/shaders/paths.go b/internal/shaders/paths.go
new file mode 100644
index 0000000..b496856
--- /dev/null
+++ b/internal/shaders/paths.go
@@ -0,0 +1,10 @@
+package shaders
+
+const BackgroundFragmentPath = "internal/shaders/background.frag.glsl"
+const BackgroundVertexPath = "internal/shaders/background.vert.glsl"
+const CursorGlowFragmentPath = "internal/shaders/cursorGlow.frag.glsl"
+const CursorGlowVertexPath = "internal/shaders/cursorGlow.vert.glsl"
+const LineFragmentPath = "internal/shaders/line.frag.glsl"
+const LineVertexPath = "internal/shaders/line.vert.glsl"
+const ParticleVertexPath = "internal/shaders/particle.vert.glsl"
+const ParticleFragmentPath = "internal/shaders/particle.frag.glsl"
diff --git a/internal/shaders/shader.go b/internal/shaders/shader.go
index 27ea2fb..8aad584 100644
--- a/internal/shaders/shader.go
+++ b/internal/shaders/shader.go
@@ -1,16 +1,30 @@
 package shaders
 
 import (
-	"log"
+	"fmt"
+	"os"
+	"strings"
 
-	"github.com/go-gl/gl/v2.1/gl"
+	"github.com/go-gl/gl/v4.1-core/gl"
 )
 
-func CompileShader(source string, shaderType uint32) (uint32, error) {
+func CompileShaderFromFile(path string, shaderType uint32) (uint32, error) {
+	sourceBytes, err := os.ReadFile(path)
+	if err != nil {
+		return 0, fmt.Errorf("failed to read shader file %q: %v", path, err)
+	}
+
+	source := string(sourceBytes)
+
+	if !strings.HasSuffix(source, "\x00") {
+		source += "\x00"
+	}
+
 	shader := gl.CreateShader(shaderType)
 	csources, free := gl.Strs(source)
+	defer free()
+
 	gl.ShaderSource(shader, 1, csources, nil)
-	free()
 	gl.CompileShader(shader)
 
 	var status int32
@@ -18,9 +32,12 @@ func CompileShader(source string, shaderType uint32) (uint32, error) {
 	if status == gl.FALSE {
 		var logLength int32
 		gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)
-		logMsg := make([]byte, logLength)
-		gl.GetShaderInfoLog(shader, logLength, nil, &logMsg[0])
-		log.Fatalf("Failed to compile shader: %s", logMsg)
+
+		logMsg := strings.Repeat("\x00", int(logLength+1))
+		gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(logMsg))
+
+		gl.DeleteShader(shader)
+		return 0, fmt.Errorf("failed to compile %s shader: %v", path, strings.TrimSpace(logMsg))
 	}
 
 	return shader, nil
diff --git a/main.go b/main.go
index b938372..510c54d 100644
--- a/main.go
+++ b/main.go
@@ -61,11 +61,11 @@ func (a *App) initGL() error {
 		return err
 	}
 
-	vertShader, err := shaders.CompileShader(shaders.LineVertexShader, gl.VERTEX_SHADER)
+	vertShader, err := shaders.CompileShaderFromFile(shaders.LineVertexPath, gl.VERTEX_SHADER)
 	if err != nil {
 		return err
 	}
-	fragShader, err := shaders.CompileShader(shaders.LineFragmentShader, gl.FRAGMENT_SHADER)
+	fragShader, err := shaders.CompileShaderFromFile(shaders.LineFragmentPath, gl.FRAGMENT_SHADER)
 	if err != nil {
 		return err
 	}
@@ -88,12 +88,15 @@ func (a *App) initGL() error {
 	gl.DeleteShader(vertShader)
 	gl.DeleteShader(fragShader)
 
-	particleVertShader, err := shaders.CompileShader(shaders.ParticleVertexShader, gl.VERTEX_SHADER)
+	particleVertShader, err := shaders.CompileShaderFromFile(
+		shaders.ParticleVertexPath,
+		gl.VERTEX_SHADER,
+	)
 	if err != nil {
 		return err
 	}
-	particleFragShader, err := shaders.CompileShader(
-		shaders.ParticleFragmentShader,
+	particleFragShader, err := shaders.CompileShaderFromFile(
+		shaders.ParticleFragmentPath,
 		gl.FRAGMENT_SHADER,
 	)
 	if err != nil {
@@ -151,11 +154,17 @@ func (a *App) initGL() error {
 
 	gl.BindVertexArray(0)
 
-	bgVertShader, err := shaders.CompileShader(shaders.BackgroundVertexShader, gl.VERTEX_SHADER)
+	bgVertShader, err := shaders.CompileShaderFromFile(
+		shaders.BackgroundVertexPath,
+		gl.VERTEX_SHADER,
+	)
 	if err != nil {
 		return err
 	}
-	bgFragShader, err := shaders.CompileShader(shaders.BackgroundFragmentShader, gl.FRAGMENT_SHADER)
+	bgFragShader, err := shaders.CompileShaderFromFile(
+		shaders.BackgroundFragmentPath,
+		gl.FRAGMENT_SHADER,
+	)
 	if err != nil {
 		return err
 	}
@@ -196,15 +205,15 @@ func (a *App) initGL() error {
 
 	gl.BindVertexArray(0)
 
-	cursorGlowVertShader, err := shaders.CompileShader(
-		shaders.CursorGlowVertexShader,
+	cursorGlowVertShader, err := shaders.CompileShaderFromFile(
+		shaders.CursorGlowVertexPath,
 		gl.VERTEX_SHADER,
 	)
 	if err != nil {
 		return err
 	}
-	cursorGlowFragShader, err := shaders.CompileShader(
-		shaders.CursorGlowFragmentShader,
+	cursorGlowFragShader, err := shaders.CompileShaderFromFile(
+		shaders.CursorGlowFragmentPath,
 		gl.FRAGMENT_SHADER,
 	)
 	if err != nil {

From b2b216c20294f5b7dde9f96561a89832e57b87de Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Wed, 15 Oct 2025 23:31:56 +0300
Subject: [PATCH 09/24] refactor: added config package

---
 internal/config/config.go | 18 ++++++++++++++++++
 main.go                   | 20 ++++----------------
 2 files changed, 22 insertions(+), 16 deletions(-)
 create mode 100644 internal/config/config.go

diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..d2c2ef5
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,18 @@
+package config
+
+import (
+	"os"
+	"path/filepath"
+)
+
+func GetPath() (string, error) {
+	homeDir, err := os.UserHomeDir()
+	if err != nil {
+		return "", err
+	}
+	configDir := filepath.Join(homeDir, ".config", "hexecute")
+	if err := os.MkdirAll(configDir, 0755); err != nil {
+		return "", err
+	}
+	return filepath.Join(configDir, "gestures.json"), nil
+}
diff --git a/main.go b/main.go
index 510c54d..c973a4f 100644
--- a/main.go
+++ b/main.go
@@ -8,11 +8,11 @@ import (
 	"math/rand"
 	"os"
 	"os/exec"
-	"path/filepath"
 	"runtime"
 	"syscall"
 	"time"
 
+	"github.com/ThatOtherAndrew/Hexecute/internal/config"
 	"github.com/ThatOtherAndrew/Hexecute/internal/models"
 	"github.com/ThatOtherAndrew/Hexecute/internal/shaders"
 	"github.com/ThatOtherAndrew/Hexecute/internal/stroke"
@@ -645,20 +645,8 @@ func (a *App) drawCursorGlow(window *wayland.WaylandWindow, cursorX, cursorY, cu
 	gl.BindVertexArray(0)
 }
 
-func getConfigPath() (string, error) {
-	homeDir, err := os.UserHomeDir()
-	if err != nil {
-		return "", err
-	}
-	configDir := filepath.Join(homeDir, ".config", "hexecute")
-	if err := os.MkdirAll(configDir, 0755); err != nil {
-		return "", err
-	}
-	return filepath.Join(configDir, "gestures.json"), nil
-}
-
 func loadGestures() ([]models.GestureConfig, error) {
-	configFile, err := getConfigPath()
+	configFile, err := config.GetPath()
 	if err != nil {
 		return nil, err
 	}
@@ -680,7 +668,7 @@ func loadGestures() ([]models.GestureConfig, error) {
 }
 
 func saveGesture(command string, templates [][]models.Point) error {
-	configFile, err := getConfigPath()
+	configFile, err := config.GetPath()
 	if err != nil {
 		return err
 	}
@@ -816,7 +804,7 @@ func main() {
 			log.Fatalf("Gesture not found: %s", *removeGesture)
 		}
 
-		configFile, err := getConfigPath()
+		configFile, err := config.GetPath()
 		if err != nil {
 			log.Fatal("Failed to get config path:", err)
 		}

From 24a8a91007a671127481344378d3350723cf79f1 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Wed, 15 Oct 2025 23:36:07 +0300
Subject: [PATCH 10/24] refactor: added execute package

---
 internal/execute/execute.go | 22 ++++++++++++++++++++++
 main.go                     | 21 ++-------------------
 2 files changed, 24 insertions(+), 19 deletions(-)
 create mode 100644 internal/execute/execute.go

diff --git a/internal/execute/execute.go b/internal/execute/execute.go
new file mode 100644
index 0000000..b470191
--- /dev/null
+++ b/internal/execute/execute.go
@@ -0,0 +1,22 @@
+package execute
+
+import (
+	"os/exec"
+	"syscall"
+)
+
+func Command(command string) error {
+	if command == "" {
+		return nil
+	}
+
+	cmd := exec.Command("sh", "-c", command)
+	cmd.SysProcAttr = &syscall.SysProcAttr{
+		Setsid: true,
+	}
+	cmd.Stdin = nil
+	cmd.Stdout = nil
+	cmd.Stderr = nil
+
+	return cmd.Start()
+}
diff --git a/main.go b/main.go
index c973a4f..9456d26 100644
--- a/main.go
+++ b/main.go
@@ -7,12 +7,11 @@ import (
 	"math"
 	"math/rand"
 	"os"
-	"os/exec"
 	"runtime"
-	"syscall"
 	"time"
 
 	"github.com/ThatOtherAndrew/Hexecute/internal/config"
+	"github.com/ThatOtherAndrew/Hexecute/internal/execute"
 	"github.com/ThatOtherAndrew/Hexecute/internal/models"
 	"github.com/ThatOtherAndrew/Hexecute/internal/shaders"
 	"github.com/ThatOtherAndrew/Hexecute/internal/stroke"
@@ -703,22 +702,6 @@ func saveGesture(command string, templates [][]models.Point) error {
 	return os.WriteFile(configFile, data, 0644)
 }
 
-func executeCommand(command string) error {
-	if command == "" {
-		return nil
-	}
-
-	cmd := exec.Command("sh", "-c", command)
-	cmd.SysProcAttr = &syscall.SysProcAttr{
-		Setsid: true,
-	}
-	cmd.Stdin = nil
-	cmd.Stdout = nil
-	cmd.Stderr = nil
-
-	return cmd.Start()
-}
-
 func (a *App) recognizeAndExecute(window *wayland.WaylandWindow, x, y float32) {
 	if len(a.points) < 5 {
 		log.Println("Gesture too short, ignoring")
@@ -744,7 +727,7 @@ func (a *App) recognizeAndExecute(window *wayland.WaylandWindow, x, y float32) {
 		command := a.savedGestures[bestMatch].Command
 		log.Printf("Matched gesture: %s (score: %.3f)", command, bestScore)
 
-		if err := executeCommand(command); err != nil {
+		if err := execute.Command(command); err != nil {
 			log.Printf("Failed to execute command: %v", err)
 		} else {
 			log.Printf("Executed: %s", command)

From 1731bab37f1641c0d8a30e3670424b78a24e9d63 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Wed, 15 Oct 2025 23:45:14 +0300
Subject: [PATCH 11/24] refactor: added default dpackage

---
 main.go => cmd/hexecute/main.go | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename main.go => cmd/hexecute/main.go (100%)

diff --git a/main.go b/cmd/hexecute/main.go
similarity index 100%
rename from main.go
rename to cmd/hexecute/main.go

From bcdc939c5e72025af206933cdb746c35a966f345 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Thu, 16 Oct 2025 00:50:33 +0300
Subject: [PATCH 12/24] refactor: added gesture package

---
 internal/gesture/gesture.go | 67 +++++++++++++++++++++++++++++++++++++
 1 file changed, 67 insertions(+)
 create mode 100644 internal/gesture/gesture.go

diff --git a/internal/gesture/gesture.go b/internal/gesture/gesture.go
new file mode 100644
index 0000000..deb647e
--- /dev/null
+++ b/internal/gesture/gesture.go
@@ -0,0 +1,67 @@
+package gestures
+
+import (
+	"encoding/json"
+	"os"
+
+	"github.com/ThatOtherAndrew/Hexecute/internal/config"
+	"github.com/ThatOtherAndrew/Hexecute/internal/models"
+)
+
+func LoadGestures() ([]models.GestureConfig, error) {
+	configFile, err := config.GetPath()
+	if err != nil {
+		return nil, err
+	}
+
+	data, err := os.ReadFile(configFile)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return []models.GestureConfig{}, nil
+		}
+		return nil, err
+	}
+
+	var gestures []models.GestureConfig
+	if err := json.Unmarshal(data, &gestures); err != nil {
+		return nil, err
+	}
+
+	return gestures, nil
+}
+
+func SaveGesture(command string, templates [][]models.Point) error {
+	configFile, err := config.GetPath()
+	if err != nil {
+		return err
+	}
+
+	var gestures []models.GestureConfig
+	if data, err := os.ReadFile(configFile); err == nil {
+		json.Unmarshal(data, &gestures)
+	}
+
+	newGesture := models.GestureConfig{
+		Command:   command,
+		Templates: templates,
+	}
+
+	found := false
+	for i, g := range gestures {
+		if g.Command == command {
+			gestures[i] = newGesture
+			found = true
+			break
+		}
+	}
+	if !found {
+		gestures = append(gestures, newGesture)
+	}
+
+	data, err := json.Marshal(gestures)
+	if err != nil {
+		return err
+	}
+
+	return os.WriteFile(configFile, data, 0644)
+}

From d7587502270758fb74db60bcae14abdd594d74b3 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Thu, 16 Oct 2025 00:51:00 +0300
Subject: [PATCH 13/24] chore: updated gitignore

---
 .gitignore | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index ffa95da..e660fd9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1 @@
 bin/
-hexecute

From 2c7426f8cd2731ece967566fd5f6564c24a59034 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Thu, 16 Oct 2025 01:07:28 +0300
Subject: [PATCH 14/24] refactor: all the project

---
 cmd/hexecute/main.go      | 816 +++-----------------------------------
 internal/draw/draw.go     | 265 +++++++++++++
 internal/fornow/fornow.go | 103 +++++
 internal/models/models.go |  36 +-
 internal/opengl/opengl.go | 229 +++++++++++
 internal/spawn/spawn.go   |  50 +++
 internal/update/update.go |  72 ++++
 7 files changed, 810 insertions(+), 761 deletions(-)
 create mode 100644 internal/draw/draw.go
 create mode 100644 internal/fornow/fornow.go
 create mode 100644 internal/opengl/opengl.go
 create mode 100644 internal/spawn/spawn.go
 create mode 100644 internal/update/update.go

diff --git a/cmd/hexecute/main.go b/cmd/hexecute/main.go
index 9456d26..e331683 100644
--- a/cmd/hexecute/main.go
+++ b/cmd/hexecute/main.go
@@ -4,17 +4,19 @@ import (
 	"encoding/json"
 	"flag"
 	"log"
-	"math"
-	"math/rand"
 	"os"
 	"runtime"
 	"time"
 
 	"github.com/ThatOtherAndrew/Hexecute/internal/config"
-	"github.com/ThatOtherAndrew/Hexecute/internal/execute"
+	"github.com/ThatOtherAndrew/Hexecute/internal/draw"
+	"github.com/ThatOtherAndrew/Hexecute/internal/fornow"
+	gestures "github.com/ThatOtherAndrew/Hexecute/internal/gesture"
 	"github.com/ThatOtherAndrew/Hexecute/internal/models"
-	"github.com/ThatOtherAndrew/Hexecute/internal/shaders"
+	"github.com/ThatOtherAndrew/Hexecute/internal/opengl"
+	"github.com/ThatOtherAndrew/Hexecute/internal/spawn"
 	"github.com/ThatOtherAndrew/Hexecute/internal/stroke"
+	"github.com/ThatOtherAndrew/Hexecute/internal/update"
 	"github.com/ThatOtherAndrew/Hexecute/pkg/wayland"
 	"github.com/go-gl/gl/v4.1-core/gl"
 )
@@ -24,722 +26,7 @@ func init() {
 }
 
 type App struct {
-	points            []models.Point
-	particles         []models.Particle
-	isDrawing         bool
-	vao               uint32
-	vbo               uint32
-	program           uint32
-	particleVAO       uint32
-	particleVBO       uint32
-	particleProgram   uint32
-	bgVAO             uint32
-	bgVBO             uint32
-	bgProgram         uint32
-	cursorGlowVAO     uint32
-	cursorGlowVBO     uint32
-	cursorGlowProgram uint32
-	startTime         time.Time
-	lastCursorX       float32
-	lastCursorY       float32
-	cursorVelocity    float32
-	smoothVelocity    float32
-	smoothRotation    float32
-	smoothDrawing     float32
-	isExiting         bool
-	exitStartTime     time.Time
-	learnMode         bool
-	learnCommand      string
-	learnGestures     [][]models.Point
-	learnCount        int
-	savedGestures     []models.GestureConfig
-}
-
-func (a *App) initGL() error {
-	if err := gl.Init(); err != nil {
-		return err
-	}
-
-	vertShader, err := shaders.CompileShaderFromFile(shaders.LineVertexPath, gl.VERTEX_SHADER)
-	if err != nil {
-		return err
-	}
-	fragShader, err := shaders.CompileShaderFromFile(shaders.LineFragmentPath, gl.FRAGMENT_SHADER)
-	if err != nil {
-		return err
-	}
-
-	a.program = gl.CreateProgram()
-	gl.AttachShader(a.program, vertShader)
-	gl.AttachShader(a.program, fragShader)
-	gl.LinkProgram(a.program)
-
-	var status int32
-	gl.GetProgramiv(a.program, gl.LINK_STATUS, &status)
-	if status == gl.FALSE {
-		var logLength int32
-		gl.GetProgramiv(a.program, gl.INFO_LOG_LENGTH, &logLength)
-		logMsg := make([]byte, logLength)
-		gl.GetProgramInfoLog(a.program, logLength, nil, &logMsg[0])
-		log.Fatalf("Failed to link program: %s", logMsg)
-	}
-
-	gl.DeleteShader(vertShader)
-	gl.DeleteShader(fragShader)
-
-	particleVertShader, err := shaders.CompileShaderFromFile(
-		shaders.ParticleVertexPath,
-		gl.VERTEX_SHADER,
-	)
-	if err != nil {
-		return err
-	}
-	particleFragShader, err := shaders.CompileShaderFromFile(
-		shaders.ParticleFragmentPath,
-		gl.FRAGMENT_SHADER,
-	)
-	if err != nil {
-		return err
-	}
-
-	a.particleProgram = gl.CreateProgram()
-	gl.AttachShader(a.particleProgram, particleVertShader)
-	gl.AttachShader(a.particleProgram, particleFragShader)
-	gl.LinkProgram(a.particleProgram)
-
-	gl.GetProgramiv(a.particleProgram, gl.LINK_STATUS, &status)
-	if status == gl.FALSE {
-		var logLength int32
-		gl.GetProgramiv(a.particleProgram, gl.INFO_LOG_LENGTH, &logLength)
-		logMsg := make([]byte, logLength)
-		gl.GetProgramInfoLog(a.particleProgram, logLength, nil, &logMsg[0])
-		log.Fatalf("Failed to link particle program: %s", logMsg)
-	}
-
-	gl.DeleteShader(particleVertShader)
-	gl.DeleteShader(particleFragShader)
-
-	gl.GenVertexArrays(1, &a.vao)
-	gl.GenBuffers(1, &a.vbo)
-
-	gl.BindVertexArray(a.vao)
-	gl.BindBuffer(gl.ARRAY_BUFFER, a.vbo)
-
-	gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 5*4, nil)
-	gl.EnableVertexAttribArray(0)
-	gl.VertexAttribPointer(1, 2, gl.FLOAT, false, 5*4, gl.PtrOffset(2*4))
-	gl.EnableVertexAttribArray(1)
-	gl.VertexAttribPointer(2, 1, gl.FLOAT, false, 5*4, gl.PtrOffset(4*4))
-	gl.EnableVertexAttribArray(2)
-
-	gl.BindVertexArray(0)
-
-	gl.GenVertexArrays(1, &a.particleVAO)
-	gl.GenBuffers(1, &a.particleVBO)
-
-	gl.BindVertexArray(a.particleVAO)
-	gl.BindBuffer(gl.ARRAY_BUFFER, a.particleVBO)
-
-	gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 6*4, nil)
-	gl.EnableVertexAttribArray(0)
-	gl.VertexAttribPointer(1, 1, gl.FLOAT, false, 6*4, gl.PtrOffset(2*4))
-	gl.EnableVertexAttribArray(1)
-	gl.VertexAttribPointer(2, 1, gl.FLOAT, false, 6*4, gl.PtrOffset(3*4))
-	gl.EnableVertexAttribArray(2)
-	gl.VertexAttribPointer(3, 1, gl.FLOAT, false, 6*4, gl.PtrOffset(4*4))
-	gl.EnableVertexAttribArray(3)
-	gl.VertexAttribPointer(4, 1, gl.FLOAT, false, 6*4, gl.PtrOffset(5*4))
-	gl.EnableVertexAttribArray(4)
-
-	gl.BindVertexArray(0)
-
-	bgVertShader, err := shaders.CompileShaderFromFile(
-		shaders.BackgroundVertexPath,
-		gl.VERTEX_SHADER,
-	)
-	if err != nil {
-		return err
-	}
-	bgFragShader, err := shaders.CompileShaderFromFile(
-		shaders.BackgroundFragmentPath,
-		gl.FRAGMENT_SHADER,
-	)
-	if err != nil {
-		return err
-	}
-
-	a.bgProgram = gl.CreateProgram()
-	gl.AttachShader(a.bgProgram, bgVertShader)
-	gl.AttachShader(a.bgProgram, bgFragShader)
-	gl.LinkProgram(a.bgProgram)
-
-	gl.GetProgramiv(a.bgProgram, gl.LINK_STATUS, &status)
-	if status == gl.FALSE {
-		var logLength int32
-		gl.GetProgramiv(a.bgProgram, gl.INFO_LOG_LENGTH, &logLength)
-		logMsg := make([]byte, logLength)
-		gl.GetProgramInfoLog(a.bgProgram, logLength, nil, &logMsg[0])
-		log.Fatalf("Failed to link background program: %s", logMsg)
-	}
-
-	gl.DeleteShader(bgVertShader)
-	gl.DeleteShader(bgFragShader)
-
-	gl.GenVertexArrays(1, &a.bgVAO)
-	gl.GenBuffers(1, &a.bgVBO)
-
-	gl.BindVertexArray(a.bgVAO)
-	gl.BindBuffer(gl.ARRAY_BUFFER, a.bgVBO)
-
-	quadVertices := []float32{
-		-1.0, -1.0,
-		1.0, -1.0,
-		-1.0, 1.0,
-		1.0, 1.0,
-	}
-	gl.BufferData(gl.ARRAY_BUFFER, len(quadVertices)*4, gl.Ptr(quadVertices), gl.STATIC_DRAW)
-
-	gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 2*4, nil)
-	gl.EnableVertexAttribArray(0)
-
-	gl.BindVertexArray(0)
-
-	cursorGlowVertShader, err := shaders.CompileShaderFromFile(
-		shaders.CursorGlowVertexPath,
-		gl.VERTEX_SHADER,
-	)
-	if err != nil {
-		return err
-	}
-	cursorGlowFragShader, err := shaders.CompileShaderFromFile(
-		shaders.CursorGlowFragmentPath,
-		gl.FRAGMENT_SHADER,
-	)
-	if err != nil {
-		return err
-	}
-
-	a.cursorGlowProgram = gl.CreateProgram()
-	gl.AttachShader(a.cursorGlowProgram, cursorGlowVertShader)
-	gl.AttachShader(a.cursorGlowProgram, cursorGlowFragShader)
-	gl.LinkProgram(a.cursorGlowProgram)
-
-	gl.GetProgramiv(a.cursorGlowProgram, gl.LINK_STATUS, &status)
-	if status == gl.FALSE {
-		var logLength int32
-		gl.GetProgramiv(a.cursorGlowProgram, gl.INFO_LOG_LENGTH, &logLength)
-		logMsg := make([]byte, logLength)
-		gl.GetProgramInfoLog(a.cursorGlowProgram, logLength, nil, &logMsg[0])
-		log.Fatalf("Failed to link cursor glow program: %s", logMsg)
-	}
-
-	gl.DeleteShader(cursorGlowVertShader)
-	gl.DeleteShader(cursorGlowFragShader)
-
-	gl.GenVertexArrays(1, &a.cursorGlowVAO)
-	gl.GenBuffers(1, &a.cursorGlowVBO)
-
-	gl.BindVertexArray(a.cursorGlowVAO)
-	gl.BindBuffer(gl.ARRAY_BUFFER, a.cursorGlowVBO)
-
-	glowQuadVertices := []float32{
-		-1.0, -1.0,
-		1.0, -1.0,
-		-1.0, 1.0,
-		1.0, 1.0,
-	}
-	gl.BufferData(
-		gl.ARRAY_BUFFER,
-		len(glowQuadVertices)*4,
-		gl.Ptr(glowQuadVertices),
-		gl.STATIC_DRAW,
-	)
-
-	gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 2*4, nil)
-	gl.EnableVertexAttribArray(0)
-
-	gl.BindVertexArray(0)
-
-	gl.Enable(gl.BLEND)
-	gl.BlendFunc(gl.SRC_ALPHA, gl.ONE)
-	gl.Enable(gl.PROGRAM_POINT_SIZE)
-
-	return nil
-}
-
-func (a *App) addPoint(x, y float32) {
-	newPoint := models.Point{X: x, Y: y, BornTime: time.Now()}
-
-	shouldAdd := false
-	if len(a.points) == 0 {
-		shouldAdd = true
-	} else {
-		lastPoint := a.points[len(a.points)-1]
-		dx := newPoint.X - lastPoint.X
-		dy := newPoint.Y - lastPoint.Y
-		if dx*dx+dy*dy > 4 {
-			shouldAdd = true
-
-			for range 3 {
-				angle := rand.Float64() * 2 * math.Pi
-				speed := rand.Float32()*50 + 20
-				a.particles = append(a.particles, models.Particle{
-					X:       x + (rand.Float32()-0.5)*10,
-					Y:       y + (rand.Float32()-0.5)*10,
-					VX:      float32(math.Cos(angle)) * speed,
-					VY:      float32(math.Sin(angle)) * speed,
-					Life:    1.0,
-					MaxLife: 1.0,
-					Size:    rand.Float32()*15 + 10,
-					Hue:     rand.Float32(),
-				})
-			}
-		}
-	}
-
-	const MAX_POINTS = 2048
-
-	if shouldAdd {
-		a.points = append(a.points, newPoint)
-		if len(a.points) > MAX_POINTS {
-			a.points = a.points[len(a.points)-MAX_POINTS:]
-		}
-	}
-}
-
-func (a *App) spawnCursorSparkles(x, y float32) {
-	for range 3 {
-		angle := rand.Float64() * 2 * math.Pi
-		speed := rand.Float32()*80 + 40
-		a.particles = append(a.particles, models.Particle{
-			X:       x + (rand.Float32()-0.5)*8,
-			Y:       y + (rand.Float32()-0.5)*8,
-			VX:      float32(math.Cos(angle)) * speed,
-			VY:      float32(math.Sin(angle))*speed - 30,
-			Life:    0.8,
-			MaxLife: 0.8,
-			Size:    rand.Float32()*8 + 6,
-			Hue:     rand.Float32(),
-		})
-	}
-}
-
-func (a *App) spawnExitWisps(x, y float32) {
-	for range 8 {
-		angle := rand.Float64() * 2 * math.Pi
-		speed := rand.Float32()*150 + 80
-		a.particles = append(a.particles, models.Particle{
-			X:       x + (rand.Float32()-0.5)*30,
-			Y:       y + (rand.Float32()-0.5)*30,
-			VX:      float32(math.Cos(angle)) * speed,
-			VY:      float32(math.Sin(angle)) * speed,
-			Life:    1.2,
-			MaxLife: 1.2,
-			Size:    rand.Float32()*12 + 8,
-			Hue:     rand.Float32(),
-		})
-	}
-}
-
-func (a *App) updateParticles(dt float32) {
-	for i := 0; i < len(a.particles); i++ {
-		p := &a.particles[i]
-		p.X += p.VX * dt
-		p.Y += p.VY * dt
-		p.VY += 100 * dt
-		p.Life -= dt
-
-		if p.Life <= 0 {
-			a.particles[i] = a.particles[len(a.particles)-1]
-			a.particles = a.particles[:len(a.particles)-1]
-			i--
-		}
-	}
-}
-
-func (a *App) updateCursor(window *wayland.WaylandWindow) {
-	x, y := window.GetCursorPos()
-	fx, fy := float32(x), float32(y)
-
-	dx := fx - a.lastCursorX
-	dy := fy - a.lastCursorY
-	a.cursorVelocity = float32(math.Sqrt(float64(dx*dx + dy*dy)))
-
-	velocityDiff := a.cursorVelocity - a.smoothVelocity
-	a.smoothVelocity += velocityDiff * 0.2
-
-	if a.cursorVelocity > 0.1 {
-		targetRotation := float32(math.Atan2(float64(dy), float64(dx)))
-
-		angleDiff := targetRotation - a.smoothRotation
-		if angleDiff > math.Pi {
-			angleDiff -= 2 * math.Pi
-		} else if angleDiff < -math.Pi {
-			angleDiff += 2 * math.Pi
-		}
-
-		velocityFactor := float32(math.Min(float64(a.smoothVelocity/5.0), 1.0))
-		smoothFactor := 0.03 + velocityFactor*0.08
-		a.smoothRotation += angleDiff * smoothFactor
-	}
-
-	var targetDrawing float32
-	if a.isDrawing {
-		targetDrawing = 1.0
-	} else {
-		targetDrawing = 0.0
-	}
-
-	diff := targetDrawing - a.smoothDrawing
-	a.smoothDrawing += diff * 0.0375
-
-	a.lastCursorX = fx
-	a.lastCursorY = fy
-}
-
-func (a *App) draw(window *wayland.WaylandWindow) {
-	gl.Clear(gl.COLOR_BUFFER_BIT)
-
-	currentTime := float32(time.Since(a.startTime).Seconds())
-
-	a.drawBackground(currentTime, window)
-
-	x, y := window.GetCursorPos()
-	a.drawCursorGlow(window, float32(x), float32(y), currentTime)
-
-	for pass := range 3 {
-		thickness := float32(7 + pass*4)
-		alpha := float32(0.7 - float32(pass)*0.15)
-		a.drawLine(window, thickness, alpha, currentTime)
-	}
-
-	a.drawParticles(window)
-}
-
-func (a *App) drawLine(
-	window *wayland.WaylandWindow,
-	baseThickness, baseAlpha, currentTime float32,
-) {
-	if len(a.points) < 2 {
-		return
-	}
-
-	vertices := make([]float32, 0, len(a.points)*10)
-
-	for i := range a.points {
-		age := float32(time.Since(a.points[i].BornTime).Seconds())
-		fade := 1.0 - (age / 1.5)
-		if fade < 0 {
-			fade = 0
-		}
-		alpha := fade * baseAlpha
-
-		var perpX, perpY float32
-
-		if i == 0 {
-			dx := a.points[i+1].X - a.points[i].X
-			dy := a.points[i+1].Y - a.points[i].Y
-			length := float32(1.0) / float32(math.Sqrt(float64(dx*dx+dy*dy)))
-			perpX = -dy * length
-			perpY = dx * length
-		} else if i == len(a.points)-1 {
-			dx := a.points[i].X - a.points[i-1].X
-			dy := a.points[i].Y - a.points[i-1].Y
-			length := float32(1.0) / float32(math.Sqrt(float64(dx*dx+dy*dy)))
-			perpX = -dy * length
-			perpY = dx * length
-		} else {
-			dx1 := a.points[i].X - a.points[i-1].X
-			dy1 := a.points[i].Y - a.points[i-1].Y
-			len1 := float32(math.Sqrt(float64(dx1*dx1 + dy1*dy1)))
-			if len1 > 0 {
-				dx1 /= len1
-				dy1 /= len1
-			}
-
-			dx2 := a.points[i+1].X - a.points[i].X
-			dy2 := a.points[i+1].Y - a.points[i].Y
-			len2 := float32(math.Sqrt(float64(dx2*dx2 + dy2*dy2)))
-			if len2 > 0 {
-				dx2 /= len2
-				dy2 /= len2
-			}
-
-			avgDx := (dx1 + dx2) * 0.5
-			avgDy := (dy1 + dy2) * 0.5
-			avgLen := float32(math.Sqrt(float64(avgDx*avgDx + avgDy*avgDy)))
-			if avgLen > 0 {
-				avgDx /= avgLen
-				avgDy /= avgLen
-			}
-
-			perpX = -avgDy
-			perpY = avgDx
-		}
-
-		vertices = append(vertices, a.points[i].X, a.points[i].Y, perpX, perpY, alpha)
-		vertices = append(vertices, a.points[i].X, a.points[i].Y, -perpX, -perpY, alpha)
-	}
-
-	cutoff := time.Now().Add(-1500 * time.Millisecond)
-	for len(a.points) > 0 && a.points[0].BornTime.Before(cutoff) {
-		a.points = a.points[1:]
-	}
-
-	if len(vertices) == 0 {
-		return
-	}
-
-	gl.BindBuffer(gl.ARRAY_BUFFER, a.vbo)
-	gl.BufferData(gl.ARRAY_BUFFER, len(vertices)*4, gl.Ptr(vertices), gl.DYNAMIC_DRAW)
-
-	width, height := window.GetSize()
-
-	gl.UseProgram(a.program)
-	resolutionLoc := gl.GetUniformLocation(a.program, gl.Str("resolution\x00"))
-	gl.Uniform2f(resolutionLoc, float32(width), float32(height))
-	thicknessLoc := gl.GetUniformLocation(a.program, gl.Str("thickness\x00"))
-	gl.Uniform1f(thicknessLoc, baseThickness)
-	timeLoc := gl.GetUniformLocation(a.program, gl.Str("time\x00"))
-	gl.Uniform1f(timeLoc, currentTime)
-
-	gl.BindVertexArray(a.vao)
-	gl.DrawArrays(gl.TRIANGLE_STRIP, 0, int32(len(a.points)*2))
-	gl.BindVertexArray(0)
-}
-
-func (a *App) drawParticles(window *wayland.WaylandWindow) {
-	if len(a.particles) == 0 {
-		return
-	}
-
-	vertices := make([]float32, 0, len(a.particles)*6)
-	for _, p := range a.particles {
-		vertices = append(vertices, p.X, p.Y, p.Life, p.MaxLife, p.Size, p.Hue)
-	}
-
-	gl.BindBuffer(gl.ARRAY_BUFFER, a.particleVBO)
-	gl.BufferData(gl.ARRAY_BUFFER, len(vertices)*4, gl.Ptr(vertices), gl.DYNAMIC_DRAW)
-
-	width, height := window.GetSize()
-
-	gl.UseProgram(a.particleProgram)
-	resolutionLoc := gl.GetUniformLocation(a.particleProgram, gl.Str("resolution\x00"))
-	gl.Uniform2f(resolutionLoc, float32(width), float32(height))
-
-	gl.BindVertexArray(a.particleVAO)
-	gl.DrawArrays(gl.POINTS, 0, int32(len(a.particles)))
-	gl.BindVertexArray(0)
-}
-
-func (a *App) drawBackground(currentTime float32, window *wayland.WaylandWindow) {
-	fadeDuration := float32(1.0)
-	targetAlpha := float32(0.75)
-
-	var alpha float32
-	if currentTime < fadeDuration {
-		progress := currentTime / fadeDuration
-		easedProgress := 1.0 - (1.0-progress)*(1.0-progress)*(1.0-progress)*(1.0-progress)*(1.0-progress)
-		alpha = easedProgress * targetAlpha
-	} else {
-		alpha = targetAlpha
-	}
-
-	if a.isExiting {
-		exitDuration := float32(0.8)
-		elapsed := float32(time.Since(a.exitStartTime).Seconds())
-		if elapsed < exitDuration {
-			progress := elapsed / exitDuration
-			easedProgress := 1.0 - (1.0-progress)*(1.0-progress)*(1.0-progress)*(1.0-progress)*(1.0-progress)
-			alpha *= (1.0 - easedProgress)
-		} else {
-			alpha = 0
-		}
-	}
-
-	x, y := window.GetCursorPos()
-	width, height := window.GetSize()
-
-	gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
-
-	gl.UseProgram(a.bgProgram)
-
-	alphaLoc := gl.GetUniformLocation(a.bgProgram, gl.Str("alpha\x00"))
-	gl.Uniform1f(alphaLoc, alpha)
-
-	cursorPosLoc := gl.GetUniformLocation(a.bgProgram, gl.Str("cursorPos\x00"))
-	gl.Uniform2f(cursorPosLoc, float32(x), float32(float64(height)-y))
-
-	resolutionLoc := gl.GetUniformLocation(a.bgProgram, gl.Str("resolution\x00"))
-	gl.Uniform2f(resolutionLoc, float32(width), float32(height))
-
-	gl.BindVertexArray(a.bgVAO)
-	gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
-	gl.BindVertexArray(0)
-
-	gl.BlendFunc(gl.SRC_ALPHA, gl.ONE)
-}
-
-func (a *App) drawCursorGlow(window *wayland.WaylandWindow, cursorX, cursorY, currentTime float32) {
-	width, height := window.GetSize()
-
-	growDuration := float32(1.2)
-	var scale float32
-	if currentTime < growDuration {
-		t := currentTime / growDuration
-		c4 := (2.0 * math.Pi) / 3.0
-		if t == 0 {
-			scale = 0
-		} else if t >= 1 {
-			scale = 1
-		} else {
-			scale = float32(math.Pow(2, -10*float64(t))*math.Sin((float64(t)*10-0.75)*c4) + 1)
-		}
-	} else {
-		scale = 1.0
-	}
-
-	var exitProgress float32
-	if a.isExiting {
-		exitDuration := float32(0.8)
-		elapsed := float32(time.Since(a.exitStartTime).Seconds())
-		if elapsed < exitDuration {
-			t := elapsed / exitDuration
-			exitProgress = t * t * t
-			scale *= (1.0 - exitProgress)
-		} else {
-			exitProgress = 1.0
-			scale = 0
-		}
-	}
-
-	gl.UseProgram(a.cursorGlowProgram)
-
-	cursorPosLoc := gl.GetUniformLocation(a.cursorGlowProgram, gl.Str("cursorPos\x00"))
-	gl.Uniform2f(cursorPosLoc, cursorX, cursorY)
-
-	resolutionLoc := gl.GetUniformLocation(a.cursorGlowProgram, gl.Str("resolution\x00"))
-	gl.Uniform2f(resolutionLoc, float32(width), float32(height))
-
-	glowSizeLoc := gl.GetUniformLocation(a.cursorGlowProgram, gl.Str("glowSize\x00"))
-	gl.Uniform1f(glowSizeLoc, 80.0*scale)
-
-	timeLoc := gl.GetUniformLocation(a.cursorGlowProgram, gl.Str("time\x00"))
-	gl.Uniform1f(timeLoc, currentTime)
-
-	velocityLoc := gl.GetUniformLocation(a.cursorGlowProgram, gl.Str("velocity\x00"))
-	gl.Uniform1f(velocityLoc, a.smoothVelocity)
-
-	rotationLoc := gl.GetUniformLocation(a.cursorGlowProgram, gl.Str("rotation\x00"))
-	gl.Uniform1f(rotationLoc, a.smoothRotation)
-
-	isDrawingLoc := gl.GetUniformLocation(a.cursorGlowProgram, gl.Str("isDrawing\x00"))
-	gl.Uniform1f(isDrawingLoc, a.smoothDrawing)
-
-	exitProgressLoc := gl.GetUniformLocation(a.cursorGlowProgram, gl.Str("exitProgress\x00"))
-	gl.Uniform1f(exitProgressLoc, exitProgress)
-
-	gl.BindVertexArray(a.cursorGlowVAO)
-	gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
-	gl.BindVertexArray(0)
-}
-
-func loadGestures() ([]models.GestureConfig, error) {
-	configFile, err := config.GetPath()
-	if err != nil {
-		return nil, err
-	}
-
-	data, err := os.ReadFile(configFile)
-	if err != nil {
-		if os.IsNotExist(err) {
-			return []models.GestureConfig{}, nil
-		}
-		return nil, err
-	}
-
-	var gestures []models.GestureConfig
-	if err := json.Unmarshal(data, &gestures); err != nil {
-		return nil, err
-	}
-
-	return gestures, nil
-}
-
-func saveGesture(command string, templates [][]models.Point) error {
-	configFile, err := config.GetPath()
-	if err != nil {
-		return err
-	}
-
-	var gestures []models.GestureConfig
-	if data, err := os.ReadFile(configFile); err == nil {
-		json.Unmarshal(data, &gestures)
-	}
-
-	newGesture := models.GestureConfig{
-		Command:   command,
-		Templates: templates,
-	}
-
-	found := false
-	for i, g := range gestures {
-		if g.Command == command {
-			gestures[i] = newGesture
-			found = true
-			break
-		}
-	}
-	if !found {
-		gestures = append(gestures, newGesture)
-	}
-
-	data, err := json.Marshal(gestures)
-	if err != nil {
-		return err
-	}
-
-	return os.WriteFile(configFile, data, 0644)
-}
-
-func (a *App) recognizeAndExecute(window *wayland.WaylandWindow, x, y float32) {
-	if len(a.points) < 5 {
-		log.Println("Gesture too short, ignoring")
-		return
-	}
-
-	processed := stroke.ProcessStroke(a.points)
-
-	bestMatch := -1
-	bestScore := 0.0
-
-	for i, gesture := range a.savedGestures {
-		match, score := stroke.UnistrokeRecognise(processed, gesture.Templates)
-		log.Printf("Gesture %d (%s): template %d, score %.3f", i, gesture.Command, match, score)
-
-		if score > bestScore {
-			bestScore = score
-			bestMatch = i
-		}
-	}
-
-	if bestMatch >= 0 && bestScore > 0.6 {
-		command := a.savedGestures[bestMatch].Command
-		log.Printf("Matched gesture: %s (score: %.3f)", command, bestScore)
-
-		if err := execute.Command(command); err != nil {
-			log.Printf("Failed to execute command: %v", err)
-		} else {
-			log.Printf("Executed: %s", command)
-		}
-
-		a.isExiting = true
-		a.exitStartTime = time.Now()
-		window.DisableInput()
-		a.spawnExitWisps(x, y)
-	} else {
-		log.Printf("No confident match (best score: %.3f)", bestScore)
-	}
+	*models.App
 }
 
 func main() {
@@ -753,7 +40,7 @@ func main() {
 	}
 
 	if *listGestures {
-		gestures, err := loadGestures()
+		gestures, err := gestures.LoadGestures()
 		if err != nil {
 			log.Fatal("Failed to load gestures:", err)
 		}
@@ -769,7 +56,7 @@ func main() {
 	}
 
 	if *removeGesture != "" {
-		gestures, err := loadGestures()
+		gestures, err := gestures.LoadGestures()
 		if err != nil {
 			log.Fatal("Failed to load gestures:", err)
 		}
@@ -811,36 +98,37 @@ func main() {
 	}
 	defer window.Destroy()
 
-	app := &App{startTime: time.Now()}
+	app := &models.App{StartTime: time.Now()}
 
 	if *learnCommand != "" {
-		app.learnMode = true
-		app.learnCommand = *learnCommand
+		app.LearnMode = true
+		app.LearnCommand = *learnCommand
 		log.Printf("Learn mode: Draw the gesture 3 times for command '%s'", *learnCommand)
 	} else {
-		gestures, err := loadGestures()
+		gestures, err := gestures.LoadGestures()
 		if err != nil {
 			log.Fatal("Failed to load gestures:", err)
 		}
-		app.savedGestures = gestures
+		app.SavedGestures = gestures
 		log.Printf("Loaded %d gesture(s)", len(gestures))
 	}
 
-	if err := app.initGL(); err != nil {
+	opengl := opengl.New(app)
+	if err := opengl.InitGL(); err != nil {
 		log.Fatal("Failed to initialize OpenGL:", err)
 	}
 
 	gl.ClearColor(0, 0, 0, 0)
 
-	for i := 0; i < 5; i++ {
+	for range 5 {
 		window.PollEvents()
 		gl.Clear(gl.COLOR_BUFFER_BIT)
 		window.SwapBuffers()
 	}
 
 	x, y := window.GetCursorPos()
-	app.lastCursorX = float32(x)
-	app.lastCursorY = float32(y)
+	app.LastCursorX = float32(x)
+	app.LastCursorY = float32(y)
 
 	lastTime := time.Now()
 	var wasPressed bool
@@ -851,68 +139,76 @@ func main() {
 		lastTime = now
 
 		window.PollEvents()
-		app.updateCursor(window)
+		update := update.New(app)
+		update.UpdateCursor(window)
 
 		if key, state, hasKey := window.GetLastKey(); hasKey {
 			if state == 1 && key == 0xff1b {
-				if !app.isExiting {
-					app.isExiting = true
-					app.exitStartTime = time.Now()
+				if !app.IsExiting {
+					app.IsExiting = true
+					app.ExitStartTime = time.Now()
 					window.DisableInput()
 					x, y := window.GetCursorPos()
-					app.spawnExitWisps(float32(x), float32(y))
+					spawn := spawn.New(app)
+					spawn.SpawnExitWisps(float32(x), float32(y))
 				}
 			}
 			window.ClearLastKey()
 		}
 
-		if app.isExiting {
-			if time.Since(app.exitStartTime).Seconds() > 0.8 {
+		if app.IsExiting {
+			if time.Since(app.ExitStartTime).Seconds() > 0.8 {
 				break
 			}
 		}
 		isPressed := window.GetMouseButton()
 		if isPressed && !wasPressed {
-			app.isDrawing = true
+			app.IsDrawing = true
 		} else if !isPressed && wasPressed {
-			app.isDrawing = false
+			app.IsDrawing = false
 
-			if app.learnMode && len(app.points) > 0 {
-				processed := stroke.ProcessStroke(app.points)
-				app.learnGestures = append(app.learnGestures, processed)
-				app.learnCount++
-				log.Printf("Captured gesture %d/3", app.learnCount)
+			if app.LearnMode && len(app.Points) > 0 {
+				processed := stroke.ProcessStroke(app.Points)
+				app.LearnGestures = append(app.LearnGestures, processed)
+				app.LearnCount++
+				log.Printf("Captured gesture %d/3", app.LearnCount)
 
-				app.points = nil
+				app.Points = nil
 
-				if app.learnCount >= 3 {
-					if err := saveGesture(app.learnCommand, app.learnGestures); err != nil {
+				if app.LearnCount >= 3 {
+					if err := gestures.SaveGesture(app.LearnCommand, app.LearnGestures); err != nil {
 						log.Fatal("Failed to save gesture:", err)
 					}
-					log.Printf("Gesture saved for command: %s", app.learnCommand)
+					log.Printf("Gesture saved for command: %s", app.LearnCommand)
 
-					app.isExiting = true
-					app.exitStartTime = time.Now()
+					app.IsExiting = true
+					app.ExitStartTime = time.Now()
 					window.DisableInput()
 					x, y := window.GetCursorPos()
-					app.spawnExitWisps(float32(x), float32(y))
+					spawn := spawn.New(app)
+					spawn.SpawnExitWisps(float32(x), float32(y))
 				}
-			} else if !app.learnMode && len(app.points) > 0 {
+			} else if !app.LearnMode && len(app.Points) > 0 {
 				x, y := window.GetCursorPos()
-				app.recognizeAndExecute(window, float32(x), float32(y))
-				app.points = nil
+				fornow := fornow.New(app)
+				fornow.RecognizeAndExecute(window, float32(x), float32(y))
+				app.Points = nil
 			}
 		}
 		wasPressed = isPressed
 
-		if app.isDrawing {
+		if app.IsDrawing {
 			x, y := window.GetCursorPos()
-			app.addPoint(float32(x), float32(y))
-			app.spawnCursorSparkles(float32(x), float32(y))
+			fornow := fornow.New(app)
+			fornow.AddPoint(float32(x), float32(y))
+
+			spawn := spawn.New(app)
+			spawn.SpawnCursorSparkles(float32(x), float32(y))
 		}
 
-		app.updateParticles(dt)
-		app.draw(window)
+		update.UpdateParticles(dt)
+		drawer := draw.New(app)
+		drawer.Draw(window)
 		window.SwapBuffers()
 	}
 }
diff --git a/internal/draw/draw.go b/internal/draw/draw.go
new file mode 100644
index 0000000..f7a2b83
--- /dev/null
+++ b/internal/draw/draw.go
@@ -0,0 +1,265 @@
+package draw
+
+import (
+	"math"
+	"time"
+
+	"github.com/ThatOtherAndrew/Hexecute/internal/models"
+	"github.com/ThatOtherAndrew/Hexecute/pkg/wayland"
+	"github.com/go-gl/gl/v4.1-core/gl"
+)
+
+type App struct {
+	app *models.App
+}
+
+func New(app *models.App) *App {
+	return &App{app: app}
+}
+
+func (a *App) Draw(window *wayland.WaylandWindow) {
+	gl.Clear(gl.COLOR_BUFFER_BIT)
+
+	currentTime := float32(time.Since(a.app.StartTime).Seconds())
+
+	a.drawBackground(currentTime, window)
+
+	x, y := window.GetCursorPos()
+	a.drawCursorGlow(window, float32(x), float32(y), currentTime)
+
+	for pass := range 3 {
+		thickness := float32(7 + pass*4)
+		alpha := float32(0.7 - float32(pass)*0.15)
+		a.drawLine(window, thickness, alpha, currentTime)
+	}
+
+	a.drawParticles(window)
+}
+
+func (a *App) drawLine(
+	window *wayland.WaylandWindow,
+	baseThickness, baseAlpha, currentTime float32,
+) {
+	if len(a.app.Points) < 2 {
+		return
+	}
+
+	vertices := make([]float32, 0, len(a.app.Points)*10)
+
+	for i := range a.app.Points {
+		age := float32(time.Since(a.app.Points[i].BornTime).Seconds())
+		fade := 1.0 - (age / 1.5)
+		if fade < 0 {
+			fade = 0
+		}
+		alpha := fade * baseAlpha
+
+		var perpX, perpY float32
+
+		if i == 0 {
+			dx := a.app.Points[i+1].X - a.app.Points[i].X
+			dy := a.app.Points[i+1].Y - a.app.Points[i].Y
+			length := float32(1.0) / float32(math.Sqrt(float64(dx*dx+dy*dy)))
+			perpX = -dy * length
+			perpY = dx * length
+		} else if i == len(a.app.Points)-1 {
+			dx := a.app.Points[i].X - a.app.Points[i-1].X
+			dy := a.app.Points[i].Y - a.app.Points[i-1].Y
+			length := float32(1.0) / float32(math.Sqrt(float64(dx*dx+dy*dy)))
+			perpX = -dy * length
+			perpY = dx * length
+		} else {
+			dx1 := a.app.Points[i].X - a.app.Points[i-1].X
+			dy1 := a.app.Points[i].Y - a.app.Points[i-1].Y
+			len1 := float32(math.Sqrt(float64(dx1*dx1 + dy1*dy1)))
+			if len1 > 0 {
+				dx1 /= len1
+				dy1 /= len1
+			}
+
+			dx2 := a.app.Points[i+1].X - a.app.Points[i].X
+			dy2 := a.app.Points[i+1].Y - a.app.Points[i].Y
+			len2 := float32(math.Sqrt(float64(dx2*dx2 + dy2*dy2)))
+			if len2 > 0 {
+				dx2 /= len2
+				dy2 /= len2
+			}
+
+			avgDx := (dx1 + dx2) * 0.5
+			avgDy := (dy1 + dy2) * 0.5
+			avgLen := float32(math.Sqrt(float64(avgDx*avgDx + avgDy*avgDy)))
+			if avgLen > 0 {
+				avgDx /= avgLen
+				avgDy /= avgLen
+			}
+
+			perpX = -avgDy
+			perpY = avgDx
+		}
+
+		vertices = append(vertices, a.app.Points[i].X, a.app.Points[i].Y, perpX, perpY, alpha)
+		vertices = append(vertices, a.app.Points[i].X, a.app.Points[i].Y, -perpX, -perpY, alpha)
+	}
+
+	cutoff := time.Now().Add(-1500 * time.Millisecond)
+	for len(a.app.Points) > 0 && a.app.Points[0].BornTime.Before(cutoff) {
+		a.app.Points = a.app.Points[1:]
+	}
+
+	if len(vertices) == 0 {
+		return
+	}
+
+	gl.BindBuffer(gl.ARRAY_BUFFER, a.app.Vbo)
+	gl.BufferData(gl.ARRAY_BUFFER, len(vertices)*4, gl.Ptr(vertices), gl.DYNAMIC_DRAW)
+
+	width, height := window.GetSize()
+
+	gl.UseProgram(a.app.Program)
+	resolutionLoc := gl.GetUniformLocation(a.app.Program, gl.Str("resolution\x00"))
+	gl.Uniform2f(resolutionLoc, float32(width), float32(height))
+	thicknessLoc := gl.GetUniformLocation(a.app.Program, gl.Str("thickness\x00"))
+	gl.Uniform1f(thicknessLoc, baseThickness)
+	timeLoc := gl.GetUniformLocation(a.app.Program, gl.Str("time\x00"))
+	gl.Uniform1f(timeLoc, currentTime)
+
+	gl.BindVertexArray(a.app.Vao)
+	gl.DrawArrays(gl.TRIANGLE_STRIP, 0, int32(len(a.app.Points)*2))
+	gl.BindVertexArray(0)
+}
+
+func (a *App) drawParticles(window *wayland.WaylandWindow) {
+	if len(a.app.Particles) == 0 {
+		return
+	}
+
+	vertices := make([]float32, 0, len(a.app.Particles)*6)
+	for _, p := range a.app.Particles {
+		vertices = append(vertices, p.X, p.Y, p.Life, p.MaxLife, p.Size, p.Hue)
+	}
+
+	gl.BindBuffer(gl.ARRAY_BUFFER, a.app.ParticleVBO)
+	gl.BufferData(gl.ARRAY_BUFFER, len(vertices)*4, gl.Ptr(vertices), gl.DYNAMIC_DRAW)
+
+	width, height := window.GetSize()
+
+	gl.UseProgram(a.app.ParticleProgram)
+	resolutionLoc := gl.GetUniformLocation(a.app.ParticleProgram, gl.Str("resolution\x00"))
+	gl.Uniform2f(resolutionLoc, float32(width), float32(height))
+
+	gl.BindVertexArray(a.app.ParticleVAO)
+	gl.DrawArrays(gl.POINTS, 0, int32(len(a.app.Particles)))
+	gl.BindVertexArray(0)
+}
+
+func (a *App) drawBackground(currentTime float32, window *wayland.WaylandWindow) {
+	fadeDuration := float32(1.0)
+	targetAlpha := float32(0.75)
+
+	var alpha float32
+	if currentTime < fadeDuration {
+		progress := currentTime / fadeDuration
+		easedProgress := 1.0 - (1.0-progress)*(1.0-progress)*(1.0-progress)*(1.0-progress)*(1.0-progress)
+		alpha = easedProgress * targetAlpha
+	} else {
+		alpha = targetAlpha
+	}
+
+	if a.app.IsExiting {
+		exitDuration := float32(0.8)
+		elapsed := float32(time.Since(a.app.ExitStartTime).Seconds())
+		if elapsed < exitDuration {
+			progress := elapsed / exitDuration
+			easedProgress := 1.0 - (1.0-progress)*(1.0-progress)*(1.0-progress)*(1.0-progress)*(1.0-progress)
+			alpha *= (1.0 - easedProgress)
+		} else {
+			alpha = 0
+		}
+	}
+
+	x, y := window.GetCursorPos()
+	width, height := window.GetSize()
+
+	gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
+
+	gl.UseProgram(a.app.BgProgram)
+
+	alphaLoc := gl.GetUniformLocation(a.app.BgProgram, gl.Str("alpha\x00"))
+	gl.Uniform1f(alphaLoc, alpha)
+
+	cursorPosLoc := gl.GetUniformLocation(a.app.BgProgram, gl.Str("cursorPos\x00"))
+	gl.Uniform2f(cursorPosLoc, float32(x), float32(float64(height)-y))
+
+	resolutionLoc := gl.GetUniformLocation(a.app.BgProgram, gl.Str("resolution\x00"))
+	gl.Uniform2f(resolutionLoc, float32(width), float32(height))
+
+	gl.BindVertexArray(a.app.BgVAO)
+	gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
+	gl.BindVertexArray(0)
+
+	gl.BlendFunc(gl.SRC_ALPHA, gl.ONE)
+}
+
+func (a *App) drawCursorGlow(window *wayland.WaylandWindow, cursorX, cursorY, currentTime float32) {
+	width, height := window.GetSize()
+
+	growDuration := float32(1.2)
+	var scale float32
+	if currentTime < growDuration {
+		t := currentTime / growDuration
+		c4 := (2.0 * math.Pi) / 3.0
+		if t == 0 {
+			scale = 0
+		} else if t >= 1 {
+			scale = 1
+		} else {
+			scale = float32(math.Pow(2, -10*float64(t))*math.Sin((float64(t)*10-0.75)*c4) + 1)
+		}
+	} else {
+		scale = 1.0
+	}
+
+	var exitProgress float32
+	if a.app.IsExiting {
+		exitDuration := float32(0.8)
+		elapsed := float32(time.Since(a.app.ExitStartTime).Seconds())
+		if elapsed < exitDuration {
+			t := elapsed / exitDuration
+			exitProgress = t * t * t
+			scale *= (1.0 - exitProgress)
+		} else {
+			exitProgress = 1.0
+			scale = 0
+		}
+	}
+
+	gl.UseProgram(a.app.CursorGlowProgram)
+
+	cursorPosLoc := gl.GetUniformLocation(a.app.CursorGlowProgram, gl.Str("cursorPos\x00"))
+	gl.Uniform2f(cursorPosLoc, cursorX, cursorY)
+
+	resolutionLoc := gl.GetUniformLocation(a.app.CursorGlowProgram, gl.Str("resolution\x00"))
+	gl.Uniform2f(resolutionLoc, float32(width), float32(height))
+
+	glowSizeLoc := gl.GetUniformLocation(a.app.CursorGlowProgram, gl.Str("glowSize\x00"))
+	gl.Uniform1f(glowSizeLoc, 80.0*scale)
+
+	timeLoc := gl.GetUniformLocation(a.app.CursorGlowProgram, gl.Str("time\x00"))
+	gl.Uniform1f(timeLoc, currentTime)
+
+	velocityLoc := gl.GetUniformLocation(a.app.CursorGlowProgram, gl.Str("velocity\x00"))
+	gl.Uniform1f(velocityLoc, a.app.SmoothVelocity)
+
+	rotationLoc := gl.GetUniformLocation(a.app.CursorGlowProgram, gl.Str("rotation\x00"))
+	gl.Uniform1f(rotationLoc, a.app.SmoothRotation)
+
+	isDrawingLoc := gl.GetUniformLocation(a.app.CursorGlowProgram, gl.Str("isDrawing\x00"))
+	gl.Uniform1f(isDrawingLoc, a.app.SmoothDrawing)
+
+	exitProgressLoc := gl.GetUniformLocation(a.app.CursorGlowProgram, gl.Str("exitProgress\x00"))
+	gl.Uniform1f(exitProgressLoc, exitProgress)
+
+	gl.BindVertexArray(a.app.CursorGlowVAO)
+	gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
+	gl.BindVertexArray(0)
+}
diff --git a/internal/fornow/fornow.go b/internal/fornow/fornow.go
new file mode 100644
index 0000000..aef340c
--- /dev/null
+++ b/internal/fornow/fornow.go
@@ -0,0 +1,103 @@
+package fornow
+
+import (
+	"log"
+	"math"
+	"math/rand/v2"
+	"time"
+
+	"github.com/ThatOtherAndrew/Hexecute/internal/execute"
+	"github.com/ThatOtherAndrew/Hexecute/internal/models"
+	"github.com/ThatOtherAndrew/Hexecute/internal/spawn"
+	"github.com/ThatOtherAndrew/Hexecute/internal/stroke"
+	"github.com/ThatOtherAndrew/Hexecute/pkg/wayland"
+)
+
+type App struct {
+	app *models.App
+}
+
+func New(app *models.App) *App {
+	return &App{app: app}
+}
+
+func (a *App) AddPoint(x, y float32) {
+	newPoint := models.Point{X: x, Y: y, BornTime: time.Now()}
+
+	shouldAdd := false
+	if len(a.app.Points) == 0 {
+		shouldAdd = true
+	} else {
+		lastPoint := a.app.Points[len(a.app.Points)-1]
+		dx := newPoint.X - lastPoint.X
+		dy := newPoint.Y - lastPoint.Y
+		if dx*dx+dy*dy > 4 {
+			shouldAdd = true
+
+			for range 3 {
+				angle := rand.Float64() * 2 * math.Pi
+				speed := rand.Float32()*50 + 20
+				a.app.Particles = append(a.app.Particles, models.Particle{
+					X:       x + (rand.Float32()-0.5)*10,
+					Y:       y + (rand.Float32()-0.5)*10,
+					VX:      float32(math.Cos(angle)) * speed,
+					VY:      float32(math.Sin(angle)) * speed,
+					Life:    1.0,
+					MaxLife: 1.0,
+					Size:    rand.Float32()*15 + 10,
+					Hue:     rand.Float32(),
+				})
+			}
+		}
+	}
+
+	const MAX_POINTS = 2048
+
+	if shouldAdd {
+		a.app.Points = append(a.app.Points, newPoint)
+		if len(a.app.Points) > MAX_POINTS {
+			a.app.Points = a.app.Points[len(a.app.Points)-MAX_POINTS:]
+		}
+	}
+}
+
+func (a *App) RecognizeAndExecute(window *wayland.WaylandWindow, x, y float32) {
+	if len(a.app.Points) < 5 {
+		log.Println("Gesture too short, ignoring")
+		return
+	}
+
+	processed := stroke.ProcessStroke(a.app.Points)
+
+	bestMatch := -1
+	bestScore := 0.0
+
+	for i, gesture := range a.app.SavedGestures {
+		match, score := stroke.UnistrokeRecognise(processed, gesture.Templates)
+		log.Printf("Gesture %d (%s): template %d, score %.3f", i, gesture.Command, match, score)
+
+		if score > bestScore {
+			bestScore = score
+			bestMatch = i
+		}
+	}
+
+	if bestMatch >= 0 && bestScore > 0.6 {
+		command := a.app.SavedGestures[bestMatch].Command
+		log.Printf("Matched gesture: %s (score: %.3f)", command, bestScore)
+
+		if err := execute.Command(command); err != nil {
+			log.Printf("Failed to execute command: %v", err)
+		} else {
+			log.Printf("Executed: %s", command)
+		}
+
+		a.app.IsExiting = true
+		a.app.ExitStartTime = time.Now()
+		window.DisableInput()
+		spawn := spawn.New(a.app)
+		spawn.SpawnExitWisps(x, y)
+	} else {
+		log.Printf("No confident match (best score: %.3f)", bestScore)
+	}
+}
diff --git a/internal/models/models.go b/internal/models/models.go
index 545020a..281ad51 100644
--- a/internal/models/models.go
+++ b/internal/models/models.go
@@ -1,6 +1,8 @@
 package models
 
-import "time"
+import (
+	"time"
+)
 
 type Point struct {
 	X, Y     float32
@@ -20,3 +22,35 @@ type GestureConfig struct {
 	Command   string    `json:"command"`
 	Templates [][]Point `json:"templates"`
 }
+
+type App struct {
+	Points            []Point
+	Particles         []Particle
+	IsDrawing         bool
+	Vao               uint32
+	Vbo               uint32
+	Program           uint32
+	ParticleVAO       uint32
+	ParticleVBO       uint32
+	ParticleProgram   uint32
+	BgVAO             uint32
+	BgVBO             uint32
+	BgProgram         uint32
+	CursorGlowVAO     uint32
+	CursorGlowVBO     uint32
+	CursorGlowProgram uint32
+	StartTime         time.Time
+	LastCursorX       float32
+	LastCursorY       float32
+	CursorVelocity    float32
+	SmoothVelocity    float32
+	SmoothRotation    float32
+	SmoothDrawing     float32
+	IsExiting         bool
+	ExitStartTime     time.Time
+	LearnMode         bool
+	LearnCommand      string
+	LearnGestures     [][]Point
+	LearnCount        int
+	SavedGestures     []GestureConfig
+}
diff --git a/internal/opengl/opengl.go b/internal/opengl/opengl.go
new file mode 100644
index 0000000..4f7d01e
--- /dev/null
+++ b/internal/opengl/opengl.go
@@ -0,0 +1,229 @@
+package opengl
+
+import (
+	"log"
+
+	"github.com/ThatOtherAndrew/Hexecute/internal/models"
+	"github.com/ThatOtherAndrew/Hexecute/internal/shaders"
+	"github.com/go-gl/gl/v4.1-core/gl"
+)
+
+type App struct {
+	app *models.App
+}
+
+func New(app *models.App) *App {
+	return &App{app: app}
+}
+
+func (a *App) InitGL() error {
+	if err := gl.Init(); err != nil {
+		return err
+	}
+
+	vertShader, err := shaders.CompileShaderFromFile(shaders.LineVertexPath, gl.VERTEX_SHADER)
+	if err != nil {
+		return err
+	}
+	fragShader, err := shaders.CompileShaderFromFile(shaders.LineFragmentPath, gl.FRAGMENT_SHADER)
+	if err != nil {
+		return err
+	}
+
+	a.app.Program = gl.CreateProgram()
+	gl.AttachShader(a.app.Program, vertShader)
+	gl.AttachShader(a.app.Program, fragShader)
+	gl.LinkProgram(a.app.Program)
+
+	var status int32
+	gl.GetProgramiv(a.app.Program, gl.LINK_STATUS, &status)
+	if status == gl.FALSE {
+		var logLength int32
+		gl.GetProgramiv(a.app.Program, gl.INFO_LOG_LENGTH, &logLength)
+		logMsg := make([]byte, logLength)
+		gl.GetProgramInfoLog(a.app.Program, logLength, nil, &logMsg[0])
+		log.Fatalf("Failed to link program: %s", logMsg)
+	}
+
+	gl.DeleteShader(vertShader)
+	gl.DeleteShader(fragShader)
+
+	particleVertShader, err := shaders.CompileShaderFromFile(
+		shaders.ParticleVertexPath,
+		gl.VERTEX_SHADER,
+	)
+	if err != nil {
+		return err
+	}
+	particleFragShader, err := shaders.CompileShaderFromFile(
+		shaders.ParticleFragmentPath,
+		gl.FRAGMENT_SHADER,
+	)
+	if err != nil {
+		return err
+	}
+
+	a.app.ParticleProgram = gl.CreateProgram()
+	gl.AttachShader(a.app.ParticleProgram, particleVertShader)
+	gl.AttachShader(a.app.ParticleProgram, particleFragShader)
+	gl.LinkProgram(a.app.ParticleProgram)
+
+	gl.GetProgramiv(a.app.ParticleProgram, gl.LINK_STATUS, &status)
+	if status == gl.FALSE {
+		var logLength int32
+		gl.GetProgramiv(a.app.ParticleProgram, gl.INFO_LOG_LENGTH, &logLength)
+		logMsg := make([]byte, logLength)
+		gl.GetProgramInfoLog(a.app.ParticleProgram, logLength, nil, &logMsg[0])
+		log.Fatalf("Failed to link particle program: %s", logMsg)
+	}
+
+	gl.DeleteShader(particleVertShader)
+	gl.DeleteShader(particleFragShader)
+
+	gl.GenVertexArrays(1, &a.app.Vao)
+	gl.GenBuffers(1, &a.app.Vbo)
+
+	gl.BindVertexArray(a.app.Vao)
+	gl.BindBuffer(gl.ARRAY_BUFFER, a.app.Vbo)
+
+	gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 5*4, nil)
+	gl.EnableVertexAttribArray(0)
+	gl.VertexAttribPointer(1, 2, gl.FLOAT, false, 5*4, gl.PtrOffset(2*4))
+	gl.EnableVertexAttribArray(1)
+	gl.VertexAttribPointer(2, 1, gl.FLOAT, false, 5*4, gl.PtrOffset(4*4))
+	gl.EnableVertexAttribArray(2)
+
+	gl.BindVertexArray(0)
+
+	gl.GenVertexArrays(1, &a.app.ParticleVAO)
+	gl.GenBuffers(1, &a.app.ParticleVBO)
+
+	gl.BindVertexArray(a.app.ParticleVAO)
+	gl.BindBuffer(gl.ARRAY_BUFFER, a.app.ParticleVBO)
+
+	gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 6*4, nil)
+	gl.EnableVertexAttribArray(0)
+	gl.VertexAttribPointer(1, 1, gl.FLOAT, false, 6*4, gl.PtrOffset(2*4))
+	gl.EnableVertexAttribArray(1)
+	gl.VertexAttribPointer(2, 1, gl.FLOAT, false, 6*4, gl.PtrOffset(3*4))
+	gl.EnableVertexAttribArray(2)
+	gl.VertexAttribPointer(3, 1, gl.FLOAT, false, 6*4, gl.PtrOffset(4*4))
+	gl.EnableVertexAttribArray(3)
+	gl.VertexAttribPointer(4, 1, gl.FLOAT, false, 6*4, gl.PtrOffset(5*4))
+	gl.EnableVertexAttribArray(4)
+
+	gl.BindVertexArray(0)
+
+	bgVertShader, err := shaders.CompileShaderFromFile(
+		shaders.BackgroundVertexPath,
+		gl.VERTEX_SHADER,
+	)
+	if err != nil {
+		return err
+	}
+	bgFragShader, err := shaders.CompileShaderFromFile(
+		shaders.BackgroundFragmentPath,
+		gl.FRAGMENT_SHADER,
+	)
+	if err != nil {
+		return err
+	}
+
+	a.app.BgProgram = gl.CreateProgram()
+	gl.AttachShader(a.app.BgProgram, bgVertShader)
+	gl.AttachShader(a.app.BgProgram, bgFragShader)
+	gl.LinkProgram(a.app.BgProgram)
+
+	gl.GetProgramiv(a.app.BgProgram, gl.LINK_STATUS, &status)
+	if status == gl.FALSE {
+		var logLength int32
+		gl.GetProgramiv(a.app.BgProgram, gl.INFO_LOG_LENGTH, &logLength)
+		logMsg := make([]byte, logLength)
+		gl.GetProgramInfoLog(a.app.BgProgram, logLength, nil, &logMsg[0])
+		log.Fatalf("Failed to link background program: %s", logMsg)
+	}
+
+	gl.DeleteShader(bgVertShader)
+	gl.DeleteShader(bgFragShader)
+
+	gl.GenVertexArrays(1, &a.app.BgVAO)
+	gl.GenBuffers(1, &a.app.BgVBO)
+
+	gl.BindVertexArray(a.app.BgVAO)
+	gl.BindBuffer(gl.ARRAY_BUFFER, a.app.BgVBO)
+
+	quadVertices := []float32{
+		-1.0, -1.0,
+		1.0, -1.0,
+		-1.0, 1.0,
+		1.0, 1.0,
+	}
+	gl.BufferData(gl.ARRAY_BUFFER, len(quadVertices)*4, gl.Ptr(quadVertices), gl.STATIC_DRAW)
+
+	gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 2*4, nil)
+	gl.EnableVertexAttribArray(0)
+
+	gl.BindVertexArray(0)
+
+	cursorGlowVertShader, err := shaders.CompileShaderFromFile(
+		shaders.CursorGlowVertexPath,
+		gl.VERTEX_SHADER,
+	)
+	if err != nil {
+		return err
+	}
+	cursorGlowFragShader, err := shaders.CompileShaderFromFile(
+		shaders.CursorGlowFragmentPath,
+		gl.FRAGMENT_SHADER,
+	)
+	if err != nil {
+		return err
+	}
+
+	a.app.CursorGlowProgram = gl.CreateProgram()
+	gl.AttachShader(a.app.CursorGlowProgram, cursorGlowVertShader)
+	gl.AttachShader(a.app.CursorGlowProgram, cursorGlowFragShader)
+	gl.LinkProgram(a.app.CursorGlowProgram)
+
+	gl.GetProgramiv(a.app.CursorGlowProgram, gl.LINK_STATUS, &status)
+	if status == gl.FALSE {
+		var logLength int32
+		gl.GetProgramiv(a.app.CursorGlowProgram, gl.INFO_LOG_LENGTH, &logLength)
+		logMsg := make([]byte, logLength)
+		gl.GetProgramInfoLog(a.app.CursorGlowProgram, logLength, nil, &logMsg[0])
+		log.Fatalf("Failed to link cursor glow program: %s", logMsg)
+	}
+
+	gl.DeleteShader(cursorGlowVertShader)
+	gl.DeleteShader(cursorGlowFragShader)
+
+	gl.GenVertexArrays(1, &a.app.CursorGlowVAO)
+	gl.GenBuffers(1, &a.app.CursorGlowVBO)
+
+	gl.BindVertexArray(a.app.CursorGlowVAO)
+	gl.BindBuffer(gl.ARRAY_BUFFER, a.app.CursorGlowVBO)
+
+	glowQuadVertices := []float32{
+		-1.0, -1.0,
+		1.0, -1.0,
+		-1.0, 1.0,
+		1.0, 1.0,
+	}
+	gl.BufferData(
+		gl.ARRAY_BUFFER,
+		len(glowQuadVertices)*4,
+		gl.Ptr(glowQuadVertices),
+		gl.STATIC_DRAW,
+	)
+
+	gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 2*4, nil)
+	gl.EnableVertexAttribArray(0)
+
+	gl.BindVertexArray(0)
+
+	gl.Enable(gl.BLEND)
+	gl.BlendFunc(gl.SRC_ALPHA, gl.ONE)
+	gl.Enable(gl.PROGRAM_POINT_SIZE)
+
+	return nil
+}
diff --git a/internal/spawn/spawn.go b/internal/spawn/spawn.go
new file mode 100644
index 0000000..5806d6b
--- /dev/null
+++ b/internal/spawn/spawn.go
@@ -0,0 +1,50 @@
+package spawn
+
+import (
+	"math"
+	"math/rand/v2"
+
+	"github.com/ThatOtherAndrew/Hexecute/internal/models"
+)
+
+type App struct {
+	app *models.App
+}
+
+func New(app *models.App) *App {
+	return &App{app: app}
+}
+
+func (a *App) SpawnCursorSparkles(x, y float32) {
+	for range 3 {
+		angle := rand.Float64() * 2 * math.Pi
+		speed := rand.Float32()*80 + 40
+		a.app.Particles = append(a.app.Particles, models.Particle{
+			X:       x + (rand.Float32()-0.5)*8,
+			Y:       y + (rand.Float32()-0.5)*8,
+			VX:      float32(math.Cos(angle)) * speed,
+			VY:      float32(math.Sin(angle))*speed - 30,
+			Life:    0.8,
+			MaxLife: 0.8,
+			Size:    rand.Float32()*8 + 6,
+			Hue:     rand.Float32(),
+		})
+	}
+}
+
+func (a *App) SpawnExitWisps(x, y float32) {
+	for range 8 {
+		angle := rand.Float64() * 2 * math.Pi
+		speed := rand.Float32()*150 + 80
+		a.app.Particles = append(a.app.Particles, models.Particle{
+			X:       x + (rand.Float32()-0.5)*30,
+			Y:       y + (rand.Float32()-0.5)*30,
+			VX:      float32(math.Cos(angle)) * speed,
+			VY:      float32(math.Sin(angle)) * speed,
+			Life:    1.2,
+			MaxLife: 1.2,
+			Size:    rand.Float32()*12 + 8,
+			Hue:     rand.Float32(),
+		})
+	}
+}
diff --git a/internal/update/update.go b/internal/update/update.go
new file mode 100644
index 0000000..4b0a140
--- /dev/null
+++ b/internal/update/update.go
@@ -0,0 +1,72 @@
+package update
+
+import (
+	"math"
+
+	"github.com/ThatOtherAndrew/Hexecute/internal/models"
+	"github.com/ThatOtherAndrew/Hexecute/pkg/wayland"
+)
+
+type App struct {
+	app *models.App
+}
+
+func New(app *models.App) *App {
+	return &App{app: app}
+}
+
+func (a *App) UpdateParticles(dt float32) {
+	for i := 0; i < len(a.app.Particles); i++ {
+		p := &a.app.Particles[i]
+		p.X += p.VX * dt
+		p.Y += p.VY * dt
+		p.VY += 100 * dt
+		p.Life -= dt
+
+		if p.Life <= 0 {
+			a.app.Particles[i] = a.app.Particles[len(a.app.Particles)-1]
+			a.app.Particles = a.app.Particles[:len(a.app.Particles)-1]
+			i--
+		}
+	}
+}
+
+func (a *App) UpdateCursor(window *wayland.WaylandWindow) {
+	x, y := window.GetCursorPos()
+	fx, fy := float32(x), float32(y)
+
+	dx := fx - a.app.LastCursorX
+	dy := fy - a.app.LastCursorY
+	a.app.CursorVelocity = float32(math.Sqrt(float64(dx*dx + dy*dy)))
+
+	velocityDiff := a.app.CursorVelocity - a.app.SmoothVelocity
+	a.app.SmoothVelocity += velocityDiff * 0.2
+
+	if a.app.CursorVelocity > 0.1 {
+		targetRotation := float32(math.Atan2(float64(dy), float64(dx)))
+
+		angleDiff := targetRotation - a.app.SmoothRotation
+		if angleDiff > math.Pi {
+			angleDiff -= 2 * math.Pi
+		} else if angleDiff < -math.Pi {
+			angleDiff += 2 * math.Pi
+		}
+
+		velocityFactor := float32(math.Min(float64(a.app.SmoothVelocity/5.0), 1.0))
+		smoothFactor := 0.03 + velocityFactor*0.08
+		a.app.SmoothRotation += angleDiff * smoothFactor
+	}
+
+	var targetDrawing float32
+	if a.app.IsDrawing {
+		targetDrawing = 1.0
+	} else {
+		targetDrawing = 0.0
+	}
+
+	diff := targetDrawing - a.app.SmoothDrawing
+	a.app.SmoothDrawing += diff * 0.0375
+
+	a.app.LastCursorX = fx
+	a.app.LastCursorY = fy
+}

From d58a38c7cc838ab88498d152dfb71354b8ab3995 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Thu, 16 Oct 2025 01:12:05 +0300
Subject: [PATCH 15/24] chore: handled unhandled error

---
 internal/gesture/gesture.go | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/internal/gesture/gesture.go b/internal/gesture/gesture.go
index deb647e..ed54c8d 100644
--- a/internal/gesture/gesture.go
+++ b/internal/gesture/gesture.go
@@ -38,7 +38,9 @@ func SaveGesture(command string, templates [][]models.Point) error {
 
 	var gestures []models.GestureConfig
 	if data, err := os.ReadFile(configFile); err == nil {
-		json.Unmarshal(data, &gestures)
+		if err := json.Unmarshal(data, &gestures); err != nil {
+			return err
+		}
 	}
 
 	newGesture := models.GestureConfig{

From 291b4966492098c4752865d83fcb32ec944f4a13 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Thu, 16 Oct 2025 01:19:34 +0300
Subject: [PATCH 16/24] chore: updated deprecated functions

---
 internal/opengl/opengl.go | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/internal/opengl/opengl.go b/internal/opengl/opengl.go
index 4f7d01e..8db29c8 100644
--- a/internal/opengl/opengl.go
+++ b/internal/opengl/opengl.go
@@ -88,9 +88,9 @@ func (a *App) InitGL() error {
 
 	gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 5*4, nil)
 	gl.EnableVertexAttribArray(0)
-	gl.VertexAttribPointer(1, 2, gl.FLOAT, false, 5*4, gl.PtrOffset(2*4))
+	gl.VertexAttribPointerWithOffset(1, 2, gl.FLOAT, false, 5*4, 2*4)
 	gl.EnableVertexAttribArray(1)
-	gl.VertexAttribPointer(2, 1, gl.FLOAT, false, 5*4, gl.PtrOffset(4*4))
+	gl.VertexAttribPointerWithOffset(2, 1, gl.FLOAT, false, 5*4, 4*4)
 	gl.EnableVertexAttribArray(2)
 
 	gl.BindVertexArray(0)
@@ -103,13 +103,13 @@ func (a *App) InitGL() error {
 
 	gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 6*4, nil)
 	gl.EnableVertexAttribArray(0)
-	gl.VertexAttribPointer(1, 1, gl.FLOAT, false, 6*4, gl.PtrOffset(2*4))
+	gl.VertexAttribPointerWithOffset(1, 1, gl.FLOAT, false, 6*4, 2*4)
 	gl.EnableVertexAttribArray(1)
-	gl.VertexAttribPointer(2, 1, gl.FLOAT, false, 6*4, gl.PtrOffset(3*4))
+	gl.VertexAttribPointerWithOffset(2, 1, gl.FLOAT, false, 6*4, 3*4)
 	gl.EnableVertexAttribArray(2)
-	gl.VertexAttribPointer(3, 1, gl.FLOAT, false, 6*4, gl.PtrOffset(4*4))
+	gl.VertexAttribPointerWithOffset(3, 1, gl.FLOAT, false, 6*4, 4*4)
 	gl.EnableVertexAttribArray(3)
-	gl.VertexAttribPointer(4, 1, gl.FLOAT, false, 6*4, gl.PtrOffset(5*4))
+	gl.VertexAttribPointerWithOffset(4, 1, gl.FLOAT, false, 6*4, 5*4)
 	gl.EnableVertexAttribArray(4)
 
 	gl.BindVertexArray(0)

From 4e8aa39f8cf88d09b897304d28a2d099f5025357 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Thu, 16 Oct 2025 01:28:20 +0300
Subject: [PATCH 17/24] feat: embedded the shader files

---
 internal/opengl/opengl.go  | 28 ++++++++++++++--------------
 internal/shaders/paths.go  | 31 +++++++++++++++++++++++++++++++
 internal/shaders/shader.go | 21 +++++++++++++++++++++
 3 files changed, 66 insertions(+), 14 deletions(-)

diff --git a/internal/opengl/opengl.go b/internal/opengl/opengl.go
index 8db29c8..af390f2 100644
--- a/internal/opengl/opengl.go
+++ b/internal/opengl/opengl.go
@@ -21,11 +21,11 @@ func (a *App) InitGL() error {
 		return err
 	}
 
-	vertShader, err := shaders.CompileShaderFromFile(shaders.LineVertexPath, gl.VERTEX_SHADER)
+	vertShader, err := shaders.CompileShaderFromSource(shaders.LineVertex, gl.VERTEX_SHADER)
 	if err != nil {
 		return err
 	}
-	fragShader, err := shaders.CompileShaderFromFile(shaders.LineFragmentPath, gl.FRAGMENT_SHADER)
+	fragShader, err := shaders.CompileShaderFromSource(shaders.LineFragment, gl.FRAGMENT_SHADER)
 	if err != nil {
 		return err
 	}
@@ -48,15 +48,15 @@ func (a *App) InitGL() error {
 	gl.DeleteShader(vertShader)
 	gl.DeleteShader(fragShader)
 
-	particleVertShader, err := shaders.CompileShaderFromFile(
-		shaders.ParticleVertexPath,
+	particleVertShader, err := shaders.CompileShaderFromSource(
+		shaders.ParticleVertex,
 		gl.VERTEX_SHADER,
 	)
 	if err != nil {
 		return err
 	}
-	particleFragShader, err := shaders.CompileShaderFromFile(
-		shaders.ParticleFragmentPath,
+	particleFragShader, err := shaders.CompileShaderFromSource(
+		shaders.ParticleFragment,
 		gl.FRAGMENT_SHADER,
 	)
 	if err != nil {
@@ -114,15 +114,15 @@ func (a *App) InitGL() error {
 
 	gl.BindVertexArray(0)
 
-	bgVertShader, err := shaders.CompileShaderFromFile(
-		shaders.BackgroundVertexPath,
+	bgVertShader, err := shaders.CompileShaderFromSource(
+		shaders.BackgroundVertex,
 		gl.VERTEX_SHADER,
 	)
 	if err != nil {
 		return err
 	}
-	bgFragShader, err := shaders.CompileShaderFromFile(
-		shaders.BackgroundFragmentPath,
+	bgFragShader, err := shaders.CompileShaderFromSource(
+		shaders.BackgroundFragment,
 		gl.FRAGMENT_SHADER,
 	)
 	if err != nil {
@@ -165,15 +165,15 @@ func (a *App) InitGL() error {
 
 	gl.BindVertexArray(0)
 
-	cursorGlowVertShader, err := shaders.CompileShaderFromFile(
-		shaders.CursorGlowVertexPath,
+	cursorGlowVertShader, err := shaders.CompileShaderFromSource(
+		shaders.CursorGlowVertex,
 		gl.VERTEX_SHADER,
 	)
 	if err != nil {
 		return err
 	}
-	cursorGlowFragShader, err := shaders.CompileShaderFromFile(
-		shaders.CursorGlowFragmentPath,
+	cursorGlowFragShader, err := shaders.CompileShaderFromSource(
+		shaders.CursorGlowFragment,
 		gl.FRAGMENT_SHADER,
 	)
 	if err != nil {
diff --git a/internal/shaders/paths.go b/internal/shaders/paths.go
index b496856..36122ff 100644
--- a/internal/shaders/paths.go
+++ b/internal/shaders/paths.go
@@ -1,5 +1,8 @@
 package shaders
 
+import _ "embed"
+
+// TODO: select one to choose embed the shaders or place like system files.
 const BackgroundFragmentPath = "internal/shaders/background.frag.glsl"
 const BackgroundVertexPath = "internal/shaders/background.vert.glsl"
 const CursorGlowFragmentPath = "internal/shaders/cursorGlow.frag.glsl"
@@ -8,3 +11,31 @@ const LineFragmentPath = "internal/shaders/line.frag.glsl"
 const LineVertexPath = "internal/shaders/line.vert.glsl"
 const ParticleVertexPath = "internal/shaders/particle.vert.glsl"
 const ParticleFragmentPath = "internal/shaders/particle.frag.glsl"
+
+// Vertex shaders
+//
+//go:embed background.vert.glsl
+var BackgroundVertex string
+
+//go:embed cursorGlow.vert.glsl
+var CursorGlowVertex string
+
+//go:embed line.vert.glsl
+var LineVertex string
+
+//go:embed particle.vert.glsl
+var ParticleVertex string
+
+// Fragment shaders
+//
+//go:embed background.frag.glsl
+var BackgroundFragment string
+
+//go:embed cursorGlow.frag.glsl
+var CursorGlowFragment string
+
+//go:embed line.frag.glsl
+var LineFragment string
+
+//go:embed particle.frag.glsl
+var ParticleFragment string
diff --git a/internal/shaders/shader.go b/internal/shaders/shader.go
index 8aad584..6237f04 100644
--- a/internal/shaders/shader.go
+++ b/internal/shaders/shader.go
@@ -8,6 +8,7 @@ import (
 	"github.com/go-gl/gl/v4.1-core/gl"
 )
 
+// TODO: select either one or use both
 func CompileShaderFromFile(path string, shaderType uint32) (uint32, error) {
 	sourceBytes, err := os.ReadFile(path)
 	if err != nil {
@@ -42,3 +43,23 @@ func CompileShaderFromFile(path string, shaderType uint32) (uint32, error) {
 
 	return shader, nil
 }
+
+func CompileShaderFromSource(source string, shaderType uint32) (uint32, error) {
+	shader := gl.CreateShader(shaderType)
+	csources, free := gl.Strs(source + "\x00")
+	gl.ShaderSource(shader, 1, csources, nil)
+	free()
+	gl.CompileShader(shader)
+
+	var status int32
+	gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
+	if status == gl.FALSE {
+		var logLength int32
+		gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)
+		logMsg := make([]byte, logLength)
+		gl.GetShaderInfoLog(shader, logLength, nil, &logMsg[0])
+		return 0, fmt.Errorf("failed to compile shader: %s", logMsg)
+	}
+
+	return shader, nil
+}

From f47aba9595099b93c8430af2d58348293053d27b Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Fri, 17 Oct 2025 18:04:52 +0300
Subject: [PATCH 18/24] chore: deleted fornow package

---
 cmd/hexecute/main.go        |  10 ++--
 internal/execute/execute.go |  56 ++++++++++++++++++++
 internal/fornow/fornow.go   | 103 ------------------------------------
 internal/gesture/gesture.go |  51 ++++++++++++++++++
 4 files changed, 112 insertions(+), 108 deletions(-)
 delete mode 100644 internal/fornow/fornow.go

diff --git a/cmd/hexecute/main.go b/cmd/hexecute/main.go
index e331683..2e2f13c 100644
--- a/cmd/hexecute/main.go
+++ b/cmd/hexecute/main.go
@@ -10,7 +10,7 @@ import (
 
 	"github.com/ThatOtherAndrew/Hexecute/internal/config"
 	"github.com/ThatOtherAndrew/Hexecute/internal/draw"
-	"github.com/ThatOtherAndrew/Hexecute/internal/fornow"
+	"github.com/ThatOtherAndrew/Hexecute/internal/execute"
 	gestures "github.com/ThatOtherAndrew/Hexecute/internal/gesture"
 	"github.com/ThatOtherAndrew/Hexecute/internal/models"
 	"github.com/ThatOtherAndrew/Hexecute/internal/opengl"
@@ -190,8 +190,8 @@ func main() {
 				}
 			} else if !app.LearnMode && len(app.Points) > 0 {
 				x, y := window.GetCursorPos()
-				fornow := fornow.New(app)
-				fornow.RecognizeAndExecute(window, float32(x), float32(y))
+				exec := execute.New(app)
+				exec.RecognizeAndExecute(window, float32(x), float32(y))
 				app.Points = nil
 			}
 		}
@@ -199,8 +199,8 @@ func main() {
 
 		if app.IsDrawing {
 			x, y := window.GetCursorPos()
-			fornow := fornow.New(app)
-			fornow.AddPoint(float32(x), float32(y))
+			gesture := gestures.New(app)
+			gesture.AddPoint(float32(x), float32(y))
 
 			spawn := spawn.New(app)
 			spawn.SpawnCursorSparkles(float32(x), float32(y))
diff --git a/internal/execute/execute.go b/internal/execute/execute.go
index b470191..2784e57 100644
--- a/internal/execute/execute.go
+++ b/internal/execute/execute.go
@@ -1,10 +1,25 @@
 package execute
 
 import (
+	"log"
 	"os/exec"
 	"syscall"
+	"time"
+
+	"github.com/ThatOtherAndrew/Hexecute/internal/models"
+	"github.com/ThatOtherAndrew/Hexecute/internal/spawn"
+	"github.com/ThatOtherAndrew/Hexecute/internal/stroke"
+	"github.com/ThatOtherAndrew/Hexecute/pkg/wayland"
 )
 
+type App struct {
+	app *models.App
+}
+
+func New(app *models.App) *App {
+	return &App{app: app}
+}
+
 func Command(command string) error {
 	if command == "" {
 		return nil
@@ -20,3 +35,44 @@ func Command(command string) error {
 
 	return cmd.Start()
 }
+
+func (a *App) RecognizeAndExecute(window *wayland.WaylandWindow, x, y float32) {
+	if len(a.app.Points) < 5 {
+		log.Println("Gesture too short, ignoring")
+		return
+	}
+
+	processed := stroke.ProcessStroke(a.app.Points)
+
+	bestMatch := -1
+	bestScore := 0.0
+
+	for i, gesture := range a.app.SavedGestures {
+		match, score := stroke.UnistrokeRecognise(processed, gesture.Templates)
+		log.Printf("Gesture %d (%s): template %d, score %.3f", i, gesture.Command, match, score)
+
+		if score > bestScore {
+			bestScore = score
+			bestMatch = i
+		}
+	}
+
+	if bestMatch >= 0 && bestScore > 0.6 {
+		command := a.app.SavedGestures[bestMatch].Command
+		log.Printf("Matched gesture: %s (score: %.3f)", command, bestScore)
+
+		if err := Command(command); err != nil {
+			log.Printf("Failed to execute command: %v", err)
+		} else {
+			log.Printf("Executed: %s", command)
+		}
+
+		a.app.IsExiting = true
+		a.app.ExitStartTime = time.Now()
+		window.DisableInput()
+		spawn := spawn.New(a.app)
+		spawn.SpawnExitWisps(x, y)
+	} else {
+		log.Printf("No confident match (best score: %.3f)", bestScore)
+	}
+}
diff --git a/internal/fornow/fornow.go b/internal/fornow/fornow.go
deleted file mode 100644
index aef340c..0000000
--- a/internal/fornow/fornow.go
+++ /dev/null
@@ -1,103 +0,0 @@
-package fornow
-
-import (
-	"log"
-	"math"
-	"math/rand/v2"
-	"time"
-
-	"github.com/ThatOtherAndrew/Hexecute/internal/execute"
-	"github.com/ThatOtherAndrew/Hexecute/internal/models"
-	"github.com/ThatOtherAndrew/Hexecute/internal/spawn"
-	"github.com/ThatOtherAndrew/Hexecute/internal/stroke"
-	"github.com/ThatOtherAndrew/Hexecute/pkg/wayland"
-)
-
-type App struct {
-	app *models.App
-}
-
-func New(app *models.App) *App {
-	return &App{app: app}
-}
-
-func (a *App) AddPoint(x, y float32) {
-	newPoint := models.Point{X: x, Y: y, BornTime: time.Now()}
-
-	shouldAdd := false
-	if len(a.app.Points) == 0 {
-		shouldAdd = true
-	} else {
-		lastPoint := a.app.Points[len(a.app.Points)-1]
-		dx := newPoint.X - lastPoint.X
-		dy := newPoint.Y - lastPoint.Y
-		if dx*dx+dy*dy > 4 {
-			shouldAdd = true
-
-			for range 3 {
-				angle := rand.Float64() * 2 * math.Pi
-				speed := rand.Float32()*50 + 20
-				a.app.Particles = append(a.app.Particles, models.Particle{
-					X:       x + (rand.Float32()-0.5)*10,
-					Y:       y + (rand.Float32()-0.5)*10,
-					VX:      float32(math.Cos(angle)) * speed,
-					VY:      float32(math.Sin(angle)) * speed,
-					Life:    1.0,
-					MaxLife: 1.0,
-					Size:    rand.Float32()*15 + 10,
-					Hue:     rand.Float32(),
-				})
-			}
-		}
-	}
-
-	const MAX_POINTS = 2048
-
-	if shouldAdd {
-		a.app.Points = append(a.app.Points, newPoint)
-		if len(a.app.Points) > MAX_POINTS {
-			a.app.Points = a.app.Points[len(a.app.Points)-MAX_POINTS:]
-		}
-	}
-}
-
-func (a *App) RecognizeAndExecute(window *wayland.WaylandWindow, x, y float32) {
-	if len(a.app.Points) < 5 {
-		log.Println("Gesture too short, ignoring")
-		return
-	}
-
-	processed := stroke.ProcessStroke(a.app.Points)
-
-	bestMatch := -1
-	bestScore := 0.0
-
-	for i, gesture := range a.app.SavedGestures {
-		match, score := stroke.UnistrokeRecognise(processed, gesture.Templates)
-		log.Printf("Gesture %d (%s): template %d, score %.3f", i, gesture.Command, match, score)
-
-		if score > bestScore {
-			bestScore = score
-			bestMatch = i
-		}
-	}
-
-	if bestMatch >= 0 && bestScore > 0.6 {
-		command := a.app.SavedGestures[bestMatch].Command
-		log.Printf("Matched gesture: %s (score: %.3f)", command, bestScore)
-
-		if err := execute.Command(command); err != nil {
-			log.Printf("Failed to execute command: %v", err)
-		} else {
-			log.Printf("Executed: %s", command)
-		}
-
-		a.app.IsExiting = true
-		a.app.ExitStartTime = time.Now()
-		window.DisableInput()
-		spawn := spawn.New(a.app)
-		spawn.SpawnExitWisps(x, y)
-	} else {
-		log.Printf("No confident match (best score: %.3f)", bestScore)
-	}
-}
diff --git a/internal/gesture/gesture.go b/internal/gesture/gesture.go
index ed54c8d..d0086c1 100644
--- a/internal/gesture/gesture.go
+++ b/internal/gesture/gesture.go
@@ -2,12 +2,23 @@ package gestures
 
 import (
 	"encoding/json"
+	"math"
+	"math/rand/v2"
 	"os"
+	"time"
 
 	"github.com/ThatOtherAndrew/Hexecute/internal/config"
 	"github.com/ThatOtherAndrew/Hexecute/internal/models"
 )
 
+type App struct {
+	app *models.App
+}
+
+func New(app *models.App) *App {
+	return &App{app: app}
+}
+
 func LoadGestures() ([]models.GestureConfig, error) {
 	configFile, err := config.GetPath()
 	if err != nil {
@@ -67,3 +78,43 @@ func SaveGesture(command string, templates [][]models.Point) error {
 
 	return os.WriteFile(configFile, data, 0644)
 }
+
+func (a *App) AddPoint(x, y float32) {
+	newPoint := models.Point{X: x, Y: y, BornTime: time.Now()}
+
+	shouldAdd := false
+	if len(a.app.Points) == 0 {
+		shouldAdd = true
+	} else {
+		lastPoint := a.app.Points[len(a.app.Points)-1]
+		dx := newPoint.X - lastPoint.X
+		dy := newPoint.Y - lastPoint.Y
+		if dx*dx+dy*dy > 4 {
+			shouldAdd = true
+
+			for range 3 {
+				angle := rand.Float64() * 2 * math.Pi
+				speed := rand.Float32()*50 + 20
+				a.app.Particles = append(a.app.Particles, models.Particle{
+					X:       x + (rand.Float32()-0.5)*10,
+					Y:       y + (rand.Float32()-0.5)*10,
+					VX:      float32(math.Cos(angle)) * speed,
+					VY:      float32(math.Sin(angle)) * speed,
+					Life:    1.0,
+					MaxLife: 1.0,
+					Size:    rand.Float32()*15 + 10,
+					Hue:     rand.Float32(),
+				})
+			}
+		}
+	}
+
+	const MAX_POINTS = 2048
+
+	if shouldAdd {
+		a.app.Points = append(a.app.Points, newPoint)
+		if len(a.app.Points) > MAX_POINTS {
+			a.app.Points = a.app.Points[len(a.app.Points)-MAX_POINTS:]
+		}
+	}
+}

From fb052ffea2b8337ba3e52c01f9aa50047b5db4e2 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Fri, 17 Oct 2025 01:00:53 +0300
Subject: [PATCH 19/24] !feat: added root cli parameter

---
 cmd/root.go | 37 +++++++++++++++++++++++++++++++++++++
 go.mod      | 10 +++++++++-
 go.sum      | 11 +++++++++++
 3 files changed, 57 insertions(+), 1 deletion(-)
 create mode 100644 cmd/root.go

diff --git a/cmd/root.go b/cmd/root.go
new file mode 100644
index 0000000..576b567
--- /dev/null
+++ b/cmd/root.go
@@ -0,0 +1,37 @@
+package cmd
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+
+
+var rootCmd = &cobra.Command{
+	Use:   "Hexecute",
+	Short: "Launch apps by casting spells!",
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+
+	// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.Hexecute.yaml)")
+
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}
+
+
diff --git a/go.mod b/go.mod
index 8772d7e..57a605b 100644
--- a/go.mod
+++ b/go.mod
@@ -2,4 +2,12 @@ module github.com/ThatOtherAndrew/Hexecute
 
 go 1.25.1
 
-require github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71
+require (
+	github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71
+	github.com/spf13/cobra v1.10.1
+)
+
+require (
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/spf13/pflag v1.0.10 // indirect
+)
diff --git a/go.sum b/go.sum
index e1d623b..9b8d571 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,13 @@
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
 github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
+github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

From d435b08a184e182df5339a3963fd25a46b850ce2 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Fri, 17 Oct 2025 01:01:13 +0300
Subject: [PATCH 20/24] !feat: added run cli parameter

---
 cmd/run.go | 133 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 133 insertions(+)
 create mode 100644 cmd/run.go

diff --git a/cmd/run.go b/cmd/run.go
new file mode 100644
index 0000000..14d0504
--- /dev/null
+++ b/cmd/run.go
@@ -0,0 +1,133 @@
+package cmd
+
+import (
+	"log"
+	"runtime"
+	"time"
+
+	"github.com/ThatOtherAndrew/Hexecute/internal/draw"
+	"github.com/ThatOtherAndrew/Hexecute/internal/execute"
+	gestures "github.com/ThatOtherAndrew/Hexecute/internal/gesture"
+	"github.com/ThatOtherAndrew/Hexecute/internal/models"
+	"github.com/ThatOtherAndrew/Hexecute/internal/opengl"
+	"github.com/ThatOtherAndrew/Hexecute/internal/spawn"
+	"github.com/ThatOtherAndrew/Hexecute/internal/update"
+	"github.com/ThatOtherAndrew/Hexecute/pkg/wayland"
+	"github.com/go-gl/gl/v4.1-core/gl"
+	"github.com/spf13/cobra"
+)
+
+var runCmd = &cobra.Command{
+	Use:   "run",
+	Short: "run the program",
+	Run:   Run,
+}
+
+func init() {
+	rootCmd.AddCommand(runCmd)
+	runtime.LockOSThread()
+
+	// Here you will define your flags and configuration settings.
+
+	// Cobra supports Persistent Flags which will work for this command
+	// and all subcommands, e.g.:
+	// runCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+	// Cobra supports local flags which will only run when this command
+	// is called directly, e.g.:
+	// runCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}
+
+func Run(cmd *cobra.Command, args []string) {
+	window, err := wayland.NewWaylandWindow()
+	if err != nil {
+		log.Fatal("Failed to create Wayland window:", err)
+	}
+	defer window.Destroy()
+
+	app := &models.App{StartTime: time.Now()}
+
+	gesture, err := gestures.LoadGestures()
+	if err != nil {
+		log.Fatal("Failed to load gestures:", err)
+	}
+	app.SavedGestures = gesture
+	log.Printf("Loaded %d gesture(s)", len(gesture))
+
+	opengl := opengl.New(app)
+	if err := opengl.InitGL(); err != nil {
+		log.Fatal("Failed to initialize OpenGL:", err)
+	}
+
+	gl.ClearColor(0, 0, 0, 0)
+
+	for range 5 {
+		window.PollEvents()
+		gl.Clear(gl.COLOR_BUFFER_BIT)
+		window.SwapBuffers()
+	}
+
+	x, y := window.GetCursorPos()
+	app.LastCursorX = float32(x)
+	app.LastCursorY = float32(y)
+
+	lastTime := time.Now()
+	var wasPressed bool
+
+	for !window.ShouldClose() {
+		now := time.Now()
+		dt := float32(now.Sub(lastTime).Seconds())
+		lastTime = now
+
+		window.PollEvents()
+		update := update.New(app)
+		update.UpdateCursor(window)
+
+		if key, state, hasKey := window.GetLastKey(); hasKey {
+			if state == 1 && key == 0xff1b {
+				if !app.IsExiting {
+					app.IsExiting = true
+					app.ExitStartTime = time.Now()
+					window.DisableInput()
+					x, y := window.GetCursorPos()
+					spawn := spawn.New(app)
+					spawn.SpawnExitWisps(float32(x), float32(y))
+				}
+			}
+			window.ClearLastKey()
+		}
+
+		if app.IsExiting {
+			if time.Since(app.ExitStartTime).Seconds() > 0.8 {
+				break
+			}
+		}
+		isPressed := window.GetMouseButton()
+		if isPressed && !wasPressed {
+			app.IsDrawing = true
+		} else if !isPressed && wasPressed {
+			app.IsDrawing = false
+
+			x, y := window.GetCursorPos()
+			exec := execute.New(app)
+			exec.RecognizeAndExecute(window, float32(x), float32(y))
+			app.Points = nil
+		}
+		wasPressed = isPressed
+
+		if app.IsDrawing {
+			x, y := window.GetCursorPos()
+			gesture := gestures.New(app)
+			gesture.AddPoint(float32(x), float32(y))
+
+			spawn := spawn.New(app)
+			spawn.SpawnCursorSparkles(float32(x), float32(y))
+		}
+
+		update.UpdateParticles(dt)
+		drawer := draw.New(app)
+		drawer.Draw(window)
+		window.SwapBuffers()
+	}
+
+}

From ec7ac095c1c86f87e21d32b193c3aa3d4046661f Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Fri, 17 Oct 2025 01:01:21 +0300
Subject: [PATCH 21/24] !feat: added learn cli parameter

---
 cmd/learn.go | 154 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 154 insertions(+)
 create mode 100644 cmd/learn.go

diff --git a/cmd/learn.go b/cmd/learn.go
new file mode 100644
index 0000000..a443605
--- /dev/null
+++ b/cmd/learn.go
@@ -0,0 +1,154 @@
+package cmd
+
+import (
+	"log"
+	"runtime"
+	"strings"
+	"time"
+
+	"github.com/ThatOtherAndrew/Hexecute/internal/draw"
+	gestures "github.com/ThatOtherAndrew/Hexecute/internal/gesture"
+	"github.com/ThatOtherAndrew/Hexecute/internal/models"
+	"github.com/ThatOtherAndrew/Hexecute/internal/opengl"
+	"github.com/ThatOtherAndrew/Hexecute/internal/spawn"
+	"github.com/ThatOtherAndrew/Hexecute/internal/stroke"
+	"github.com/ThatOtherAndrew/Hexecute/internal/update"
+	"github.com/ThatOtherAndrew/Hexecute/pkg/wayland"
+	"github.com/go-gl/gl/v4.1-core/gl"
+	"github.com/spf13/cobra"
+)
+
+// learnCmd represents the learn command
+var learnCmd = &cobra.Command{
+	Use:   "learn",
+	Short: "Learn a new gesture for the specified command",
+	Run:   learnGesture,
+}
+
+func init() {
+	rootCmd.AddCommand(learnCmd)
+	runtime.LockOSThread()
+
+	// Here you will define your flags and configuration settings.
+
+	// Cobra supports Persistent Flags which will work for this command
+	// and all subcommands, e.g.:
+	// learnCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+	// Cobra supports local flags which will only run when this command
+	// is called directly, e.g.:
+	// learnCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}
+
+func learnGesture(cmd *cobra.Command, args []string) {
+	window, err := wayland.NewWaylandWindow()
+	if err != nil {
+		log.Fatal("Failed to create Wayland window:", err)
+	}
+
+	defer window.Destroy()
+	app := &models.App{StartTime: time.Now()}
+
+	if len(args) <= 0 {
+		log.Fatalf("Please specify a command")
+	}
+
+	command := strings.Join(args, " ")
+
+	app.LearnCommand = command
+	log.Printf("Learn mode: Draw the gesture 3 times for command '%s'", command)
+
+	opengl := opengl.New(app)
+	if err := opengl.InitGL(); err != nil {
+		log.Fatal("Failed to initialize OpenGL:", err)
+	}
+
+	gl.ClearColor(0, 0, 0, 0)
+
+	for range 5 {
+		window.PollEvents()
+		gl.Clear(gl.COLOR_BUFFER_BIT)
+		window.SwapBuffers()
+	}
+
+	x, y := window.GetCursorPos()
+	app.LastCursorX = float32(x)
+	app.LastCursorY = float32(y)
+
+	lastTime := time.Now()
+	var wasPressed bool
+
+	for !window.ShouldClose() {
+		now := time.Now()
+		dt := float32(now.Sub(lastTime).Seconds())
+		lastTime = now
+
+		window.PollEvents()
+		update := update.New(app)
+		update.UpdateCursor(window)
+
+		if key, state, hasKey := window.GetLastKey(); hasKey {
+			if state == 1 && key == 0xff1b {
+				if !app.IsExiting {
+					app.IsExiting = true
+					app.ExitStartTime = time.Now()
+					window.DisableInput()
+					x, y := window.GetCursorPos()
+					spawn := spawn.New(app)
+					spawn.SpawnExitWisps(float32(x), float32(y))
+				}
+			}
+			window.ClearLastKey()
+		}
+
+		if app.IsExiting {
+			if time.Since(app.ExitStartTime).Seconds() > 0.8 {
+				break
+			}
+		}
+		isPressed := window.GetMouseButton()
+		if isPressed && !wasPressed {
+			app.IsDrawing = true
+		} else if !isPressed && wasPressed {
+			app.IsDrawing = false
+
+			if len(app.Points) > 0 {
+				processed := stroke.ProcessStroke(app.Points)
+				app.LearnGestures = append(app.LearnGestures, processed)
+				app.LearnCount++
+				log.Printf("Captured gesture %d/3", app.LearnCount)
+
+				app.Points = nil
+
+				if app.LearnCount >= 3 {
+					if err := gestures.SaveGesture(app.LearnCommand, app.LearnGestures); err != nil {
+						log.Fatal("Failed to save gesture:", err)
+					}
+					log.Printf("Gesture saved for command: %s", app.LearnCommand)
+
+					app.IsExiting = true
+					app.ExitStartTime = time.Now()
+					window.DisableInput()
+					x, y := window.GetCursorPos()
+					spawn := spawn.New(app)
+					spawn.SpawnExitWisps(float32(x), float32(y))
+				}
+			}
+		}
+		wasPressed = isPressed
+
+		if app.IsDrawing {
+			x, y := window.GetCursorPos()
+			gesture := gestures.New(app)
+			gesture.AddPoint(float32(x), float32(y))
+
+			spawn := spawn.New(app)
+			spawn.SpawnCursorSparkles(float32(x), float32(y))
+		}
+
+		update.UpdateParticles(dt)
+		drawer := draw.New(app)
+		drawer.Draw(window)
+		window.SwapBuffers()
+	}
+}

From 2543e6c7d9ef83212896cb83a6ed88b32cf16c03 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Fri, 17 Oct 2025 01:01:30 +0300
Subject: [PATCH 22/24] !feat: added list cli parameter

---
 cmd/list.go | 45 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 45 insertions(+)
 create mode 100644 cmd/list.go

diff --git a/cmd/list.go b/cmd/list.go
new file mode 100644
index 0000000..61a6e3c
--- /dev/null
+++ b/cmd/list.go
@@ -0,0 +1,45 @@
+package cmd
+
+import (
+	"fmt"
+	"log"
+
+	gestures "github.com/ThatOtherAndrew/Hexecute/internal/gesture"
+	"github.com/spf13/cobra"
+)
+
+var listCmd = &cobra.Command{
+	Use:   "list",
+	Short: "List all registered gestures",
+	Run:   listGestures,
+}
+
+func init() {
+	rootCmd.AddCommand(listCmd)
+
+	// Here you will define your flags and configuration settings.
+
+	// Cobra supports Persistent Flags which will work for this command
+	// and all subcommands, e.g.:
+	// listCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+	// Cobra supports local flags which will only run when this command
+	// is called directly, e.g.:
+	// listCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}
+
+func listGestures(cmd *cobra.Command, args []string) {
+
+	gestures, err := gestures.LoadGestures()
+	if err != nil {
+		log.Fatal("Failed to load gestures:", err)
+	}
+	if len(gestures) == 0 {
+		fmt.Println("No gestures registered")
+	} else {
+		fmt.Println("Registered gestures:")
+		for _, g := range gestures {
+			fmt.Println("  ", g.Command)
+		}
+	}
+}

From 7c23e39cd979a3fed4426af6d710b3f39f6341b3 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Fri, 17 Oct 2025 01:01:39 +0300
Subject: [PATCH 23/24] !feat: added remove cli parameter

---
 cmd/remove.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 73 insertions(+)
 create mode 100644 cmd/remove.go

diff --git a/cmd/remove.go b/cmd/remove.go
new file mode 100644
index 0000000..515a794
--- /dev/null
+++ b/cmd/remove.go
@@ -0,0 +1,73 @@
+package cmd
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"os"
+
+	"github.com/ThatOtherAndrew/Hexecute/internal/config"
+	gestures "github.com/ThatOtherAndrew/Hexecute/internal/gesture"
+	"github.com/spf13/cobra"
+)
+
+var removeCmd = &cobra.Command{
+	Use:   "remove [gesture]",
+	Short: "Remove a gesture by command name",
+	Run:   removeGesture,
+}
+
+func init() {
+	rootCmd.AddCommand(removeCmd)
+	log.SetFlags(0)
+
+	// Here you will define your flags and configuration settings.
+
+	// Cobra supports Persistent Flags which will work for this command
+	// and all subcommands, e.g.:
+	// removeCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+	// Cobra supports local flags which will only run when this command
+	// is called directly, e.g.:
+	// removeCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}
+
+func removeGesture(cmd *cobra.Command, args []string) {
+
+	if len(args) <= 0 {
+		log.Fatalf("Please specify a gesture")
+	}
+	gestures, err := gestures.LoadGestures()
+	if err != nil {
+		log.Fatal("Failed to load gestures:", err)
+	}
+
+	found := false
+	for i, g := range gestures {
+		if g.Command == args[0] {
+			gestures = append(gestures[:i], gestures[i+1:]...)
+			found = true
+			break
+		}
+	}
+
+	if !found {
+		log.Fatalf("Gesture not found: %s", args[0])
+	}
+
+	configFile, err := config.GetPath()
+	if err != nil {
+		log.Fatal("Failed to get config path:", err)
+	}
+
+	data, err := json.Marshal(gestures)
+	if err != nil {
+		log.Fatal("Failed to marshal gestures:", err)
+	}
+
+	if err := os.WriteFile(configFile, data, 0644); err != nil {
+		log.Fatal("Failed to save gestures:", err)
+	}
+
+	fmt.Println("Removed gesture:", args[0])
+}

From 8f86e69e2533cc5e66e7f917e7e3262f218068b6 Mon Sep 17 00:00:00 2001
From: cilginc <104676201+cilginc@users.noreply.github.com>
Date: Fri, 17 Oct 2025 01:36:43 +0300
Subject: [PATCH 24/24] !feat: added completion cli parameter

---
 cmd/completion.go    |  47 ++++++++++
 cmd/hexecute/main.go | 211 +------------------------------------------
 cmd/root.go          |   6 +-
 3 files changed, 50 insertions(+), 214 deletions(-)
 create mode 100644 cmd/completion.go

diff --git a/cmd/completion.go b/cmd/completion.go
new file mode 100644
index 0000000..c00aa22
--- /dev/null
+++ b/cmd/completion.go
@@ -0,0 +1,47 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+var completionCmd = &cobra.Command{
+	Use:                   "completion [bash|zsh|fish]",
+	Short:                 "Get completions for your favorite shell",
+	RunE:                  GetCompletion,
+	ValidArgs:             []string{"bash", "zsh", "fish"},
+	DisableFlagsInUseLine: true,
+}
+
+func init() {
+	rootCmd.AddCommand(completionCmd)
+
+	// Here you will define your flags and configuration settings.
+
+	// Cobra supports Persistent Flags which will work for this command
+	// and all subcommands, e.g.:
+	// completionCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+	// Cobra supports local flags which will only run when this command
+	// is called directly, e.g.:
+	// completionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}
+
+func GetCompletion(cmd *cobra.Command, args []string) error {
+	if len(args) <= 0 {
+		return cmd.Help()
+	}
+
+	switch args[0] {
+	case "bash":
+		return rootCmd.GenBashCompletionV2(os.Stdout, true)
+	case "zsh":
+		return rootCmd.GenZshCompletion(os.Stdout)
+	case "fish":
+		return rootCmd.GenFishCompletion(os.Stdout, true)
+	default:
+		return fmt.Errorf("unsupported shell: %s", args[0])
+	}
+}
diff --git a/cmd/hexecute/main.go b/cmd/hexecute/main.go
index 2e2f13c..b95b287 100644
--- a/cmd/hexecute/main.go
+++ b/cmd/hexecute/main.go
@@ -1,214 +1,7 @@
 package main
 
-import (
-	"encoding/json"
-	"flag"
-	"log"
-	"os"
-	"runtime"
-	"time"
-
-	"github.com/ThatOtherAndrew/Hexecute/internal/config"
-	"github.com/ThatOtherAndrew/Hexecute/internal/draw"
-	"github.com/ThatOtherAndrew/Hexecute/internal/execute"
-	gestures "github.com/ThatOtherAndrew/Hexecute/internal/gesture"
-	"github.com/ThatOtherAndrew/Hexecute/internal/models"
-	"github.com/ThatOtherAndrew/Hexecute/internal/opengl"
-	"github.com/ThatOtherAndrew/Hexecute/internal/spawn"
-	"github.com/ThatOtherAndrew/Hexecute/internal/stroke"
-	"github.com/ThatOtherAndrew/Hexecute/internal/update"
-	"github.com/ThatOtherAndrew/Hexecute/pkg/wayland"
-	"github.com/go-gl/gl/v4.1-core/gl"
-)
-
-func init() {
-	runtime.LockOSThread()
-}
-
-type App struct {
-	*models.App
-}
+import "github.com/ThatOtherAndrew/Hexecute/cmd"
 
 func main() {
-	learnCommand := flag.String("learn", "", "Learn a new gesture for the specified command")
-	listGestures := flag.Bool("list", false, "List all registered gestures")
-	removeGesture := flag.String("remove", "", "Remove a gesture by command name")
-	flag.Parse()
-
-	if flag.NArg() > 0 {
-		log.Fatalf("Unknown arguments: %v", flag.Args())
-	}
-
-	if *listGestures {
-		gestures, err := gestures.LoadGestures()
-		if err != nil {
-			log.Fatal("Failed to load gestures:", err)
-		}
-		if len(gestures) == 0 {
-			println("No gestures registered")
-		} else {
-			println("Registered gestures:")
-			for _, g := range gestures {
-				println("  ", g.Command)
-			}
-		}
-		return
-	}
-
-	if *removeGesture != "" {
-		gestures, err := gestures.LoadGestures()
-		if err != nil {
-			log.Fatal("Failed to load gestures:", err)
-		}
-
-		found := false
-		for i, g := range gestures {
-			if g.Command == *removeGesture {
-				gestures = append(gestures[:i], gestures[i+1:]...)
-				found = true
-				break
-			}
-		}
-
-		if !found {
-			log.Fatalf("Gesture not found: %s", *removeGesture)
-		}
-
-		configFile, err := config.GetPath()
-		if err != nil {
-			log.Fatal("Failed to get config path:", err)
-		}
-
-		data, err := json.Marshal(gestures)
-		if err != nil {
-			log.Fatal("Failed to marshal gestures:", err)
-		}
-
-		if err := os.WriteFile(configFile, data, 0644); err != nil {
-			log.Fatal("Failed to save gestures:", err)
-		}
-
-		println("Removed gesture:", *removeGesture)
-		return
-	}
-
-	window, err := wayland.NewWaylandWindow()
-	if err != nil {
-		log.Fatal("Failed to create Wayland window:", err)
-	}
-	defer window.Destroy()
-
-	app := &models.App{StartTime: time.Now()}
-
-	if *learnCommand != "" {
-		app.LearnMode = true
-		app.LearnCommand = *learnCommand
-		log.Printf("Learn mode: Draw the gesture 3 times for command '%s'", *learnCommand)
-	} else {
-		gestures, err := gestures.LoadGestures()
-		if err != nil {
-			log.Fatal("Failed to load gestures:", err)
-		}
-		app.SavedGestures = gestures
-		log.Printf("Loaded %d gesture(s)", len(gestures))
-	}
-
-	opengl := opengl.New(app)
-	if err := opengl.InitGL(); err != nil {
-		log.Fatal("Failed to initialize OpenGL:", err)
-	}
-
-	gl.ClearColor(0, 0, 0, 0)
-
-	for range 5 {
-		window.PollEvents()
-		gl.Clear(gl.COLOR_BUFFER_BIT)
-		window.SwapBuffers()
-	}
-
-	x, y := window.GetCursorPos()
-	app.LastCursorX = float32(x)
-	app.LastCursorY = float32(y)
-
-	lastTime := time.Now()
-	var wasPressed bool
-
-	for !window.ShouldClose() {
-		now := time.Now()
-		dt := float32(now.Sub(lastTime).Seconds())
-		lastTime = now
-
-		window.PollEvents()
-		update := update.New(app)
-		update.UpdateCursor(window)
-
-		if key, state, hasKey := window.GetLastKey(); hasKey {
-			if state == 1 && key == 0xff1b {
-				if !app.IsExiting {
-					app.IsExiting = true
-					app.ExitStartTime = time.Now()
-					window.DisableInput()
-					x, y := window.GetCursorPos()
-					spawn := spawn.New(app)
-					spawn.SpawnExitWisps(float32(x), float32(y))
-				}
-			}
-			window.ClearLastKey()
-		}
-
-		if app.IsExiting {
-			if time.Since(app.ExitStartTime).Seconds() > 0.8 {
-				break
-			}
-		}
-		isPressed := window.GetMouseButton()
-		if isPressed && !wasPressed {
-			app.IsDrawing = true
-		} else if !isPressed && wasPressed {
-			app.IsDrawing = false
-
-			if app.LearnMode && len(app.Points) > 0 {
-				processed := stroke.ProcessStroke(app.Points)
-				app.LearnGestures = append(app.LearnGestures, processed)
-				app.LearnCount++
-				log.Printf("Captured gesture %d/3", app.LearnCount)
-
-				app.Points = nil
-
-				if app.LearnCount >= 3 {
-					if err := gestures.SaveGesture(app.LearnCommand, app.LearnGestures); err != nil {
-						log.Fatal("Failed to save gesture:", err)
-					}
-					log.Printf("Gesture saved for command: %s", app.LearnCommand)
-
-					app.IsExiting = true
-					app.ExitStartTime = time.Now()
-					window.DisableInput()
-					x, y := window.GetCursorPos()
-					spawn := spawn.New(app)
-					spawn.SpawnExitWisps(float32(x), float32(y))
-				}
-			} else if !app.LearnMode && len(app.Points) > 0 {
-				x, y := window.GetCursorPos()
-				exec := execute.New(app)
-				exec.RecognizeAndExecute(window, float32(x), float32(y))
-				app.Points = nil
-			}
-		}
-		wasPressed = isPressed
-
-		if app.IsDrawing {
-			x, y := window.GetCursorPos()
-			gesture := gestures.New(app)
-			gesture.AddPoint(float32(x), float32(y))
-
-			spawn := spawn.New(app)
-			spawn.SpawnCursorSparkles(float32(x), float32(y))
-		}
-
-		update.UpdateParticles(dt)
-		drawer := draw.New(app)
-		drawer.Draw(window)
-		window.SwapBuffers()
-	}
+	cmd.Execute()
 }
diff --git a/cmd/root.go b/cmd/root.go
index 576b567..945d89f 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -6,10 +6,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
-
-
 var rootCmd = &cobra.Command{
-	Use:   "Hexecute",
+	Use:   "hexecute",
 	Short: "Launch apps by casting spells!",
 }
 
@@ -33,5 +31,3 @@ func init() {
 	// when this action is called directly.
 	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
 }
-
-