Skip to content

Decky 0.3.0

Choose a tag to compare

@github-actions github-actions released this 12 Sep 11:21
cccc15a

Desktop app 0.3.0 · Deck firmware 0.1.5 (updated)

Installed Decky updates itself: use the updates pill in the title bar, or Settings → Updates. New installs: run the Decky-Setup installer. The deck's firmware updates from the same place, with the deck on USB.

Desktop

  • Keep the profiles panel still, and let it wear the theme's scrollbar (cccc15a)
    Arming the bin put a warning above the list, which pushed the list down
    under the pointer; it goes to the notice at the foot of the window instead,
    and a row keeps its height while its name is being typed. The rule that
    separated the list from the buttons hung below them, separating nothing - it
    sits above them now, and in the import view under the last line of what the
    profile holds, with the name field clear of it.
    Scrollbars are Decky's everywhere rather than Windows' own: one rule, so a
    scrolling area added later cannot arrive in the wrong colours.
    The UI check walks the import view now - what a profile holds, the widgets
    this Decky has retired, what its keys would run, and the name it will be
    kept under - and asserts the list does not move when the bin is armed.
  • Name a profile as it is imported, and never keep two of the same name (eaae396)
    Importing your own profile back left two called Default. The inspector now
    has the name it will be kept under, filled in with the one it came with and
    yours to change before you press the button; and a name another profile
    already has takes a number, whatever it was asked for.
  • Make a hover felt, and a delete take two presses (37a37c2)
    The rows in Pages and Profiles, their icon buttons, the panel close buttons,
    the editor's tabs and the icon grid had no hover at all, and the dock's was
    four percent of a warm white - so the pointer said nothing. They lift now,
    each over 0.15s, and the bin reddens where it means it.
    Deleting a profile takes two presses: the bin arms, a tick confirms, and it
    disarms itself after four seconds. While it is armed the panel says what
    goes with it - the scripts the profile brought.
  • Let Windows open a .deckyprofile with Decky, and keep it that way (e55ea0e)
    The installer registers the file type, but nothing kept it right afterwards:
    an in-app update, a Decky that moved, or a copy run from elsewhere would
    leave it pointing at where Decky used to be. A packaged Decky now writes the
    four HKCU\Software\Classes keys on start, and only the ones that do not
    already point at itself - no administrator, and nothing written when it is
    already right.
    The icon comes from the branding through build-icons.mjs, which now builds
    both icons in one pass, and ships unpacked: inside the asar Windows could
    never have read it.
  • Keep several profiles, and carry one whole in a .deckyprofile (6aa1d09)
    Each profile is a folder of its own under profiles/ - its pages, what its
    widgets counted, and the apps it talks to - with the one setup an older
    Decky kept adopted as the first. What belongs to the deck rather than a
    setup, its pairing and its firmware, stays where it was, so switching never
    unpairs anything. Switching reloads what a profile owns in place and lets
    the deck start a fresh session, so two profiles' pages never sit in its
    memory at once.
    A .deckyprofile carries a whole setup: its pages, the apps' settings without
    their passwords, and the scripts its keys run, gzipped and encrypted with a
    key Decky carries - which keeps the file out of a text editor rather than
    out of anyone's hands, and says so where it matters. Importing adds a
    profile, never replaces the one in use: the files land in that profile's own
    folder and its keys are rewritten to point there.
    Nothing is written before it is seen. One inspector serves both importing a
    file and switching to a profile of your own: its pages, its widgets - the
    ones this Decky no longer has named and counted - and every program and
    script a key would run, which is the part worth reading in someone else's
    profile.
  • Have each kind of action and widget say what files it keeps (c4f46c5)
    An exported profile has to carry the scripts its keys run, and a list of
    what to carry would be wrong the first time someone added a kind and forgot.
    Instead every kind says for itself, beside where it is already declared:
    ACTION_FILES gives the paths in an action and the action with them moved,
    and whether the file travels at all - a script does, a program is wherever
    it is installed. WIDGET_FILES does the same for widgets, which keep none
    today.
    Both are mapped over every kind, so a new action or widget does not compile
    until it has an answer.
  • Give the page you are looking at the deck's last room for a live picture (28deffb)
    The widget keys that kept going blank were the deck running out of PSRAM:
    ERR live memory, forty times in deck.log, from the moment a microphone key
    was added to a profile of 8 pages and 25 widgets. A key picture is ~29 KB,
    but live_room() asks for the 768 KB floor plus a whole page in one piece
    before it will allocate one, so live pictures stop long before memory is
    full - and the app, told nothing it could act on, kept sending.
    The firmware now takes the live pictures of the oldest page copy that is not
    on screen rather than refusing, and the desktop does the same from its side
    on ERR live memory, leaving hidden pages alone for a minute afterwards.
    Neither needs the other: the deck's own eviction shows up as a base-CRC
    mismatch, which already sends the whole key again.
  • Add a speed test key, over Speedtest.net's own servers (cddc48e)
    Tap it and it measures the line: their config for where you are, their
    server list for what is near, the five nearest pinged and the quickest kept,
    then ten seconds down and ten seconds up. Tap again to stop. It ends on both
    speeds with the ping, and rests there until it is tapped again.
    While it hunts for a server the key is a ring the deck turns itself; while
    it measures it is a dial like the volume's, following the speed on a
    logarithmic scale and eased towards each reading, with the megabits in the
    middle.
    Decky's own client for their published endpoints, not Ookla's code or
    binary: speedtest-net@1 is 2019-era with old transitive dependencies, and
    its v2 downloads a CLI that does not survive packaging. Bytes are counted as
    they stream over six connections down and four up, the first 1.5 s dropped
    for slow start, so no correction factor is needed.
    Their mirrors are asked in their own client's order, c.speedtest.net first:
    the www ones hand out a short list of whatever, and a run from Istanbul
    picked Liechtenstein at 46 ms. With c. first the same line finds Istanbul at
    7 ms. A server that answers a ping but serves nothing is dropped for the
    next best, and a dropped connection costs that stream its file, not the run.
  • Let a key's icon size be set back to 100% (24a2577)
    The size slider means "no setting" by 100%, so it hands the editor an
    appearance with no iconSize at all - and the key editor merged that with the
    key it had, where a field left out cannot win. The size sprang back to what
    it was, and the next step over 100 skipped it: 95, then 105.
    Appearance fields are now replaced together (withAppearance), so leaving one
    out takes it away, as removing an image already did by hand. The UI check
    fails if the slider ever springs back off 100.
  • Say in deck.log why a widget key's picture is not reaching the deck (2b8c4b4)
    A key that stops showing what it should leaves no trace today: the send is
    refused, skipped as unchanged, or never made, and all three look the same
    from outside. Each page load now logs the copy it made, and a widget key
    logs what became of its pictures - at most a line every ten seconds, or a
    minute for one that keeps drawing the picture the deck is thought to hold -
    until it is sending again.
    Also replays the reported case in a test: a key edited on a page of live
    widgets, and a widget key added afterwards, both keep their pictures on the
    deck. It passes, so whatever is wrong is not in this path.
  • Drop the icon/text spacing setting (1f5a3a7)
    The gap between a key's icon and its label is now what the setting's
    default gave (a fifteenth of the key), so nothing looks different; the
    slider, the saved labelGap and its validation are gone. Old profiles that
    carry one still load: the field is simply left behind.
  • Pick a widget's design on the key itself (2a1d931)
    A hold on a clock, a die or a level offers the looks it can take, the way
    Discord's microphone key offers its devices: the key draws each design over
    a dot for each, a tap moves to the next, and three quiet seconds keep the
    one showing in the profile.
    Each kind names its designs, so a new one needs nothing more. Dice held to
    roll, which a tap already did, so its hold is free for this; and the deck
    stops turning a key while a design is picked, so the preview shows.
  • Keep a key's picture in the middle when it has a label (292b5e7)
    A label used to move what the key shows: the icon and the label were
    centred together, so the icon rode up, and a widget's caption pushed the
    widget down the key. Now the label sits at the foot of the key and the
    icon stays in the middle, shrinking only as far as the label and the
    spacing under it need; a widget's caption leaves as much room at the
    foot as it takes at the top, so the widget is a little smaller and stays
    where it was.
  • Notice a deck that went, instead of waiting on a link that is gone (59d1e57)
    A cable pulled and plugged back in while Decky was loading left it looking
    connected: the unplug had fallen back to Wi-Fi, the replug took the deck's
    power, and a write to a peer that is gone waits on TCP's own timeouts -
    minutes - with every deck command queued behind it, the ping that would
    have noticed included. Meanwhile the deck came back from its card with the
    pages but no live pictures, and the app went on believing it held them, so
    widget keys stayed blank while the window drew them.
    Nothing here asks the deck anything more often; it all watches this PC:
    • the socket goes when the deck ends it, and a write to one that is gone is
      refused rather than throwing EPIPE at whoever asked;
    • a write over Wi-Fi that takes more than 3 seconds counts as the link gone;
    • a ping still waiting while the deck has said nothing for 8 seconds
      (DeckLink.silentFor) means the queue is stuck on a dead link: the deck
      counts as gone and closing the link frees everything waiting;
    • the port watch notices a port go, not only appear, and this deck's own
      cable going means it is gone at once;
    • a patch that fails because the deck went is no longer an error: the window
      draws the key again once it is back;
    • HELLO drops every live picture on the deck, so the app forgets its record
      of them there too - it forgot only the overlays before.
  • Grey out an app's keys while it is out of reach, and go back where you came from (dd5e0bd)
    A key that controls an app it cannot reach now looks disabled, on the
    deck and in the window: its icon grey and struck through, its label
    grey, an image greyed - the look its app's widgets already had. Only its
    OFF picture is drawn so, since an app out of reach reports no state and
    its toggles show OFF; the deck takes ON pictures whole, 29 KB each, and
    redrawing those on every change of an app's reach froze it for seconds.
    A key that cycles through options is its own thing now (choice.ts, the
    renderer's choice face): the option chosen, a dot for each, a tap moving
    to the next. Discord's microphone and output switchers are the first
    two, and another key can be one.
    Back returns to the page it was opened from - a page reached from
    several goes back where you were - and to the page it was created under
    when that is not known or is gone. Pictures drawn for the profile as it
    was, still waiting behind another upload when it is saved again, are
    answered as stale and drawn again, instead of being refused as an error.
  • Let a key size its icon, and add the logos Lucide lacks (fbf35bc)
    Appearance has an Icon size slider (40-200%), for any key that shows an
    icon and for a widget that draws the key's own - a voice channel with no
    one in it. The picker also searches about sixty brands' logos (Simple
    Icons, CC0) that Lucide has none of: Discord, OBS Studio, Twitch,
    Spotify, Steam, GitHub. A logo is drawn solid in the key's colour, a
    little smaller than an outline icon so it weighs the same.
    A widget's view can now say that it draws the key's icon, and add its
    own looks to the Appearance tab - a voice key's layout, and whether it
    shows the channel's name. Any library icon draws on a widget's canvas at
    once (key-icon-canvas.ts), so a widget's first picture has it too.
  • Add Discord's keys, through StreamKit (bf69d89)
    Discord gives RPC to an unapproved application only for its owner, so
    Decky asks as Discord StreamKit Overlay, whose permission any user can
    give: Authorize once, and the token lasts seven days, kept encrypted.
    This is unofficial, and Discord can end it.
    Its controls are toggle keys whose ON and OFF follow Discord, however
    they were switched: mute, deafen, camera, screen share, noise
    suppression, echo cancellation, automatic gain and leave call. Deafened
    counts as muted, as Discord counts it. The window never says ON and OFF
    for them; each control names its two states (Unmuted and Muted, Not in a
    call and In a call). Camera and screen share come from Windows' own
    record, which Discord's RPC does not report, and starting a share brings
    Discord to the front as it was left - a discord:// link would take it
    to Home.
    Its widgets: a voice channel or the call you are in, showing who is
    there as avatars with a green ring on whoever speaks; the microphone and
    output Discord uses, a tap moving to the next; and how many
    notifications came since the last tap, a tap opening that conversation.
    Discord lists no devices for a moment after it starts, so a switcher
    waits rather than showing a device that is not there.
  • Show Decky on Discord, when its owner wants it (fcd196a)
    Settings has a Show on Discord switch, off until it is turned on. On,
    friends see "Watching Decky": a line for what is happening - the deck at
    hand, a key being edited, OBS recording or streaming, an update
    installing, the deck unplugged - the presses on the deck today, and for
    how long.
    It goes over Discord's own pipe as Decky's application, with no sign-in
    and no secret, and nothing leaves the PC but what Discord shows. A card
    goes at most every four seconds and only when it changed; Discord
    closed, it is tried again every fifteen. On and today's presses are kept
    in presence.json.
  • Let the deck move a ring's arc itself, and keep its loop free (53d3f95)
    A ring drawn a second at a time steps; over USB every frame would be a
    key upload. The deck moves the arc now (SWEEP, sweep=1 in ID): the app
    sends where the ring is, where it goes and by when, in 32 bytes, and the
    deck draws each frame between its own blocks. Sliding text works the
    same way, so both are KeyOverlay subclasses on a page, cloned into a new
    version and sent after the picture they belong to.
    Two things blocked the loop while that ran: waiting for the panel's
    vertical blank, and waiting for a key to finish drawing. The vertical
    blank is stamped by its interrupt now (Bus_RGB, panel::last_vsync_us),
    so nothing waits on it, and draw_key gives way while it waits, drawing
    overlay frames meanwhile. DISPLAY_STATE says how many overlay frames
    were drawn and the longest gap between them.
    The keys' own countdowns still step: a whole second at a time, so a
    picture drawn again for another reason is the same picture and never
    travels. Only OBS's five-second ring sweeps, as the user asked.
  • Load everything at boot, not at the first press (96d37bc)
    Every page's widgets and the looks the deck turns go up while the deck
    still shows its progress, so no page opens on a placeholder or waits for
    a look. The hotkey helper starts with Decky rather than on the first
    press, which used to wait for it.
  • Add OBS Studio as the first app Decky talks to (404337c)
    An app is declared once (shared/integrations/): its name, its settings,
    what is said of each standing - not installed, closed, its API off, its
    password refused, connected - and what its card's button does then. The
    window draws its page and its connection card from that, so a new app
    needs no screens of its own; main gives it a service and a row in
    IntegrationServices.
    OBS speaks obs-websocket 5. Its Recording and Streaming keys follow the
    events OBS sends, never a status asked for again, since answers can come
    back out of order and one did leave a stopped recording counting. A
    touch counts five seconds down before OBS is asked, and a second touch
    calls it off. The link is kept while any OBS key exists, retried while
    OBS is closed, and Open OBS starts the copy the finder found.
    A key can also be an app's control ({ kind: "app" }), which the runner
    leaves to the workspace.
  • Make the volume and microphone one kind, and taps a table (ee95c4f)
    They were near copies of each other. Both are one kind now, made for a
    flow: shared/widgets/level.ts, the renderer's kinds/level/, and main's
    SoundAction. The microphone shows as a level like the volume, with its
    own dial and mute.
    What a press does is a table too. tap-runs.ts counts taps and holds and
    gives the widget's action the gesture it knows (a tap, a double, a
    triple, a hold), so a widget that tells them apart waits for the next
    tap and one that does not acts at once. WidgetReadings holds what
    widgets read - pings, feeds - and follows the profile in one place.
  • Split the app's main process into parts, and widgets into kinds (fc5aece)
    main/index.ts only builds and wires the parts, each in its folder: app/,
    deck/, pages/, widgets/, profile/, updates/, ipc/, logging/.
    Each widget type is a kind (shared/widgets/: its settings, defaults,
    validation, what a press does to it), a view (renderer widgets/kinds/:
    its drawing, settings, sample readings, deck look) and, when a press
    does more than change its state, an action (main/widgets/actions/).
    Each has a registry, typed so a type missing from one does not compile.
    Step and action kinds, and the serial link's kinds of line, are tables
    too.
    Comments name the firmware's new files.
  • Add live widget keys, and keep edits fast (f85f834)
    Thirteen widgets: clock, timer, pomodoro, countdown, counter, ping, note,
    now playing, volume, microphone, system, crypto and dice. The app draws
    them and patches them onto the deck as they change. Sound and prices
    arrive as events (Core Audio callbacks, Binance's public stream), not
    polls, so mute and volume show at once. Keys the deck can animate are
    handed to it: drums, the volume dial, the die, and song titles too long
    for their key, which slide.
    Loading brings every page's widgets up to date before the app opens, and
    the Disconnected screen says the deck is starting meanwhile. An edit sends
    only the keys that changed, as patches: changing a widget's style on a
    page of eight live widgets went from 10.4 s to 0.66 s.
    The app records deck restarts and failed uploads in deck.log, and says
    when the deck's USB adapter has stopped answering and needs a replug.

Firmware

  • Give the page you are looking at the deck's last room for a live picture (28deffb)
    The widget keys that kept going blank were the deck running out of PSRAM:
    ERR live memory, forty times in deck.log, from the moment a microphone key
    was added to a profile of 8 pages and 25 widgets. A key picture is ~29 KB,
    but live_room() asks for the 768 KB floor plus a whole page in one piece
    before it will allocate one, so live pictures stop long before memory is
    full - and the app, told nothing it could act on, kept sending.
    The firmware now takes the live pictures of the oldest page copy that is not
    on screen rather than refusing, and the desktop does the same from its side
    on ERR live memory, leaving hidden pages alone for a minute afterwards.
    Neither needs the other: the deck's own eviction shows up as a base-CRC
    mismatch, which already sends the whole key again.
  • Let the deck move a ring's arc itself, and keep its loop free (53d3f95)
    A ring drawn a second at a time steps; over USB every frame would be a
    key upload. The deck moves the arc now (SWEEP, sweep=1 in ID): the app
    sends where the ring is, where it goes and by when, in 32 bytes, and the
    deck draws each frame between its own blocks. Sliding text works the
    same way, so both are KeyOverlay subclasses on a page, cloned into a new
    version and sent after the picture they belong to.
    Two things blocked the loop while that ran: waiting for the panel's
    vertical blank, and waiting for a key to finish drawing. The vertical
    blank is stamped by its interrupt now (Bus_RGB, panel::last_vsync_us),
    so nothing waits on it, and draw_key gives way while it waits, drawing
    overlay frames meanwhile. DISPLAY_STATE says how many overlay frames
    were drawn and the longest gap between them.
    The keys' own countdowns still step: a whole second at a time, so a
    picture drawn again for another reason is the same picture and never
    travels. Only OBS's five-second ring sweeps, as the user asked.
  • Split the firmware into folders and classes (5adc41d)
    One Deck owns every part; main.cpp only makes it and runs it. Each area
    is a folder: deck/, protocol/, pages/, display/, input/, keys/ (drum/,
    dial/, die/, text/), storage/, network/, security/, common/.
    Commands go through a router to the CommandSet that knows them, each a
    table of commands and the methods that parse them. Animated keys are a
    Look subclass (what the desktop sends, from a version table) and a
    KeyAnimation (how it moves and draws). The Wi-Fi commands and reset
    reasons are tables too.
    tools/golden_run.py drives every command the desktop uses and compares
    the replies and two screen captures with a recorded run: identical
    before and after. The unused AnimatedGIF library is gone.
  • Draw and animate widget keys on the deck (ca85ef7)
    Widget pictures arrive as patches (LIVE) into live pictures the deck keeps
    apart from each page's own. A page's own pictures therefore always match
    its signature and the card's copy, and a new version of a page needs only
    its changed keys, which come as patches too (PATCH): a whole page is 22 KB
    instead of 311.
    The deck turns some keys by itself, at the panel's rate: picker drums for
    countdowns and lists, the volume dial (an arc with its number, a bar
    without), and a die drawn in 3D that is thrown, bounces off the key's edges
    and lands wherever it stops, each face as likely. It slides text too long
    for its key along (SLIDE), takes every page's widgets while it loads
    (warm=1), takes uploads in 2 KB blocks (BLOCK), and says why it last
    restarted (reset=).