← Documentation overviewDeveloper guide / START-HERE

From turning it on
to drawing a pixel.

Start here if Linux, Rust and graphics APIs are new to you. Follow one real Lucent installation from files on disk to a running, interactive desktop.

01 / ORIENTATION

There are a few different
systems in the picture.

Linux is the kernel: the central program that manages processes, memory and access to devices. Arch Linux supplies that kernel plus installed software and a package manager. Omarchy builds a configured desktop environment on Arch, including Hyprland, shortcuts, applications and management tools.

Hyprland is the compositor. It combines the images produced by applications, decides where their windows appear and routes keyboard/mouse input. Wayland is the protocol those applications use to communicate with a compositor. In this setup, Hyprland is the Wayland server.

Lucent is one of those applications. It draws the bar, dock, launcher, widgets and notifications using special desktop surfaces. It also asks system services to do things such as changing volume or starting another application. Hyprland still tiles that new application’s window.

Running programs · simplified process/ownership map
Inside the virtual machine

Linux + system services
  └─ your graphical session
       ├─ Hyprland                    places and combines surfaces
       ├─ stock Omarchy services      remaining system integrations
       ├─ lucent-desktop              bar / dock / launcher / widgets
       └─ normal applications        terminal / browser / editor

On the host: QEMU displays the VM; Astro serves this website.

A process is a running instance of a program. A service is a program managed in the background. A session is the set of activity associated with a logged-in user. The host is your actual machine; the guest is the virtual machine. A command in the host terminal does not automatically run in the guest.

Two meanings of “shell”

A terminal shell, such as Bash, reads commands. A desktop shell, such as Lucent, provides the visible desktop controls. Opening a terminal starts an application containing a command shell; it does not create another Lucent desktop.

02 / BEFORE ANYTHING RUNS

The repo contains code.
Linux runs executables.

The repository is a collection of source files. Rust source ends in .rs. Cargo is Rust’s build and dependency tool; rustc is the compiler that turns Rust into machine code. A crate is a unit of Rust code compiled as a library or program. A Cargo package can expose a library and multiple executable targets; a workspace groups related packages.

Our workspace is described by lucent/Cargo.toml. Each member has its own Cargo.toml, listing dependencies. Cargo.lock records the resolved dependency versions. A Rust use statement makes a name convenient to reference; it does not start a service or dynamically load an application.

Build time → installation time → runtime
Source files + Cargo manifests + dependency sources
                         ↓ cargo build
                  compiler + linker
                         ↓
             lucent/target/release/lucent-desktop
                         ↓ installer copies it
                   ~/.local/bin/lucent-desktop
                         ↓ operating system starts it
                 a running desktop process

The library crates are compiled and linked into the executable as needed. They do not become nine separate background programs, and Linux does not scan the crates/ directory at login. Native system libraries and graphics drivers are still runtime dependencies. You do not need Cargo running while you use the desktop.

~ means the current user’s home directory. ~/.local/bin holds user-installed executables; ~/.config holds user configuration; ~/.local/state holds persistent application state. These are different from the repository checkout. Editing source changes the checkout; it does not replace the already-installed binary.

03 / THE STARTUP CHAIN

Who starts whom?

Choose a stage to follow the chain. The login stage below assumes Lucent’s optional greetd integration is activated. An installation keeping Omarchy’s original login mechanism joins the same chain at the user’s Hyprland session.

VM firmware → Linux kernel → systemd

Start Linux

The virtual machine presents a virtual CPU, memory, disk and display. Its firmware starts the boot loader, which loads the Linux kernel. The kernel manages hardware access, memory and running programs. It starts the system service manager, systemd.

Running now: the operating system and its background services. Lucent has not started.
Read this part of the implementation ↗

The installer writes ~/.config/systemd/user/lucent.service. A unit is systemd’s configuration for a managed resource. In this unit, ExecStart points to ~/.local/lib/lucent/session.py, and ExecStopPost runs its restoration path. systemctl --user talks to your user’s manager; a system-wide service such as greetd is managed separately.

