Skip to content

Latest commit

 

History

History
269 lines (193 loc) · 12.6 KB

File metadata and controls

269 lines (193 loc) · 12.6 KB

Developer Notes

This project is a standalone Quickshell config while the widget is still being shaped. Keep it runnable next to an existing end-4 dots setup.

Start The Widget

Run from the repository root:

quickshell --path quickshell

For a short smoke test that exits automatically:

timeout 5 quickshell --path quickshell --verbose

Expected successful load:

INFO: Configuration Loaded

Quickshell reloads when QML files change, so keep this process open while editing.

Idle behavior:

  • the default shape is a small pure-black bump at the top center
  • it reserves only a small constant top zone through layer-shell
  • hover expansion can overlap windows; this avoids Hyprland pushing windows down on every hover
  • the visible strip can be very thin, but the idle hitbox remains as tall as the reserved zone
  • top corners are sharp where the shape touches the screen edge
  • bottom corners use a capped adaptive radius; avoid height / 2 on large media layouts because it makes the island and volume trace too round
  • collapsed idle corners are slightly softer than before, but the handle must still read as a top-attached glacier, not a pill
  • hover the bump to expand it temporarily
  • hover shows a tiny text-style bump/strip switch inside the idle island content; the same styles are also available through IPC
  • hover shows battery charge in the menu when UPower exposes a laptop display battery
  • if a MPRIS player has active media, hovering the bump shows a compact media player instead of the generic idle peek
  • click the bump to pin or unpin the expanded idle state
  • click while a non-media event state is visible to collapse back to idle

Trigger Test States

Run these in another terminal from the repository root:

quickshell ipc --path quickshell show

The output should include target dynamicGlacier.

Manual state triggers:

quickshell ipc --path quickshell call dynamicGlacier demo
quickshell ipc --path quickshell call dynamicGlacier notify "Build finished" "Dynamic Glacier is alive" "Hello"
quickshell ipc --path quickshell call dynamicGlacier media "Night Drive" "Glacier FM" true ""
quickshell ipc --path quickshell call dynamicGlacier volume 72 false
quickshell ipc --path quickshell call dynamicGlacier handle bump
quickshell ipc --path quickshell call dynamicGlacier handle strip
quickshell ipc --path quickshell call dynamicGlacier toggleHandle
quickshell ipc --path quickshell call dynamicGlacier privacy true false
quickshell ipc --path quickshell call dynamicGlacier privacy false true
quickshell ipc --path quickshell call dynamicGlacier privacyLive
quickshell ipc --path quickshell call dynamicGlacier live true
quickshell ipc --path quickshell call dynamicGlacier live false
quickshell ipc --path quickshell call dynamicGlacier idle

privacy true false forces the green microphone dot. privacy false true forces the green camera dot. privacyLive clears the override and returns to real PipeWire/fallback detection.

Looping demo:

quickshell ipc --path quickshell call dynamicGlacier demoLoop

Run demoLoop again to stop the loop.

Inspect And Stop Instances

List this config's instances:

quickshell list --path quickshell

List every Quickshell instance, including end-4's shell:

quickshell list --all

Follow logs for this config:

quickshell log --path quickshell --follow

Stop only this config:

quickshell kill --path quickshell

Do not kill all Quickshell instances unless you intentionally want to stop the main end-4 shell too.

Where To Change Things

  • quickshell/shell.qml: root entrypoint. Keep this small.
  • quickshell/modules/dynamicGlacier/DynamicGlacier.qml: state machine, timers, window placement, IPC API.
  • quickshell/modules/dynamicGlacier/IslandSurface.qml: outer shape, glow, background, animation of shell geometry.
  • quickshell/modules/dynamicGlacier/IslandContent.qml: per-mode content layout for idle, notification, and media.
  • quickshell/modules/dynamicGlacier/IslandSurface.qml: also owns the subtle open U-shaped volume trace.
  • docs/architecture.md: bigger design decisions and integration notes.

