Skip to content

Latest commit

 

History

55 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Wireless Display

Cast your Omarchy desktop to a TV over Wi-Fi — no cable, no dongle.

The Wireless Display panel listing a Samsung TV found over Miracast, an Apple TV over AirPlay, and the same Samsung over AirPlay, each row with a pair button

Adds a bar widget that finds wireless displays and connects to them two ways:

  • Extend — the TV becomes a second monitor you can drag windows onto.
  • Mirror — the TV shows a copy of your existing screen.

Two protocols are supported:

Miracast AirPlay
Typical device smart TV with Screen Share Apple TV, some smart TVs
How it connects Wi-Fi Direct, straight to the TV your existing network
At once one as many as you like
Extend yes with a recent doubletake

A single Miracast display and any number of AirPlay ones can run together.

Status: early. Confirmed working against real hardware — an LG webOS TV, a Samsung Tizen TV and an Apple TV — with picture, sound, and working mouse and keyboard. Expect rough edges, and read If it doesn't work before filing a bug — TVs vary more than you would hope, and the ones that fail tend to fail in ways that look like a bug here.

What you need

  • Omarchy with Hyprland 0.55 or newer, with its stock firewall (ufw) in place. That is what the automatic networking setup is built against.

  • A Wi-Fi adapter (with Wi-Fi Direct support for Miracast). Nearly all do. To check:

    iw list | grep -A 3 "valid interface combinations"

    You want a line offering managed alongside P2P-client or P2P-GO.

  • A Miracast TV. Most smart TVs since ~2015 qualify; look for "Screen Mirroring", "Screen Share" or "Miracast" in the source menu. Only needed for Miracast — AirPlay receivers want none of the above.

  • An AirPlay receiver, if you want that half: an Apple TV, or a TV that advertises AirPlay. It has to be on the same network as this machine.

Install

1. Install the casting backends

yay -S waycast-bin              # Miracast
yay -S doubletake-alchemy-bin   # AirPlay

waycast speaks Miracast and doubletake speaks AirPlay; doubletake-alchemy-bin is the build that carries extended-desktop support. The plugin finds them, drives them, and shows you what they are doing.

Install only the one you need. The panel names the other in a line of its own — AirPlay unavailable — install doubletake-alchemy-bin — and clicking that line copies the install command, so nothing has to be typed from memory. The line goes away once the package is there, without a restart.

Check your system is ready for Miracast:

waycast doctor

2. Networking — already handled

Installing waycast-bin sets up its networking helper for you. Miracast needs the TV to open a connection back to your laptop, and on a stock Omarchy firewall that is blocked; the helper opens exactly what a session needs, for as long as that session lasts, and closes it again afterwards.

You are not asked for a password when you cast: authorisation is granted once at install time, and casting is then available to whoever is logged in at the machine.

Two things to know:

  • A ufw reload ends any active session. Reloading rebuilds the chain the helper is using. Reconnect from the panel afterwards.
  • A custom firewall needs its own rule. The helper hooks into ufw. If you also run your own /etc/nftables.conf with a drop policy, an allow in ufw does not override a drop there — add tcp dport 7236 accept to your input chain. Stock Omarchy has no such ruleset and needs nothing.

If casting fails and you suspect the helper, check it is running:

systemctl status waycast-networkd

AirPlay needs none of this. It runs over the network you are already on, and its video and control channels are outbound connections, so a stock firewall does not block them.

3. Install the plugin

omarchy plugin add https://github.com/alchemy/omarchy-wireless-display.git --enable

It asks where to place the widget in the bar, then its icon appears there: a screen with wireless waves rising from its bottom-left corner.

The command clones the plugin, registers it with the shell and enables it, so no restart is needed. If the icon does not show up, omarchy restart shell.

To update it later, omarchy plugin update omarchy-wireless-display.

Using it

On the TV first: open its screen-sharing mode and leave that screen up. It is usually under the source or input menu — Screen Share on LG, Screen Mirroring on Samsung. Most TVs only accept connections while it is open.