The activation script adds a managed block to ~/.config/hypr/autostart.lua. Hyprland runs that block at session startup. Existing Omarchy configuration remains relevant: it starts the compositor environment and the services Lucent still relies on. The wrapper’s readiness checks prevent a failed new shell from simply hiding the fallback UI.

Source: installation and activation, session handoff and restoration, optional login integration.

04 / INSIDE THE EXECUTABLE

The entry point
is intentionally small.

Want to see exactly how these calls connect?

Follow the wiring from main to a frame: expand desktop_adapters(), trace declarations to their runtime callers, explore coordinates and follow a launcher action into its adapter.

Live source · the desktop entry point
fn main() -> Result<(), Box<dyn std::error::Error>> {
    if std::env::args().nth(1).as_deref() == Some("--display-guard") {
        return lucent_services::displays::run_guard().map_err(Into::into);
    }
    let mut desktop = Desktop::new(platform::desktop_adapters());
    desktop.live_wallpaper.enable_frame_tracing(
        std::env::var_os("LUCENT_PROFILE_PRESENTATION").is_some_and(|value| value == "1"),
    );
    lucent_wayland::run(desktop)
}

Read the nested call from the inside out:

  1. platform::desktop_adapters() constructs the concrete service adapters: settings storage, applications, compositor, audio and the rest. It returns a bundle of capabilities.
  2. Desktop::new(…) takes that bundle and creates initial state. It initializes child controllers, loads the small bundled symbol set and sets defaults. Widget views later build their registry from the compiled catalog. The installed app list and most external snapshots have not arrived yet.
  3. lucent_wayland::run(…) takes ownership of the desktop and runs it. This call stays active until the application exits or a fatal error occurs.

The runtime connects using session environment variables. WAYLAND_DISPLAY identifies the compositor connection; XDG_RUNTIME_DIR identifies your user’s temporary runtime directory. A socket is a local communication endpoint—programs send messages through it. Starting the binary from an unrelated SSH environment may fail because it lacks the graphical session’s connection information.

Next the runtime binds advertised Wayland interfaces, loads the client’s embedded fonts, creates a shared graphics device and builds an event loop. An event loop waits for input, worker results and compositor messages, handles them, and waits again. An effect worker runs queued external operations; subscription workers produce ongoing updates.

The runtime calls init to schedule startup loading. It reads surfaces() and creates native Wayland layer surfaces for the bar, dock and widgets. A surface is a place an application can submit pixels and receive input. Hyprland replies with a configuration, including dimensions; the runtime then has enough information to render it.

Startup overlaps: workers can still be loading app metadata while an initial frame displays default state. “Constructed the Desktop struct” and “all services are ready” are different milestones. The session wrapper waits for observable rendered frames and notification readiness.

05 / READ THIS AS A MAP, NOT A CHECKLIST

Where each crate fits.

Conceptual ownership map · not every Cargo dependency is shown
lucent-desktop: state, views, widgets, composition
  ├─ desktop behavior: domain ← usecases / services
  ├─ visual choices:   design → api
  └─ execution:        wayland runtime
                          ├─ ui: Element → Scene
                          └─ render: Paint → wgpu → Vulkan

lucent-menu: independent picker client, same framework
lucent-session: shared lock/login UI + separate binaries
  └─ auth: PAM / greetd conversations
