Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add doorstopper extension #17991

Merged
merged 5 commits into from
Mar 21, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions extensions/doorstopper/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules

# Raycast specific files
raycast-env.d.ts
.raycast-swift-build
.swiftpm
compiled_raycast_swift

# misc
.DS_Store
4 changes: 4 additions & 0 deletions extensions/doorstopper/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"printWidth": 120,
"singleQuote": false
}
5 changes: 5 additions & 0 deletions extensions/doorstopper/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Doorstopper Changelog

## [Add Doorstopper] - 2025-03-19

Initial version
21 changes: 21 additions & 0 deletions extensions/doorstopper/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Roland Schaer

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
48 changes: 48 additions & 0 deletions extensions/doorstopper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<p align="center">
<img src="./assets/icon.png" height="128">
<h1 align="center">Doorstopper Extension</h1>
</p>

Doorstopper is a Raycast extension which prevents your MacBook from going to sleep when you close the lid.

> **Note:**
> The underlying commands need to be run with `sudo` privileges.
> The extension will prompt you for your password.
## Installation 🛠️

To install the Doorstopper extension, follow these steps:

1. Open Raycast.
2. Search for "Store" and navigate to the Raycast Store.
3. Search for "Doorstopper" and click "Install."

## Usage 🚀

Once installed, simply trigger the Raycast command palette and search for the Doorstopper commands.

<p align="center">
<img src="./media/capture-1.png" alt="Raycast Command Palette for Doorstopper">
</p>

## Features ✨

### 1. `Enable Doorstopper`

This command will enable Doorstopper, preventing your MacBook from sleeping when the lid is closed.

### 2. `Disable Doorstopper`

This command will disable Doorstopper, allowing your MacBook to sleep when the lid is closed.

### 3. `Toggle Doorstopper`

This command will toggle the Doorstopper status between enabled and disabled.

### 4. `Doorstopper Status`

This command will show the current status of Doorstopper (enabled or disabled).

### 5. `Doorstopper Status Menu Bar`

This command will show the current status of Doorstopper in the menu bar.
1 change: 1 addition & 0 deletions extensions/doorstopper/assets/door-closed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions extensions/doorstopper/assets/door-open.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added extensions/doorstopper/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions extensions/doorstopper/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const { defineConfig } = require("eslint/config");
const raycastConfig = require("@raycast/eslint-config");

module.exports = defineConfig([
...raycastConfig,
]);
Binary file added extensions/doorstopper/media/capture-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added extensions/doorstopper/metadata/capture-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3,189 changes: 3,189 additions & 0 deletions extensions/doorstopper/package-lock.json

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions extensions/doorstopper/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "doorstopper",
"title": "Doorstopper",
"description": "Prevent your MacBook from going to sleep when you close the lid",
"categories": ["System"],
"icon": "icon.png",
"author": "roele",
"license": "MIT",
"commands": [
{
"name": "enable",
"title": "Enable Doorstopper",
"description": "Prevent the MacBook from going to sleep when closing the lid",
"mode": "no-view"
},
{
"name": "disable",
"title": "Disable Doorstopper",
"description": "Allow the MacBook to go to sleep when closing the lid",
"mode": "no-view"
},
{
"name": "toggle",
"title": "Toggle Doorstopper",
"description": "Toggle Doorstopper",
"mode": "no-view"
},
{
"name": "status",
"title": "Doorstopper Status",
"description": "Check if Doorstopper is enabled or disabled",
"mode": "no-view",
"interval": "15s"
},
{
"name": "statusmenu",
"title": "Doorstopper Status Menu Bar",
"description": "Get the Doorstopper status in your menu bar",
"mode": "menu-bar",
"interval": "1m",
"preferences": [
{
"name": "hiddenWhenDisabled",
"description": "Hide the icon when disabled",
"type": "checkbox",
"required": false,
"default": false,
"title": "When Disabled",
"label": "Hide the icon"
}
]
}
],
"scripts": {
"build": "ray build",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint",
"prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1",
"publish": "npx @raycast/api@latest publish",
"pull": "npx @raycast/api@latest pull-contributions"
},
"dependencies": {
"@raycast/api": "^1.94.0",
"@raycast/utils": "^1.17.0"
},
"devDependencies": {
"@raycast/eslint-config": "^2.0.4",
"@types/node": "22.13.10",
"@types/react": "19.0.10",
"eslint": "^9.22.0",
"prettier": "^3.5.3",
"typescript": "^5.8.2"
}
}
5 changes: 5 additions & 0 deletions extensions/doorstopper/src/disable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { stopDoorstopper } from "./util";

