# Folia Lyrics Guide English · [简体中文](guide.zh-CN.md) · [Back to project](../README.md) ## How it works This project is an Omarchy Quickshell `bar-widget` plugin that: - polls `http://127.0.0.1:32109/v1/lyric` by default; - reads word timing from `lines[].words[]`; - supports an optional signed top-level `offset` in milliseconds; - obtains the active player, playback position, and media title from MPRIS; - only follows the position of a player matching [`playerMatch`](#playermatch), so an unrelated player cannot advance the lyric time axis; - falls back to the MPRIS title when the API is unavailable or returns no lyrics; - can disable lyric requests entirely and operate as a standalone MPRIS title widget. The lyrics API provides a snapshot but no playback position. The player must expose the current media and `position` through MPRIS for synchronization to work. ## Display behavior - Past, current, and future words use different opacity levels. - The current word uses lightweight layered text for its glow, with no offscreen blur. - The previous line remains visible during gaps between lyric lines. - The final line remains visible after its timing ends, until the track changes. - Before lyrics begin, or when the API is unavailable or returns no lyrics, the MPRIS title is shown. - Long text scrolls inside the widget. Pausing freezes the scroll and adds a strikethrough. - Left-click toggles play/pause; right- or middle-click refreshes lyrics. ## Hover detail popup Hovering the widget expands a detail card below it, showing: - the MPRIS cover art (`trackArtUrl`), with a placeholder glyph when it is missing or fails to load; - title, artist, and album; - a progress bar and `mm:ss / mm:ss` times; only the elapsed time is shown for players that do not publish a length; - the current lyric line's **original and translation together**, independent of `lyricTextPriority`; the original keeps the same word-by-word highlighting as the bar; - previous / play-pause / next buttons, dimmed for actions the player does not support. The card is dismissed on a short delay after the pointer leaves, so the cursor can travel from the bar into the card to press a button. While enabled, the card replaces the plain text tooltip; set `detailPopup` to `"off"` to get the tooltip back. Both the card's data and its transport controls call `Quickshell.Services.Mpris` directly rather than going through the `omarchy.media` service, so they work on setups where that service is absent. ## Installation (Omarchy) Install the public Git repository and enable the plugin through Omarchy: ```bash omarchy plugin add https://github.com/chthollyphile/lia.lines.git --enable ``` The manifest places the widget in the center section by default. To move it explicitly: ```bash omarchy bar move lia.folia-lyrics --section center ``` Omarchy manages updates for Git-installed plugins: ```bash omarchy plugin update lia.folia-lyrics ``` When using Folia, enable its lyrics API in the application: ```text Settings → Connections and integrations → Lyrics API → Enable lyrics API ``` ## Installation (standalone Quickshell, without Omarchy) `LyricWidget.qml` is written against Omarchy's plugin API: it extends `BarWidget` from `qs.Ui`, uses `PopupCard`, `BorderSurface`, `Button`, and `PanelSeparator` from the same module, and reads the `Style`, `Color`, and `Border` singletons from `qs.Commons`. None of that exists in a plain Quickshell install. The `standalone/` directory in this repository supplies a minimal implementation of exactly those eight pieces — nothing else from Omarchy is required. ### Requirements - Quickshell, built with the `Mpris` and `Io` service modules (both are in the default build); - a Wayland compositor supporting `wlr-layer-shell`, for the example bar. Unlike Omarchy's own `PopupCard`, the bundled one does not use `HyprlandFocusGrab`, so the widget is not restricted to Hyprland; - `curl` on `PATH`, used to poll the lyrics API; - a Nerd Font, for the cover placeholder and transport button glyphs. ### Set up a dedicated configuration Quickshell resolves the `qs.*` import prefix from the root of the running configuration, so `qs.Commons` is always `/Commons`. Giving the widget its own configuration directory keeps the bundled modules from colliding with anything in your main shell: ```bash git clone https://github.com/chthollyphile/lia.lines.git cd lia.lines mkdir -p ~/.config/quickshell/folia-lyrics cp -r standalone/Commons standalone/Ui ~/.config/quickshell/folia-lyrics/ cp standalone/shell.qml LyricWidget.qml LyricModel.js ~/.config/quickshell/folia-lyrics/ qs -c folia-lyrics ``` That produces a thin bar on every screen containing only the lyric widget. The result should look like this: ```text ~/.config/quickshell/folia-lyrics/ ├── shell.qml # example bar; edit `settings` here ├── LyricWidget.qml # the widget ├── LyricModel.js # its data model ├── Commons/ # Style, Color, Border singletons └── Ui/ # BarWidget, PopupCard, BorderSurface, Button, PanelSeparator ``` ### Configure and theme it Every option documented under [Configuration](#configuration) is available. Instead of `shell.json`, set them in the `settings` block of `shell.qml`: ```qml LyricWidget { anchors.centerIn: parent settings: ({ displayMode: "auto", apiUrl: "http://127.0.0.1:32109/v1/lyric", maxWidth: 360, detailPopup: "on", popupWidth: 320 }) } ``` Without an Omarchy bar host the widget's `bar` property stays null and every color and font falls back to the bundled singletons. Set the font — including the Nerd Font needed for the glyphs — and the palette in `Commons/Style.qml` and `Commons/Color.qml`: ```qml // Commons/Style.qml property string fontFamily: "CaskaydiaCove Nerd Font" property int fontBaseSize: 12 property real spacingScale: 1.0 // scales the whole widget property int cornerRadius: 8 // Commons/Color.qml property color foreground: "#e6e6e6" property color accent: "#7aa2f7" property color urgent: "#f7768e" // current-word glow ``` ### Embedding it in an existing bar Copy the `LyricWidget { }` block from `shell.qml` into your own bar and give it a width, then merge the bundled modules into your configuration root. Because `qs.Commons` and `qs.Ui` always resolve there, this is where collisions happen: if you already have a `Ui/Button.qml` or a `Commons/Style.qml`, do **not** overwrite them. Keep your own and make sure they provide the members the widget reads: - `Style`: `space()`, `normalFillFor()`, `cornerRadius`, `gapsOut`, `font.{family,caption,bodySmall,body,subtitle,displayLarge,iconLarge}`, `spacing.{labelGap,controlPaddingX,controlPaddingY,panelGap,popupPadding}`, `bar.sizeHorizontal` - `Color`: `foreground`, `accent`, `urgent`, `popups.{background,border}` - `Border`: `none()`, `controlSpec(state, foreground, accent)`, `width(spec)`, `color(spec)` - `Button`: `iconText`, `foreground`, `iconSize`, `horizontalPadding`, `verticalPadding`, `clicked()` - `BorderSurface`: `borderSpec`, plus `color` and `radius` from `Rectangle` - `PanelSeparator`: `foreground` - `PopupCard`: `anchorItem`, `bar`, `owner`, `triggerMode`, `open`, `contentWidth`, `contentHeight`, `containsMouse`, `fittedContentWidth()`, `fittedContentHeight()`, and a default content property ### Differences from the Omarchy build - With `detailPopup` set to `"off"` there is no hover feedback at all: the plain text tooltip is drawn by Omarchy's bar host, which is absent here. - There is no popout coordinator, so the detail card neither closes nor is closed by other panels. - Active-player selection falls back to the widget's own MPRIS scan; Omarchy's `omarchy.media` service is never consulted. Transport controls behave identically either way, since they already call MPRIS directly. - Diagnostics use Quickshell's own IPC rather than `omarchy-shell`: ```bash qs -c folia-lyrics ipc call lia.folia-lyrics status qs -c folia-lyrics ipc call lia.folia-lyrics refresh ``` ## Uninstallation Remove the plugin and its managed installation through Omarchy: ```bash omarchy plugin remove lia.folia-lyrics ``` For a standalone install, delete the configuration directory: ```bash rm -rf ~/.config/quickshell/folia-lyrics ``` ## Configuration Configure the widget entry in `~/.config/omarchy/shell.json`: ```json { "id": "lia.folia-lyrics", "displayMode": "auto", "lyricTextPriority": "original", "maxWidth": 360, "pollInterval": 750, "apiUrl": "http://127.0.0.1:32109/v1/lyric", "playerMatch": "folia", "glow": "on", "detailPopup": "on", "popupWidth": 320, "popupHideDelay": 250 } ``` ### `displayMode` - `"auto"` (default): prefer synchronized lyrics and fall back to the MPRIS title; - `"mpris"`: stop polling the lyrics API and operate only as an MPRIS title widget. ### `apiUrl` Any player endpoint may be used if its response is compatible with the Folia v1 lyrics API. A loopback address is recommended; do not expose an unauthenticated endpoint through port forwarding or a reverse proxy. ### `playerMatch` Comma-separated substrings, matched case-insensitively against each MPRIS player's identity, desktop entry and bus name. Default `"folia"`. The lyrics API only returns a snapshot, so playback position has to come from MPRIS. Without this filter, any other player — a browser tab, a video — would advance the lyric time axis of a track it is not playing. Lyrics therefore follow a matching player only; while a non-matching player is the active one, the widget shows that player's media title instead. Set it to the name of your own player when using a different Folia-compatible endpoint, or leave it empty to accept any player. Run the `status` diagnostic to see what the running players report as `playerIdentity`. ### `lyricTextPriority` - `"original"` (default): display the original text with its word timing; - `"translation"`: prefer each line's `translation` and fall back to the original text when it is missing. Translations normally have no word timing, so the plugin treats the line's `startTime` and `endTime` as timing for the complete translated sentence. Romanization is not offered as a display option. ### `maxWidth` Maximum lyric or title width, from 120 to 640. Text exceeding this width scrolls. ### `pollInterval` Lyrics API polling interval in milliseconds, from 500 to 5000. A value between 500 and 1000 is recommended. ### `glow` - `"on"` (default): show the current-word glow; - `"off"`: disable the glow. A vertical bar displays a compact music symbol instead of text and glow effects. ### `detailPopup` - `"on"` (default): expand the detail card on hover; - `"off"`: never expand the card, and show the plain text tooltip on hover instead. ### `popupWidth` Detail card width, from 240 to 520. It is narrowed automatically when the screen has less room available. ### `popupHideDelay` Delay in milliseconds before the card is dismissed after the pointer leaves the widget, from 0 to 2000. The delay is what lets the pointer cross the gap between the bar and the card; setting it to `0` makes the card's buttons unreachable. ## Compatible API response Basic response example: ```json { "offset": 500, "wordByWord": true, "title": "Example", "artist": "Artist", "lines": [ { "text": "This is a lyric line", "startTime": 3.2, "endTime": 7.8, "words": [ { "text": "This ", "startTime": 3.2, "endTime": 4.1 } ] } ] } ``` The response may also be JSON `null`, indicating that no lyrics are available. Timing rules: - `startTime` and `endTime` are expressed in seconds; - the top-level `offset` is optional and expressed in milliseconds; - a positive `offset` delays every line and word; - a negative `offset` advances every line and word; - a missing or invalid `offset` is treated as `0`. ## Controls and diagnostics Refresh lyrics manually: ```bash omarchy-shell lia.folia-lyrics refresh ``` Inspect runtime state: ```bash omarchy-shell lia.folia-lyrics status ``` The status output includes API, MPRIS, display mode, lyric offset, and scrolling state. ## Development checks Run the data-model tests with Node.js: ```bash node --test tests/lyric-model.test.js ``` At runtime, Omarchy's `omarchy-shell` provides `BarWidget`, `qs.Ui`, and `qs.Commons`.