lucent-cli: short-lived command sender to the running desktop
Library cratePlain meaningWhat lives there
lucent-apiThe shared vocabularyDefines Element, Component, Message-related contracts, Effects, surfaces, motion and the widget registry. Like the public interfaces and data types of a UI library.
lucent-domainDesktop facts and capabilitiesDefines Application records, workspaces, settings, notifications and service traits. No drawing or OS calls.
lucent-usecasesDesktop decisionsSearch ranking, launch-versus-focus, snapping, menu navigation and notification timeout rules. Calls domain ports when a decision needs an external action.
lucent-servicesActual OS integrationsImplements the domain ports using files, Hyprland sockets, commands, HTTP and D-Bus. Turns external data into domain values.
lucent-designThe visual languageTokens, palettes, control recipes and app icon artwork. Turns shared visual choices into reusable elements.
lucent-uiLayout and input behaviorTurns an Element tree into positioned rectangles, drawing instructions and clickable regions. Tracks focus, hovering, editing and dragging.
lucent-renderDrawing those instructionsOwns GPU resources, text/image textures, shaders and wgpu render passes. It does not launch apps or decide which widget is visible.
lucent-waylandThe running application engineConnects to the compositor and owns surfaces, input delivery, worker messages, frame pacing and the event loop. It calls the UI and renderer crates.
lucent-authAuthentication adaptersTalks to PAM or greetd for the lock/login clients. The desktop shell does not authenticate a password itself.

Client here means an application using our framework. It does not mean a website or a network client. lucent-desktop is the main client. The lucent-session package supplies the shared session UI and the lucent-lock/lucent-greeter executables. hello-layer is a small example client. The website is a separate Astro project.

A port is a Rust trait describing a capability, such as “launch this application.” An adapter is code implementing it using the real OS. A use case makes the decision, such as “focus an existing window if this was a dock click.” Passing adapters into the client at startup is called dependency injection. Tests can supply fake adapters without changing view code.

Start reading in main.rs, then platform.rs, then desktop.rs. Follow one message into views.rs or a port. You can understand a launcher interaction before reading a Vulkan shader.

06 / SEVERAL DIFFERENT REGISTRIES

What gets “registered”?

ThingWho registers or discovers it?What that actually means
A systemd unitInstaller writes lucent.service; daemon-reload makes the user manager reread units.A configuration file tells the manager which process to start. Hyprland autostart explicitly starts it. Writing a unit is not the same as starting it.
A Wayland interfaceHyprland advertises protocol objects; our runtime binds the interfaces it needs.A local connection to the compositor. SCTK handles protocol plumbing. This does not register a Lucent widget with Linux.
A Lucent widgetThe client catalog supplies selector entries, geometry and view functions. The widget view builds the framework registry for its borrowed context.An in-memory list inside lucent-desktop. A stable string such as clock identifies the widget in saved settings.
An installed applicationA package or installer writes a .desktop entry; XdgApplications discovers it.A small metadata file describing name, icon and launch command. The launcher searches those records, not every executable on the disk.
The notification serviceThe adapter requests org.freedesktop.Notifications on the session D-Bus.A well-known local service name. Other applications send notifications to its current owner, so two competing notification servers cannot both own it.
A subscriptionThe client returns a stream recipe with an ID; the runtime starts a worker for it.A producer of typed messages inside the same application. The runtime retains or cancels it by ID.

A widget registration, in actual code.

