Skip to content

Instantly share code, notes, and snippets.

@rcarmo
Last active October 12, 2025 20:12
Show Gist options
  • Select an option

  • Save rcarmo/5132874cdaf2755e42907508802e864f to your computer and use it in GitHub Desktop.

Select an option

Save rcarmo/5132874cdaf2755e42907508802e864f to your computer and use it in GitHub Desktop.
Phoenix configuration
// ********************************************************************************
// Setup and TODO
// ********************************************************************************
Phoenix.set({
daemon: false,
openAtLogin: true
});
/*
* API docs: https://kasper.github.io/phoenix/
*
* TODO:
* - [ ] Drag and Drop Snap (can't be implemented in current Phoenix, apparently)
* - [X] Move mode
* - [x] Move to adjacent window
* - [x] More logical handling of sixths
* - [x] Hint Manager class
* - [x] Cleanup Constants
* - [x] Basic Space handling
* - [x] Move mouse pointer on rotation focus changes
* - [x] Centered popup
* - [x] Frame abstraction
*/
// ********************************************************************************
// Constants & Feature Flags
// ********************************************************************************
const DEBUG = false; // Toggle verbose logging & optional debug helpers.
/**
* Direction identifiers used throughout layout, navigation and tiling logic.
* Semantic (capitalized) strings are used instead of single-letter aliases for clarity.
*/
const DIR = Object.freeze({
NONE: "none",
COLS: "Cols",
FULL: "Full",
NORTH: "North",
SOUTH: "South",
EAST: "East",
WEST: "West",
NW: "North-West",
NE: "North-East",
SW: "South-West",
SE: "South-East"
});
/**
* Layout related configuration.
* MODES enumerates the available tiling strategies applied per-screen in rotation.
*/
const LAYOUT = Object.freeze({
MODES: [DIR.NONE, DIR.EAST, DIR.WEST, DIR.COLS, 'CenterRing', 'CenterColumn'] // Added CenterColumn: 40% wide central column
});
/**
* Hint system tuning.
* CHARS defines the selection alphabet; ICON batching avoids UI stalls on large window counts.
*/
const HINT = Object.freeze({
APPEARANCE: "dark",
CANCEL_KEY: "escape",
CHARS: "FJDKSLAGHRUEIWOVNCM",
LAZY_ICONS: true,
ICON_BATCH_DELAY: 0.015,
ICON_BATCH_SIZE: 12,
MAX_OVERLAP_ADJUST: 500,
TEXT_ONLY_THRESHOLD: 25,
DEBUG_OVERLAY: false,
});
// Direction aliases removed (use DIR.* directly for clarity and grep-ability)
/** Geometry and heuristic values (pixel units). */
const GEOM = Object.freeze({
PADDING: 8,
OVERLAP_TOLERANCE: 6
});
/** Performance tuning thresholds for window registry & event debounce. */
const PERF = Object.freeze({
// Registry TTL/adaptive parameters removed in simplified model (left here for backward compatibility no-ops)
REGISTRY_TTL_BASE_MS: 0,
REGISTRY_TTL_MAX_MS: 0,
REGISTRY_TTL_MIN_MS: 0,
REGISTRY_ADAPTIVE: false,
EVENT_DEBOUNCE_MS: 80,
NEIGHBOR_SCAN_MAX_DIST: 600, // px: early break distance when building directional neighbor lists (horizontal & vertical)
NEIGHBOR_DISTANCE_ENABLED: true, // set false to disable early break distance caps
FOCUS_FORCE_REBUILD_ON_MISS: true, // if no candidates found, trigger one registry rebuild attempt
REGISTRY_SLOW_GROW_FACTOR: 0,
REGISTRY_FAST_SHRINK_FACTOR: 0,
REGISTRY_MISS_STREAK_TRIGGER: 0,
REGISTRY_DISABLE_TTL: true,
FOCUS_ENUM_CACHE_MS: 2500 // reuse per-screen enumeration & indices if another focusAdjacent fires within this window
,FOCUS_MAX_SCAN: 160 // cap directional scan iterations before fallback tie-breaks
,FOCUS_DISTANCE_CAP: 4000 // absolute primary distance cap (px) to ignore very distant windows
});
/** Focus tracking limits & feature toggles. */
const FOCUS = Object.freeze({
ENABLE_GLOBAL_MRU: true,
GLOBAL_MRU_LIMIT: 25,
ENABLE_DIRECTIONAL_MRU: true,
DIRECTIONAL_MRU_LIMIT: 15,
MOVE_POINTER_ON_FOCUS: true // Toggle mouse centering after focus changes
});
/** Move mode nudge distances (pixels). */
const MOVE = Object.freeze({
NUDGE_SMALL: 10,
NUDGE_LARGE: 50
});
// Runtime mutable tracking
var LAST_POSITION = {
window: null,
grid: "",
positions: []
};
var LAST_POSITION_INDEX = -1;
var ICON_CACHE = {};
function iconKey(win) {
try {
const app = win.app();
if (app.bundleIdentifier) {
const bid = app.bundleIdentifier();
if (bid) return bid;
}
return 'pid:' + app.pid();
} catch (e) {
return 'hash:' + win.hash();
}
}
// --------------------------------------------------------------------------------
// Utilities Namespace
// --------------------------------------------------------------------------------
const Util = (() => {
function log(...args) { if (DEBUG) Phoenix.log(args.join(' ')); }
function rotateArray(arr, n) {
if (!arr.length) return arr;
n = ((n % arr.length) + arr.length) % arr.length;
return arr.slice(n).concat(arr.slice(0, n));
}
function clamp(v, min, max) { return v < min ? min : (v > max ? max : v); }
/** All visible windows on a given screen (ordered by recency). */
function windowsForScreen(screen) {
try {
const recent = Window.recent().filter(w => {
try { return w && w.isVisible && w.isVisible() && w.screen() && w.screen().isEqual(screen); } catch (e) { return false; }
});
// Defensive augmentation: some versions/setups may yield an incomplete Window.recent() list.
// Merge in any visible windows from screen.windows({visible:true}) that are missing.
let full = recent;
try {
const byScreen = screen.windows ? (screen.windows({ visible: true }) || []) : [];
if (byScreen.length > recent.length) {
const recentHashes = new Set(recent.map(w => w.hash()));
byScreen.forEach(w => {
try { if (w && w.isVisible && w.isVisible() && w.screen() && w.screen().isEqual(screen) && !recentHashes.has(w.hash())) full.push(w); } catch (e) { }
});
}
} catch (e) { }
return full;
} catch (e) {
// Fallback to legacy per-screen enumeration if recent() fails
try { return screen.windows({ visible: true }) || []; } catch (e2) { return []; }
}
}
/** All visible windows across all screens (ordered by recency). */
function allVisibleWindows() {
try { return Window.recent().filter(w => { try { return w.isVisible && w.isVisible(); } catch (e) { return false; } }); } catch (e) { return Window.all({ visible: true }); }
}
return { log, rotateArray, clamp, windowsForScreen, allVisibleWindows };
})();
// ********************************************************************************
// Keyboard Bindings
// Cheat Sheet:
// ctrl+opt+return / arrows : stepped sizing / grid cycles
// shift+ctrl+opt+arrows : vertical reposition sequences
// ctrl+opt+h/j/k/l : focus adjacent (vim)
// ctrl+opt+cmd+←/→ : move window to screen
// shift+ctrl+opt+cmd+←/→ : move window to space
// shift+ctrl+z : cycle tiling mode
// shift+cmd+space : hints
// ctrl+opt+cmd+m : move mode (later binding)
// ********************************************************************************
// --- 1. Grid / Size Cycling (Moom-like) ---
// ********************************************************************************
// Keyboard Bindings
// ********************************************************************************
// Moom-like bindings
Key.on("return", ["control", "option"], () => { const w=Window.focused(); if(!w) return; w.positionInGrid(4, 0, 3).centerMouse() });
// Mixed grid bases (4/6/16) intentionally provide progressive sizing steps.
Key.on("left", ["control", "option"], () => { const w=Window.focused(); if(!w) return; steppedSizing(w, [[4, 0, 2], [6, 0, 3], [16, 0, 9]]) });
Key.on("right", ["control", "option"], () => { const w=Window.focused(); if(!w) return; steppedSizing(w, [[4, 1, 3], [6, 2, 5], [16, 6, 15]]) });
Key.on("up", ["control", "option"], () => { const w=Window.focused(); if(!w) return; steppedSizing(w, [[4, 0, 1], [4, 0, 3]]) });
Key.on("down", ["control", "option"], () => { const w=Window.focused(); if(!w) return; steppedSizing(w, [[4, 2, 3], [8, 2, 13], [6, 1, 4]]) });
Key.on("left", ["shift", "control", "option"], () => { const w=Window.focused(); if(!w) return; w.reposition(DIR.WEST).centerMouse() });
Key.on("right", ["shift", "control", "option"], () => { const w=Window.focused(); if(!w) return; w.reposition(DIR.EAST).centerMouse() });
Key.on("up", ["shift", "control", "option"], () => { const w=Window.focused(); if(!w) return; w.reposition(DIR.NORTH).centerMouse() });
Key.on("down", ["shift", "control", "option"], () => { const w=Window.focused(); if(!w) return; w.reposition(DIR.SOUTH).centerMouse() });
// Move horizontally between screens
// --- 2. Screen Transfer ---
Key.on("right", ["control", "option", "command"], () => { const w=Window.focused(); if(!w) return; w.toScreen(DIR.EAST).centerMouse() });
Key.on("left", ["control", "option", "command"], () => { const w=Window.focused(); if(!w) return; w.toScreen(DIR.WEST).centerMouse() });
// Move horizontally between spaces
// --- 3. Space Transfer ---
Key.on("right", ["shift", "control", "option", "command"], () => { const w=Window.focused(); if(!w) return; w.toSpace(DIR.EAST) });
Key.on("left", ["shift", "control", "option", "command"], () => { const w=Window.focused(); if(!w) return; w.toSpace(DIR.WEST) });
// Change focus to adjacent window (Vim-like: h=West, l=East)
// --- 4. Focus Navigation (Vim-like) ---
Key.on("h", ["control", "option"], () => { const w=Window.focused(); if(!w) return; w.focusAdjacent(DIR.WEST).centerMouse() });
Key.on("j", ["control", "option"], () => { const w=Window.focused(); if(!w) return; w.focusAdjacent(DIR.SOUTH).centerMouse() });
Key.on("k", ["control", "option"], () => { const w=Window.focused(); if(!w) return; w.focusAdjacent(DIR.NORTH).centerMouse() });
Key.on("l", ["control", "option"], () => { const w=Window.focused(); if(!w) return; w.focusAdjacent(DIR.EAST).centerMouse() });
// --- 5. Tiling Mode / Rotation ---
Key.on("z", ["shift", "control"], () => wm.change_tiling_mode())
Key.on("n", ["shift", "control"], () => wm.rotate(-1, false));
Key.on("n", ["shift", "control", "option"], () => wm.rotate(-1, true));
Key.on("m", ["shift", "control"], () => wm.rotate(1, false));
Key.on("m", ["shift", "control", "option"], () => wm.rotate(1, true));
// Re-tile current mode on focused screen
Key.on("x", ["shift", "control"], () => wm.retile()); 
// --- 7. Modes & Hints ---
Key.on('m', ['control', 'option', 'command'], () => { const w=Window.focused(); if(!w) return; MoveMode.activate(w) });
Key.on("space", ["shift", "command"], () => HintManager.activate());
// ********************************************************************************
// Size steps
// ********************************************************************************
function steppedSizing(win, gridPositions) {
if (!gridPositions || !gridPositions.length) return win; // Guard against empty cycles
if (!win.isEqual(LAST_POSITION.window)) {
LAST_POSITION_INDEX = 0;
LAST_POSITION.window = win;
} else if (JSON.stringify(LAST_POSITION.positions) != JSON.stringify(gridPositions)) {
LAST_POSITION_INDEX = 0; // Accept simple stringify comparison (small arrays)
}
const res = win.positionInGrid.apply(win, gridPositions[LAST_POSITION_INDEX]).centerMouse();
LAST_POSITION.grid = gridPositions[LAST_POSITION_INDEX].join(",");
LAST_POSITION_INDEX = (LAST_POSITION_INDEX + 1) % gridPositions.length;
LAST_POSITION.positions = gridPositions;
return res
}
// ********************************************************************************
// Hints
// ********************************************************************************
class Hints {
constructor() {
this.active = false;
this.keys = [];
this.hints = {};
this.escbind = null;
this.bsbind = null;
this.id = Math.random();
} // constructor
cancel() {
for (var activator in this.hints) {
if (this.hints[activator])
this.hints[activator].modal.close();
};
// remove all key bindings
Key.off(this.escbind);
Key.off(this.bsbind);
this.keys.map(Key.off);
// clear hints
this.hints = {};
this.keys = [];
this.active = false;
return this;
} // cancel
show(windows, prefix) {
const self = this;
prefix = prefix || "";
const t0 = Date.now();
// Allow callers to omit windows: pull from WindowRegistry (all screens) then filter to current space
if (!windows) {
try {
const focused = Window.focused();
let spaceId = null;
if (focused && focused.spaces) {
const sp = focused.spaces();
if (sp && sp.length) {
try { spaceId = sp[0].hash ? sp[0].hash() : sp[0]; } catch (e) { }
}
}
if (spaceId) {
// Directly query space-partitioned entries (avoids allocating all entries and filtering)
windows = WindowRegistry.getSpaceEntries(spaceId).map(e => e.win);
} else {
// Fallback to all entries if space cannot be determined
windows = WindowRegistry.getAllEntries().map(e => e.win);
}
} catch (e) {
windows = WindowRegistry.getAllEntries().map(e => e.win);
}
}
if (!windows || !windows.length) return this;
const totalWindows = windows.length;
const textOnly = totalWindows > HINT.TEXT_ONLY_THRESHOLD;
// If more windows than hint characters, build a two-level selection rather than recursive early-return.
// Partition windows into buckets keyed by first-level char.
if (windows.length > HINT.CHARS.length) {
const buckets = {};
windows.forEach((win, idx) => {
const key = HINT.CHARS[idx % HINT.CHARS.length];
(buckets[key] = buckets[key] || []).push(win);
});
// Show only first-level hints now; activating one expands second level.
Object.keys(buckets).forEach((ch, i) => {
const pseudoWin = buckets[ch][0]; // anchor modal to first window in bucket
const hash = iconKey(pseudoWin);
if (!ICON_CACHE[hash]) ICON_CACHE[hash] = pseudoWin.app().icon();
const hint = Modal.build({
text: prefix + ch,
appearance: HINT.APPEARANCE,
icon: ICON_CACHE[hash],
weight: 16,
duration: 0,
}).attach(pseudoWin);
self.hints[prefix + ch] = { win: pseudoWin, modal: hint, position: 0, active: true, bucket: buckets[ch] };
});
self._prepareKeyHandlersForBuckets(prefix);
this.active = true;
return this;
}
// Use WindowRegistry metadata directly (avoids duplicate frame/title/icon lookups)
const entriesByHash = {};
WindowRegistry.getAllEntries().forEach(e => { entriesByHash[e.hash] = e; });
const tMetaStart = Date.now();
const meta = windows.map((win, i) => {
const h = win.hash();
const snap = entriesByHash[h];
if (snap) return { win, i, hash: h, multi: snap.multi, frame: snap.frame, titleShort: snap.titleShort };
// fallback minimal (rare path)
return { win, i, hash: h, multi: false, frame: win.frame(), titleShort: "" };
});
const tMetaEnd = Date.now();
// Build hints in one pass
// Create all modals without (or with cached) icons first for fast initial paint
const lazyQueue = [];
const tModalStart = Date.now();
meta.forEach(m => {
const ikey = iconKey(m.win);
const hasIcon = !!ICON_CACHE[ikey];
if (!textOnly) {
if (hasIcon === false && HINT.LAZY_ICONS) lazyQueue.push({ key: ikey, win: m.win });
else if (!hasIcon && !HINT.LAZY_ICONS) { // eager path if lazy disabled
try { ICON_CACHE[ikey] = m.win.app().icon(); } catch (e) { }
}
}
let label = "";
if (!textOnly && m.multi) label += " | " + (m.titleShort || "");
const hint = Modal.build({
text: prefix + HINT.CHARS[m.i] + label,
appearance: HINT.APPEARANCE,
icon: (textOnly ? undefined : (ICON_CACHE[ikey] || undefined)),
weight: 16,
duration: 0,
}).attach(m.win);
self.hints[prefix + HINT.CHARS[m.i]] = { win: m.win, modal: hint, position: 0, active: true, _iconKey: ikey };
});
const tModalEnd = Date.now();
// Lazy icon loading in small timed batches to keep UI responsive
if (!textOnly && HINT.LAZY_ICONS && lazyQueue.length) {
let idx = 0;
function loadBatch() {
const slice = lazyQueue.slice(idx, idx + HINT.ICON_BATCH_SIZE);
slice.forEach(entry => {
if (!ICON_CACHE[entry.key]) {
try { ICON_CACHE[entry.key] = entry.win.app().icon(); } catch (e) { }
}
// Update modal icon if still active
for (const activator in self.hints) {
const h = self.hints[activator];
if (h && h._iconKey === entry.key && ICON_CACHE[entry.key]) {
h.modal.icon = ICON_CACHE[entry.key];
}
}
});
idx += HINT.ICON_BATCH_SIZE;
if (idx < lazyQueue.length) Timer.after(HINT.ICON_BATCH_DELAY, loadBatch);
}
Timer.after(HINT.ICON_BATCH_DELAY, loadBatch);
}
// Overlap adjustment (single pass, sorted by origin.y)
const activators = Object.keys(self.hints).sort((a, b) => self.hints[a].modal.origin.y - self.hints[b].modal.origin.y);
if (!textOnly && activators.length > 1) { // skip costly adjustments in text-only mode
let adjustments = 0;
for (let i = 0; i < activators.length; i++) {
const a = self.hints[activators[i]].modal;
const aBottom = a.origin.y + a.frame().height + GEOM.PADDING;
for (let j = i + 1; j < activators.length; j++) {
if (adjustments > HINT.MAX_OVERLAP_ADJUST) break;
const b = self.hints[activators[j]].modal;
// Early vertical non-overlap break (list sorted by y increasing)
if (b.origin.y > aBottom) break;
// Overlap test
if (
a.origin.x < b.origin.x + b.frame().width + GEOM.PADDING &&
a.origin.x + a.frame().width > b.origin.x - GEOM.PADDING &&
a.origin.y < b.origin.y + b.frame().height + GEOM.PADDING &&
a.origin.y + a.frame().height > b.origin.y - GEOM.PADDING
) {
b.origin = { x: b.origin.x, y: a.origin.y + a.frame().height + GEOM.PADDING };
adjustments++;
}
}
if (adjustments > HINT.MAX_OVERLAP_ADJUST) break;
}
}
self.escbind = Key.on(HINT.CANCEL_KEY, [], () => self.cancel());
this.active = true;
// Debug overlay (appears briefly)
if (HINT.DEBUG_OVERLAY) {
const tEnd = Date.now();
try {
const stats = [
`win:${totalWindows}`,
`text:${textOnly}`,
`meta:${tMetaEnd - tMetaStart}ms`,
`modal:${tModalEnd - tModalStart}ms`,
`lazyQ:${lazyQueue.length}`,
`total:${tEnd - t0}ms`
].join(' ');
Modal.build({
text: stats,
appearance: HINT.APPEARANCE,
weight: 12,
duration: 0.9,
}).flash(Screen.main ? Screen.main() : Screen.all()[0]);
} catch (e) { }
}
return this;
} // show
_prepareKeyHandlersForBuckets(prefix) {
// Replace existing handlers with bucket-expansion logic.
const self = this;
const sequences = Object.keys(self.hints);
self.keys = [];
HINT.CHARS.split("").forEach(ch => {
self.keys.push(Key.on(ch, [], function () {
const activator = prefix + ch;
const entry = self.hints[activator];
if (!entry) return; // not a valid first-level key
// Close first-level modals except chosen bucket
sequences.forEach(seq => { if (seq !== activator) self.hints[seq].modal.close(); });
const bucket = entry.bucket;
// Clear hints and rebuild with second-level inside bucket
const bucketWins = bucket;
self.cancel();
self.show(bucketWins, activator); // recursing with prefix expands second level
}));
});
}
activate() {
var self = this;
if (this.active) {
self.cancel();
} else {
Event.once("mouseDidLeftClick", function () { self.cancel(); });
this.show(Util.allVisibleWindows());
var sequence = "";
self.keys = [];
HINT.CHARS.split("").forEach(function (hintchar) {
// set up each individual hint handler
self.keys.push(Key.on(hintchar, [], function () {
// ISSUE: Potential race if windows close mid-sequence; consider validating hint.win.exists().
sequence += hintchar;
for (var activator in self.hints) {
var hint = self.hints[activator];
if (!hint.active) continue;
if (activator[hint.position] === hintchar) {
hint.position++;
if (hint.position === activator.length) {
hint.win.focus();
Mouse.move({
x: hint.modal.origin.x + hint.modal.frame().width / 2,
y: Screen.all()[0].frame().height - hint.modal.origin.y - hint.modal.frame().height / 2
});
return self.cancel();
}
hint.modal.text = hint.modal.text.substr(1);
} else {
hint.modal.close();
hint.active = false;
}
}
}));
});
self.bsbind = Key.on("delete", [], function () {
if (!sequence.length)
self.cancel();
var letter = sequence[sequence.length - 1];
sequence = sequence.substr(0, sequence.length - 1);
for (var activator in self.hints) {
var hint = self.hints[activator];
if (hint.active) {
hint.position--;
hint.modal.text = letter + hint.modal.text;
} else if (activator.substr(0, sequence.length) === sequence) {
hint.modal.show();
hint.active = true;
}
}
});
}
} // activate
}
// ********************************************************************************
// Window Manager Abstraction
// ********************************************************************************
class WindowManager {
constructor() {
this.tiling_modes = Array(Screen.all().length)
this.tiling_modes.fill(DIR.NONE)
this.timers = Array(Screen.all().length)
this.layouts = Array(Screen.all().length)
}
change_tiling_mode() {
const focused = Window.focused();
if (!focused) return this; // No focused window -> nothing to tile
let screen = null;
try { screen = focused.screen && focused.screen(); } catch (e) { }
if (!screen) return this; // Cannot resolve screen
const screens = Screen.all();
const index = screens.indexOf(screen);
if (index === -1) return this; // Screen not tracked
// unified enumeration
const visible = Util.windowsForScreen(screen);
// rotate tiling mode
this.tiling_modes[index] = LAYOUT.MODES[(LAYOUT.MODES.indexOf(this.tiling_modes[index]) + 1) % LAYOUT.MODES.length]
if (DEBUG) Phoenix.log(index + " " + this.tiling_modes[index]);
// Try layout cache reuse
const key = layoutCacheKey(screen, this.tiling_modes[index], visible)
let cached = LAYOUT_CACHE.get(key)
if (cached) {
const layout = new Layout(screen, this.tiling_modes[index])
layout.frames = cached.map(f => new Frame(f.x, f.y, f.width, f.height))
this.layouts[index] = layout
} else {
this.layouts[index] = new Layout(screen, this.tiling_modes[index])
// store frames snapshot
LAYOUT_CACHE.set(key, this.layouts[index].frames.map(f => ({ x: f.x, y: f.y, width: f.width, height: f.height })))
}
var modal = Modal.build({
text: "Tiling mode: " + this.tiling_modes[index],
appearance: HINT.APPEARANCE,
weight: 24,
icon: App.get('Phoenix').icon(),
duration: 0.5,
}).flash(screen)
if (DEBUG) Phoenix.log("layouts pre-apply count=" + this.layouts[index].frames.length);
this.layouts[index].windows = visible
this.layouts[index].apply()
if (DEBUG) Phoenix.log("layouts applied");
return this
}
rotate(dir, focus_only) {
const focused = Window.focused();
if (!focused) return this;
let screen = null; try { screen = focused.screen && focused.screen(); } catch(e) {}
if (!screen) return this;
const index = Screen.all().indexOf(screen);
if (index === -1) return this;
const layout = this.layouts[index];
if (!layout) return this;
const list = (layout.windows || []);
if (DEBUG) {
try { Phoenix.log(index + " rotate " + dir + " [" + list.map(w=>{try{return w.hash()}catch(e){return 'invalid'}}).join(',') + "]"); } catch(e) {}
}
layout.rotate(dir).apply(screen)
return this
}
// Re-apply current tiling mode on focused screen without changing mode.
retile() {
const focused = Window.focused();
if (!focused) return this;
let screen = null; try { screen = focused.screen && focused.screen(); } catch(e) {}
if (!screen) return this;
const screens = Screen.all();
const index = screens.indexOf(screen);
if (index === -1) return this;
const mode = this.tiling_modes[index];
if (!mode || mode === DIR.NONE) return this; // nothing to tile
const visible = Util.windowsForScreen(screen);
// Rebuild layout for current mode
const key = layoutCacheKey(screen, mode, visible);
let layout;
const cached = LAYOUT_CACHE.get(key);
if (cached) {
layout = new Layout(screen, mode);
layout.frames = cached.map(f => new Frame(f.x, f.y, f.width, f.height));
} else {
layout = new Layout(screen, mode);
LAYOUT_CACHE.set(key, layout.frames.map(f => ({ x: f.x, y: f.y, width: f.width, height: f.height })));
}
layout.windows = visible;
layout.apply();
this.layouts[index] = layout;
if (DEBUG) try { Phoenix.log("retile applied mode=" + mode + " windows=" + visible.length); } catch(e) {}
return this;
}
}
const wm = new WindowManager()
// ********************************************************************************
// Layout Abstraction
// ********************************************************************************
class Layout {
constructor(screen, mode) {
this.frames = []
this.windows = []
switch (mode) {
case DIR.EAST:
this.east(screen)
break;
case DIR.WEST:
this.west(screen)
break;
case DIR.COLS:
this.cols(screen)
break;
case 'CenterRing':
this.centerRing(screen)
break;
case 'CenterColumn':
this.centerColumn(screen)
break;
}
}
east(screen) {
const f = screen.flippedVisibleFrame(),
// unified enumeration
v = Util.windowsForScreen(screen),
c = v.length - 1,
w = f.width / 2,
h = ~~(f.height / c),
self = this
this.screen = screen
if (v.length === 1) {
this.frames.push(new Frame(f.x, f.y, f.width, f.height).pad())
} else {
// main window
self.frames.push(new Frame(f.x, f.y, w, f.height)
.displace(f.width / 2, 0).pad())
// secondary windows
for (var i = 0; i < c; i++) {
self.frames.push(new Frame(f.x, f.y, w, h)
.displace(0, h * i).pad())
}
}
return this
}
west(screen) {
const f = screen.flippedVisibleFrame(),
v = Util.windowsForScreen(screen),
c = v.length - 1,
w = f.width / 2,
h = ~~(f.height / c),
self = this
this.screen = screen
if (v.length === 1) {
self.frames.push(new Frame(f.x, f.y, f.width, f.height).pad())
} else {
// main window
self.frames.push(new Frame(f.x, f.y, w, f.height)
.pad())
// secondary windows
for (var i = 0; i < c; i++) {
self.frames.push(new Frame(f.x, f.y, w, h)
.displace(w, h * i).pad())
}
}
return this
}
cols(screen) {
const f = screen.flippedVisibleFrame(),
v = Util.windowsForScreen(screen),
c = v.length,
w = ~~(f.width / c),
h = f.height,
self = this
this.screen = screen
// all windows
for (var i = 0; i < c; i++) {
self.frames.push(new Frame(f.x, f.y, w, h)
.displace(w * i, 0).pad())
}
return this
}
// CenterRing layout: first (most recent) window gets a centered 40% frame.
// Remaining windows are distributed in bands: top, bottom, left, right around the center.
// Simplifying assumptions: we ignore corner micro-tiling; top/bottom span full width, left/right span the vertical middle strip.
// Distribution strategy: round-robin across bands in order [top,bottom,left,right] for visual balance.
centerRing(screen) {
const f = screen.flippedVisibleFrame();
const v = Util.windowsForScreen(screen);
this.screen = screen;
if (!v.length) return this;
const cxw = Math.round(f.width * 0.40);
const cxh = Math.round(f.height * 0.40);
const remW = f.width - cxw;
const remH = f.height - cxh;
// Split remaining equally top/bottom and left/right
const topH = Math.round(remH / 2);
const bottomH = remH - topH;
const leftW = Math.round(remW / 2);
const rightW = remW - leftW;
const centerX = f.x + leftW;
const centerY = f.y + topH;
// Center window frame
this.frames.push(new Frame(centerX, centerY, cxw, cxh).pad());
if (v.length === 1) return this;
// Pre-calculate band rectangles
const bands = {
top: { x: f.x, y: f.y, width: f.width, height: topH },
bottom: { x: f.x, y: f.y + topH + cxh, width: f.width, height: bottomH },
left: { x: f.x, y: centerY, width: leftW, height: cxh },
right: { x: centerX + cxw, y: centerY, width: rightW, height: cxh },
};
// Collect remaining windows and distribute counts per band for simple tiling.
const remaining = v.slice(1); // exclude center
if (!remaining.length) return this;
const bandOrder = ['top','bottom','left','right'];
const allocation = { top:[], bottom:[], left:[], right:[] };
remaining.forEach((win, i) => {
allocation[bandOrder[i % bandOrder.length]].push(win);
});
function tileHoriz(rect, count) {
if (count === 0) return [];
const w = Math.round(rect.width / count);
const frames = [];
for (let i=0;i<count;i++) {
frames.push(new Frame(rect.x + w*i, rect.y, (i === count-1 ? rect.x + rect.width - (rect.x + w*i) : w), rect.height).pad());
}
return frames;
}
function tileVert(rect, count) {
if (count === 0) return [];
const h = Math.round(rect.height / count);
const frames = [];
for (let i=0;i<count;i++) {
frames.push(new Frame(rect.x, rect.y + h*i, rect.width, (i === count-1 ? rect.y + rect.height - (rect.y + h*i) : h)).pad());
}
return frames;
}
// Tile each band according to orientation
const topFrames = tileHoriz(bands.top, allocation.top.length);
const bottomFrames = tileHoriz(bands.bottom, allocation.bottom.length);
const leftFrames = tileVert(bands.left, allocation.left.length);
const rightFrames = tileVert(bands.right, allocation.right.length);
// Preserve original window ordering (center first), then follow bandOrder sequence
this.frames.push(...topFrames, ...bottomFrames, ...leftFrames, ...rightFrames);
return this;
}
// CenterColumn layout: first window gets middle column (40% width, full height).
// Remaining windows are distributed vertically in the left and right columns (each ~30% width).
// We alternate assignment left/right to keep balance, then within each side column windows are stacked top->bottom.
centerColumn(screen) {
const f = screen.flippedVisibleFrame();
const v = Util.windowsForScreen(screen);
this.screen = screen;
if (!v.length) return this;
const centerW = Math.round(f.width * 0.40);
const sideTotal = f.width - centerW; // remaining width
const leftW = Math.round(sideTotal / 2);
const rightW = sideTotal - leftW;
const centerX = f.x + leftW;
// Center (primary) window full height
this.frames.push(new Frame(centerX, f.y, centerW, f.height).pad());
if (v.length === 1) return this;
const remaining = v.slice(1);
const leftWins = [];
const rightWins = [];
remaining.forEach((w,i)=>{ (i % 2 === 0 ? leftWins : rightWins).push(w); });
function verticalStack(x, width, y, height, count) {
if (!count) return [];
const cell = Math.round(height / count);
const frames = [];
for (let i=0;i<count;i++) {
const h = (i === count-1) ? (y + height) - (y + cell*i) : cell;
frames.push(new Frame(x, y + cell*i, width, h).pad());
}
return frames;
}
const leftFrames = verticalStack(f.x, leftW, f.y, f.height, leftWins.length);
const rightFrames = verticalStack(centerX + centerW, rightW, f.y, f.height, rightWins.length);
// Maintain deterministic order: center, then interleave left/right stacks by original grouping order.
// For simplicity just append all left then all right; window order mapping will still align with frames sequence.
this.frames.push(...leftFrames, ...rightFrames);
return this;
}
none(screen) {
this.frames = []
return this
}
rotate(dir) {
// Use utility rotation (non-mutating) instead of Array.prototype extension.
this.windows = Util.rotateArray(this.windows, dir);
return this
}
apply() {
// Prune invalid/closed windows
const pruned = [];
for (let i=0;i<this.windows.length;i++) {
const w = this.windows[i];
try { if (w && w.app && w.app()) pruned.push(w); } catch(e){}
}
this.windows = pruned;
if (!this.frames.length || !this.windows.length) return this;
const n = Math.min(this.frames.length, this.windows.length);
for (let i=0;i<n;i++) {
try { this.windows[i].setFrame(this.frames[i]); } catch(e) { /* ignore individual failures */ }
}
return this;
}
}
// ********************************************************************************
// Frame Abstraction
// ********************************************************************************
class Frame {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
pad() {
return new Frame(
this.x + GEOM.PADDING / 2,
this.y + GEOM.PADDING / 2,
this.width - GEOM.PADDING,
this.height - GEOM.PADDING
)
}
snap(screen, dir) { // Expects a DIR.* token; unsupported values leave frame unchanged.
const s = screen.flippedVisibleFrame();
const f = new Frame(this.x, this.y, this.width, this.height);
// Align to right side for east-oriented quadrants
if ([DIR.EAST, DIR.NE, DIR.SE].includes(dir)) f.x += s.width - f.width;
// Align to bottom side for south-oriented quadrants
if ([DIR.SE, DIR.SW].includes(dir)) f.y += s.height - f.height;
// Full width if explicitly requested
if (dir === DIR.FULL) f.width = s.width;
// Full height for full or pure east/west halves
if ([DIR.FULL, DIR.EAST, DIR.WEST].includes(dir)) f.height = s.height;
return f;
}
displace(x, y) { // Does not clamp to screen intentionally (used for relative moves)
return new Frame(
this.x + x,
this.y + y,
this.width,
this.height
)
}
log() {
Phoenix.log(this.x + ", " + this.y + ", " + this.width + ", " + this.height);
return this;
}
rect() {
return {
x: this.x,
y: this.y,
width: this.width,
height: this.height
}
}
ratio(a, b) { // Returns a transformer that scales a frame from space a -> space b
var wr = b.width / a.width,
hr = b.height / a.height;
return ({ x, y, width, height }) => {
x = Math.round(b.x + (x - a.x) * wr);
y = Math.round(b.y + (y - a.y) * hr);
width = Math.round(width * wr);
height = Math.round(height * hr);
return { x, y, width, height }
}
}
}
// ********************************************************************************
// Window Extensions
// ********************************************************************************
// Snap a window in a given direction
Window.prototype.to = function (direction) { // Half-screen quadrant snap utility
var s = this.screen(),
f = s.flippedVisibleFrame(),
frame = new Frame(
f.x,
f.y,
f.width / 2,
f.height / 2,
).snap(s, direction).pad();
this.setFrame(frame);
};
// Move a window to a given screen
Window.prototype.toScreen = function (dir) { // East/West nearest screen by X origin (vertical stack not handled yet)
var screen = null;
if (dir === DIR.EAST || dir === DIR.WEST) {
const cur = this.screen();
const curX = cur.origin().x;
// Find nearest screen strictly to the east or west
screen = Screen.all()
.filter(s => dir === DIR.EAST ? s.origin().x > curX : s.origin().x < curX)
.sort((a, b) => Math.abs(a.origin().x - curX) - Math.abs(b.origin().x - curX))[0];
if (screen) {
const ratio = Frame.prototype.ratio(cur.flippedVisibleFrame(), screen.flippedVisibleFrame());
this.setFrame(ratio(this.frame()));
}
}
return this;
};
// Algorithmic vertical repositioning: cycles a column between spanning both rows and a single-top/single-bottom cell.
// Grid assumptions: 2 rows layout (cells even; indices laid out row-major). Examples (4 cells: 2x2, 6 cells: 3x2)
// Behavior:
// - If window spans both rows in a column and moved NORTH -> collapse to top cell of that column.
// - If window spans both rows and moved SOUTH -> collapse to bottom cell of that column.
// - If window is a single bottom cell and moved NORTH -> move to top cell (same column).
// - If window is a single top cell and moved SOUTH -> move to bottom cell (same column).
// - Otherwise no change.
Window.prototype.reposition = function(dir) {
if (!this.isEqual(LAST_POSITION.window)) return this;
const spec = LAST_POSITION.grid; // e.g. "6,2,5"
if (!spec) return this;
const parts = spec.split(',').map(s => parseInt(s,10));
if (parts.length !== 3 || parts.some(isNaN)) return this;
const [cells,start,end] = parts;
if (cells % 2 !== 0) return this; // only handle 2-row grids (row-major assumption)
const cols = cells / 2;
const startCol = start % cols;
const endCol = end % cols;
const startRow = Math.floor(start / cols);
const endRow = Math.floor(end / cols);
let targetStart = start, targetEnd = end;
if (dir === DIR.NORTH || dir === DIR.SOUTH) {
// Vertical cycling (existing logic, slightly reorganized)
// Operate only when selection is a single column span (may cover both rows or a single cell)
if (startCol !== endCol) return this;
const spansBoth = startRow !== endRow; // two rows
if (dir === DIR.NORTH) {
if (spansBoth || startRow === 1) { // spanning or bottom single -> collapse to top
targetStart = startCol; // row 0
targetEnd = targetStart;
} else return this; // already top
} else { // SOUTH
if (spansBoth || startRow === 0) { // spanning or top single -> collapse to bottom
targetStart = startCol + cols; // row 1
targetEnd = targetStart;
} else return this; // already bottom
}
} else if (dir === DIR.EAST || dir === DIR.WEST) {
// Horizontal cycling: mirror vertical behavior across columns within a single row.
// Only operate when selection spans a single row (may cover multiple columns) OR is a single cell.
if (startRow !== endRow) return this; // ignore multi-row spans (vertical logic handles those)
const spansMultiple = startCol !== endCol;
if (spansMultiple) {
// Collapse a multi-column span to an edge cell depending on direction.
if (dir === DIR.WEST) {
targetStart = startRow * cols + startCol; // leftmost
targetEnd = targetStart;
} else { // EAST
targetStart = startRow * cols + endCol; // rightmost
targetEnd = targetStart;
}
} else { // single cell -> move laterally within row if possible
if (dir === DIR.WEST && startCol > 0) {
targetStart = targetEnd = start - 1;
} else if (dir === DIR.EAST && startCol < cols - 1) {
targetStart = targetEnd = start + 1;
} else return this; // edge cell, no change
}
} else {
return this; // unsupported direction
}
if (targetStart !== start || targetEnd !== end) {
this.positionInGrid(cells, targetStart, targetEnd);
}
return this;
};
Window.prototype.positionInGrid = function (cells, start, end) {
if (cells <= 0) return this;
if (start < 0 || end < 0 || start >= cells || end >= cells) return this; // Guard invalid indices
LAST_POSITION = {
window: this,
grid: cells + "," + start + "," + end
}
var cols = ~~(cells / 2);
var screen = this.screen();
var cellwidth = (screen.width() - ((cols - 1) * GEOM.PADDING)) / cols;
var cellheight = (screen.height() - GEOM.PADDING) / 2;
var startc = start % cols,
startw = ~~(start / cols),
startl = screen.origin().x + (cellwidth + GEOM.PADDING) * startc,
startt = screen.origin().y + (cellheight + GEOM.PADDING) * startw,
startr = startl + cellwidth,
startb = startt + cellheight;
var endc = end % cols,
endw = ~~(end / cols),
endl = screen.origin().x + (cellwidth + GEOM.PADDING) * endc,
endt = screen.origin().y + (cellheight + GEOM.PADDING) * endw,
endr = endl + cellwidth,
endb = endt + cellheight;
var frame = this.frame();
frame.x = Math.min(startl, endl);
frame.y = Math.min(startt, endt);
frame.width = Math.max(startr, endr) - frame.x;
frame.height = Math.max(startb, endb) - frame.y;
this.setFrame(frame);
return this;
}
// Resize a window by coeff units in the given direction
// coeff: -n shrinks by pixels units, +n grows by n pixels
Window.prototype.resize = function (dir, coeff) {
var frame = this.frame()
if (dir === DIR.WEST) frame.x += coeff * -1
if (dir === DIR.NORTH) frame.y += coeff * -1
if ([DIR.EAST, DIR.WEST].indexOf(dir) > -1) frame.width += coeff
if ([DIR.NORTH, DIR.SOUTH].indexOf(dir) > -1) frame.height += coeff
// Minimal size clamp
if (frame.width < 40) frame.width = 40;
if (frame.height < 40) frame.height = 40;
this.setFrame(frame)
return this
}
Window.prototype.toSpace = function (dir) {
var curSpace = this.spaces()[0],
newSpace = curSpace.next()
if (dir === DIR.WEST)
newSpace = curSpace.previous()
if (!newSpace) return this; // Guard end-of-chain
curSpace.removeWindows([this])
newSpace.addWindows([this])
this.focus()
return this
}
Window.prototype.centerMouse = function () {
if (!FOCUS.MOVE_POINTER_ON_FOCUS) return this;
Mouse.move({
x: this.frame().x + this.frame().width / 2,
y: this.frame().y + this.frame().height / 2
})
return this
}
// Nudge the window by an (x,y) offset determined by direction
// dir: one of EAST/WEST/NORTH/SOUTH or single-char h/j/k/l synonyms
// amount: pixels (default MOVE.NUDGE_SMALL)
Window.prototype.nudge = function (dir, amount) {
amount = amount || MOVE.NUDGE_SMALL;
const frame = this.frame();
switch (dir) {
case DIR.EAST: case 'E': case 'l': frame.x += amount; break;
case DIR.WEST: case 'W': case 'h': frame.x -= amount; break;
case DIR.SOUTH: case 'S': case 'j': frame.y += amount; break;
case DIR.NORTH: case 'N': case 'k': frame.y -= amount; break;
default: return this;
}
// Clamp to screen visible frame
try {
const s = this.screen().flippedVisibleFrame();
if (frame.x < s.x) frame.x = s.x;
if (frame.y < s.y) frame.y = s.y;
if (frame.x + frame.width > s.x + s.width) frame.x = s.x + s.width - frame.width;
if (frame.y + frame.height > s.y + s.height) frame.y = s.y + s.height - frame.height;
} catch (e) { }
this.setFrame(frame);
return this;
}
Window.prototype.log = function () {
Phoenix.log(this.frame().x + "," + this.frame().y + "," + this.frame().width + "," + this.frame().height)
return this
}
// ********************************************************************************
// Focus Adjacent Window
// ********************************************************************************
// Refined adjacency: uses frame edges & overlap rather than just centers.
// Ranking (updated):
// 1. Overlapping along perpendicular axis (preferred)
// 2. Larger overlap span (how much they overlap along the perpendicular axis)
// 3. Larger window area (prefer substantial targets)
// 4. Primary directional distance (edge-to-edge, nearer is better)
// 5. Perpendicular center distance (tie-breaker)
// If no overlapping candidates exist, non-overlapping ones are considered (overlap span = 0).
// Does not traverse to other screens (future extension point).
// Simplified fallback implementation: rebuild candidate list on-the-fly each invocation.
// Ignores registry neighbor indices; uses direct spatial filtering + ranking.
Window.prototype.focusAdjacent = function (dir) {
const cur = this.frame();
const screen = this.screen();
// Ensure registry is fresh if we recently augmented enumeration (some windows might have been missing earlier)
// TTL-based purging disabled: we no longer clear the registry here; event hooks keep it current.
// (Retained guard intentionally lightweight.)
try { if (!WindowRegistry.containsWindow || !WindowRegistry.containsWindow(this)) { /* no action */ } } catch (e) { }
// Fast path: pull stats first (does not rebuild). If snapshot still valid soon, we avoid revalidation churn across rapid key repeats.
const stats = (WindowRegistry.stats && WindowRegistry.stats()) || null;
const enumRec = (WindowRegistry.getIndices && WindowRegistry.getIndices(screen)) ? WindowRegistry.getIndices(screen) : null;
const allEntries = (WindowRegistry.get && WindowRegistry.get(screen)) || [];
const screenEntries = allEntries.filter(e => !e.win.isEqual(this));
if (!screenEntries.length) return this;
const { byLeft, byRight, byTop, byBottom } = enumRec || { byLeft:[], byRight:[], byTop:[], byBottom:[] };
// Allow a small tolerance so touching or slightly overlapping edges still count
const TOL = FOCUS.ENABLE_GLOBAL_MRU ? GEOM.OVERLAP_TOLERANCE : 0; // pixels (centralized)
const curCenterX = cur.x + cur.width / 2;
const curCenterY = cur.y + cur.height / 2;
const horizOverlap = (a, b) => !(a.x + a.width <= b.x + TOL || b.x + b.width <= a.x + TOL);
const vertOverlap = (a, b) => !(a.y + a.height <= b.y + TOL || b.y + b.height <= a.y + TOL);
// MRU bias helper (lower rank = more recent). Missing => large number.
function mruRank(win) {
if (!FOCUS.ENABLE_GLOBAL_MRU || !FOCUS_MRU.length) return 9999;
const h = win.hash();
const idx = FOCUS_MRU.indexOf(h);
return idx === -1 ? 9999 : idx; // index 0 = most recent
}
function directionalMruRank(win) {
if (!FOCUS.ENABLE_DIRECTIONAL_MRU) return 9999;
const list = DIRECTIONAL_FOCUS_MRU[dir];
if (!list || !list.length) return 9999;
const h = win.hash();
const idx = list.indexOf(h);
return idx === -1 ? 9999 : idx;
}
function classify(entry) {
const w = entry.win;
const f = entry.frame;
const centerX = entry.cx;
const centerY = entry.cy;
let primaryDist = Infinity;
let overlapsPerp = false;
let overlapSpan = 0;
switch (dir) {
case DIR.NORTH:
if (f.y + f.height <= cur.y + TOL || centerY < curCenterY) {
overlapsPerp = horizOverlap(f, cur);
if (overlapsPerp) {
const left = Math.max(f.x, cur.x);
const right = Math.min(f.x + f.width, cur.x + cur.width);
overlapSpan = Math.max(0, right - left);
}
primaryDist = Math.max(0, cur.y - (f.y + f.height));
}
break;
case DIR.SOUTH:
if (f.y >= cur.y + cur.height - TOL || centerY > curCenterY) {
overlapsPerp = horizOverlap(f, cur);
if (overlapsPerp) {
const left = Math.max(f.x, cur.x);
const right = Math.min(f.x + f.width, cur.x + cur.width);
overlapSpan = Math.max(0, right - left);
}
primaryDist = Math.max(0, f.y - (cur.y + cur.height));
}
break;
case DIR.EAST:
if (f.x >= cur.x + cur.width - TOL || centerX > curCenterX) {
overlapsPerp = vertOverlap(f, cur);
if (overlapsPerp) {
const top = Math.max(f.y, cur.y);
const bottom = Math.min(f.y + f.height, cur.y + cur.height);
overlapSpan = Math.max(0, bottom - top);
}
primaryDist = Math.max(0, f.x - (cur.x + cur.width));
}
break;
case DIR.WEST:
if (f.x + f.width <= cur.x + TOL || centerX < curCenterX) {
overlapsPerp = vertOverlap(f, cur);
if (overlapsPerp) {
const top = Math.max(f.y, cur.y);
const bottom = Math.min(f.y + f.height, cur.y + cur.height);
overlapSpan = Math.max(0, bottom - top);
}
primaryDist = Math.max(0, cur.x - (f.x + f.width));
}
break;
default:
return null;
}
if (primaryDist === Infinity) return null;
// Lazy compute of perpendicular center delta only if needed later; store centers for tie-break.
return { w, f, overlapsPerp, overlapSpan, area: f.width * f.height, primaryDist, cx: centerX, cy: centerY, z: entry.z, mru: mruRank(w), dmru: directionalMruRank(w) };
}
// Binary search helpers for lower/upper bounds
function upperBound(arr, getter, value) { // last index with getter(el) <= value
let lo = 0, hi = arr.length - 1, ans = -1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (getter(arr[mid]) <= value) { ans = mid; lo = mid + 1; } else hi = mid - 1;
}
return ans;
}
function lowerBound(arr, getter, value) { // first index with getter(el) >= value
let lo = 0, hi = arr.length - 1, ans = arr.length;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (getter(arr[mid]) >= value) { ans = mid; hi = mid - 1; } else lo = mid + 1;
}
return ans;
}
let candidatePool = [];
// Directional coarse filtering
// Optimized scanning: iterate only directional-sorted list, early exit once distance exceeds best.
let directionalList = [];
let forward = true; // iteration direction
if (dir === DIR.NORTH) { directionalList = byBottom; forward = true; }
else if (dir === DIR.SOUTH) { directionalList = byTop; forward = true; }
else if (dir === DIR.EAST) { directionalList = byLeft; forward = true; }
else if (dir === DIR.WEST) { directionalList = byRight; forward = true; }
else directionalList = screenEntries; // fallback
const MAX_SCAN = PERF.FOCUS_MAX_SCAN || 80; // safety cap
const DIST_CAP = PERF.FOCUS_DISTANCE_CAP || Infinity; // optional absolute distance cap
let bestDist = Infinity;
let ties = [];
function consider(entry) {
const c = classify(entry); if (!c) return;
if (c.primaryDist > DIST_CAP) return;
if (c.primaryDist < bestDist) { bestDist = c.primaryDist; ties = [c]; }
else if (c.primaryDist === bestDist) { ties.push(c); }
}
// Determine starting index bounds for directional scan to reduce iterations
if (dir === DIR.NORTH) {
// Find last index whose bottom edge <= cur.y + TOL
const ub = upperBound(directionalList, e => e.frame.y + e.frame.height, cur.y + TOL);
for (let i = ub, scanned = 0; i >= 0 && scanned < MAX_SCAN; i--, scanned++) {
const e = directionalList[i];
const distEdge = cur.y - (e.frame.y + e.frame.height);
if (bestDist !== Infinity && distEdge > bestDist + TOL) break; // further ones are farther north
consider(e);
}
} else if (dir === DIR.SOUTH) {
// Start at first window whose top edge >= bottom of current - TOL
const lb = lowerBound(directionalList, e => e.frame.y, cur.y + cur.height - TOL);
for (let i = lb, scanned = 0; i < directionalList.length && scanned < MAX_SCAN; i++, scanned++) {
const e = directionalList[i];
const distEdge = e.frame.y - (cur.y + cur.height);
if (bestDist !== Infinity && distEdge > bestDist + TOL) break; // remaining tops only increase distance
consider(e);
}
} else if (dir === DIR.EAST) {
const lb = lowerBound(directionalList, e => e.frame.x, cur.x + cur.width - TOL);
for (let i = lb, scanned = 0; i < directionalList.length && scanned < MAX_SCAN; i++, scanned++) {
const e = directionalList[i];
const distEdge = e.frame.x - (cur.x + cur.width);
if (bestDist !== Infinity && distEdge > bestDist + TOL) break;
consider(e);
}
} else if (dir === DIR.WEST) {
const ub = upperBound(directionalList, e => e.frame.x + e.frame.width, cur.x + TOL);
for (let i = ub, scanned = 0; i >= 0 && scanned < MAX_SCAN; i--, scanned++) {
const e = directionalList[i];
const distEdge = cur.x - (e.frame.x + e.frame.width);
if (bestDist !== Infinity && distEdge > bestDist + TOL) break;
consider(e);
}
} else {
// Fallback: classify all (rare path)
screenEntries.forEach(consider);
}
if (!ties.length) {
// cross-screen fallback logic below will handle empty
} else if (ties.length > 1) {
// Apply secondary tie-break ordering (overlap -> span -> perpendicular center -> MRU -> area -> z)
ties.sort((a,b)=>{
const ov = (a.overlapsPerp===b.overlapsPerp?0:(a.overlapsPerp?-1:1)); if (ov) return ov;
const span = b.overlapSpan - a.overlapSpan; if (span) return span;
const aPerp = (dir===DIR.NORTH||dir===DIR.SOUTH)?Math.abs(curCenterX-a.cx):Math.abs(curCenterY-a.cy);
const bPerp = (dir===DIR.NORTH||dir===DIR.SOUTH)?Math.abs(curCenterX-b.cx):Math.abs(curCenterY-b.cy);
if (aPerp!==bPerp) return aPerp-bPerp;
const dmru = a.dmru - b.dmru; if (dmru) return dmru;
const mru = a.mru - b.mru; if (mru) return mru;
const area = b.area - a.area; if (area) return area;
return (a.z??9999)-(b.z??9999);
});
}
const target = ties[0];
if (target) {
target.w.focus();
// Record directional MRU hit
recordDirectionalFocus(dir, target.w);
// Return the newly focused window so method chaining (e.g. .centerMouse()) applies to it
return target.w;
}
// Fallback: if no candidate on this screen, optionally jump to nearest window on adjacent screen in that direction
try {
const allScreens = Screen.all();
const curIdx = allScreens.findIndex(s => s.isEqual(screen));
let nextScreen = null;
if (dir === DIR.EAST && curIdx < allScreens.length - 1) nextScreen = allScreens[curIdx + 1];
else if (dir === DIR.WEST && curIdx > 0) nextScreen = allScreens[curIdx - 1];
else if ((dir === DIR.NORTH || dir === DIR.SOUTH) && allScreens.length > 1) {
// Pick screen whose vertical center is closest (simple heuristic)
const curMidX = cur.x + cur.width / 2;
let best = null, bestDist = Infinity;
for (let s of allScreens) {
if (s.isEqual(screen)) continue;
const f = s.frame();
const midX = f.x + f.width / 2;
const d = Math.abs(midX - curMidX);
if (d < bestDist) { best = s; bestDist = d; }
}
nextScreen = best;
}
if (nextScreen) {
const foreignEntries = WindowRegistry.get(nextScreen);
if (foreignEntries && foreignEntries.length) {
// Choose top candidate by proximity of centers along intended axis
let best = null, bestMetric = Infinity;
foreignEntries.forEach(e => {
const f = e.frame;
let metric = 0;
if (dir === DIR.EAST) metric = Math.max(0, f.x - (cur.x + cur.width));
else if (dir === DIR.WEST) metric = Math.max(0, cur.x - (f.x + f.width));
else if (dir === DIR.NORTH) metric = Math.max(0, cur.y - (f.y + f.height));
else if (dir === DIR.SOUTH) metric = Math.max(0, f.y - (cur.y + cur.height));
if (metric < bestMetric) { bestMetric = metric; best = e.win; }
});
if (best) { best.focus(); recordDirectionalFocus(dir, best); return best; }
}
}
} catch (e) { }
return this; // no change
}
// ********************************************************************************
// Modal Extensions
// ********************************************************************************
// Flash a modal in the center of a given screen
Modal.prototype.flash = function (screen) {
var tf = this.frame(),
sf = screen.frame();
this.origin = {
x: sf.x + sf.width / 2 - tf.width / 2,
y: sf.y + sf.height / 2 - tf.height / 2
}
this.show();
return this;
}
Modal.prototype.attach = function (window) {
var tf = this.frame(),
wf = window.frame(),
sf = window.screen().frame();
this.origin = {
x: Math.min(
Math.max(wf.x + wf.width / 2 - tf.width / 2, sf.x),
sf.x + sf.width - tf.width
),
y: Math.min(
Math.max(Screen.all()[0].frame().height - (wf.y + wf.height / 2 + tf.height / 2), sf.y),
sf.y + sf.height - tf.height
)
};
this.show();
return this;
}
// ********************************************************************************
// Screen Extensions
// ********************************************************************************
Screen.prototype.width = function () { // DEPRECATED
return this.flippedVisibleFrame().width - GEOM.PADDING * 2
}
Screen.prototype.height = function () { // DEPRECATED
return this.flippedVisibleFrame().height - GEOM.PADDING * 2
}
Screen.prototype.origin = function () { // DEPRECATED
return {
x: this.flippedVisibleFrame().x + GEOM.PADDING,
y: this.flippedVisibleFrame().y + GEOM.PADDING
}
}
// ********************************************************************************
// Utilities
// ********************************************************************************
// Simplified WindowRegistry (On-Demand)
// ------------------------------------
// Design goals:
// - Zero state / zero invalidation: every call enumerates live windows via Util.windowsForScreen.
// - Stable API surface for existing consumers (hints, layout) without adaptive TTL / neighbor graphs.
// - Cheap enough for interactive usage given typical window counts (< ~150). If this grows, add shallow memoization.
// - Space partitioning retained (first-space heuristic) for hint scoping.
// - Methods that previously exposed advanced data (neighbors, stats, miss streak) now return benign placeholders.
const WindowRegistry = (() => {
// Ephemeral per-screen enumeration micro-cache for focusAdjacent.
// Stores: { ts, screenId, entries:[{win,frame,cx,cy,z,hash}], indices:{byLeft,byRight,byTop,byBottom}, hashSig }
// hashSig is a sorted concatenation of window hashes to detect structural changes.
let focusEnumCache = new Map(); // screenIndex -> cache record
function buildScreenEnumeration(screen) {
const wins = Util.windowsForScreen(screen) || [];
const entries = wins.map((w,i) => { const f = w.frame(); return { win: w, frame: f, x: f.x, y: f.y, w: f.width, h: f.height, cx: f.x + f.width/2, cy: f.y + f.height/2, area: f.width*f.height, z: i, hash: w.hash() }; });
const byLeft = entries.slice().sort((a,b)=>a.x-b.x);
const byRight = entries.slice().sort((a,b)=>(a.x+a.w)-(b.x+b.w));
const byTop = entries.slice().sort((a,b)=>a.y-b.y);
const byBottom = entries.slice().sort((a,b)=>(a.y+a.h)-(b.y+b.h));
const hashSig = entries.map(e=>e.hash).sort().join(':');
return { ts: Date.now(), entries, indices:{byLeft,byRight,byTop,byBottom}, hashSig };
}
function getScreenEnumeration(screen) {
const idx = Screen.all().indexOf(screen);
if (idx === -1) return { entries: [], indices:{byLeft:[],byRight:[],byTop:[],byBottom:[]}};
const now = Date.now();
const rec = focusEnumCache.get(idx);
if (rec && (now - rec.ts) <= (PERF.FOCUS_ENUM_CACHE_MS||0)) {
// Validate structural signature still matches to avoid stale after open/close
const wins = Util.windowsForScreen(screen) || [];
const sig = wins.map(w=>w.hash()).sort().join(':');
if (sig === rec.hashSig) return rec; // reuse fully
}
const fresh = buildScreenEnumeration(screen);
focusEnumCache.set(idx, fresh);
return fresh;
}
function enumerateAll() {
const allEntries = [];
const bySpace = new Map();
Screen.all().forEach((scr, sIdx) => {
const wins = Util.windowsForScreen(scr);
wins.forEach((w, i) => {
let f; try { f = w.frame(); } catch (e) { return; }
const frame = f;
const entry = {
win: w,
frame,
x: frame.x, y: frame.y, w: frame.width, h: frame.height,
cx: frame.x + frame.width / 2,
cy: frame.y + frame.height / 2,
area: frame.width * frame.height,
z: i,
hash: w.hash(),
};
allEntries.push({ screenIndex: sIdx, entry });
// space partition (first space heuristic)
try {
const sp = w.spaces && w.spaces();
if (sp && sp.length) {
const sid = sp[0].hash ? sp[0].hash() : sp[0];
if (!bySpace.has(sid)) bySpace.set(sid, []);
bySpace.get(sid).push(entry);
}
} catch (e) { }
});
});
return { allEntries, bySpace };
}
function allEntriesFlat() { return enumerateAll().allEntries.map(o => o.entry); }
return {
get(screen) {
// Provide flat list of entries for this screen using cached enumeration
const idx = Screen.all().indexOf(screen);
if (idx === -1) return [];
return getScreenEnumeration(screen).entries;
},
containsWindow(win) {
try { const h = win.hash(); return allEntriesFlat().some(e => e.hash === h); } catch (e) { return false; }
},
getIndices(screen) {
const rec = getScreenEnumeration(screen);
return rec.indices;
},
getAllEntries() { return allEntriesFlat(); },
getAllEntriesFlattened() { return allEntriesFlat(); },
getSpaceEntries(spaceId) { const { bySpace } = enumerateAll(); return bySpace.get(spaceId) || []; },
updateWindow(win, kind) {
// Incremental update of cached screen enumeration for move/resize.
// If anything unexpected occurs, fall back by deleting the cache entry (forces rebuild on next access).
if (!win) return;
try {
const screen = win.screen && win.screen();
if (!screen) return;
const idx = Screen.all().indexOf(screen);
if (idx === -1) return;
const rec = focusEnumCache.get(idx);
if (!rec) return; // nothing cached yet
// Locate entry by hash
const h = win.hash();
const entry = rec.entries.find(e => e.hash === h);
if (!entry) { // structure changed (new window) -> invalidate
focusEnumCache.delete(idx);
return;
}
// Update geometry
const f = win.frame();
entry.frame = f;
entry.x = f.x; entry.y = f.y; entry.w = f.width; entry.h = f.height;
entry.cx = f.x + f.width / 2; entry.cy = f.y + f.height / 2;
entry.area = f.width * f.height;
// Rebuild directional indices cheaply (local sort on updated entries)
const entries = rec.entries;
rec.indices.byLeft = entries.slice().sort((a,b)=>a.x-b.x);
rec.indices.byRight = entries.slice().sort((a,b)=>(a.x+a.w)-(b.x+b.w));
rec.indices.byTop = entries.slice().sort((a,b)=>a.y-b.y);
rec.indices.byBottom = entries.slice().sort((a,b)=>(a.y+a.h)-(b.y+b.h));
rec.ts = Date.now(); // refresh timestamp so cache isn't immediately discarded
// hashSig only needs updating if structure changed (it didn't), so we leave it.
} catch (e) {
// fallback: remove all caches so next access fully rebuilds
try { const screen = win.screen && win.screen(); const idx = Screen.all().indexOf(screen); if (idx !== -1) focusEnumCache.delete(idx); } catch(e2) {}
}
},
neighbors() { return { North: [], South: [], East: [], West: [] }; },
clear() { focusEnumCache.clear(); },
stats() { return { ttl: 0, expiresIn: 0, lastDuration: 0, version: 0, cachedFlat: false }; },
recordFocusMiss() {}, getMissStreak() { return 0; }, resetMissStreak() {},
};
})();
// Layout cache: key = screenIndex|mode|windowHashes (sorted). Stores array of frames.
const LAYOUT_CACHE = new Map();
function layoutCacheKey(screen, mode, windows) {
const idx = Screen.all().indexOf(screen);
const hashes = windows.map(w => w.hash()).sort().join(':');
return idx + '|' + mode + '|' + hashes;
}
// (Removed) Array.prototype.rotate -- replaced with Util.rotateArray to avoid prototype pollution.
// Opposite direction lookup (includes diagonals for symmetry operations)
const OPPOSITE = Object.freeze({
[DIR.NORTH]: DIR.SOUTH,
[DIR.SOUTH]: DIR.NORTH,
[DIR.EAST]: DIR.WEST,
[DIR.WEST]: DIR.EAST,
[DIR.NW]: DIR.SE,
[DIR.NE]: DIR.SW,
[DIR.SW]: DIR.NE,
[DIR.SE]: DIR.NW,
});
function opposite(dir) { return OPPOSITE[dir] || dir; }
// ********************************************************************************
// Focus MRU Tracking
// ********************************************************************************
// Tracks most recently focused windows to bias adjacency (recency preference after area/overlap).
const FOCUS_MRU = [];
// Directional MRU: direction -> array of window hashes (most recent first)
const DIRECTIONAL_FOCUS_MRU = { North: [], South: [], East: [], West: [] };
function recordFocus(win) {
if (!FOCUS.ENABLE_GLOBAL_MRU || !win) return;
try {
const h = win.hash();
const idx = FOCUS_MRU.indexOf(h);
if (idx !== -1) FOCUS_MRU.splice(idx, 1);
FOCUS_MRU.unshift(h);
if (FOCUS_MRU.length > FOCUS.GLOBAL_MRU_LIMIT) FOCUS_MRU.pop();
} catch (e) { }
}
function recordDirectionalFocus(dir, win) {
if (!FOCUS.ENABLE_DIRECTIONAL_MRU || !win) return;
const bucket = DIRECTIONAL_FOCUS_MRU[dir];
if (!bucket) return;
try {
const h = win.hash();
const idx = bucket.indexOf(h);
if (idx !== -1) bucket.splice(idx, 1);
bucket.unshift(h);
if (bucket.length > FOCUS.DIRECTIONAL_MRU_LIMIT) bucket.pop();
} catch (e) { }
}
['windowDidBecomeMain', 'windowDidFocus'].forEach(ev => Event.on(ev, () => {
try { const w = Window.focused(); if (w) recordFocus(w); } catch (e) { }
}));
// ********************************************************************************
// Startup
// ********************************************************************************
const HintManager = new Hints() // ISSUE: Singleton pattern; if reloaded dynamically may leak old key bindings.
// ********************************************************************************
// Move Mode Controller
// ********************************************************************************
class MoveModeController {
constructor() {
this.active = false;
this.window = null;
this.overlay = null;
this.bindings = [];
this.lastMoveDir = null; // Track last nudge direction for anchored resize
}
_buildOverlay() {
if (!this.window) return;
try { if (this.overlay) this.overlay.close(); } catch (e) { }
try {
// Fetch or cache the app icon for the focused window
let ic = undefined;
try {
const key = iconKey(this.window);
if (!ICON_CACHE[key]) {
ICON_CACHE[key] = this.window.app().icon();
}
ic = ICON_CACHE[key];
} catch (e) { }
this.overlay = Modal.build({
text: '[ MOVE ]\nEnter/Esc to exit',
appearance: HINT.APPEARANCE,
weight: 18,
duration: 0, // persistent until closed
icon: ic
}).attach(this.window);
} catch (e) { }
}
_repositionOverlay() { // refresh modal position after window move
if (!this.overlay || !this.window) return;
try { this.overlay.attach(this.window); } catch (e) { }
}
activate(win) {
if (this.active) { // toggle off
return this.deactivate();
}
this.window = win || Window.focused();
if (!this.window) return;
this.active = true;
this._buildOverlay();
this._bindKeys();
}
_bindKeys() {
const self = this;
const push = (k, mods, fn) => { self.bindings.push(Key.on(k, mods, fn)); };
function nudgeDir(dir, amt) {
if (!self.window || !self.active) return;
self.window.nudge(dir, amt);
self.lastMoveDir = dir; // record direction for anchored resizing
self._repositionOverlay();
}
function resizeAxis(dir, amt, grow=true) {
if (!self.window || !self.active) return;
const w = self.window;
const f = w.frame();
const MIN = 40;
// Simplified, predictable semantics:
// Horizontal:
// Control+L grow width to the right (anchor left)
// Control+H shrink width from the right (anchor left)
// Control+Shift+L shrink from left (anchor right)
// Control+Shift+H grow to left (anchor right)
// For grow=true: extend in direction; grow=false: shrink from direction.
if (dir === DIR.EAST) {
if (grow) {
// Grow to right (anchor left)
f.width += amt;
} else {
// Shrink from right (anchor left)
f.width = Math.max(MIN, f.width - amt);
}
} else if (dir === DIR.WEST) {
if (grow) {
// Grow to left (anchor right)
f.x -= amt; f.width += amt;
} else {
// Shrink from left (anchor right)
if (f.width - amt >= MIN) { f.x += amt; f.width -= amt; } else { f.x += f.width - MIN; f.width = MIN; }
}
} else if (dir === DIR.SOUTH) {
if (grow) {
// Grow downward (anchor top)
f.height += amt;
} else {
// Shrink from bottom (anchor top)
f.height = Math.max(MIN, f.height - amt);
}
} else if (dir === DIR.NORTH) {
if (grow) {
// Grow upward (anchor bottom)
f.y -= amt; f.height += amt;
} else {
// Shrink from top (anchor bottom)
if (f.height - amt >= MIN) { f.y += amt; f.height -= amt; } else { f.y += f.height - MIN; f.height = MIN; }
}
}
if (f.width < MIN) f.width = MIN;
if (f.height < MIN) f.height = MIN;
w.setFrame(f);
self._repositionOverlay();
}
// Unmodified keys = LARGE nudges
push('h', [], () => nudgeDir(DIR.WEST, MOVE.NUDGE_LARGE));
push('j', [], () => nudgeDir(DIR.SOUTH, MOVE.NUDGE_LARGE));
push('k', [], () => nudgeDir(DIR.NORTH, MOVE.NUDGE_LARGE));
push('l', [], () => nudgeDir(DIR.EAST, MOVE.NUDGE_LARGE));
push('left', [], () => nudgeDir(DIR.WEST, MOVE.NUDGE_LARGE));
push('down', [], () => nudgeDir(DIR.SOUTH, MOVE.NUDGE_LARGE));
push('up', [], () => nudgeDir(DIR.NORTH, MOVE.NUDGE_LARGE));
push('right', [], () => nudgeDir(DIR.EAST, MOVE.NUDGE_LARGE));
// Shift-modified keys = SMALL nudges
push('h', ['shift'], () => nudgeDir(DIR.WEST, MOVE.NUDGE_SMALL));
push('j', ['shift'], () => nudgeDir(DIR.SOUTH, MOVE.NUDGE_SMALL));
push('k', ['shift'], () => nudgeDir(DIR.NORTH, MOVE.NUDGE_SMALL));
push('l', ['shift'], () => nudgeDir(DIR.EAST, MOVE.NUDGE_SMALL));
push('left', ['shift'], () => nudgeDir(DIR.WEST, MOVE.NUDGE_SMALL));
push('down', ['shift'], () => nudgeDir(DIR.SOUTH, MOVE.NUDGE_SMALL));
push('up', ['shift'], () => nudgeDir(DIR.NORTH, MOVE.NUDGE_SMALL));
push('right', ['shift'], () => nudgeDir(DIR.EAST, MOVE.NUDGE_SMALL));
// Control = grow toward direction; Control+Shift = shrink from the *opposite* edge (i.e. shrink in opposite direction).
const g = MOVE.NUDGE_SMALL;
// Grow
push('h', ['control'], () => resizeAxis(DIR.WEST, g, true));
push('l', ['control'], () => resizeAxis(DIR.EAST, g, true));
push('left', ['control'], () => resizeAxis(DIR.WEST, g, true));
push('right', ['control'], () => resizeAxis(DIR.EAST, g, true));
push('k', ['control'], () => resizeAxis(DIR.NORTH, g, true));
push('j', ['control'], () => resizeAxis(DIR.SOUTH, g, true));
push('up', ['control'], () => resizeAxis(DIR.NORTH, g, true));
push('down', ['control'], () => resizeAxis(DIR.SOUTH, g, true));
// Shrink using opposite direction target (so key direction indicates anchoring side visually)
push('h', ['control','shift'], () => resizeAxis(DIR.EAST, g, false)); // shrink from right edge
push('l', ['control','shift'], () => resizeAxis(DIR.WEST, g, false)); // shrink from left edge
push('left', ['control','shift'], () => resizeAxis(DIR.EAST, g, false));
push('right', ['control','shift'], () => resizeAxis(DIR.WEST, g, false));
push('k', ['control','shift'], () => resizeAxis(DIR.SOUTH, g, false)); // shrink from bottom
push('j', ['control','shift'], () => resizeAxis(DIR.NORTH, g, false)); // shrink from top
push('up', ['control','shift'], () => resizeAxis(DIR.SOUTH, g, false));
push('down', ['control','shift'], () => resizeAxis(DIR.NORTH, g, false));
// Exit keys
push('escape', [], () => self.deactivate());
push('return', [], () => self.deactivate());
// Auto-cancel on mouse click (like hints)
Event.once("mouseDidLeftClick", () => self.deactivate());
}
deactivate() {
if (!this.active) return;
this.active = false;
this.window = null;
this.bindings.forEach(Key.off); this.bindings = [];
try { if (this.overlay) this.overlay.close(); } catch (e) { }
this.overlay = null;
}
}
const MoveMode = new MoveModeController();
Modal.build({
text: "Ready",
appearance: HINT.APPEARANCE,
weight: 24,
icon: App.get('Phoenix').icon(),
duration: 0.5,
}).flash(Screen.all()[0]);
// ********************************************************************************
// Cache Invalidation Hooks
// ********************************************************************************
// Debounced invalidation so rapid bursts of events (e.g. live resize) only rebuild once.
; (function () {
if (typeof WindowRegistry === 'undefined') return;
let invalidateTimer = null;
const DEBOUNCE_MS = PERF.EVENT_DEBOUNCE_MS; // migrated from WINDOW_EVENT_DEBOUNCE_MS
function scheduleInvalidate(kind, winFetcher) {
// For open/close we still fallback to full clear; for move/resize attempt incremental
if (kind === 'move' || kind === 'resize') {
try { const w = winFetcher && winFetcher(); if (w) WindowRegistry.updateWindow(w, kind); } catch (e) { }
return; // no debounce needed for simple geometry update
}
if (invalidateTimer) return; // coalesce full rebuild triggers
invalidateTimer = Timer.after(DEBOUNCE_MS / 1000, () => { WindowRegistry.clear(); invalidateTimer = null; });
}
Event.on('windowDidMove', () => scheduleInvalidate('move', () => Window.focused()));
Event.on('windowDidResize', () => scheduleInvalidate('resize', () => Window.focused()));
Event.on('windowDidOpen', () => scheduleInvalidate('open'));
Event.on('windowDidClose', () => scheduleInvalidate('close'));
Event.on('windowDidMinimize', () => scheduleInvalidate('close'));
Event.on('windowDidUnminimize', () => scheduleInvalidate('open'));
Event.on('windowDidChangeScreen', () => scheduleInvalidate('open'));
Event.on('windowDidChangeSpace', () => scheduleInvalidate('open'));
Event.on('windowDidBecomeMain', () => scheduleInvalidate('move', () => Window.focused()));
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment