mps
A plain-text personal productivity CLI. Append journal entries, tasks, reminders, logs, and notes to daily .mps files; search, list, and export them; sync via git. Single binary, no runtime dependencies.
Rust rewrite of mps (Ruby). Fully backward-compatible with all Ruby-generated files and config.
Install
The installed binary is named mps.
From source
Quick start
Element types
| Type | Description |
|---|---|
task |
Something to do, with open/done status |
note |
Free-form observation or thought |
reminder |
Time-anchored alert |
log |
Timed work block (start/end time → duration) |
character |
Running monologue about a person |
Commands
mps [open] [DATE]
Open the .mps file for DATE in $EDITOR (falls back to vim). Creates the file if absent.
mps list [DATE]
Print elements for DATE as an indented tree.
| Flag | Short | Description |
|---|---|---|
--type TYPE |
-t |
Filter by type: task, note, log, reminder, character |
--tag TAG |
-g |
Filter by tag name |
--status STATUS |
-s |
Filter tasks by open or done (hides all non-task elements) |
--name NAME |
-n |
Filter character entries by person name |
--since DATE |
-S |
Show elements from DATE up to the target date |
--refs |
-r |
Show human-readable ref column (task-1, mps-1.2, …) |
--all |
-a |
List across the entire archive, not just one date |
mps append TYPE BODY [FLAGS]
Append one element to today's file without opening an editor. TYPE is resolved through type_aliases from config (e.g. t → task).
| Flag | Description |
|---|---|
--tags t1,t2 |
Comma-separated tags |
--status open|done |
Task status (default: open) |
--at TIME |
Time for reminders (5pm, 10:30, …) |
--start-time HH:MM |
Start time for logs |
--end-time HH:MM |
End time for logs |
--name NAME |
-n Person name for character entries |
mps edit REFPATH [--date DATE]
Open an element's body text in $EDITOR. The body is written to a temp file; on save the original file is updated atomically. No-op if the content is unchanged.
mps delete REFPATH [FLAGS]
Remove an element entirely from its file. Prompts for confirmation unless --yes is given.
| Flag | Short | Description |
|---|---|---|
--yes |
-y |
Skip confirmation prompt |
--date DATE |
-d |
Date context for human refs (default: today) |
mps update REFPATH [FLAGS]
Update one element's attributes in-place. REFPATH is a human ref (task-1, mps-1.2) or an epoch ref (20260428.1732500287.1).
| Flag | Description |
|---|---|
--status open|done |
Set task status |
--start-time HH:MM |
Set log start time |
--end-time HH:MM |
Set log end time |
--at TIME |
Set reminder time |
--date DATE |
-d Date context for human refs (default: today) |
mps done REFPATH [--date DATE]
Shorthand for mps update REFPATH --status done.
mps search QUERY [FLAGS]
Full-text search across all .mps files. Returns matching elements with date and ref.
| Flag | Short | Description |
|---|---|---|
--type TYPE |
-t |
Filter by element type |
--tag TAG |
-g |
Filter by tag |
--name NAME |
-n |
Filter character entries by person name |
--since DATE |
-S |
Search from DATE onward |
mps stats [DATE] [FLAGS]
Show element counts and total log durations.
| Flag | Short | Description |
|---|---|---|
--since DATE |
-S |
Stats from DATE up to target date |
--all |
-a |
Stats across the entire archive |
mps tags [DATE] [FLAGS]
Show tag usage as a frequency bar chart.
| Flag | Short | Description |
|---|---|---|
--type TYPE |
-t |
Count tags for this element type only |
--status STATUS |
-s |
Restrict to tasks with this status |
--name NAME |
-n |
Restrict to character entries for this person |
--since DATE |
-S |
Tags from DATE up to target date |
--all |
-a |
Count across the entire archive |
mps export [DATE] [FLAGS]
Export elements to stdout as JSON or CSV.
| Flag | Short | Description |
|---|---|---|
--format json|csv |
-f |
Output format (default: json) |
--type TYPE |
-t |
Filter by element type |
--since DATE |
-S |
Export from DATE up to target date |
CSV columns: date, ref, type, tags, body, status, at, start, end, name
mps config [show|edit]
View or edit configuration.
mps notify [FLAGS]
Check for due reminders and open tasks, then send desktop notifications via notify-send.
| Flag | Description |
|---|---|
--dry-run |
Print what would be sent without actually sending |
--window MINS |
Override the due-now window in minutes (default: from config) |
--force |
Fire even if already notified within the window |
Reminders fire when the current time falls within window_minutes of the reminder's --at time. Each reminder is deduplicated by its ref so it won't re-fire until the cooldown has elapsed.
Open-task briefing fires once per day at task_notify_at time (e.g. "9am"), listing all open tasks from today plus up to overdue_days past days.
Notification settings live in ~/.mps_config.yaml under the notify: key or in .mps.meta for cross-device sync (see Meta config).
mps daemon SUBCOMMAND
Manage a systemd user timer that calls mps daemon run (= mps notify) once per minute.
| Subcommand | Description |
|---|---|
install |
Write unit files, enable and start the timer |
remove |
Stop and disable the timer, remove unit files |
status |
Pass-through to systemctl --user status mps-notify.timer |
run |
Run one notify tick (invoked by systemd; also safe to call manually) |
Unit files are written to ~/.config/systemd/user/. .mps.local (notification history) is automatically added to .gitignore on install so it stays off git.
mps meta [SUBCOMMAND]
Inspect or edit the .mps.meta sidecar config file.
| Subcommand | Description |
|---|---|
show (default) |
Pretty-print .mps.meta and .mps.local |
edit |
Open .mps.meta in $EDITOR |
clear |
Delete .mps.local (notification history + cache) |
mps git ARGS / mps autogit / mps cmd ARGS
Run git or shell commands inside the storage directory.
mps version
Print the version string.
File format
Files are named YYYYMMDD.<epoch>.mps (or YYYYMMDD.mps for files without an epoch). The format is identical to the Ruby gem — no migration needed.
@task[work, release, status: done]{
Ship the API refactor
}
@note{
The auth token expiry edge case only appears under concurrent load
}
@reminder[at: 3pm]{
Team standup
}
@log[work, start: 09:00, end: 11:30]{
Debugging the auth flow
}
@character[name: Dr. Alice, mentor, trusted]{
Explained the layered caching approach in detail.
Would consult again for architecture decisions.
}
@mps[sprint-42]{
@task[backend]{
Nested task inside a sprint block
}
@note{
Retrospective note
}
}
Brackets are optional — @task{ body } is valid. Tags and named attributes share the bracket: [tag1, tag2, status: done, at: 5pm].
Date formats
Accepted everywhere a DATE is expected:
| Input | Meaning |
|---|---|
today |
Today |
yesterday |
Yesterday |
monday … sunday |
Most recent occurrence of that weekday |
last friday |
The Friday before the most recent one |
3 days ago |
3 days before today |
last week |
7 days ago |
20260421 |
Explicit YYYYMMDD |
2026-04-21 |
Explicit YYYY-MM-DD |
Configuration
Config file: ~/.mps_config.yaml. Created automatically on first run. The same file written by the Ruby gem is accepted without changes (Ruby symbol-key YAML like :storage_dir: is handled transparently).
mps_dir: /home/you/.mps
storage_dir: /home/you/.mps/mps
log_file: /home/you/.mps/mps.log
git_remote: origin
git_branch: master
default_command: list # command run by bare `mps` invocation (open or list)
# Short-hand element type aliases (also accepts legacy key: aliases)
type_aliases:
t: task
n: note
r: reminder
l: log
c: character
# Short-hand command aliases
command_aliases:
a: append
"+": append
s: search
l: list
# Canonical tag list synced across devices via .mps.meta
custom_tags:
- work
- personal
- urgent
# Desktop notification settings
notify:
enabled: true
window_minutes: 5 # minutes either side of a reminder time counts as "due"
task_notify_at: "9am" # morning briefing time; omit to disable open-task briefing
notify_open_tasks: true
open_task_tags: # if non-empty, only tasks with these tags are included
task_cooldown_minutes: 60 # min gap between repeat notifications for the same reminder
overdue_days: 7 # how many past days to scan for overdue open tasks
With the above config:
mps a t "Fix the bug" --tags work→ appends a task (a→append,t→task)mps + n "Interesting observation"→ appends a notemps s "auth"→ searches for "auth"
Override the config path:
# or
MPS_CONFIG=/path/to/other.yaml
Meta config
.mps.meta is a JSON sidecar file at ~/.mps/mps/.mps.meta that is git-tracked and synced across all your devices via mps autogit. It acts as a second config layer — machine-agnostic settings you want consistent everywhere.
Fields supported (union-merged with ~/.mps_config.yaml at startup; meta wins for scalars, maps are unioned with YAML taking priority on key conflicts):
Edit it directly with mps meta edit. Inspect the current state with mps meta show.
.mps.local (also in ~/.mps/mps/) holds per-device transient state (notification history, cache). It is gitignored and never committed.
Architecture
src/
main.rs Entry point — alias pre-processing, clap dispatch
cli.rs Cli + Commands (#[derive(Parser)])
config.rs Config struct; YAML load/init; Ruby symbol-key normalization
meta.rs MetaShared (.mps.meta) + MetaLocal (.mps.local) structs
time_parse.rs parse_time() — "5pm", "9:30am", "17:00", "noon"
constants.rs Filename regexes, new_file_name()
date_parse.rs parse_date() — natural-language + absolute formats
error.rs MpsError (thiserror)
parser.rs Position-based stack parser; mirrors Ruby Engines::Parser
ref_resolver.rs Bidirectional epoch ↔ human ref (task-1, mps-1.2)
store.rs Store — all filesystem I/O; append, parse, search, rewrite
elements/ Element enum + per-type Data structs
commands/ One module per command + shared display helpers
Two sidecar files live in storage_dir (~/.mps/mps/):
| File | Tracked | Purpose |
|---|---|---|
.mps.meta |
Git-tracked | Second config layer — aliases, notify settings, custom tags |
.mps.local |
Gitignored | Per-device state — notification history, cache |
Requirements
- Rust 1.70+
git(forgit/autogitcommands)$EDITORor$VISUAL(foropen/config edit/meta edit; falls back tovim)notify-send(formps notify/mps daemon— Linux desktop notifications; optional)
License
MIT