Live source · the seven built-in widgets
pub const WIDGETS: [Definition; 7] = [
    Definition {
        id: "calendar",
        title: "Calendar",
        description: "Your month at a glance",
        size: (size::calendar::WIDTH, size::calendar::HEIGHT),
        from_right: false,
        position: (layout::LEFT, layout::TOP),
        keyboard_interactive: true,
        render: view::calendar,
    },
    Definition {
        id: "clock",
        title: "Clock",
        description: "Large, stacked time",
        size: (size::clock::WIDTH, size::clock::HEIGHT),
        from_right: true,
        position: (layout::RIGHT_INSET, layout::CLOCK_TOP),
        keyboard_interactive: false,
        render: view::clock,
    },
    Definition {
        id: "weather",
        title: "Weather",
        description: "Current local conditions",
        size: (size::weather::WIDTH, size::weather::HEIGHT),
        from_right: false,
        position: (layout::WEATHER_LEFT, layout::WEATHER_TOP),
        keyboard_interactive: true,
        render: view::weather,
    },
    Definition {
        id: "media",
        title: "Media",
        description: "Now playing and controls",
        size: (size::media::WIDTH, size::media::HEIGHT),
        from_right: true,
        position: (layout::RIGHT_INSET, layout::MEDIA_TOP),
        keyboard_interactive: true,
        render: view::media,
    },
    Definition {
        id: "system",
        title: "System",
        description: "Live CPU and memory",
        size: (size::system::WIDTH, size::system::HEIGHT),
        from_right: false,
        position: (layout::LEFT, layout::SYSTEM_TOP),
        keyboard_interactive: false,
        render: view::system,
    },
    Definition {
        id: "notes",
        title: "Notes",
        description: "A thought to keep nearby",
        size: (size::notes::WIDTH, size::notes::HEIGHT),
        from_right: false,
        position: (layout::SECOND_COLUMN, layout::TOP),
        keyboard_interactive: true,
        render: view::notes,
    },
    Definition {
        id: "timer",
        title: "Focus",
        description: "A 25-minute focus timer",
        size: (size::timer::WIDTH, size::timer::HEIGHT),
        from_right: false,
        position: (layout::SECOND_COLUMN, layout::TIMER_TOP),
        keyboard_interactive: true,
        render: view::timer,
    },
];

Each catalog definition holds a stable ID, title, description, token geometry and a function. Rust functions can be stored as values and called later. register appends the descriptor/function pair and rejects duplicate IDs. When drawing a visible widget, the client looks up its ID and calls that stored function with current state and view context.

Registering a widget makes it available; saved visibility determines whether it is shown. Current widgets are compiled into the executable. Dropping a Rust file in a folder does not install a widget. The selector, IPC validation, default geometry and view registry all use this single WIDGETS catalog. A new catalog entry must supply its compiled view function.

07 / THE INPUTS TO YOUR DESKTOP

What gets loaded,
from where, and when?

InputLocation / providerWhen it becomes available
Fonts, shader source, symbolsAssets embedded by the Rust buildStartup initializes font/graphics resources. Textures are created or cached when needed. Editing these sources requires rebuilding.
Widget positions, pins, notes, theme modeLucent’s XDG state directory, normally ~/.local/state/lucent/desktop.jsonThe init effect loads JSON; Message::Loaded installs the result. Serde converts JSON fields into Rust values.
ApplicationsXDG application directories, normally ~/.local/share/applications and /usr/share/applicationsThe application adapter registers directory observation before discovery and publishes changed catalogs through ApplicationPort.watch. Search uses current records; failures retain last-good data, and updates preserve the selected AppId when it remains available. The catalog guide describes filesystem limits and remaining native acceptance.
Icons and wallpaper previewsBundled app catalog, installed icon sources and wallpaper filesAfter metadata arrives, an effect loads application icons and sends Message::Icons. Local wallpaper previews load only when their cards are needed, through a bounded client coordinator and AssetPort. The renderer uploads/caches prepared textures as they are used.
Clock, system, media, weatherInjected adapters reading the OS or configured weather endpointSubscription workers sample at their configured intervals and send snapshots. An unconfigured weather service remains unavailable.
Windows and workspacesHyprland’s IPC socketsThe adapter requests snapshots and watches compositor events. This is separate from the Wayland connection used for our own surfaces.
NotificationsOther applications via the session D-BusThe notification server receives requests, updates its inbox and publishes snapshots to the client.

IPC means inter-process communication: programs exchanging messages. D-Bus is one IPC system used by Linux desktop services. Hyprland sockets, the Wayland connection and Lucent’s own command socket are other channels with different message formats. “Uses IPC” does not mean they all talk through one global bus.

Data flows back to the UI as a Message, an enum identifying what happened. Only the UI event-loop thread updates desktop state. A thread is a path of execution within a process; workers let the desktop continue handling input while an external operation runs.