If a change affects runtime behavior, test it with timeout 5 quickshell --path quickshell --verbose and at least one IPC command.

Live Link Tests

Dynamic Glacier currently listens to:

  • MPRIS media players through Quickshell.Services.Mpris
  • PipeWire default sink volume through Quickshell.Services.Pipewire
  • PipeWire link groups for microphone/video privacy activity through Quickshell.Services.Pipewire
  • shell fallbacks for privacy activity: pactl list source-outputs short for microphone capture and fuser /dev/video* for direct V4L2 camera users
  • UPower display battery through Quickshell.Services.UPower

Volume deliberately does not open a full island state because end-4 already has its own volume OSD. It only paints a light gray open U-shaped trace just inside the current island perimeter for about a second; the top edge must stay unconnected.

Privacy indicator behavior:

  • a small green dot appears to the right of the island when mic or camera privacy activity is detected
  • the dot is hidden immediately during hover; it must not animate or slide back into place after hover closes
  • the dot is informational only and should not reserve layout space or move Hyprland windows
  • if real camera/mic activity does not show the dot, first test the visual path with IPC privacy; then inspect PipeWire links and the shell fallbacks because some direct V4L2 camera users bypass PipeWire

Media hover behavior:

  • hovering the idle handle shows the media layout when a MPRIS player has an active track
  • live MPRIS track changes must not call showMedia() by themselves; they only update cached track fields for the next hover
  • the media layout should stay compact; fix clipping with internal padding and smaller controls/artwork, not by making the island tall
  • album art is read from MPRIS trackArtUrl; if it is missing or not loaded, the widget falls back to the minimal equalizer mark
  • track progress uses the active MPRIS player's position and length; the formatter accepts either seconds or MPRIS microseconds
  • clicking or dragging the media progress bar requests an absolute seek on the active MPRIS player when canSeek and positionSupported are true
  • previous, play/pause, and next buttons sit under the timeline and call the active MPRIS player directly through Quickshell
  • while the media layout is visible, the hover hitbox does not accept clicks so the player buttons can receive them
  • the bump / strip handle switch is also visible in the media layout, not only in the generic idle hover content

Hover menu behavior:

  • keep the bump/strip switch inside IslandContent.qml, not as a separate overlay sibling in DynamicGlacier.qml
  • use HandleStyleSwitch.qml for both idle and media layouts so hover/click behavior stays consistent
  • the switch should be text-like and minimal so it does not collide with the idle label
  • when interactionOpen is true, the top-level hover hitbox must use Qt.NoButton; otherwise it can steal clicks from the internal switch
  • privacy dots intentionally disappear during hover so they do not fight with expanded island interaction

Check available live sources:

playerctl -l
playerctl metadata --format '{{playerName}}|{{status}}|{{position}}|{{mpris:length}}|{{artist}}|{{title}}|{{mpris:artUrl}}'
wpctl get-volume @DEFAULT_AUDIO_SINK@
upower -e
upower -i /org/freedesktop/UPower/devices/DisplayDevice

Inspect active PipeWire source/link state:

pw-dump | jq -r '.[] | select(.type == "PipeWire:Interface:Node") | [.id, .info.props["media.class"], .info.props["node.name"], .info.props["node.description"], .info.props["application.name"]] | @tsv' | rg 'Audio/Source|Video/Source|Stream/Input/Audio|camera|microphone'
pw-dump --monitor | rg 'Audio/Source|Video/Source|Stream/Input/Audio|camera|microphone'
pactl list source-outputs short
fuser /dev/video*

Manual privacy visual tests:

quickshell ipc --path quickshell call dynamicGlacier privacy true false
quickshell ipc --path quickshell call dynamicGlacier privacy false true
quickshell ipc --path quickshell call dynamicGlacier privacyLive

Live microphone trigger test:

timeout 5 pw-record --media-category=Capture --media-role=Communication /tmp/dynamic-glacier-mic-test.wav

