Public
Watch
1
Fork
You've already forked pulse
0
0

Latest commit

maotovisk
feat: fix sampling interval
All checks were successful
release / build (push) Successful in 25s
5ec9781 2026-09-11 12:12:33 -03:00 27 commits
2026-09-10 16:19:25 -03:00
2026-09-11 12:12:33 -03:00
2026-09-10 16:19:25 -03:00
2026-09-11 11:55:28 -03:00

Pulse

A lightweight, visually polished system task manager built from scratch in Odin.

Linux-first. Single binary, GPU-accelerated vector UI, no runtime dependencies beyond SDL3.

Features

  • Apps (default) — searchable, sortable, virtualized tree of opened apps and background processes (subprocesses nest under their group entry, which shows aggregated CPU/memory/GPU), proportional CPU% (share of the whole machine), app icons, per-process GPU%/VRAM, context menus, a bottom action bar and a closable floating detail panel; terminate, force kill and renice behind a confirmation modal
  • Performance — resource rail (System plus one entry per CPU/Memory/GPU device/NIC/storage aggregate, each with a live mini-graph) beside the detail surface: CPU (KPI rings, total + per-core graphs, details with cpufreq governor/driver, temperature), Memory (usage/swap/VRAM KPI rings, memory + swap history graphs, details with DRAM frequency when exposed and DIMM temperature), GPU (per-device panels with utilization/VRAM graphs, engine activity and top GPU processes — click jumps to the app), plus System overview, Network (rate graphs, per-process TCP traffic via sock_diag, sockets, interfaces) and Storage (read/write graphs, devices, mounts)
  • Details — every process as a flat, sortable row (PID, user, CPU, memory, GPU, VRAM, threads, nice, state, command) sharing the Apps filter; double-click or the context menu jumps to the app inspector

Stack

Layer Choice
Language Odin
Windowing / input SDL3
Rendering OpenGL + nanovg (vector)
Text system font (GTK/KDE settings, fontconfig fallback) with embedded Inter as backup; type scale follows the system font size
Data /proc, /sys

Install

Curl the installer and pipe it into a shell:

curl -fsSL https://git.maot.dev/maot/pulse/raw/branch/main/install.sh | sh

It installs runtime dependencies for Arch, Fedora and Ubuntu/Debian, downloads the latest release, verifies its checksum, installs to ~/.local/lib/pulse with a ~/.local/bin/pulse symlink, and registers a desktop entry.

sh install.sh --version v1.2.0   # pin a version
sh install.sh --prefix /opt      # custom prefix
sh install.sh --no-deps          # skip dependency installation
sh install.sh --uninstall        # remove Pulse

Releases & CI

Tagging pushes through the Forgejo Actions workflow in .forgejo/workflows/release.yml:

git tag v0.1.0
git push origin v0.1.0

The workflow builds x86_64 and arm64 on either native runner architecture. Odin emits target object files, then native GCC cross-toolchains link them. There are no QEMU builds, foreign containers, or privileged binfmt setup. Both requested architectures must succeed before the release is published.

The SDK image carries extracted SDL3 link stubs for both architectures (no Mesa/LLVM dependency tree). It caches the pinned native Odin compiler and the five required stb libraries per target. SDL is never compiled. Only SDK input changes rebuild this image; normal releases compile both targets in parallel and package the two binaries offline.

TAG=v0.1.0 sh scripts/ci_build.sh                  # both architectures
TAG=v0.1.0 TARGETS=x86_64 sh scripts/ci_build.sh   # one target

Docker access is required. The SDK cache lives in the runner's Docker daemon; keep that daemon's image storage between jobs. For ephemeral runners, publish an SDK built from ci/Dockerfile to your registry and set the repository variable PULSE_SDK_IMAGE to its immutable reference. Its architecture must match the Docker host (the image itself can compile both release targets). See ci/README.md for cache maintenance and validation.

Release binaries now use system SDL3 3.2.10+, rather than embedding SDL. The installer installs libsdl3-0 on Debian/Ubuntu, SDL3 on Fedora, or sdl3 on Arch. Debian 13 is the reference build environment; older distros without SDL3 packages are no longer supported by these release archives. Existing static-release users must install the SDL3 runtime before using the in-app updater. Building from source remains an option on other environments.

The workflow needs a repository secret FORGEJO_TOKEN with write:repository scope to create releases and upload assets.

The in-app updater picks pulse-<tag>-linux-arm64.tar.gz or pulse-<tag>-linux-x86_64.tar.gz according to the architecture it was built for.

Auto-update

Pulse checks the Forgejo releases API a few seconds after startup (when built with a real PULSE_VERSION), asks before doing anything, then downloads the release tarball, verifies its SHA-256, replaces the binary next to the running executable and relaunches. libcurl is loaded with dlopen, so the app keeps no hard dependency on it.

Build

Requires the Odin compiler and SDL3 development files.

make run            # debug build + run
make release        # optimized build
make check          # type check only
make install-local  # build with the latest git tag and install to ~/.local

make install-local builds a release binary stamped with the latest git tag and installs it with the same layout as install.sh (~/.local/lib/pulse, ~/.local/bin/pulse symlink, desktop entry and icon). Override the prefix with PREFIX=/opt, the tag with PULSE_TAG=v1.2.3, or the XDG data dir with XDG_DATA_HOME=....

If your Odin install ships without prebuilt Linux vendor:stb libraries (needed by nanovg/fontstash), run sh scripts/setup_local_odin_root.sh once; the Makefile picks up the user-local Odin root automatically.

Development hooks

Variable Purpose
PULSE_VIEW=apps|performance|details Open a specific tab
PULSE_PERF=system|cpu|memory|gpu[:N]|network|storage Resource shown in Performance
PULSE_SCREENSHOT=/path.png Render one frame to a PNG and exit
PULSE_SHOT_DELAY=6 Seconds to wait before the screenshot (fills graphs)
PULSE_SCREENSHOT2=/path2.png + PULSE_SHOT_DELAY2=6.3 Capture a second frame from the same run (motion checks)
PULSE_SCROLL=-5 Inject one wheel event after 1.8 s (combine with PULSE_HOVER=x,y)
PULSE_RESIZE=900x600 Resize the window after 1.2 s (layout testing)
PULSE_MAXIMIZED=1 Start maximized
PULSE_SELECT_PID=1 Pre-select a process in the Processes view
PULSE_NO_INPUT=1 Discard input (deterministic screenshots)
PULSE_NATIVE_DECOR=1 Use server-side decorations instead of the CSD header
PULSE_CSD_SHADOW=0 Disable the client-side shadow/rounded corners
PULSE_HITTEST=1 Log hit-test zones at sample points and exit
PULSE_HOVER=x,y Force the mouse position (app coordinates) for captures
PULSE_CLICK=x,y + PULSE_CLICKS=n Inject n clicks at an app-space point
PULSE_FAKE_UPDATE=v9.9.9 Show the update dialog without a real release
PULSE_AUTO_UPDATE=1 / PULSE_AUTO_APPLY=1 Auto-start download / apply (testing)
PULSE_UPDATE_API=URL Override the releases API used for update checks
PULSE_SCALE=1.15 Force UI units per window coordinate (default is exact 1:1 with display scale)

Client-side decorations

The window is borderless and decorated by the app: the tab bar doubles as the title bar. The header (minus tabs and window controls) is a drag region, the outer 6 px are resize edges, and double-clicking the empty header area toggles maximize. All of it goes through SDL_SetWindowHitTest, which SDL3 maps to xdg_toplevel.move / xdg_toplevel.resize on Wayland and to WM messages on X11. Window controls (minimize / maximize-restore / close) live in the top-right of the tab bar.

Compositors do not draw shadows for client-side decorated windows, so Pulse draws its own: the window is created with SDL_WINDOW_TRANSPARENT, a 16 px margin holds a soft shadow and the app is clipped to rounded corners. Enabled by default; set PULSE_CSD_SHADOW=0 to disable on compositors without ARGB support.

Resize bands track the visible app bounds (not the shadow margin) and are disabled entirely while maximized or fullscreen, so the compositor never starts a bogus interactive resize; maximizing also drops the margin, shadow and rounded corners.

Design language

  • Graph cards are full-bleed: the card background is the plot; labels and values are overlaid, never inset into a separate plot area.
  • Graphs are inspectable: hovering any time-series plot shows a crosshair, per-series markers and a compact values-only tooltip.
  • Graphs zoom smoothly: scrolling over a plot zooms the time axis — stretching recent samples for detail or revealing more history — with an animated transition. Each plot remembers its own zoom for the session, including individual network/storage cards and CPU cores. Percentage plots (CPU, per-core, memory, GPU, VRAM) are pinned to a fixed 0100% vertical range; the temperature plot runs from 0° up to the highest reading seen this session.
  • Process tables (Processes tab and Overview top list) share the same renderer: app icons resolved from .desktop entries with deterministic letter avatars as fallback, CPU share bars, and consistent columns. The Processes tab groups processes into an Applications forest (one root per installed app that resolves to a .desktop entry; helper processes such as Web Content / RDD Process nest under it) and a Background processes forest; expandable group entries show subtree aggregates and stay virtualized.
  • Destructive process actions (stop / force stop) go through a confirmation modal with a dim backdrop and scale/fade transitions; right-clicking a row opens a shared context menu (context_menu.odin).
  • Every table (processes, connections, devices, mounts, interfaces) uses the same list.odin component: card surface, scrollable rows with a slim auto-hiding scrollbar (draggable / click-to-jump), a matching horizontal bar when columns overflow (Shift+wheel or horizontal wheel pans), and columns that can be resized (drag the grip shown when hovering a column edge, EW cursor) and reordered (drag the header).
  • Sortable columns cycle on click: order by the column, reverse it, then clear back to the listing's default order (CPU for the process tables).
  • Side-by-side cards are separated by draggable splitters (split.odin): devices/mounts and the process table/detail panel; the network view stacks connections over interfaces with a horizontal splitter.
  • Few, flat panels; hairlines instead of stacked cards.
  • System-font sizing with a readable 16 px minimum body size, a 15% larger layout, and desktop display scaling applied consistently to rendering, input and window decorations. Pixel density is handled separately to avoid double scaling.

Data pipeline

  • Meters (CPU/RAM/GPU/disk/network) are sampled every 200 ms; the process table, mount capacity and DRM engine counters are rescanned every 1 s and reused in between. Process trees and sorted lists rebuild only when that slower snapshot changes or the user changes their ordering.
  • Redraws are vsynced to the monitor's refresh rate; minimized windows skip rendering and release their frame resources.
  • GPU engines use DRM fdinfo from accessible clients, so they may not include workloads owned by other users. Shared clients are deduplicated per PCI device; engine capacity is respected. A dash means a counter is unavailable, while 0% is a measured idle engine. Scroll over the GPU stats column to reach additional adapters in small windows.
  • The UI renders linear interpolation between the two newest raw samples, so values are exact at sample boundaries and move smoothly in between.
  • Charts use a fixed pixel-per-interval time axis anchored to the right edge: samples scroll left at a constant rate and never re-scale as the ring buffer fills. Scrolling over a plot changes how many samples are visible (temporal zoom); the ring buffer holds ~4 minutes at the 200 ms cadence.

Status

M0 (shell), M1 (sampler + overview), M2 (processes) and the socket-binding half of M3 are complete, including CSD with client-side shadows, app icons, per-process GPU/VRAM, a Forgejo release pipeline, the curl installer and in-app auto-update. Next: per-socket TCP byte counters (sock_diag), settings, and the privileged helper for system-wide visibility.

Run make check test for type checking and the zoom, history, DPI and DRM accounting regression tests.