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.
Run with just run-tui or cargo run --release.
The REST API is under development and not yet released. To enable it locally:
cargo build --features rest-apiBitPill ships a TUI built with ratatui.
This 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.
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).
Status: WIP — The REST API is under development and not yet ready for production use.
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 # cargo run --release (TUI)
just test # tests + coverage report
just lint # cargo clippy -- -D warnings
just fmt # cargo fmt
just fmt-check # formatting check only
just lint-workflows # validate .github/workflows/*.yml with actionlint
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) │
├──────────────────────────────────────────┤
│ 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 — tui/ (ratatui terminal UI, WIP: rest/ actix-web HTTP API) |
src/
├── domain/
│ ├── entities/ # Medication, DoseRecord
│ └── value_objects/ # Dosage, MedicationId, ScheduledTime, TakenAt, …
├── application/
│ ├── dtos/ # Request/response DTOs
│ │ ├── requests.rs
│ │ └── responses.rs
│ ├── ports/ # Trait definitions + fakes/ (test doubles)
│ │ ├── inbound/
│ │ ├── outbound/
│ │ └── fakes/
│ └── services/ # Use-case implementations
├── infrastructure/
│ ├── clock/ # SystemClock, SystemScheduledTimeSupplier
│ ├── notifications/ # ConsoleNotificationAdapter
│ ├── persistence/ # JSON repositories
│ └── container.rs # Composition root
└── presentation/
└── 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 ❌ |
src/application/
├── dtos/
│ ├── requests.rs # All request DTOs (one file)
│ └── responses.rs # All response DTOs (one file)
├── ports/
│ ├── inbound/ # Port traits (one per file)
│ ├── outbound/ # Repository/trait ports
│ └── fakes/ # Test doubles
└── services/ # Use-case implementations
- Add DTOs — add
RequestandResponsestructs todtos/requests.rsanddtos/responses.rs. - Define the port — create
src/application/ports/my_action_port.rswith a trait. - Implement the service — create
src/application/services/my_action_service.rs. No I/O allowed. - Add a fake — create test doubles in
src/application/ports/fakes/. - Wire the container — add concrete adapters in
src/infrastructure/, then wire incontainer.rs. - Expose in presentation — add a TUI handler (REST is WIP).
- One primary type per file — filename matches the type.
- DTOs in one file — all requests in
requests.rs, all responses inresponses.rs. - Imports grouped at file top — use the
crate::application::{ ... }pattern. - Unit tests — in
#[cfg(test)]at bottom of source file. - Integration tests — in
tests/at crate root. - No magic numbers — use named constants.
- Domain stays pure — no
chrono,uuid,async, or I/O insrc/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.
Use actionlint to statically validate all workflow files without needing a runner or Docker:
# Install (one-time)
curl -sL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash | bash
mv actionlint ~/.local/bin/ # or anywhere on $PATH
# Validate
just lint-workflowsactionlint checks: YAML syntax, ${{ }} expression types, shell scripts (via shellcheck), action inputs, and env: variable usage.
To also run workflows locally end-to-end (requires Docker), use act:
# Install
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
# Dry-run (resolves actions, validates steps without executing)
act push --dry-run
act pull_request --dry-run
# Run a specific workflow
act push -W .forgejo/workflows/lint.yml
act push -W .forgejo/workflows/run-tests.yml
act pull_request -W .forgejo/workflows/commit-check.yml
act pull_request -W .forgejo/workflows/check-branch.ymlForgejo note: workflows use
actions/checkout@v4andactions/cache@v4. These resolve from GitHub if your instance hasDEFAULT_ACTIONS_URL = https://github.cominapp.ini, or fromcode.forgejo.orgif you prefix them withhttps://code.forgejo.org/.
