Skip to content

Commit 06bfe3d

Browse files
committed
Initial commit
Signed-off-by: birkhoff <git@birkhoff.me>
0 parents  commit 06bfe3d

13 files changed

Lines changed: 1828 additions & 0 deletions

File tree

.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use flake

.github/workflows/ci.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
check:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
17+
- name: Install Nix
18+
uses: cachix/install-nix-action@v27
19+
with:
20+
extra_nix_config: |
21+
experimental-features = nix-command flakes
22+
accept-flake-config = true
23+
24+
- name: Setup Cachix
25+
uses: cachix/cachix-action@v15
26+
with:
27+
name: devenv
28+
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
29+
skipPush: true
30+
31+
- name: Check flake
32+
run: nix flake check
33+
34+
- name: Build default package
35+
run: nix build
36+
37+
- name: Test cross-compilation
38+
run: |
39+
echo "Testing cross-compilation builds..."
40+
nix build .#statements-linux-amd64
41+
nix build .#statements-darwin-arm64

.github/workflows/release.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
jobs:
9+
build-and-release:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write
13+
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v4
17+
18+
- name: Install Nix
19+
uses: cachix/install-nix-action@v27
20+
with:
21+
extra_nix_config: |
22+
experimental-features = nix-command flakes
23+
accept-flake-config = true
24+
25+
- name: Setup Cachix
26+
uses: cachix/cachix-action@v15
27+
with:
28+
name: devenv
29+
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
30+
skipPush: true
31+
32+
- name: Build Linux AMD64
33+
run: |
34+
nix build .#statements-linux-amd64
35+
cp result/bin/statements statements-linux-amd64
36+
37+
- name: Build Linux ARM64
38+
run: |
39+
nix build .#statements-linux-arm64
40+
cp result/bin/statements statements-linux-arm64
41+
42+
- name: Build Darwin AMD64
43+
run: |
44+
nix build .#statements-darwin-amd64
45+
cp result/bin/statements statements-darwin-amd64
46+
47+
- name: Build Darwin ARM64
48+
run: |
49+
nix build .#statements-darwin-arm64
50+
cp result/bin/statements statements-darwin-arm64
51+
52+
- name: Build Windows AMD64
53+
run: |
54+
nix build .#statements-windows-amd64
55+
cp result/bin/statements statements-windows-amd64.exe
56+
57+
- name: Create Release
58+
uses: softprops/action-gh-release@v2
59+
with:
60+
files: |
61+
statements-linux-amd64
62+
statements-linux-arm64
63+
statements-darwin-amd64
64+
statements-darwin-arm64
65+
statements-windows-amd64.exe
66+
draft: false
67+
prerelease: false
68+
generate_release_notes: true
69+
env:
70+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
statements
2+
3+
# ---- Go ----
4+
# If you prefer the allow list template instead of the deny list, see community template:
5+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
6+
#
7+
# Binaries for programs and plugins
8+
*.exe
9+
*.exe~
10+
*.dll
11+
*.so
12+
*.dylib
13+
14+
# Test binary, built with `go test -c`
15+
*.test
16+
17+
# Code coverage profiles and other test artifacts
18+
*.out
19+
coverage.*
20+
*.coverprofile
21+
profile.cov
22+
23+
# Dependency directories (remove the comment below to include it)
24+
# vendor/
25+
26+
# Go workspace file
27+
go.work
28+
go.work.sum
29+
30+
# env file
31+
.env
32+
33+
# Editor/IDE
34+
# .idea/
35+
# .vscode/
36+
37+
# ---- Nix ----
38+
# Ignore build outputs from performing a nix-build or `nix build` command
39+
result
40+
result-*
41+
42+
# Ignore automatically generated direnv output
43+
.direnv
44+

CLAUDE.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
A Terminal User Interface (TUI) application for analyzing credit card statements. Built with Go and the Bubble Tea framework, it provides an interactive interface to browse statements and categorize transactions. Currently tested with HSBC Taiwan credit card statements.
8+
9+
## Building and Running
10+
11+
### Traditional Go Build
12+
13+
```bash
14+
# Build the application
15+
go build -o statements
16+
17+
# Run the application
18+
./statements <path-to-statementlist.json>
19+
```
20+
21+
### Nix Flakes
22+
23+
The project includes a Nix flake for reproducible builds. Requires Nix with flakes enabled.
24+
25+
```bash
26+
# Run directly
27+
nix run . -- <path-to-statementlist.json>
28+
29+
# Build the application
30+
nix build
31+
./result/bin/statements <path-to-statementlist.json>
32+
33+
# Enter development environment (provides Go 1.25, gopls, gotools, claude, git, jq)
34+
nix develop
35+
36+
# Cross-platform builds
37+
nix build .#statements-linux-amd64
38+
nix build .#statements-linux-arm64
39+
nix build .#statements-darwin-amd64
40+
nix build .#statements-darwin-arm64
41+
nix build .#statements-windows-amd64
42+
```
43+
44+
Note: The flake is configured to allow unfree packages (required for claude-code).
45+
46+
## Architecture
47+
48+
### Core Components
49+
50+
The codebase is structured around three main files:
51+
52+
- **types.go**: Defines the data model (`Transaction`, `Statement`, `CategorizedTransactions`)
53+
- **analyzer.go**: Transaction analysis logic including categorization, normalization, and data loading
54+
- **main.go**: TUI implementation using Bubble Tea framework (model, view, update pattern)
55+
56+
### Data Flow
57+
58+
1. JSON statements are loaded via `LoadStatements()` in analyzer.go
59+
2. Transactions are categorized by `CategorizeTransactions()` which:
60+
- Normalizes descriptions using `ToCDB()` (full-width to half-width character conversion)
61+
- Detects Apple Pay transactions and extracts card metadata
62+
- Categorizes transactions by type (Transport, Food, Shopping, Travel, Utilities, Other)
63+
3. The Bubble Tea model manages two views (Summary, Statements) with dual-panel layout
64+
4. The Statements view uses two Bubble Tea table components for navigation
65+
66+
### Transaction Categorization System
67+
68+
The categorization happens in two layers:
69+
70+
1. **Payment Method Detection** (analyzer.go:124-159): Detects Apple Pay (`APE` prefix with card last 4 digits), PayPal, and foreign transaction fees
71+
2. **Merchant Category Detection** (analyzer.go:48-97): Uses prefix/substring matching to categorize by merchant type
72+
73+
Each transaction gets:
74+
- A `NormalizedDescription` (half-width characters)
75+
- An `ApplePayCardLast4` field (if applicable)
76+
- A `Category` field (Transport/Food/Shopping/Travel/Utilities/Other)
77+
78+
### View State Management
79+
80+
The model tracks:
81+
- Current view mode (summary or statements)
82+
- Selected statement index
83+
- Sort mode (Date/Amount/Location/Category)
84+
- Category filter (All or specific category)
85+
- Focused table (statements or transactions)
86+
87+
Key state update: `updateTransactionsTable()` (main.go:437-610) rebuilds the transaction table when:
88+
- Statement selection changes
89+
- Sort mode changes
90+
- Category filter changes
91+
92+
### Foreign Transaction Fee Handling
93+
94+
Foreign fees are aggregated and displayed as a single row per statement (main.go:446-462, 580-606). They are filtered out from regular transactions and summarized to avoid cluttering the transaction list.
95+
96+
## Key Technical Details
97+
98+
### Character Normalization
99+
100+
The `ToCDB()` function (analyzer.go:24-38) converts full-width CJK characters to half-width for consistent string matching. This is critical for proper categorization of Chinese/Japanese merchant names.
101+
102+
### Apple Pay Extraction
103+
104+
Uses regex `^APE(\d{4})` to extract the last 4 digits of the card used for Apple Pay transactions. The clean description (without the APE prefix) is obtained via `GetCleanDescription()`.
105+
106+
### Table Navigation
107+
108+
Implements wrap-around navigation (main.go:256-312) - pressing up at the first item wraps to the last item, and vice versa. Also supports Home/End keys and Page Up/Down for quick navigation.
109+
110+
### Dynamic Column Layout
111+
112+
The transactions table adjusts columns based on the active category filter (main.go:502-523). When filtering by a specific category, the Category column is hidden to provide more space for the Description column.

0 commit comments

Comments
 (0)