Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions extensions/apple-tv-remote/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Dependencies
node_modules/

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

# Build
dist/

# Misc
.DS_Store
4 changes: 4 additions & 0 deletions extensions/apple-tv-remote/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"printWidth": 120,
"singleQuote": false
}
12 changes: 12 additions & 0 deletions extensions/apple-tv-remote/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Apple TV Remote Changelog

## [Apple TV Remote] - 2026-07-01

- Visual Apple TV remote with a persistent connection — clickable button grid plus a no-modifier keyboard layer (WASD/HJKL, F select, Space play/pause) and ⌥-shortcuts
- Bonjour discovery + on-screen PIN pairing, with manual IP/port fallback
- Full Siri Remote function set ported from pyatv: context menu (hold-select), app switcher, Control Center, ±10s skip, sleep/wake, screensaver
- App launcher grid backed by the device's live app list, with hotkey-able Quicklinks per app
- Ask Apple TV: one-shot natural-language commands (pause, open an app, type text, play a show)
- Title playback via real JustWatch-resolved deep links where tvOS supports them, with an Apple TV universal-search typing flow for Netflix and unresolved titles
- Menu bar quick controls and per-key commands (disabled by default) for global hotkeys
- AI tools: press remote keys, launch apps, play content, sleep/wake — usable from Raycast AI chat and Quick AI
21 changes: 21 additions & 0 deletions extensions/apple-tv-remote/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Cameron Schroeder

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.
17 changes: 17 additions & 0 deletions extensions/apple-tv-remote/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Apple TV Remote for Raycast
Copyright (c) 2026 Cameron Schroeder — MIT License (see LICENSE)

This project builds on the following third-party work:

