inv is a terminal invoice manager written in Go. It uses Bubble Tea for the TUI, stores invoice data locally under your config directory, exports invoices to PDF, and keeps a lightweight git history of invoice changes.
The app is designed around a simple workflow:
- create or switch a client
- draft invoices with one or more line items
- export PDFs for review or sending
- mark invoices as sent to lock them from further edits
- terminal-first invoice workflow with vim-style navigation
- first-run setup flow for your business info and first client
- per-client defaults for address, payment terms, currency, and last-used line items
- sequential invoice numbering in
YYYY-NNNformat - PDF export with subtotal, tax, and total rendering
- draft vs sent invoice states
- sent invoices are immutable
- local persistence in JSON and TOML
- automatic git initialization and best-effort pull/commit/push for the invoice config directory
- optional GitHub remote creation on first run when
ghis installed and authenticated
- Go 1.26 or newer
giton yourPATHif you want the built-in history/sync behavior- a terminal that supports interactive TUI apps
Optional but useful:
xdg-opento auto-open exported PDFs on Linux$EDITORor$VISUALfor multi-line line item descriptionsghif you want the app to auto-create a private GitHub backup repo on first run
Clone the repository and build the binary:
git clone https://github.com/carterj-c/inv
cd inv
go build .That produces an inv binary in the project root. You can also install it into your Go bin directory:
go install .If you use go install, make sure your Go bin directory is on PATH. On many systems that is:
export PATH="$HOME/go/bin:$PATH"From the repository:
go run .Or, if you built the binary:
./invOr, after go install:
invManual PDF sync:
inv syncOn first launch, inv creates its data directory and walks you through:
- your name or business name
- your address
- the PDF export directory
- your default currency
- your first client
Default values baked into the app:
- export directory:
~/invoices - default currency:
CAD - default payment terms for new clients:
Net 30
After setup, the app:
- writes the global config
- writes the first client config
- initializes a git repository in the invoice config directory if one does not already exist
- if
ghis installed, authenticated, and nooriginremote exists, it attempts to create a private GitHub repo namedinvoice-configand push the initial state
The app stores everything under:
$XDG_CONFIG_HOME/invoiceifXDG_CONFIG_HOMEis set- otherwise
~/.config/invoice
Directory layout:
~/.config/invoice/
├── .git/
├── config.toml
├── clients/
│ └── <client-slug>.toml
└── data/
├── invoices.json
└── pdfs/
Files are used as follows:
config.toml: your business info, export directory, active client, default currencyclients/*.toml: per-client details and the last line items used for that clientdata/invoices.json: invoice records and next invoice number countersdata/pdfs/: tracked archive copies of exported PDFs
Global config example:
[user]
name = "Jane Doe"
address = "123 Main St, Toronto, ON M5V 2H1"
[settings]
export_dir = "~/invoices"
active_client = "acme-corp"
default_currency = "CAD"Client config example:
name = "Acme Corp"
address = "456 Business Ave, Suite 200, Montreal, QC H2X 1Y4"
payment_terms = "Net 30"
currency = "CAD"
[[tax]]
name = "GST"
rate = 0.05
[[last_line_items]]
description = "Software development"
quantity = 5
rate = 15000Note: the data model supports tax entries, but the current TUI does not provide screens for creating or editing tax rules. To use tax lines in totals and PDFs, add them directly to the client TOML file.
Main list view:
j/k: move selectionn: new invoiceeorEnter: edit selected draftd: delete selected draftp: export selected invoice to PDFs: mark selected invoice as sentc: switch client?: toggle helpq: quit
Editor view:
Tab/Shift+Tab: move between fieldsa: add a line iteme: edit the selected field or line itemd: remove the selected line itemj/k: move between line itemsCtrl+e: open the selected description in$EDITOREnter: saveEsc: cancel
Behavior worth knowing:
- new invoice numbers are allocated per year, for example
2026-001 - invoice numbers are intentionally not reused
- new invoices are prefilled from the client's
last_line_itemswhen available - sent invoices cannot be edited or deleted
- PDFs are written to the configured export directory, which is created automatically if needed
- every exported PDF is also copied into the tracked archive at
data/pdfs/inside the config repo - on launch, the app performs a non-destructive PDF sync between the export directory and
data/pdfs/ - when the same PDF exists in both places with different contents, the newer file wins
- sync does not delete PDFs from either side automatically
The app keeps invoice data in a git repo inside the config directory:
- first run initializes the repo
- launch performs a best-effort
git pull --rebase --quietiforiginexists - launch then syncs PDFs between the user-facing export directory and the tracked archive
- saving, deleting, or marking an invoice as sent stages all changes and commits them
- exporting a PDF also updates the tracked archive copy and commits it
inv syncruns the same PDF sync from the command line and commits archive changes- after commit, the app attempts a best-effort
git push --quietiforiginexists
If gh is not installed or not authenticated, the app leaves the repo local-only. In that case, add a remote yourself:
cd ~/.config/invoice
git remote add origin <your-remote-url>
git push -u origin mainYou may need to create the initial branch name that matches your git config.
Build:
go build ./...Test:
go test ./...There are currently no Go test files in the repository, but the package set should build cleanly.
main.go: entrypointinternal/tui: Bubble Tea application and viewsinternal/config: config paths and TOML persistenceinternal/store: JSON invoice storageinternal/pdf: PDF generationinternal/git: git helpers for backup/syncinternal/model: core data model