Then click the icon. The panel searches automatically and lists what it finds.

  • Displays carry an EXTEND switch. Off — the default — the TV mirrors the screen you already have; on, it becomes a second monitor. Set it before connecting: once a display is live the switch shows what it negotiated and stops accepting clicks, because changing it means reconnecting. An AirPlay row carries the switch only when the running doubletake says it can extend; where it cannot, the row has no switch and the display mirrors.
  • An AirPlay extend needs the Miracast display disconnected first. Both protocols hand their virtual output to the same screen-share hook, so setting one up while a Miracast session is live would interrupt it. The panel refuses and says so rather than breaking a working stream.
  • Click 󰌷 on a display to connect. The TV usually asks you to approve the first connection from a new machine.
  • If an AirPlay receiver wants a PIN, the row opens a field for it — the same prompt the Wi-Fi panel uses for a passphrase. Type the code shown on the receiver and press Enter, or 󰄬. Escape gives up and ends the attempt. You are asked once; the credentials are saved and reused afterwards.
  • Connected displays move to the top of the list on a lighter background, each with a 󰅙 button to disconnect it.
  • Connecting a second Miracast display disconnects the first — Wi-Fi Direct allows one at a time. AirPlay receivers have no such limit: connect as many as you like, and a Miracast display alongside them.
  • One connection is set up at a time. While a display is connecting the other rows' buttons are greyed out and inactive; they come back when it settles.
  • 󰑓 searches again. If something is connected it asks first, because searching ends the session. It is greyed out while a search is already running.
  • When something goes wrong the message appears in red under the title, even while another display carries on streaming. Messages stay until you deal with them: 󰅖 dismisses them, and a rescan or a new connect clears them too, but nothing else does — not disconnecting, not closing the panel, not restarting the shell. Several failures are listed newest first, up to five. 󰆏 copies the exact text of all of them — backend errors are long and precise and worth pasting into a bug report rather than retyping.
  • Something worth knowing that is not a failure — say, a connect that went ahead in mirror mode because the AirPlay daemon cannot extend — appears dimmed under the errors and goes the same way.

The bar icon doubles as a status light. Its three waves appear one at a time while searching and fade in and out while connecting; once a display is connected they merge into a solid quarter-circle. When something has gone wrong the screen closes over them and shows a warning triangle.

Extending gives you a second monitor placed beside your existing one — 1920×1080 over Miracast, a canvas doubletake negotiates with the receiver over AirPlay — which Hyprland treats like any other display. Drag windows to it, and your mouse and keyboard work across both.

If it doesn't work

Nothing is found. Check the TV's sharing screen is still open — many TVs close it after a minute. Discovery is radio work, not network work, so a firewall cannot be the cause of an empty list.

It finds the TV, connects, then fails after a few seconds. The TV's connection is being dropped somewhere. Check the helper is running and has a session, then look for a second firewall that the helper does not manage:

systemctl status waycast-networkd
sudo nft list ruleset | grep -E '7236|waycast'

If reverse-path filtering is discarding the TV's packets the counter climbs while you try:

nstat -az | grep IPReversePathFilter

That is worth checking before changing, since the kernel takes the higher of the global and per-interface settings and the relevant interface is the p2p-* one that only exists during a session.

The TV shows a spinner but no picture. Give it a few seconds — the first frame can take a moment. If it persists, your TV may want a narrower video format than the 1080p this sends.

It worked once, now every attempt times out. Give the TV 30–60 seconds. TVs generally need to fully reset a session before accepting a new one, and rapid reconnects fail reliably until they do.

The picture arrives but only fills part of the screen. Some TVs display a 1080p signal at its native size rather than scaling it up. Look for a zoom, aspect or Screen Fit option in the TV's own picture menu.

No AirPlay devices are listed. The receiver has to be on the same network and reachable by mDNS — a guest network or client isolation on the access point will hide it. Check from a terminal:

doubletake -daemonize
doubletake-ctl discover

An AirPlay device says "Wrong PIN or password". The receiver rejected the code. Connect again and it will ask afresh — the digits change each time.

Note that a PIN and a password are different things, and the prompt says which one it wants. A PIN appears on the receiver's own screen when you connect. A password is one you set on the device beforehand (Apple TV: Settings → AirPlay and HomeKit → Require Password) and nothing is shown on screen at all — if you are waiting for a code to appear there, it never will.

Either way you are asked once. The credentials are saved and reused, so later connections go straight through.

The panel says no displays even with the TV ready. The shell may not be able to find waycast or doubletake. Its PATH is the graphical session's, not your terminal's:

tr '\0' '\n' < /proc/$(pgrep -f 'quickshell.*omarchy' | head -1)/environ | grep ^PATH

Installing the -bin packages from the AUR puts both in /usr/bin, which is always on that PATH. A copy built by hand in ~/.cargo/bin or a checkout's bin/ is not.

