Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7681302
chore: remove empty test-monitors.js
KonstantinMerkel Jun 1, 2026
4a01376
docs: create CONTRIBUTING.md and refine AGENTS.md
KonstantinMerkel Jun 4, 2026
6bc577e
Refactor/Restructure documentation (#14)
MAkexander Jun 4, 2026
6c468f0
refactor(keybindings): transition to native Mutter intercept and SRP …
KonstantinMerkel Jun 10, 2026
29ad424
Makefile reminders for logging out
KonstantinMerkel Jun 10, 2026
6942c88
fix(drag): prevent out of bounds matrix access
KonstantinMerkel Jun 11, 2026
aa4d6a9
bugfix: ensures no old signals survive a disablement of the extension
KonstantinMerkel Jun 16, 2026
41b3ea3
bugfix: implemented Expected State Cache to avoid race-conditions
KonstantinMerkel Jun 7, 2026
c3482fb
bugfix: prevent infinite Wayland loop during Expected State Cache res…
KonstantinMerkel Jun 10, 2026
1a8c757
premature: changes, should amend this commit once working
KonstantinMerkel Jun 11, 2026
d126a77
feat: Implement robust cross-monitor drag and tiling transitions - fi…
KonstantinMerkel Jun 12, 2026
c5c5a41
gitignore .agents
KonstantinMerkel Jun 14, 2026
1cd9b31
refactor: first iteration
KonstantinMerkel Jun 14, 2026
1a63de0
refactor: 2 iteration
KonstantinMerkel Jun 14, 2026
30408bc
bugfix: resolve floating window drag regressions and overtiling
KonstantinMerkel Jun 14, 2026
06c8910
fix: adversarial tests mock wrappers
KonstantinMerkel Jun 17, 2026
52fb765
refactor: iteration 3 outsourcing and function unbloating
KonstantinMerkel Jun 17, 2026
d7201b4
fix: window move deferral, geometry overlap calculation, and add stat…
KonstantinMerkel Jun 17, 2026
836f18c
bugfix: missing monitor notification
KonstantinMerkel Jun 19, 2026
78c4a56
feat: phase 1 - create keyboard shortcuts tab
KonstantinMerkel Jun 11, 2026
cc09186
feat: phase 2 - integrate native shortcuts and refine UI
KonstantinMerkel Jun 11, 2026
1471a0f
fix: tests
KonstantinMerkel Jun 19, 2026
ae4b972
first working iteration maximisation rework
KonstantinMerkel Jun 8, 2026
265873a
test: add unit tests for override features and fix mocks
KonstantinMerkel Jun 10, 2026
db367e0
Fix window.test.js test failures due to mock leaks
KonstantinMerkel Jun 17, 2026
02bf3cf
bugfix: missing singal window wrapper
KonstantinMerkel Jun 19, 2026
e5de076
bugfix: cross monitor window transition with max escalator overtiling…
KonstantinMerkel Jun 19, 2026
127f9e9
UI simplification: Maximisation Shortcuts
KonstantinMerkel Jun 19, 2026
7771cf9
bugfix: Floating windows dropped outside layout slots properly remain…
KonstantinMerkel Jun 19, 2026
95056c7
refactor: Fixing highest prio potential bugs
KonstantinMerkel Jun 22, 2026
7313ea7
refactor: Shadow restore and workspace transactions
KonstantinMerkel Jun 22, 2026
3f4c43a
refactor: deleted dead code and minor refactorings
KonstantinMerkel Jun 22, 2026
f9e28fa
bugfix: switching monitors leading to layout scramble
KonstantinMerkel Jun 22, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ CLAUDE.md
CODEX.md
CURSOR.md
GEMINI.md
.agents
coverage/
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Workflow Tiling AI Development Guidelines

## Role & Mission
You are an AI developer assisting with the Workflow Tiling GNOME Shell extension.
You are a useful AI developer assisting with the Workflow Tiling GNOME Shell extension.
For this purpose, you will find general project-level guidelines here.
Please also refer to your developer's exclusive instruction file (e.g., `CLAUDE.md`, `CODEX.md`, `CURSOR.md`, or `GEMINI.md`) for detailed instructions on how you can serve them best.

## General Mandates

Expand Down
41 changes: 41 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Development Guidelines for `Workflow Tiling`
Welcome! Thanks for wanting to contribute. If you have any issues please reach out to the maintainer.

## Branch Structure
We follow a structured branching model to keep things organized:

* **`master`**: This is our stable, release-ready branch. Please do not push directly here.
* **`develop`**: This is where new features come together for integration testing. Once the testing phase is complete, `develop` merges into `master` for a new release.
* **`feature/*`, `bugfix/*`, `refactor/*`**: Use these short-lived branches for your daily work. When ready, open a Pull Request into `develop`.
* **`hotfix/*`** (Exception): If `master` breaks, you can create a hotfix branch to merge directly into `master`.

## Issues and Feature Requests
Please report issues and request features under issues.

### AI Friendliness
This project has a project-level `AGENTS.md` set up.
You may have your own requirements in `CLAUDE.md`, `CODEX.md`, `CURSOR.md`, or `GEMINI.md`.
Please make sure to tell your agent to respect both `AGENTS.md` and your local instruction file.

### Settings UI Design
We love a clean UI! For `prefs.js`, follow a **"minimal clutter, expand only after needed"** design philosophy.
Feel free to use `Adw` (libadwaita) components and bind visibility state dynamically to reduce visual noise.

### Logging
Using debuggable logging (`Logger` in `lib/logger.js`) is highly suggested. Including verbose logs for complex state sequences makes troubleshooting much easier for everyone — especially me. Thank you!

### GNOME APIs & Event-Driven Architecture
We aim to use the newest solutions and APIs available for GNOME Shell.
Please rely on GNOME Shell signals (`size-changed`, `window-created`, etc.) or frame-synced deferrals (`GLib.idle_add`, `Meta.LaterType.BEFORE_REDRAW`) instead of arbitrary timeouts (`GLib.timeout_add`).

### Testing
We highly value stability! Please make sure to write unit tests for new features—wherever possible—and run existing tests before proposing a change.
Run them easily using:
```bash
npm test
```

### Commentary & Documentation
We prefer documentation that describes the current state and behavior of the system.
* Try to keep comments focused without diary entries or historical tracking.
* If your changes affect how the system works (class names, execution flow, or API), please help us keep `architecture.md`, `layouts.md`, and `README.md` up-to-date!
19 changes: 15 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,24 @@ UUID = workflow-tiling@konstantin.dev
EXT_DIR = ~/.local/share/gnome-shell/extensions/$(UUID)
FILES = extension.js metadata.json lib/ schemas/ prefs.js

.PHONY: all sync install test pack enable disable clean
.PHONY: all sync install test pack enable disable clean reminder do_sync do_enable

all: sync

sync:
reminder:
@echo ""
@echo "========================================================="
@echo " REMINDER: Log out and log back in for changes to take effect"
@echo "========================================================="
@echo ""

sync: do_sync reminder

do_sync:
mkdir -p $(EXT_DIR)
cp -r $(FILES) $(EXT_DIR)/

install: sync enable
install: do_sync do_enable reminder

test:
npm test
Expand All @@ -19,7 +28,9 @@ pack:
glib-compile-schemas schemas/ 2>/dev/null || true
zip -r extension.zip $(FILES)

enable:
enable: do_enable reminder

do_enable:
gnome-extensions enable $(UUID)

disable:
Expand Down
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
[![Project Status: Beta](https://img.shields.io/badge/status-beta-orange.svg)](https://github.com/KonstantinMerkel/workflow-tiling)

# Workflow Tiling

A deterministic customizable auto-tiler extension for GNOME Shell (GNOME 50+).

## Summary
- [Workflow Tiling](#workflow-tiling)
- [Summary](#summary)
- [Features](#features)
- [Customizations](#customizations)
- [Custom Layouts](#custom-layouts)
- [Development](#development)
- [Contributing](#contributing)
- [Running Tests](#running-tests)
- [Installation](#installation)
- [Recommended Extensions](#recommended-extensions)

## Features
- **Deterministic Escalation**: Automatically tiles windows in a customizable geometric sequence.
- **Multi-Monitor Support**: Each monitor tiles independently.
- **Workspace Isolation**: Tiling states are unique to each GNOME workspace.
- **Stability Focused**: Uses WindowWrapper object modeling and compositor-native synchronization (Meta.LaterType) to prevent race conditions and Shell crashes.

## Recommended Extensions
Workflow Tiling does not natively draw an active window border. For visual indication of the focused window, it is highly recommended to use an extension like **P7 Border**
## Customizations

Customizations like window gaps and keybinds can be customized under `Gnome Extension Manager` > `Workflow Tiling` > Settings.
Please be warned that duplicate shortcuts can lead to unexpected window movement.

## Custom Layouts
### Custom Layouts
Layout transitions are configured via JSON string, supporting custom window counts and sizes.

Optional `id` (1-indexed) integer properties in the JSON structure define how windows transition between states. It is required for all elements:

```json
{
"1": [
Expand All @@ -37,6 +49,9 @@ Optional `id` (1-indexed) integer properties in the JSON structure define how wi

## Development

### Contributing
see [CONTRIBUTING.md](CONTRIBUTING.md)

### Running Tests
Unit tests are written using **Vitest**.
```bash
Expand All @@ -49,3 +64,6 @@ To deploy the extension to your local GNOME Shell directory:
```bash
make install
```

## Recommended Extensions
Workflow Tiling does not natively draw an active window border. For visual indication of the focused window, it is highly recommended to use an extension like [P7 Border](https://github.com/prasannavl/p7-borders-shell-extension)
25 changes: 24 additions & 1 deletion architecture.md → docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,38 @@ Tracks monitor topology.
Detects hotplug events.
Identifies stable monitor IDs.
Manages window evacuation during monitor removal.
Provides directional monitor lookup via `getMonitorInDirection()` for cross-monitor transitions.

## SignalListener (`lib/signals.js`)
Binds GNOME Shell signals.
Intercepts `window-created`, `window-entered-monitor`, `window-left-monitor`.
Intercepts drag operations via `grab-op-begin` and `grab-op-end`.
Binds global keyboard shortcuts.
Translates events to `TilingController` calls.

## KeybindingManager (`lib/keybindings.js`)
Binds global keyboard shortcuts.
Hijacks hardcoded native GNOME shortcuts via C-handlers (`default` mode).
Delegates custom shortcut conflict resolution to `ShadowManager`.
Translates keyboard events to `TilingController` actions.

## ShadowManager (`lib/shadows.js`)
Implements Dynamic Schema Shadowing.
Scans GNOME native schemas for custom shortcut conflicts.
Temporarily unbinds conflicting native keys to allow `Main.wm.addKeybinding` to succeed.
Persists original keys in `shadowed-keybindings` state for perfect restoration on disable.

## WindowWrapper (`lib/window.js`)
Encapsulates `Meta.Window`.
Applies calculated geometry.
Binds single-shot `size-changed` signals to detect external resizing.

## WorkspaceManager & WorkspaceLayout (`lib/workspace.js`)
`WorkspaceManager` tracks multiple layouts across GNOME workspaces.
`WorkspaceManager` provides batch monitor operations (close, switch, port to workspace).
`WorkspaceLayout` tracks windows per workspace and monitor.
Calculates window slots based on insertion order.
Provides window displacement and swapping logic.
Handles cross-monitor window transitions via configurable swap or escalate behavior.

## StateTracker (`lib/state.js`)
Maintains stable ordered list of windows.
Expand All @@ -42,10 +56,12 @@ Swaps window positions.
Tracks window drag-and-drop operations.
Renders visual swap indicators.
Triggers geometric swapping based on pointer intersections.
Handles cross-monitor drag transitions with visual previews and deferred retiles.

## SettingsManager (`lib/settings.js`)
Loads configuration preferences.
Parses layout JSON into valid escalator transitions.
Exposes monitor transition behavior configuration.

## Logger (`lib/logger.js`)
Provides debug and trace logging.
Expand All @@ -54,6 +70,7 @@ Configurable output verbosity.
## Escalator (`lib/layout.js`)
Generates tile geometries.
Provides geometric estates based on current window count.
Provides edge-adjacent slot lookup via `getEdgingSlot()` for directional transitions.

## Execution Flow
Signal triggers event.
Expand All @@ -62,3 +79,9 @@ Controller updates WorkspaceLayout state.
Controller schedules deferred retile.
Retile queries Escalator for layouts.
Retile invokes WindowWrapper to apply geometries.

## Cross-Monitor Flow
Keyboard/drag triggers direction detection.
Controller/DragManager delegates to WorkspaceLayout.
WorkspaceLayout escalates or swaps window between monitor trackers.
Controller schedules retile on both monitors.
26 changes: 26 additions & 0 deletions docs/keybindings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Keybindings & Schema Shadowing

The extension uses two different strategies to bind shortcuts due to GNOME Mutter architecture limitations:

## 1. Native C-Handler Hijacking (`Meta.keybindings_set_custom_handler`)
Used strictly in `Default` mode for known GNOME shortcuts (like `<Super>Left` which GNOME maps to `toggle-tiled-left`).
* **Why:** High priority. Safely intercepts the GNOME action at the C-level before Mutter can process it.
* **Limitation:** Requires knowing the hardcoded GNOME string action name (e.g. `toggle-tiled-left`). It cannot intercept an arbitrary custom keystroke without knowing what action it triggers.

## 2. Extension Bindings (`Main.wm.addKeybinding`)
Used for all `Custom` modes and utility shortcuts.
* **Why:** Allows binding to custom `gsettings` schema keys that the user configures.
* **Limitation:** Extremely low priority. If the user picks a key (e.g., `<Super><Alt>Down`) that Mutter already listens to globally (e.g., `shift-overview-down`), Mutter consumes the event. The extension never fires.

## Dynamic Schema Shadowing (`ShadowManager`)
To bypass the limitation of `Main.wm.addKeybinding`, `ShadowManager` temporarily deletes conflicting shortcuts from GNOME settings while the extension is active.

### Execution Flow:
1. **Normalize:** Target custom keystrokes are parsed via `Gtk.accelerator_parse` to normalize modifier ordering (`<Alt><Super>` vs `<Super><Alt>`).
2. **Scan:** Iterates through native schemas (`wm.keybindings`, `mutter.keybindings`, `shell.keybindings`).
3. **Filter:** If a native array contains the normalized shortcut string, it is explicitly filtered out.
4. **Backup:** The *original* array is saved into `org.gnome.shell.extensions.workflow-tiling.shadowed-keybindings` (JSON string).
5. **Restore:** On `disable()` or `rebindAll()`, the JSON backup is read, and the native schema keys are written back to their exact original arrays.

### Crash Resilience
Because the backup is written to standard `gsettings` before any keys are unbound, an unexpected shell crash will not permanently destroy native user shortcuts. The state is restored perfectly on the next instantiation.
File renamed without changes.
2 changes: 1 addition & 1 deletion vision.md → docs/vision.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Layout transitions are predictable, minimal, and fully customizable. The system
- **Error Shielding**: Every Mutter API interaction is wrapped in defensive checks and error handling to prevent Shell crashes.

### Scaling & Isolation
- **Multi-Monitor Support**: Each monitor maintains an independent tiling state and respects its own work area/resolution.
- **Multi-Monitor Support**: Each monitor maintains an independent tiling state and respects its own work area/resolution. Monitors support cross-monitor window transitions via keyboard and drag, with configurable swap/escalate behavior.
- **Workspace Isolation**: Tiling is scoped per GNOME workspace.

## Future Roadmap
Expand Down
47 changes: 45 additions & 2 deletions extension.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
import Gio from 'gi://Gio';
import { TilingController } from './lib/controller.js';
import { SignalListener } from './lib/signals.js';
import { SettingsManager } from './lib/settings.js';
import { Logger } from './lib/logger.js';
import { KeybindingManager } from './lib/keybindings.js';

/**
* Main extension class. Manages controller and signals.
Expand All @@ -16,18 +18,33 @@ export default class WorkflowTilingExtension extends Extension {
this._settings = new SettingsManager(this);
this._controller = new TilingController(this._settings);
this._signals = new SignalListener(this._controller);
this._keybindings = new KeybindingManager(this._controller);

this._isActive = false;
this._wasSuspended = false;

this._isActive = false;
this._wasSuspended = false;

try {
this._wmSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.wm.preferences' });
let currentLayout = this._wmSettings.get_string('button-layout');
if (currentLayout.includes('maximize')) {
let newLayout = currentLayout.replace(/maximize,?/g, '').replace(/,maximize/g, '');
this._wmSettings.set_string('button-layout', newLayout);
}
} catch (e) {
Logger.warn('Failed to hide maximize button', e);
}

this._settings.onSettingsChanged = () => {
if (this._applyCustomLayouts()) {
this._controller.hydrate();
}
};

this._settings.onKeybindingsChanged = () => {
if (this._isActive) this._signals.rebindKeybindings();
if (this._isActive) this._keybindings.rebindAll();
}

this._applyCustomLayouts();
Expand All @@ -46,6 +63,7 @@ export default class WorkflowTilingExtension extends Extension {
Main.notifyError('Workflow Tiling', `Invalid layouts JSON. Suspending extension.\n${e.message}`);
if (this._isActive) {
this._signals.unbind();
this._keybindings.unbindAll();
this._controller.clear();
this._isActive = false;
this._wasSuspended = true;
Expand All @@ -57,6 +75,7 @@ export default class WorkflowTilingExtension extends Extension {

if (!this._isActive) {
this._signals.bind();
this._keybindings.bindAll();
this._isActive = true;
if (this._wasSuspended) {
Main.notify('Workflow Tiling', 'Valid layout provided. Extension resumed.');
Expand All @@ -68,13 +87,37 @@ export default class WorkflowTilingExtension extends Extension {

disable() {
Logger.info(`Disabling ${this.metadata.name}`);
if (this._isActive) this._signals.unbind();
if (this._isActive) {
this._signals.unbind();
this._keybindings.unbindAll();
}
if (this._settings) this._settings.destroy();
if (this._controller) this._controller.clear();
this._signals = null;
this._keybindings = null;
this._settings = null;
this._controller = null;
this._isActive = false;
this._wasSuspended = false;

if (this._wmSettings) {
try {
let layout = this._wmSettings.get_string('button-layout');
if (!layout.includes('maximize')) {
if (layout.includes('minimize,close')) {
layout = layout.replace('minimize,close', 'minimize,maximize,close');
} else if (layout.includes('close')) {
layout = layout.replace('close', 'maximize,close');
} else {
layout += ',maximize';
}
this._wmSettings.set_string('button-layout', layout);
Gio.Settings.sync();
}
} catch (e) {
Logger.error('Failed to restore maximize button', e);
}
this._wmSettings = null;
}
}
}
Loading
Loading