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
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Geometry tests
run: node --test tests/*.test.mjs
- name: Validate manifest
run: node -e 'const m=require("./manifest.json"); for (const k of ["schemaVersion","id","name","version","kinds","entryPoints"]) if (!(k in m)) throw new Error("manifest missing " + k); for (const f of Object.values(m.entryPoints)) require("fs").accessSync(f)'
- name: Lua syntax
run: |
sudo apt-get install -y --no-install-recommends lua5.4 >/dev/null
luac5.4 -p src/lua/mousefirst.lua
- name: QML syntax
run: |
sudo apt-get install -y --no-install-recommends qt6-declarative-dev-tools >/dev/null
out=$(for f in $(git ls-files '*.qml'); do /usr/lib/qt6/bin/qmllint "$f" 2>&1; done | grep '\[syntax\]' || true)
if [ -n "$out" ]; then echo "$out"; exit 1; fi
- name: Shell scripts
run: shellcheck scripts/*.sh
2,615 changes: 36 additions & 2,579 deletions BarWidget.qml

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Changelog

## 0.2.0

A rewrite of the plugin's internals. Every v0.1 feature is still there, and your existing `~/.config/omarchy/mouse-first.json` keeps working.

### Fixed
- The controls now stay attached to the window during <kbd>SUPER</kbd> + drag, border resizes and keyboard moves.
- Pulling a snapped or maximized window out with <kbd>SUPER</kbd> + drag no longer detaches it from the cursor. The window returns to its previous size when you drop it.
- Dragging a snapped or maximized window from the top strip restores it immediately, with the grabbed point staying under the cursor.
- Maximize and restore no longer race Hyprland, so the button icon always shows the real state. Windows maximized with Hyprland's own keybind are recognized.
- On multi-monitor setups the controls, snapping and dragging now work on every monitor, and windows can be dragged between monitors. Previously each monitor drew its own copy of the controls, positioned in the wrong coordinates.
- Snapping now respects the bar and dock on every side and uses your Hyprland gaps. Reserved space was previously hard-coded.
- "Disable tiling" now uses a window rule, so windows open floating instead of jumping from tiled to floating. Turning it off takes effect right away.
- Opening a special workspace, such as the Omarchy scratchpad, no longer steals focus.
- Minimized windows now use `special:minimized` and appear in omadock. Windows restored after a shell restart return to the workspace they came from, instead of workspace 1.
- The settings file is written atomically and no longer breaks on special characters.
- Memory used for closed windows is now released.

### Added
- Snapping, snap preview and immediate un-snapping also work with <kbd>SUPER</kbd> + drag and app title bars. The plugin takes over drags that Hyprland starts on floating windows.
- Resizing a snapped window by its inner border resizes its snapped neighbour too.
- Bar icons group the windows of each app, with a window count; clicking cycles through them. A right-click menu lists the windows and offers *New window*, *Pin/Unpin* (shared with omadock) and *Close*.
- New options:
- drag to the top edge to maximize;
- center new windows (on or off);
- built-in snapping shortcuts (<kbd>SUPER</kbd> + <kbd>CTRL</kbd> + <kbd>SHIFT</kbd> + arrows, off by default);
- hide the controls for chosen apps.
- The controls are skipped on picture-in-picture players and windows too narrow for them.
- Spanish translation.
- The icon index is cached on disk and rebuilt only when an icon is missing.

### Changed
- The plugin is now a shell service plus a small bar widget. It is event-driven: no polling and no helper scripts. The seven `bin/` scripts (Python/Bash) are gone.
- Colors come from Omarchy's theme API instead of parsing `colors.toml` every five seconds.
- Added a language selector (English/Português) to the settings panel.
- Added IPC actions for keyboard shortcuts (`snap`, `toggleMaximize`, `minimize`, `restoreLast`, `settings`, `status`).
- Removed `install.sh`. Install with `omarchy plugin add`. The plugin no longer writes to `~/.config/hypr/looknfeel.lua`.

## 0.1.0

- Initial release.
194 changes: 105 additions & 89 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,140 +1,156 @@
<h1 align="center">Mouse-First</h1>

<p align="center">
<b>Intuitive, mouse-driven window management designed for Hyprland and Omarchy Quattro.</b>
<b>Mouse-driven window management for Hyprland on Omarchy.</b>
</p>

<p align="center">
<img src="preview.png" alt="Mouse-First window controls on Omarchy" width="100%">
</p>

Mouse-First brings traditional mouse-friendly desktop convenience to Hyprland's dynamic tiling and floating environment. It gives every window intuitive floating controls, an edge-drag zone for effortless repositioning and un-maximizing, full-screen and quarter-screen edge snapping with dock detection, and a system bar area that neatly hosts minimized applications.

> Designed for users who love Hyprland's aesthetics and speed, but prefer the comfort, muscle memory, and flexibility of mouse-driven workflows.
Mouse-First adds the desktop conveniences mouse users expect to Hyprland: minimize, maximize and close buttons on the active window, dragging windows by their top edge, edge and corner snapping, and app icons on the system bar. Keyboard workflows keep working as before.

<p align="center">
<img src="assets/02.png" alt="Mouse-First workflow overview" width="100%">
</p>

---

## Features

- **Floating Window Controls**: Sleek, theme-aware minimize, maximize/restore, and close buttons integrated directly with the active window.
- **Top-Edge Drag Zone**: Grab, move, and un-maximize windows naturally by clicking and dragging anywhere along the top edge.
- **Smart Edge Snapping**: Half-screen edge snapping (left and right) and quarter-screen corner snapping (top-left, top-right, bottom-left, bottom-right).
- **Dock & Monitor Detection**: Intelligently honors dock reservations (like OmaDock) and multi-monitor boundaries when tiling and snapping.
- **System Bar Minimization**: Minimized windows are neatly organized directly into the top bar with their real application icons and titles for 1-click restore.
- **Native Wayland Socket IPC**: Direct UNIX domain socket communication with Hyprland for zero input lag, smooth dragging, and high performance.
- **Customizable**: Configure button visibility, order, bar alignment, snapping behavior, and colors via an intuitive right-click context settings menu.

---
- **Window controls.** A small capsule at the top-right corner of the active window with minimize, maximize/restore and close buttons (plus an optional float/tile button). You can reorder the buttons, hide any of them, and drag the capsule to a different spot.
- **Top-edge dragging.** Grab any window by the strip along its top edge and move it, including onto another monitor. Double-click the strip to maximize or restore.
- **Snapping.** Drop a window against a screen edge to fill half of the screen, or into a corner for a quarter. Snapped and maximized windows respect the bar, the dock and your Hyprland gaps.
- **Snapping everywhere.** Snapping and natural un-snapping work the same whether you drag by the top strip, with <kbd>SUPER</kbd> + drag, or by an app's own title bar: a snapped or maximized window returns to its previous size the moment you pull it away, staying under the cursor.
- **Split-view resizing.** Resize one of two snapped windows by its inner border and the neighbour follows, so they keep sharing the screen.
- **App icons on the bar.** Pinned and open apps appear on the system bar, one icon per app with a window count. Click to launch, focus, minimize, restore or cycle through an app's windows; right-click for a menu with its windows, *New window*, *Pin/Unpin* (shared with omadock) and *Close*. Minimized windows are shared with omadock.
- **Stays out of the way.** No controls on small windows, picture-in-picture players, or apps you exclude.
- **Floating by default (optional).** Every window opens floating, like a traditional desktop, centered or where the app chooses.
- **Optional snapping shortcuts.** <kbd>SUPER</kbd> + <kbd>CTRL</kbd> + <kbd>SHIFT</kbd> with the arrow keys.
- **English, Português and Español.**
- **Theme-aware.** Follows the Omarchy theme, or uses your own colors.
- **Lightweight.** Event-driven with no polling, and it doesn't spawn processes while you work.

## Requirements

- **Omarchy Quattro** (or any modern Hyprland installation with Quickshell)
- **Quickshell** `0.3.x`
- **Hyprland** (with Wayland socket IPC)
- **Dependencies**: `jq`, `socat`
- Omarchy 4 with its Quickshell-based shell.
- Hyprland **0.55 or newer** with a Lua configuration (the Omarchy default).

---
No extra packages are needed.

## Installation

### Via Official Omarchy Plugin CLI (Recommended)

To install directly using the Omarchy plugin manager:

```bash
omarchy plugin add https://github.com/DjaboDev/Mouse-First.git --enable
omarchy restart shell
```

### Via Quick Install Script

Alternatively, you can clone and run the provided interactive installer:

```bash
git clone https://github.com/DjaboDev/Mouse-First.git
cd Mouse-First
bash install.sh
```

The script will ask where you'd like minimized applications placed in the system bar (`left`, `center`, or `right`) and automatically register the plugin.

---
The plugin is also listed in the [Omarchy plugin store](https://plugins.omarchy.org/plugin.html?id=io.github.mousefirst.controls).

## Usage

- **Drag to Move**: Click and hold the top edge of any floating or tiled window to drag it across workspaces or monitors.
- **Un-maximize**: Dragging a maximized window away from the top edge smoothly un-maximizes it, keeping the window attached to your cursor.
- **Edge Snapping**: Drag a window into the left or right screen border to snap it to half screen. Touch the screen corners to snap into quarters. Pulling the window away cleanly restores its original floating size.
- **Minimize to Bar**: Click the minimize button (`—`) to send the window to the system bar. Click the app icon in the bar to bring it back into focus.
- **Settings & Preferences**: Right-click the floating control capsule on the active window to open the settings panel (toggle buttons, color theme, and drag behaviors).
| Action | How |
| --- | --- |
| Move a window | Drag the strip along its top edge (or the optional ⠿ grip) |
| Maximize / restore | Maximize button, or double-click the top strip |
| Snap | Drag the window (top strip, <kbd>SUPER</kbd> + drag or title bar) against an edge for half the screen, or into a corner for a quarter. Optionally, the top edge maximizes |
| Un-snap / un-maximize | Drag the window away from its snapped position |
| Resize two snapped windows together | Drag the border they share |
| Minimize | Minimize button, or click the focused app's icon on the bar |
| Restore | Click the app's icon on the bar (or in omadock) |
| Cycle an app's windows | Click its bar icon repeatedly |
| App menu (windows, new window, pin, close) | Right-click its bar icon |
| Close a window from the bar | Middle-click its icon |
| Move the control capsule | Drag the thin handle under the buttons; double-click it to reset |
| Settings | Right-click the control capsule |

### Keyboard shortcuts (optional)

Turn on **Enable snapping shortcuts** in the settings for built-in bindings:

| Keys | Action |
| --- | --- |
| <kbd>SUPER</kbd> + <kbd>CTRL</kbd> + <kbd>SHIFT</kbd> + <kbd>←</kbd> / <kbd>→</kbd> | Snap to the left / right half |
| <kbd>SUPER</kbd> + <kbd>CTRL</kbd> + <kbd>SHIFT</kbd> + <kbd>↑</kbd> | Maximize or restore |
| <kbd>SUPER</kbd> + <kbd>CTRL</kbd> + <kbd>SHIFT</kbd> + <kbd>↓</kbd> | Restore a snapped/maximized window, otherwise minimize |

For other keys, every action is also available over Quickshell IPC, so you can bind it yourself in `~/.config/hypr/bindings.lua`:

```lua
local mf = "qs -p /usr/share/omarchy/shell ipc call mousefirst "
o.bind("SUPER + ALT + LEFT", "Snap left", mf .. "snap left")
o.bind("SUPER + ALT + RIGHT", "Snap right", mf .. "snap right")
o.bind("SUPER + ALT + UP", "Maximize / restore", mf .. "toggleMaximize")
o.bind("SUPER + ALT + DOWN", "Minimize", mf .. "minimize")
```

---
| Function | Description |
| --- | --- |
| `snap <zone>` | `left`, `right`, `top`, `bottom`, `top-left`, `top-right`, `bottom-left`, `bottom-right`, `maximize` |
| `toggleMaximize` | Maximize or restore the active window |
| `restore` | Restore the active window from a snap or maximize |
| `minimize` / `restoreLast` | Minimize the active window / restore the most recently minimized one |
| `settings` | Open or close the settings panel |
| `status` | Print diagnostic state as JSON (useful for bug reports) |

## Configuration

Mouse-First saves your preferences to:
Settings are changed from the settings panel and saved to `~/.config/omarchy/mouse-first.json`. You can also edit the file by hand; changes apply immediately.

| Key | Default | Description |
| --- | --- | --- |
| `language` | from `$LANG` | `en`, `pt` or `es` |
| `disableTiling` | `true` | Open every window floating |
| `centerNewWindows` | `true` | Center new floating windows (with `disableTiling`) |
| `alwaysVisible` | `true` | Keep the control capsule visible (otherwise it appears on hover) |
| `dragFullWidth` | `true` | Enable the top-edge drag strip |
| `showDragHandle` | `false` | Show a ⠿ drag grip in the capsule |
| `disableSnapping` | `false` | Turn edge snapping off |
| `topEdgeMaximizes` | `false` | Dropping on the top edge maximizes instead of snapping to the top half |
| `enableShortcuts` | `false` | Register the built-in snapping shortcuts |
| `excludedClasses` | `[]` | Window classes that never get controls |
| `minControlsWidth` | `240` | Windows narrower than this (px) get no controls |
| `ignoreDock` | `false` | Let snapped and maximized windows cover the dock area |
| `minimizeToBar` | `true` | Show app icons on the bar (when off, they appear only if no dock is running) |
| `barSection` | `left` | Bar section for the icons (`left`, `center`, `right`) |
| `enableMenuDrag` | `true` | Allow moving the control capsule |
| `rememberMenuPerWindow` / `rememberMenuGlobal` | `false` | Keep the capsule position per application, or use one position for all windows |
| `alwaysShowMenuDragZone` | `false` | Always show the capsule's move handle |
| `buttonOrder` / `buttonVisible` | | Order and visibility of `float`, `minimize`, `maximize` and `close` |
| `colorMode` | `theme` | `theme` follows Omarchy; `custom` uses `customBg`, `customFg`, `customAccent` and `customRed` |

## How it works

- **`Service.qml`** runs once per shell. It owns the state and draws one transparent overlay per monitor. The overlay only takes input on the controls and the drag strip.
- **`src/lua/mousefirst.lua`** is loaded into Hyprland with `hyprctl eval`. It watches the active window and the monitor layout from inside the compositor and reports changes as custom IPC events, so the shell never polls. It is reloaded automatically after `hyprctl reload`.
- **Native drags are handed over.** Two non-consuming binds track the left mouse button (Hyprland's Lua API cannot read it). When Hyprland starts moving a floating window, the module ends Hyprland's drag and the shell's drag controller continues it, so every drag shares the same snapping logic.
- A note for Lua config authors: in Hyprland 0.56.2, calling a method on an `HL.Keybind` whose bind no longer exists crashes the compositor, and `:remove()` drops every bind with the same key and modifiers. The module therefore never calls methods on keybind handles.
- **`src/js/geometry.js`** holds the snapping and placement math as pure functions, covered by the tests in `tests/` (which also check that every translation is complete).
- **`BarWidget.qml`** only renders the app icons. Omarchy creates one per monitor, and all of them share the service.

Minimized windows live on `special:minimized`, the same workspace omadock uses, and each one's original workspace is remembered in `~/.local/state/mouse-first/`.

## Development

```text
~/.config/omarchy/mouse-first.json
```

A typical configuration file:

```json
{
"language": "en",
"disableTiling": true,
"alwaysVisible": true,
"dragFullWidth": true,
"showDragHandle": false,
"ignoreDock": false,
"disableSnapping": false,
"enableMenuDrag": true,
"minimizeToBar": true,
"barSection": "left",
"buttonOrder": ["float", "minimize", "maximize", "close"],
"buttonVisible": {
"float": false,
"minimize": true,
"maximize": true,
"close": true
},
"colorMode": "theme"
}
```bash
git clone https://github.com/DjaboDev/Mouse-First.git
cd Mouse-First
scripts/dev-link.sh # symlink the checkout over the installed plugin
omarchy restart shell # services reload only on a shell restart
node --test tests/*.test.mjs # geometry tests
```

---

## Updating & Removal

### Update
## Updating & removal

```bash
omarchy plugin update io.github.mousefirst.controls
omarchy restart shell
```

### Removal

To remove the plugin from your system:

```bash
omarchy plugin remove io.github.mousefirst.controls --yes
omarchy restart shell
rm -rf ~/.config/omarchy/mouse-first.json ~/.local/state/mouse-first
```

To clean up configuration files after removal:

```bash
rm -f ~/.config/omarchy/mouse-first.json
```

---

## License

This project is licensed under the [MIT License](LICENSE).
[MIT](LICENSE)
Loading
Loading