Initial commit
This commit is contained in:
4
.eslintrc.json
Normal file
4
.eslintrc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["@raycast"]
|
||||
}
|
||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# Raycast specific files
|
||||
raycast-env.d.ts
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": false
|
||||
}
|
||||
41
CHANGELOG.md
Normal file
41
CHANGELOG.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Yabai Changelog
|
||||
|
||||
## [Added search windows command] - 2025-02-25
|
||||
|
||||
- Adds "Focus Next Same App" command
|
||||
- Focuses on the next window with the same application name
|
||||
- MacOS has same feature, but it is only available in the current space
|
||||
- Adds "Search Windows" command
|
||||
- Searches for windows managed by Yabai
|
||||
- Allows to search by window title or application name, it is really useful when windows have the same app name
|
||||
|
||||
## [Added window movement commands] - 2024-11-25
|
||||
|
||||
- Adds "Focus Active Window" commands
|
||||
- "Focus Active Window North" moves window focus to the north.
|
||||
- "Focus Active Window South" moves window focus to the south.
|
||||
- "Focus Active Window East" moves window focus to the east.
|
||||
- "Focus Active Window West" moves window focus to the west.
|
||||
- Adds "Swap Active Window" commands
|
||||
- "Swap Active Window North" swaps the active window with the window to the north.
|
||||
- "Swap Active Window South" swaps the active window with the window to the south.
|
||||
- "Swap Active Window East" swaps the active window with the window to the east.
|
||||
- "Swap Active Window West" swaps the active window with the window to the west.
|
||||
- Adds "Warp Active Window" commands
|
||||
- "Warps Active Window North" warps the active window with the window to the north.
|
||||
- "Warps Active Window South" warps the active window with the window to the south.
|
||||
- "Warps Active Window East" warps the active window with the window to the east.
|
||||
- "Warps Active Window West" warps the active window with the window to the west.
|
||||
|
||||
## [Added MenuBar to indicate the current screen and listed windows of the current screen] - 2024-07-05
|
||||
|
||||
## [Enhancements] - 2024-06-28
|
||||
|
||||
-- Adds "Restart" command.
|
||||
-- Adds "Start" command.
|
||||
-- Adds "Stop" command.
|
||||
-- Adds "Layout BSP" command.
|
||||
-- Adds "Layout Stack" command.
|
||||
-- Adds "Layout Float" command.
|
||||
|
||||
## [Initial Version] - 2023-12-23
|
||||
30
README.md
Normal file
30
README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Yabai
|
||||
|
||||
> Run Yabai window management commands directly from Raycast.
|
||||
|
||||
## Installation
|
||||
|
||||
To use this extension, you must have `yabai` installed on your machine.
|
||||
|
||||
The easiest way to install this is using [Homebrew](https://brew.sh/). After you have Homebrew installed, run the
|
||||
following command in your terminal:
|
||||
|
||||
```bash
|
||||
brew install koekeishiya/formulae/yabai
|
||||
```
|
||||
|
||||
[Official installation instructions](<https://github.com/koekeishiya/yabai/wiki/Installing-yabai-(latest-release)>)
|
||||
|
||||
## Additional Setup for MenuBar Desktop Indicator
|
||||
|
||||
To enable the desktop indicator in the MenuBar, you need to add a specific command to your yabairc configuration file.
|
||||
|
||||
Please add the following line to your yabairc file:
|
||||
|
||||
```bash
|
||||
yabai -m signal --add event=space_changed action="nohup open -g raycast://extensions/krzysztoff1/yabai/screens-menu-bar?launchType=background > /dev/null 2>&1 &"
|
||||
```
|
||||
|
||||
This command will allow the Raycast extension to run in the background and update the menubar indicator whenever you switch desktop spaces.
|
||||
|
||||
After adding this line, make sure to reload your yabairc configuration or restart yabai for the changes to take effect.
|
||||
BIN
assets/yabai-icon-512px.png
Normal file
BIN
assets/yabai-icon-512px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
3687
package-lock.json
generated
Normal file
3687
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
276
package.json
Normal file
276
package.json
Normal file
@@ -0,0 +1,276 @@
|
||||
{
|
||||
"$schema": "https://www.raycast.com/schemas/extension.json",
|
||||
"name": "yabai",
|
||||
"title": "Yabai",
|
||||
"description": "Control Yabai using Raycast. Requires yabai to be installed.",
|
||||
"icon": "yabai-icon-512px.png",
|
||||
"author": "krzysztoff1",
|
||||
"contributors": [
|
||||
"rubentsirunyan",
|
||||
"webdesus",
|
||||
"d34dh0r53",
|
||||
"lhy-a"
|
||||
],
|
||||
"categories": [
|
||||
"Developer Tools",
|
||||
"Productivity"
|
||||
],
|
||||
"license": "MIT",
|
||||
"commands": [
|
||||
{
|
||||
"name": "custom-yabai-command",
|
||||
"title": "Run Custom Yabai Command",
|
||||
"description": "Run custom Yabai command",
|
||||
"mode": "no-view",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "command",
|
||||
"placeholder": "Command",
|
||||
"type": "text",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "screens-menu-bar",
|
||||
"title": "Show Current Screen",
|
||||
"description": "Update the status information about the currently selected screen.",
|
||||
"mode": "menu-bar"
|
||||
},
|
||||
{
|
||||
"name": "windows-list-current-space",
|
||||
"title": "Windows List by Current Space",
|
||||
"description": "Show the application of the current space.",
|
||||
"mode": "view"
|
||||
},
|
||||
{
|
||||
"name": "toggle",
|
||||
"title": "Toggle Yabai",
|
||||
"description": "This command will start Yabai using `yabai --start-service` or stop it using `yabai --stop-service`.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "rotate",
|
||||
"title": "Rotate",
|
||||
"description": "Rotate the tree 90 degrees clockwise.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "mirror-x",
|
||||
"title": "Mirror X",
|
||||
"description": "Mirror the tree on the X axis.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "mirror-y",
|
||||
"title": "Mirror Y",
|
||||
"description": "Mirror the tree on the Y axis.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "balance",
|
||||
"title": "Balance",
|
||||
"description": "Balance the windows.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "restart",
|
||||
"title": "Restart",
|
||||
"description": "Restart yabai service.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "start",
|
||||
"title": "Start",
|
||||
"description": "Start yabai service.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "stop",
|
||||
"title": "Stop",
|
||||
"description": "Stop yabai service.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "layout-bsp",
|
||||
"title": "Layout BSP",
|
||||
"description": "Switch to BSP layout for the current space.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "layout-stack",
|
||||
"title": "Layout Stack",
|
||||
"description": "Switch to stack layout for the current space.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "layout-float",
|
||||
"title": "Layout Float",
|
||||
"description": "Switch to float layout for the current space.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "create-space-and-focus",
|
||||
"title": "Create & Focus New Space",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Creates a new workspace and focuses it.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "focus-space",
|
||||
"title": "Focus Space",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Focuses the specified space.",
|
||||
"mode": "no-view",
|
||||
"arguments": [
|
||||
{
|
||||
"type": "text",
|
||||
"placeholder": "Space Number",
|
||||
"required": true,
|
||||
"name": "spaceIndex"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "destroy-space",
|
||||
"title": "Destroy Current Space",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Destroys the currently focused space.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "focus-window-north",
|
||||
"title": "Focus Active Window North",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Focus the window to the north of the currently focused window.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "focus-window-south",
|
||||
"title": "Focus Active Window South",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Focus the window to the south of the currently focused window.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "focus-window-east",
|
||||
"title": "Focus Active Window East",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Focus the window to the east of the currently focused window.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "focus-window-west",
|
||||
"title": "Focus Active Window West",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Focus the window to the west of the currently focused window.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "swap-window-north",
|
||||
"title": "Swap Active Window North",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Swap window position and size with window to the north of the active window.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "swap-window-south",
|
||||
"title": "Swap Active Window South",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Swap window position and size with window to the south of the active window.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "swap-window-east",
|
||||
"title": "Swap Active Window East",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Swap window position and size with window to the east of the active window.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "swap-window-west",
|
||||
"title": "Swap Active Window West",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Swap window position and size with window to the west of the active window.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "warp-window-north",
|
||||
"title": "Warp Active Window North",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Warp at window to the north of the focused window.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "warp-window-south",
|
||||
"title": "Warp Active Window South",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Warp at window to the south of the focused window.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "warp-window-east",
|
||||
"title": "Warp Active Window East",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Warp at window to the east of the focused window.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "warp-window-west",
|
||||
"title": "Warp Active Window West",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Warp at window to the west of the focused window.",
|
||||
"mode": "no-view"
|
||||
},
|
||||
{
|
||||
"name": "search-windows",
|
||||
"title": "Search Windows",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Search and focus Yabai managed windows",
|
||||
"mode": "view",
|
||||
"keywords": [
|
||||
"s",
|
||||
"window",
|
||||
"search"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "focus-next-same-app",
|
||||
"title": "Focus Next Window of Same App",
|
||||
"subtitle": "Yabai",
|
||||
"description": "Focus the next window of the current application",
|
||||
"mode": "no-view"
|
||||
}
|
||||
],
|
||||
"preferences": [
|
||||
{
|
||||
"name": "yabaiPath",
|
||||
"title": "Yabai installation path",
|
||||
"required": false,
|
||||
"type": "textfield",
|
||||
"description": "Location to the Yabai installation (Defaults to `/opt/homebrew/bin/yabai` on M1 Macs, and `/usr/local/bin/yabai` otherwise)"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@raycast/api": "^1.64.4",
|
||||
"@raycast/utils": "^1.10.1",
|
||||
"execa": "^8.0.1",
|
||||
"ray": "^0.0.1",
|
||||
"tiny-pinyin": "^1.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@raycast/eslint-config": "1.0.8",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/react": "18.2.45",
|
||||
"eslint": "^8.56.0",
|
||||
"prettier": "^3.1.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "ray build -e dist",
|
||||
"dev": "ray develop",
|
||||
"fix-lint": "ray lint --fix",
|
||||
"lint": "ray lint",
|
||||
"publish": "npx @raycast/api@latest publish"
|
||||
}
|
||||
}
|
||||
23
src/balance.ts
Normal file
23
src/balance.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { showHUD } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m space --balance");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
showHUD("Balanced space");
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("Yabai executable not found")) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast(error, {
|
||||
title: "Failed to balance space, make sure you have Yabai installed and running.",
|
||||
});
|
||||
}
|
||||
}
|
||||
35
src/create-space-and-focus.tsx
Normal file
35
src/create-space-and-focus.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { showHUD } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand(`-m space --create`);
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
const { stdout: spacesOutput } = await runYabaiCommand(`-m query --spaces`);
|
||||
const spaces = JSON.parse(spacesOutput);
|
||||
const lastSpaceIndex = spaces.filter((space: { [x: string]: never }) => !space["is-native-fullscreen"]).pop().index;
|
||||
|
||||
await runYabaiCommand(`-m space --focus ${lastSpaceIndex}`);
|
||||
|
||||
showHUD(`Created space: ${lastSpaceIndex}`);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes("Yabai executable not found")) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast(error, {
|
||||
title: "Failed to create space",
|
||||
});
|
||||
} else {
|
||||
showFailureToast(error, {
|
||||
title: "Failed to create space",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/custom-yabai-command.tsx
Normal file
35
src/custom-yabai-command.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { LaunchProps } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
const prefixesToRemove = ["yabai -m", "yabai --message", "-m", "--message"];
|
||||
|
||||
export default async function Command(launchProps: LaunchProps) {
|
||||
try {
|
||||
let command = launchProps.arguments.command.trim().replace(/\s\s+/g, " ");
|
||||
|
||||
for (const prefix of prefixesToRemove) {
|
||||
if (command.startsWith(prefix)) {
|
||||
command = command.replace(prefix, "").trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (!command) {
|
||||
showFailureToast("No command provided.");
|
||||
return;
|
||||
}
|
||||
|
||||
command = `-m ${command}`;
|
||||
|
||||
const { stderr } = await runYabaiCommand(command);
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("Yabai executable not found")) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast(error);
|
||||
}
|
||||
}
|
||||
40
src/destroy-space.tsx
Normal file
40
src/destroy-space.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { showHUD } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stdout: rawRecentSpace, stderr } = await runYabaiCommand(`-m query --spaces --space recent`);
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
const recentSpace = JSON.parse(rawRecentSpace);
|
||||
const lastSpaceIndex = recentSpace.index;
|
||||
|
||||
await runYabaiCommand(`-m space --destroy`);
|
||||
|
||||
try {
|
||||
await runYabaiCommand(`-m space --focus ${lastSpaceIndex}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to focus space: ${error}`);
|
||||
}
|
||||
|
||||
await showHUD(`Destroyed Space`);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes("Yabai executable not found")) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast(error, {
|
||||
title: "Failed to destroy space",
|
||||
});
|
||||
} else {
|
||||
showFailureToast(error, {
|
||||
title: "Failed to destroy space",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/focus-next-same-app.tsx
Normal file
44
src/focus-next-same-app.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { closeMainWindow, showHUD } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { sortWindows, BaseWindow } from "./helpers/window-utils";
|
||||
|
||||
interface Window extends BaseWindow {
|
||||
"has-focus": boolean;
|
||||
"is-minimized": boolean;
|
||||
}
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stdout: windowsJson } = await runYabaiCommand("-m query --windows");
|
||||
const allWindows: Window[] = JSON.parse(windowsJson);
|
||||
|
||||
const sortedWindows = sortWindows(allWindows);
|
||||
|
||||
const currentWindow = sortedWindows.find((w) => w["has-focus"]);
|
||||
if (!currentWindow) {
|
||||
await showHUD("No focused window found");
|
||||
return;
|
||||
}
|
||||
|
||||
const sameAppWindows = sortedWindows.filter((w) => w.app === currentWindow.app);
|
||||
if (sameAppWindows.length <= 1) {
|
||||
await showHUD("No other windows of the same app");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = sameAppWindows.findIndex((w) => w.id === currentWindow.id);
|
||||
const nextIndex = (currentIndex + 1) % sameAppWindows.length;
|
||||
const nextWindow = sameAppWindows[nextIndex];
|
||||
|
||||
await runYabaiCommand(`-m window --focus ${nextWindow.id}`);
|
||||
if (nextWindow["is-minimized"]) {
|
||||
await runYabaiCommand(`-m window ${nextWindow.id} --deminimize`);
|
||||
}
|
||||
|
||||
await showHUD(`Focused ${nextWindow.app}`);
|
||||
await closeMainWindow();
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
await showHUD("Failed to focus next window");
|
||||
}
|
||||
}
|
||||
39
src/focus-space.tsx
Normal file
39
src/focus-space.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { LaunchProps, showHUD } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command(args: LaunchProps) {
|
||||
if (isNaN(args.arguments.spaceIndex)) {
|
||||
showHUD(`Invalid space index: ${args.arguments.spaceIndex}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const spaceIndex = parseInt(args.arguments.spaceIndex, 10);
|
||||
|
||||
await focusSpace(spaceIndex);
|
||||
}
|
||||
export async function focusSpace(spaceIndex: number) {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand(`-m space --focus ${spaceIndex}`);
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
showHUD(`Focused space ${spaceIndex}`);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes("already focused space")) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast(error, {
|
||||
title: "Failed to focus space",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast("Failed to focus space");
|
||||
}
|
||||
}
|
||||
16
src/focus-window-east.ts
Normal file
16
src/focus-window-east.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m window --focus east");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
} catch (error) {
|
||||
showFailureToast(error, {
|
||||
title: "Failed to warp window.",
|
||||
});
|
||||
}
|
||||
}
|
||||
16
src/focus-window-north.ts
Normal file
16
src/focus-window-north.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m window --focus north");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
} catch (error) {
|
||||
showFailureToast(error, {
|
||||
title: "Failed to warp window.",
|
||||
});
|
||||
}
|
||||
}
|
||||
16
src/focus-window-south.ts
Normal file
16
src/focus-window-south.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m window --focus south");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
} catch (error) {
|
||||
showFailureToast(error, {
|
||||
title: "Failed to warp window.",
|
||||
});
|
||||
}
|
||||
}
|
||||
16
src/focus-window-west.ts
Normal file
16
src/focus-window-west.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m window --focus west");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
} catch (error) {
|
||||
showFailureToast(error, {
|
||||
title: "Failed to warp window.",
|
||||
});
|
||||
}
|
||||
}
|
||||
29
src/helpers/mirror.ts
Normal file
29
src/helpers/mirror.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { showHUD } from "@raycast/api";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
import { runYabaiCommand } from "./scripts";
|
||||
|
||||
export async function mirror(axis: "x" | "y") {
|
||||
const cmd = `-m space --mirror ${axis}-axis`;
|
||||
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand(cmd);
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
showHUD(`Mirrored space in the ${axis} axis`);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
typeof error?.message === "string" &&
|
||||
error.message.includes("Yabai executable not found")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast(error, {
|
||||
title: `Failed to mirror space in the ${axis} axis. Make sure Yabai is installed and running.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
24
src/helpers/scripts.ts
Normal file
24
src/helpers/scripts.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getPreferenceValues, showToast, Toast } from "@raycast/api";
|
||||
import { execaCommand } from "execa";
|
||||
import { userInfo } from "os";
|
||||
import { cpus } from "os";
|
||||
import fs from "fs";
|
||||
|
||||
const userEnv = `env USER=${userInfo().username}`;
|
||||
|
||||
export const runYabaiCommand = async (command: string, opt?: { shell?: boolean }) => {
|
||||
const preferences = getPreferenceValues<Preferences>();
|
||||
const yabaiPath: string =
|
||||
preferences.yabaiPath && preferences.yabaiPath.length > 0
|
||||
? preferences.yabaiPath
|
||||
: cpus()[0].model.includes("Apple")
|
||||
? "/opt/homebrew/bin/yabai"
|
||||
: "/usr/local/bin/yabai";
|
||||
|
||||
if (!fs.existsSync(yabaiPath)) {
|
||||
await showToast(Toast.Style.Failure, "Yabai executable not found", `Is yabai installed at ${yabaiPath}?`);
|
||||
return { stdout: "", stderr: "Yabai executable not found" };
|
||||
}
|
||||
|
||||
return await execaCommand([userEnv, yabaiPath, command].join(" "), opt);
|
||||
};
|
||||
42
src/helpers/window-utils.ts
Normal file
42
src/helpers/window-utils.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export interface BaseWindow {
|
||||
id: number;
|
||||
pid: number;
|
||||
app: string;
|
||||
title: string;
|
||||
space: number;
|
||||
frame: {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function sortWindows<T extends BaseWindow>(windows: T[]): T[] {
|
||||
return [...windows].sort((a, b) => {
|
||||
if (a.space !== b.space) {
|
||||
return a.space - b.space;
|
||||
}
|
||||
|
||||
const aArea = {
|
||||
min: { x: a.frame.x, y: a.frame.y },
|
||||
max: { x: a.frame.x + a.frame.w, y: a.frame.y + a.frame.h },
|
||||
};
|
||||
const bArea = {
|
||||
min: { x: b.frame.x, y: b.frame.y },
|
||||
max: { x: b.frame.x + b.frame.w, y: b.frame.y + b.frame.h },
|
||||
};
|
||||
|
||||
// calculate the overlap of the two windows
|
||||
const overlapY = Math.max(0, Math.min(aArea.max.y, bArea.max.y) - Math.max(aArea.min.y, bArea.min.y));
|
||||
const minHeight = Math.min(a.frame.h, b.frame.h);
|
||||
|
||||
// if the overlap of the two windows is greater than 50% of the window height, consider them in the same row
|
||||
if (overlapY > minHeight * 0.5) {
|
||||
return aArea.min.x - bArea.min.x;
|
||||
}
|
||||
|
||||
// otherwise, sort by the top edge
|
||||
return aArea.min.y - bArea.min.y;
|
||||
});
|
||||
}
|
||||
23
src/layout-bsp.ts
Normal file
23
src/layout-bsp.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { showHUD } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m space --layout bsp");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
showHUD("Switched to BSP layout");
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("Yabai executable not found")) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast(error, {
|
||||
title: "Failed to switch to BSP layout, make sure you have Yabai installed and running.",
|
||||
});
|
||||
}
|
||||
}
|
||||
23
src/layout-float.ts
Normal file
23
src/layout-float.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { showHUD } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m space --layout float");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
showHUD("Switched to float layout");
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("Yabai executable not found")) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast(error, {
|
||||
title: "Failed to switch to float layout, make sure you have Yabai installed and running.",
|
||||
});
|
||||
}
|
||||
}
|
||||
23
src/layout-stack.ts
Normal file
23
src/layout-stack.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { showHUD } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m space --layout stack");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
showHUD("Switched to stack layout");
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("Yabai executable not found")) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast(error, {
|
||||
title: "Failed to switch to stack layout, make sure you have Yabai installed and running.",
|
||||
});
|
||||
}
|
||||
}
|
||||
5
src/mirror-x.ts
Normal file
5
src/mirror-x.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { mirror } from "./helpers/mirror";
|
||||
|
||||
export default async function Command() {
|
||||
await mirror("x");
|
||||
}
|
||||
5
src/mirror-y.ts
Normal file
5
src/mirror-y.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { mirror } from "./helpers/mirror";
|
||||
|
||||
export default async function Command() {
|
||||
await mirror("y");
|
||||
}
|
||||
33
src/restart.ts
Normal file
33
src/restart.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { showHUD } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("--restart-service");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
showHUD("Restarted yabai service");
|
||||
} catch (error) {
|
||||
try {
|
||||
const { stderr: startStderr } = await runYabaiCommand("--start-service");
|
||||
|
||||
if (startStderr) {
|
||||
throw new Error(startStderr);
|
||||
}
|
||||
|
||||
showHUD("Yabai was not running. Started yabai service");
|
||||
} catch (startError) {
|
||||
if (startError instanceof Error && startError.message.includes("Yabai executable not found")) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast(startError, {
|
||||
title: "Failed to start yabai service, make sure you have Yabai installed.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/rotate.ts
Normal file
27
src/rotate.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { showHUD } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async () => {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m space --rotate 90");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
showHUD("Rotated window tree");
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
typeof error?.message === "string" &&
|
||||
error.message.includes("Yabai executable not found")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast(error, {
|
||||
title: "Failed to rotate window tree, make sure you have Yabai installed and running.",
|
||||
});
|
||||
}
|
||||
};
|
||||
54
src/screens-menu-bar.tsx
Normal file
54
src/screens-menu-bar.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Icon, MenuBarExtra } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { focusSpace } from "./focus-space";
|
||||
|
||||
interface IDesktop {
|
||||
index: number;
|
||||
label: string;
|
||||
"is-visible": boolean;
|
||||
}
|
||||
|
||||
async function getDesktopList(): Promise<IDesktop[]> {
|
||||
const desktopList = await runYabaiCommand("-m query --spaces --display");
|
||||
if (desktopList.stdout) {
|
||||
return JSON.parse(desktopList.stdout);
|
||||
}
|
||||
throw new Error(desktopList.stderr);
|
||||
}
|
||||
|
||||
const useDesktop = () => {
|
||||
const [state, setState] = useState<{ desktop: string; desktopList: IDesktop[]; isLoading: boolean }>({
|
||||
desktop: "0",
|
||||
desktopList: [],
|
||||
isLoading: true,
|
||||
});
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const desktopList = await getDesktopList();
|
||||
const desktop = desktopList.filter((f) => f["is-visible"])[0];
|
||||
setState({
|
||||
desktop: desktop.label || desktop.index.toString(),
|
||||
desktopList,
|
||||
isLoading: false,
|
||||
});
|
||||
})();
|
||||
}, []);
|
||||
return state;
|
||||
};
|
||||
export default function Command() {
|
||||
const { desktop, desktopList, isLoading } = useDesktop();
|
||||
const icon = Icon.Desktop;
|
||||
|
||||
return (
|
||||
<MenuBarExtra title={`${desktop}`} icon={icon} isLoading={isLoading}>
|
||||
{desktopList?.map((item) => (
|
||||
<MenuBarExtra.Item
|
||||
key={item.index}
|
||||
title={item.label || item.index.toString()}
|
||||
onAction={() => focusSpace(item.index)}
|
||||
/>
|
||||
))}
|
||||
</MenuBarExtra>
|
||||
);
|
||||
}
|
||||
157
src/search-windows.tsx
Normal file
157
src/search-windows.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import { ActionPanel, Action, List, showToast, Toast } from "@raycast/api";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { execaCommand } from "execa";
|
||||
import { sortWindows, BaseWindow } from "./helpers/window-utils";
|
||||
import * as pinyin from "tiny-pinyin";
|
||||
|
||||
interface Window extends BaseWindow {
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
async function findAppPath(pid: number): Promise<string> {
|
||||
if (!Number.isInteger(pid) || pid <= 0) {
|
||||
throw new Error("Invalid process ID");
|
||||
}
|
||||
const { stdout, stderr } = await execaCommand(`/usr/sbin/lsof -p ${pid} | grep txt | grep -v DEL | head -n 1 `, {
|
||||
shell: true,
|
||||
});
|
||||
if (stderr) {
|
||||
console.error(stderr);
|
||||
return "";
|
||||
}
|
||||
const beginIndex = stdout.indexOf("/");
|
||||
const appIndex = stdout.indexOf(".app");
|
||||
if (appIndex === -1) {
|
||||
return stdout;
|
||||
}
|
||||
return stdout.substring(beginIndex, appIndex + 4);
|
||||
}
|
||||
|
||||
function getPinyin(text: string): string {
|
||||
if (!/[\u4e00-\u9fa5]/.test(text)) {
|
||||
return text.toLowerCase();
|
||||
}
|
||||
return pinyin.convertToPinyin(text, "", true).toLowerCase();
|
||||
}
|
||||
|
||||
function timeLog(label: string, startTime: number) {
|
||||
const endTime = performance.now();
|
||||
console.debug(`${label}: ${endTime - startTime}ms`);
|
||||
}
|
||||
|
||||
export default function Command() {
|
||||
const [windows, setWindows] = useState<Window[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [filteredWindows, setFilteredWindows] = useState<Window[]>([]);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const filteredWindowsMemo = useMemo(() => {
|
||||
const startTime = performance.now();
|
||||
|
||||
if (!searchText) {
|
||||
timeLog("Filter (no search)", startTime);
|
||||
return windows;
|
||||
}
|
||||
|
||||
const lowerSearchText = searchText.toLowerCase();
|
||||
|
||||
const results = windows.filter((window) => {
|
||||
const filterStartTime = performance.now();
|
||||
|
||||
if (window.title.toLowerCase().includes(lowerSearchText) || window.app.toLowerCase().includes(lowerSearchText)) {
|
||||
timeLog("Filter (text match)", filterStartTime);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^[a-z]+$/.test(lowerSearchText)) {
|
||||
const pinyinStartTime = performance.now();
|
||||
const titlePinyin = getPinyin(window.title);
|
||||
const appPinyin = getPinyin(window.app);
|
||||
const matched = titlePinyin.includes(lowerSearchText) || appPinyin.includes(lowerSearchText);
|
||||
timeLog("Filter (pinyin match)", pinyinStartTime);
|
||||
return matched;
|
||||
}
|
||||
|
||||
timeLog("Filter (no match)", filterStartTime);
|
||||
return false;
|
||||
});
|
||||
|
||||
timeLog("Total filter time", startTime);
|
||||
return results;
|
||||
}, [windows, searchText]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchWindows() {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const { stdout, stderr } = await runYabaiCommand("-m query --windows");
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
const windowsData: Window[] = JSON.parse(stdout);
|
||||
|
||||
const sortedWindows = sortWindows(windowsData);
|
||||
|
||||
timeLog("Fetch windows data", startTime);
|
||||
|
||||
const iconStartTime = performance.now();
|
||||
const windowsWithIcons = await Promise.all(
|
||||
sortedWindows.map(async (window) => ({
|
||||
...window,
|
||||
icon: await findAppPath(window.pid),
|
||||
})),
|
||||
);
|
||||
timeLog("Add icons", iconStartTime);
|
||||
|
||||
setWindows(windowsWithIcons);
|
||||
} catch (error) {
|
||||
showToast(Toast.Style.Failure, "Failed to fetch windows");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
timeLog("Total fetch time", startTime);
|
||||
}
|
||||
}
|
||||
|
||||
fetchWindows();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredWindows(filteredWindowsMemo);
|
||||
}, [filteredWindowsMemo]);
|
||||
|
||||
async function focusWindow(windowId: number) {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand(`-m window --focus ${windowId}`);
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
showToast(Toast.Style.Success, "Window focused");
|
||||
} catch (error) {
|
||||
showToast(Toast.Style.Failure, "Failed to focus window");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<List
|
||||
isLoading={isLoading}
|
||||
searchBarPlaceholder="Search windows by name or pinyin..."
|
||||
onSearchTextChange={setSearchText}
|
||||
>
|
||||
{filteredWindows.map((window) => (
|
||||
<List.Item
|
||||
key={window.id}
|
||||
icon={{ fileIcon: window.icon || window.app }}
|
||||
title={window.app}
|
||||
subtitle={window.title}
|
||||
accessories={[{ text: `Space ${window.space}` }]}
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action title="Focus Window" onAction={() => focusWindow(window.id)} />
|
||||
</ActionPanel>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
23
src/start.ts
Normal file
23
src/start.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { showHUD } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("--start-service");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
showHUD("Started yabai service");
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("Yabai executable not found")) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast(error, {
|
||||
title: "Failed to start yabai service, make sure you have Yabai installed.",
|
||||
});
|
||||
}
|
||||
}
|
||||
23
src/stop.ts
Normal file
23
src/stop.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { showHUD } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("--stop-service");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
showHUD("Stopped yabai service");
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("Yabai executable not found")) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast(error, {
|
||||
title: "Failed to stop yabai service, make sure you have Yabai installed and running.",
|
||||
});
|
||||
}
|
||||
}
|
||||
16
src/swap-window-east.ts
Normal file
16
src/swap-window-east.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m window --swap east");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
} catch (error) {
|
||||
showFailureToast(error, {
|
||||
title: "Failed to swap window.",
|
||||
});
|
||||
}
|
||||
}
|
||||
16
src/swap-window-north.ts
Normal file
16
src/swap-window-north.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m window --swap north");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
} catch (error) {
|
||||
showFailureToast(error, {
|
||||
title: "Failed to swap window.",
|
||||
});
|
||||
}
|
||||
}
|
||||
16
src/swap-window-south.ts
Normal file
16
src/swap-window-south.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m window --swap south");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
} catch (error) {
|
||||
showFailureToast(error, {
|
||||
title: "Failed to swap window.",
|
||||
});
|
||||
}
|
||||
}
|
||||
16
src/swap-window-west.ts
Normal file
16
src/swap-window-west.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m window --swap west");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
} catch (error) {
|
||||
showFailureToast(error, {
|
||||
title: "Failed to swap window.",
|
||||
});
|
||||
}
|
||||
}
|
||||
51
src/toggle.ts
Normal file
51
src/toggle.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { showHUD } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async () => {
|
||||
let serviceRunning = false;
|
||||
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("--stop-service");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
showHUD("Yabai has been stopped.");
|
||||
|
||||
serviceRunning = false;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("Yabai executable not found")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (String(error).includes("Could not find service")) {
|
||||
serviceRunning = true;
|
||||
} else {
|
||||
showFailureToast(error, {
|
||||
title: "Error stopping Yabai",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceRunning) {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("--start-service");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
showHUD("Yabai has been started.");
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("Yabai executable not found")) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFailureToast(error, {
|
||||
title: "Error starting Yabai",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
16
src/warp-window-east.ts
Normal file
16
src/warp-window-east.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m window --warp east");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
} catch (error) {
|
||||
showFailureToast(error, {
|
||||
title: "Failed to warp window.",
|
||||
});
|
||||
}
|
||||
}
|
||||
16
src/warp-window-north.ts
Normal file
16
src/warp-window-north.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m window --warp north");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
} catch (error) {
|
||||
showFailureToast(error, {
|
||||
title: "Failed to warp window.",
|
||||
});
|
||||
}
|
||||
}
|
||||
16
src/warp-window-south.ts
Normal file
16
src/warp-window-south.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m window --warp south");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
} catch (error) {
|
||||
showFailureToast(error, {
|
||||
title: "Failed to warp window.",
|
||||
});
|
||||
}
|
||||
}
|
||||
16
src/warp-window-west.ts
Normal file
16
src/warp-window-west.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { showFailureToast } from "@raycast/utils";
|
||||
|
||||
export default async function Command() {
|
||||
try {
|
||||
const { stderr } = await runYabaiCommand("-m window --warp west");
|
||||
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
} catch (error) {
|
||||
showFailureToast(error, {
|
||||
title: "Failed to warp window.",
|
||||
});
|
||||
}
|
||||
}
|
||||
104
src/windows-list-current-space.tsx
Normal file
104
src/windows-list-current-space.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Action, ActionPanel, List, PopToRootType, closeMainWindow } from "@raycast/api";
|
||||
import { runYabaiCommand } from "./helpers/scripts";
|
||||
import { execaCommand } from "execa";
|
||||
|
||||
interface IWindow {
|
||||
id: number;
|
||||
pid: number;
|
||||
title: string;
|
||||
icon: string;
|
||||
app: string;
|
||||
"has-focus": boolean;
|
||||
"stack-index": number;
|
||||
}
|
||||
async function getWindowsList(): Promise<IWindow[]> {
|
||||
const windowsList = await runYabaiCommand(`-m query --windows --space`);
|
||||
if (windowsList.stdout) {
|
||||
return JSON.parse(windowsList.stdout);
|
||||
}
|
||||
throw new Error(windowsList.stderr);
|
||||
}
|
||||
const useWindowsList = () => {
|
||||
const [state, setState] = useState<{ list: IWindow[]; isLoading: boolean }>({
|
||||
list: [],
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const list = await getWindowsList();
|
||||
list.sort((a, b) => a["stack-index"] - b["stack-index"]);
|
||||
setState({
|
||||
list: await Promise.all(
|
||||
list.map(async (el) => {
|
||||
el.icon = (await findAppPath(el.pid)) || "";
|
||||
el.title = el.title || el.app;
|
||||
return el;
|
||||
}),
|
||||
),
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setState({
|
||||
list: [],
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
};
|
||||
async function findAppPath(pid: number): Promise<string> {
|
||||
const { stdout, stderr } = await execaCommand(`/usr/sbin/lsof -p ${pid} | grep txt | grep -v DEL | head -n 1 `, {
|
||||
shell: true,
|
||||
});
|
||||
if (stderr) {
|
||||
console.error(stderr);
|
||||
return "";
|
||||
}
|
||||
const beginIndex = stdout.indexOf("/");
|
||||
const appIndex = stdout.indexOf(".app");
|
||||
if (appIndex === -1) {
|
||||
return stdout;
|
||||
}
|
||||
return stdout.substring(beginIndex, appIndex + 4);
|
||||
}
|
||||
|
||||
export function selectWindow(id: number) {
|
||||
runYabaiCommand(`-m window --focus ${id}`);
|
||||
closeMainWindow({ clearRootSearch: true, popToRootType: PopToRootType.Immediate });
|
||||
}
|
||||
|
||||
export default function Command() {
|
||||
const { list, isLoading } = useWindowsList();
|
||||
|
||||
let selectedItemId = 0;
|
||||
if (list) {
|
||||
selectedItemId = list.find((f) => f["has-focus"])?.id || 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<List isLoading={isLoading} selectedItemId={selectedItemId.toString()}>
|
||||
{list?.map((item) => (
|
||||
<List.Item
|
||||
id={item.id.toString()}
|
||||
key={item.id}
|
||||
icon={{ fileIcon: item.icon }}
|
||||
title={item.title}
|
||||
accessories={[{ text: item.app }]}
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<ActionPanel.Section>
|
||||
<Action title="Focus Window" onAction={() => selectWindow(item.id)} />
|
||||
</ActionPanel.Section>
|
||||
</ActionPanel>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "Node 16",
|
||||
"include": ["src/**/*", "raycast-env.d.ts"],
|
||||
"compilerOptions": {
|
||||
"lib": ["es2021"],
|
||||
"module": "commonjs",
|
||||
"target": "es2021",
|
||||
"strict": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user