# 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)](https://github.com/mash-97/mps). Fully backward-compatible with all Ruby-generated files and config.
---
## Install
```bash
cargo install mps-rs
```
The installed binary is named `mps`.
### From source
```bash
git clone https://github.com/mash-97/mps-rs
cd mps-rs
cargo install --path .
```
---
## Quick start
```bash
mps # open today's file in $EDITOR
mps list # print today's elements
mps list --since monday # all elements since last Monday
mps append task "Fix the auth bug" --tags work,backend
mps append note "The edge case only appears under load"
mps append reminder "Team standup" --at 3pm
mps append log "Deep-work session" --start-time 09:00 --end-time 11:30
mps append character "Helped me think through the design" --name "Dr. Alice" --tags mentor
mps done task-1 # mark task-1 done
mps edit note-1 # rewrite note-1's body in $EDITOR
mps delete task-3 --yes # delete task-3 without confirmation
mps search "auth" --since "last week"
mps stats --since monday
mps export --format csv --since "20260501" > may.csv
mps autogit # stage + commit + pull + push
mps notify --dry-run # preview due reminders + open tasks
mps daemon install # enable minutely background checker
mps meta show # inspect cross-device config layer
```
---
## Element types
| `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.
```bash
mps # today
mps open yesterday
mps open last friday
```
---
### `mps list [DATE]`
Print elements for DATE as an indented tree.
| `--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 |
```bash
mps list
mps list --type task --status open
mps list --since monday --refs
mps list --all --name "Dr. Alice"
```
---
### `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`).
| `--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 |
```bash
mps append task "Review the PR" --tags work,backend
mps append note "Cache invalidation approach"
mps append reminder "1:1 with manager" --at 2pm
mps append log "Debugging session" --start-time 14:00 --end-time 16:30
mps append character "Explained the system clearly" --name "Mahfuz Vai" --tags mentor,work
mps append task "Write tests" --status done --tags ci
```
---
### `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.
```bash
mps edit task-1
mps edit note-2 --date yesterday
mps edit 20260601.1779000000.1 # epoch ref works too
```
---
### `mps delete REFPATH [FLAGS]`
Remove an element entirely from its file. Prompts for confirmation unless `--yes` is given.
| `--yes` | `-y` | Skip confirmation prompt |
| `--date DATE` | `-d` | Date context for human refs (default: today) |
```bash
mps delete task-1
mps delete note-3 --yes
mps delete task-2 --date yesterday
```
---
### `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`).
| `--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) |
```bash
mps update task-1 --status done
mps update mps-1.2 --end-time 17:00
mps update task-3 --status done --date yesterday
```
---
### `mps done REFPATH [--date DATE]`
Shorthand for `mps update REFPATH --status done`.
```bash
mps done task-1
mps done task-2 --date yesterday
```
---
### `mps search QUERY [FLAGS]`
Full-text search across all `.mps` files. Returns matching elements with date and ref.
| `--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 |
```bash
mps search "auth"
mps search "design" --type character --name "Dr. Alice"
mps search "deploy" --since "last week" --type log
```
---
### `mps stats [DATE] [FLAGS]`
Show element counts and total log durations.
| `--since DATE` | `-S` | Stats from DATE up to target date |
| `--all` | `-a` | Stats across the entire archive |
```bash
mps stats
mps stats --since monday
mps stats --all
```
---
### `mps tags [DATE] [FLAGS]`
Show tag usage as a frequency bar chart.
| `--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 |
```bash
mps tags
mps tags --all --type task --status open
mps tags --since monday --name "Dr. Alice"
```
---
### `mps export [DATE] [FLAGS]`
Export elements to stdout as JSON or CSV.
| `--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`
```bash
mps export --format json
mps export --format csv --since "20260501" > may.csv
mps export --since monday --type log
```
---
### `mps config [show|edit]`
View or edit configuration.
```bash
mps config # same as mps config show
mps config show
mps config edit # opens config file in $EDITOR
```
---
### `mps notify [FLAGS]`
Check for due reminders and open tasks, then send desktop notifications via `notify-send`.
| `--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.
```bash
mps notify --dry-run
mps notify --force # re-fire even if already sent
mps notify --window 10 # 10-minute window
```
Notification settings live in `~/.mps_config.yaml` under the `notify:` key or in `.mps.meta` for cross-device sync (see [Meta config](#meta-config)).
---
### `mps daemon SUBCOMMAND`
Manage a systemd user timer that calls `mps daemon run` (= `mps notify`) once per minute.
| `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) |
```bash
mps daemon install
mps daemon status
mps daemon remove
```
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.
| `show` (default) | Pretty-print `.mps.meta` and `.mps.local` |
| `edit` | Open `.mps.meta` in `$EDITOR` |
| `clear` | Delete `.mps.local` (notification history + cache) |
```bash
mps meta # same as mps meta show
mps meta edit
mps meta clear
```
---
### `mps git ARGS` / `mps autogit` / `mps cmd ARGS`
Run git or shell commands inside the storage directory.
```bash
mps git status
mps git auto # add . + commit + pull + push
mps git autocommit # add . + commit only
mps autogit # same as mps git auto
mps cmd ls -la # any shell command in storage dir
```
---
### `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:
| `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).
```yaml
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 note
- `mps s "auth"` → searches for "auth"
Override the config path:
```bash
mps --config-path /path/to/other.yaml list
# or
MPS_CONFIG=/path/to/other.yaml mps list
```
---
## 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):
```json
{
"version": 1,
"config": {
"type_aliases": { "t": "task", "c": "character" },
"command_aliases": { "a": "append", "+": "append" },
"default_command": "list",
"custom_tags": ["work", "personal", "urgent", "health"],
"notify": {
"enabled": true,
"window_minutes": 5,
"task_notify_at": "9am",
"notify_open_tasks": true,
"open_task_tags": [],
"task_cooldown_minutes": 60,
"overdue_days": 7
}
}
}
```
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/`):
| `.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` (for `git` / `autogit` commands)
- `$EDITOR` or `$VISUAL` (for `open` / `config edit` / `meta edit`; falls back to `vim`)
- `notify-send` (for `mps notify` / `mps daemon` — Linux desktop notifications; optional)
---
## License
MIT