Desktop lyrics overlay and video wallpaper for macOS.
Displays synced lyrics from LRCLIB over your desktop, with optional video wallpaper and mouse-reactive ripple effects. Text appears with a matrix-style decode animation.
If lyra is useful to you, please consider starring the repo. It helps other macOS users find the project and supports future official Homebrew submission.
# via Homebrew
brew tap generald/tap
brew install lyra
# via Mint
mint install GeneralD/lyra
# or build from source
make installlyra start # start as background daemon
lyra stop # stop the daemon
lyra restart # restart
lyra daemon # run in foreground (debug)
lyra version # show version
lyra healthcheck # check API connectivity
lyra config template # print default config to stdout
lyra config init # create config file with defaults
lyra config edit # open config in $EDITOR
lyra config open # open config in GUI app
lyra track # show now-playing info as JSON
lyra track -r # resolve metadata (MusicBrainz/regex)
lyra track -l # include lyrics (LRCLIB)
lyra track -rl # resolve + lyrics
lyra benchmark # measure CPU/memory baselines
lyra benchmark -d 30 # 30s per scenario
lyra benchmark --json # JSON output for CI# via Homebrew (recommended for Homebrew installs)
brew services start lyra
brew services stop lyra
# or manually (Mint / source-build users)
lyra service install # register LaunchAgent directly
lyra service uninstallNote: Both methods use LaunchAgent but with different labels (
homebrew.mxcl.lyravscom.generald.lyra). Use one approach — do not mix them, or the daemon will run twice.
# zsh / bash / fish
eval "$(lyra completion zsh)"Homebrew installs completions automatically.
# Generate a starter config with all defaults
lyra config init # creates ~/.config/lyra/config.toml
lyra config init --format json # JSON variant
lyra config template > custom.toml # pipe to any pathOr create ~/.config/lyra/config.toml (or config.json) manually. All fields are optional — missing values use sensible defaults.
Alternative paths: ~/.lyra/config.toml, $XDG_CONFIG_HOME/lyra/config.toml
| Key | Type | Default | Description |
|---|---|---|---|
screen |
string / int | "main" |
Which display to use (see Screen selection) |
screen_debounce |
number | 5 |
Seconds between re-evaluations in "vacant" mode |
wallpaper |
string | — | Video wallpaper. Local path, HTTP(S) URL, or YouTube URL (see Wallpaper) |
includes |
array | — | TOML-only: list of additional TOML files to merge (ignored for config.json; paths relative to config dir or absolute) |
All text sections inherit from [text.default]. Section-specific values override the base.
| Key | Type | Default | Description |
|---|---|---|---|
font |
string | system font | Font family name (e.g. "Helvetica Neue") |
size |
number | 12 |
Font size in points |
weight |
string | "regular" |
Font weight: "regular", "medium", "bold", etc. |
color |
string / array | "#FFFFFFD9" |
Solid hex "#RRGGBBAA" or gradient ["#AAA", "#BBB"] |
shadow |
string | "#000000E6" |
Shadow color in hex |
spacing |
number | 6 |
Vertical padding around each line |
Each overrides specific properties from [text.default]. Unset properties fall back to the base.
| Section | Built-in overrides |
|---|---|
title |
size = 18, weight = "bold" |
artist |
weight = "medium" |
lyric |
inherits default as-is |
highlight |
color = ["#B8942DFF", "#EDCF73FF", "#FFEB99FF", "#CCA64DFF", "#A68038FF"] (gold gradient). Inherits from lyric, then default |
Controls the matrix-style text reveal animation.
| Key | Type | Default | Description |
|---|---|---|---|
duration |
number | 0.8 |
Animation duration in seconds |
charset |
string / array | all | Character sets for scramble: "latin", "cyrillic", "greek", "symbols", "cjk". Single string or array |
| Key | Type | Default | Description |
|---|---|---|---|
size |
number | 96 |
Album artwork size in points |
opacity |
number | 1.0 |
0 hides artwork (text aligns left), 1 fully visible |
Mouse-reactive ripple effect on the overlay.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Set to false to disable ripple effects entirely |
color |
string | "#AAAAFFFF" |
Ripple color in hex |
radius |
number | 60 |
Ripple radius in points |
duration |
number | 0.6 |
Ripple animation duration in seconds |
idle |
number | 1 |
Seconds before ripple fades after mouse stops |
shape |
string / table | "circle" |
Ripple outline shape. See below |
Polymorphic shape spec. Three accepted forms:
# 1. Omit entirely → defaults to circle
[ripple]
radius = 60
# 2. Bare string for parameterless shapes
[ripple]
shape = "circle"
# 3. Table form for shapes that take parameters
[ripple.shape]
type = "polygon"
sides = 6
angle = 15| Shape | Required keys | Optional keys | Notes |
|---|---|---|---|
circle |
— | — | Same as omitting shape |
polygon |
sides (int 3...256) |
angle (degrees, default 0) |
Out-of-range sides values fail config decoding. angle = 0 orients one vertex straight up |
Optional LLM-based song title and artist extraction via any OpenAI-compatible API. When omitted, lyra uses regex-based parsing only. All three fields are required to enable this feature.
| Key | Type | Default | Description |
|---|---|---|---|
endpoint |
string | — | OpenAI-compatible API base URL (e.g. "https://api.openai.com/v1") |
model |
string | — | Model name (e.g. "gpt-4o-mini") |
api_key |
string | — | API key for authentication |
Tip: Keep your API key out of version control by splitting
[ai]into a separate file and usingincludes:# config.toml includes = ["ai.toml"]# ai.toml (add to .gitignore) [ai] endpoint = "https://api.openai.com/v1" model = "gpt-4o-mini" api_key = "sk-..."Included files are merged into the main config. Values in the main file take precedence over included ones.
| Value | Behavior |
|---|---|
"main" |
Current main display (with focused window) |
"primary" |
Primary display (menu bar screen) |
"smallest" |
Smallest display by area |
"largest" |
Largest display by area |
"vacant" |
Least-occupied display (auto-migrates every screen_debounce seconds) |
0, 1, … |
Display by index |
The wallpaper field accepts three types of values:
# Local file (relative to config dir or absolute)
wallpaper = "loop.mp4"
wallpaper = "/Users/me/Videos/bg.mp4"
# Direct HTTP(S) URL
wallpaper = "https://example.com/background.mp4"
# YouTube URL
wallpaper = "https://www.youtube.com/watch?v=XXXXX"
wallpaper = "https://youtu.be/XXXXX"Remote and YouTube videos are downloaded once and cached in ~/.cache/lyra/wallpapers/. Subsequent launches use the cached file instantly.
YouTube requirements:
| Tool | Install | Notes |
|---|---|---|
yt-dlp |
brew install yt-dlp |
Preferred. Downloads video-only H.264 at up to 4K |
uvx |
brew install uv |
Zero-install alternative — runs uvx yt-dlp without global install |
ffmpeg |
brew install ffmpeg |
Required for auto-loop. Remuxes DASH container to standard MP4 |
If neither yt-dlp nor uvx is found, lyra will show an error. If ffmpeg is not found, the video plays but may not loop automatically.
Trim playback range (optional):
[wallpaper]
location = "https://www.youtube.com/watch?v=XXXXX"
start = "0:30" # skip intro
end = "3:45" # stop before outro
scale = 1.15 # enlarge this video to hide letterboxingTime format: M:SS, H:MM:SS, or fractional seconds (1:23.5). Both start and end are optional. scale defaults to 1.0; values above 1.0 zoom the current video only. The bare string format (wallpaper = "file.mp4") still works for simple cases.
Multiple wallpapers (optional):
Provide multiple videos with [[wallpaper.items]] and choose between sequential (cycle) and random (shuffle) playback:
[wallpaper]
mode = "cycle" # or "shuffle" — default is "cycle"
[[wallpaper.items]]
location = "loop.mp4"
[[wallpaper.items]]
location = "https://www.youtube.com/watch?v=XXXXX"
start = "0:30"
end = "3:45"
scale = 1.2
[[wallpaper.items]]
location = "https://example.com/bg.mp4"
scale = 1.05cycleplays items in the order written, advancing when each item finishes (wraps around at the end).shuffleadvances to a random item each time playback completes, never repeating the current one twice in a row.scaleis configured per item, so videos with different letterboxing can use different zoom values.- All items are resolved in parallel. In
cycle, playback starts as soon as the first configured item is ready — later items play in configured order regardless of download speed. Inshuffle, playback starts with whichever item resolves first.
includes = ["ai.toml"]
screen = "vacant"
screen_debounce = 5
[wallpaper]
mode = "cycle"
[[wallpaper.items]]
location = "https://www.youtube.com/watch?v=Sn1ieBOLGB0"
start = "0:17"
end = "3:37"
[[wallpaper.items]]
location = "https://www.youtube.com/watch?v=P0az9IS2XQQ"
start = "0:24"
end = "3:15"
scale = 1.325
[text.default]
font = "Zen Maru Gothic"
size = 12
color = "#FFFFFFD9"
shadow = "#000000E6"
spacing = 6
[text.title]
font = "Zen Kaku Gothic New"
size = 18
weight = "bold"
[text.artist]
font = "Zen Kaku Gothic New"
size = 12
weight = "medium"
[text.lyric]
color = "#FFFFFFE6"
[text.highlight]
color = ["#B8942DFF", "#EDCF73FF", "#FFEB99FF", "#CCA64DFF", "#A68038FF"]
[artwork]
size = 96
opacity = 0.8
[ripple]
color = "#AAAAFFFF"
radius = 60
duration = 0.4
idle = 1.3This example uses Zen Maru Gothic and Zen Kaku Gothic New. If those fonts are not installed, install them with Homebrew Cask:
brew install --cask font-zen-maru-gothic font-zen-kaku-gothic-new[ai]
endpoint = "https://api.openai.com/v1"
model = "gpt-4o-mini"
api_key = "sk-..."- macOS 14+
- Swift 6.0+
GPL-3.0