Still stuck? The session log says where it stopped:

cat "$XDG_RUNTIME_DIR/omarchy-wireless-display/daemon.jsonl"        # Miracast
cat "$XDG_RUNTIME_DIR/omarchy-wireless-display/daemon.err"          # Miracast
cat "$XDG_RUNTIME_DIR/omarchy-wireless-display/airplay-daemon.log"  # AirPlay

Both are capped, and the caps are why: they live on a tmpfs, so an unbounded log is the user's memory rather than their disk. The event log holds the first 200 events of a session and the error log the most recent 64 KB; a session that runs past the event cap is stopped, on the grounds that a well-behaved one costs seven events and 353 bytes.


Technical notes

Everything below is background. You do not need it to use the plugin.

How it fits together

The panel is a thin front end. It polls one shell script — bin/omarchy-wireless-display-ctl — which drives waycast and folds its JSON event stream into a state file the panel reads. The panel never talks to waycast directly, so the backend can change without touching the UI.

Extend mode gives the backend a virtual output of its own and streams that; mirror mode has no output to create, so the portal asks which screen to share. Either way the output reaches the backend through a one-shot override in ~/.config/hypr/xdph.conf, which points the desktop portal at it for a single capture request. waycast creates its output with hyprctl and arms the override itself; doubletake creates and names its own. They share the file, which is why only one of them may be setting an extend up at a time.

Driving it from the terminal

omarchy-wireless-display-ctl scan-start
omarchy-wireless-display-ctl state | jq .
omarchy-wireless-display-ctl extend <display-id>
omarchy-wireless-display-ctl mirror <display-id>
omarchy-wireless-display-ctl rescan        # ends any session, then searches
omarchy-wireless-display-ctl disconnect
omarchy-wireless-display-ctl clear-errors  # what the panel's dismiss button runs

state prints the same JSON the panel reads, which is the quickest way to see exactly where a connection stalled.

Configuration

Variable Default Purpose
OMARCHY_WIRELESS_DISPLAY_WAYCAST_BIN waycast Path to the waycast binary
OMARCHY_WIRELESS_DISPLAY_DOUBLETAKE_BIN doubletake Path to the doubletake binary
OMARCHY_WIRELESS_DISPLAY_DOUBLETAKE_CTL_BIN doubletake-ctl Path to its control client
OMARCHY_WIRELESS_DISPLAY_INTERFACE autodetected Wi-Fi interface for Miracast discovery
OMARCHY_WIRELESS_DISPLAY_DISCOVER_TIMEOUT 8 Search duration, seconds
OMARCHY_WIRELESS_DISPLAY_DISCONNECT_GRACE_SECONDS 10 Teardown grace before force-kill
OMARCHY_WIRELESS_DISPLAY_MAX_EVENT_BYTES 4096 Longest event line handed to the panel
OMARCHY_WIRELESS_DISPLAY_MAX_EVENTS 200 Events accepted before a session is judged broken
OMARCHY_WIRELESS_DISPLAY_MAX_STDERR_BYTES 65536 Daemon stderr retained
OMARCHY_WIRELESS_DISPLAY_FLOOD_GRACE_SECONDS 2 Teardown grace for a session stopped for flooding

How the two backends are driven

They are shaped differently, and the control script follows each rather than forcing a shared abstraction on them.

waycast is one blocking process per session that streams JSONL events. The script launches it, tails the log, and folds each event into the state file. One session at a time: it holds the Wi-Fi Direct interface, and in extend mode an edit to ~/.config/hypr/xdph.conf.

doubletake is a daemon that owns every stream itself and answers status with the full list, and with what it is capable of. There is nothing to tail and no pid to track — the script asks the daemon what it has and makes its own state agree. That reconciliation covers every case an event feed would need separate handling for, including a stream started by some other doubletake client, which the panel adopts and can end. The daemon is started when the panel first wants AirPlay and stopped again once nothing is using it, so an unopened panel leaves no mDNS chatter on the network. A daemon you started yourself is left alone.

Whether doubletake can extend is read from that reply — session_modes has to offer extend and per_session_mode has to be true — and never from a version or a package name, which is what doubletake asks of its clients: a newer binary can sit on disk while an older daemon is still running. Both fields are required, because a daemon taking a mode per connection but unable to extend would quietly mirror, and the panel would be claiming a second desktop that never appeared. A daemon that says nothing is an older one, and absent capabilities read as "cannot" rather than "unknown": the panel has to decide whether to draw a switch, and offering one that does nothing is the worse mistake.