08 / THE GRAPHICS STACK

From a Rust value
to a visible rounded box.

Rendering means computing an image. A GPU is a processor designed for graphics and other parallel work. Vulkan is a low-level API for talking to a graphics driver. Our Rust code uses wgpu, a graphics library, with its Vulkan backend enabled.

WGSL is the language of our shader program. A shader describes calculations used to draw shapes or sample textures. wgpu prepares that shader for the selected backend. WGL is a different name you may encounter: it is a Windows/OpenGL interface and is not used here.

One frame · each stage has a different owner
1. State:       launcher is open; width target is 448
2. Motion:      current time → current animated width
3. View:        Element tree describes controls and styles
4. Layout:      logical rectangles + paint list + hit targets
5. Renderer:    textures + primitive data + GPU commands
6. wgpu/Vulkan: execute drawing and present a surface buffer
7. Hyprland:    combine that buffer with other surfaces
8. Display:     show the composed image

Paint is our list of drawing instructions, such as “draw this rounded rectangle” or “place this image here.” It is data, not a callback from Vulkan. Our renderer translates it into buffers and draw calls. A buffer stores data; a texture is image data arranged for graphics sampling. The driver executes the graphics API on the available hardware or software implementation.

Our VM currently uses Mesa software Vulkan. The same Vulkan API is being executed on CPU resources there; using Vulkan does not itself prove that the host GPU is rendering Lucent. Hyprland’s rendering and Lucent’s rendering are also separate clients of graphics machinery.

Who moves the box between frames?

Our CPU-side Motion stores a start value, target, start time, duration and easing curve. Each frame samples it with the current time. A 500 ms transition means it reaches the target after half a second, regardless of how many intermediate frames were possible. The GPU draws the sampled shape; it does not own that timeline.

The runtime marks a surface dirty when its picture needs updating. It requests a one-shot Wayland frame callback to pace further drawing. When that callback arrives, it draws again if motion or another event still requires it. Once settled, it stops repainting until something changes. Service sampling can still happen while no frame is drawn.

If you know HTML: view produces a tree, layout resolves boxes, paint creates drawing instructions and the graphics layer produces pixels. Our framework rebuilds a dirty surface’s tree and scene; it has no browser DOM, CSS engine or automatic DOM diff.

09 / PUT THE PIECES TOGETHER

Press Super+Space,
search, then launch.

  1. Hyprland catches the shortcut. Its user binding starts the small program lucent-cli launcher toggle.
  2. The CLI sends a local command. It connects to $XDG_RUNTIME_DIR/lucent.sock, writes the command and waits for a reply. It does not create another desktop process.
  3. The running desktop receives a message. Its command handler converts the text to Message::Launcher(launcher::Message::Toggle). The event loop calls update.
  4. State changes. The client opens Apps, clears the search and retargets its panel motion. Its surface declaration requests overlay placement and keyboard focus for the open launcher.
  5. A frame is requested. View, layout and rendering draw the growing panel. Keyboard input becomes query/navigation messages, and pure search ranks the already-loaded app list.
  6. You press Enter. The selected app ID resolves to its domain record. An effect calls the application port; the adapter starts its parsed executable and argument list.
  7. The new application talks to the compositor. A native Wayland app creates its own window; an X11 app can be mediated by XWayland. Hyprland places the window and manages focus/tiling. Lucent’s compositor adapter then observes the updated window state.

The launcher does not render the terminal’s text or the browser’s webpage. Those applications produce their own images. Lucent only presents desktop controls around them. The same separation lets Lucent crash or restart without intentionally closing your normal applications.

10 / WHAT A CHANGE ACTUALLY REQUIRES

Which file should I edit?