1. pyatv — https://github.com/postlund/pyatv
Copyright (c) 2020 Pierre Ståhl — MIT License
The Companion-protocol payloads implemented in src/lib/companion-extras.ts
(app launching, app listing, power, Control Center, hold gestures, skip)
are ported from pyatv's reference implementation and protocol
documentation (https://pyatv.dev/documentation/protocols/).

2. @bharper/atv-js — https://github.com/bsharper/atvjs
Copyright (c) Brian Harper; carries the MIT license of pyatv
(Copyright (c) 2020 Pierre Ståhl), of which it is a derivative
TypeScript port. Used as the Companion-protocol transport
(discovery, pairing, encrypted session, remote keys, text input).
95 changes: 95 additions & 0 deletions extensions/apple-tv-remote/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Apple TV Remote

Control your Apple TV from Raycast. No Python, no helper apps, nothing to install beyond the extension. It talks to the Apple TV directly over Apple's Companion protocol in pure TypeScript.

![The remote](media/remote.png)

## Features

- **Apple TV Remote.** A clickable on-screen remote that keeps one live connection open, so every press lands instantly. D-pad, select, back, home, hold-select context menu, app switcher, Control Center, playback, 10-second skip, and typing into TV search fields.
- **Launch Apple TV App.** A grid of the apps on your Apple TV. Open any of them, or save one as a hotkey-able Quicklink.
- **Ask Apple TV.** One-off natural language, like `pause`, `open netflix`, `play severance`, `type stranger things`, or `sleep`.
- **Menu bar.** Quick taps for play/pause, skip, navigation, opening apps, and sleep/wake, plus one click through to the full remote.
- **Per-key hotkey commands.** Every remote function is also its own command (most are off by default). Enable the ones you want and give them global hotkeys.
- **AI tools.** Drive it from Raycast AI, like `@apple-tv-remote pause` or `@apple-tv-remote play Rick and Morty on Netflix` (needs Raycast Pro).

| Every function, one search away | Remote in your menu bar |
| ------------------------------- | ------------------------------ |
| ![Commands](media/commands.png) | ![Menu bar](media/menubar.png) |

## Setup

1. Run **Set up Apple TV**.
2. Pick your Apple TV from the list, or add it by IP and port if discovery is blocked on your network.
3. Type the 4-digit PIN that appears on the TV.

Pairing is interactive because the Apple TV shows the PIN on screen, which is why setup is a command rather than an extension preference.

## The remote

Open **Apple TV Remote** and either click the buttons or use the keyboard.

| Control | Bare key | With ⌥ |
| -------------------------- | ------------------------------------ | ------------------- |
| Navigate | `W` `A` `S` `D` (or `H` `J` `K` `L`) | `⌥↑` `⌥↓` `⌥←` `⌥→` |
| Select | `F` (or `G`) | `⌥↩` |
| Back | `B` | `⌥⌫` |
| Home | `Q` | |
| Play/Pause | `Space` | `⌥P` |
| Skip 10s | `,` / `.` | |
| Previous / Next | `[` / `]` | |
| Context menu (hold select) | `V` | |
| App switcher | `X` | |
| Control Center | `C` | |
| Type text on TV | `T` | |
| Screensaver | | `⌥S` |

The bare keys work because the view treats your typing as button presses, so no modifiers are needed. Volume has optional hotkey commands (`Volume Up` and `Volume Down`), but they only do anything when the Apple TV controls volume over HDMI-CEC. Most TVs handle volume themselves.

## Playing a show

`play <title> [on <app>]` looks the title up on JustWatch and opens a real provider deep link where tvOS supports one (Apple TV+, Disney+, Max, YouTube, and others). It does not guess at app IDs. Netflix dropped tvOS deep links in late 2025, so for Netflix and for anything that does not resolve, the extension opens the Apple TV's universal Search and types the title for you, then you pick the result on screen. The **Search Automation** preference sets how far it goes.

## Preferences

- **Streaming Country.** Two-letter code for the where-to-watch lookup. Leave it blank to detect your Mac's region.
- **Search Automation.** What happens after a title is typed into Apple TV Search: stop there, open the top result (the default), or also press Play. Pressing Play can start the wrong app, since the title page usually lists several.
- **Connection Timeout.** How long a command waits to reach the Apple TV.

## How it works

The extension speaks Apple's Companion protocol, the same one the iOS Remote uses, straight from Raycast's Node runtime through [`@bharper/atv-js`](https://github.com/bsharper/atvjs), a pure-TypeScript port of [pyatv](https://pyatv.dev). Discovery is Bonjour, pairing is HAP SRP with the on-screen PIN, and the session is chacha20-poly1305 encrypted. App launching, app listing, sleep/wake, Control Center, hold-select, and 10-second skip are built on top of the library in this extension, ported from pyatv's reference implementation.

### Credential storage

Pairing produces machine-generated key material rather than a password you type, so it lives in Raycast's encrypted [LocalStorage](https://developers.raycast.com/api-reference/storage), scoped to this extension. The extension never touches the macOS Keychain, and it sends nothing off your local network. The only internet request is the optional JustWatch title lookup.

## Development

```bash
npm install
npm run dev # ray develop, hot-reloads into Raycast
npm run lint # ray lint
npm run build # ray build
```

| Module | Role |
| -------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `src/lib/connection.ts` | Persistent session for the remote view, plus per-command `withConnection()` |
| `src/lib/companion-extras.ts` | Companion payloads ported from pyatv (launch app, app list, power, Control Center, hold, skip) |
| `src/lib/credentials.ts`, `devices.ts` | Pairing credentials and selected device in LocalStorage |
| `src/lib/justwatch.ts` | Title to deep-link resolution (keyless GraphQL, cached) |
| `src/lib/play-flow.ts` | Deep link, then universal-search typing, then app launch |
| `src/lib/deep-links.ts` | Curated app and bundle map, plus installed-app cache |
| `src/lib/errors.ts` | Typed errors turned into actionable toasts |

## Credits

- [pyatv](https://pyatv.dev) by Pierre Ståhl (MIT). The reference implementation and protocol docs this extension is built on. The Companion payloads for app launching, app listing, power, and gestures are ported from it.
- [`@bharper/atv-js`](https://github.com/bsharper/atvjs) by Brian Harper (MIT). The TypeScript Companion-protocol port, itself derived from pyatv (© Pierre Ståhl).

See [NOTICE](NOTICE) for third-party license details.

## License

MIT
Binary file added extensions/apple-tv-remote/assets/blank.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added extensions/apple-tv-remote/assets/menubar-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions extensions/apple-tv-remote/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const { defineConfig } = require("eslint/config");
const raycastConfig = require("@raycast/eslint-config");

module.exports = defineConfig([...raycastConfig]);
Binary file added extensions/apple-tv-remote/media/commands.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/apple-tv-remote/media/menubar.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/apple-tv-remote/media/remote.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: check the metadata screenshot size and padding

Can you make sure the metadata screenshot is the correct size and has the proper padding?

Right now it looks cropped and has a white border. Please use the capture window command in Raycast.

Reference:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recaptured with Window Capture at a clean 2000×1250 — no white border, nothing cropped. The original was a manual screenshot that got letterboxed.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: check the metadata screenshot size and padding

Can you make sure the metadata screenshot is the correct size and has the proper padding?

Right now it looks cropped and has a white border. Please use the capture window command in Raycast.

Reference:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same fix — recaptured via Window Capture at 2000×1250.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading