Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
covers/
data/
libro-test.db
libro.db
libro.bak
node_modules/
dist/
dist-electron/
package-lock.json

.ruff_cache
uv.lock
.venv/
*.log
*.local
.DS_Store
.vscode/*
.vite
230 changes: 60 additions & 170 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,19 @@
# AGENTS.md
# CLAUDE.md

This file provides guidance to LLMs working with code in this repository.

## Project Overview

Libro is a terminal-based reading tracker written in Python that stores data in a local SQLite database. It is a pure CLI tool, interactive input uses `prompt_toolkit` for field prompts with validation and autocomplete. Output is formatted with `rich` Python library.
Libro is a desktop reading tracker built with Electron + React + Vite. It uses a local SQLite database.

## Development Commands

### Running

```bash
# Run the application
uv run libro # Show books read this year
uv run libro add # Add a book + review
uv run libro report # Same as default
uv run libro --db ./libro.db report # Use specific database file
```

### Using Justfile

```bash
just run # Run the app
just test # Run pytest tests
just lint # Check with ruff
just lint-fix # Fix lint issues
just type-check # Run mypy
just ci # Run all checks (lint, type-check, test)
just install # Install dependencies with uv sync
just dev-install # Install dev dependencies
just build # Build the package
```

### Testing and Quality

```bash
uv run python -m pytest tests/ -v
uv run python -m ruff check src/libro/
uv run python -m mypy --package libro
npm run dev # Start Vite renderer + Electron (hot-reload)
npm run build # Typecheck, build renderer, build Electron main/preload
npm run package # Full build + electron-builder → .dmg
npm run typecheck # tsc on both renderer and Electron tsconfigs
npm run lint # eslint
```

## Code Architecture
Expand All @@ -46,27 +22,41 @@ uv run python -m mypy --package libro

```
libro/
├── src/libro/
│ ├── main.py # Entry point, command routing via match/case
│ ├── config.py # Argument parsing and database location logic
│ ├── models.py # Dataclasses: Book, Review, BookReview, ReadingList, ReadingListBook
│ └── actions/
│ ├── db.py # Database init and migration
│ ├── show.py # Display functions (rich tables)
│ ├── report.py # Report views (author stats, yearly charts)
│ ├── modify.py # Add/edit books and reviews (prompt_toolkit input)
│ ├── lists.py # Reading list management
│ └── importer.py # CSV import (Goodreads format and generic CSV)
└── tests/
├── electron/
│ ├── main.ts # Electron main process, BrowserWindow setup
│ ├── preload.ts # Context bridge — exposes LibroApi to renderer
│ ├── ipc.ts # IPC handler registration
│ └── db/
│ ├── client.ts # Database singleton, path resolution, migration call
│ ├── migrate.ts # Schema migrations
│ ├── books.ts # Book/review queries
│ ├── reports.ts # Year counts, author counts, review filters
│ └── lists.ts # Reading list CRUD
├── shared/
│ └── types.ts # Shared TypeScript interfaces (LibroApi, all input/output types)
└── src/ # React renderer
├── App.tsx # Shell with nav (Books, Reports, Search, Lists, Add Book)
├── lib/
│ ├── api.ts # Typed wrapper around window.libro (the context bridge)
│ ├── ratings.ts # Star rating helpers
│ ├── ReviewTable.tsx # Shared review table component (rows open BookDetailModal)
│ └── BookDetailModal.tsx # Modal showing full book details + all reviews
└── features/
├── books/BooksByYearView.tsx
├── reports/ReportsView.tsx
├── search/SearchView.tsx
├── lists/ListsView.tsx
└── add/AddBookReviewForm.tsx
```

### Key Technologies

- **CLI Parsing**: argparse with subparsers
- **Interactive Input**: prompt_toolkit (PromptSession with validators and completers)
- **Terminal Output**: rich (tables, progress bars, styled text)
- **Database**: SQLite3 (stdlib)
- **Package Manager**: uv
- **Framework**: Electron 33 + React 19 + Vite 5
- **Database**: better-sqlite3 (synchronous, runs in main process only)
- **Styling**: Halfmoon CSS
- **Build**: tsup for Electron main/preload, Vite for renderer
- **Packaging**: electron-builder (targets macOS DMG)
- **Package Manager**: npm

## Data Model

Expand All @@ -82,14 +72,7 @@ The application separates **books** from **reviews** to support:
3. `reading_lists` - Curated book collections
4. `reading_list_books` - Many-to-many join table (with priority and added_date)

**Models** (all dataclasses in `models.py`):
- `Book` - Single book entity with `insert()`, `get_by_id()`, `find_by_title_author()`
- `Review` - Single review entity with `insert()`
- `BookReview` - Combined book + review, most commonly used for display; has `get_by_id()`
- `ReadingList` - List entity with full CRUD methods
- `ReadingListBook` - Association with `get_books_in_list()`, `get_list_stats()`

### Database Schema
**Database Schema:**

```sql
CREATE TABLE books (
Expand Down Expand Up @@ -129,128 +112,35 @@ CREATE TABLE reading_list_books (
);
```

## Application Flow

### Entry Point

`main.py` calls `init_args()` from `config.py`, connects to the database, runs `migrate_db()`, then routes to the correct action via `match args["command"]`.

### Database Location Priority

1. `--db` command-line flag
2. `libro.db` in current directory
3. `LIBRO_DB` environment variable
4. Platform-specific data directory (via `appdirs`):
- Linux: `~/.local/share/Libro/mkaz/libro.db`
- macOS: `~/Library/Application Support/Libro/mkaz/libro.db`
- Windows: `%APPDATA%\mkaz\Libro\libro.db`

## CLI Commands

```bash
# Default - show books read this year
libro
libro report

# Year chart of books read
libro report --chart

# Author stats (all authors with counts)
libro report --author
# Books by specific author
libro report --author "Stephen King"

# Show book/review detail by review ID
libro report <id>

# Filter by year
libro report --year 2024

# Add book + review (interactive prompts)
libro add

# Book management
libro book # Show recent books
libro book <id> # Show specific book
libro book add # Add book only (no review)
libro book edit <id> # Edit book details
libro book --author "King" # Filter by author
libro book --title "Found" # Filter by title
libro book --year 2024 # Filter by year

# Review management
libro review # Show recent reviews
libro review <id> # Show review detail
libro review add <book_id> # Add review to existing book
libro review edit <id> # Edit review
libro review --author "King" # Filter by author
libro review --year 2024 # Filter by year

# Reading list management
libro list create "Name" --description "..."
libro list show # All lists
libro list show <id> # Specific list
libro list add <list_id> [book_ids...] # Add books to list (interactive if no IDs)
libro list remove <list_id> <book_id>
libro list stats # All list stats
libro list stats <id> # Specific list stats
libro list edit <id> --name "New Name"
libro list delete <id>
libro list import <file.csv> --name "List Name"

# Import from Goodreads CSV
libro import <file.csv>
```

## Important Patterns

### Interactive Input (modify.py)

`add_book_review()`, `add_book()`, `add_review()`, `edit_book()`, `edit_review()` all use `prompt_toolkit.PromptSession` with:
- **Validators**: `NonEmptyValidator`, `IntValidator`, `RatingValidator` (1-5), `DateValidator` (YYYY-MM-DD)
- **Completers**: `AuthorCompleter` (sorted by frequency), `GenreCompleter`
- Tab completion works in any field with a completer attached

### Date Handling

Dates are stored as ISO 8601 strings. `models.py` registers a SQLite adapter/converter:

```python
sqlite3.register_adapter(date, adapt_date_iso) # date -> "YYYY-MM-DD"
sqlite3.register_converter("date", convert_date) # bytes -> date
```

The connection must be opened with `detect_types=sqlite3.PARSE_DECLTYPES` for converters to fire, but the current code uses `db.row_factory = sqlite3.Row` without `detect_types`. Date fields come back as strings; convert with `date.fromisoformat()` if needed.

### Adding a New Action

1. Add argument parsing to `config.py` (argparse subparser or flag)
2. Create or update a function in the appropriate `actions/` file
3. Add a case in `main.py`'s `match args["command"]` block
4. Use `rich.Table` or `rich.console` for output; use `PromptSession` for interactive input

### Adding a New Store Query
## IPC Architecture

SQL lives directly in the model methods (`models.py`) and action files. There is no separate store layer. Keep SELECT queries in model classmethods when returning typed objects; put display-only queries in `show.py` or `report.py`.
All database access happens in the main process. The renderer never touches the database directly.

## Troubleshooting
- `preload.ts` exposes a typed `LibroApi` object on `window.libro` via `contextBridge`
- `src/lib/api.ts` re-exports this as `api` for use in React components
- `ipc.ts` registers `ipcMain.handle` handlers that call into `electron/db/` functions

### Duplicate Book on Import
IPC channel naming: `namespace:verb-noun` (e.g. `books:add-book-review`, `lists:get-by-id`).

`importer.py` calls `Book.find_by_title_author()` before inserting. If a book already exists, it skips the insert and adds a review to the existing book.
## Database Location

### Date Shows Wrong Value
Priority order:

Check that the date string is valid ISO format (`YYYY-MM-DD`). The `DateValidator` in `modify.py` enforces this on input. For database reads without `detect_types`, call `date.fromisoformat(row["date_read"])` explicitly.
1. `libro.db` in cwd (if it exists)
2. `LIBRO_DB` environment variable
3. `{appData}/Libro/mkaz/libro.db`

### Reading List Migration
## Adding a New Feature

Old databases without reading list tables are handled by `migrate_db()` in `actions/db.py`, which runs on every startup and creates missing tables idempotently.
1. Add types to `shared/types.ts` (input, output, and the `LibroApi` interface entry)
2. Add the query function in the appropriate `electron/db/*.ts` file
3. Register an IPC handler in `electron/ipc.ts`
4. Wire it up in `preload.ts` and `src/lib/api.ts`
5. Build the React component in `src/features/`

## Code Style

- Python 3.10+ (`match/case`, `list[str]` type hints)
- Dataclasses for models
- Type hints throughout; mypy enforced via `just type-check`
- ruff for linting and formatting
- No global state; `db` connection passed explicitly to every function
- Strict TypeScript; all types defined in `shared/types.ts`
- Two separate tsconfig targets: `tsconfig.app.json` (renderer) and `tsconfig.electron.json` (main/preload)
- No ORM; raw SQL via better-sqlite3 synchronous API
- eslint with react-hooks and react-refresh plugins
59 changes: 10 additions & 49 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,62 +1,23 @@
# Contributing to Libro

Hey there! Thanks for your interest in contributing to Libro. This is just a hobby project I work on in my spare time, but I'm totally open to help from others who find it useful or interesting.
Libro is a hobby project, but contributions are welcome.

## Getting Started

The easiest way to get up and running:
1. Fork and clone the repo
2. Install dependencies: `npm install`
3. Run the dev build: `just dev`

1. Fork the repo
2. Clone your fork locally
3. Make sure you have Python 3.10+ and install `uv` if you don't have it
4. Run `just install` to set up dependencies
5. Run `uv run libro --help` to make sure everything works
## Development

## Making Changes

### Development Workflow

- `just lint` - Check your code style (uses ruff)
- `just run <args>` - Test your changes with the CLI
- `just build` - Full build process before submitting

### What I'm Looking For
`just dev` builds the Electron main process, starts the Vite dev server, then launches the app. Vite handles hot-reload for renderer changes. If you change anything in `electron/`, restart `just dev`.

I'm pretty relaxed about contributions, but here are some things that would be especially welcome:

- **Bug fixes** - If something's broken, I'd love help fixing it
- **Small features** - New commands, import formats, display options, etc.
- **Documentation improvements** - Better help text, examples, etc.
- **Testing** - The project could use more automated tests
- **Performance improvements** - Making things faster is always good

### What Probably Won't Get Merged

- Major architectural changes without discussion first
- Features that significantly complicate the codebase
- Anything that breaks backward compatibility without a really good reason
## Making Changes

## Code Style
Follow the pattern in CLAUDE.md for adding new features — types in `shared/types.ts`, queries in `electron/db/`, IPC handler in `electron/ipc.ts`, wired up in `preload.ts` and `src/lib/api.ts`, then the React component in `src/features/`.

I use ruff for formatting and linting, so just run `just lint` and fix any issues it finds. The existing code style is pretty straightforward - try to match what's already there.
Run `npm run typecheck` and `npm run lint` before submitting.

## Submitting Changes

1. Create a branch for your changes
2. Make your changes and test them
3. Run `just lint` to make sure everything looks good
4. Open a pull request with a clear description of what you changed and why

No need for fancy commit message formats or elaborate PR templates. Just explain what you did in plain English.

## Questions or Ideas?

Feel free to open an issue if you want to discuss something before diving in. I'm usually pretty responsive, though sometimes it might take a few days if I'm busy with other stuff.

## License

By contributing, you agree that your contributions will be licensed under the same license as the project (check the LICENSE file).

---

Thanks for considering contributing! Even small improvements are appreciated. 🙂
Open a pull request with a plain description of what you changed and why. No fancy templates needed.
Loading
Loading