Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ on:

jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
permissions:
id-token: "write"
contents: "read"
Expand All @@ -19,5 +23,5 @@ jobs:
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: hexecute
name: hexecute-${{ matrix.os }}
path: ./result/bin/
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Hexecute

A gesture-based launcher for Wayland. Launch apps by casting spells! 🪄
A gesture-based launcher for Wayland and macOS. Launch apps by casting spells! 🪄

![Demo GIF](.github/assets/demo.gif)

Expand Down Expand Up @@ -54,14 +54,24 @@ git clone https://github.com/m31-galaxy/Hexecute
cd Hexecute
```

If you have [Nix](https://nixos.org/) installed, simply run `nix build`.
If you have [Nix](https://nixos.org/) installed, simply run `nix build`. This works on both Linux and macOS.

Otherwise, make sure you have Go (and all dependent Wayland (and X11!?) libs) installed, then run:
Otherwise:

**On Linux**, make sure you have Go (and all dependent Wayland (and X11!?) libs) installed, then run:
```bash
mkdir -p bin
go build -o bin ./...
./bin/hexecute
```

**On macOS**, make sure you have Go and the Xcode command line tools (`xcode-select --install`) installed, then run:
```bash
mkdir -p bin
go build -o bin ./...
./bin/hexecute
```
The macOS build uses a native Cocoa overlay window and the system OpenGL framework — no Wayland or X11 libraries are required.

## Usage

Expand All @@ -86,6 +96,12 @@ If you're using Sway, add the following line to your `~/.config/sway/config`:
bindsym $mod+space exec hexecute
```

#### macOS

Running `hexecute` (with no arguments) **draws a gesture immediately**: the overlay appears, you cast once, and it dismisses. Bind it to a keyboard shortcut with your preferred hotkey tool (e.g. a Shortcuts.app quick action, Raycast, or skhd) to launch it on demand.

> Note: depending on your macOS version, drawing the overlay over other applications may require granting your terminal (or whichever app launches Hexecute) **Screen Recording** and/or **Accessibility** permission under System Settings → Privacy & Security.

### Learning a Gesture

To configure a gesture to launch an application, run `hexecute --learn [command]` in a terminal. Hexecute should launch - simply draw your chosen gesture **3 times** and it will be mapped to the command.
Expand Down
78 changes: 56 additions & 22 deletions cmd/hexecute/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,16 @@ import (
gestures "github.com/m31-galaxy/Hexecute/internal/gesture"
"github.com/m31-galaxy/Hexecute/internal/models"
"github.com/m31-galaxy/Hexecute/internal/opengl"
"github.com/m31-galaxy/Hexecute/internal/platform"
"github.com/m31-galaxy/Hexecute/internal/spawn"
"github.com/m31-galaxy/Hexecute/internal/stroke"
"github.com/m31-galaxy/Hexecute/internal/update"
"github.com/m31-galaxy/Hexecute/pkg/wayland"
)

func init() {
runtime.LockOSThread()
}

type App struct {
*models.App
}

func main() {
learnCommand := flag.String("learn", "", "Learn a new gesture for the specified command")
listGestures := flag.Bool("list", false, "List all registered gestures")
Expand Down Expand Up @@ -92,12 +88,6 @@ func main() {
return
}

window, err := wayland.NewWaylandWindow()
if err != nil {
log.Fatal("Failed to create Wayland window:", err)
}
defer window.Destroy()

settings, err := config.LoadSettings()
if err != nil {
log.Fatal("Failed to load settings:", err)
Expand All @@ -112,32 +102,76 @@ func main() {
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))
runOnce(app)
return
}

opengl := opengl.New(app)
if err := opengl.InitGL(); err != nil {
log.Fatal("Failed to initialize OpenGL:", err)
loaded, err := gestures.LoadGestures()
if err != nil {
log.Fatal("Failed to load gestures:", err)
}
app.SavedGestures = loaded
log.Printf("Loaded %d gesture(s)", len(loaded))

gl.ClearColor(0, 0, 0, 0)
// Launch the overlay and draw a gesture now.
runOnce(app)
}

// initGLAndWarm compiles shaders and pumps a few clear frames so the first real
// frame is ready.
func initGLAndWarm(app *models.App, window platform.Window) error {
o := opengl.New(app)
if err := o.InitGL(); err != nil {
return err
}

gl.ClearColor(0, 0, 0, 0)
for range 5 {
window.PollEvents()
gl.Clear(gl.COLOR_BUFFER_BIT)
window.SwapBuffers()
}
return nil
}

// resetSession clears per-cast state so a reused window (resident mode) starts a
// fresh gesture each time the overlay is shown.
func resetSession(app *models.App, window platform.Window) {
app.StartTime = time.Now()
app.IsExiting = false
app.IsDrawing = false
app.Points = nil
app.Particles = nil
app.CursorVelocity = 0
app.SmoothVelocity = 0
app.SmoothRotation = 0
app.SmoothDrawing = 0

x, y := window.GetCursorPos()
app.LastCursorX = float32(x)
app.LastCursorY = float32(y)
}