Display ids are namespaced — miracast:<mac>, airplay:<ip> — so the panel hands one back without knowing which backend owns it, and the two lists cannot collide.

A receiver waiting for a PIN or password stays in pending with an awaiting field naming which of the two it asked for, and the panel expands that row into a prompt. The answer travels on stdin the whole way — from the panel's Process into omarchy-wireless-display-ctl credential <id>, and from there straight to doubletake's control socket, which the script speaks itself rather than going through doubletake-ctl. That client takes the code as a command-line argument, and an argument is readable by every local user with ps.

Only one connection is set up at a time, whichever protocol. Both backends reach for the screencast portal while they start, and waycast arms a one-shot picker override for its headless output moments before its own request; a second connect landing in that window could consume the override and be handed waycast's output instead of the screen it asked for.

How the daemon's output is bounded

waycast is a long-lived process whose JSONL event stream drives the whole panel. Its output is piped, never spooled: stdout goes through a filter that truncates each line, forwards at most a fixed number of events, and keeps a bounded copy on disk; stderr goes through a capture that retains only its most recent bytes. An earlier version wrote both streams to files with no ceiling and followed one of them with tail -F, which left the size of those files entirely to the daemon.

Past the event cap the filter keeps reading and stops writing. A filter that exits instead leaves the daemon blocked writing into a pipe nobody drains, and a process blocked in a write may never reach the handler for the signal that would end it. The one line the filter emits on breach is an event of its own, so the session is torn down by the ordinary path rather than a second one that would have to be kept correct — with a short grace, because a daemon that has flooded has already shown it will not shut down tidily.

The daemon, both filters and the event reader share one process group, so teardown takes all of them with a single signal; the daemon's own pid is tracked separately, because ending a session means SIGINT to waycast specifically, so its own cleanup runs.

How the networking gets out of your way

Miracast forms a direct Wi-Fi link between your machine and the TV, separate from your home network. Which end hosts that link is negotiated, and neither side chooses:

  • When the TV hosts, it assigns your machine an address and opens a control connection to it on TCP 7236.
  • When your machine hosts, it becomes the TV's DHCP server and has to answer the TV's requests.

An LG TV took the host role every time in testing; a Samsung about one time in four. Since the outcome is effectively random, both directions have to work, and neither is allowed by a stock desktop firewall.

Rather than have you open ports permanently, waycast ships a small root helper, waycast-networkd. The unprivileged part of waycast asks it over D-Bus to begin a session; the helper resolves the peer itself, watches for the real P2P interface to appear, and adds allowances scoped to that interface and to the ports actually negotiated — including the media sockets, which are chosen at runtime and cannot be known in advance. When the session ends, so do the rules. Polkit grants this to an active local session, which is why casting needs no password after installation.

The practical consequence is that firewall configuration is not part of using this plugin, and a permanently open port is not the price of casting.

Limitations

  • 1920×1080, not 4K over Miracast. Classic Miracast has no 4K in its negotiable formats, so 4K TVs still cap at 1080p there. AirPlay negotiates its own canvas with the receiver.
  • One Miracast display at a time. That backend holds the Wi-Fi Direct interface and the portal for a single session. AirPlay has no such limit.
  • AirPlay extend needs a recent doubletake. The switch appears only when the running daemon advertises both a per-connection mode and an extend mode. Capability is read from the daemon that answers, never from the version on disk, so a newer binary behind a still-running older daemon reads as cannot. Asked to extend anyway from the terminal, it mirrors and says so.
  • No AirPlay extend beside a live Miracast session. The two share one screen-share hook, and setting the AirPlay half up restarts the portal. Disconnect the Miracast display first.
  • A rejected PIN means starting over. doubletake does not re-prompt within the same attempt, so a mistyped code ends the connection and you connect again — with a fresh code, since receivers change theirs each time.
  • No signal strength — neither backend reports it.
  • Mouse only in the panel; no keyboard navigation yet.
  • Reconnects need a cooldown, per the troubleshooting note above.
  • A forced kill leaks state. SIGKILL skips cleanup, leaving a stray headless output; the next run detects and clears it. A normal quit, SIGINT or SIGTERM all clean up properly.

License

MIT — see LICENSE.

About

Omarchy Quickshell plugin for discovering and connecting to Miracast wireless displays from the desktop bar.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages