X11 activity tracker. Records focused window intervals to SQLite. Generates usage reports and workflow timelines.
- Linux with X11 (Wayland not supported)
- Tested: Debian 13 + XFCE, single monitor
- Requires: X11 server, EWMH
_NET_ACTIVE_WINDOW, XScreenSaver extension (for idle detection)
# rxtt report --no-tree --no-ansi
Usage report for 2026-07-17 (3h 53m 04s/12h 39m 26s)
Alacritty 1h 30m 52s
Chromium 47m 11s
firefox 39m 40s
Spotify 26m 41s
TelegramDesktop 20m 26s
Free Download Manager 6m 37s
Xfdesktop 26s
Throne 20s
Xfce4-clipman-history 11s
# rxtt report --no-ansi
Usage report for 2026-07-17 (3h 53m 04s/12h 39m 26s)
Alacritty 1h 32m 45s
├─ "rxtt" 37m 36s
├─ "ilya@ilya:~/projects/rust/rxtt" 19m 06s
├─ "π - rxtt" 9m 12s
├─ "nvim README.md" 3m 43s
├─ "nvim ~/.config/rxtt/config.toml" 3m 09s
# and so on...
Grab the pre-compiled binaries from the latest release page.
Prerequisites:
- Rust toolchain (edition 2024)
- X11 development headers (
libx11-devon Debian/Ubuntu,libX11-develon Fedora)
git clone <repo> && cd rxtt
cargo build --release
# or, build statically linked binaries using glibc or musl (make required)
make build-static-glibc
make build-static-musl # require musl-toolsBinary lands at target/release/rxtt. Install globally:
cargo install --path .Dependencies:
| Crate | Purpose |
|---|---|
clap (derive) |
CLI parsing |
x11rb (+screensaver) |
X11 connection, EWMH atoms, idle queries |
rusqlite (bundled) |
SQLite storage (sqlite3 bundled, no system dep) |
chrono |
Date/time handling |
regex |
Title grouping patterns |
toml + serde |
Config file parsing |
unicode-normalization |
Title normalization (NFKC, format-mark removal) |
signal-hook |
SIGINT/SIGTERM handling |
anstyle |
ANSI terminal styling |
nix (poll) |
poll() for X11 event loop |
| Command | Description |
|---|---|
rxtt daemon |
Foreground tracker. Watches X11 focus/idle, writes intervals to SQLite |
rxtt report |
Usage totals grouped by application and title for a date range |
rxtt workflow |
Chronological sequence of active window spans |
rxtt title-test |
Trace how a raw title normalizes through grouping rules |
rxtt probe |
Diagnostic: print X11/EWMH/XScreenSaver support and current active window |
| Flag | Description |
|---|---|
--config PATH |
TOML config path (default: $XDG_CONFIG_HOME/rxtt/config.toml or ~/.config/rxtt/config.toml). Auto-creates file with default rules on first run |
Run foreground. Watches _NET_ACTIVE_WINDOW property changes and XScreenSaver idle state. Writes completed intervals to SQLite. Streams status events to stdout. Stops on Ctrl+C (SIGINT) or SIGTERM.
Only completed intervals persist. If the process dies mid-interval, in-memory data for that interval is lost.
| Flag | Default | Description |
|---|---|---|
--database PATH |
$XDG_STATE_HOME/rxtt/activity.sqlite3 (or ~/.local/state/rxtt/activity.sqlite3) |
SQLite database path |
--idle-threshold SECONDS |
300 (5 min) |
Seconds without input before entering idle state. Must be finite, >= 0 |
--sample-interval SECONDS |
0.25 |
Seconds between safety focus/idle polls. Must be finite, > 0 |
--title-interval SECONDS |
5 |
Minimum seconds between emitted title-change events for one active window (throttle) |
--no-idle |
off | Disable idle detection. Track focused windows only (no idle intervals) |
# Default: 5 min idle threshold, 5 s title throttle
rxtt daemon
# Custom database, no idle tracking
rxtt daemon --database /tmp/activity.sqlite3 --no-idle
# Faster polling, longer idle
rxtt daemon --idle-threshold 600 --sample-interval 0.5- Subscribe to
_NET_ACTIVE_WINDOWproperty changes - Subscribe to current window's title change events
- Reconcile current state (active/idle) → start interval if needed
- Loop: drain X11 events → safety poll → flush pending title update → reconcile
- On shutdown: finish current interval → dispatch completes → exit 0
Print usage totals grouped by application. Shows title tree (collapsed equivalent titles) beneath each app.
Date range defaults to today. Heading shows (active usage / elapsed selected range). When range includes today, elapsed ends at current time.
| Flag | Default | Description |
|---|---|---|
--database PATH |
same as daemon | SQLite database path |
--since YYYY-MM-DD |
today | First included calendar date (inclusive) |
--until YYYY-MM-DD |
today | Last included calendar date (inclusive) |
--no-tree |
off | Hide title branches below each application |
--no-group-titles |
off | Show exact stored titles instead of grouping equivalents |
--no-ansi |
off | Disable ANSI styling (use when redirecting to file) |
Grouping merges titles that differ only in formatting or volatile UI state:
- Universal normalization: NFKC unicode, lowercase, remove format marks, normalize curly quotes, collapse whitespace
- App-specific regex rules: Strip browser tab badges (
(5) Inbox→inbox), Telegram unread counters, terminal spinner prefixes, etc.
Rules run in file order. Every replacement in a rule runs in order. See Config for details.
# Today
rxtt report
# Two-week range with title tree
rxtt report --since 2026-07-01 --until 2026-07-16
# Flat output, exact titles, no ANSI (for piping)
rxtt report --since 2026-07-01 --until 2026-07-16 --no-tree --no-group-titles --no-ansiPrint chronological active window spans. Each span shows: local start/end time, application, exact stored title, duration.
Adjacent spans with identical app + raw title merge. Idle time omitted. Title changes shown at daemon's --title-interval resolution.
| Flag | Default | Description |
|---|---|---|
--database PATH |
same as daemon | SQLite database path |
--since YYYY-MM-DD |
today | First included calendar date (inclusive) |
--until YYYY-MM-DD |
today | Last included calendar date (inclusive) |
--no-ansi |
off | Disable ANSI styling |
--json |
off | Export as JSON array (suppresses heading/ANSI) |
Each entry: { "app_id", "title" (or null), "started_at" (unix), "ended_at" (unix), "duration_seconds" }
# Chronological view for two weeks
rxtt workflow --since 2026-07-01 --until 2026-07-16
# JSON export
rxtt workflow --since 2026-07-01 --json > workflow.jsonTrace title normalization pipeline. Shows universal normalization step, then every app rule (matched/skipped) with per-replacement input/output.
No database needed. Uses same config and pipeline as report.
| Positional | Description |
|---|---|
APP_ID |
Application identifier (matched against rule match regex) |
TITLE |
Raw window title to normalize |
| Flag | Description |
|---|---|
--expect TITLE |
Expected final title. Prints PASS/FAIL and exits 0/1. Useful for shell regression |
# Trace normalization
rxtt title-test Firefox '(33) Inbox'
# Regression check (exits nonzero on mismatch)
rxtt title-test Firefox '(33) Inbox' --expect inboxOne-shot diagnostic. Connects to X11, checks EWMH _NET_ACTIVE_WINDOW and XScreenSaver support, prints active window info and any warnings.
No arguments beyond --config.
rxtt probeFirst rxtt run creates $XDG_CONFIG_HOME/rxtt/config.toml (or ~/.config/rxtt/config.toml). Use --config PATH with any command to select alternate file.
Format: TOML. Invalid TOML, wrong version, or bad regex → warning + built-in defaults for that run.
version = 1
[[title_grouping.apps]]
match = "(?i)my-app"
replacements = [
{ pattern = '^\[\d+%\]\s*', replacement = "" },
]| Field | Type | Description |
|---|---|---|
version |
integer | Optional. Default - latest config version, now '1' |
title_grouping.apps |
array | Ordered list of app rules |
apps[].match |
regex | Rust regex pattern matched against application ID |
apps[].replacements |
array | Ordered list of { pattern, replacement } |
replacements[].pattern |
regex | Pattern to match (Rust regex syntax) |
replacements[].replacement |
string | Replacement string. Supports $1, ${name} capture refs |
| App match | What it strips |
|---|---|
| Firefox, Chromium, Chrome, Brave | Tab count *, (N), N · , Telegram-style message counters |
| Telegram | Unread counters (N), inline - (N) |
| Terminals (Alacritty, Kitty, WezTerm, GNOME Terminal, Konsole, Terminator, Tilix, Foot, Xterm) | Block-element spinner chars, [!] / [·] status prefixes |
Rules run in file order. Multiple rules can match same app (all replacements chain).
SQLite file at ~/.local/state/rxtt/activity.sqlite3 (or --database path).
Settings: WAL mode, foreign keys ON, 5s busy timeout, versioned migrations (PRAGMA user_version = 1).
activity_interval — completed intervals
| Column | Type | Notes |
|---|---|---|
id |
INTEGER PK | Auto-increment |
tracker_interval_id |
INTEGER | Unique per daemon session |
state |
TEXT | 'active' or 'idle' |
started_at |
INTEGER | Unix epoch seconds |
ended_at |
INTEGER | Unix epoch seconds |
start_reason |
TEXT | e.g. "startup", "_NET_ACTIVE_WINDOW changed" |
end_reason |
TEXT | e.g. "shutdown", "periodic sample; idle=305.2s" |
initial_window_id |
INTEGER | X11 window ID |
initial_title |
TEXT | Window title at interval start |
initial_wm_instance |
TEXT | WM_INSTANCE_NAME |
initial_wm_class |
TEXT | WM_CLASS class |
initial_pid |
INTEGER | Process ID |
initial_executable |
TEXT | Executable path |
app_id |
TEXT | Derived application identifier |
window_metadata_change — title/metadata updates during interval
| Column | Type | Notes |
|---|---|---|
id |
INTEGER PK | Auto-increment |
interval_id |
INTEGER FK | → activity_interval.id (CASCADE delete) |
observed_at |
INTEGER | Unix epoch seconds |
window_id |
INTEGER | X11 window ID |
title |
TEXT | Updated title |
wm_instance |
TEXT | WM_INSTANCE_NAME |
wm_class |
TEXT | WM_CLASS class |
pid |
INTEGER | Process ID |
executable |
TEXT | Executable path |
app_id |
TEXT | Derived application identifier |
activity_title_segment — view for title-level reporting
Splits each active interval into contiguous title segments. Starts from initial title, ends each segment at next metadata change or interval end. Exposes duration_seconds per segment.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Runtime error (storage fail, dispatcher crash) |
| 2 | Configuration error (invalid path, bad dates, X11 init fail) |
- WAL mode: Database readable while daemon runs (concurrent reads safe)
- No recovery: Unfinished intervals on crash are not recovered
- Title throttle:
--title-intervalcontrols resolution. Shorter = more granular title tracking, more DB rows
Project is licensed under the BSD-3-Clause license.