// runOnce creates a window, runs a single gesture session, and tears it down.
// Used for `--learn` (all platforms) and the per-launch path on non-macOS.
func runOnce(app *models.App) {
window, err := platform.NewWindow()
if err != nil {
log.Fatal("Failed to create window:", err)
}
defer window.Destroy()

if err := initGLAndWarm(app, window); err != nil {
log.Fatal("Failed to initialize OpenGL:", err)
}

resetSession(app, window)
runSession(app, window)
}

// runSession runs the gesture-capture loop on an already-shown window, returning
// when the cast ends (gesture executed, Esc, or learn complete).
func runSession(app *models.App, window platform.Window) {
lastTime := time.Now()
var wasPressed bool

Expand Down
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

150 changes: 93 additions & 57 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -8,74 +8,110 @@
outputs =
inputs:
let
system = "x86_64-linux";
pkgs = inputs.nixpkgs.legacyPackages.${system};
hexecute = pkgs.buildGoModule {
pname = "hexecute";
version = "0.1.0";
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forAllSystems =
f:
inputs.nixpkgs.lib.genAttrs supportedSystems (
system: f system inputs.nixpkgs.legacyPackages.${system}
);

src = ./.;
mkHexecute =
system: pkgs:
let
inherit (pkgs) lib stdenv;
isDarwin = stdenv.isDarwin;
in
pkgs.buildGoModule {
pname = "hexecute";
version = "0.1.0";

vendorHash = "sha256-CIlYhcX7F08Xwrr3/0tkgrfuP68UU0CeQ+HV63b6Ddg=";
src = ./.;

nativeBuildInputs = with pkgs; [
pkg-config
makeWrapper
];
vendorHash = "sha256-CIlYhcX7F08Xwrr3/0tkgrfuP68UU0CeQ+HV63b6Ddg=";

buildInputs = with pkgs; [
wayland
wayland-protocols
libxkbcommon
libGL
libGLU
mesa
xorg.libX11
];
nativeBuildInputs = with pkgs; [
pkg-config
] ++ lib.optionals (!isDarwin) [ makeWrapper ];

postFixup = ''
wrapProgram $out/bin/hexecute \
--prefix __EGL_VENDOR_LIBRARY_DIRS : "/run/opengl-driver/share/glvnd/egl_vendor.d" \
--prefix __EGL_VENDOR_LIBRARY_DIRS : "${pkgs.mesa}/share/glvnd/egl_vendor.d" \
--prefix LIBGL_DRIVERS_PATH : "/run/opengl-driver/lib/dri" \
--prefix LIBGL_DRIVERS_PATH : "${pkgs.mesa}/lib/dri"
'';
buildInputs =
if isDarwin then
# On modern nixpkgs the macOS SDK (Cocoa, OpenGL, ...) is provided
# by the stdenv itself, so the cgo `-framework` flags resolve
# without any explicit framework derivations — those legacy
# `darwin.apple_sdk.frameworks.*` stubs have been removed.
[ ]
else
with pkgs; [
wayland
wayland-protocols
libxkbcommon
libGL
libGLU
mesa
xorg.libX11
];

meta = {
description = "Launch apps by casting spells! 🪄";
homepage = "https://github.com/m31-galaxy/Hexecute";
license = pkgs.lib.licenses.gpl3;
platforms = pkgs.lib.platforms.linux;
# The Wayland/EGL driver path wrapping only applies on Linux.
postFixup = lib.optionalString (!isDarwin) ''
wrapProgram $out/bin/hexecute \
--prefix __EGL_VENDOR_LIBRARY_DIRS : "/run/opengl-driver/share/glvnd/egl_vendor.d" \
--prefix __EGL_VENDOR_LIBRARY_DIRS : "${pkgs.mesa}/share/glvnd/egl_vendor.d" \
--prefix LIBGL_DRIVERS_PATH : "/run/opengl-driver/lib/dri" \
--prefix LIBGL_DRIVERS_PATH : "${pkgs.mesa}/lib/dri"
'';

meta = {
description = "Launch apps by casting spells! 🪄";
homepage = "https://hexecute.app";
license = lib.licenses.gpl3;
platforms = lib.platforms.linux ++ lib.platforms.darwin;
};
};
};
in
{
packages.${system} = {
inherit hexecute;
default = hexecute;
};

devShells.${system}.default = pkgs.mkShell {
name = "hexecute";

packages = with pkgs; [
go
pkg-config
packages = forAllSystems (
system: pkgs:
let
hexecute = mkHexecute system pkgs;
in
{
inherit hexecute;
default = hexecute;
}
);

# Wayland libraries
wayland
wayland-protocols
wayland-scanner
libxkbcommon
devShells = forAllSystems (
system: pkgs:
{
default = pkgs.mkShell {
name = "hexecute";

# EGL and OpenGL
libGL
libGLU
mesa
packages =
with pkgs;
[
go
pkg-config
gcc
]
++ pkgs.lib.optionals (!pkgs.stdenv.isDarwin) [
# Wayland libraries
wayland
wayland-protocols
wayland-scanner
libxkbcommon

# Build tools
gcc
];
};
# EGL and OpenGL
libGL
libGLU
mesa
];
};
}
);
};
}
Loading
Loading