{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreib3l6tbpd3bkt2yanfc7223at2jqivgtsnw4lh2bodtoy3555voay",
"uri": "at://did:plc:2qoyip3q6sfzu5joeeaipu6m/app.bsky.feed.post/3mo4zc4mlbrr2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreihxeszp7ufxci6obcgcrn267tdwl3mn455o73u5bfinryzcszmpza"
},
"mimeType": "image/png",
"size": 3567386
},
"description": "Two single-file native apps put a Dell U5226KW’s five inputs, brightness, contrast, volume, and power in the Windows tray and the macOS menu bar over DDC/CI.",
"path": "/ddc-input-select/",
"publishedAt": "2026-06-13T00:38:24.000Z",
"site": "https://blog.elefunc.com",
"tags": [
"Dell Display Manager 2.3.2",
"Dell Display and Peripheral Manager for Windows 2.2.2",
"Dell Display and Peripheral Manager for macOS"
],
"textContent": "For years my desk ran on a 49-inch LG TV that thought it was a monitor. It was 4K, it was cheap, it was enormous, and it had exactly one virtue as a productivity device: it never asked anything of me. Then a Dell UltraSharp U5226KW arrived: 52 inches of gently curved glass, 6144 by 2560 pixels at 120 Hz, two 9-watt speakers, and five video inputs. Suddenly my desk had more computers attached to one screen than some offices I have worked in. A Mac lives on the Thunderbolt port. A Windows tower lives on DisplayPort. Spares come and go on the other three.\n\nFive inputs means switching inputs, and switching inputs means the joystick. If you have never used a modern monitor: the entire on-screen menu system hangs off one rubber nub hidden behind the lower-right corner of the bezel, placed precisely where no human arm naturally bends. Brightness lives three menu levels deep. Volume lives somewhere else. Power is a different button entirely. The U5226KW is a wonderful display operated through a hole in space directly behind it.\n\nI am too lazy for that, and this is a story about how far laziness will scale. The project, ddc-input-select, started as a little bash picker for my Linux box, then escalated the way side projects do: within the week there was a native Windows tray app and a native macOS menu bar app, with Claude Fable 5 writing the code, ChatGPT drawing the icons, and me supplying the laziness, the monitor, and the opinions.\n\n## Dell ships an app for this. It is a lot of app.\n\nTo be fair to Dell, they ship software for this monitor, and it does many things. As listed on Dell's own support site in June 2026: Dell Display Manager 2.3.2 for Windows is a 71.83 MB installer, and its newer sibling, Dell Display and Peripheral Manager for Windows 2.2.2, is a 406 MB one. The macOS edition, Dell Display and Peripheral Manager for macOS, is a comparatively svelte 16.45 MB. These are real products with webcam firmware to manage and conference rooms to please, and I am sure every megabyte has a story. I just did not need any of them for switching inputs.\n\nThe replacement on this desk is one C file that compiles to a 0.9 MB executable, and one Swift file that compiles to a 223 KB binary. No installer, no service, no updater, no account. The entire Windows app, including its icons and an embedded second copy of itself (more on that shortly), is smaller than most apps' splash screens. And because both apps speak to the monitor directly, they can do things like graying out the inputs that have no cable plugged in, a thing I could not find anywhere in Dell's official tooling.\n\n## DDC/CI, the protocol hiding in your video cable\n\nEvery video cable on your desk carries a slow, ancient side channel that speaks I2C, the same two-wire protocol that reads the temperature sensor on a hobby board. Monitors use it to announce their resolution (that part is EDID), and almost all of them also accept commands over it. That part is DDC/CI, and the command vocabulary is VESA's MCCS: read a control, write a control, each control identified by a one-byte VCP code.\n\nThe apps use six standard codes and one mystery guest:\n\nCode| MCCS name| What the apps do with it\n---|---|---\n0x60| Input Select| Read the active input, switch inputs.\n0x10| Luminance| Brightness, 0 to 100.\n0x12| Contrast| Contrast, 0 to 100.\n0x62| Audio: Speaker Volume| Volume, 0 to 100. On this monitor, 128 + level means muted at that level.\n0x8D| Audio Mute / Screen Blank| The mute toggle itself: 1 mutes, 2 unmutes.\n0xD6| Power Mode| 1 is on, 4 is soft off. This monitor answers DDC while the panel sleeps.\n0xE7| (vendor specific)| The mystery guest: a per-input cable-connection bitmask, found nowhere in the manual.\n\nTwo details in the fine print turn out to carry the whole project. First, a VCP reply contains both a _current_ value and a _maximum_ value, and nothing stops a vendor from hiding extra payload in the half nobody reads. Second, this monitor keeps answering DDC even when the panel is off, which is what makes software power buttons possible at all.\n\nDDC/CI request and reply over the video cable A host computer sends a VCP request through the video cable's I2C side channel to the monitor's controller. The reply carries the control's current value and its fixed maximum. Host Mac or PC Monitor controller, awake in sleep video cable, I2C side channel get VCP 0x10 reply: current 0x004B, maximum 0x0064 luminance: 75 of 100 For a real control the maximum field is a constant, 100 for luminance, on every read. A maximum that changes when you unplug a cable is the tell that what you are reading is not really a control.\n\n## The Windows app: one C file, two personalities\n\nThe Windows app is about 2,200 lines of C11 and raw Win32. No framework, no runtime, no dependencies beyond what every Windows install already has: `dxva2.dll` for the physical monitor handles, `user32` for the menus, `shell32` for the tray icon, `advapi32` to read the theme from the registry. It compiles with MinGW from the same Makefile that installs the Linux picker.\n\nIts best trick is stolen from Visual Studio. For decades, `devenv` has shipped as two binaries: `devenv.exe`, the GUI, and `devenv.com`, a console twin, and because `PATHEXT` ranks `.COM` ahead of `.EXE`, typing `devenv` in a shell gets you the one that behaves like a console program. This app does the same with one file: the GUI `tool.exe` (the Windows binary really is just called `tool`) carries a signed console build of itself as an embedded resource and extracts `tool.com` next to itself on first launch. Double-click and you get a silent tray app. Type `tool --brightness 40` in PowerShell and the shell waits, prints, and exits like a proper CLI, because it quietly ran the twin. One distributable, two subsystems, zero flashing console windows.\n\nThe rest of the file is the kind of bookkeeping Windows makes you earn. Per-monitor DPI awareness v2, because without it the tray menu materializes in the wrong place on any scaled display. Tray icons in two flavors, swapped live when the taskbar theme changes, because the taskbar and the apps each have their own light/dark registry key and they disagree more often than you would hope. Dark context menus via `uxtheme.dll` ordinals 135 and 136, which is the polite way of saying an undocumented API that has been stable since 2018, called fail-soft so the worst case is a light menu, and skipped entirely under high contrast where forcing dark would fight accessibility. And a global hotkey, Win+backtick, registered with `MOD_NOREPEAT` and surrendered gracefully if something else owns it. If you run Windows Terminal with quake mode, that something is probably you; the app notices, keeps working from the tray, and notes the loss in its tooltip instead of fighting over the key.\n\n## The macOS app: one Swift file, zero permission prompts\n\nThe macOS side is about 1,500 lines of Swift and AppKit, a menu bar app with no Dock icon. Apple Silicon has no public API for DDC on external displays, so the app bridges to the private `IOAVService` I2C functions for its reads and writes. Private API, used carefully: every reply is checksum-verified before anything is displayed, and a corrupt frame is retried once and otherwise discarded, never shown.\n\nThe fun engineering is in the hotkey. The menu opens on Cmd+backtick from anywhere, registered through the Carbon hotkey API, which dates back to the turn of the century and is still the only way to get a global hotkey on macOS without triggering a single permission dialog. But Carbon hotkeys do not fire while a menu is tracking, so \"press it again to close the menu\" cannot work the obvious way. The app's solution: the moment the menu opens, it unregisters its own hotkey, and a hidden menu item with key equivalent Cmd+backtick catches the re-press inside the menu's own event loop.\n\nThe hidden item's key equivalent is even rebuilt on every open from the physical key position, so the toggle keeps working when I switch to a German layout and the backtick wanders off. When the menu closes, the hotkey re-registers. You cannot see any of this; you just press the same chord to open and close, like it was always supposed to work that way. The honest fine print: Cmd+backtick is also the system's \"Move focus to next window\" shortcut, and a registered global hotkey wins, so this app assumes you cycle windows some other way or are willing to rebind one of them.\n\nTwo more native gaps got filled in-process. AppKit menus do not wrap when you arrow past the last item, and Escape slams the whole menu tree shut instead of stepping out one level. The app swaps the implementations of three private menu-tracking handlers inside its own process, deferring to the originals wherever it does not act, so arrows wrap and Escape walks out of a submenu before it dismisses the menu, the way Windows has always done it. No event tap, no Accessibility approval, no Input Monitoring: the app never sees a single keystroke that was not aimed at its own menu, and macOS never has a reason to show a consent dialog. On a platform where most menu bar utilities open with a tour of System Settings, \"zero prompts\" is a feature with its own line in the spec.\n\n## Same brain, different bodies\n\nSide by side, the two apps are the same product. The menu layout is identical: five inputs at the top, the active one checked, the unplugged ones grayed; Brightness, Contrast, and Volume with their live values in the label; a checkable On item; Exit at the bottom. The same tab-separated config file names the inputs. The same CLI flags work in both shells (macOS skips only `--caps`). The same wake loop, the same prefetch, the same fail-open philosophy: when a read fails, controls say \"unavailable\" and inputs stay clickable, because a monitor that answers strangely is exactly the monitor you most want to send commands to.\n\nThe Windows and macOS menus side by side Two nearly identical menus. Five inputs with the active one checked and unplugged ones grayed, Brightness, Contrast, and Volume with live values, a checkable On item, and Exit or Quit. D1 is pre-highlighted in both. Windows, dark theme✓TD1D2H1H2Brightness: 75▸Contrast: 75▸Volume: 4 (muted)▸✓OnExit macOS, light theme✓TD1D2H1H2Brightness: 75▸Contrast: 75▸Volume: 4 (muted)▸✓OnQuit⌘Q The grayed entries are not a mockup convention: D2, H1, and H2 are grayed because register 0xE7 reported no live cable on them. Both volume rows are the 128 + level mute encoding at work: each host is decoding the same raw 132 from register 0x62.\n\nThe differences are all platform physics. Windows fans the menu snapshot out over five threads; macOS pushes every transaction through one strict serial queue because two concurrent I2C reads on this hardware corrupt each other, a fact discovered the usual way. Windows had to build its own dark mode, its own focus restoration on menu close, and its own keyboard submenu peek out of timers and undocumented messages; AppKit gives all three away for free. In exchange, macOS needed private-selector surgery for arrow wrapping that Win32 menus have done natively since the nineties. Each platform got the other's homework as its own weekend assignment.\n\n| Windows| macOS\n---|---|---\nSource| One C11 file, about 2,200 lines, raw Win32| One Swift file, about 1,500 lines, AppKit\nBinary| 0.9 MB exe, console twin included| 223 KB arm64 binary in a standard bundle\nDDC transport| dxva2 physical-monitor API| Private IOAVService I2C, checksum-verified\nGlobal hotkey| Win+` via RegisterHotKey| Cmd+` via Carbon, no permission prompts\nHotkey re-press closes| EndMenu from the hotkey handler| Hidden menu item catches the chord in-menu\nConcurrency| Five snapshot threads, serialized by the bus| One strict serial queue by design\nDark mode| Hand-built: registry keys plus uxtheme ordinals| Free: template icon, native menu\nArrow-key wrap| Native Win32 behavior| In-process override of private handlers\nFocus restore on close| Reconstructed by hand| Free: status menus never steal focus\nMenu snapshot, before and after| 312 ms to 260 ms| 411 ms to 65 ms\n\n## Register 0xE7, the bitmask the manual never mentions\n\nHere is the feature I am proudest of, and I did not build it; I found it. No standard DDC facility can tell you which input ports have a live cable. Code 0x60 reports the single active input. The capabilities string lists the inputs the monitor supports, not the ones connected. Every OS display API ends at your own cable. Dell Display Manager's command line can read and even switch the active input, but as far as I can tell, nothing in Dell's tooling reports which of the other ports have live cables.\n\nBut the U5226KW knows, and it tells anyone who asks an undocumented question. Vendor code 0xE7 returns a value whose maximum field smuggles a bitmask in its low byte: one bit per input, set when a cable with a live source is plugged in. Read 0x0203 and Thunderbolt plus DisplayPort 1 are connected; unplug the Mac and it drops to 0x0202 in real time. The read is global (every host sees the same answer), passive (nothing flickers), and free. That is the entire mechanism behind the grayed-out menu items, and I will be honest about the confidence levels, because they are part of the story: bit 0 was confirmed by ceremonial unplugging and replugging, bit 1 is strongly indicated, and bits 2 through 4 are inferred from input ordering because I ran out of cables before I ran out of curiosity. On this unit, with this firmware. Vendor registers come with vendor warranties, which is to say none.\n\n## Waking a monitor that plays dead\n\nThe power feature sounds trivial: write 1 to code 0xD6 to wake, 4 to sleep. Then the monitor enters deep sleep and the trivial version dies. The controller keeps acknowledging writes while doing nothing, then briefly stops answering reads, and early versions of \"turn on\" reported success to a black screen. The apps now refuse to trust any write: wake is a loop of write, wait half a second, read 0xD6 back, up to six times, and only a verified read of \"on\" counts.\n\nThen comes the better bug. A freshly woken monitor runs its own input arbitration and frequently lands on whichever port shouts first, which on this desk means pressing the wake hotkey from Windows and watching the monitor greet you with the Mac. The fix hides in plain sight: the 0x60 reply's high byte names the port the asking host is connected to. The monitor tells you which door you knocked on. So after every verified hotkey wake, the app re-asserts its own host's input, and the screen you wake up to is the computer you woke it from. (The menu's On toggle deliberately wakes without touching the input: restoring power is not permission to steal the screen.) The deepest sleep state over Thunderbolt is still an open research question on the Mac side; the monitor may tear the link down so thoroughly that whether software can wake it at all remains to be measured.\n\n## The milliseconds nobody asked me to shave\n\nOpening the menu takes five DDC reads: input, brightness, contrast, volume, connection mask. It used to take six; the dedicated mute read was dropped once it turned out the volume register already encodes mute by adding 128 to the level, so \"Volume: 4 (muted)\" costs nothing extra. On Windows that took the snapshot from 312 ms to 260 ms, and the arithmetic is satisfyingly dumb: the bus serializes everything no matter how many threads you throw at it, each read costs about 52 ms, and six times 52 is 312 while five times 52 is 260. The parallelism was always theater; deleting one read was the only real optimization available.\n\nmacOS went from 411 ms to 65 ms, which is a real number from a real stopwatch and my favorite measurement of the project. The service handle is now cached between transactions instead of rebuilt, and a fixed 50 ms settle delay after each write was cut to 5 ms, protected by the checksum check: across roughly 7,600 verified reads the replies stayed bit-identical even with the settle dialed all the way down to zero, so 5 ms sits right at the measured readiness point, with the checksum check and a single 50 ms retry as the backstop, while deleting 45 ms from every single read. Multiply by five reads and the menu stops feeling like a network request.\n\nThe last trick makes the remaining cost invisible: both apps start reading the moment the mouse touches the tray icon, before any click. If your cursor dwells on the icon for longer than a snapshot takes, the menu opens on data that is already there, so a mouse open is roughly zero milliseconds of perceived DDC latency. Hotkey opens skip the cache and read fresh, on the theory that a keyboard user has already decided and deserves the truth.\n\n## Fit and finish\n\nEverything above is plumbing. What makes the apps pleasant is a pile of small decisions, and since the entire point of this post is attention to detail, here is the pile.\n\nSwitching inputs is two keystrokes. The hotkey opens the menu with the first _connected, non-active_ input already highlighted, so hotkey then Enter flips to the other machine. If the panel is asleep, the highlight lands on this host's own input instead, so the same two keystrokes mean \"wake up and look at me\". The control submenus play the same game: open Brightness while it sits at 75 and the 75 preset is pre-highlighted, so your arrow keys start from where reality is. Mute is the bare letter m on macOS, no modifier, because the menu is already open and knows what you mean. Escape closes one level at a time on both platforms. Arrow keys wrap on both. Pressing the hotkey again closes everything. And when the menu closes, focus returns to whatever app had it before, a thing AppKit does for free and the Windows app reconstructs by hand, capturing the foreground window before the menu steals it and handing focus back after, because nothing breaks flow like a vanished cursor.\n\nThe power item is checkable, and its checkmark renders instantly from the last known state instead of blocking the menu on a read of a possibly-sleeping monitor. A background read then corrects the checkbox in place while the menu is open. You can watch it happen if the monitor was off: the menu appears immediately, and a beat later the checkmark quietly agrees with reality.\n\n### The icons\n\nThe icons came out of a ChatGPT session: one 1024-pixel master per platform, each one a tiny portrait of the ultrawide itself with the host platform's logo glowing on screen, as if that input were active, plus a neutral gray tray glyph template. The masters are not used as-is. A small pipeline isolates the glyph's enclosed screen area by flood-filling the alpha channel from the corners, then refills that region at 40 percent opacity, because a fully transparent screen disappeared into dark taskbars and a fully opaque one looked like a sticker on light ones. From that one template the same pipeline stamps out every size both platforms want: paired light-glyph and dark-glyph Windows .ico files that the app swaps live as the taskbar theme changes, and an 18-point macOS template pair that AppKit tints by itself.\n\nThe tray glyph (right) is the one users actually meet: each finished Windows .ico packs eight sizes, from 256 pixels down to a 16-pixel cell.\n\n## Shipping the laziness\n\nBoth apps currently install the artisanal way: you build them and copy a file. That is about to change. The Windows app is headed to the Microsoft Store soon, and the macOS app should reach the Apple App Store later this year, once Elefunc, Inc. has its developer account, and once a few of the tricks described above are reworked into shapes store review may smile upon; private I2C bridges and hidden hotkey items are exactly the kind of cleverness that needs a second, more diplomatic draft. The icons are on that list too: platform logos do not belong inside third-party store icons, so the store builds get logo-free art. The timing, of course, is each store's call, not mine.\n\nTotal bill for never touching the on-screen display again: two single files, six VCP codes, one undocumented bitmask, and five days.\n\nThe joystick is still back there. It can stay.",
"title": "ddc-input-select: switching monitor inputs without reaching behind the monitor",
"updatedAt": "2026-06-13T00:38:24.942Z"
}