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
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/
12 changes: 12 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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.
Expand All @@ -40,9 +41,11 @@ 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 @@ -53,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 @@ -65,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 @@ -73,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.
2 changes: 1 addition & 1 deletion 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
60 changes: 50 additions & 10 deletions lib/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Logger } from './logger.js';
import { MonitorManager } from './monitor.js';
import { DragManager } from './drag.js';
import { TILABLE_WINDOW_TYPES } from './signals.js';
import { getEnteringEdge } from './utils/geometry.js';

/**
* TilingController: The central orchestration layer.
Expand Down Expand Up @@ -88,6 +89,11 @@ export class TilingController {
wrapper.bindSizeChanged();

try {
if (this.dragManager && this.dragManager.isWindowInDragPreview(window)) {
Logger.debug(`tilingRequest: Ignored for window in active drag preview.`);
return;
}

const context = this._resolveTilingContext(window, wrapper);
if (!context) {
Logger.debug(`tilingRequest: Aborted. No context resolved.`);
Expand All @@ -103,18 +109,52 @@ export class TilingController {
return;
}

const oldSlot = this._handleWorkspaceChange(window, wrapper, workspace, monitorId);
const finalPreferredSlot = isRestoring ? preferredSlot : (oldSlot !== undefined ? oldSlot : undefined);
this._updateWrapperCache(wrapper, workspace, monitorIndex, monitorId);
this._applyTrackingState(window, monitorId, workspace, isRestoring, finalPreferredSlot);

Logger.debug(`tilingRequest: State applied. Scheduling retile.`);
this._scheduleRetile(workspace, monitorId, monitorIndex);
const layout = this.workspaceManager.getLayout(workspace);
const isMonitorChange = wrapper.workspace && wrapper.workspace === workspace && wrapper.monitorId && wrapper.monitorId !== monitorId;

if (isMonitorChange) {
this._handleMonitorTransitionChange(window, wrapper, layout, wrapper.monitorIndex, wrapper.monitorId, monitorIndex, monitorId, workspace);
} else {
this._handleNormalTilingRequest(window, wrapper, workspace, monitorId, monitorIndex, isRestoring, preferredSlot);
}
} catch (e) {
Logger.warn(`Tiling attempt failed for "${wrapper ? wrapper.title : 'unknown'}"`, e);
}
}

_handleMonitorTransitionChange(window, wrapper, layout, sourceMonitorIndex, sourceMonitorId, monitorIndex, monitorId, workspace) {
const slot = layout.getWindowSlot(sourceMonitorId, window);
const sourceSlot = slot !== undefined ? slot : 0;

const sourceRect = global.display.get_monitor_geometry(sourceMonitorIndex);
const targetRect = global.display.get_monitor_geometry(monitorIndex);

const enteringEdge = getEnteringEdge(sourceRect, targetRect);

const result = layout.handleMonitorTransition(window, sourceMonitorId, monitorId, enteringEdge, sourceSlot);

this.updateWindowWrapperMonitor(window, monitorId, monitorIndex);

if (result && result.swappedWindow) {
this.updateWindowWrapperMonitor(result.swappedWindow, sourceMonitorId, sourceMonitorIndex);
if (result.swappedWindow.move_to_monitor) result.swappedWindow.move_to_monitor(sourceMonitorIndex);
}

Logger.debug(`tilingRequest: Monitor transition handled. Scheduling retile.`);
this._scheduleRetile(workspace, sourceMonitorId, sourceMonitorIndex);
this._scheduleRetile(workspace, monitorId, monitorIndex);
}

_handleNormalTilingRequest(window, wrapper, workspace, monitorId, monitorIndex, isRestoring, preferredSlot) {
const oldSlot = this._handleWorkspaceChange(window, wrapper, workspace, monitorId);
const finalPreferredSlot = isRestoring ? preferredSlot : (oldSlot !== undefined ? oldSlot : undefined);
this._updateWrapperCache(wrapper, workspace, monitorIndex, monitorId);
this._applyTrackingState(window, monitorId, workspace, isRestoring, finalPreferredSlot);

Logger.debug(`tilingRequest: State applied. Scheduling retile.`);
this._scheduleRetile(workspace, monitorId, monitorIndex);
}

_ensureWrapper(window) {
const type = window.get_window_type ? window.get_window_type() : Meta.WindowType.NORMAL;
const skipTaskbar = window.is_skip_taskbar ? window.is_skip_taskbar() : false;
Expand Down Expand Up @@ -363,19 +403,19 @@ export class TilingController {
}

closeMonitorWindows(monitorIndex, includeMinimized) {
this.monitorManager.closeMonitorWindows(monitorIndex, includeMinimized);
this.workspaceManager.closeMonitorWindows(monitorIndex, includeMinimized);
}

closeWorkspaceWindows(workspace) {
this.workspaceManager.closeWorkspaceWindows(workspace);
}

switchMonitors(activeMonitorIndex) {
this.monitorManager.switchMonitors(activeMonitorIndex);
this.workspaceManager.switchMonitors(activeMonitorIndex);
}

portMonitorToWorkspace(monitorIndex, direction) {
this.monitorManager.portMonitorToWorkspace(monitorIndex, direction);
this.workspaceManager.portMonitorToWorkspace(monitorIndex, direction);
}

unminimizeWorkspace(workspace) {
Expand Down
Loading
Loading