Din is a social computer. You sign in with your Nostr account, and Din bridges your identity into every application on the desktop — connecting you to your friends across the network. Instead of relying on cloud services, Din stores your data encrypted on free public infrastructure (Nostr relays and Blossom servers). You can also self-host your own relays and servers, or operate entirely locally.
Din features advanced AI integrations: control your whole computer by talking to an agent, with support for local LLMs. You can also easily add sandboxed OpenClaw agents that run 24/7 — as long as your computer is on.
Din is a web-native desktop environment built in Electron, targeting Linux systems. The desktop itself — windows, taskbar, apps, login — is a Vite + React 19 + TailwindCSS 4 + shadcn/ui app (based on the MKStack template) hosted by a thin Electron shell, so it can run:
- in kiosk mode on top of any OS that Electron supports,
- windowed on a normal desktop for development,
- or as a plain web app in any browser (with reduced integration).
DinOS is the flagship OS of Din: a minimal NixOS system that boots straight into Din fullscreen. No X11, no display manager, no traditional desktop — just:
Linux kernel -> systemd -> din-shell (Din's own Wayland compositor) -> Electron -> Din
din-shell is din-compositor session (native/din-compositor/src/session), a
Smithay compositor that drives the displays through DRM/KMS and input through
libinput, shows Din's one window across every display, and speaks the wlroots
protocols the rest of the system uses: output management (wlr-randr, Din's
display settings), screencopy (grim, AirPlay) and gamma control (night
light).
Each display has a zoom of its own, its scale in din-shell: automatic from
its pixel density (src/lib/autoZoom.ts) until the user picks one in
Settings → Display, which is remembered for that display
(src/lib/displayZoom.ts, din display set <monitor> --zoom). Din's window
is drawn at the densest display's scale and scaled down on the others, so a
hidpi panel beside a low-dpi monitor shows the desktop at the same size on
both. Elsewhere (a windowed shell, bare X11) the zoom is one Chromium page
zoom for the whole desktop.
- Node.js (dev servers and builds)
- Xvfb, for running native Linux apps inside Din; optionally Rust (cargo), to
build the Wayland compositor that hosts the apps able to use it (and draws
them on the GPU), plus Xwayland, to put X11 apps there too. The compositor
links against pixman, gbm, and xkbcommon, so it needs their development
packages:
- Fedora:
sudo dnf install xorg-x11-server-Xvfb xorg-x11-server-Xwayland pixman-devel mesa-libgbm-devel libxkbcommon-devel - Debian/Ubuntu:
sudo apt install xvfb xwayland libpixman-1-dev libgbm-dev libxkbcommon-dev - Void:
sudo xbps-install xorg-server-xvfb xorg-server-xwayland pixman-devel libgbm-devel libxkbcommon-devel(a plainxorginstall doesn't include Xvfb)
- Fedora:
- Wine, for Windows programs
- Nix (with flakes enabled; the Determinate installer does this by default) —
only needed for the DinOS
os:*commands - QEMU (
qemu-system-x86_64) on the host for thenpm run os:rundemo
npm run dev # run Din in Electron with live HMR (windowed + DevTools)
npm run dev:browser # run Din in a plain browser with HMR (http://localhost:8080)
npm test # type-check + lint + tests + build
npm run build # build the web app into dist/
npm run build:native # build the Wayland compositor + GPU buffer addon (cargo)
npm run os:build # build the installable DinOS disk image (./result/nixos.img)
npm run os:run # demo DinOS in a QEMU window
npm run os:clean # remove the build output and the demo VM disk (dinos.qcow2)npm run dev starts Vite, then launches the same electron/main.mts DinOS
uses (Electron comes from devDependencies, pinned to the version the OS image
ships) against the dev server via DIN_DEV_URL. This gives you the real
Electron/app:// environment with Vite HMR + React fast refresh. Ctrl-C stops
both processes.
From a checkout, Din can go on your login screen's session menu (GDM, LightDM, SDDM) next to the desktop you already have:
npm run session:install # build this checkout and add "Din" to the login screen
npm run session:install -- --default # ...and log in to it by default
npm run session:update # rebuild from the checkout for the next login
npm run session -- rollback # back to the build before
npm run session -- restore # log in to your old desktop by default again
npm run session:uninstall # take Din off the login screenSettings → About does the same from npm run dev (Use Din as your
desktop), and from the session itself, where it also updates, rolls back,
and returns to the old desktop. The session runs Din the way DinOS does, Din's
compositor on the screen running Electron, but from a copy of a build in
~/.local/share/din/session, never the checkout, so a broken edit can't keep
you from logging in. sudo asks for the password once, to install
/usr/share/wayland-sessions/din.desktop, the /usr/local/bin/din-session
it runs, on Ubuntu 24.04 and later an AppArmor profile that lets
Electron's sandbox start, and with LightDM a setting that keeps sessions on VT
7 and up, away from the console login prompts that would otherwise read what's
typed in Din (Fedora's LightDM starts at VT 1). If Din fails three times running just after
logging in, the login screen goes back to your old desktop. In the session,
the account menu has Log out, and what asks polkit for the password
(pkexec, timedatectl, firmware updates) asks in Din, through
os/din-polkit-agent.py on the system's Python and PyGObject. Settings
works the system the way DinOS's does: Date & Time through timedated, the
keyboard layout as the session's own (~/.config/din/keyboard, which the
compositor starts with), the firewall through firewalld where it runs,
firmware through the system's fwupd, and Optional Features from the
system's packages (below). Links and folders native apps open come to Din,
as on DinOS: installing (and updating) adds din-open.desktop to
~/.local/share/applications and makes it the default for web, nostr: and
nsite: links and folders in ~/.config/din-mimeapps.list, which only Din's
session reads, so your other desktop keeps its browser. Uninstalling shuts
the ports Settings → Firewall let into firewalld. DinOS-only parts (updates, the Installer, the
lock screen, nostrd) stay off. scripts/session.mjs has the details.
Building it needs Rust 1.85 or newer (rustup; distribution packages are often older) and the compositor's libraries:
- Debian/Ubuntu:
sudo apt install pkg-config libseat-dev libinput-dev libudev-dev libgbm-dev libdrm-dev libxkbcommon-dev libpixman-1-dev - Fedora:
sudo dnf install pkgconf-pkg-config libseat-devel libinput-devel systemd-devel mesa-libgbm-devel libdrm-devel libxkbcommon-devel pixman-devel - Arch:
sudo pacman -S --needed pkgconf seatd libinput systemd-libs mesa libdrm libxkbcommon pixman
The script says which are missing. NixOS can't take it (/usr is read-only);
it has a module instead.
Din can stand in for Explorer as the desktop a Windows user logs in to:
npm run build && npm run windows:package # dist-windows/din, with Electron's Windows buildCopy dist-windows/din to the Windows machine, and in the user's PowerShell
run .\din\install-shell.ps1 (-Restore goes back to Explorer). It sets the
per-user shell (HKCU\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\Shell)
to din.exe --din-shell, from the next sign-in (tried on Windows 11
Enterprise). Ctrl+Alt+Del → Task Manager → Run new task →
explorer.exe is the way out for a session where Din won't start.
As the shell (electron/windows-shell.mts), Din's window is the desktop:
electron/windows-shell-helper.ps1 keeps it under every program's window,
as Explorer's desktop is, and sets the work area so maximized programs leave
the menu bar showing. Programs (.exe, .lnk, .msi, .bat) open with
Windows itself, not Wine; Log out signs out of Windows, and Restart and Shut
down go through shutdown.exe. Din's desktop and apps see Windows' paths
in POSIX form (C:\Users\Din is /C:/Users/Din, and / is the computer,
whose folders are its drives), translated where they cross into main
(electron/windows-paths.mts); the files Windows hides (desktop.ini,
Thumbs.db, …) are hidden, and UTF-16 text is read and saved as it came.
Not there yet: programs' windows in the Dock
(Alt+Tab switches between them), Din's menus and windows over programs', the
Run keys and Startup folder, and the Linux-only parts (Terminal, displays,
sound, Wi-Fi, …).
On NixOS, Din goes on the login screen from the system's configuration, with
os/flake.nix's nixosModules.din (os/desktop-module.nix):
# flake.nix
inputs.din.url = "git+https://git.shakespeare.diy/npub10qdp2fc9ta6vraczxrcs8prqnv69fru2k6s2dj48gqjcylulmtjsg9arpj/din.git?dir=os";
# the system's modules
din.nixosModules.din
{ services.desktopManager.din.enable = true; }It's the session above, built by Nix from the same derivations as DinOS
(os/packages.nix) with the system's own nixpkgs, and with the din
command. It turns on what Din's Settings works through, NetworkManager,
Bluetooth, UDisks, UPower, PipeWire and the login keyring, each only by
default (enableSystemServices = false leaves them to you), and makes Din's
file chooser the portal's in Din's session alone. extraElectronFlags passes
Electron more arguments. Updates are the system's: Din updates when the
input does. DinOS's own parts (its updater, the Installer, the lock screen,
nostrd, optional features) stay off.
nix run ./os#desktop-vm tries it in a throwaway NixOS: log in as tester,
password din. nix build ./os#checks.x86_64-linux.desktop-module checks
that the module adds nothing when it's off and none of DinOS when it's on.
npm run os:build produces the real artifact: a raw UEFI disk image meant to
be installed on a physical machine. Write it to a drive and boot from it:
sudo dd if=result/nixos.img of=/dev/sdX bs=4M status=progress conv=fsyncThe image is 6 GiB, to fit any 8 GB stick; any tool that writes images
(Etcher, Rufus, dd) will do. Its root is Btrfs laid out as an installed
disk is (below), with everything compressed at zstd's strongest level, since
a USB stick is slow to read, and about 1 GiB free for a live session; nothing
grows it at boot (os/make-stick-image.nix).
Written to a USB stick instead, the same image is a live system: boot a laptop
from it and Din opens the Install DinOS app, which copies the running
system onto the computer's own disk — erasing it, or into its unpartitioned
space next to Windows (shrink Windows' partition in Disk Management first; the
firmware's boot menu then picks between them). It copies with btrfs send,
the data still compressed, so each byte is read off the stick once, as stored. The sign-in, saved Wi-Fi, and
files from the live session come along. The app only appears when DinOS is
running from removable media; the disk work is os/din-install.sh, run as
root through a sudo rule for that one program. Because the stick and the
installed disk are copies of each other, nothing is mounted by label: the
initrd finds root by GPT auto-discovery on the disk the boot loader ran from.
Every install has the same layout, with no choices offered: an ESP and one
system partition (optionally LUKS-encrypted) holding a Btrfs filesystem with
an @ subvolume for the system (the default, which the initrd mounts as
root) and @home for home, sharing the disk's space and compressed with
zstd. Disks under 56 GiB are refused. Run on a disk that already has DinOS,
the Installer offers to reinstall and keep your files: it builds the new
system in @new, never mounts @home, carries over saved Wi-Fi, Bluetooth
pairings, the Nostr account keys, and the machine's identity, and only swaps
the new system in once it's complete.
Set up for someone else (din-install install <disk> oem, din installer install) erases the disk and leaves it as if DinOS had been installed there
and then erased (below), the way a computer comes from the shop: nothing of
the live session's (home is empty, and the rest goes by the same list
din-erase uses, os/din-pristine.sh), no password, and the disk encrypted
under a new random key on the boot partition, which opens it without asking
until the first start's Setup replaces it with its owner's password. Unlike
erasing in place, nothing anyone had is left on the disk.
Systems installed before Btrfs have an LVM volume group, dinos, in its
place, with ext4 root and home volumes; they still boot, and reinstalling
one keeps that layout (formatting root, never mounting home). Moving one
to Btrfs means copying home off and erasing the disk. The group is created
with LVM autoactivation off; only the initrd activates it, and only on the
disk it booted from, so a DinOS disk attached to another DinOS machine (or
under a live stick) is left alone. nix build ./os#checks.x86_64-linux.install
installs from the image over USB in a VM and boots each result: a new
install, a reinstall, and a disk installed before Btrfs, before and after
reinstalling it.
One password runs the computer. Encrypting the disk sets its passphrase as
din's password too, and as the password of nostrd, the signer daemon that holds the Nostr account
keys as its own user so nothing din runs can read them. Din signs through
nostrd's socket, and the passphrase typed at boot unlocks it. It unlocks the
login keyring too (which the first boot creates with it, and Passwords'
vault), and any GPG key whose passphrase is the same, so signing
commits doesn't ask (os/din-gpg.py). Settings → Password changes it
everywhere at once (os/din-password.py, disk first).
sudo (Din's own helpers excepted) asks you to approve the command in Din,
with a click and no password: the agent's commands in the Assistant's
conversation, others (typed in Din's Terminal, say) from a notification.
Root asks from
sudo's PAM stack (os/din-password.py's sudo check) and only takes an
answer from Din's main process, the one child of din-shell's compositor,
whose Electron has its Node inspector fused off so nothing else can run code
in it. With no Din to ask (the text console, SSH), or no answer, sudo asks
for the password instead, in Din when there's no terminal (os/din-askpass.py,
electron/askpass.mts). Settings → Password can have it always ask for the
password, or never ask. Without encryption there's no password, and sudo
doesn't ask.
os/tests/accounts.nix
checks all of this in a VM: nix build ./os#checks.x86_64-linux.accounts.
Settings → About's Erase all data (din update erase) starts the
computer over: no files, accounts, apps, saved networks or settings, and no
password (os/din-erase.sh). It takes the password, and only erases a
computer whose next boot is official DinOS, since a build of the user's own
comes from ~/DinOS, which goes; the dialog builds official DinOS first if
it isn't. Every other system leaves the boot menu, the disk gets a random
key on the boot partition, which the initrd unlocks it with from then on
without asking, and Din isolates din-erase.target: everything but the
system's own base stops, Din and the user's session included, and with only
the boot screen's "Erasing…" showing, din-erase run swaps an empty
@home in for the old one and deletes it, empties /var/lib and the rest
of the user's traces, and takes the old password's keyslot off the disk,
leaving only that key. Then the computer restarts. An erase cut short is
finished at the next boot, before anything has started (the initrd's
dinos-erase, then din-erase-finish). Din shows
Setup (src/components/desktop/Setup.tsx) in place of the desktop until
there's a password again: the keyboard layout, then the password, which
din-password setup sets everywhere, replacing the key. Deleted files aren't
overwritten, and an encrypted disk keeps its volume key, so whoever has the
key can read them; the dialog points to the Installer's Set up for someone
else for giving the computer away. nix build ./os#checks.x86_64-linux.erase.
The boot is quiet: no boot menu (hold Space to get it), no log lines, just
Plymouth drawing Din's boot screen (os/boot-screen/din.script). On an
encrypted disk that's where the password is typed, over the user's wallpaper,
with a clock, in the keyboard layout Settings set. None of that can come from
the encrypted disk, so the wallpaper, layout, time zone and clock format are
kept on the boot partition under din/ (os/din-boot-screen.sh), and the
initrd copies them out before Plymouth starts. Once root is mounted the screen
fades to black, and Din fades in from there.
nix build ./os#checks.x86_64-linux.boot-screen draws it in a VM and leaves
screenshots in result/.
Closing the lid, or Lock in the account menu, locks the computer: the boot
screen again, run as root on its own terminal by os/din-lock.py, with
terminal switching turned off until the password is typed, so nothing running
as din can get past it. On an encrypted disk the computer then sleeps, and
after two hours (or on low battery) hibernates to a swap file on the encrypted
root, so the disk is locked too; waking from that goes through the boot
screen's password, and the lock screen steps aside. Without a password nothing
locks, and the lid just suspends. nix build ./os#checks.x86_64-linux.lock.
Installed systems update themselves, by building Din from source. Each keeps
a checkout of Din in /var/lib/dinos/src, which only root can write to, and
follows one branch of one repository: by default main of Din's own,
nostr://npub10qdp2…/git.shakespeare.diy/din, which git fetches through
git-remote-nostr (from ngit, which DinOS ships), taking the branch's tip
from the maintainers' signed repository state. din-update.service runs
os/din-update.sh five minutes after boot and every six hours after: it
fetches the branch, and if its tip is a commit the system isn't already
running or staging, builds that commit's os#dinos and stages it with
switch-to-configuration boot, the same as nixos-rebuild boot. Each
system records the commit it was built from (/etc/din-revision), and a
commit that fails to build isn't tried again until someone asks. Nix's
builds run at idle priority, and the timer's runs wait for AC power.
Before it builds, the updater asks Nix (--dry-run) how much the update
downloads, and makes sure root has that plus 4 GB free for the store and
/home 10 GB for build scratch (nix.settings.build-dir). If not, it drops
systems from the profile and collects garbage: first ones that were staged
and never started (a newer build took their place; an activation script
notes every system that boots or is switched to in /var/lib/dinos/ran),
keeping the newest, the next boot's, then those beyond the boot menu's five;
if that's still not enough it stops and says why. Every update prunes the
profile the same way after staging, and min-free/max-free have Nix make
room on its own if a build runs short anyway. Settings → About shows the
build's progress, offers the restart, turns automatic updates off (a
developer building by hand wants that; Check now still works), and
changes the repository and branch, so anyone can run their own fork:
din-update source <url> <branch>, through sudo, which asks first, since
whoever picks the source picks what runs as root. Older systems stay in the
boot menu. Live sticks aren't updated.
A staged update needn't wait for the next boot. Settings → About (and the
notification saying it's ready) offers Apply now, din-update apply:
the same switch as nixos-rebuild switch, which restarts the services that
changed but never din-shell.service, Din itself. If the update brings a
new Din, the old one runs on until Restart Din (din-update restart-din), which closes every app; if it brings a new kernel, initrd,
kernel parameters, modules or firmware, it isn't applied, and the computer
restarts instead. din-update status reports what the running system
still needs (restart) and what the staged one would (stagedNeeds), by
comparing those with the booted system's and the command din-shell is
running with the system's own; Din offers the restart either way, including
after a switch by hand. Din's launcher names the updater through
/run/current-system, since a Din that hasn't restarted may only run the
new system's.
A computer's own changes live in ~/DinOS, the user's copy of Din, built
with din update local (the assistant's dinos skill,
os/opencode/skills/dinos): Din resolves main of ~/DinOS to its commit
and runs din-update local <commit> through sudo, which asks the user
("Allow the assistant to update Din?"). The service builds that commit like
any update, then the command switches to it and Din restarts if it changed.
The newest local wins: asked for while another commit (or any update)
builds, it stops that build and starts its own, and the one set aside says
so; an install or switch under way finishes first.
Updates keep them: when the
commit the newest system was built from isn't in the followed branch, the
updater merges the branch's tip into that exact commit, which the user
approved when they built it, and builds the merge. It reads the commit from
~/DinOS as the user if its checkout lacks it, and never builds whatever
else is there. A merge that conflicts or doesn't build, or a system built
from uncommitted changes, fails with a reason; Din then offers Fix with
assistant, which merges in ~/DinOS, where sudo asks again. So does a
system built from a commit the updater can't find at all (not on the
branch, nor in ~/DinOS): building the branch over it could go back to an
older Din, so it's left alone until the user picks Use
instead (din-update replace). A merge that comes out the same as the
branch's tip, once the user's changes are upstream, builds the tip itself,
so the system stops being custom. The checkout's upstream branch is the
tip last fetched and system the last commit it staged, for ~/DinOS to
pull. A developer can point ~/DinOS at their own checkout with a
symlink. Dismissing that notification sets the failure aside, across
restarts, until an update stops on another commit of the user's: the branch
moving on doesn't bring it back. Settings → About still shows it.
A system behind its branch (updates off, on battery, or the user's changes conflicting) may lack a fix for a security issue, and a commit that fixes one says so with a trailer in its message:
Check the path the fs bridge is given against the folders it grants
Security: high
The severity is low, medium, high or critical. A commit that only
later turns out to have fixed one is flagged by a later commit (empty, if
there's nothing else to it) naming it: Security-Fix: <commit>, with its
own Security:. The trailers ride on the signed branch like the code
they're on, so there's nothing else to publish. Whenever a run ends, the
updater lists the branch's flagged commits the newest system has no change
like (a fix cherry-picked into the user's commits counts), and with updates
off or on battery the timer's runs still fetch the branch to look for
them. din-update status reports them (security), Settings → About lists
them, and Din puts up a notification for each new one, even once the
failure keeping it out was dismissed, offering Update now or, when the
user's changes are in the way, Fix with assistant. Dismissing it sets
those fixes aside for good. A fork set as the source passes on Din's fixes
only once its maintainer merges them.
New installs start from a disk image. To cut one, bump os/version, commit,
tag v<version>, and run npm run os:release -- -- --nbunksec-file <file>
(or pass any other ngit release publish signer option after the second
--). It builds the image from a clean worktree of the tag and publishes it
as a NIP-82 release of the application din (.ngit/release.yaml), signed
by the soapbox.pub key, with the file on Blossom.
npm run os:release -- --stage-only builds it into .release-artifacts/
without publishing anything. Installed systems don't use releases.
For debugging on real hardware, nix build ./os#debug-image builds the same
image plus an SSH server (key-only) and mDNS (dinos.local). No key is baked
in: after writing the drive, mount its root partition and put a public key in
/root/.ssh/authorized_keys, and a NetworkManager profile in
/etc/NetworkManager/system-connections/ (mode 600) if the machine needs
Wi-Fi. A system installed from a debug stick keeps that access.
npm run os:run is
for demoing: it boots the same system in a QEMU window (software rendering,
since QEMU provides no GPU), with boot logs mirrored to the serial console
(your terminal).
src/— the Din desktop environment (React, Tailwind, shadcn/ui)src/apps/— the built-in apps, each a page of its own atdin://<id>/in its own process, reaching the desktop only throughwindow.din(src/sdk/); code they share is insrc/shared/. Seedocs/README.md.electron/main.mts— the Electron host; servesdist/over a customapp://protocol (real origin, SPA fallback for BrowserRouter)electron/din-apps.mts— the built-in apps'din://protocol, session and portselectron/preload.cjs,electron/pty-host.py—window.dinos, the desktop's bridges to the system (fs, pty, wifi, …)electron/native-*.mts,electron/wayland-bridge.mts,native/— the embedded display that runs native Linux apps inside Din windows (see "Native Linux apps" below)os/— DinOS, the flagship OS:os/flake.nix— entry point; exposes the installable disk image as./os#image(the default) and the QEMU demo as./os#vm, and pulls in the Din source tree (the repo root) as an inputos/configuration.nix— the whole OS definition: the din-shell service (Din's compositor), the Electron launcher and the restos/packages.nix— the Nix builds of Din (buildNpmPackage), its compositor, din-fd and the Electron it runs on, shared by DinOS andos/desktop-module.nix, Din as a desktop on any NixOS (see "Din on NixOS")os/din-install.sh— the Installer app's privileged helper (seeelectron/installer.mts)
Three tiers, fastest first:
- Browser + HMR (
npm run dev:browser): Din is an ordinary Vite app; editsrc/and see changes instantly at http://localhost:8080. - Electron windowed + HMR (
npm run dev): runs Din inside the actual Electron host (app://protocol, CSP, download handling) pointed at the Vite dev server, so it matches DinOS while staying hot-reloadable. Under the hood this just setsDIN_DEV_URL=http://localhost:8080forelectron/main.mts. - Full OS demo (
npm run os:run): rebuilds DinOS and boots it in QEMU; only changed layers rebuild. The demo VM disk (dinos.qcow2) persists between boots;npm run os:cleanresets it.
Installed desktop apps launch onto Din's embedded display, a pair of private servers whose windows Din composites into its own:
- Wayland —
native/din-compositor, a headless Smithay compositor (Rust). Apps that speak Wayland (GTK, Qt, Firefox, Electron, SDL) use it. With a GPU, it composites each window on the GPU into shared dmabufs that Din's main process imports once each (sharedTexture; the descriptors arrive vianative/din-fd, a small N-API addon) and the shell draws without any CPU copy - apps get linux-dmabuf and render on the GPU too. A window that is one surface in one GPU buffer (most games, video players and X11 apps) isn't composited at all: Din imports the app's own buffer (in any layout when the compositor is on the GPU Chromium uses, only linear ones otherwise, as with two GPUs). Without one, pixels stream to the shell over a token-guarded localhost WebSocket. Either way, apps are paced by the shell's display refresh. Apps draw at the shell's pixel ratio (its zoom, times the screen's scale): the outputs carry it, as a fractional scale (wp_fractional_scale, with wp_viewporter) or a whole buffer scale, and windows reach Din at it, so they're as sharp as Din's own at 200%. A buffer the app drew at that scale is still passed on as it is. - X11 — the compositor's rootless Xwayland, which it manages as the X
window manager, so X11 apps take the same (GPU) path as Wayland ones and get
hardware OpenGL. X apps draw at 1, which Din scales up: Xwayland can't tell
its clients a scale, and making it run at the screen's pixels would leave
every app that doesn't scale itself tiny. Where Xwayland isn't installed, or without the compositor,
X11 apps fall back to a private Xvfb, managed and composited by
electron/native-display-worker.mts(software rendering).
The compositor also speaks relative-pointer and pointer-constraints, for mouse-look in games (Wayland or X11 via Xwayland): an app's pointer lock becomes DOM Pointer Lock on its window, Escape releases it, and the next click in the window lets the app take it again.
Apps type with the system's keyboard layout (electron/keyboard.mts reads it
from XKB_DEFAULT_*, GNOME or KDE settings, the host X server, or the system
keyboard config). Din sends them physical keys and both servers apply the
layout; the Xvfb fallback needs setxkbmap for it. Settings → Keyboard shows
the layouts (up to four, XKB's groups) and changes them on DinOS and GNOME.
DinOS keeps them in /var/lib/din/keyboard (os/din-keyboard.sh writes it as
root), which din-shell starts with; Din tells it a changed list and the layout
typing in $XDG_RUNTIME_DIR/din-shell-keyboard
(native/din-compositor/src/session/layouts.rs), so a change takes effect at
once. With more than one, the menu bar shows the layout typing and switches
it, and so do Ctrl+Space (back to the last one) and Ctrl+Alt+Space (the next),
as Control-Space and Control-Option-Space on a Mac; the embedded display's
servers get the keymap with the layout typing first.
Settings → Date & Time shows the system's time zone and clock, read from
systemd-timedated (electron/datetime.mts), and on DinOS changes them
through os/din-time.sh, which runs timedatectl as root. Chromium follows
/etc/localtime, so the new zone takes effect without a restart. The clock's
12/24-hour format, and whether the menu bar shows the date and seconds, are
Din's own settings; the boot and lock screens follow the format too.
Build both crates with npm run build:native. Without the compositor, every
app runs on Xvfb; without din-fd, the compositor stays on the CPU path.
DIN_NATIVE_DISPLAY=x11 forces X, DIN_COMPOSITOR_GPU=0 the CPU path and
DIN_COMPOSITOR_PASSTHROUGH=0 compositing every window, for comparisons, and DIN_COMPOSITOR_STATS=1 logs the compositor's frame
rate.
Opening an .exe, .msi, .bat or .lnk (in Files, or a finished browser
download) runs it through Wine on the same embedded display, so it gets a Din
window like any native app. Din keeps one hidden Wine prefix in
~/.local/share/din/windows, created on first use without any Wine dialogs:
C: plus the home folder as H: (no Z: drive), the user folders linked to the
home ones, and crashes closing quietly instead of opening winedbg. Installers
need no help: the Start-menu shortcuts they create become desktop entries,
which appear in the launcher as soon as the install finishes (Uninstall
shortcuts are left out). See electron/windows-apps.mts.
Every Wine process runs in a bubblewrap sandbox (electron/wine-sandbox.mts).
It gets the system read-only, plus the GPU, sound, controllers and the
network. It also gets the prefix and the folders Windows has too: Desktop,
Documents, Downloads, Music, Pictures and Videos. Nothing else of home is
visible: no dotfiles, Din data, session bus, din socket or key agents. A
program or document opened from somewhere else (a USB stick) brings its
folder along. Wine's Start-menu entries, icons and file types go to
~/.local/share/din/windows-shell instead of ~/.local/share. Din lists
them without trusting them: an entry from there only ever runs Wine, in the
prefix and the sandbox. All sandboxes share one /tmp (under
$XDG_RUNTIME_DIR/din-windows), where the prefix's wineserver keeps its
socket. Where bubblewrap is missing or can't make namespaces, programs run
unsandboxed.
An installed Windows program has Uninstall in its right-click menus (and
din apps remove), as in Windows' Apps & Features. Din finds the program
in the prefix registry's Uninstall keys: first by place (the shortcut's
folder inside its InstallLocation or its icon's folder), then by name
("Foo 2.1 (64-bit)" for "Foo", never "Foobar"). It then runs wine uninstaller --remove <key>, which opens
the program's own uninstaller. A program without one only loses its
shortcut. Wine never takes away an entry whose shortcut an uninstaller
deleted, so Din hides those. Wine's .desktop copies of desktop shortcuts
go to a folder out of sight, since the .lnk on the Desktop is the same
icon.
Windows programs' notification-area icons appear in Din's tray. Wine only
speaks the old XEmbed tray (_NET_SYSTEM_TRAY), so the compositor hosts
one on its Xwayland (native/din-compositor/src/tray.rs). It offers a
32-bit visual, so icons keep their transparency. Each docked icon sits in a
hidden container off every screen, under every other window, and the
compositor never shows it. The icon is redirected and its pixmap read every
half second; a changed icon is sent to electron/tray.mts, which lists it
with the StatusNotifierItems. A click comes back as the X button press and
release a real tray's icon gets: Activate is button 1, ContextMenu
button 3, both at the point clicked. The container moves under that point
first, so the program's own menu opens under Din's icon. Wine's explorer
draws every program's icon, so an icon can't say which program it's for.
Files' and the desktop's right-click menus have Open With for the
installed apps that open a file (din apps open-with), by its type. Types
come from the shared MIME database's globs, plus the types Windows programs
register, which Wine writes as packages beside their entries. Wine's entries
for those types are NoDisplay, so Din keeps NoDisplay entries only for
opening files. They're named and drawn from their program's own version
info and icon, found through the ProgID's open command in the registry. A
file of a kind Din has no app for opens in the first of them: Windows
programs' own types first, then by name.
On DinOS, Wine (wineWow64Packages.stableFull, which includes Wine Mono
and Gecko) is the Windows apps optional feature (below). Elsewhere Din uses
the system's wine, or $DIN_WINE.
The feature also brings Direct3D on Vulkan, which games need to run well or
at all. DXVK covers Direct3D 8 to 11 (nixpkgs' MinGW builds,
pkgs.dxvk.dxvk64/dxvk32). vkd3d-proton covers Direct3D 12 (its own
release tarball, since nixpkgs has no Windows build of it). Din links their
DLLs over Wine's in system32 and syswow64 ($DIN_WINE_DXVK_X64 and
the rest) and sets them to load Windows' first, then Wine's (the
native,builtin override). Links left dangling by a removed feature are
taken away, so Wine's own OpenGL ones load again.
Wine Mono only stands in for the old .NET Framework, so the feature also
brings .NET (the runtime and its Windows Desktop part, 64- and 32-bit,
Microsoft's MIT-licensed builds, unpacked in the store), which Din links
into the prefix's C:\Program Files\dotnet and C:\Program Files (x86)\dotnet ($DIN_WINE_DOTNET_X64, $DIN_WINE_DOTNET_X86), beside any
.NET installed there already. Programs built for an older .NET run on it
(DOTNET_ROLL_FORWARD=Major). A new .NET is dotnetVersion and the parts'
hashes in os/configuration.nix.
The menu bar's AirPlay item (once a receiver is nearby) and Settings →
Display share the screen with Apple TVs, AirPlay TVs and Rokus. Like a
monitor plugged in, a receiver connects the way it was last used, extending
the desktop by default as a separate display arranged in Settings like any
other; while connected it can be switched to mirroring the primary display
and back, from the menu or with the display-switch key (F7 on ThinkPads), and
that is remembered (airplay.json in Din's config). Sound goes to the
receiver too. A receiver that drops or refuses the connection while it wakes
up is tried again for half a minute, still "Connecting…", as a Mac does; if
that gives up, Din says so in a sentence. The
sending is deckmon's, a daemon DinOS runs as a
user service (the flake's deckmon input) and Din talks to over its socket
(electron/airplay.mts). It captures the screen through din-shell's wlroots
screencopy. A separate display is one din-shell adds with no screen behind it
(HEADLESS-1, through its own din-virtual-output protocol,
native/din-compositor/protocols/): it's drawn only into deckmon's copies, as
soon as Din draws, and goes with deckmon, even if deckmon is killed.
Elsewhere Din uses a deckmon on PATH.
The Passwords app keeps logins, verification codes (TOTP) and secure notes
in a KeePass file, ~/.local/share/din/Passwords.kdbx (KDBX 4, Argon2id),
which KeePassXC, Strongbox and KeePassDX open with the same password. Only
Din's main process holds it open (electron/passwords.mts); the app reaches
it through its passwords permission. Argon2 runs in a worker thread with
hash-wasm (electron/passwords-kdf.mts), since Electron's BoringSSL has none.
The vault comes with the computer, set up with its password: the first boot
after the Installer, creating the login keyring with the boot screen's
password, leaves a key for it derived from that password (os/din-keyring.py)
that Din makes the vault from, and Setup after erasing hands Din the new
password to make it with. A computer without one gets a setup screen that
takes the computer's password, checked by din-password check, or one of
the vault's own.
Set up with the computer's password, it opens at login with no prompt. The
key derived from the password, not the password, is kept in the login
keyring over the Secret Service (electron/secret-service.mts), which the
boot screen's password unlocks; the vault keeps its KDF salt across saves so
that key stays valid. Settings → Password re-keys the vault along with
everything else when its password was the computer's old one. Lock in the
app locks it until the password is typed; a save by another app (which draws
a new salt) makes the next unlock ask once.
The Web Browser fills logins in. electron/passwords-preload.cjs, in every
frame of web pages, reports focus on a login field; the desktop draws the
vault's logins for the site below it (PasswordAutofillHost), and main
checks the one picked against the frame's own URL (same host or registrable
domain, never https to http) before sending it to that frame. New-password
fields offer a strong password. A login submitted that the vault doesn't
have becomes an offer to save it, and the toolbar shows a site's saved
logins to fill or copy. Copied passwords and codes leave the clipboard after
90 seconds.
Passkeys work too: Din is the authenticator, since Chromium has none on
Linux. electron/passkeys-preload.cjs gives pages a navigator.credentials
whose public-key requests go to main (electron/passkeys.mts), and the
desktop asks the user below the toolbar (PasskeyPrompt): save a passkey,
or pick an account to sign in with. Main builds the signed client data from
the frame's own origin and checks the relying party ID against it, and keeps
each passkey in the vault the way KeePassXC does (KPEX_PASSKEY_* fields),
with the account's password when the vault has one, so KeePassXC, Strongbox
and KeePassDX use the same passkeys. A request for a security key, a frame
from another site, and the prompt's "Use a Security Key" go to Chromium's own
WebAuthn.
Import (the + menu, or Settings) adds another KeePass file's items, KDBX 3 or
4 with its password or key file, in a folder named after it; or another app's
export (electron/passwords-import.mts): CSVs from Google Passwords, the
Chromium browsers, Firefox, Safari and Apple Passwords, Bitwarden, 1Password,
LastPass, KeePassXC, Proton Pass, Dashlane, NordPass and the like, read by
their header rows, and Bitwarden's unencrypted JSON. The app reads the file
the user picks and hands its bytes to main, which parses it; items the vault
already has (same site, username and password) are skipped.
The Virtual Machines app runs other systems (Windows, macOS, Linux, anything
x86) with QEMU and KVM, no libvirt (electron/vms.mts, the vms permission).
Each machine is a folder in ~/VMs: vm.json, disk.qcow2, the UEFI
firmware's variables (vars.qcow2), the TPM's state (tpm/) and macOS's
boot loader (opencore/). A running
machine is a detached qemu-system-x86_64 as the user, with its QMP, VNC and
swtpm sockets in $XDG_RUNTIME_DIR/din-vms/<uuid>/; it keeps running when the
app closes, and Din finds it again after a restart by its pidfile. Its screen
is QEMU's VNC server, drawn by noVNC in the app, whose bytes go over the
app's bridge port (apps can't open sockets).
The app's gallery installs a system in one click
(electron/vm-installers.mts): Din makes the machine at once, sized to this
computer, then asks the system's own servers for its newest release and its
SHA-256 (Ubuntu's meta-release-lts and SHA256SUMS, Fedora's
releases.json, and for Windows Microsoft's download page, whose form a
hidden Electron window fills in since Microsoft turns away scripted
requests, and whose table lists each language's SHA-256), downloads it to ~/VMs/.installers/ (resumable, shared by
machines installing the same release, checked against that hash), and
installs it hands-free. Windows starts from its disc (Din presses Enter
for its "Press any key" prompt) with an autounattend.xml on a FAT drive
QEMU makes from a folder: Windows 11 Pro with Microsoft's generic key
(installed, not activated), the whole disk, and a local account with no
password, so no Microsoft account and no questions. At its first logon it
writes a line to the serial port, and Din ejects the disc and answer drive
while it runs. Linux installers boot their own kernel straight from
the disc image, with an answer file on a FAT drive QEMU makes from a folder
(Ubuntu's autoinstall, Fedora's kickstart): the account is the computer's
user's name, with no password, logged in automatically. The installer
powering the machine off is it finishing; Din ejects the disc and starts the
new system. The install's state is kept in vm.json, so it carries on after
a restart. Right-clicking a system offers Custom Install, the full form with
that system filled in; when a download fails, the app links to the system's
own download page and installs from the disc image the user chooses.
macOS is the newest release Apple's Internet Recovery offers the last Intel
iMac (iMac20,1's board, so macOS Tahoe 26), asked for as a Mac asks
(osrecovery.apple.com) and checked chunk by chunk against the chunklist
Apple signs with its EFI key. QEMU reads the .dmg itself, as a disk.
OpenCore, from acidanthera's newest GitHub release (checked against the
SHA-256 GitHub lists), starts it from a FAT drive QEMU makes of the machine's
opencore/, with a config made from the release's own Sample.plist: no
kexts, Apple Secure Boot off, and the machine's own iMac20,1 serial numbers.
Recovery has no answer file, so once its menu bar shows (Din reads the screen
over QMP) Din opens its Terminal with Shift-Command-T and types one command,
which runs a script on the answer drive: it erases the disk and runs
Apple's startosinstall, which downloads the full installer and restarts
the machine to install it. Din takes the answer drive out, and Apple's
installer goes on through its own restarts to Setup Assistant, which Din
answers in its own way: once Setup Assistant's card shows on the screen, it
turns the machine off and starts it again in the recovery image, plugged in
as a USB drive with a copy of OpenCore that looks only at USB drives, and
types a second command. That script makes the account (the computer's
user's name, an administrator with no password, logged into automatically
and with passwordless sudo), sets this computer's time zone and region,
marks Setup Assistant done, turns on Reduce Transparency (macOS draws
everything on the CPU, and blurring what's behind its menus and windows
every frame is what makes it crawl), keeps it from sleeping (its disk
doesn't come back from sleep in QEMU), then turns the machine off, says so
on the serial port, and Din starts macOS at its desktop.
Windows machines get what Windows 11's installer checks for and has drivers
for: UEFI with Secure Boot and Microsoft's keys (OVMF's OVMF_VARS.ms.fd), a
TPM 2.0 (swtpm), a SATA disk, an e1000e network card and Hyper-V
enlightenments. macOS gets an Intel CPU model (on AMD hosts too, with the
TSC's rate given so it can still be saved running), QEMU's Apple SMC, USB
input and a SATA SSD, and no sound (it has no driver for QEMU's); its
screen is 1280×800, since it has no driver for QEMU's display and draws
every pixel on the CPU. Linux gets virtio, and its screen follows the window's size.
The clipboard is shared with guests running spice-vdagent; sound goes through
PipeWire. Snapshots are qcow2's own: taken while running, they keep memory
too and come back running.
DinOS passes QEMU, swtpm and OVMF to Din as DIN_QEMU, DIN_QEMU_IMG,
DIN_SWTPM and DIN_OVMF, which are there once the Virtual machines
optional feature is installed (below); elsewhere Din looks on PATH and in the usual
OVMF folders (/usr/share/OVMF, /usr/share/edk2/ovmf, …), and needs
/dev/kvm to be read-writable by the user.
The Games app plays the games in ~/roms, laid out as RetroArch, EmulationStation
and Batocera lay them out: a folder per system (nes, snes, gba, n64,
nds, psx, dreamcast…; electron/game-systems.mts lists them and the
other names each goes by), its games in it, zipped or not, a game with
several files in a folder of its own. Main reads them (electron/games.mts,
game-library.mts, the games permission), groups each game's copies
(regions, revisions, discs) by their No-Intro names (game-names.mts),
picks the copy for the computer's language, and hides BIOS files, bad dumps,
demos and betas. Box art comes from libretro-thumbnails, matched by name, with
the list of what it has for a system cached in ~/.cache/din/games/art; the
app fetches each picture once (din.fetch), shrinks it and keeps it in its
own IndexedDB.
Playing a game runs RetroArch with the system's core, full screen on the
embedded display, as an app of its own named for the game with its box art
in the Dock (launchAsApp in electron/native-apps.mts), where its frames go
straight to the screen and the performance power mode comes on. Din writes
RetroArch's settings itself, in ~/.local/share/din/games (saves, the
moments games were left at, BIOS files), and never touches
~/.config/retroarch: Vulkan, PipeWire, controllers set up by RetroArch's
autoconfig profiles, pixel art scaled sharp, the game paused while its window
isn't in front, and where a game was left saved when it quits and picked up
when it starts again. Start+Select quits back to the library; the Home button
or L3+R3 opens RetroArch's menu. The app is driven by a controller as well as
the mouse (src/apps/games/games/input.ts), and full screen it becomes a TV's
view. GameCube, Wii, 3DS, PS2 and Switch games are listed but can't be played
yet: their emulators aren't libretro cores.
On DinOS RetroArch and its cores are the Games optional feature, found through
DIN_GAMES (a part per core, all free software in the binary cache);
elsewhere Din runs retroarch from PATH with the cores it finds in the usual
places.
Printers need no setup on DinOS. CUPS runs with Avahi and ipp-usb, so a
printer on the network, or one plugged in that speaks IPP over USB, appears
in the print dialog as soon as it's on. The first time Din prints to one (or
makes it the default) it gives it a driverless queue of its own, so it's
still there after a restart. Din reads the printer list in the background
once it starts, so the print dialog doesn't wait on the network, and
remembers the network printers found on each Wi-Fi or wired network
(printers.json in Din's config), so they're listed again as soon as the
computer is back on it, before CUPS has found them. Settings →
Printers shows them and their queues, sets the
default, prints a test page, and adds older USB printers with the best
driver installed (gutenprint, brlaser).
The print dialog is Din's own, a window per document
(src/components/desktop/apps/PrintWindow.tsx), not a modal, so the
desktop stays usable while a job is sent: Chromium's would open a GTK
window, which DinOS's compositor wouldn't show. electron/print-preload.cjs sends every page's print(), frames'
included, there, and so do Ctrl+P, the browser's menu and right-click menu,
and Print in Files and Preview. Whatever is printed goes to CUPS as a PDF
laid out by Chromium, and the dialog previews that PDF's pages, drawn by
poppler (electron/printing.mts). Save as PDF needs no printer at all.
The Assistant app, the menu bar's robot, and Super+A (a prompt from
anywhere) talk to OpenCode v2, the coding agent,
through its per-user background server (opencode serve --service). Din
starts it at login while Settings → Assistant has it on, and holds its
password and event stream in the main process (electron/assistant.mts);
the shell only gets what it asks for under /api/. Conversations are
OpenCode's sessions, so opencode in a Terminal shares them. OpenCode is the
user's own self-updating install in ~/.opencode/bin (DinOS puts it on
PATH). DinOS's disk image comes with it there (opencode in
os/flake.nix; bump its version and hash now and then). Where it's missing,
or won't run (it's the AVX2 build), Din runs the v2 installer;
$DIN_OPENCODE names another binary. As OpenCode's TUI does, Din asks
OpenCode's update service for a newer release every few hours while the
assistant is on, and offers it at the foot of the Assistant's list and in
Settings → Assistant (din assistant update): opencode upgrade, then the
server restarts on it.
Super+H types what you say into whatever has the keyboard: a text field, a
web page, the Terminal, or a native app (pasted, since the display only sends
keys). Holding Super+A opens the assistant's prompt listening, and the
prompt's mic button dictates into it. The shell records the microphone
(src/lib/dictation.ts) and whisper.cpp's server turns it into text on the
GPU (electron/dictation.mts), started on first use and stopped after a few
idle minutes. On DinOS the server and Whisper large-v3-turbo (about
550 MB) are the Dictation & transcripts optional feature (below), named in
$DIN_WHISPER_SERVER and $DIN_WHISPER_MODEL; elsewhere, set both to
dictate.
Windows float by default. Super+Shift+T (or Settings → Windows, or Super+T
on a window) turns on Hyprland-style dwindle tiling: each new window splits
the focused tile (src/lib/tiling.ts). The rest of the Super keys follow
Omarchy's bindings (src/lib/wmBindings.ts; Super+K lists them), and work
with tiling on or off. Each monitor has workspaces 1 to 10 of its own;
windows on the ones not showing stay mounted, hidden like minimized ones.
The window manager (src/hooks/useWindows.ts) keeps the tiling trees, and
settle() derives every window's tile and visibility after each change.
While tiling is on, or a workspace other than 1 is in use, the menu bar shows
the workspaces and a switch to turn tiling off. Keys pressed inside web pages
reach the shell through the host's wm shortcut (electron/main.mts).
Some of DinOS takes a lot of space and only some people use it, so it isn't
on the disk until someone installs it, in Settings → Optional Features or
with din features: Windows apps (Wine), Virtual machines (QEMU, swtpm,
OVMF), Dictation & transcripts (whisper.cpp and its model), a web view for
Linux apps (WebKitGTK, on nix-ld's library path) and Linux containers
(Podman, docker, distrobox). Together they're over 2 GB the disk image
doesn't carry.
Each is a few packages from the system's nixpkgs, its parts, listed in
dinFeatures in os/configuration.nix. The system names them in
/etc/dinos/features.json only by the store paths they are, so none of
them, nor how they're built, is in the system (and so not on the disk
image); installing one downloads each part as the user through the Nix
daemon (electron/features.mts) from the binary cache, which has
everything nixpkgs builds, into ~/.local/state/din/features/<id>/<part>,
a GC root. The release checks every part is there
(scripts/dinos-release.sh). A part that's a plain download, the Whisper
model, gives its URL and hash and lands where fetchurl would put it.
Everything that uses a feature looks at its parts' links (the launcher's
PATH and DIN_* variables, nix-ld's library path; docker is a small
script in the system for Podman), so it works as soon as it's installed.
Removing one deletes its links and whatever only it used. A system update
downloads the new versions of installed ones before switching
(os/din-update.sh), and Din updates any it couldn't soon after it
starts; one installed before features had parts, a single link, is
downloaded again in parts. A computer updated from a
DinOS that had them all built in keeps them all: the first time Din starts
with optional features there, it installs every one, from what's still in
the store.
Dictation is default: it comes with the computer all the same, without
the disk image carrying it. Din keeps which features the user wants in
~/.local/state/din/feature-choices.json, and a default one counts as
wanted until they remove it, so a new or erased computer downloads it in
the background soon after Din first starts, and an install that a restart
or a lost connection cut short carries on the next time Din starts or the
computer is online (it tries every few minutes). Nothing downloads by
itself from a live USB stick (/run/din/live); the computer it's installed
onto does. A computer set up before defaults existed, whose features
folder was already there, treats a default feature it doesn't have as
removed.
A new optional feature is an entry in dinFeatures with a title,
description, size and package (and default = true if it comes with the
computer), whose users look for it under its link.
On another Linux, Settings → Optional Features and din features are the
system's own packages instead (electron/system-packages.mts): the same
features where the distribution packages them, and the tools DinOS has
built in that Din's features need (Xvfb for Linux apps, wf-recorder for
screen recording, bsdtar/7-Zip/unar for archives, jmtpfs and ifuse for
phones, Poppler for print previews), each installed with apt, dnf, pacman,
zypper, xbps or apk through sudo -A, the password asked in Din. Din
doesn't remove them; that's the package manager's. Each entry in CATALOG
names what shows it's there and its packages per package manager, and one a
distribution doesn't package isn't offered there. What can't open for want of
one says where to get it ("… Install Archives in Settings → Optional
Features."), and an Install button goes beside it: FeatureInstallButton
(src/shared/FeatureInstall.tsx) in Screenshot, Files, Preview, Studio and
Virtual Machines (apps with the features permission), and on the desktop's
notifications (src/lib/featureNotifications.ts). Which feature is never
read from the message: each place names the one what it does needs (an
archive Archives, a .exe Windows apps, a phone's needsFeature), and the
button shows only while that feature isn't installed. Dictation isn't among them: distributions don't package whisper.cpp's
programs.
The Nix build fetches npm deps from package-lock.json pinned by hash.
After changing dependencies, refresh the hash in os/packages.nix:
nix run --inputs-from ./os nixpkgs#prefetch-npm-deps -- package-lock.json
# copy the sha256-... into npmDepsHash in os/packages.nixAlso note: when built from a git checkout, the flake only sees git-tracked
files — git add new source files before npm run os:build.
- The Web Browser app supports Chrome extensions (tabs, toolbar actions,
content scripts) via
electron-chrome-extensions;
about:extensions(the browser menu's Extensions) lists the installed ones and adds more: from the Chrome Web Store, a folder (which Din keeps loading from where it is), a.crxfile, or Nostr (NIP-82 apps with a.crx, seeNIP.md), and publishes any of them from its menu. Extensions run only in the browser's session partition — never against the shell. Installed web apps share that partition, like installed apps in Chrome: a site signed in in a tab is signed in in its app window, and the reverse. Manifest V2 is allowed (Electron lacksdeclarativeNetRequest, so classic content blockers like uBlock Origin need it);chrome.*API coverage is broad but not complete. - Din's signer is the system's NIP-07 provider. Browser tabs (not private
ones) and installed web apps get a
window.nostr(electron/nostr-preload.cjs) whose calls Din answers: a site has to ask to connect before it learns who you are, and Din asks before it signs, encrypts or decrypts anything unless you told it to remember (per site, and per kind for signing). Apps on other devices can use your account the same way over NIP-46, with a bunker:// link Din makes or a nostrconnect:// link they make (electron/bunker.mts). Settings → Connected apps lists all of them; the key button in the browser's toolbar shows the current site's. - The Settings app manages WiFi through NetworkManager (
nmclibehind an Electron IPC bridge,electron/network.mts): joining, Add network (hidden and 802.1X enterprise networks), Saved networks, and each network's own page with Android's options (auto-connect, metered, MAC privacy, password or sign-in, static IP and DNS, proxy, hidden, share as a QR code). A network's proxy is Din's own (kept inwifi-proxy.json) and applies to Din's browser and apps while that network is up. NetworkManager picks up physical adapters (wired and wireless) automatically. Note that QEMU has no wireless hardware, so the WiFi panel will show no networks inside the demo VM. - The Settings app and the menu bar manage Bluetooth through BlueZ over the
system bus (
electron/bluetooth.mts): turning it on and off, finding, pairing, connecting and forgetting devices. Prompts a pairing needs (a code to compare or type) appear on the desktop. Headsets show up in the Sound panel through WirePlumber once connected. - Calendar keeps events only on this computer, as a private desktop
calendar does: a folder per calendar in
~/.local/share/din/calendar, one iCalendar (.ics) file per event, with the calendar's name and colour indisplaynameandcolorbeside them (src/shared/localCalendar.ts,src/shared/ical.ts), the vdir layout vdirsyncer can sync with a CalDAV server later. Day, week and month views, all-day and timed events, and calendars of the user's own (Personal to start). Beside them is the Nostr calendar, marked Public: the account's NIP-52 events (src/shared/calendar.ts) and invitations by npub with Going / Maybe / Not Going answers (View → Nostr Calendar hides it). An event goes to Nostr only when the user picks the Nostr calendar for it. The menu bar's clock opens it, anddin calendarlists, adds, answers and deletes events, and manages the calendars. - Contacts is the address book, kept only on this computer: one vCard per
person in
~/.local/share/din/contacts(src/shared/contacts.ts,src/shared/vcard.ts), the vdir layout vdirsyncer and khard use, with groups (the cards' CATEGORIES), pictures, birthdays and Apple's and Google's custom labels. It imports and exports .vcf files (Files opens them there; someone already in the book gets what the file adds rather than a second card), shows each phone's synced contacts beside it, read-only, to add from, and starts a text in Messages or an email in Mail.din contactslists, shows, adds, edits and deletes people, and groups, imports and exports them. - Messages reads and answers the texts on the user's phones, added in
Settings → Phone (
electron/phone.mts): iPhones and Android phones alike over Bluetooth's Message Access Profile, through BlueZ's obexd (electron/phone-map.mts), as Windows does for iPhones; no pictures or group conversations. New texts pop up as notifications with a reply field. Messages are kept only on this computer, sealed with the login keyring. - A text with a verification code in it (
electron/phone-codes.mtsfinds it) has the code offered in the Web Browser's code fields for 10 minutes, as a Mac offers the codes from Messages: the suggestions below the field (autocomplete="one-time-code", a field named like one, or a row of one-digit boxes) list it "From Messages", and the one picked is typed in, spread across the boxes. A code arriving while the cursor is in the field brings them up. The code is kept in memory only, and goes only to the page it's picked for; its notification has a button to copy it. "Fill in verification codes" on the phone's page (din phone fill-codes) turns it off. - A phone added there can share files to the computer over Bluetooth (the
share sheet's Bluetooth target):
electron/phone-receive.mtsis obexd's agent, saving them to Downloads without asking, with one notification per share for its progress and then Open or Show in Files. Other devices are turned away; "Receive files" on the phone's page (din phone receive-files) turns it off. - A phone added in Settings → Phone with "Receive files" on can share
files, text and links with Quick Share (
electron/quickshare.mts), with nothing to turn on: while it's connected over Bluetooth, Din listens for the beacon its share sheet sends, and sends the Bluetooth LE advertisement Android receivers send for 90 seconds after. No server is involved. The phone connects over Bluetooth (RFCOMM, throughelectron/quickshare-bt.py, since Node can't take the socket BlueZ hands over), on a link encrypted with the pairing's key, which proves which phone it is, so its share arrives without asking, in a notification. Anything else connecting that way is asked about in a notification, with the code. - Bluetooth is slow, so a share over it moves to a WPA2 hotspot Din hosts on
the Wi-Fi card alongside its own connection, on the same channel
(
electron/quickshare-hotspot.mts); Android phones leave their own Wi-Fi while the share sheet is open, so a hotspot is what both can reach. Adding its interface (dinqs0) needs root, soos/din-hotspot.shdoes that through sudo, and NetworkManager runs the network from a profile it doesn't save, with a new name and password each time. A udev rule keeps the interface coming and going from restarting wpa_supplicant (NixOS's rule for any Wi-Fi interface), which would drop the computer's own connection. Without the helper (outside DinOS) the share stays on Bluetooth. - On hardware, Din runs GPU-accelerated (Mesa via
hardware.graphics, plus redistributable firmware). Only the QEMU demo variant passes--disable-gpu(no virgl) and falls back to software rendering. - Din runs as the
dinuser with Chromium's sandbox and context isolation enabled; the renderer has no Node access. - The
app://protocol handler inmain.mtsserves only files insidedist/(path-traversal guarded) and falls back toindex.htmlfor SPA routes.