Desired changeOwning layerHow it reaches the running system
Change row padding or a semantic colordesign/tokens.json → generated Rust/CSS consumersRun the generator, rebuild the Rust client, install it and restart the service. Astro uses the website consumer separately.
Add a widgetClient widget view and one catalog entry with metadata and token geometryCompile the function into the client, install/restart, then enable its stable ID through visibility settings.
Change launch-versus-focus behaviorlucent-usecases and its testsRebuild the client depending on that crate. No Wayland protocol or shader change is needed.
Add a system capabilityDomain port → adapter → composition root → client messages/viewsImplement the external work, choose its startup/subscription policy, build and install the client.
Change a saved widget positionClient state and settings adapterDrag/release updates it live and saves it. No compilation is needed.
Change a global shortcut or window radiusUser Hyprland integrationUpdate the supported configuration and reload Hyprland. The compositor owns these rules.

A restart starts a fresh process, which reads startup configuration again. A reload asks an existing program to reread something it supports reloading. Lucent has specific live interactions; it does not have a universal mechanism that reloads every source or settings file after arbitrary edits.

The installation guide provides the full build/install/activation sequence. “cargo build succeeded” establishes that code compiled; the VM still needs the resulting binaries installed. Likewise, rebuilding this Astro website changes documentation, not the desktop executable.

11 / OBSERVE THE REAL SYSTEM

Try these in the VM.

Run these read-only commands in a terminal inside the prepared Lucent guest. They reveal the same chain explained above.

Guest terminal · inspect startup, logs and state
# Is the managed desktop service running?
systemctl --user status lucent.service

# What command starts it?
systemctl --user cat lucent.service

# What happened during this boot?
journalctl --user -u lucent.service -b --no-pager -n 60

# Which user programs are running?
pgrep -a -u "$(id -u)" 'Hyprland|lucent|quickshell'

# Which connection information did this terminal inherit?
printf 'Wayland: %s\nRuntime directory: %s\n' "$WAYLAND_DISPLAY" "$XDG_RUNTIME_DIR"

# Ask the running Lucent client for its current state and surfaces
lucent-cli inspect

status shows whether systemd considers the service active. The main PID can belong to the Python wrapper; the Rust executable is its child. journalctl reads recorded service output. inspect asks the live desktop rather than reading a saved JSON file. Lock/login clients deliberately do not expose this debug socket.

Follow the first broken connection

If the executable is missing, check installation. If the service fails, read its journal. If Wayland cannot connect, check the session environment. If the shell draws but an app is missing, check its .desktop metadata. If a weather widget is empty, check that adapter’s configuration. These failures belong to different layers.

Read the build and installation guide before changing integration. The rollback script removes its managed startup/binding blocks and stops Lucent, while preserving user settings and installed binaries.

12 / JUST ENOUGH RUST TO READ ALONG

A small translation table.

Rust notationRead it asLucent example
structA named record of fields.Desktop owns current UI state; Placement contains x/y.
enumOne of a defined set of variants, sometimes carrying data.launcher::Message::Query(String) means the search text changed.
trait / implA behavior contract / an implementation of it.ApplicationPort / XdgApplications.
&T / &mut TBorrow a value to read it / borrow it exclusively to change it.view reads state; update can mutate it.
Option<T>A value is present or absent: Some or None.A compositor adapter may be unavailable.
Result<T, E> / ?Success or error / return early if this operation failed.Wayland connection or a settings read can fail.
Arc<dyn Port>Shared ownership of an object implementing that trait.The client and workers share a service adapter; Arc itself does not make arbitrary mutation safe.
Element<Message>An element parameterized by the messages its interactions produce.A button click produces a typed desktop message.
mod / use / ::Declare a module / bring a name into scope / navigate a Rust path.platform::desktop_adapters calls a function in that module.
#[derive(Serialize, Deserialize)]Generate Serde conversion implementations.Convert settings between Rust values and JSON; the adapter still does the file I/O.

You now have the startup and ownership map. The detailed guides below follow the same boundaries, with exact contracts and implementation limits.