Work in progress. A personal medication management application built in Rust.
BitPill helps individuals manage their daily medications — tracking pills, dosages, and schedules in one place.
It is being built with a focus on reliability and correctness, because when it comes to medication, errors matter.
I built BitPill to solve a personal problem: managing my complex medication regimen for a chronic condition.
This was developed because my medications are expensive and if I don't take them correctly, I risk my health and waste money.
I can have convulsions if I miss doses, so I need a reliable way to track when to take each medication and ensure I dont forget.
By building my own, I can tailor it exactly to my needs and ensure it works correctly.
- Rust (edition 2024, stable toolchain)
- just — task runner (
cargo install justor via your package manager)
BitPill is designed to be simple to run locally without a lot of external dependencies.
It uses JSON as the storage format and keeps data in memory for simplicity.
This means there are no database setup steps required.
If you have Rust and just installed you can install all dependency tools with just tools.
BitPill ships a TUI built with ratatui.
A REST API is in WIP state but it’s built with actix-web.
By default just run starts it on port 8080 and also launches the TUI in the foreground.
You can choose to run just one of them if you prefer:
just run-api # REST server only (http://localhost:8080)
just run-tui # Terminal UI onlyThis project was intended to be a terminal application from the start, so the TUI is the primary interface and the REST API is a secondary delivery adapter that still needs work.
To launch it instead of the REST server, replace main.rs with:
The TUI uses a VIM-like modal interface with two main modes:
- Normal mode for navigation.
- Insert mode for typing into form fields.
When you first open the app, you start in Normal mode. Press i to enter Insert mode when a form field is selected, and Esc to return to Normal mode.
| Screen | Key | Action |
|---|---|---|
| Medication list | c |
Open the create-medication form |
| Medication list | j / ↓ |
Move selection down |
| Medication list | k / ↑ |
Move selection up |
| Medication list | q |
Quit |
| Medication list | Enter or v |
Open medication details for selected item |
| Medication details | s |
Open Mark-as-taken selection for today's slots/records |
| Create form | Tab |
Cycle between fields (Name → Amount → Times) |
| Create form | Enter |
Submit the form |
| Create form | Esc |
Cancel and go back |
| Schedule result | any key | Dismiss and go back |
Pressing use h, j, k or l, to navigate between fields, and Enter will submit the form.
You have to be in insert mode to type into form fields.
Press i to enter insert mode when a form field is selected, and Esc to exit back to normal mode.
- Input validation errors (e.g., invalid amount or malformed time slots) are shown in a modal over the current screen. The background is dimmed to focus the modal; press Esc or Enter (or any key) to dismiss and return to the form.
- Shortcuts are contextual: actions such as "mark as taken" are only available on screens that support them (for example,
sfor marking doses is only active inside the Medication Details screen).
wip
just test # full suite with coverage (cargo llvm-cov)Runs formatting check, lint, and tests with coverage in one command:
justjust build # cargo build
just run # REST server (http://localhost:8080)
just run-tui # Terminal UI
just run-api # REST server (background) + TUI (foreground)
just test # tests + coverage report
just lint # cargo clippy -- -D warnings
just fmt # cargo fmt
just fmt-check # formatting check only
just clean # cargo clean
just tools # install rustfmt, clippy, cargo-llvm-covBitPill follows Hexagonal Architecture (Ports & Adapters). Dependencies always point inward — outer layers know about inner layers, never the reverse.
┌──────────────────────────────────────────┐
│ Presentation Layer │
│ (TUI, REST API) │
├──────────────────────────────────────────┤
│ Infrastructure Layer │
│ (Persistence, Clock, Notifications) │
├──────────────────────────────────────────┤
│ Application Layer │
│ (Use-Case Services, Ports) │
├──────────────────────────────────────────┤
│ Domain Layer │
│ (Entities, Value Objects) │
└──────────────────────────────────────────┘
↑ Dependencies point inward ↑
| Layer | Responsibility |
|---|---|
| Domain | Core business rules — Medication, DoseRecord, Dosage, ScheduledTime, etc. Zero external dependencies; pure logic only. |
| Application | Use-case services (CreateMedicationService, MarkDoseTakenService, ScheduleDoseService, ListAllMedicationsService). Defines port traits that infrastructure implements. |
| Infrastructure | Concrete adapters: InMemoryMedicationRepository, InMemoryDoseRecordRepository, SystemClock, ConsoleNotificationAdapter. Wired together in container.rs. |
| Presentation | Delivery adapters — rest/ (actix-web HTTP API) and tui/ (ratatui terminal UI). |
src/
├── domain/
│ ├── entities/ # Medication, DoseRecord
│ └── value_objects/ # Dosage, MedicationId, ScheduledTime, TakenAt, …
├── application/
│ ├── ports/ # Trait definitions + fakes/ (test doubles)
│ └── services/ # Use-case implementations
├── infrastructure/
│ ├── clock/ # SystemClock, SystemScheduledTimeSupplier
│ ├── notifications/ # ConsoleNotificationAdapter
│ ├── persistence/ # InMemoryMedicationRepository, InMemoryDoseRecordRepository
│ └── container.rs # Composition root
└── presentation/
├── rest/ # actix-web server + handlers
└── tui/ # ratatui app + screens + event handling
| Allowed | Forbidden |
|---|---|
presentation → application ✅ |
domain → anything outer ❌ |
presentation → domain ✅ |
application → infrastructure ❌ |
infrastructure → application ✅ |
application → presentation ❌ |
application → domain ✅ |
infrastructure → presentation ❌ |
- Make sure
justand a stable Rust toolchain are installed. - Install dev tools:
just tools - Confirm everything passes before touching any code:
just
- Define the port — create
src/application/ports/my_action_port.rswith aRequest,Response, and atrait MyActionPort: Send + Sync. - Implement the service — create
src/application/services/my_action_service.rs. Inject dependencies viaArc<dyn SomePort>innew(). No I/O allowed here. - Add a fake — add
src/application/ports/fakes/fake_my_repo.rsif the service needs a new repository port. Re-export it fromsrc/application/ports/fakes/mod.rs. - Wire the container — add the concrete adapter (if new) under
src/infrastructure/, then add the service tosrc/infrastructure/container.rs. - Expose in presentation — add a REST handler in
src/presentation/rest/handlers/and/or a TUI screen action.
- One primary type per file. File name = type name in
snake_case. - Unit tests go in a
#[cfg(test)]block at the bottom of the file under test. Use fakes fromcrate::application::ports::fakes, never real I/O. - Integration tests go in
tests/at the crate root and may use real infrastructure adapters. - No magic numbers or strings — use named constants.
- No
Box<dyn Error>in domain or application signatures — use typed error enums withthiserror. - Domain stays pure — no
chrono, nouuid, noasync, no I/O insidesrc/domain/.
This is equivalent to what CI runs:
just # fmt-check + lint + test with coverageAll of these must pass before a contribution is considered complete.