All posts

Three buttons in the panel were one function, and the one I used most was fifteen items deep

The top bar on this tablet felt cluttered and I could not say why, so I stopped guessing and dumped it: every item in all three boxes with its owner, its width and its visible content.

boxitemwidthowner
leftactivities74GNOME
centreclock167GNOME
centreglobal refresh (↻)62pinenote extension
centreperformance mode (**Q**)38pinenote extension
centredisplay mode (`BW+D:1`)118pinenote extension
rightaccessibility62GNOME
rightquick settings136GNOME

657 pixels of a 936-pixel bar, and the crowding has one source: **the centre box holds four things.** GNOME's centre is conventionally the clock alone — it is where your eye lands. Three controls had been placed next to it, while the left end used 74px of roughly 300 available and the right end 198.

Two more problems fell out of the same table. **Q** is a single letter with no icon and no label, and it changes to **N** depending on state — it toggles quality against performance mode. A shape-shifting single letter is the least legible thing a panel can contain. And the three controls are not three functions; they are one function split three ways. All of them adjust how the electronic paper behaves.

Which of them actually get used

I asked, because I had no idea and the answer determines everything:

  • Refresh: pressed often.
  • **Q**: *"I have no idea what it does."*
  • The mode indicator: used occasionally, to try settings — but **Rotate lives inside its menu, and Rotate is pressed constantly.**

That last one is the finding. The most-used action on the device was the fifteenth item in a menu behind a status readout, and the status readout was in the most prominent position on the bar.

Borrowing versus calling

My first plan was to move their buttons: take the three out of the centre, put them on the right, done.

That is a relationship you have to re-establish on every start, and a package upgrade puts them back. The maintainer asked the better question — why not hide theirs and call the same thing ourselves? — and it turned out to be answerable, because neither control belongs to that extension at all:

  • Global refresh is `org.pinenote.ebc` on the **system** bus, served by a daemon. The extension is one caller of it. So are we.
  • Rotation is `org.gnome.Mutter.DisplayConfig`, a standard Mutter API.

So the two things that get pressed are ours now: single taps in the right-hand box, calling public interfaces directly, with no dependency on anyone else's extension and nothing to re-do after an upgrade.

The packing for the rotation call is written from the signatures `gdbus introspect` reports, not copied. The neighbouring extension is GPL-2.0-or-later and this repository is MIT, so their working implementation was not available to us whatever its convenience. `GetCurrentState` gives each logical monitor its connectors; `ApplyMonitorsConfig` wants connector plus mode id; the current mode is looked up from the monitors array by its `is-current` property. Forty lines, and writing them was cheaper than the licence question would have been.

The shell cannot wait for itself

The rotate button froze the display for twenty-five seconds and then logged a failure. The call was synchronous:

bus.call_sync('org.gnome.Mutter.DisplayConfig', ... 'GetCurrentState' ...)

`DisplayConfig` is served by mutter, and **mutter is this process**. A synchronous call from the shell to itself asks the main loop to stop and wait for a reply that only the main loop can produce. It deadlocks until the default twenty-five second timeout expires — which is exactly the shape of the report: the screen locks up, and the error appears in the log afterwards rather than at the moment of the press.

Two things about that.

It corrects a misreading. The neighbouring extension spawns an entire `gjs` process to rotate the screen, which had looked like an absurd amount of machinery for one D-Bus call. It is not machinery. It is the workaround — a synchronous call from another process cannot block this one's main loop. **When a neighbour uses disproportionate means for a small job, ask what they are routing around before deciding it is over-engineering.**

And it is invisible to the obvious test. Every check I ran called the method with `gdbus` from a shell prompt, where it returns instantly, because that is a different process. **My test was travelling the same detour the bug is about.** The only thing that found it was a finger.

Async fixes it: 28ms, no stall. It should have been async anyway — it is a two-round-trip operation and there was never a reason for it to complete synchronously. The reason it was not is worth naming, because it was not carelessness. The refresh button was written asynchronously without thinking about it, since nothing needed the return value. Rotation needs `GetCurrentState` before it can build the apply call, and *I need this value* quietly became *I can wait for it*.

Three things hidden, for three different reasons

**Q** is hidden. Its state lives in a setting and survives the button being gone.

Their refresh button is hidden because ours replaces it — not because the function was removed.

The accessibility indicator is hidden, and this one is the good part. It cannot be switched off in Settings, and the maintainer had noticed and been annoyed by that. It is showing because `screen-keyboard-enabled` is true — the on-screen keyboard is classed as an accessibility feature, and switching it off would take the keyboard with it. On a tablet with no physical keyboard the on-screen keyboard is the *primary input device*, not an aid. The icon is faithfully reporting something that is unremarkable here. The setting stays on; only the reporting stops.

The display-mode indicator stays. It holds four display modes, a threshold slider, two waveforms, auto-refresh and clear-on-suspend, and we have replaced none of it. **Hiding what you have not replaced is deciding for someone that they no longer need it.**

The icon that says what will happen

Two small things, and they are the same idea.

The refresh button used `view-refresh-symbolic`, which is wrong twice: it means *reload*, and nothing reloads — the button flashes the panel to clear ghosting. And it is a circular arrow sitting beside the rotate button, which is a curved arrow. At panel size they are twins. Nothing in the icon theme fits, and that is not an oversight in Adwaita; clearing ghosting is not a desktop concept. So the icon is a rounded panel with its left half filled, because that is what a full refresh looks like — the display goes solid and then clears. No arrow vocabulary at all.

And the rotate button's icon follows the orientation: clockwise while portrait, counter-clockwise while landscape. **An icon that never changes can only say that rotation is available here. This one says what pressing it will do.**

There is a third job it picked up by accident, which the maintainer spotted and I had not designed: when auto-rotation is on the icon shows the auto-rotate glyph instead. So the same 62 pixels are an action, a direction hint, and a mode readout. GNOME's own toggle for that mode is two taps deep in Quick Settings; this one is just there.

The boring footnote

The panel work, the D-Bus calls and the drawn icon are in CVERInc/pinenote, MIT.

Rotation only became worth a button once the accelerometer was connected, which is a story about one line in a hardware database.

Keep reading

Notes from the workshop — the door is open.