export default async function main() {
await stopDoorstopper({ menubar: false, status: false }, "Doorstopper is now disabled");
}
5 changes: 5 additions & 0 deletions extensions/doorstopper/src/enable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { startDoorstopper } from "./util";

export default async function main() {
await startDoorstopper({ menubar: true, status: true }, "Doorstopper is now enabled");
}
9 changes: 9 additions & 0 deletions extensions/doorstopper/src/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { updateCommandMetadata } from "@raycast/api";
import { isDoorstopperEnabled } from "./util";

export default async function Command() {
const isEnabled = isDoorstopperEnabled();
const subtitle = isEnabled ? "✔ Enabled" : "✖ Disabled";

updateCommandMetadata({ subtitle });
}
47 changes: 47 additions & 0 deletions extensions/doorstopper/src/statusmenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Color, getPreferenceValues, LaunchProps, MenuBarExtra, showHUD } from "@raycast/api";
import { useEffect, useState } from "react";
import { isDoorstopperEnabled, startDoorstopper, stopDoorstopper } from "./util";

export default function Command(props: LaunchProps) {
const hasLaunchContext = props.launchContext?.enabled !== undefined;
const isEnabled = hasLaunchContext ? props?.launchContext?.enabled : isDoorstopperEnabled();
const preferences = getPreferenceValues<Preferences.Statusmenu>();
const [status, setDoorstopperStatus] = useState(isEnabled);

useEffect(() => {
setDoorstopperStatus(isEnabled);
}, [isEnabled]);

const handleStatus = async () => {
if (status) {
setDoorstopperStatus(false);
stopDoorstopper({ status: true });
if (preferences.hiddenWhenDisabled) {
showHUD("Doorstopper is now disabled");
}
} else {
setDoorstopperStatus(true);
startDoorstopper({ status: true });
}
};

if (preferences.hiddenWhenDisabled && !status) {
return null;
}

return (
<MenuBarExtra
isLoading={false}
icon={
status
? { source: `door-open.svg`, tintColor: Color.PrimaryText }
: { source: `door-closed.svg`, tintColor: Color.PrimaryText }
}
>
<>
<MenuBarExtra.Section title={`Doorstopper is ${status ? "enabled" : "disabled"}`} />
<MenuBarExtra.Item title={status ? "Disable" : "Enable"} onAction={handleStatus} />
</>
</MenuBarExtra>
);
}
9 changes: 9 additions & 0 deletions extensions/doorstopper/src/toggle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { isDoorstopperEnabled, startDoorstopper, stopDoorstopper } from "./util";

export default async function main() {
if (isDoorstopperEnabled()) {
await stopDoorstopper({ menubar: true, status: true }, "Doorstopper is now disabled");
} else {
await startDoorstopper({ menubar: true, status: true }, "Doorstopper is now enabled");
}
}
55 changes: 55 additions & 0 deletions extensions/doorstopper/src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { launchCommand, LaunchType, showHUD } from "@raycast/api";
import { execSync } from "node:child_process";

type Updates = {
menubar?: boolean;
status: boolean;
};

async function update(updates: Updates, enabled: boolean) {
if (updates.menubar) {
await tryLaunchCommand("statusmenu", { enabled });
}
if (updates.status) {
await tryLaunchCommand("status", { enabled });
}
}

async function tryLaunchCommand(commandName: string, context: { enabled: boolean }) {
try {
await launchCommand({ name: commandName, type: LaunchType.Background, context });
} catch (error) {
console.error(`Error launching command ${commandName}:`, error);
}
}

export function isDoorstopperEnabled(): boolean {
try {
const out = execSync("pmset -g | awk '/SleepDisabled.*?([0-9])/{print $2}'");
return out.toString().trim() === "1";
} catch {
return false;
}
}

export async function startDoorstopper(updates: Updates, hudMessage?: string) {
const shellCommand = "pmset -a disablesleep 1";
execSync(
`osascript -e 'do shell script "${shellCommand}" with prompt "Doorstopper requires admin privileges" with administrator privileges'`,
);
await update(updates, true);
if (hudMessage) {
await showHUD(hudMessage);
}
}

export async function stopDoorstopper(updates: Updates, hudMessage?: string) {
const shellCommand = "pmset -a disablesleep 0";
execSync(
`osascript -e 'do shell script "${shellCommand}" with prompt "Doorstopper requires admin privileges" with administrator privileges'`,
);
await update(updates, false);
if (hudMessage) {
await showHUD(hudMessage);
}
}
16 changes: 16 additions & 0 deletions extensions/doorstopper/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["src/**/*", "raycast-env.d.ts"],
"compilerOptions": {
"lib": ["ES2023"],
"module": "commonjs",
"target": "ES2023",
"strict": true,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react-jsx",
"resolveJsonModule": true
}
}