Skip to content
Open
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
32 changes: 32 additions & 0 deletions extensions/window-layouts/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
# Window Layouts Changelog

## [New Features & Improvements] - {PR_MERGE_DATE}

### New Layouts
- Added Horizontal 75/25 and 25/75 split layouts
- Added Vertical 75/25 and 25/75 split layouts
- Added Grid of 6 (3x2) and Grid of 9 (3x3) layouts
- Added Centered Focus layout (large center + two sidebars)
- Added Picture in Picture layout (full screen + small corner window)

### New Commands
- **Auto Layout**: automatically picks the best layout based on the number of open windows
- **Pick Layout**: browse layouts and reorder windows to assign them to specific slots
- **Save Current Layout**: save window positions by app name for later restoration
- **Restore Saved Layout**: browse and restore previously saved window positions
- **Create Custom Layout**: define custom layouts using JSON grid notation
- **Custom Layouts**: browse and apply user-defined custom layouts

### New Preferences
- **Excluded Apps**: comma-separated list of app names to exclude from tiling

### Bug Fixes
- Fixed `parseInt` + `??` fallback that would not catch `NaN` values in gap preference
- Fixed animated toast staying visible on early return (no windows/desktop found)
- Fixed `Promise.allSettled` silently swallowing window arrangement failures — now reports the count
- Fixed typo "heigth" → "height" in vertical-50-50 command description

### Improvements
- Unified `getActiveDesktop` + `getResizableWindows` into a single `getDesktopContext` call (one `canAccess` check, parallel API calls via `Promise.all`)
- Made `getUserPreferences` synchronous (underlying `getPreferenceValues` is sync)
- Exported `calculateCellSize` and `getWindowFrames` for reuse by Pick Layout command
- Restore Layout now correctly handles multiple windows from the same app

## [Improvements] - 2024-11-20

- Refined window gap alignment to match Raycast's default spacing, ensuring consistent visual layout and improved user experience
Expand Down
73 changes: 57 additions & 16 deletions extensions/window-layouts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,67 @@

</div>

### 🙌 Requires Raycast PRO
### Requires Raycast PRO

This extension takes X amount of open windows and tiles them into a chosen layout. Affected amount of windows depends on each individual layout.
This extension takes open windows and tiles them into a chosen layout. Affected amount of windows depends on each individual layout.

You can choose a gap in extension settings that is applied between and around the windows.

> **NOTE!** This extension is **NOT** an automatic tiling window manager. Layout is only applied when the command is run from Raycast. At least one window must have focus when running a command.

## Commands

- **Grid Layout**: Tiles windows into a grid layout.
- **Horizontal 1-2**: Tiles windows into a horizontal layout with 1 large window and 2 smaller windows.
- **Horizontal 3**: Tiles windows into a horizontal layout with 3 equal-sized windows.
- **Horizontal 30-70**: Tiles windows into a horizontal layout with 30% and 70% sized windows.
- **Horizontal 50-50**: Tiles windows into a horizontal layout with 50% sized windows.
- **Horizontal 70-30**: Tiles windows into a horizontal layout with 70% and 30% sized windows.
- **Vertical 1-2**: Tiles windows into a vertical layout with 1 large window and 2 smaller windows.
- **Vertical 2-1**: Tiles windows into a vertical layout with 2 smaller windows and 1 large window.
- **Vertical 3**: Tiles windows into a vertical layout with 3 equal-sized windows.
- **Vertical 30-70**: Tiles windows into a vertical layout with 30% and 70% sized windows.
- **Vertical 50-50**: Tiles windows into a vertical layout with 50% sized windows.
- **Vertical 70-30**: Tiles windows into a vertical layout with 70% and 30% sized windows.
## Tiling Layouts

### Horizontal

- **Horizontal 50/50** — Two windows, 50% width each, 100% height
- **Horizontal 70/30** — Two windows, 70% and 30% width, 100% height
- **Horizontal 30/70** — Two windows, 30% and 70% width, 100% height
- **Horizontal 75/25** — Two windows, 75% and 25% width, 100% height
- **Horizontal 25/75** — Two windows, 25% and 75% width, 100% height
- **Horizontal 3 Columns** — Three windows, 1/3 width each, 100% height
- **Horizontal 1+2** — Three windows, one large left, two stacked right
- **Horizontal 2+1** — Three windows, two stacked left, one large right

### Vertical

- **Vertical 50/50** — Two windows, 100% width, 50% height each
- **Vertical 70/30** — Two windows, 100% width, 70% and 30% height
- **Vertical 30/70** — Two windows, 100% width, 30% and 70% height
- **Vertical 75/25** — Two windows, 100% width, 75% and 25% height
- **Vertical 25/75** — Two windows, 100% width, 25% and 75% height
- **Vertical 3 Rows** — Three windows, 100% width, 1/3 height each
- **Vertical 1+2** — Three windows, one large top, two side-by-side bottom
- **Vertical 2+1** — Three windows, two side-by-side top, one large bottom

### Grid

- **Grid of 4** — Even grid, 2 columns, 2 rows
- **Grid of 6** — Even grid, 3 columns, 2 rows
- **Grid of 9** — Even grid, 3 columns, 3 rows

### Special

- **Centered Focus** — Three windows, large center (60%), two narrow sidebars
- **Picture in Picture** — Main window on top (80%), small PiP bottom-right. With 3 windows, the bottom-left is also filled

## Smart Commands

- **Auto Layout** — Automatically picks the best layout based on the number of open windows
- **Pick Layout** — Browse all layouts and reorder windows to decide which goes in which slot

## Save & Restore

- **Save Current Layout** — Save the current window positions to restore later
- **Restore Saved Layout** — Browse and restore previously saved window positions

## Custom Layouts

- **Create Custom Layout** — Define your own layout using a JSON grid (e.g. `[[1,1,2],[3,4,2]]`)
- **Custom Layouts** — Browse and apply your custom layouts

## Preferences

- **Gap** — Gap size between and around windows (0–128px)
- **Disable notifications** — Hide the "Windows arranged" success toast
- **Keep Raycast open** — Keep the Raycast window open after tiling
- **Excluded Apps** — Comma-separated list of app names to exclude from tiling (e.g. "Finder, Spotify")
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added extensions/window-layouts/assets/icons/grid-6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added extensions/window-layouts/assets/icons/pip.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extensions/window-layouts/metadata/window-layouts-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extensions/window-layouts/metadata/window-layouts-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extensions/window-layouts/metadata/window-layouts-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
108 changes: 107 additions & 1 deletion extensions/window-layouts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
{
"name": "vertical-50-50",
"title": "Vertical 50 / 50",
"description": "Two windows - 100% width and 50% heigth.",
"description": "Two windows - 100% width and 50% height.",
"mode": "no-view",
"icon": "icons/vertical-50-50.png"
},
Expand Down Expand Up @@ -104,6 +104,104 @@
"description": "Three windows - first 100% width and 50% height, aligned to the bottom. Other two 50% width and 50% height, aligned to the top.",
"mode": "no-view",
"icon": "icons/vertical-2-1.png"
},
{
"name": "create-custom-layout",
"title": "Create Custom Layout",
"description": "Define a custom window layout using a JSON grid.",
"mode": "view",
"icon": "icons/create-custom-layout.png"
},
{
"name": "custom-layouts",
"title": "Custom Layouts",
"description": "Browse and apply your custom window layouts.",
"mode": "view",
"icon": "icons/custom-layouts.png"
},
{
"name": "pick-layout",
"title": "Pick Layout",
"description": "Choose a layout and reorder windows to decide which window goes in which slot.",
"mode": "view",
"icon": "icons/pick-layout.png"
},
{
"name": "save-layout",
"title": "Save Current Layout",
"description": "Save the current window positions so you can restore them later.",
"mode": "view",
"icon": "icons/save-layout.png"
},
{
"name": "restore-layout",
"title": "Restore Saved Layout",
"description": "Restore a previously saved window layout.",
"mode": "view",
"icon": "icons/restore-layout.png"
},
{
"name": "auto-layout",
"title": "Auto Layout",
"description": "Automatically pick the best layout based on the number of open windows.",
"mode": "no-view",
"icon": "icons/auto-layout.png"
},
{
"name": "horizontal-75-25",
"title": "Horizontal 75 / 25",
"description": "Two windows - 75% and 25% width, 100% height.",
"mode": "no-view",
"icon": "icons/horizontal-75-25.png"
},
{
"name": "horizontal-25-75",
"title": "Horizontal 25 / 75",
"description": "Two windows - 25% and 75% width, 100% height.",
"mode": "no-view",
"icon": "icons/horizontal-25-75.png"
},
{
"name": "vertical-75-25",
"title": "Vertical 75 / 25",
"description": "Two windows - 100% width, 75% and 25% height.",
"mode": "no-view",
"icon": "icons/vertical-75-25.png"
},
{
"name": "vertical-25-75",
"title": "Vertical 25 / 75",
"description": "Two windows - 100% width, 25% and 75% height.",
"mode": "no-view",
"icon": "icons/vertical-25-75.png"
},
{
"name": "grid-6",
"title": "Grid of 6",
"description": "Even grid of 6 windows - 3 columns and 2 rows.",
"mode": "no-view",
"icon": "icons/grid-6.png"
},
{
"name": "grid-3x3",
"title": "Grid of 9",
"description": "Even grid of 9 windows - 3 columns and 3 rows.",
"mode": "no-view",
"icon": "icons/grid-3x3.png"
},
{
"name": "centered-focus",
"title": "Centered Focus",
"description": "Three windows - large center (60%), two narrow sidebars (20% each).",
"mode": "no-view",
"icon": "icons/centered-focus.png"
},
{
"name": "pip",
"title": "Picture in Picture",
"description": "Main window on top (80%), small PiP in the bottom-right corner. With 3 windows, the bottom-left is also filled.",
"mode": "no-view",
"icon": "icons/pip.png"
}
],
"preferences": [
Expand Down Expand Up @@ -165,6 +263,14 @@
"default": false,
"title": "Keep Raycast open after tiling",
"description": "By default, Raycast window will close after tiling is complete. This option will disable the behavior."
},
{
"type": "textfield",
"name": "excludedApps",
"required": false,
"title": "Excluded Apps",
"description": "Comma-separated list of app names to exclude from tiling (e.g. \"Finder, Spotify\").",
"placeholder": "Finder, Spotify"
}
],
"dependencies": {
Expand Down
33 changes: 33 additions & 0 deletions extensions/window-layouts/src/auto-layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { showFailureToast } from "@raycast/utils";
import { createLayout, getDesktopContext } from "./utils";
import { GRID, GRID_3X3, GRID_6, HORIZONTAL_1_2, HORIZONTAL_50_50 } from "./utils/layout";
import type { Layout } from "./utils/layout/types";

const AUTO_LAYOUTS: Record<number, Layout> = {
1: [[1]],
2: HORIZONTAL_50_50,
3: HORIZONTAL_1_2,
4: GRID,
5: GRID_6,
6: GRID_6,
7: GRID_3X3,
8: GRID_3X3,
9: GRID_3X3,
};

export default async function Command() {
try {
const context = await getDesktopContext();
if (!context) return;

const windowCount = context.windows.length;
const layout = AUTO_LAYOUTS[windowCount] ?? GRID_3X3;

return createLayout(layout, context);
} catch (error) {
console.error("Auto layout error:", error);
await showFailureToast("Failed to auto-arrange windows", {
message: error instanceof Error ? error.message : undefined,
});
}
}
6 changes: 6 additions & 0 deletions extensions/window-layouts/src/centered-focus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createLayout } from "./utils";
import { CENTERED_FOCUS } from "./utils/layout";

export default function Command() {
return createLayout(CENTERED_FOCUS);
}
75 changes: 75 additions & 0 deletions extensions/window-layouts/src/create-custom-layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Action, ActionPanel, Form, showToast, Toast, popToRoot } from "@raycast/api";
import { validateLayout, getLayoutValidationMessage } from "./utils";
import { saveCustomLayout } from "./utils/custom-layouts";

export default function Command() {
async function handleSubmit(values: { name: string; grid: string }) {
const name = values.name.trim();
if (!name) {
await showToast({ style: Toast.Style.Failure, title: "Please enter a name" });
return;
}

let grid: number[][];
try {
grid = JSON.parse(values.grid);
if (!Array.isArray(grid) || !grid.every((row) => Array.isArray(row))) {
throw new Error("Must be a 2D array");
}
} catch {
await showToast({
style: Toast.Style.Failure,
title: "Invalid JSON",
message: "Enter a valid 2D array, e.g. [[1,1,2],[3,4,2]]",
});
return;
}

const validation = validateLayout(grid);
if (!validation.isValid) {
await showToast({
style: Toast.Style.Failure,
title: "Invalid layout",
message: getLayoutValidationMessage(validation.errors),
});
return;
}

await saveCustomLayout({
name,
grid,
createdAt: new Date().toISOString(),
});

await showToast({
style: Toast.Style.Success,
title: `Layout "${name}" created`,
});
await popToRoot();
}

return (
<Form
actions={
<ActionPanel>
<Action.SubmitForm title="Create Layout" onSubmit={handleSubmit} />
</ActionPanel>
}
>
<Form.TextField id="name" title="Layout Name" placeholder="My Custom Layout" />
<Form.TextArea
id="grid"
title="Grid (JSON)"
placeholder="[[1,1,2],[3,4,2]]"
info={
"Define a 2D grid where each number represents a window.\n" +
"Repeated numbers make a window span multiple cells.\n\n" +
"Examples:\n" +
" [[1,2]] → Two columns 50/50\n" +
" [[1,1,2],[3,4,2]] → 4 windows, #1 spans 2 cols, #2 spans 2 rows\n" +
" [[1,2],[1,3]] → #1 tall left, #2 and #3 stacked right"
}
/>
</Form>
);
}
Loading
Loading