Live camera trigger test through PipeWire:

pw-dump | jq -r '.[] | select(.type == "PipeWire:Interface:Node") | select((.info.props["media.class"] // "") | test("Video/Source")) | [.id, .info.props["node.name"], .info.props["node.description"]] | @tsv'
timeout 5 gst-launch-1.0 pipewiresrc target-object=<node-name-or-id> ! videoconvert ! fakesink

Browser/portal camera usage should normally be visible through PipeWire link groups. Direct /dev/video* users are detected by the fuser /dev/video* fallback when the device node is visible to the session.

Safe volume event test:

wpctl set-volume @DEFAULT_AUDIO_SINK@ 1%+
wpctl set-volume @DEFAULT_AUDIO_SINK@ 1%-

Optional media event test:

playerctl play-pause
playerctl play-pause
playerctl position 10+
playerctl position 10-

The media test intentionally toggles playback and seeks the active player; do not run it if you do not want to touch the active player. Track changes from live MPRIS should not expand the island automatically; hover the idle handle to inspect the updated track.

Development Rules

  • Do not create repo-local AGENTS.md; project memory belongs in /home/mavxa/zed/agents/.
  • Keep the prototype standalone until integration points are clear.
  • Do not add NotificationServer by default yet. It can conflict with the existing end-4 notification daemon.
  • Do not depend on JS/HTML/CSS, Electron, webviews, AGS, or EWW for the main UI.
  • Prefer a small runnable QML slice over speculative architecture.
  • Keep state changes centralized in DynamicGlacier.qml; services should request mode transitions, not resize the island directly.
  • Keep live service listeners non-owning. MPRIS, PipeWire, and UPower are safe; notifications need bridge/integration work.
  • Keep the visual identity minimal, OLED-friendly, and distinct from Apple's Dynamic Island.
  • Default visuals should be pure black first; add visible decoration only when it materially improves interaction clarity.
  • Keep the island in normal WlrLayer.Top with ExclusionMode.Normal, but keep exclusiveZone constant and small.
  • Do not bind exclusiveZone to expanded island height; that makes Hyprland push windows down during hover.
  • Keep the attached-top silhouette: sharp top corners, capped rounded bottom corners.

Common Failure Modes

No running instances for ".../quickshell/shell.qml" means the widget process is not running or already exited. Start it again with quickshell --path quickshell.

IpcHandler is not a type usually means DynamicGlacier.qml is missing import Quickshell.Io.

Module path contains invalid characters means a QML import directory probably contains a hyphen. Keep the module directory named dynamicGlacier, not dynamic-glacier.

If the island does not appear but logs say Configuration Loaded, check whether it is hidden behind another layer or on the focused monitor. Current placement follows Hyprland.focusedMonitor and falls back to the first Quickshell screen.

If tiled windows move too much on hover, check DynamicGlacier.qml first. exclusiveZone should be root.reservedZone, not implicitHeight.

If the idle handle is too hard to find, test strip mode:

quickshell ipc --path quickshell call dynamicGlacier handle strip

If a gap appears between the island and the top screen edge, check expandedTopMargin in DynamicGlacier.qml. It should stay 0; the island should grow downward from the top edge.

If text looks clipped or deformed, check IslandSurface.qml z-ordering. Background shape rectangles should stay below IslandContent.

If live events get annoying while editing, disable them temporarily:

quickshell ipc --path quickshell call dynamicGlacier live false

Next Useful Tests

  • Run with the main end-4 shell active and verify there is no notification daemon conflict.
  • Test on each monitor by focusing a window there before starting the widget.
  • Test long notification and media strings for clipping and layout jumps.
  • Test volume values 0, 1, 50, 100, and muted state; verify only the open U-shaped trace appears, not a second mixer and not a top-connected loop.
  • Test media hover with an active player and verify the timeline updates and previous/play-pause/next buttons are clickable.
  • Keep an eye on animation feel after each visual change; aggressive motion will get annoying quickly.