Skip to content
Merged
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
File renamed without changes.
File renamed without changes.
File renamed without changes
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
result/
hexecute
# go build -o bin ./...
bin/

# nix build (produces a symlink)
result
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -58,8 +58,9 @@ If you have [Nix](https://nixos.org/) installed, simply run `nix build`.

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

## Usage
Expand Down
214 changes: 214 additions & 0 deletions cmd/hexecute/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
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
}

Comment on lines +28 to +31
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] This wrapper type is declared but not used in the file; consider removing it to keep the entrypoint lean.

Suggested change
type App struct {
*models.App
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finally, accurate information from AI! The issue has been resolved in this PR, though it currently doesn’t affect the project.

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 {
Comment on lines +42 to +47
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The local variable gestures shadows the imported package name 'gestures', which reduces readability and can lead to subtle mistakes. Consider renaming the variable to 'gs' or 'loadedGestures'.

Copilot uses AI. Check for mistakes.
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))
}
Comment on lines +152 to +154
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The local variable spawn shadows the imported package 'spawn'. Rename the variable (e.g., 'spawner') to avoid confusion; the same pattern appears again when spawning cursor sparkles.

Copilot uses AI. Check for mistakes.
}
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))
Comment on lines +205 to +206
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Same shadowing as above; rename the local variable to avoid masking the package import.

Copilot uses AI. Check for mistakes.
}

update.UpdateParticles(dt)
drawer := draw.New(app)
drawer.Draw(window)
window.SwapBuffers()
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module hexecute
module github.com/ThatOtherAndrew/Hexecute

go 1.25.1

Expand Down
18 changes: 18 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading