diff --git a/extensions/window-layouts/CHANGELOG.md b/extensions/window-layouts/CHANGELOG.md index e5df433778a..10faeacebc0 100644 --- a/extensions/window-layouts/CHANGELOG.md +++ b/extensions/window-layouts/CHANGELOG.md @@ -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 diff --git a/extensions/window-layouts/README.md b/extensions/window-layouts/README.md index 978714a7b80..c36ce804773 100644 --- a/extensions/window-layouts/README.md +++ b/extensions/window-layouts/README.md @@ -8,26 +8,67 @@ -### 🙌 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") diff --git a/extensions/window-layouts/assets/icons/auto-layout.png b/extensions/window-layouts/assets/icons/auto-layout.png new file mode 100644 index 00000000000..7cad56493cd Binary files /dev/null and b/extensions/window-layouts/assets/icons/auto-layout.png differ diff --git a/extensions/window-layouts/assets/icons/centered-focus.png b/extensions/window-layouts/assets/icons/centered-focus.png new file mode 100644 index 00000000000..20511101053 Binary files /dev/null and b/extensions/window-layouts/assets/icons/centered-focus.png differ diff --git a/extensions/window-layouts/assets/icons/create-custom-layout.png b/extensions/window-layouts/assets/icons/create-custom-layout.png new file mode 100644 index 00000000000..612e49cd731 Binary files /dev/null and b/extensions/window-layouts/assets/icons/create-custom-layout.png differ diff --git a/extensions/window-layouts/assets/icons/custom-layouts.png b/extensions/window-layouts/assets/icons/custom-layouts.png new file mode 100644 index 00000000000..d0fb29d5d18 Binary files /dev/null and b/extensions/window-layouts/assets/icons/custom-layouts.png differ diff --git a/extensions/window-layouts/assets/icons/grid-3x3.png b/extensions/window-layouts/assets/icons/grid-3x3.png new file mode 100644 index 00000000000..e62e541d5bf Binary files /dev/null and b/extensions/window-layouts/assets/icons/grid-3x3.png differ diff --git a/extensions/window-layouts/assets/icons/grid-6.png b/extensions/window-layouts/assets/icons/grid-6.png new file mode 100644 index 00000000000..4458e4acd39 Binary files /dev/null and b/extensions/window-layouts/assets/icons/grid-6.png differ diff --git a/extensions/window-layouts/assets/icons/horizontal-25-75.png b/extensions/window-layouts/assets/icons/horizontal-25-75.png new file mode 100644 index 00000000000..f73d4f75689 Binary files /dev/null and b/extensions/window-layouts/assets/icons/horizontal-25-75.png differ diff --git a/extensions/window-layouts/assets/icons/horizontal-75-25.png b/extensions/window-layouts/assets/icons/horizontal-75-25.png new file mode 100644 index 00000000000..c9d3d6e9f49 Binary files /dev/null and b/extensions/window-layouts/assets/icons/horizontal-75-25.png differ diff --git a/extensions/window-layouts/assets/icons/pick-layout.png b/extensions/window-layouts/assets/icons/pick-layout.png new file mode 100644 index 00000000000..50b8d398ff9 Binary files /dev/null and b/extensions/window-layouts/assets/icons/pick-layout.png differ diff --git a/extensions/window-layouts/assets/icons/pip.png b/extensions/window-layouts/assets/icons/pip.png new file mode 100644 index 00000000000..7ff6bec94a1 Binary files /dev/null and b/extensions/window-layouts/assets/icons/pip.png differ diff --git a/extensions/window-layouts/assets/icons/restore-layout.png b/extensions/window-layouts/assets/icons/restore-layout.png new file mode 100644 index 00000000000..bdc9bf23db6 Binary files /dev/null and b/extensions/window-layouts/assets/icons/restore-layout.png differ diff --git a/extensions/window-layouts/assets/icons/save-layout.png b/extensions/window-layouts/assets/icons/save-layout.png new file mode 100644 index 00000000000..f413a5fba7c Binary files /dev/null and b/extensions/window-layouts/assets/icons/save-layout.png differ diff --git a/extensions/window-layouts/assets/icons/vertical-25-75.png b/extensions/window-layouts/assets/icons/vertical-25-75.png new file mode 100644 index 00000000000..c6b76d5dc99 Binary files /dev/null and b/extensions/window-layouts/assets/icons/vertical-25-75.png differ diff --git a/extensions/window-layouts/assets/icons/vertical-75-25.png b/extensions/window-layouts/assets/icons/vertical-75-25.png new file mode 100644 index 00000000000..fb69493183c Binary files /dev/null and b/extensions/window-layouts/assets/icons/vertical-75-25.png differ diff --git a/extensions/window-layouts/metadata/window-layouts-1.png b/extensions/window-layouts/metadata/window-layouts-1.png index 2610f0157a1..e8f50ee9814 100644 Binary files a/extensions/window-layouts/metadata/window-layouts-1.png and b/extensions/window-layouts/metadata/window-layouts-1.png differ diff --git a/extensions/window-layouts/metadata/window-layouts-2.png b/extensions/window-layouts/metadata/window-layouts-2.png index b7149236b10..eb5286ff193 100644 Binary files a/extensions/window-layouts/metadata/window-layouts-2.png and b/extensions/window-layouts/metadata/window-layouts-2.png differ diff --git a/extensions/window-layouts/metadata/window-layouts-3.png b/extensions/window-layouts/metadata/window-layouts-3.png index de4d44171fd..a2d61e61b08 100644 Binary files a/extensions/window-layouts/metadata/window-layouts-3.png and b/extensions/window-layouts/metadata/window-layouts-3.png differ diff --git a/extensions/window-layouts/package.json b/extensions/window-layouts/package.json index bbb0d6e55f2..7996bd7b254 100644 --- a/extensions/window-layouts/package.json +++ b/extensions/window-layouts/package.json @@ -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" }, @@ -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": [ @@ -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": { diff --git a/extensions/window-layouts/src/auto-layout.ts b/extensions/window-layouts/src/auto-layout.ts new file mode 100644 index 00000000000..79052d90bdc --- /dev/null +++ b/extensions/window-layouts/src/auto-layout.ts @@ -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 = { + 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, + }); + } +} diff --git a/extensions/window-layouts/src/centered-focus.ts b/extensions/window-layouts/src/centered-focus.ts new file mode 100644 index 00000000000..cbef1ffaa57 --- /dev/null +++ b/extensions/window-layouts/src/centered-focus.ts @@ -0,0 +1,6 @@ +import { createLayout } from "./utils"; +import { CENTERED_FOCUS } from "./utils/layout"; + +export default function Command() { + return createLayout(CENTERED_FOCUS); +} diff --git a/extensions/window-layouts/src/create-custom-layout.tsx b/extensions/window-layouts/src/create-custom-layout.tsx new file mode 100644 index 00000000000..96eb62da907 --- /dev/null +++ b/extensions/window-layouts/src/create-custom-layout.tsx @@ -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 ( +
+ + + } + > + + + + ); +} diff --git a/extensions/window-layouts/src/custom-layouts.tsx b/extensions/window-layouts/src/custom-layouts.tsx new file mode 100644 index 00000000000..c933796cb22 --- /dev/null +++ b/extensions/window-layouts/src/custom-layouts.tsx @@ -0,0 +1,73 @@ +import { Action, ActionPanel, Icon, List, showToast, Toast } from "@raycast/api"; +import { useEffect, useState } from "react"; +import { createLayout } from "./utils"; +import { deleteCustomLayout, getCustomLayouts, type CustomLayout } from "./utils/custom-layouts"; + +export default function Command() { + const [layouts, setLayouts] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + async function loadLayouts() { + setIsLoading(true); + const all = await getCustomLayouts(); + setLayouts(all); + setIsLoading(false); + } + + useEffect(() => { + loadLayouts(); + }, []); + + async function handleApply(layout: CustomLayout) { + await createLayout(layout.grid); + } + + async function handleDelete(layout: CustomLayout) { + await deleteCustomLayout(layout.name); + await loadLayouts(); + await showToast({ style: Toast.Style.Success, title: `Deleted "${layout.name}"` }); + } + + function gridPreview(grid: number[][]): string { + const rows = grid.length; + const firstRow = grid[0]; + if (rows === 0 || !Array.isArray(firstRow)) { + return "Invalid grid"; + } + const cols = firstRow.length; + const windowCount = new Set(grid.flat()).size; + return `${rows}×${cols} grid, ${windowCount} windows`; + } + + return ( + + {layouts.length === 0 && !isLoading ? ( + + ) : ( + layouts.map((layout) => ( + + handleApply(layout)} /> + handleDelete(layout)} + /> + + } + /> + )) + )} + + ); +} diff --git a/extensions/window-layouts/src/grid-3x3.ts b/extensions/window-layouts/src/grid-3x3.ts new file mode 100644 index 00000000000..0bf0808a13e --- /dev/null +++ b/extensions/window-layouts/src/grid-3x3.ts @@ -0,0 +1,6 @@ +import { createLayout } from "./utils"; +import { GRID_3X3 } from "./utils/layout"; + +export default function Command() { + return createLayout(GRID_3X3); +} diff --git a/extensions/window-layouts/src/grid-6.ts b/extensions/window-layouts/src/grid-6.ts new file mode 100644 index 00000000000..bffdae79fa0 --- /dev/null +++ b/extensions/window-layouts/src/grid-6.ts @@ -0,0 +1,6 @@ +import { createLayout } from "./utils"; +import { GRID_6 } from "./utils/layout"; + +export default function Command() { + return createLayout(GRID_6); +} diff --git a/extensions/window-layouts/src/horizontal-25-75.ts b/extensions/window-layouts/src/horizontal-25-75.ts new file mode 100644 index 00000000000..7ef9caa27d9 --- /dev/null +++ b/extensions/window-layouts/src/horizontal-25-75.ts @@ -0,0 +1,6 @@ +import { createLayout } from "./utils"; +import { HORIZONTAL_25_75 } from "./utils/layout"; + +export default function Command() { + return createLayout(HORIZONTAL_25_75); +} diff --git a/extensions/window-layouts/src/horizontal-75-25.ts b/extensions/window-layouts/src/horizontal-75-25.ts new file mode 100644 index 00000000000..c225e5c3073 --- /dev/null +++ b/extensions/window-layouts/src/horizontal-75-25.ts @@ -0,0 +1,6 @@ +import { createLayout } from "./utils"; +import { HORIZONTAL_75_25 } from "./utils/layout"; + +export default function Command() { + return createLayout(HORIZONTAL_75_25); +} diff --git a/extensions/window-layouts/src/pick-layout.tsx b/extensions/window-layouts/src/pick-layout.tsx new file mode 100644 index 00000000000..4137950909e --- /dev/null +++ b/extensions/window-layouts/src/pick-layout.tsx @@ -0,0 +1,171 @@ +import { Action, ActionPanel, closeMainWindow, Icon, List, showToast, Toast, WindowManagement } from "@raycast/api"; +import { useEffect, useState } from "react"; +import { + calculateCellSize, + getDesktopContext, + getUserPreferences, + getWindowFrames, + type DesktopContext, +} from "./utils"; +import type { Layout } from "./utils/layout/types"; +import * as layouts from "./utils/layout"; + +type NamedLayout = { name: string; layout: Layout; slots: number }; + +const AVAILABLE_LAYOUTS: NamedLayout[] = [ + { name: "Horizontal 50/50", layout: layouts.HORIZONTAL_50_50, slots: 2 }, + { name: "Horizontal 70/30", layout: layouts.HORIZONTAL_70_30, slots: 2 }, + { name: "Horizontal 30/70", layout: layouts.HORIZONTAL_30_70, slots: 2 }, + { name: "Horizontal 75/25", layout: layouts.HORIZONTAL_75_25, slots: 2 }, + { name: "Horizontal 25/75", layout: layouts.HORIZONTAL_25_75, slots: 2 }, + { name: "Horizontal 3 Columns", layout: layouts.HORIZONTAL_3, slots: 3 }, + { name: "Horizontal 1+2", layout: layouts.HORIZONTAL_1_2, slots: 3 }, + { name: "Horizontal 2+1", layout: layouts.HORIZONTAL_2_1, slots: 3 }, + { name: "Vertical 50/50", layout: layouts.VERTICAL_50_50, slots: 2 }, + { name: "Vertical 70/30", layout: layouts.VERTICAL_70_30, slots: 2 }, + { name: "Vertical 30/70", layout: layouts.VERTICAL_30_70, slots: 2 }, + { name: "Vertical 75/25", layout: layouts.VERTICAL_75_25, slots: 2 }, + { name: "Vertical 25/75", layout: layouts.VERTICAL_25_75, slots: 2 }, + { name: "Vertical 3 Rows", layout: layouts.VERTICAL_3, slots: 3 }, + { name: "Vertical 1+2", layout: layouts.VERTICAL_1_2, slots: 3 }, + { name: "Vertical 2+1", layout: layouts.VERTICAL_2_1, slots: 3 }, + { name: "Grid of 4", layout: layouts.GRID, slots: 4 }, + { name: "Grid of 6", layout: layouts.GRID_6, slots: 6 }, + { name: "Grid of 9", layout: layouts.GRID_3X3, slots: 9 }, + { name: "Centered Focus", layout: layouts.CENTERED_FOCUS, slots: 3 }, + { name: "Picture in Picture", layout: layouts.PIP, slots: 2 }, +]; + +export default function Command() { + const [context, setContext] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [selectedWindows, setSelectedWindows] = useState([]); + + useEffect(() => { + (async () => { + const ctx = await getDesktopContext(); + if (ctx) { + setContext(ctx); + setSelectedWindows(ctx.windows); + } + setIsLoading(false); + })(); + }, []); + + async function applyWithOrder(namedLayout: NamedLayout) { + if (!context) return; + + const preferences = getUserPreferences(); + const gap = preferences.gap; + const { width, height } = context.desktop.size; + const layout = namedLayout.layout; + const numberOfRows = layout.length; + const numberOfColumns = layout[0].length; + + const { cellWidth, cellHeight } = calculateCellSize({ + screenWidth: width, + screenHeight: height, + numberOfRows, + numberOfColumns, + gap, + }); + const windowFrames = getWindowFrames({ layout, cellWidth, cellHeight, gap }); + + const toast = await showToast({ style: Toast.Style.Animated, title: "Arranging windows..." }); + + let failures = 0; + const entries = Object.entries(windowFrames); + for (const [windowNumber, frame] of entries) { + const idx = parseInt(windowNumber, 10) - 1; + if (idx >= selectedWindows.length) continue; + try { + await WindowManagement.setWindowBounds({ + id: selectedWindows[idx].id, + desktopId: context.desktop.id, + bounds: { + position: { x: frame.x, y: frame.y }, + size: { width: frame.width, height: frame.height }, + }, + }); + } catch { + failures++; + } + } + + if (!preferences.keepWindowOpenAfterTiling) { + await closeMainWindow(); + } + + if (failures > 0) { + toast.style = Toast.Style.Failure; + toast.title = `${failures} window(s) could not be arranged`; + } else if (!preferences.disableToasts) { + toast.style = Toast.Style.Success; + toast.title = "Windows arranged"; + } else { + toast.hide(); + } + } + + function moveWindowUp(index: number) { + if (index <= 0) return; + const newOrder = [...selectedWindows]; + [newOrder[index - 1], newOrder[index]] = [newOrder[index], newOrder[index - 1]]; + setSelectedWindows(newOrder); + } + + function moveWindowDown(index: number) { + if (index >= selectedWindows.length - 1) return; + const newOrder = [...selectedWindows]; + [newOrder[index], newOrder[index + 1]] = [newOrder[index + 1], newOrder[index]]; + setSelectedWindows(newOrder); + } + + const windowCount = selectedWindows.length; + const matchingLayouts = AVAILABLE_LAYOUTS.filter((l) => l.slots <= windowCount); + + return ( + + + {selectedWindows.map((w, i) => ( + + moveWindowUp(i)} + /> + moveWindowDown(i)} + /> + + } + /> + ))} + + + {matchingLayouts.map((nl) => ( + + applyWithOrder(nl)} /> + + } + /> + ))} + + + ); +} diff --git a/extensions/window-layouts/src/pip.ts b/extensions/window-layouts/src/pip.ts new file mode 100644 index 00000000000..fdb61c68f14 --- /dev/null +++ b/extensions/window-layouts/src/pip.ts @@ -0,0 +1,6 @@ +import { createLayout } from "./utils"; +import { PIP } from "./utils/layout"; + +export default function Command() { + return createLayout(PIP); +} diff --git a/extensions/window-layouts/src/restore-layout.tsx b/extensions/window-layouts/src/restore-layout.tsx new file mode 100644 index 00000000000..d592e4d833b --- /dev/null +++ b/extensions/window-layouts/src/restore-layout.tsx @@ -0,0 +1,93 @@ +import { Action, ActionPanel, closeMainWindow, Icon, List, showToast, Toast, WindowManagement } from "@raycast/api"; +import { useEffect, useState } from "react"; +import { getDesktopContext } from "./utils"; +import { deleteSavedLayout, getSavedLayouts, type SavedLayout } from "./utils/saved-layouts"; + +export default function Command() { + const [layouts, setLayouts] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + async function loadLayouts() { + setIsLoading(true); + const saved = await getSavedLayouts(); + setLayouts(saved); + setIsLoading(false); + } + + useEffect(() => { + loadLayouts(); + }, []); + + async function handleRestore(layout: SavedLayout) { + const context = await getDesktopContext(); + if (!context) return; + + // Track which windows have been matched so duplicates of the same app + // are assigned to separate saved positions + const usedWindowIds = new Set(); + let restored = 0; + + for (const saved of layout.windows) { + const match = context.windows.find( + (w) => !usedWindowIds.has(w.id) && (w.application?.name ?? "").toLowerCase() === saved.appName.toLowerCase(), + ); + if (match) { + usedWindowIds.add(match.id); + try { + await WindowManagement.setWindowBounds({ + id: match.id, + desktopId: context.desktop.id, + bounds: { + position: { x: saved.bounds.x, y: saved.bounds.y }, + size: { width: saved.bounds.width, height: saved.bounds.height }, + }, + }); + restored++; + } catch (err) { + console.error(`Failed to restore ${saved.appName}:`, err); + } + } + } + + await closeMainWindow(); + await showToast({ + style: restored > 0 ? Toast.Style.Success : Toast.Style.Failure, + title: restored > 0 ? `Restored ${restored} window(s)` : "No matching windows found", + }); + } + + async function handleDelete(layout: SavedLayout) { + await deleteSavedLayout(layout.name); + await loadLayouts(); + await showToast({ style: Toast.Style.Success, title: `Deleted "${layout.name}"` }); + } + + return ( + + {layouts.length === 0 && !isLoading ? ( + + ) : ( + layouts.map((layout) => ( + + handleRestore(layout)} /> + handleDelete(layout)} + /> + + } + /> + )) + )} + + ); +} diff --git a/extensions/window-layouts/src/save-layout.tsx b/extensions/window-layouts/src/save-layout.tsx new file mode 100644 index 00000000000..70b2ef700f1 --- /dev/null +++ b/extensions/window-layouts/src/save-layout.tsx @@ -0,0 +1,56 @@ +import { Action, ActionPanel, Form, popToRoot, showToast, Toast } from "@raycast/api"; +import { getDesktopContext } from "./utils"; +import { saveLayout, type SavedWindow } from "./utils/saved-layouts"; + +export default function Command() { + async function handleSubmit(values: { name: string }) { + const name = values.name.trim(); + if (!name) { + await showToast({ style: Toast.Style.Failure, title: "Please enter a name" }); + return; + } + + const context = await getDesktopContext(); + if (!context) return; + + const windows: SavedWindow[] = context.windows + .filter((w) => typeof w.bounds === "object" && w.bounds !== null && "position" in w.bounds) + .map((w) => { + const bounds = w.bounds as { position: { x: number; y: number }; size: { width: number; height: number } }; + return { + appName: w.application?.name ?? "Unknown", + bounds: { + x: bounds.position.x, + y: bounds.position.y, + width: bounds.size.width, + height: bounds.size.height, + }, + }; + }); + + await saveLayout({ + name, + windows, + savedAt: new Date().toISOString(), + }); + + await showToast({ + style: Toast.Style.Success, + title: `Layout "${name}" saved`, + message: `${windows.length} window(s)`, + }); + await popToRoot(); + } + + return ( +
+ + + } + > + + + ); +} diff --git a/extensions/window-layouts/src/utils/custom-layouts.ts b/extensions/window-layouts/src/utils/custom-layouts.ts new file mode 100644 index 00000000000..a3be1af27cb --- /dev/null +++ b/extensions/window-layouts/src/utils/custom-layouts.ts @@ -0,0 +1,36 @@ +import { LocalStorage } from "@raycast/api"; + +const STORAGE_KEY = "custom-layouts"; + +export type CustomLayout = Readonly<{ + name: string; + grid: number[][]; + createdAt: string; +}>; + +export async function getCustomLayouts(): Promise { + const raw = await LocalStorage.getItem(STORAGE_KEY); + if (!raw) return []; + try { + return JSON.parse(raw) as CustomLayout[]; + } catch { + return []; + } +} + +export async function saveCustomLayout(layout: CustomLayout): Promise { + const all = await getCustomLayouts(); + const existing = all.findIndex((l) => l.name === layout.name); + if (existing >= 0) { + all[existing] = layout; + } else { + all.push(layout); + } + await LocalStorage.setItem(STORAGE_KEY, JSON.stringify(all)); +} + +export async function deleteCustomLayout(name: string): Promise { + const all = await getCustomLayouts(); + const filtered = all.filter((l) => l.name !== name); + await LocalStorage.setItem(STORAGE_KEY, JSON.stringify(filtered)); +} diff --git a/extensions/window-layouts/src/utils/get-active-desktop.ts b/extensions/window-layouts/src/utils/get-active-desktop.ts deleted file mode 100644 index e037001d0b4..00000000000 --- a/extensions/window-layouts/src/utils/get-active-desktop.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { environment, WindowManagement } from "@raycast/api"; -import { showFailureToast } from "@raycast/utils"; - -export async function getActiveDesktop() { - if (!environment.canAccess(WindowManagement)) { - showFailureToast("Error!", { - title: "Not Supported", - message: "This script requires access to Raycast WindowManagement API.", - }); - return null; - } - - const desktops = await WindowManagement.getDesktops(); - const activeDesktop = desktops?.find((desktop) => desktop.active); - - if (!desktops?.length || !activeDesktop) { - showFailureToast("Error!", { - title: "No Desktops Found", - message: "Please make sure you have at least one desktop active.", - }); - return null; - } - - return activeDesktop; -} diff --git a/extensions/window-layouts/src/utils/get-desktop-context.ts b/extensions/window-layouts/src/utils/get-desktop-context.ts new file mode 100644 index 00000000000..0df659fe08a --- /dev/null +++ b/extensions/window-layouts/src/utils/get-desktop-context.ts @@ -0,0 +1,54 @@ +import { environment, WindowManagement } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import { getUserPreferences } from "./get-user-preferences"; + +export type DesktopContext = Readonly<{ + desktop: WindowManagement.Desktop; + windows: WindowManagement.Window[]; +}>; + +export async function getDesktopContext(): Promise { + if (!environment.canAccess(WindowManagement)) { + await showFailureToast("Not Supported", { + message: "This command requires access to Raycast WindowManagement API.", + }); + return null; + } + + const [desktops, windows] = await Promise.all([ + WindowManagement.getDesktops(), + WindowManagement.getWindowsOnActiveDesktop(), + ]); + + const activeDesktop = desktops?.find((desktop) => desktop.active); + + if (!activeDesktop) { + await showFailureToast("No Desktops Found", { + message: "Please make sure you have at least one desktop active.", + }); + return null; + } + + const { excludedApps } = getUserPreferences(); + + const resizableWindows = windows?.filter((window) => { + if (!window.resizable || !window.positionable) return false; + if (excludedApps.length > 0) { + const appName = (window.application?.name ?? "").toLowerCase(); + if (excludedApps.some((excluded) => appName.includes(excluded))) return false; + } + return true; + }); + + if (!resizableWindows?.length) { + await showFailureToast("No resizable windows found", { + message: "Please make sure you have at least one resizable window on the active desktop.", + }); + return null; + } + + return { + desktop: activeDesktop, + windows: resizableWindows, + }; +} diff --git a/extensions/window-layouts/src/utils/get-resizable-windows.ts b/extensions/window-layouts/src/utils/get-resizable-windows.ts deleted file mode 100644 index 8542fb6bf8c..00000000000 --- a/extensions/window-layouts/src/utils/get-resizable-windows.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { environment, WindowManagement } from "@raycast/api"; -import { showFailureToast } from "@raycast/utils"; - -export async function getResizableWindows() { - if (!environment.canAccess(WindowManagement)) { - showFailureToast("Error!", { - title: "Not Supported", - message: "This script requires access to Raycast WindowManagement API.", - }); - return []; - } - - const windows = await WindowManagement.getWindowsOnActiveDesktop(); - const resizableWindows = windows?.filter((window) => window.resizable && window.positionable); - - if (!windows?.length || !resizableWindows?.length) { - showFailureToast("Error!", { - title: "No resizable windows found", - message: "Please make sure you have at least one resizable window on the active desktop.", - }); - return []; - } - - return resizableWindows; -} diff --git a/extensions/window-layouts/src/utils/get-user-preferences.ts b/extensions/window-layouts/src/utils/get-user-preferences.ts index 74a9b08e55c..ab230a9b1d6 100644 --- a/extensions/window-layouts/src/utils/get-user-preferences.ts +++ b/extensions/window-layouts/src/utils/get-user-preferences.ts @@ -1,30 +1,17 @@ import { getPreferenceValues } from "@raycast/api"; -import { showFailureToast } from "@raycast/utils"; -type UserPreferences = Readonly<{ - gap: number; - disableToasts: boolean; - keepWindowOpenAfterTiling: boolean; -}>; +export function getUserPreferences() { + const userPreferences = getPreferenceValues(); + const gap = parseInt(userPreferences.gap as string, 10); + const rawExcluded = (userPreferences.excludedApps as string) ?? ""; -export async function getUserPreferences(): Promise { - try { - const userPreferences = getPreferenceValues(); - - return { - gap: parseInt(userPreferences.gap as string, 10) ?? 0, - disableToasts: (userPreferences.disableToasts as boolean) ?? false, - keepWindowOpenAfterTiling: (userPreferences.keepWindowOpenAfterTiling as boolean) ?? false, - } as UserPreferences; - } catch { - await showFailureToast("Failed to get preferences", { - message: "Using default values.", - }).catch(console.error); - - return { - gap: 0, - disableToasts: false, - keepWindowOpenAfterTiling: false, - } as UserPreferences; - } + return { + gap: Number.isNaN(gap) ? 0 : gap, + disableToasts: Boolean(userPreferences.disableToasts), + keepWindowOpenAfterTiling: Boolean(userPreferences.keepWindowOpenAfterTiling), + excludedApps: rawExcluded + .split(",") + .map((s) => s.trim().toLowerCase()) + .filter(Boolean), + }; } diff --git a/extensions/window-layouts/src/utils/index.ts b/extensions/window-layouts/src/utils/index.ts index 9f6bef1e6c2..f66ef967e48 100644 --- a/extensions/window-layouts/src/utils/index.ts +++ b/extensions/window-layouts/src/utils/index.ts @@ -1,4 +1,10 @@ -export { getActiveDesktop } from "./get-active-desktop"; -export { getResizableWindows } from "./get-resizable-windows"; +export { getDesktopContext, type DesktopContext } from "./get-desktop-context"; export { getUserPreferences } from "./get-user-preferences"; -export { createLayout, getLayoutValidationMessage, validateLayout, type Layout } from "./layout"; +export { + calculateCellSize, + createLayout, + getLayoutValidationMessage, + getWindowFrames, + validateLayout, + type Layout, +} from "./layout"; diff --git a/extensions/window-layouts/src/utils/layout/create-layout.ts b/extensions/window-layouts/src/utils/layout/create-layout.ts index e18fa80834e..0a6182c15cf 100644 --- a/extensions/window-layouts/src/utils/layout/create-layout.ts +++ b/extensions/window-layouts/src/utils/layout/create-layout.ts @@ -1,12 +1,7 @@ import { closeMainWindow, showToast, Toast, WindowManagement } from "@raycast/api"; import { showFailureToast } from "@raycast/utils"; -import { - getActiveDesktop, - getLayoutValidationMessage, - getResizableWindows, - getUserPreferences, - validateLayout, -} from ".."; +import { getDesktopContext, getLayoutValidationMessage, getUserPreferences, validateLayout } from ".."; +import type { DesktopContext } from "../get-desktop-context"; import type { CellDimensions, Frame, Layout } from "./types"; /** @@ -23,7 +18,7 @@ import type { CellDimensions, Frame, Layout } from "./types"; */ // Calculate the size of each cell in the grid -function calculateCellSize({ +export function calculateCellSize({ screenWidth, screenHeight, numberOfRows, @@ -86,7 +81,7 @@ function updateBounds({ } // Determine the "frame" for each window based on the layout -function getWindowFrames({ +export function getWindowFrames({ layout, cellWidth, cellHeight, @@ -136,7 +131,7 @@ async function applyLayout({ windowFrames: Record; windowIds: string[]; desktopId: string; -}): Promise { +}): Promise { if (!windowIds.length) { throw new Error("No window IDs provided"); } @@ -158,22 +153,22 @@ async function applyLayout({ }); }); - await Promise.allSettled(updates); + const results = await Promise.allSettled(updates); + const failures = results.filter((r) => r.status === "rejected"); + return failures.length; } -export async function createLayout(layout: Layout): Promise { +export async function createLayout(layout: Layout, existingContext?: DesktopContext): Promise { const toast = await showToast({ title: "Tiling windows", style: Toast.Style.Animated, }); try { - const windows = await getResizableWindows(); - const activeDesktop = await getActiveDesktop(); + const context = existingContext ?? (await getDesktopContext()); - // getResizableWindows and getActiveDesktop show failure toasts if there are no windows or desktops - // so it's enough to just return early here - if (!activeDesktop || !windows?.length) { + if (!context) { + toast.hide(); return; } @@ -183,14 +178,15 @@ export async function createLayout(layout: Layout): Promise { await showFailureToast("Invalid layout", { message: getLayoutValidationMessage(validationStatus.errors), }); + toast.hide(); return; } - const preferences = await getUserPreferences(); - const gap = preferences.gap ?? 0; + const preferences = getUserPreferences(); + const gap = preferences.gap; - const windowIds = windows.map((window) => window.id); - const { width, height } = activeDesktop.size; + const windowIds = context.windows.map((window) => window.id); + const { width, height } = context.desktop.size; const numberOfRows = layout.length; const numberOfColumns = layout[0].length; @@ -204,28 +200,25 @@ export async function createLayout(layout: Layout): Promise { const windowFrames = getWindowFrames({ layout, cellWidth, cellHeight, gap }); - await applyLayout({ windowFrames, windowIds, desktopId: activeDesktop.id }).catch((err) => { - console.error("Error arranging windows:", err); - showFailureToast("Failed to arrange windows", { - message: err.message, - }); - }); + const failures = await applyLayout({ windowFrames, windowIds, desktopId: context.desktop.id }); if (!preferences.keepWindowOpenAfterTiling) { closeMainWindow(); } - if (!preferences.disableToasts) { + if (failures > 0) { + toast.style = Toast.Style.Failure; + toast.title = `${failures} window(s) could not be arranged`; + } else if (!preferences.disableToasts) { toast.style = Toast.Style.Success; toast.title = "Windows arranged"; - toast.show(); } else { toast.hide(); } } catch (error) { console.error("Error arranging windows:", error); - showFailureToast("Failed to arrange windows", { - message: error instanceof Error ? error.message : undefined, - }); + toast.style = Toast.Style.Failure; + toast.title = "Failed to arrange windows"; + toast.message = error instanceof Error ? error.message : undefined; } } diff --git a/extensions/window-layouts/src/utils/layout/index.ts b/extensions/window-layouts/src/utils/layout/index.ts index 77f8d73b494..71a22804af2 100644 --- a/extensions/window-layouts/src/utils/layout/index.ts +++ b/extensions/window-layouts/src/utils/layout/index.ts @@ -1,19 +1,27 @@ -export { createLayout } from "./create-layout"; +export { calculateCellSize, createLayout, getWindowFrames } from "./create-layout"; export type { Layout } from "./types"; export { getLayoutValidationMessage, validateLayout } from "./validate-layout"; export { + CENTERED_FOCUS, GRID, + GRID_3X3, + GRID_6, HORIZONTAL_1_2, HORIZONTAL_2_1, + HORIZONTAL_25_75, HORIZONTAL_3, HORIZONTAL_30_70, HORIZONTAL_50_50, HORIZONTAL_70_30, + HORIZONTAL_75_25, + PIP, VERTICAL_1_2, VERTICAL_2_1, + VERTICAL_25_75, VERTICAL_3, VERTICAL_30_70, VERTICAL_50_50, VERTICAL_70_30, + VERTICAL_75_25, } from "./window-layouts"; diff --git a/extensions/window-layouts/src/utils/layout/window-layouts.ts b/extensions/window-layouts/src/utils/layout/window-layouts.ts index 387bb9ff0ab..deebdff1d7f 100644 --- a/extensions/window-layouts/src/utils/layout/window-layouts.ts +++ b/extensions/window-layouts/src/utils/layout/window-layouts.ts @@ -13,17 +13,36 @@ import type { Layout } from "./types"; +// --- Grids --- + export const GRID: Layout = [ [1, 2], [3, 4], ]; +export const GRID_3X3: Layout = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], +]; + +export const GRID_6: Layout = [ + [1, 2, 3], + [4, 5, 6], +]; + +// --- Horizontal --- + export const HORIZONTAL_50_50: Layout = [[1, 2]]; export const HORIZONTAL_70_30: Layout = [[1, 1, 2]]; export const HORIZONTAL_30_70: Layout = [[1, 2, 2]]; +export const HORIZONTAL_75_25: Layout = [[1, 1, 1, 2]]; + +export const HORIZONTAL_25_75: Layout = [[1, 2, 2, 2]]; + export const HORIZONTAL_3: Layout = [[1, 2, 3]]; export const HORIZONTAL_1_2: Layout = [ @@ -36,12 +55,18 @@ export const HORIZONTAL_2_1: Layout = [ [3, 1], ]; +// --- Vertical --- + export const VERTICAL_50_50: Layout = [[1], [2]]; export const VERTICAL_70_30: Layout = [[1], [1], [2]]; export const VERTICAL_30_70: Layout = [[1], [2], [2]]; +export const VERTICAL_75_25: Layout = [[1], [1], [1], [2]]; + +export const VERTICAL_25_75: Layout = [[1], [2], [2], [2]]; + export const VERTICAL_3: Layout = [[1], [2], [3]]; export const VERTICAL_1_2: Layout = [ @@ -53,3 +78,19 @@ export const VERTICAL_2_1: Layout = [ [2, 3], [1, 1], ]; + +// --- Special --- + +export const CENTERED_FOCUS: Layout = [ + [2, 1, 1, 1, 3], + [2, 1, 1, 1, 3], + [2, 1, 1, 1, 3], +]; + +export const PIP: Layout = [ + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [3, 3, 3, 2, 2], +]; diff --git a/extensions/window-layouts/src/utils/saved-layouts.ts b/extensions/window-layouts/src/utils/saved-layouts.ts new file mode 100644 index 00000000000..349ccd62dba --- /dev/null +++ b/extensions/window-layouts/src/utils/saved-layouts.ts @@ -0,0 +1,46 @@ +import { LocalStorage } from "@raycast/api"; + +const STORAGE_KEY = "saved-layouts"; + +export type SavedWindow = Readonly<{ + appName: string; + bounds: { + x: number; + y: number; + width: number; + height: number; + }; +}>; + +export type SavedLayout = Readonly<{ + name: string; + windows: SavedWindow[]; + savedAt: string; +}>; + +export async function getSavedLayouts(): Promise { + const raw = await LocalStorage.getItem(STORAGE_KEY); + if (!raw) return []; + try { + return JSON.parse(raw) as SavedLayout[]; + } catch { + return []; + } +} + +export async function saveLayout(layout: SavedLayout): Promise { + const layouts = await getSavedLayouts(); + const existing = layouts.findIndex((l) => l.name === layout.name); + if (existing >= 0) { + layouts[existing] = layout; + } else { + layouts.push(layout); + } + await LocalStorage.setItem(STORAGE_KEY, JSON.stringify(layouts)); +} + +export async function deleteSavedLayout(name: string): Promise { + const layouts = await getSavedLayouts(); + const filtered = layouts.filter((l) => l.name !== name); + await LocalStorage.setItem(STORAGE_KEY, JSON.stringify(filtered)); +} diff --git a/extensions/window-layouts/src/vertical-25-75.ts b/extensions/window-layouts/src/vertical-25-75.ts new file mode 100644 index 00000000000..cfa0a069240 --- /dev/null +++ b/extensions/window-layouts/src/vertical-25-75.ts @@ -0,0 +1,6 @@ +import { createLayout } from "./utils"; +import { VERTICAL_25_75 } from "./utils/layout"; + +export default function Command() { + return createLayout(VERTICAL_25_75); +} diff --git a/extensions/window-layouts/src/vertical-75-25.ts b/extensions/window-layouts/src/vertical-75-25.ts new file mode 100644 index 00000000000..f6c838bb8b3 --- /dev/null +++ b/extensions/window-layouts/src/vertical-75-25.ts @@ -0,0 +1,6 @@ +import { createLayout } from "./utils"; +import { VERTICAL_75_25 } from "./utils/layout"; + +export default function Command() { + return createLayout(VERTICAL_75_25); +}