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 |
You can add .desktop file to ~/.config/autostart/ directory:
[Desktop Entry]
Type=Application
Name=RXTT
Exec=/home/<user>/.cargo/bin/rxtt daemon
Terminal=false
You can change path depending from where rxtt is installed.
| 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 active window spans and unlogged periods |
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). Creates a missing file with default rules on first run, without overwriting an existing file |
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, and representable as a duration |
--sample-interval SECONDS |
0.25 |
Seconds between safety focus/idle polls. Must be finite, positive after nanosecond conversion, and fit the polling limit (2,147,483,647 ms) |
--title-interval SECONDS |
5 |
Minimum seconds between emitted title-change events for one active window (throttle); finite, >= 0, representable as a duration |
--no-idle |
off | Disable idle detection. Track focused windows only (no idle intervals) |
Invalid or overflowing duration arguments are rejected during CLI parsing with exit code 2. Fractional seconds remain supported.
# 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.
Below the heading, Unlogged: <duration> shows elapsed time with no completed active or idle record, including gaps before the first record and after the last. Recorded idle time is logged and does not count toward this total. Use --verbose to list each unlogged period with its local start/end times and duration. Future time is excluded; an initialized database with no records makes the entire elapsed selection unlogged. A missing database is an error (exit code 1); reporting does not create it or its parent directories.
| 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) |
--verbose |
off | List each unlogged period beneath the unlogged total |
--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
# Show exactly when time was unlogged
rxtt report --verbose
# 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 and unlogged periods. Active spans show local start/end time, application, exact stored title, and duration. Unlogged periods show local start/end time, Unlogged, and duration. Dates appear for multi-day selections and spans crossing midnight.
Like report, workflow requires an existing, initialized database and exits with code 1 if it is missing.
Adjacent spans with identical app + raw title merge. Recorded idle time is omitted but does not count as unlogged. Gaps include the leading and trailing elapsed time in the selection, stopping at now. 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) |
--verbose |
off | Include unlogged entries and entry kinds in JSON; text already includes gaps |
Default JSON remains an array of active entries: { "app_id", "title" (or null), "started_at" (unix), "ended_at" (unix), "duration_seconds" }.
With --json --verbose, the array includes unlogged periods in chronological order. Active entries gain "kind": "active"; unlogged entries have no application/title fields:
{"kind": "unlogged", "started_at": 1789456500, "ended_at": 1789458300, "duration_seconds": 1800}# Chronological view for two weeks
rxtt workflow --since 2026-07-01 --until 2026-07-16
# JSON export with unlogged periods
rxtt workflow --json --verbose
# Active-only JSON export
rxtt workflow --since 2026-07-01 --json > workflow.json# rxtt report --verbose --no-ansi
Usage report for 2026-09-15 (1h 00m 00s/2h 00m 00s)
Unlogged: 30m 00s
01:00:00–01:30:00 Unlogged 30m 00s
Alacritty 1h 00m 00s
# rxtt workflow --no-ansi
Workflow for 2026-09-15
00:00:00–01:00:00 Alacritty "rxtt" 1h 00m 00s
01:00:00–01:30:00 Unlogged 30m 00s
This example has 30 minutes of recorded idle time after the gap. “Unlogged” describes missing completed records, not proof that the computer was offline. The daemon keeps its current interval in memory until it finishes, so that unfinished interval temporarily appears unlogged. Shutdowns, tracker downtime, and lost unfinished intervals can also leave gaps. These commands infer gaps without changing stored data.
Trace 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: The daemon initializes SQLite with WAL mode, foreign keys ON, a 5s busy timeout, and versioned migrations (PRAGMA user_version = 1).
report and workflow open existing databases read-only, validate schema version 1, and read each result within one snapshot transaction. They do not initialize or migrate databases or change journal mode. SQLite may still use WAL coordination files when reading a running daemon’s database. Configuration-file initialization remains independent of database access.
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) |
-
Terminal text: Titles, application identifiers, diagnostic values, and other external text escape terminal control characters and Unicode bidi controls before styling. Newlines, carriage returns, and tabs appear as
\n,\r, and\t; other controls appear as\u{...}. Backslashes and enclosing quotes are escaped consistently. Ordinary Unicode and emoji remain readable. Stored metadata and JSON values retain their raw text. -
X11 metadata limits: Each title, window-manager name, or
WM_CLASSproperty is limited to 64 KiB. Oversized or incorrectly formatted properties are treated as unavailable, not stored as truncated prefixes. Existing fallback paths still apply, such asWM_NAMEwhen_NET_WM_NAMEis unavailable. Scalar window/PID properties read the first 32-bit value and ignore trailing values for window-manager compatibility; the supported-atom list is limited to 16,384 atoms, with incomplete lists treated as unavailable. -
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.