Compare commits

..

143 Commits

Author SHA1 Message Date
bel
045ab30dfa install script
All checks were successful
cicd / ci (push) Successful in 2m6s
2026-01-03 11:21:47 -07:00
Bel LaPointe
b1cf639f39 un-break shared
All checks were successful
cicd / ci (push) Successful in 3m5s
2025-10-31 07:28:26 -06:00
Bel LaPointe
cc546d9b2d fix
All checks were successful
cicd / ci (push) Successful in 1m14s
2025-10-26 13:11:09 -06:00
Bel LaPointe
0066a267a6 bal prints percents
All checks were successful
cicd / ci (push) Successful in 2m40s
2025-10-26 09:27:07 -06:00
Bel LaPointe
f58053ebe9 5% raise neat
All checks were successful
cicd / ci (push) Successful in 1m55s
2025-10-15 14:55:00 -06:00
Bel LaPointe
b30b1811ea shared flags
All checks were successful
cicd / ci (push) Successful in 1m53s
2025-09-04 22:18:04 -06:00
Bel LaPointe
3fb1ee4ea3 qt got a turtle rock promo~
All checks were successful
cicd / ci (push) Successful in 2m13s
2025-08-28 20:56:35 -06:00
Bel LaPointe
7a790fa31e group date
All checks were successful
cicd / ci (push) Successful in 1m51s
2025-08-23 08:25:08 -06:00
Bel LaPointe
774de58bf7 not u
All checks were successful
cicd / ci (push) Successful in 1m3s
2025-06-16 18:18:04 -06:00
Bel LaPointe
6aa549cb02 bpis predictFixedGrowth fixed
All checks were successful
cicd / ci (push) Successful in 1m15s
2025-06-16 18:12:11 -06:00
Bel LaPointe
fe7c3a9682 oop 3500 saved per mo not 3600
All checks were successful
cicd / ci (push) Successful in 2m45s
2025-06-16 17:58:38 -06:00
Bel LaPointe
36dcf70dbe from Asset to Bel:Asset
All checks were successful
cicd / ci (push) Successful in 1m1s
2025-06-16 17:57:06 -06:00
Bel LaPointe
989bc3c2ff update prediction default
All checks were successful
cicd / ci (push) Successful in 2m19s
2025-06-16 17:55:16 -06:00
Bel LaPointe
f22d7d958b WILDCARD
All checks were successful
cicd / ci (push) Successful in 1m28s
2025-05-25 13:04:27 -06:00
Bel LaPointe
6a4e10ee2b trigger on self 2025-05-25 13:04:06 -06:00
Bel LaPointe
09da985455 build on build/ 2025-05-25 13:03:36 -06:00
Bel LaPointe
f52832ee82 docker build 2025-05-25 13:02:59 -06:00
bel
0cafba0571 bel pay raise
Some checks failed
cicd / ci (push) Failing after 1m30s
2025-05-24 08:46:51 -06:00
Bel LaPointe
1a397dbf45 mmmm giving out security info wasnt great so rotate bank passwords
Some checks failed
cicd / ci (push) Failing after 15s
2025-05-23 23:36:02 -06:00
Bel LaPointe
b10283d752 multi-acc rate limited lookin pretty good~
Some checks failed
cicd / ci (push) Failing after 14s
2025-05-23 23:17:54 -06:00
Bel LaPointe
284613b5bc fix https
Some checks failed
cicd / ci (push) Failing after 14s
2025-05-23 23:01:15 -06:00
Bel LaPointe
929d15c5b7 teller accepts multi token in tokens.txt
Some checks failed
cicd / ci (push) Failing after 17s
2025-05-23 23:00:09 -06:00
Bel LaPointe
5e61378d63 resolved chase since dawn of time yay 2025-05-23 22:38:52 -06:00
Bel LaPointe
c948a32458 fix onedayoff checks day after
Some checks failed
cicd / ci (push) Failing after 15s
2025-05-23 22:24:05 -06:00
Bel LaPointe
35e2e40ce6 oooo onedayoff ok
Some checks failed
cicd / ci (push) Failing after 15s
2025-05-23 22:15:10 -06:00
Bel LaPointe
847cd736b6 at least round both wrong
Some checks failed
cicd / ci (push) Failing after 15s
2025-05-23 22:02:27 -06:00
Bel LaPointe
9cf8cb0736 rec ok with negatives/positives
Some checks failed
cicd / ci (push) Failing after 21s
2025-05-23 21:59:19 -06:00
Bel LaPointe
4997264f4c fix cache test 2025-05-23 21:56:36 -06:00
Bel LaPointe
f69a850bd8 got a silly ui that can yield a test token 2025-05-23 21:38:07 -06:00
Bel LaPointe
5a3d5e5610 stub teller.Init 2025-05-23 21:12:27 -06:00
Bel LaPointe
c38e8529af ready to get a real token 2025-05-23 21:09:26 -06:00
Bel LaPointe
7a946b7604 go run ./ cli rec to pull all from teller
Some checks failed
cicd / ci (push) Failing after 18s
2025-05-23 21:01:09 -06:00
Bel LaPointe
e0fa44eef7 test bank.cache
Some checks failed
cicd / ci (push) Failing after 17s
2025-05-23 20:32:49 -06:00
Bel LaPointe
6440b07d14 impl teller.Client returns bank.*
Some checks failed
cicd / ci (push) Failing after 14s
2025-05-23 20:15:59 -06:00
Bel LaPointe
bae33f8c60 try /transactions too
Some checks failed
cicd / ci (push) Failing after 19s
2025-05-23 19:44:24 -06:00
Bel LaPointe
daa446cc02 teller seems k so long as they accept 2025-05-23 19:41:39 -06:00
Bel LaPointe
daac3907f4 teller certs
Some checks failed
cicd / ci (push) Failing after 1m7s
2025-05-23 19:23:52 -06:00
Bel LaPointe
83305227db no more ofx 2025-05-23 19:20:52 -06:00
Bel LaPointe
f5d82fc6aa ofx died 3y ago 2025-05-23 18:59:39 -06:00
Bel LaPointe
e581b7835c DRAW
Some checks failed
cicd / ci (push) Failing after 23s
2025-05-07 15:54:58 -06:00
Bel LaPointe
19f4b614d3 insta..sh
Some checks failed
cicd / ci (push) Failing after 17s
2025-05-07 15:35:05 -06:00
Bel LaPointe
58462fb5a4 cli graph graphs
Some checks failed
cicd / ci (push) Failing after 51s
2025-05-07 15:34:31 -06:00
Bel LaPointe
2f8dba4e23 accept --cpi=FILE --cpiy=YEAR to translate money to target year
All checks were successful
cicd / ci (push) Successful in 1m30s
2025-05-07 09:55:16 -06:00
Bel LaPointe
0d91cd63db helpers 2025-05-07 09:40:53 -06:00
Bel LaPointe
9681050a46 non usd 3 decimal places
All checks were successful
cicd / ci (push) Successful in 48s
2025-04-11 09:29:13 -06:00
Bel LaPointe
0b4d78796e minor refactor but i think no problem
All checks were successful
cicd / ci (push) Successful in 1m28s
2025-04-11 09:22:41 -06:00
bel
1805a087e0 cli depth doesnt require mindepth
All checks were successful
cicd / ci (push) Successful in 45s
2025-04-06 11:27:54 -06:00
bel
fabd26e8e9 reg prettier print same col w
All checks were successful
cicd / ci (push) Successful in 54s
2025-04-06 11:08:38 -06:00
bel
9646356d7f reg -c prints DATE FULL:ACC DELTA (BAL)
All checks were successful
cicd / ci (push) Successful in 46s
2025-04-06 11:03:53 -06:00
bel
84bbda1031 fix and
All checks were successful
cicd / ci (push) Successful in 51s
2025-04-06 10:40:44 -06:00
bel
9b4accafe3 neater bal print 2025-04-06 10:38:54 -06:00
bel
9352ca82de WAY faster doing cumulative balances over summing [0,i] every reg loop
All checks were successful
cicd / ci (push) Successful in 1m39s
2025-04-06 10:05:42 -06:00
bel
058874b566 buffering writes did NOT save any time
All checks were successful
cicd / ci (push) Successful in 1m37s
2025-04-05 23:07:56 -06:00
bel
8ade122378 ana-ledger-cli shared FILES
All checks were successful
cicd / ci (push) Successful in 55s
2025-04-03 23:36:31 -06:00
bel
fe98fa67f3 printing normalize appends factor and real delta
All checks were successful
cicd / ci (push) Successful in 58s
2025-04-03 23:20:50 -06:00
bel
5cc9b141b9 normalize
All checks were successful
cicd / ci (push) Successful in 1m0s
2025-04-03 23:04:08 -06:00
bel
266af7353a cli accepts -with PATTERN to filter transactions by all attendees
All checks were successful
cicd / ci (push) Successful in 1m31s
2025-04-03 22:32:56 -06:00
Bel LaPointe
dd20f066a3 WIP cmd/cli$ go run ../ cli $(printf " -f %s" $HOME/Sync/Core/ledger/eras/2022-/*.txt) -n -w ^Housey --depth 1 -usd bal
All checks were successful
cicd / ci (push) Successful in 1m32s
2025-04-03 21:39:36 -06:00
Bel LaPointe
697fc16b52 each delta has .with and LikeWith to find X related to Y 2025-04-03 17:28:57 -06:00
Bel LaPointe
8621619433 cli bal nonzero
All checks were successful
cicd / ci (push) Successful in 54s
2025-04-03 12:28:00 -06:00
Bel LaPointe
768ce8e92e cumulative bal in reg includes bpis
All checks were successful
cicd / ci (push) Successful in 59s
2025-04-03 12:25:39 -06:00
Bel LaPointe
757afa603e hide zeros only in bal 2025-04-03 12:11:32 -06:00
Bel LaPointe
d093db1a2b cli supports bal again!
All checks were successful
cicd / ci (push) Successful in 1m35s
2025-04-03 11:55:19 -06:00
Bel LaPointe
30a0414dcd evalulate acc = VALUE in date, filename, lineno order 2025-04-03 11:55:06 -06:00
Bel LaPointe
1f9919a172 deltas try to remember filename, lineno 2025-04-03 11:54:29 -06:00
Bel LaPointe
fb1ddc72c3 ledger.Balances accept Like, Group kinda 2025-04-03 11:53:33 -06:00
Bel LaPointe
0eaeeec359 cli accepts -n normalie
All checks were successful
cicd / ci (push) Successful in 1m50s
2025-04-03 10:24:49 -06:00
Bel LaPointe
188b31aa0c move me and qt salaries to ana.NewDefaultNormalizer 2025-04-03 10:15:05 -06:00
Bel LaPointe
af1b23731a gui CREATE button
All checks were successful
cicd / ci (push) Successful in 1m3s
2025-02-21 16:24:41 -07:00
Bel LaPointe
fcd7dd208c impl /api/create 2025-02-21 15:55:12 -07:00
Bel LaPointe
9bbdeeae9c oop no extra loop
All checks were successful
cicd / ci (push) Successful in 52s
2025-02-21 15:38:03 -07:00
Bel LaPointe
4d01a5e481 impl normalize in HTTP
All checks were successful
cicd / ci (push) Successful in 2m18s
2025-02-21 15:31:09 -07:00
Bel LaPointe
5cc7b7ec55 impl ana.Normalizer 2025-02-21 15:06:44 -07:00
Bel LaPointe
ed0ce83556 impl cli reg depth
All checks were successful
cicd / ci (push) Successful in 1m24s
2025-02-21 14:47:30 -07:00
Bel LaPointe
a1631019f0 tood 2025-02-21 14:37:00 -07:00
Bel LaPointe
d7edc1ff1d nicer but my fsm is wrong if i want to bundle ands first
All checks were successful
cicd / ci (push) Successful in 3m29s
2024-12-13 10:49:19 -07:00
Bel LaPointe
012024ac66 i need fsm 2024-12-13 09:48:24 -07:00
Bel LaPointe
a5b1e653ae go run ./.. cli --period 2024 $(printf " -f %s" $HOME/Sync/Core/ledger/eras/2022-/*) reg [^K]:[^:]*:Retirement$ 2024-12-12 23:03:17 -07:00
Bel LaPointe
37f6f47375 pretty print vs cumulative
All checks were successful
cicd / ci (push) Successful in 1m52s
2024-12-12 22:56:29 -07:00
Bel LaPointe
1a517eb8f2 temp 2024-12-12 22:53:40 -07:00
Bel LaPointe
c3f7800dec can get retirement contributions with grepping 2024-12-12 22:44:38 -07:00
Bel LaPointe
e25d52b141 prettier printing 2024-12-12 22:33:21 -07:00
Bel LaPointe
7a828f8463 reg only prints accounts matching LIKES and not NOTLIKEs 2024-12-12 22:29:30 -07:00
Bel LaPointe
117c300533 reg prints balance per xaction now 2024-12-12 22:28:40 -07:00
Bel LaPointe
4d484b8aa4 reg prints balance per xaction now 2024-12-12 22:27:44 -07:00
Bel LaPointe
4831914251 o my register is a balance series but not a transaction series try again
All checks were successful
cicd / ci (push) Successful in 1m52s
2024-12-12 22:18:38 -07:00
Bel LaPointe
eb3af4b54f ledger balances helpers like Sub, Sum, Invert 2024-12-12 22:17:08 -07:00
Bel LaPointe
849a8696f5 a reg 2024-12-12 22:10:16 -07:00
Bel LaPointe
1a2c88687f progress i guess 2024-12-12 21:59:51 -07:00
Bel LaPointe
889dc48d6c oo more tood 2024-12-12 21:53:18 -07:00
Bel LaPointe
881162357b no debug log 2024-12-12 21:50:18 -07:00
Bel LaPointe
62e65c47df support --period 2024-12-12 21:50:03 -07:00
Bel LaPointe
8ab5a0edf5 prints bal
All checks were successful
cicd / ci (push) Successful in 2m1s
2024-12-12 21:37:02 -07:00
Bel LaPointe
d1b3da3fa3 wip
Some checks failed
cicd / ci (push) Failing after 1m54s
2024-12-12 21:16:02 -07:00
Bel LaPointe
e4d60a9e73 stub cli
All checks were successful
cicd / ci (push) Successful in 1m1s
2024-12-12 11:37:16 -07:00
Bel LaPointe
23bd6e29c9 handle single-line ; comments
All checks were successful
cicd / ci (push) Successful in 1m51s
2024-12-12 11:34:00 -07:00
Bel LaPointe
5587429922 resize
All checks were successful
cicd / ci (push) Successful in 2m6s
2024-07-22 11:40:00 -06:00
Bel LaPointe
e3c1af4f5d update title
All checks were successful
cicd / ci (push) Successful in 50s
2024-07-22 07:16:25 -06:00
Bel LaPointe
ce19c74cb0 oop
All checks were successful
cicd / ci (push) Successful in 1m36s
2024-07-22 07:13:12 -06:00
Bel LaPointe
16bbeb8749 pie trims common prefix and sorts slice names 2024-07-22 07:12:36 -06:00
bel
3211dbd73f home links to pies
All checks were successful
cicd / ci (push) Successful in 1m18s
2024-07-21 17:20:45 -06:00
bel
6df7171057 /api/trends has PIES
All checks were successful
cicd / ci (push) Successful in 1m36s
2024-07-21 17:18:56 -06:00
bel
6be00d6423 /api/trends has 1 pie of median 2024-07-21 17:12:52 -06:00
bel
b8c78fe55e /api/trends prints pie of each of last N months 2024-07-21 17:03:44 -06:00
bel
18a19e52f5 build defaults to ts 2024-07-21 15:44:03 -06:00
bel
bb11214180 always highlight all inbox transactions
All checks were successful
cicd / ci (push) Successful in 50s
2024-07-21 08:43:09 -06:00
bel
6c66b6debb highlight original on amend... 2024-07-21 08:36:16 -06:00
bel
518909b7f7 transactions.html can edit though needs some kinda user feedback of the changed row or something...
All checks were successful
cicd / ci (push) Successful in 1m28s
2024-07-21 08:31:37 -06:00
bel
d69de4da11 ui ready enough for amending 2024-07-21 08:23:34 -06:00
bel
b8323f731f TODO clicking on transactions switches them to a stub state that will be a form that submits amendments BUT ASSERTS ONLY 1 DELTA MODIFIED BEFORE SENDING and sends the first
All checks were successful
cicd / ci (push) Successful in 56s
2024-07-20 21:32:59 -06:00
bel
912c3a2659 anywidth
All checks were successful
cicd / ci (push) Successful in 1m12s
2024-07-20 21:19:42 -06:00
bel
6026d54062 fill a dummy form 2024-07-20 21:16:54 -06:00
bel
b6cf4656c8 dummy mvp is ew 2024-07-20 21:12:49 -06:00
bel
45c4d7b684 transactions.html omits non-payee currency and value table cells
All checks were successful
cicd / ci (push) Successful in 1m3s
2024-07-20 20:45:31 -06:00
bel
59205fba4c ledger.Delta.Payee = true 2024-07-20 20:25:37 -06:00
bel
1d18cb50c5 ledger.Transaction.Payee()
All checks were successful
cicd / ci (push) Successful in 1m21s
2024-07-20 20:10:35 -06:00
bel
bd85dcc86a register cleaner 2024-07-20 19:56:29 -06:00
Bel LaPointe
2ce78d2b42 got transactions into rows in transactions.html, now gotta fix column with for description then go for editable
All checks were successful
cicd / ci (push) Successful in 55s
2024-07-20 11:40:06 -06:00
Bel LaPointe
8886442e89 /api/transactions returns .transactions [][]Delta
All checks were successful
cicd / ci (push) Successful in 1m0s
2024-07-20 11:30:42 -06:00
Bel LaPointe
1e50274681 todo 2024-07-20 11:29:20 -06:00
Bel LaPointe
80ca2ea61e make ledger.Deltas.Transactions()
All checks were successful
cicd / ci (push) Successful in 1m8s
2024-07-20 11:29:10 -06:00
Bel LaPointe
e8f42c7a5d Fix /api/bal t route to router.APIReg
All checks were successful
cicd / ci (push) Successful in 54s
2024-07-20 10:45:59 -06:00
Bel LaPointe
ae44534fc3 api/transactions pretty 2024-07-20 10:43:57 -06:00
bel
cd36e12b68 DEBUG=true go run . http -http :38080 -group-date=^....-.. -group-name=^[^:]*:[^:]* -bpi=./http/bpi.dat ./http/macro.d/ 2024-07-20 10:19:24 -06:00
bel
9a0eb89f54 WIP api transactions doesnt return net house debt hrmmmm
All checks were successful
cicd / ci (push) Successful in 1m39s
2024-07-20 10:18:58 -06:00
bel
fe8d80ffc8 balances.NotLike 2024-07-20 10:05:17 -06:00
bel
f41386b3b4 del code related to last n lines 2024-07-20 09:55:03 -06:00
bel
2141e030ef no more last n lines 2024-07-20 09:53:41 -06:00
bel
ee6ce95c0a ui balance filters and groups to houseyMcHouseface 2024-07-20 09:48:46 -06:00
bel
6d174b031b balances.Group 2024-07-20 09:46:19 -06:00
bel
b6c6e83443 Refactoring http/main into http router
All checks were successful
cicd / ci (push) Successful in 1m39s
2024-07-20 09:27:26 -06:00
bel
ddc95c9054 delta.Transaction export field 2024-07-20 09:27:08 -06:00
bel
666965042a http chart to src/view 2024-07-20 09:07:06 -06:00
Bel LaPointe
f6b8b92bff impl and test src.ledger.Files.Amend(old, new Delta)
All checks were successful
cicd / ci (push) Successful in 1m15s
2024-07-14 14:24:29 -06:00
bel
3e001b8ddd test start
All checks were successful
cicd / ci (push) Successful in 1m28s
2024-07-14 08:42:31 -06:00
bel
682dc9880e todo
All checks were successful
cicd / ci (push) Successful in 1m21s
2024-07-13 23:35:52 -06:00
bel
4e72d25dce todo 2024-07-13 23:29:42 -06:00
bel
0d456f4d73 move files.Add to inbox.txt 2024-07-13 23:23:32 -06:00
bel
9b1ad420fe allow updates as invert + inbox, inserts as inbox 2024-07-13 22:46:59 -06:00
bel
e633ee07ab .src.ledger.Delta.transaction = filename/idx
All checks were successful
cicd / ci (push) Successful in 1m27s
2024-07-13 12:35:10 -06:00
Bel LaPointe
bdcd9fe26a hide because i dont want to finish it
All checks were successful
cicd / ci (push) Successful in 1m15s
2023-11-11 18:43:38 -07:00
Bel LaPointe
2051fa1cdc rename because ledger-ui doesnt like symlinks
All checks were successful
cicd / ci (push) Successful in 50s
2023-11-07 07:00:43 -07:00
Bel LaPointe
f7098def0e use bpi to create monopoly money and weigh contributions to house and update staging for it
All checks were successful
cicd / ci (push) Successful in 1m40s
2023-11-07 06:36:15 -07:00
456 changed files with 230286 additions and 859 deletions

View File

@@ -6,6 +6,8 @@ on:
paths:
- 'cmd/**'
- 'src/**'
- 'build/**'
- '.gitea/**'
jobs:
ci:

View File

@@ -1,4 +1,4 @@
FROM golang:1.21.3-alpine3.18 as builder
FROM golang:1.23.9-alpine3.21 as builder
COPY ./ /go/src/ana-ledger
WORKDIR /go/src/ana-ledger

View File

@@ -2,7 +2,7 @@
set -e
img=registry-app.inhome.blapointe.com:5001/bel/ana-ledger:latest
img=registry-app.inhome.blapointe.com:5001/bel/ana-ledger:${1:-$(date +%Y%m%d%H%M)}
cd "$(dirname "$(dirname "$(realpath "$BASH_SOURCE")")")"
was=$(docker inspect $img | jq -r .[0].Id | sed 's/^sha256://')
docker build -f ./build/Dockerfile -t $img .

1
cmd/cli/bpi.dat Symbolic link
View File

@@ -0,0 +1 @@
../../../../../../Sync/Core/ledger/bpi.dat

21
cmd/cli/config.go Normal file
View File

@@ -0,0 +1,21 @@
package cli
type Config struct {
Files FileList
BPI string
CPI string
CPIYear int
Query struct {
Period Period
Sort string
NoRounding bool
Depth int
With string
NoExchanging bool
Normalize bool
USDOnly bool
}
Compact bool
GroupDate string
NoPercent bool
}

1
cmd/cli/cpi.dat Symbolic link
View File

@@ -0,0 +1 @@
../../../../../../Sync/Core/ledger/cpi.dat

90
cmd/cli/flag.go Normal file
View File

@@ -0,0 +1,90 @@
package cli
import (
"fmt"
"os"
"strings"
"time"
)
type FlagStringArray []string
func (array *FlagStringArray) String() string {
return strings.Join(*array, ", ")
}
func (array *FlagStringArray) Set(s string) error {
*array = append(*array, s)
return nil
}
type FileList FlagStringArray
func (fileList FileList) Strings() []string {
return ([]string)(fileList)
}
func (fileList *FileList) String() string {
return (*FlagStringArray)(fileList).String()
}
func (fileList *FileList) Set(s string) error {
if _, err := os.Stat(s); os.IsNotExist(err) {
return fmt.Errorf("file does not exist: %s", s)
}
return (*FlagStringArray)(fileList).Set(s)
}
type Period struct {
Start time.Time
Stop time.Time
}
func (period Period) Empty() bool {
return period.Stop.Sub(period.Start) == 0
}
func (period *Period) String() string {
return fmt.Sprintf("%s..%s", period.Start, period.Stop)
}
func (period *Period) Set(s string) error {
ss := strings.Split(s, "..")
if err := period.setStartStop(ss[0]); err != nil {
return err
}
if len(ss) != 2 {
} else if err := period.setStop(ss[1]); err != nil {
return err
}
return nil
}
func (period *Period) setStartStop(s string) error {
stop, err := period.setT(s, &period.Start)
period.Stop = stop
return err
}
func (period *Period) setStop(s string) error {
_, err := period.setT(s, &period.Stop)
return err
}
func (*Period) setT(s string, t *time.Time) (time.Time, error) {
if result, err := time.Parse("2006", s); err == nil {
*t = result
return result.AddDate(1, 0, 0).Add(-1 * time.Minute), nil
}
if result, err := time.Parse("2006-01", s); err == nil {
*t = result
return result.AddDate(0, 1, 0).Add(-1 * time.Minute), nil
}
if result, err := time.Parse("2006-01-02", s); err == nil {
*t = result
return result.AddDate(0, 0, 1).Add(-1 * time.Minute), nil
}
return time.Time{}, fmt.Errorf("unimplemented format: %s", s)
}

443
cmd/cli/main.go Normal file
View File

@@ -0,0 +1,443 @@
package cli
import (
"bufio"
"context"
"flag"
"fmt"
"io"
"math"
"os"
"os/signal"
"slices"
"strings"
"syscall"
"time"
"gogs.inhome.blapointe.com/ana-ledger/src/ana"
"gogs.inhome.blapointe.com/ana-ledger/src/bank"
"gogs.inhome.blapointe.com/ana-ledger/src/bank/cache"
"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
"github.com/guptarohit/asciigraph"
"golang.org/x/crypto/ssh/terminal"
)
func Main() {
var config Config
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
fs.Var(&config.Files, "f", "paths to files")
fs.Var(&config.Query.Period, "period", "period can be YYYY, YYYY-mm, YYYY-mm-dd, x..y")
fs.StringVar(&config.Query.Sort, "S", "", "sort ie date")
fs.BoolVar(&config.Query.NoRounding, "no-rounding", false, "no rounding")
fs.BoolVar(&config.Compact, "c", false, "reg entries oneline")
fs.StringVar(&config.Query.With, "w", "", "regexp for transactions")
fs.IntVar(&config.Query.Depth, "depth", 0, "depth grouping")
fs.BoolVar(&config.Query.Normalize, "n", false, "normalize with default normalizer")
fs.BoolVar(&config.Query.USDOnly, "usd", false, "filter to usd")
fs.BoolVar(&config.Query.NoExchanging, "no-exchanging", true, "omit currency exchanges")
fs.StringVar(&config.BPI, "bpi", "", "path to bpi")
fs.StringVar(&config.CPI, "cpi", "", "path to cpi")
fs.StringVar(&config.GroupDate, "group-date", "^....-..-..", "date grouping")
fs.IntVar(&config.CPIYear, "cpiy", 0, "use cpi to convert usd to this year's value")
fs.BoolVar(&config.NoPercent, "no-percent", false, "compute percent")
if err := fs.Parse(os.Args[1:]); err != nil {
panic(err)
}
files := config.Files.Strings()
if len(files) == 0 {
panic("must specify at least one file")
}
ledgerFiles, err := ledger.NewFiles(files[0], files[1:]...)
if err != nil {
panic(err)
}
positional := fs.Args()
if len(positional) == 0 || len(positional[0]) < 3 {
panic("positional arguments required, ie bal|reg PATTERN MATCHING")
}
cmd := positional[0]
q, err := BuildQuery(config, positional[1:])
if err != nil {
panic(err)
}
deltas, err := ledgerFiles.Deltas()
if err != nil {
panic(err)
}
if period := config.Query.Period; !period.Empty() {
after := period.Start.Format("2006-01-02")
before := period.Stop.Format("2006-01-02")
deltas = deltas.Like(
ledger.LikeAfter(after),
ledger.LikeBefore(before),
)
}
pattern := ".*"
if depth := config.Query.Depth; depth > 0 {
patterns := []string{}
pattern = ""
for i := 0; i < depth; i++ {
pattern += "[^:]*:"
patterns = append([]string{strings.Trim(pattern, ":")}, patterns...)
}
pattern = strings.Join(patterns, "|")
}
group := ledger.GroupName(pattern)
bpis := make(ledger.BPIs)
if config.BPI != "" {
b, err := ledger.NewBPIs(config.BPI)
if err != nil {
panic(err)
}
bpis = b
}
cpiNormalizer := ana.NewNormalizer()
if config.CPI != "" && config.CPIYear > 0 {
c, err := ledger.NewBPIs(config.CPI)
if err != nil {
panic(err)
}
cpi := c["CPI"]
cpiy := cpi.Lookup(fmt.Sprintf("%d-06-01", config.CPIYear))
if cpiy == nil {
panic(fmt.Errorf("no cpi for year %d", config.CPIYear))
}
for date, value := range cpi {
cpiNormalizer = cpiNormalizer.With(".*", date, value/(*cpiy))
}
}
if config.Query.Normalize {
deltas = ana.NewDefaultNormalizer().Normalize(deltas)
}
if config.Query.With != "" {
deltas = deltas.Like(ledger.LikeWith(config.Query.With))
}
w := bufio.NewWriter(os.Stdout)
defer w.Flush()
maxAccW := 0
for _, delta := range deltas.Like(q).Group(group) {
if accW := len(delta.Name); accW > maxAccW {
maxAccW = accW
}
}
switch cmd[:3] {
case "bal": // balances
balances := deltas.Group(ledger.GroupDate(config.GroupDate)).Balances().
WithBPIs(bpis).
KindaLike(q).
KindaGroup(group).
Nonzero().
Normalize(cpiNormalizer, "9")
cumulatives := make(ledger.Balances)
cumulativesFormat := "%s%.2f"
if !config.NoPercent {
var sum float64
for key := range balances {
if _, ok := cumulatives[key]; !ok {
cumulatives[key] = make(ledger.Balance)
}
for currency, val := range balances[key] {
if currency == ledger.USD {
cumulatives[key][currency] = val
sum += val
} else {
cumulatives[key][currency] = 0
}
}
}
for key := range cumulatives {
cumulatives[key][ledger.USD] = 100 * cumulatives[key][ledger.USD] / sum
}
cumulativesFormat = "%.0f%%"
}
FPrintBalances(w, "", balances, cumulatives, config.Query.USDOnly, config.Query.Normalize, time.Now().Format("2006-01-02"), false, maxAccW, cumulativesFormat)
case "gra": // graph
dateGrouping := "^[0-9]{4}-[0-9]{2}"
if period := config.Query.Period; !period.Empty() {
day := time.Hour * 24
year := day * 365
r := period.Stop.Sub(period.Start)
if r > 10*year {
dateGrouping = "^[0-9]{4}"
} else if r > 5*year {
} else if r > year {
dateGrouping = "^[0-9]{4}-[0-9]{2}-[0-9]"
} else {
dateGrouping = "^[0-9]{4}-[0-9]{2}-[0-9]{2}"
}
}
deltas = deltas.Group(ledger.GroupDate(dateGrouping))
transactions := deltas.Transactions()
cumulative := make(ledger.Balances)
data := map[string][]float64{}
pushData := func() {
soFar := 0
for _, v := range data {
soFar = len(v)
}
for k, balance := range cumulative {
for curr, v := range balance {
if curr != ledger.USD {
continue
}
if _, ok := data[k]; !ok {
data[k] = make([]float64, soFar)
}
data[k] = append(data[k], v)
}
}
}
for i, transaction := range transactions {
if i > 0 && transactions[i-1][0].Date != transaction[0].Date {
pushData()
}
balances := ledger.Deltas(transaction).
Like(q).
Group(group).
Balances().
WithBPIsAt(bpis, transaction[0].Date).
Nonzero().
Normalize(cpiNormalizer, transaction[0].Date)
cumulative.PushAll(balances)
}
pushData()
labels := []string{}
for k := range data {
labels = append(labels, k)
}
slices.Sort(labels)
points := [][]float64{}
for _, k := range labels {
points = append(points, data[k])
}
for i := range points {
for j := range points[i] {
points[i][j] /= 1000.0
}
}
options := []asciigraph.Option{asciigraph.Precision(0)}
if _, h, err := terminal.GetSize(0); err == nil && h > 4 {
options = append(options, asciigraph.Height(h-4))
}
if len(labels) < 256 {
seriesColors := make([]asciigraph.AnsiColor, len(labels))
for i := range seriesColors {
seriesColors[i] = asciigraph.AnsiColor(i)
}
options = append(options, asciigraph.SeriesLegends(labels...))
options = append(options, asciigraph.SeriesColors(seriesColors...))
}
fmt.Println(asciigraph.PlotMany(points, options...))
case "rec": // reconcile via teller // DEAD
panic("dead and bad")
byDate := map[string]ledger.Deltas{}
for _, delta := range deltas {
delta := delta
byDate[delta.Date] = append(byDate[delta.Date], delta)
}
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
defer can()
teller, err := teller.New()
if err != nil {
panic(err)
}
client := cache.New(teller)
accounts, err := client.Accounts(ctx)
if err != nil {
panic(err)
}
inDay := func(date string, transaction bank.Transaction) bool {
return slices.ContainsFunc(byDate[date], func(d ledger.Delta) bool {
v := fmt.Sprintf("%.2f", d.Value)
nv := fmt.Sprintf("%.2f", -1.0*d.Value)
a := fmt.Sprintf("%.2f", transaction.Amount)
return v == a || nv == a
})
}
for _, acc := range accounts {
transactions, err := client.Transactions(ctx, acc)
if err != nil {
panic(err)
}
for _, transaction := range transactions {
if inDay(transaction.Date, transaction) {
continue
}
msg := "missing"
ts, err := time.ParseInLocation("2006-01-02", transaction.Date, time.Local)
if err != nil {
panic(err)
}
dayBefore := ts.Add(-24 * time.Hour).Format("2006-01-02")
dayAfter := ts.Add(24 * time.Hour).Format("2006-01-02")
if inDay(dayBefore, transaction) || inDay(dayAfter, transaction) {
msg = "1dayoff"
}
prefix := " "
if transaction.Status != "posted" {
prefix = "! "
}
fmt.Printf("[%s] %s $%7.2f %s%s (%s)\n", msg, transaction.Date, transaction.Amount, prefix, transaction.Details.CounterParty.Name, transaction.Description)
}
}
case "reg": // reg
deltas = deltas.Group(ledger.GroupDate(config.GroupDate))
transactions := deltas.Transactions()
cumulative := make(ledger.Balances)
for _, transaction := range transactions {
balances := ledger.Deltas(transaction).Like(q).Group(group).Balances().WithBPIsAt(bpis, transaction[0].Date).Nonzero().Normalize(cpiNormalizer, transaction[0].Date)
shouldPrint := false
shouldPrint = shouldPrint || len(balances) > 2
if config.Query.NoExchanging {
shouldPrint = shouldPrint || len(balances) > 1
for _, v := range balances {
shouldPrint = shouldPrint || len(v) == 1
}
} else {
shouldPrint = shouldPrint || len(balances) > 0
}
if shouldPrint {
cumulative.PushAll(balances)
cumulative = cumulative.Nonzero()
FPrintBalancesFor(transaction[0].Description, w, "\t\t", balances, cumulative, config.Query.USDOnly, config.Query.Normalize, transaction[0].Date, config.Compact, maxAccW, "%s%.2f")
}
}
default:
panic("unknown command " + positional[0])
}
}
func FPrintBalancesFor(description string, w io.Writer, linePrefix string, balances, cumulatives ledger.Balances, usdOnly, normalized bool, date string, compact bool, keyW int, cumulativeFormat string) {
if compact {
FPrintBalances(w, date+"\t", balances, cumulatives, usdOnly, normalized, date, compact, keyW, cumulativeFormat)
} else {
fmt.Fprintf(w, "%s\t%s\n", date, description)
FPrintBalances(w, linePrefix, balances, cumulatives, usdOnly, normalized, date, compact, keyW, cumulativeFormat)
}
}
func FPrintBalances(w io.Writer, linePrefix string, balances, cumulatives ledger.Balances, usdOnly, normalized bool, date string, fullKey bool, max int, cumulativeFormat string) {
maxes := map[ledger.Currency]float64{}
keys := []string{}
for k, v := range balances {
keys = append(keys, k)
for k2, v2 := range v {
if math.Abs(v2) > math.Abs(maxes[k2]) {
maxes[k2] = math.Abs(v2)
}
}
}
slices.Sort(keys)
normalizer := ana.NewDefaultNormalizer()
cumulativeFormat = strings.ReplaceAll(cumulativeFormat, "%", "%%")
format := fmt.Sprintf("%s%%-%ds\t%%s%%.2f ("+cumulativeFormat+")\n", linePrefix, max)
if normalized {
format = fmt.Sprintf("%s%%-%ds\t%%s%%.2f (%%s%%.2f (%%.2f @%%.2f ("+cumulativeFormat+")))\n", linePrefix, max)
}
for i, key := range keys {
printableKey := key
if fullKey {
} else if i == 0 {
} else {
commonPrefixLen := func() int {
j := 0
n := len(keys[i])
if n2 := len(keys[i-1]); n2 < n {
n = n2
}
for j = 0; j < n; j++ {
if keys[i-1][j] != keys[i][j] {
break
}
}
for keys[i][j] != ':' && j > 0 {
j -= 1
}
return j
}()
printableKey = strings.Repeat(" ", commonPrefixLen) + keys[i][commonPrefixLen:]
}
currencies := []ledger.Currency{}
for currency := range balances[key] {
currencies = append(currencies, currency)
}
slices.Sort(currencies)
for _, currency := range currencies {
printableCurrency := currency
format := format
if printableCurrency != "$" {
printableCurrency += " "
format = strings.ReplaceAll(format, "%.2f", "%.3f")
}
if usdOnly && printableCurrency != "$" {
continue
}
cumulative := balances[key][currency]
if balance, ok := cumulatives[key]; !ok {
} else if value, ok := balance[currency]; !ok {
} else {
cumulative = value
}
printingPercents := strings.Contains(cumulativeFormat, "%%%%")
if !printingPercents {
if !normalized {
fmt.Fprintf(w, format,
printableKey,
printableCurrency, balances[key][currency],
printableCurrency, cumulative,
)
} else {
factor := normalizer.NormalizeFactor(key, date)
trailingMax := maxes[currency] - math.Abs(balances[key][currency])
fmt.Fprintf(w, format, printableKey, printableCurrency, balances[key][currency], printableCurrency, cumulative, cumulative*factor, factor, printableCurrency, factor*trailingMax)
}
} else {
if !normalized {
fmt.Fprintf(w, format, printableKey, printableCurrency, balances[key][currency], cumulative)
} else {
factor := normalizer.NormalizeFactor(key, date)
trailingMax := maxes[currency] - math.Abs(balances[key][currency])
fmt.Fprintf(w, format, printableKey, printableCurrency, balances[key][currency], printableCurrency, cumulative, cumulative*factor, factor, factor*trailingMax)
}
}
}
}
}

100
cmd/cli/query.go Normal file
View File

@@ -0,0 +1,100 @@
package cli
import (
"fmt"
"log"
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
)
type Query struct{}
func BuildQuery(config Config, args args) (ledger.Like, error) {
var result ledger.Like
var err error
func() {
defer func() {
if err := recover(); err != nil {
err = fmt.Errorf("panicked: %v", err)
}
}()
result, err = buildQuery(config, args)
}()
return result, err
}
func buildQuery(config Config, args args) (ledger.Like, error) {
likeName := func(s string) ledger.Like {
return ledger.LikeName(s)
}
andLike := func(a, b ledger.Like) ledger.Like {
return (ledger.Likes{a, b}).All
}
orLike := func(a, b ledger.Like) ledger.Like {
return (ledger.Likes{a, b}).Any
}
notLike := func(a ledger.Like) ledger.Like {
return ledger.LikeNot(a)
}
if !args.more() {
return likeName(""), nil
}
like := likeName("ajskfdlsjfkdasj")
for args.more() {
switch args.peek() {
case "and":
args.pop()
var and ledger.Like
switch args.peek() {
case "not":
args.pop()
log.Println("and not", args.peek())
and = notLike(likeName(args.pop()))
default:
log.Println("and ", args.peek())
and = likeName(args.pop())
}
like = andLike(like, and)
case "not":
args.pop()
log.Println("or not ", args.peek())
like = orLike(like, notLike(
likeName(args.pop()),
))
default:
log.Println("or ", args.peek())
like = orLike(like,
likeName(args.pop()),
)
}
}
return like, nil
}
type args []string
func (args *args) pop() string {
if !args.more() {
panic("expected more arguments")
}
first := ""
if len(*args) > 0 {
first = (*args)[0]
*args = (*args)[1:]
}
return first
}
func (args *args) peek() string {
if len(*args) == 0 {
return ""
}
return (*args)[0]
}
func (args *args) more() bool {
return len(*args) > 0
}

12
cmd/cli/run.sh Normal file
View File

@@ -0,0 +1,12 @@
#! /bin/bash
cmd="bal"
case "$1" in
bal|reg|gra|graph|rec )
cmd="$1"
shift
;;
esac
cd "$(dirname "$(realpath "$BASH_SOURCE")")"
go run ../ cli "$@" $(printf " -f %s" $HOME/Sync/Core/ledger/eras/2022-/*.txt) "$cmd" :AssetAccount:

View File

@@ -1,55 +1,24 @@
package http
import (
"embed"
"encoding/json"
"flag"
"fmt"
"io"
"io/fs"
"log"
"maps"
"net/http"
"os"
"path"
"slices"
"sort"
"strconv"
"strings"
"time"
_ "embed"
"github.com/go-echarts/go-echarts/v2/charts"
"github.com/go-echarts/go-echarts/v2/opts"
"gogs.inhome.blapointe.com/ana-ledger/src/ana"
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
)
//go:embed public/*
var _staticFileDir embed.FS
var publicHandler = func() http.Handler {
if os.Getenv("DEBUG") != "" {
return http.FileServer(http.Dir("./http/public"))
}
sub, err := fs.Sub(_staticFileDir, "public")
if err != nil {
panic(err)
}
return http.FileServer(http.FS(sub))
}()
func Main() {
foo := flag.String("foo", "bal", "bal or reg")
likeName := flag.String("like-name", ".", "regexp to match")
likeBefore := flag.String("like-before", "9", "date str to compare")
likeAfter := flag.String("like-after", "0", "date str to compare")
likeLedger := flag.Bool("like-ledger", false, "limit data to these -like-* rather than zoom to these -like-*")
groupName := flag.String("group-name", ".*", "grouping to apply to names")
groupDate := flag.String("group-date", ".*", "grouping to apply to dates")
bpiPath := flag.String("bpi", "/dev/null", "bpi file")
jsonOutput := flag.Bool("json", false, "json output")
httpOutput := flag.String("http", "", "http output listen address, like :8080")
httpOutput := flag.String("http", ":8080", "http output listen address, like :8080")
flag.Parse()
if flag.NArg() < 1 {
@@ -61,452 +30,17 @@ func Main() {
panic(err)
}
bpis := make(ledger.BPIs)
if *bpiPath != "" {
bpis, err = ledger.NewBPIs(*bpiPath)
if err != nil {
panic(err)
}
}
if *httpOutput != "" {
foo := func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/api") {
publicHandler.ServeHTTP(w, r)
return
}
reqF := f
if queryF := r.URL.Query().Get("f"); queryF != "" {
queryF = path.Join("http", queryF)
reqF, err = ledger.NewFiles(queryF)
if err != nil {
panic(err)
}
}
switch r.URL.Path {
case "/api/transactions":
lastNLines, err := reqF.TempGetLastNLines(20)
if err != nil {
panic(err)
}
deltas, err := reqF.Deltas()
if err != nil {
panic(err)
}
json.NewEncoder(w).Encode(map[string]any{
"deltas": deltas.Like(ledger.LikeAfter(time.Now().Add(-1 * time.Hour * 24 * 365 / 2).Format("2006-01"))),
"balances": deltas.Balances().Like("^AssetAccount:").WithBPIs(bpis),
"lastNLines": lastNLines,
})
return
case "/api/lastnlines":
if r.Method != http.MethodPut {
http.NotFound(w, r)
return
}
var lines []string
if err := json.NewDecoder(r.Body).Decode(&lines); err != nil {
panic(err)
}
if err := reqF.TempSetLastNLines(20, lines); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
w.WriteHeader(http.StatusResetContent)
return
}
deltas, err := f.Deltas()
if err != nil {
panic(err)
}
deltas = deltas.Group(ledger.GroupName(*groupName), ledger.GroupDate(*groupDate))
like := ledger.Likes{
ledger.LikeName(*likeName),
ledger.LikeBefore(*likeBefore),
ledger.LikeAfter(*likeAfter),
}
foolike := make(ledger.Likes, 0)
for _, v := range r.URL.Query()["likeName"] {
foolike = append(foolike, ledger.LikeName(v))
}
for _, v := range r.URL.Query()["likeAfter"] {
foolike = append(foolike, ledger.LikeAfter(v))
}
for _, v := range r.URL.Query()["likeBefore"] {
foolike = append(foolike, ledger.LikeBefore(v))
}
if len(foolike) == 0 {
foolike = like
}
deltas = deltas.Like(foolike...)
// MODIFIERS
for i, whatIf := range r.URL.Query()["whatIf"] {
fields := strings.Fields(whatIf)
date := "2001-01"
name := fields[0]
currency := ledger.Currency(fields[1])
value, err := strconv.ParseFloat(fields[2], 64)
if err != nil {
panic(err)
}
deltas = append(deltas, ledger.Delta{
Date: date,
Name: name,
Value: value,
Currency: currency,
Description: fmt.Sprintf("?whatIf[%d]", i),
})
}
register := deltas.Register()
predicted := make(ledger.Register)
bpis := maps.Clone(bpis)
if predictionMonths, err := strconv.ParseInt(r.URL.Query().Get("predictionMonths"), 10, 16); err == nil && predictionMonths > 0 {
window := time.Hour * 24.0 * 365.0 / 12.0 * time.Duration(predictionMonths)
// TODO whatif
prediction := make(ana.Prediction, 0)
for _, spec := range r.URL.Query()["prediction"] {
idx := strings.Index(spec, "=")
k := spec[:idx]
fields := strings.Fields(spec[idx+1:])
switch k {
case "interest":
apy, err := strconv.ParseFloat(fields[2], 64)
if err != nil {
panic(err)
}
prediction = append(prediction, ana.NewInterestPredictor(fields[0], fields[1], apy))
case "autoContributions":
prediction = append(prediction, ana.NewAutoContributionPredictor(register))
case "contributions":
name := fields[0]
currency := ledger.Currency(fields[1])
value, err := strconv.ParseFloat(fields[2], 64)
if err != nil {
panic(err)
}
prediction = append(prediction, ana.NewContributionPredictor(ledger.Balances{name: ledger.Balance{currency: value}}))
default:
panic(k)
}
}
predicted = prediction.Predict(register, window)
for _, currencyRate := range r.URL.Query()["predictFixedGrowth"] {
currency := strings.Split(currencyRate, "=")[0]
rate, err := strconv.ParseFloat(strings.Split(currencyRate, "=")[1], 64)
if err != nil {
panic(err)
}
bpis, err = ana.BPIsWithFixedGrowthPrediction(bpis, window, currency, rate)
if err != nil {
panic(err)
}
}
}
if r.URL.Query().Get("bpi") == "true" {
register = register.WithBPIs(bpis)
predicted = predicted.WithBPIs(bpis)
}
if zoomStart, err := time.ParseInLocation("2006-01", r.URL.Query().Get("zoomStart"), time.Local); err == nil {
register = register.Between(zoomStart, time.Now().Add(time.Hour*24*365*100))
predicted = predicted.Between(zoomStart, time.Now().Add(time.Hour*24*365*100))
}
// /MODIFIERS
dates := register.Dates()
names := register.Names()
for _, date := range predicted.Dates() {
found := false
for i := range dates {
found = found || dates[i] == date
}
if !found {
dates = append(dates, date)
}
}
for _, name := range predicted.Names() {
found := false
for i := range names {
found = found || names[i] == name
}
if !found {
names = append(names, name)
}
}
instant := map[string]string{}
toChart := func(cumulative bool, display string, reg ledger.Register) Chart {
nameCurrencyDateValue := map[string]map[ledger.Currency]map[string]float64{}
for date, balances := range reg {
for name, balance := range balances {
for currency, value := range balance {
if _, ok := nameCurrencyDateValue[name]; !ok {
nameCurrencyDateValue[name] = make(map[ledger.Currency]map[string]float64)
}
if _, ok := nameCurrencyDateValue[name][currency]; !ok {
nameCurrencyDateValue[name][currency] = make(map[string]float64)
}
nameCurrencyDateValue[name][currency][date] += value
}
}
}
chart := NewChart("line")
if v := display; v != "" {
chart = NewChart(v)
}
chart.AddX(dates)
if cumulative {
for _, name := range names {
currencyDateValue := nameCurrencyDateValue[name]
for currency, dateValue := range currencyDateValue {
series := make([]int, len(dates))
for i := range dates {
var lastValue float64
for j := range dates[:i+1] {
if newLastValue, ok := dateValue[dates[j]]; ok {
lastValue = newLastValue
}
}
series[i] = int(lastValue)
}
key := fmt.Sprintf("%s (%s)", name, currency)
for i := range dates {
if !(reg.Dates()[0] <= dates[i] && dates[i] <= reg.Dates()[len(reg.Dates())-1]) {
series[i] = 0
} else {
instant[key] = fmt.Sprintf("@%s %v", dates[i], series[i])
}
}
if slices.Min(series) != 0 || slices.Max(series) != 0 {
chart.AddY(key, series)
}
}
}
} else {
for _, name := range names {
currencyDateValue := nameCurrencyDateValue[name]
for currency, dateValue := range currencyDateValue {
series := make([]int, len(dates))
for i := range dates {
var prevValue float64
var lastValue float64
for j := range dates[:i+1] {
if newLastValue, ok := dateValue[dates[j]]; ok {
prevValue = lastValue
lastValue = newLastValue
}
}
series[i] = int(lastValue - prevValue)
}
for i := range series { // TODO no prior so no delta
if series[i] != 0 {
series[i] = 0
break
}
}
key := fmt.Sprintf("%s (%s)", name, currency)
for i := range dates {
if !(reg.Dates()[0] <= dates[i] && dates[i] <= reg.Dates()[len(reg.Dates())-1]) {
series[i] = 0
} else {
instant[key] = fmt.Sprintf("@%s %v", dates[i], series[i])
}
}
if slices.Min(series) != 0 || slices.Max(series) != 0 {
chart.AddY(key, series)
}
}
}
}
return chart
}
primary := toChart(r.URL.Path == "/api/bal", r.URL.Query().Get("chart"), register)
if len(predicted) > 0 {
primary.Overlap(toChart(r.URL.Path == "/api/bal", "line", predicted))
}
if err := primary.Render(w); err != nil {
panic(err)
}
for k, v := range instant {
fmt.Fprintf(w, "<br>\n%s = %s", k, v)
}
}
log.Println("listening on", *httpOutput)
if err := http.ListenAndServe(*httpOutput, http.HandlerFunc(foo)); err != nil {
panic(err)
}
} else {
deltas, err := f.Deltas()
if err != nil {
panic(err)
}
deltas = deltas.Group(ledger.GroupName(*groupName), ledger.GroupDate(*groupDate))
like := ledger.Likes{ledger.LikeName(*likeName)}
if *likeLedger {
like = append(like, ledger.LikeBefore(*likeBefore))
like = append(like, ledger.LikeAfter(*likeAfter))
deltas = deltas.Like(like...)
} else {
deltas = deltas.Like(like...)
like = append(like, ledger.LikeBefore(*likeBefore))
like = append(like, ledger.LikeAfter(*likeAfter))
}
jsonResult := []any{}
switch *foo {
case "reg":
sort.Slice(deltas, func(i, j int) bool {
return deltas[i].Debug() < deltas[j].Debug()
})
register := deltas.Register()
for i := range deltas {
if like.All(deltas[i]) {
if !*jsonOutput {
fmt.Printf("%s (%+v)\n", deltas[i].Debug(), register[deltas[i].Date][deltas[i].Name].Debug())
} else {
jsonResult = append(jsonResult, map[string]any{
"name": deltas[i].Name,
"delta": deltas[i],
"balance": register[deltas[i].Date][deltas[i].Name],
})
}
}
}
case "bal":
deltas = deltas.Like(like...)
for k, v := range deltas.Balances() {
results := []string{}
for subk, subv := range v {
results = append(results, fmt.Sprintf("%s %.2f", subk, subv))
}
if len(results) > 0 {
if !*jsonOutput {
fmt.Printf("%s\t%s\n", k, strings.Join(results, " + "))
} else {
jsonResult = append(jsonResult, map[string]any{
"name": k,
"balance": v,
})
}
}
}
default:
panic(fmt.Errorf("not impl %q", *foo))
}
if *jsonOutput {
json.NewEncoder(os.Stdout).Encode(jsonResult)
}
r := NewRouter(
f,
*likeName,
*likeBefore,
*likeAfter,
*groupName,
*groupDate,
*bpiPath,
)
log.Println("listening on", *httpOutput)
if err := http.ListenAndServe(*httpOutput, r); err != nil {
panic(err)
}
}
type Chart interface {
AddX(interface{})
AddY(string, []int)
Render(io.Writer) error
Overlap(Chart)
}
func NewChart(name string) Chart {
switch name {
case "line":
return NewLine()
case "bar":
return NewBar()
case "stack":
return NewStack()
default:
panic("bad chart name " + name)
}
}
type Line struct {
*charts.Line
}
func NewLine() Line {
return Line{Line: charts.NewLine()}
}
func (line Line) AddX(v interface{}) {
line.SetXAxis(v)
}
func (line Line) AddY(name string, v []int) {
y := make([]opts.LineData, len(v))
for i := range y {
y[i].Value = v[i]
}
line.AddSeries(name, y).
SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{
Stack: "stackB",
}))
}
func (line Line) Overlap(other Chart) {
overlapper, ok := other.(charts.Overlaper)
if !ok {
panic(fmt.Sprintf("cannot overlap %T", other))
}
line.Line.Overlap(overlapper)
}
type Bar struct {
*charts.Bar
}
func NewBar() Bar {
return Bar{Bar: charts.NewBar()}
}
func (bar Bar) AddX(v interface{}) {
bar.SetXAxis(v)
}
func (bar Bar) AddY(name string, v []int) {
y := make([]opts.BarData, len(v))
for i := range v {
y[i].Value = v[i]
}
bar.AddSeries(name, y)
}
func (bar Bar) Overlap(other Chart) {
overlapper, ok := other.(charts.Overlaper)
if !ok {
panic(fmt.Sprintf("cannot overlap %T", other))
}
bar.Bar.Overlap(overlapper)
}
type Stack struct {
Bar
}
func NewStack() Stack {
bar := NewBar()
bar.SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{Stack: "x"}))
return Stack{Bar: bar}
}
func (stack Stack) AddY(name string, v []int) {
y := make([]opts.BarData, len(v))
for i := range v {
y[i].Value = v[i]
}
stack.AddSeries(name, y).
SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{
Stack: "stackA",
}))
}

View File

@@ -3088,4 +3088,3 @@
2023-10-27 GOOD EARTH FOODS OR
AssetAccount:Chase:5876 $-173.57
Withdrawal:GOODEARTHFOODSOR

View File

@@ -47,7 +47,7 @@
<span>
<label for="likeName">likeName</label>
<input name="likeName" type="text" value="AssetAccount"/>
<input name="likeName" type="text" value="Bel:AssetAccount"/>
</span>
<span>
@@ -72,7 +72,7 @@
<span>
<label for="prediction">prediction</label>
<input name="prediction" type="text" value="interest=AssetAccount:Cash \$ 0.02&prediction=contributions=AssetAccount:Bonds $ 1875&prediction=interest=AssetAccount:Monthly \$ 0.03&prediction=contributions=AssetAccount:Monthly $ 2500"/>
<input name="prediction" type="text" value="interest=Bel:AssetAccount:Cash \$ 0.02&prediction=contributions=Bel:AssetAccount:Bonds $ 1916&prediction=interest=Bel:AssetAccount:Monthly \$ 0.03&prediction=contributions=Bel:AssetAccount:Monthly $ 3500"/>
</span>
<span>
@@ -82,7 +82,7 @@
<span>
<label for="whatIf">whatIf</label>
<input name="whatIf" type="text" value="AssetAccount:Cash $ -.10000"/>
<input name="whatIf" type="text" value="Bel:AssetAccount:Cash $ -.10000"/>
</span>
<span>

View File

@@ -17,9 +17,10 @@
<h1>Moolah2 Hub</h1>
<ul style="line-height: 3em;">
<li><a href="/transactions.html">Transactions on Shared Chase</a></li>
<li><a href="/api/trends">Where does the house money go?</a></li>
<li><a href="/explore.html">Explore Bel's Money</a></li>
<li><a href="/api/bal?x=y&mode=bal&likeName=AssetAccount&chart=stack&predictionMonths=120&bpi=true&zoomStart=YYYY-MM&prediction=interest=AssetAccount:Cash%20\$%200.02&prediction=contributions=AssetAccount:Bonds%20$%201875&prediction=interest=AssetAccount:Monthly%20\$%200.03&prediction=contributions=AssetAccount:Monthly%20$%202500&predictFixedGrowth=VBTLX=0.02&predictFixedGrowth=GLD=0.02&predictFixedGrowth=FXAIX=0.03&predictFixedGrowth=FSPSX=0.03&whatIf=AssetAccount:Cash%20$%20-.10000&=">Project Bel's Net Worth</a></li>
<li><a href="/api/reg?x=y&mode=reg&likeName=Withdrawal:&chart=stack&predictionMonths=3&bpi=false&zoomStart=YYYY-MM&prediction=autoContributions=&predictFixedGrowth=VBTLX=0&whatIf=AssetAccount:Cash%20$%20-.10000&=">Expect Bel's Expenses</a></li>
<!--<li><a href="/api/reg?x=y&mode=reg&likeName=Bel:Withdrawal:&chart=stack&predictionMonths=3&bpi=false&zoomStart=YYYY-MM&prediction=autoContributions=&predictFixedGrowth=VBTLX=0&whatIf=AssetAccount:Cash%20$%20-.10000&=">Expect Bel's Expenses</a></li>-->
</ul>
</body>
<footer>

View File

@@ -1,27 +1,40 @@
<html>
<header>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css">
<style>
input[type="text"] {
border: 1px solid black;
}
.amended {
border: 2px solid red;
}
.revised {
border: 2px solid green;
}
</style>
<script>
function http(method, remote, callback, body) {
var xmlhttp = new XMLHttpRequest();
var xmlhttp = new XMLHttpRequest()
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == XMLHttpRequest.DONE) {
callback(xmlhttp.responseText, xmlhttp.status)
}
};
xmlhttp.open(method, remote, true);
}
xmlhttp.open(method, remote, true)
if (typeof body == "undefined") {
body = null
}
xmlhttp.send(body);
xmlhttp.send(body)
}
function callback(responseBody, responseStatus) {
}
var f = String(window.location).split("/transactions.html")[1]
if (!f) {
f = "/moolah.dat"
}
f = "." + f
//var f = String(window.location).split("/transactions.html")[1]
//if (!f) {
// f = "/ledger.dat"
//}
//f = "." + f
function init() {
const zeroPad = (num, places) => String(num).padStart(places, '0')
var d = new Date()
@@ -30,17 +43,35 @@
var iframe = document.getElementsByTagName("iframe")[0]
iframe.src = iframe.src.replace("YYYY-MM", replacement)
load(f)
load()
}
function load(f) {
http("GET", "/api/transactions?f="+f, (body, status) => {
function load(callback) {
http("GET", "/api/transactions", (body, status) => {
var d = JSON.parse(body)
console.log("loading", d)
loadNormalized(d.normalized)
loadBalances(d.balances)
loadDeltas(d.deltas)
loadLastNLines(d.lastNLines)
loadTransactions(d.transactions)
if (callback != null) {
callback()
}
})
}
function loadNormalized(normalized) {
console.log("loading normalized", normalized)
var result = `<table>`
for (var k in normalized) {
result += `<tr style="display: flex; flex-direction: row; width: 100%; justify-content: space-between;"><td>${k}</td><td>${Math.floor(normalized[k]["$"])}</td></tr>`
}
result += `</table>`
document.getElementById("norm").innerHTML = result
}
function loadBalances(balances) {
console.log("loading balances", balances)
var result = `<table>`
for (var k in balances) {
result += `<tr style="display: flex; flex-direction: row; width: 100%; justify-content: space-between;"><td>${k}</td><td>${Math.floor(balances[k]["$"])}</td></tr>`
@@ -48,77 +79,163 @@
result += `</table>`
document.getElementById("bal").innerHTML = result
}
function loadDeltas(deltas) {
console.log(deltas[0])
for (var i = 0; i < deltas.length/2; i++) {
tmp = deltas[i]
deltas[i] = deltas[deltas.length-1-i]
deltas[deltas.length-1-i] = tmp
}
console.log(deltas[0])
function loadTransactions(transactions) {
transactions.reverse()
console.log(transactions[0])
var result = `<table>`
for (var k of deltas) {
result += `<tr>`
result += ` <td>${k.Date}</td>`
result += ` <td>${k.Description}</td>`
result += ` <td>${k.Name}</td>`
result += ` <td style="text-align: right">${k.Currency}</td>`
result += ` <td style="text-align: right">${k.Value}</td>`
for (var t of transactions) {
result += `<tr name="transaction-${t[0].Date}-${t[0].Description}" transaction='${btoa(JSON.stringify(t))}' style="cursor: crosshair;" onclick="stageEdit(this); return false;" class="${t[0].Transaction.includes("inbox") ? "revised" : ""}">`
result += ` <td style="width: 6em;">${t[0].Date}</td>`
result += ` <td style="width: 10em;">${t[0].Description}</td>`
result += ` <td><table style="margin: 0;">`
for (var delta of t) {
result += ` <tr>`
result += ` <td><span style="font-variant: petite-caps;">${delta.Name.split(":")[0]}</span><span style="opacity: 0.6;"> :${delta.Name.split(":").slice(1, 100).join(":")}</span></td>`
result += ` <td style="text-align: right; width: 2em;">${delta.Payee ? delta.Currency : ""}</td>`
result += ` <td style="text-align: right; width: 5em;">${delta.Payee ? delta.Value : ""}</td>`
result += ` </tr>`
}
result += ` </table></td>`
result += `</tr>`
}
result += `</table>`
document.getElementById("reg").innerHTML = result
}
function setLastNLines(form) {
http("PUT", "/api/lastnlines?f="+f, (body, status) => {
if (status == 205) {
init()
}
document.getElementById("lastNLinesStatus").innerHTML = `(${status}) ${body}`
}, JSON.stringify(form.elements["lastNLines"].value.split("\n")))
}
function loadLastNLines(lastNLines) {
var result = `<form onsubmit="setLastNLines(this); return false;" action="#">`
result += ` <div>${f}</div>`
result += ` <textarea id="lastNLinesTextarea" name="lastNLines" style="height: 30em;">`
for (var k in lastNLines) {
result += lastNLines[k] + "\n"
function stageEdit(row) {
const xactionJSON = atob(row.attributes.transaction.value)
const xaction = JSON.parse(xactionJSON)
console.log(xaction)
row.attributes.onclick = ""
row.onclick = ""
row.style = ""
console.log(row)
var result = `<td colspan="3">`
result += `<form method="modal">`
result += ` <div>`
result += ` <label for="date">Date</label>`
result += ` <input type="text" name="date" value='${xaction[0].Date}' style="width: 100%;"/>`
result += ` </div>`
result += ` <div>`
result += ` <label for="description">Description</label>`
result += ` <input type="text" name="description" value='${xaction[0].Description}' style="width: 100%;"/>`
result += ` </div>`
for (var i in xaction) {
var delta = xaction[i]
result += `<hr>`
result += `<div style="display: flex; flex-direction: row; width: 100%; margin-top: 1em;">`
result += ` <div style="flex-grow: 1; margin-left: 1em; margin-right: 1em;">`
result += ` <label for="name${i}">Name${i}</label>`
result += ` <input type="text" name="name${i}" value='${delta.Name}' style="width: 100%;"/>`
result += ` </div>`
result += ` <div style="margin-left: 1em; margin-right: 1em;">`
result += ` <label for="value${i}">Value${i}</label>`
result += ` <input type="text" name="value${i}" value='${delta.Value}' style="width: 100%;"/>`
result += ` </div>`
result += ` <input type="button" name="submit${i}" value="Submit${i}" onclick="submitEdit(this); return false;" delta="${btoa(JSON.stringify(delta))}"/>`
result += `</div>`
}
result += ` </textarea>`
result += ` <input type="submit">`
result += `</form>`
document.getElementById("lastNLines").innerHTML = result
result += `</form`
result += `</td>`
row.innerHTML = result
}
function submitEdit(btn) {
const old = JSON.parse(atob(btn.attributes.delta.value))
const now = JSON.parse(atob(btn.attributes.delta.value))
const idx = btn.name.split("submit")[1]
const form = btn.parentElement.parentElement
now.Value = parseFloat(form[`value${idx}`].value)
now.Name = form[`name${idx}`].value
now.Description = form[`description`].value
now.Date = form[`date`].value
var different = false
for(var k in old) {
different = different || old[k] != now[k]
}
if (!different) {
return
}
http("PUT", "/api/amend", (body, status) => {
if (status != 200)
throw(`Unexpected status ${status}: ${body}`)
load(() => {
/*
var elements = document.getElementsByName(`transaction-${old.Date}-${old.Description}`)
for (var ele of elements)
ele.className = "amended"
var elements = document.getElementsByName(`transaction-${now.Date}-${now.Description}`)
for (var ele of elements)
ele.className = "revised"
*/
})
}, JSON.stringify({
old: old,
now: now,
}))
}
function create() {
http("POST", "/api/create", (body, status) => {
if (status != 200)
throw(`Unexpected status ${status}: ${body}`)
loadTransactions([])
load()
}, JSON.stringify({}))
}
function stage(who, contributesToHouse) {
var benefactor = who
var beneficiary = "AssetAccount:Chase:5876"
if (!contributesToHouse) {
var tmp = beneficiary
beneficiary = benefactor
benefactor = tmp
}
const zeroPad = (num, places) => String(num).padStart(places, '0')
var d = new Date()
const zeroPad = (num, places) => String(num).padStart(places, '0')
const today = `${d.getFullYear()}-${zeroPad(1+d.getMonth(), 2)}-${zeroPad(d.getDate(), 2)}`
var who_currency = who.split(":").at(-1).toUpperCase()
var benefactor = who
var benefactor_value = `-0.00 ${who_currency}`
var beneficiary = "AssetAccount:Chase:5876"
var beneficiary_value = `$0.00`
if (!contributesToHouse) {
beneficiary_value = `$-0.00`
benefactor_value = `0.00 ${who_currency}`
}
console.log(`@${today} ${benefactor} gave to ${beneficiary}`)
document.getElementById("lastNLinesTextarea").value += `\n${today} TODO moolah2`
document.getElementById("lastNLinesTextarea").value += `\n ${beneficiary} $0.00`
document.getElementById("lastNLinesTextarea").value += `\n ${benefactor}`
}
</script>
</header>
<body onload="init();" style="min-width: 1024px;">
<body onload="init();" style="">
<h2>Moolah2</h2>
<details open>
<summary>Normalized</summary>
<div id="norm">
</details>
<details>
<summary>Balance</summary>
<div id="bal">
</div>
<details>
<summary><i>Look at this graph</i></summary>
<iframe style="background: white; width: 100%;" src="/api/reg?x=y&mode=reg&likeName=Withdrawal:[0123]&chart=stack&predictionMonths=6&prediction=autoContributions=&bpi=true&zoomStart=YYYY-MM"></iframe>
<iframe style="background: white; width: 100%; resize: both;" src="/api/reg?x=y&mode=reg&likeName=Withdrawal:[0123]&chart=stack&predictionMonths=6&prediction=autoContributions=&bpi=true&zoomStart=YYYY-MM"></iframe>
</details>
<details>
<summary><i>Where did the money go</i></summary>
<iframe style="background: white; width: 100%; resize: both;" src="/api/trends"></iframe>
</details>
</details>
<details open>
<details open style="display: none;">
<summary>Edit</summary>
<div style="display:flex; flex-direction:row; width:100%; justify-content:space-between;">
<div>
@@ -130,13 +247,14 @@
<input type="button" onclick="stage('AssetAccount:Bel', false)" value="Stage Bel's Charge"/>
</div>
</div>
<div id="lastNLinesStatus">
</div>
<div id="lastNLines">
</div>
</details>
<details>
<details open>
<summary>Register</summary>
<form action="#" onsubmit="create(); return false;">
<button style="width: 100%">
CREATE
</button>
</form>
<div id="reg">
</div>
</details>

485
cmd/http/router.go Normal file
View File

@@ -0,0 +1,485 @@
package http
import (
"embed"
"encoding/json"
"fmt"
"io"
"io/fs"
"math"
"net/http"
"os"
"slices"
"strconv"
"strings"
"time"
"gogs.inhome.blapointe.com/ana-ledger/src/ana"
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
"gogs.inhome.blapointe.com/ana-ledger/src/view"
)
//go:embed public/*
var _staticFileDir embed.FS
type Router struct {
files ledger.Files
like struct {
name string
before string
after string
}
group struct {
name string
date string
}
bpiPath string
}
func NewRouter(files ledger.Files, likeName, likeBefore, likeAfter string, groupName, groupDate string, bpiPath string) Router {
r := Router{}
r.files = files
r.like.name = likeName
r.like.before = likeBefore
r.like.after = likeAfter
r.group.name = groupName
r.group.date = groupDate
r.bpiPath = bpiPath
return r
}
func (router Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch strings.Split(r.URL.Path, "/")[1] {
case "api":
router.API(w, r)
default:
router.FS(w, r)
}
}
func (router Router) FS(w http.ResponseWriter, r *http.Request) {
if os.Getenv("DEBUG") != "" {
http.FileServer(http.Dir("./http/public")).ServeHTTP(w, r)
} else {
sub, err := fs.Sub(_staticFileDir, "public")
if err != nil {
panic(err)
}
http.FileServer(http.FS(sub)).ServeHTTP(w, r)
}
}
func (router Router) API(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/transactions":
router.APITransactions(w, r)
case "/api/amend":
router.APIAmend(w, r)
case "/api/create":
router.APICreate(w, r)
case "/api/trends":
router.APITrends(w, r)
case "/api/reg", "/api/bal":
router.APIReg(w, r)
default:
http.NotFound(w, r)
}
}
func (router Router) APITransactions(w http.ResponseWriter, r *http.Request) {
bpis, err := router.bpis()
if err != nil {
panic(err)
}
deltas, err := router.files.Deltas()
if err != nil {
panic(err)
}
houseRelatedDeltas := deltas.Like(ledger.LikeTransactions(
deltas.Like(ledger.LikeName(`^House`))...,
))
recent := time.Hour * 24 * 365 / 6
normalizer := ana.NewDefaultNormalizer()
{
deltas := houseRelatedDeltas.
Like(ledger.LikeAfter(time.Now().Add(-1 * recent).Format("2006-01")))
balances := houseRelatedDeltas.Balances().
Like(`^(Zach|Bel|House[^:]*:Debts:)`).
Group(`^[^:]*`).
WithBPIs(bpis)
transactions := houseRelatedDeltas.
Like(ledger.LikeAfter(time.Now().Add(-1 * recent).Format("2006-01"))).
Transactions()
normalized := normalizer.Normalize(houseRelatedDeltas).Balances().
Like(`^(Zach|Bel):`).
Group(`^[^:]*:`).
WithBPIs(bpis)
var biggest float64
for _, v := range normalized {
if v := math.Abs(v["$"]); v > biggest {
biggest = v
}
}
ks := []string{}
for k := range normalized {
ks = append(ks, k)
}
for _, k := range ks {
v := normalized[k]
if v := math.Abs(v["$"]); v < biggest {
normalizedDelta := biggest - v
normalizedFactor := normalizer.NormalizeFactor(k, time.Now().Format("2006-01-02"))
normalized[fmt.Sprintf(`(%s trailing $)`, k)] = ledger.Balance{"$": normalizedDelta * normalizedFactor}
}
}
json.NewEncoder(w).Encode(map[string]any{
"deltas": deltas,
"balances": balances,
"normalized": normalized,
"transactions": transactions,
})
}
}
func (router Router) APITrends(w http.ResponseWriter, r *http.Request) {
bpis, err := router.bpis()
if err != nil {
panic(err)
}
deltas, err := router.files.Deltas()
if err != nil {
panic(err)
}
recent := time.Hour * 24 * 365 / 2
pie := func(title, groupName string, min int) {
recentHouseRelatedDeltas := deltas.
Like(ledger.LikeTransactions(
deltas.Like(ledger.LikeName(`^House`))...,
)).
Like(ledger.LikeAfter(time.Now().Add(-1 * recent).Format("2006-01"))).
Group(ledger.GroupName(groupName)).
Group(ledger.GroupDate(`^[0-9]*-[0-9]*`)).
Like(ledger.LikeNotName(`^$`))
monthsToDeltas := map[string]ledger.Deltas{}
for _, delta := range recentHouseRelatedDeltas {
monthsToDeltas[delta.Date] = append(monthsToDeltas[delta.Date], delta)
}
months := []string{}
for k := range monthsToDeltas {
months = append(months, k)
}
slices.Sort(months)
catToMonth := map[string][]int{}
for _, month := range months {
balances := monthsToDeltas[month].Balances().WithBPIs(bpis)
for category, balance := range balances {
catToMonth[category] = append(catToMonth[category], int(balance[ledger.USD]))
}
}
chart := view.NewChart("pie")
ttl := 0
for cat, month := range catToMonth {
for i := 0; i < len(months)-len(month); i++ {
month = append(month, 0)
}
slices.Sort(month)
median := month[len(month)/2]
ttl += median
if median > min {
chart.AddY(cat, []int{median})
}
}
fmt.Fprintln(w, "<!DOCTYPE html><h2>", title, "($", ttl, ")</h2>")
if err := chart.Render(w); err != nil {
panic(err)
}
}
pie(fmt.Sprintf("Median Monthly Spending Since %dmo ago", int(recent/time.Hour/24/30)), `Withdrawal:[0-9]*`, 50)
pie("Median Monthly Spending (detailed)", `Withdrawal:[0-9]*:[^:]*`, 25)
pie("Median Monthly Spending (MORE detailed)", `Withdrawal:[0-9]*:[^:]*:[^:]*`, 10)
}
func (router Router) APICreate(w http.ResponseWriter, r *http.Request) {
new := ledger.Delta{
Name: "TODO",
Date: time.Now().Format(`2006-01-02`),
Description: "TODO",
Currency: "$",
Value: 0.01,
}
if err := router.files.Add("HouseyMcHouseface:Withdrawal:0:TODO", new); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
func (router Router) APIAmend(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
var req struct {
Old ledger.Delta
Now ledger.Delta
}
if err := json.Unmarshal(b, &req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
req.Now.Name = strings.ReplaceAll(req.Now.Name, " ", "_")
if err := router.files.Amend(req.Old, req.Now); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
func (router Router) APIReg(w http.ResponseWriter, r *http.Request) {
deltas, err := router.files.Deltas()
if err != nil {
panic(err)
}
deltas = deltas.Group(ledger.GroupName(router.group.name), ledger.GroupDate(router.group.date))
like := ledger.Likes{
ledger.LikeName(router.like.name),
ledger.LikeBefore(router.like.before),
ledger.LikeAfter(router.like.after),
}
foolike := make(ledger.Likes, 0)
for _, v := range r.URL.Query()["likeName"] {
foolike = append(foolike, ledger.LikeName(v))
}
for _, v := range r.URL.Query()["likeAfter"] {
foolike = append(foolike, ledger.LikeAfter(v))
}
for _, v := range r.URL.Query()["likeBefore"] {
foolike = append(foolike, ledger.LikeBefore(v))
}
if len(foolike) == 0 {
foolike = like
}
deltas = deltas.Like(foolike...)
// MODIFIERS
for i, whatIf := range r.URL.Query()["whatIf"] {
fields := strings.Fields(whatIf)
date := "2001-01"
name := fields[0]
currency := ledger.Currency(fields[1])
value, err := strconv.ParseFloat(fields[2], 64)
if err != nil {
panic(err)
}
deltas = append(deltas, ledger.Delta{
Date: date,
Name: name,
Value: value,
Currency: currency,
Description: fmt.Sprintf("?whatIf[%d]", i),
})
}
register := deltas.Register()
predicted := make(ledger.Register)
bpis, err := router.bpis()
if err != nil {
panic(err)
}
if predictionMonths, err := strconv.ParseInt(r.URL.Query().Get("predictionMonths"), 10, 16); err == nil && predictionMonths > 0 {
window := time.Hour * 24.0 * 365.0 / 12.0 * time.Duration(predictionMonths)
// TODO whatif
prediction := make(ana.Prediction, 0)
for _, spec := range r.URL.Query()["prediction"] {
idx := strings.Index(spec, "=")
k := spec[:idx]
fields := strings.Fields(spec[idx+1:])
switch k {
case "interest":
apy, err := strconv.ParseFloat(fields[2], 64)
if err != nil {
panic(err)
}
prediction = append(prediction, ana.NewInterestPredictor(fields[0], fields[1], apy))
case "autoContributions":
prediction = append(prediction, ana.NewAutoContributionPredictor(register))
case "contributions":
name := fields[0]
currency := ledger.Currency(fields[1])
value, err := strconv.ParseFloat(fields[2], 64)
if err != nil {
panic(err)
}
prediction = append(prediction, ana.NewContributionPredictor(ledger.Balances{name: ledger.Balance{currency: value}}))
default:
panic(k)
}
}
predicted = prediction.Predict(register, window)
for _, currencyRate := range r.URL.Query()["predictFixedGrowth"] {
currency := strings.Split(currencyRate, "=")[0]
rate, err := strconv.ParseFloat(strings.Split(currencyRate, "=")[1], 64)
if err != nil {
panic(err)
}
bpis, err = ana.BPIsWithFixedGrowthPrediction(bpis, window, currency, rate)
if err != nil {
panic(err)
}
}
}
if r.URL.Query().Get("bpi") == "true" {
register = register.WithBPIs(bpis)
predicted = predicted.WithBPIs(bpis)
}
if zoomStart, err := time.ParseInLocation("2006-01", r.URL.Query().Get("zoomStart"), time.Local); err == nil {
register = register.Between(zoomStart, time.Now().Add(time.Hour*24*365*100))
predicted = predicted.Between(zoomStart, time.Now().Add(time.Hour*24*365*100))
}
// /MODIFIERS
dates := register.Dates()
names := register.Names()
for _, date := range predicted.Dates() {
found := false
for i := range dates {
found = found || dates[i] == date
}
if !found {
dates = append(dates, date)
}
}
for _, name := range predicted.Names() {
found := false
for i := range names {
found = found || names[i] == name
}
if !found {
names = append(names, name)
}
}
instant := map[string]string{}
toChart := func(cumulative bool, display string, reg ledger.Register) view.Chart {
nameCurrencyDateValue := map[string]map[ledger.Currency]map[string]float64{}
for date, balances := range reg {
for name, balance := range balances {
for currency, value := range balance {
if _, ok := nameCurrencyDateValue[name]; !ok {
nameCurrencyDateValue[name] = make(map[ledger.Currency]map[string]float64)
}
if _, ok := nameCurrencyDateValue[name][currency]; !ok {
nameCurrencyDateValue[name][currency] = make(map[string]float64)
}
nameCurrencyDateValue[name][currency][date] += value
}
}
}
chart := view.NewChart("line")
if v := display; v != "" {
chart = view.NewChart(v)
}
chart.AddX(dates)
if cumulative {
for _, name := range names {
currencyDateValue := nameCurrencyDateValue[name]
for currency, dateValue := range currencyDateValue {
series := make([]int, len(dates))
for i := range dates {
var lastValue float64
for j := range dates[:i+1] {
if newLastValue, ok := dateValue[dates[j]]; ok {
lastValue = newLastValue
}
}
series[i] = int(lastValue)
}
key := fmt.Sprintf("%s (%s)", name, currency)
for i := range dates {
if !(reg.Dates()[0] <= dates[i] && dates[i] <= reg.Dates()[len(reg.Dates())-1]) {
series[i] = 0
} else {
instant[key] = fmt.Sprintf("@%s %v", dates[i], series[i])
}
}
if slices.Min(series) != 0 || slices.Max(series) != 0 {
chart.AddY(key, series)
}
}
}
} else {
for _, name := range names {
currencyDateValue := nameCurrencyDateValue[name]
for currency, dateValue := range currencyDateValue {
series := make([]int, len(dates))
for i := range dates {
var prevValue float64
var lastValue float64
for j := range dates[:i+1] {
if newLastValue, ok := dateValue[dates[j]]; ok {
prevValue = lastValue
lastValue = newLastValue
}
}
series[i] = int(lastValue - prevValue)
}
for i := range series { // TODO no prior so no delta
if series[i] != 0 {
series[i] = 0
break
}
}
key := fmt.Sprintf("%s (%s)", name, currency)
for i := range dates {
if !(reg.Dates()[0] <= dates[i] && dates[i] <= reg.Dates()[len(reg.Dates())-1]) {
series[i] = 0
} else {
instant[key] = fmt.Sprintf("@%s %v", dates[i], series[i])
}
}
if slices.Min(series) != 0 || slices.Max(series) != 0 {
chart.AddY(key, series)
}
}
}
}
return chart
}
primary := toChart(r.URL.Path == "/api/bal", r.URL.Query().Get("chart"), register)
if len(predicted) > 0 {
primary.Overlap(toChart(r.URL.Path == "/api/bal", "line", predicted))
}
if err := primary.Render(w); err != nil {
panic(err)
}
for k, v := range instant {
fmt.Fprintf(w, "<br>\n%s = %s", k, v)
}
}
func (r Router) bpis() (ledger.BPIs, error) {
if r.bpiPath == "" {
return make(ledger.BPIs), nil
}
return ledger.NewBPIs(r.bpiPath)
}

3
cmd/install.sh Normal file
View File

@@ -0,0 +1,3 @@
#! /bin/bash
cd "$(dirname "$(realpath "$BASH_SOURCE")")"
go build -o $GOPATH/bin/ana-ledger

1
cmd/install_scratch.sh Normal file
View File

@@ -0,0 +1 @@
CGO_ENABLED=1 CC=x86_64-linux-musl-gcc go build -ldflags="-linkmode external -extldflags -static" -o $HOME/Go/bin/ana-ledger

View File

@@ -1,15 +1,57 @@
package main
import (
"context"
"log"
"os"
"os/signal"
"strings"
"syscall"
"gogs.inhome.blapointe.com/ana-ledger/cmd/cli"
"gogs.inhome.blapointe.com/ana-ledger/cmd/http"
"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
)
func main() {
switch os.Args[1] {
case "tel":
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
defer can()
if c, err := teller.New(); err != nil {
} else if _, err := c.Accounts(ctx); err != nil {
} else {
log.Println("teller already init")
}
if err := teller.Init(ctx); err != nil {
panic(err)
}
case "http":
os.Args = append([]string{os.Args[0]}, os.Args[2:]...)
http.Main()
case "cli":
os.Args = append([]string{os.Args[0]}, os.Args[2:]...)
cli.Main()
case "shared":
files := os.Args[2:]
os.Args = []string{os.Args[0], "cli"}
for _, f := range files {
if strings.HasPrefix(f, "-") {
os.Args = append(os.Args, f)
} else {
os.Args = append(os.Args, "-f", f)
}
}
os.Args = append(os.Args,
"-w=^Housey",
"--depth=1",
"--usd",
"-n",
"--no-percent",
"bal", "^Bel", "^Zach",
)
main()
}
}

16
go.mod
View File

@@ -1,5 +1,17 @@
module gogs.inhome.blapointe.com/ana-ledger
go 1.21.1
go 1.23.0
require github.com/go-echarts/go-echarts/v2 v2.3.1
toolchain go1.24.2
require (
github.com/go-echarts/go-echarts/v2 v2.3.1
github.com/guptarohit/asciigraph v0.7.3
golang.org/x/crypto v0.38.0
golang.org/x/time v0.11.0
)
require (
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
)

10
go.sum
View File

@@ -2,9 +2,19 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-echarts/go-echarts/v2 v2.3.1 h1:Yw0HVjVTxpYm48l974dMjRzx8ni2ql0kKi/kawSgxFE=
github.com/go-echarts/go-echarts/v2 v2.3.1/go.mod h1:56YlvzhW/a+du15f3S2qUGNDfKnFOeJSThBIrVFHDtI=
github.com/guptarohit/asciigraph v0.7.3 h1:p05XDDn7cBTWiBqWb30mrwxd6oU0claAjqeytllnsPY=
github.com/guptarohit/asciigraph v0.7.3/go.mod h1:dYl5wwK4gNsnFf9Zp+l06rFiDZ5YtXM6x7SRWZ3KGag=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

89
src/ana/normalize.go Normal file
View File

@@ -0,0 +1,89 @@
package ana
import (
"regexp"
"slices"
"strings"
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
)
type Normalizer struct {
m map[string][]normalize
}
type normalize struct {
startDate string
factor float64
}
func NewDefaultNormalizer() Normalizer {
return NewNormalizer().
With("^Zach", "2025-09-01", 151). // turtle up
With("^Zach", "2023-10-05", 139). // to turtle
With("^Zach", "2021-12-30", 135). // at pluralsight
With("^Zach", "2020-07-30", 120). // to pluralsight
With("^Zach", "2019-07-16", 77). // at fedex
With("^Zach", "2017-02-16", 49). // to fedex
With("^Bel", "2025-10-01", 225). // render up
With("^Bel", "2025-04-01", 214). // lc4 at render
With("^Bel", "2023-12-05", 190). // to render
With("^Bel", "2022-12-31", 154). // at q
With("^Bel", "2022-06-30", 148). // at q
With("^Bel", "2021-12-31", 122). // at q
With("^Bel", "2020-12-31", 118). // at q
With("^Bel", "2019-12-31", 111). // at q
With("^Bel", "2018-12-31", 92). // at q
With("^Bel", "2018-02-16", 86) // to q
}
func NewNormalizer() Normalizer {
return Normalizer{
m: make(map[string][]normalize),
}
}
func (n Normalizer) With(pattern, startDate string, factor float64) Normalizer {
n.m[pattern] = append(n.m[pattern], normalize{startDate: startDate, factor: factor})
slices.SortFunc(n.m[pattern], func(a, b normalize) int {
return -1 * strings.Compare(a.startDate, b.startDate)
})
return n
}
func (n Normalizer) Normalize(deltas ledger.Deltas) ledger.Deltas {
deltas = slices.Clone(deltas)
patterns := []string{}
for pattern := range n.m {
patterns = append(patterns, pattern)
}
like := ledger.LikeName(strings.Join(patterns, "|"))
for i, delta := range deltas {
if !like(delta) {
continue
}
deltas[i] = n.NormalizeDelta(delta)
}
return deltas
}
func (n Normalizer) NormalizeDelta(delta ledger.Delta) ledger.Delta {
delta.Value /= n.NormalizeFactor(delta.Name, delta.Date)
return delta
}
func (n Normalizer) NormalizeFactor(name, date string) float64 {
for pattern := range n.m {
if regexp.MustCompile(pattern).MatchString(name) {
for _, normalize := range n.m[pattern] {
if normalize.startDate < date {
return normalize.factor
}
}
}
}
return 1.0
}

30
src/ana/normalize_test.go Normal file
View File

@@ -0,0 +1,30 @@
package ana
import (
"testing"
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
)
func TestNormalize(t *testing.T) {
deltas := ledger.Deltas{
ledger.Delta{Date: "2024-12-04", Value: 100, Name: "Bel:Withdrawal"},
ledger.Delta{Date: "2024-12-06", Value: 100, Name: "Bel:Withdrawal"},
}
normalizer := NewNormalizer().
With("^Bel:", "2024-12-05", 190_000).
With("^Bel:", "2018-02-16", 86_000)
normalized := normalizer.Normalize(deltas)
for i, delta := range normalized {
t.Logf("[%d] %+v", i, delta)
}
if normalized[0].Value != 100/float64(86_000) {
t.Errorf("earliest transaction didnt use earliest salary")
}
if normalized[1].Value != 100/float64(190_000) {
t.Errorf("latest transaction didnt use latest salary")
}
}

107
src/bank/cache/cache.go vendored Normal file
View File

@@ -0,0 +1,107 @@
package cache
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"path"
"time"
"gogs.inhome.blapointe.com/ana-ledger/src/bank"
)
type Client struct {
Client bank.Agg
}
var _ bank.Agg = Client{}
func New(client bank.Agg) Client {
return Client{Client: client}
}
func (c Client) Accounts(ctx context.Context) ([]bank.Account, error) {
k := "accounts"
result := []bank.Account{}
if err := fromCache(k, &result); err != nil {
log.Printf("%q not in cache: %v", k, err)
} else {
return result, nil
}
result, err := c.Client.Accounts(ctx)
if err != nil {
return nil, err
}
toCache(k, result)
return result, nil
}
func (c Client) Transactions(ctx context.Context, a bank.Account) ([]bank.Transaction, error) {
k := path.Join("accounts.d", a.Account)
result := []bank.Transaction{}
if err := fromCache(k, &result); err != nil {
log.Printf("%q not in cache: %v", k, err)
} else {
return result, nil
}
result, err := c.Client.Transactions(ctx, a)
if err != nil {
return nil, err
}
toCache(k, result)
return result, nil
}
var (
d = path.Join("/tmp/ana_ledger_bank_cache.d")
)
func toCache(k string, v interface{}) {
if err := _toCache(k, v); err != nil {
log.Printf("failed to cache %s: %v", k, err)
}
}
func _toCache(k string, v interface{}) error {
b, err := json.Marshal(v)
if err != nil {
return err
}
p := path.Join(d, k)
os.MkdirAll(path.Dir(p), os.ModePerm)
if err := os.WriteFile(p, b, os.ModePerm); err != nil {
os.Remove(p)
return err
}
return nil
}
func fromCache(k string, ptr interface{}) error {
p := path.Join(d, k)
if stat, err := os.Stat(p); err != nil {
return err
} else if time.Since(stat.ModTime()) > 24*time.Hour {
return fmt.Errorf("stale")
}
b, err := os.ReadFile(p)
if err != nil {
return err
}
if err := json.Unmarshal(b, ptr); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,50 @@
//go:build integration
package cache_test
import (
"context"
"strconv"
"testing"
"gogs.inhome.blapointe.com/ana-ledger/src/bank/cache"
"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
)
func Test(t *testing.T) {
tellerC, err := teller.New()
if err != nil {
t.Fatal(err)
}
client := cache.New(tellerC)
ctx := context.Background()
for i := 0; i < 2; i++ {
i := i
client := client
t.Run(strconv.Itoa(i), func(t *testing.T) {
accounts, err := client.Accounts(ctx)
if err != nil {
t.Fatal(err)
}
for _, account := range accounts {
account := account
t.Run(account.Account, func(t *testing.T) {
transactions, err := client.Transactions(ctx, account)
if err != nil {
t.Fatal(err)
}
for i, tr := range transactions {
t.Logf("[%d] %+v", i, tr)
}
})
break
}
})
client.Client = nil
}
}

View File

@@ -0,0 +1 @@
app_pdvv33dtmta4fema66000

View File

@@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIExjCCAq6gAwIBAgIIGEJSCPAVjIYwDQYJKoZIhvcNAQELBQAwYTELMAkGA1UE
BhMCR0IxEDAOBgNVBAgMB0VuZ2xhbmQxDzANBgNVBAcMBkxvbmRvbjEPMA0GA1UE
CgwGVGVsbGVyMR4wHAYDVQQLDBVUZWxsZXIgQXBwbGljYXRpb24gQ0EwHhcNMjUw
NTI0MDEyMzIzWhcNMjgwNTIzMDEyMzIzWjAkMSIwIAYDVQQDDBlhcHBfcGR2djMz
ZHRtdGE0ZmVtYTY2MDAwMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
tWbYAc2wQBBvKJhTlo5YPLqkM5GgaYaWUqJJ5bFht+U2PL0rJHbS7oRG9fXtgb12
jEzD3CUUPpCxj7oRgYdji2SH5NKmo3M85/Cry1y5pmotmqGlqrt01zStj2A+FuzR
BqdmVb1CIE2iJmttGn3C3f9OCbV5kMKreu4DhVdPW7eafpo+yIMaoJxx2CAMqw5Y
GK2NNpehMHzpL4Z0032yEQJaBblYeUT4zgMCxoupXQ7hCsyjn/ws7ocpMHXY9r08
6PjaY1j6wYwX9OUJDXz0zgoeShr2vcfkjBc1QavRLVQQUWgYJevS/8rCfo32B7ub
Ym1CmWz2cY3+4UV3uihJgQIDAQABo4G+MIG7MA4GA1UdDwEB/wQEAwIF4DATBgNV
HSUEDDAKBggrBgEFBQcDAjCBkwYDVR0jBIGLMIGIgBSEq++simSLxXkuNSUKjel6
pmhxmqFlpGMwYTELMAkGA1UEBhMCR0IxEDAOBgNVBAgMB0VuZ2xhbmQxDzANBgNV
BAcMBkxvbmRvbjEPMA0GA1UECgwGVGVsbGVyMR4wHAYDVQQLDBVUZWxsZXIgQXBw
bGljYXRpb24gQ0GCCQDiNWG/vm85CTANBgkqhkiG9w0BAQsFAAOCAgEARv/Kjwcu
ppXbTf9pvsesEgo6O+OM1qW73SkmQeB5ZfF6KEOn57SujTjVQRlBGhVs//+Ezlav
GKKm4Xw0eCKUfISIgz+nY7lSlVvW2REZAdpuiY6owsqtiL/Fe/RBvkUmNSWnu8vc
OIfnpqP6flKL7KjXwQXoI/Xt9Zw6D56dHQi5hYgYtP0HqtEZn6vdtroHM3mbIL6D
Dnfdhb3ywwVZTiQE5ceQk+StDYuzTz7PNQL6IWxcH4j73dQlvFzMLSDm9yA2NguK
SEiRi4gYmluQxSiTN3gYqfOMeVv/buklHknkRHCInOTOiDWX8ku0FWwApiwsmvLS
3z/9WK4QEMLQfxBbp7UJePWx0Tq6KE61gcTgMbjqz+Xi9n2KM6RenDdMg2rQXWX4
xqC0sOOuSS19WFCGYDBRjI2JOQqfeEymq8pQsrGm+XyzCPMi+eFyLZqkRiT85MCr
IZuUWpMTcqILGP3Ar5Z0ppDI1ppVDhWMoq5EYx0iuJjNQZEtHlbu1j+cw6uVAoqJ
T/E5/qKcLRkkDKV68B+CP2z+iOXH5M1dcYFu6yEkKxVbEuTJNyN5gHhBATyIoMQ0
RdohL8F9hdOHvTLo89xrFEHiLExIT0NISjlT0M/mTqq4rbr5kp5W/ee9h5uSVUJ8
3nCv0NH3CX0Ygjzd7Czd9hWRahQz+vv55u4=
-----END CERTIFICATE-----

64
src/bank/teller/init.go Normal file
View File

@@ -0,0 +1,64 @@
package teller
import (
"context"
_ "embed"
"fmt"
"io"
"net/http"
"os"
"slices"
"text/template"
)
var (
//go:embed application_id.txt
applicationId string
//go:embed init.html
initHTML string
)
func Init(ctx context.Context) error {
environment := "development"
if sandbox := !slices.Contains(os.Args, "forreal"); sandbox {
environment = "sandbox"
}
fmt.Printf("environment=%q\n", environment)
newTokens := make(chan string)
defer close(newTokens)
s := &http.Server{
Addr: ":20000",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
b, _ := io.ReadAll(r.Body)
newTokens <- string(b)
return
}
t, err := template.New("initHTML").Parse(initHTML)
if err != nil {
panic(err)
}
if err := t.Execute(w, map[string]string{
"applicationId": applicationId,
"environment": environment,
}); err != nil {
panic(err)
}
}),
}
defer s.Close()
go s.ListenAndServe()
fmt.Println("Open http://localhost:20000")
select {
case <-ctx.Done():
case newToken := <-newTokens:
return fmt.Errorf("not impl: %q >> token.txt", newToken)
}
return ctx.Err()
}

58
src/bank/teller/init.html Normal file
View File

@@ -0,0 +1,58 @@
<html>
<head></head>
<body>
<button id="teller-connect">Connect to your bank</button>
<h3 id="log">
</h3>
<script src="https://cdn.teller.io/connect/connect.js"></script>
<script>
function logme(msg) {
document.getElementById("log").innerHTML += `<br>* ${msg}`
}
function http(method, remote, callback, body) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == XMLHttpRequest.DONE) {
callback(xmlhttp.responseText, xmlhttp.status)
}
};
xmlhttp.open(method, remote, true);
if (typeof body == "undefined") {
body = null
}
xmlhttp.send(body);
}
function callback(responseBody, responseStatus) {
}
document.addEventListener("DOMContentLoaded", function() {
var tellerConnect = TellerConnect.setup({
applicationId: "{{.applicationId}}",
environment: "{{.environment}}",
products: ["verify", "balance", "transactions"],
onInit: function() {
logme("Teller Connect has initialized")
},
onSuccess: function(enrollment) {
logme(`User enrolled successfully: ${enrollment.accessToken}`)
http("post", "/", callback, enrollment.accessToken)
},
onExit: function() {
logme("User closed Teller Connect")
},
onFailure: function(failure) {
logme(`Failed: type=${failure.type} code=${failure.code} message=${failure.message}`)
},
});
var el = document.getElementById("teller-connect");
el.addEventListener("click", function() {
tellerConnect.open();
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1ZtgBzbBAEG8o
mFOWjlg8uqQzkaBphpZSoknlsWG35TY8vSskdtLuhEb19e2BvXaMTMPcJRQ+kLGP
uhGBh2OLZIfk0qajczzn8KvLXLmmai2aoaWqu3TXNK2PYD4W7NEGp2ZVvUIgTaIm
a20afcLd/04JtXmQwqt67gOFV09bt5p+mj7IgxqgnHHYIAyrDlgYrY02l6EwfOkv
hnTTfbIRAloFuVh5RPjOAwLGi6ldDuEKzKOf/Czuhykwddj2vTzo+NpjWPrBjBf0
5QkNfPTOCh5KGva9x+SMFzVBq9EtVBBRaBgl69L/ysJ+jfYHu5tibUKZbPZxjf7h
RXe6KEmBAgMBAAECggEAExDEEyxzIciYZkPcRS6gx4E2UNU1buHeWsED00hZZOKK
WMfpCOQUN01fx+oZFFG9a/GFhFXBUvISN3Du9hYsuDHQtpQNP5CVDiuVYsJUINF4
CZCDwPYCybuXokITRIWPUou1jb1efdaq/C6+QNKG8J4srYiNRlGvhDQP2qvag2ET
YtY+6OuQPF9QSeoKLBCJJi8bP/wy3OfOQkTqvlpyEGVOUc5utGuziITShL2mLtQw
1O22r4BIFtkvnhM4kGimbSDMYwtAgMrBrktR4Xp7JICV6eZn6fJgjpYOix/4ILxS
Ri69qPLso4qF+ML/vsTHlxQj7FwZDZozT4hm3DK3gQKBgQDdnEAdQYuKIvwrHflk
Pxb9q7MjrMJMGiA9GvSVTimeWu68kH2y/dUUQyBtr8SRCScNUmhRCfaZCBakSYR0
0v+6FLXleI28dD93Qzn70G0N6kVdKLdM3Rt0r+fojPhP+8pHn74UzvcoGUDOzoax
VPvHLe19wqrbRFIU4IAE6qjmNQKBgQDRjUFjRJIh4wgrB6rROeoTtYRRyS9RLliw
dpH78Vz7OYTx7qnvtVOl8led82Ott7hEXcK9Ihobz8uPCewW/x+sPYty13shT46K
8zEj9FoP9XhAS3MOqLDNx7h5jv7nvua8aRQEPkF30SmPqb+X4nN5+JjuJiTqWgJ+
5nfnyX4PnQKBgQCE87brVmV27GJJI+R5JfiPG7GPl5fBvHLW9hMCeDAz1u4fprgi
6HIrg9IyvB67vLf3IBeBdu7BBL9AtPKIfAX8B2zRTLAL/doNnQFud67ViFUw/Lpr
nMNaECabt+dJZRAIRGfvZ/OT1QKyj+jy/r9G0eEHcAC9J5HvAHkNehL2eQKBgAU+
a6x4Qs/mRoYNIxEpSdpEaJNDXZPCfSWtUenkGFeREOqc9lOxTe6RKfAh7xShzFKp
pf3lpJGdmZJyxR2uNLSytZKiIcqrmv2PKGOl8bsEgYXaXX64afQ8Uzl3gpl6BXwh
hQa2KB0/drLJpKnAWPNsbSdIfRQAPJ/AVK/QMv9hAoGAMtmCDJK1KVZcpFJbFSMk
pPJjcp1XRWKsiBOPfUKlwbBSFDBUnhyPcsfL9ooAwzBnBcpNf6S5I+I22AdDuk8E
S3uOBBlNhoecWN1tqVlcbTR1p5kXV0WVAcQ09hGPqja5ghrTTpWbC7SviSVNa0fx
LXKvqDx5qTleSmrPwPs5TW4=
-----END PRIVATE KEY-----

95
src/bank/teller/teller.go Normal file
View File

@@ -0,0 +1,95 @@
package teller
import (
"context"
"crypto/tls"
_ "embed"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"gogs.inhome.blapointe.com/ana-ledger/src/bank"
"golang.org/x/time/rate"
)
type Client struct {
cert tls.Certificate
}
var _ bank.Agg = Client{}
var (
//go:embed certificate.pem
certificate []byte
//go:embed private_key.pem
privateKey []byte
//go:embed token.txt
Tokens string
)
func New() (Client, error) {
cert, err := tls.X509KeyPair(certificate, privateKey)
return Client{cert: cert}, err
}
func (c Client) Accounts(ctx context.Context) ([]bank.Account, error) {
var result []bank.Account
for _, token := range strings.Fields(Tokens) {
var more []bank.Account
if err := c.get(ctx, "https://api.teller.io/accounts", token, &more); err != nil {
return nil, err
}
for i := range more {
more[i].Token = token
}
result = append(result, more...)
}
return result, nil
}
func (c Client) Transactions(ctx context.Context, a bank.Account) ([]bank.Transaction, error) {
var result []bank.Transaction
err := c.get(ctx, "https://api.teller.io/accounts/"+a.Account+"/transactions", a.Token, &result)
return result, err
}
var limiter = rate.NewLimiter(0.1, 1)
func (c Client) get(ctx context.Context, url, token string, ptr interface{}) error {
if err := limiter.Wait(ctx); err != nil {
return err
}
log.Printf("Teller.Get(%s, %s)", url, token)
httpc := &http.Client{
Timeout: time.Second,
Transport: &http.Transport{
DisableKeepAlives: true,
TLSClientConfig: &tls.Config{Certificates: []tls.Certificate{c.cert}},
},
}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return err
}
req.SetBasicAuth(token, "")
req = req.WithContext(ctx)
resp, err := httpc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if err := json.Unmarshal(b, &ptr); err != nil {
return fmt.Errorf("cannot unmarshal: %w: %s", err, b)
}
return nil
}

View File

@@ -0,0 +1,41 @@
//go:build integration
package teller_test
import (
"context"
"testing"
"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
)
func Test(t *testing.T) {
teller.Tokens = "test_token_bfu2cyvq3il6o"
c, err := teller.New()
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
accounts, err := c.Accounts(ctx)
if err != nil {
t.Fatal(err)
}
for _, account := range accounts {
account := account
t.Run(account.Account, func(t *testing.T) {
transactions, err := c.Transactions(ctx, account)
if err != nil {
t.Fatal(err)
}
for i, tr := range transactions {
t.Logf("[%d] %+v", i, tr)
}
})
break
}
}

View File

@@ -0,0 +1,59 @@
//go:build manual
package teller_test
import (
"crypto/tls"
"io"
"net/http"
"testing"
"time"
"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
)
func TestIntegration(t *testing.T) {
teller.Tokens = "test_token_bfu2cyvq3il6o"
//curl --cert certificate.pem --cert-key private_key.pem --auth test_token_bfu2cyvq3il6o: https://api.teller.io/accounts
cert, err := tls.LoadX509KeyPair("./certificate.pem", "./private_key.pem")
if err != nil {
t.Fatal(err)
}
c := &http.Client{
Timeout: time.Second,
Transport: &http.Transport{
DisableKeepAlives: true,
TLSClientConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
},
}
//curl --cert certificate.pem --cert-key private_key.pem --auth test_token_bfu2cyvq3il6o: https://api.teller.io/accounts
for _, url := range []string{
"https://api.teller.io/accounts",
"https://api.teller.io/accounts/acc_pdvv4810fi9hmrcn6g000/transactions",
} {
url := url
t.Run(url, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
t.Fatal(err)
}
req.SetBasicAuth("test_token_bfu2cyvq3il6o", "")
resp, err := c.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if code := resp.StatusCode; code >= 300 {
t.Fatalf("(%d) %s", code, body)
}
t.Logf("(%d) %s", resp.StatusCode, body)
})
}
}

View File

@@ -0,0 +1,2 @@
token_2utqstwpn3pxwgvyno56hqdehq
token_vr6dnzvfv7c24wuxtmnnzyiqbm

31
src/bank/types.go Normal file
View File

@@ -0,0 +1,31 @@
package bank
import "context"
type Agg interface {
Accounts(context.Context) ([]Account, error)
Transactions(context.Context, Account) ([]Transaction, error)
}
type Account struct {
Institution struct {
Name string `json:"name"`
} `json:"institution"`
Name string `json:"last_four"`
Account string `json:"id"`
Token string `json:"__token"`
}
type Transaction struct {
Amount float64 `json:"amount,string"`
Details struct {
ProcessingStatus string `json:"processing_status"`
CounterParty struct {
Name string `json:"name"`
} `json:"counterparty"`
} `json:"details"`
Description string `json:"description"`
Date string `json:"date"`
Type string `json:"type"`
Status string `json:"status"`
}

View File

@@ -4,13 +4,82 @@ import (
"fmt"
"maps"
"regexp"
"sort"
"strings"
"time"
)
type Normalizer interface {
NormalizeFactor(string, string) float64
}
type Balances map[string]Balance
type Balance map[Currency]float64
func (balances Balances) Sub(other Balances) Balances {
result := make(Balances)
for k, v := range balances {
result[k] = v.Sub(other[k])
}
for k, v := range other {
if _, ok := balances[k]; ok {
continue
}
result[k] = v.Invert()
}
return result
}
func (balances Balances) Nonzero() Balances {
result := make(Balances)
for k, v := range balances {
if nonzero := v.Nonzero(); len(nonzero) > 0 {
result[k] = nonzero
}
}
return result
}
func (balances Balances) NotLike(pattern string) Balances {
result := make(Balances)
p := regexp.MustCompile(pattern)
for k, v := range balances {
if !p.MatchString(k) {
result[k] = maps.Clone(v)
}
}
return result
}
func (balances Balances) KindaLike(like Like) Balances {
ks := []string{}
for k := range balances {
ks = append(ks, k)
}
sort.Strings(ks)
result := make(Balances)
for _, k := range ks {
v := balances[k]
if like(v.kinda(k)) {
result[k] = maps.Clone(v)
}
}
return result
}
func (b Balance) kinda(k string) Delta {
return Delta{
Date: time.Now().Format("2006-01-02"),
Name: k,
Value: b[USD],
Description: k,
Transaction: k,
Payee: false,
}
}
func (balances Balances) Like(pattern string) Balances {
result := make(Balances)
p := regexp.MustCompile(pattern)
@@ -22,22 +91,80 @@ func (balances Balances) Like(pattern string) Balances {
return result
}
func (balances Balances) Group(pattern string) Balances {
result := make(Balances)
p := regexp.MustCompile(pattern)
for k, v := range balances {
k2 := k
if p.MatchString(k) {
k2 = p.FindString(k)
}
was := result[k2]
if was == nil {
was = make(Balance)
}
for k3, v3 := range v {
was[k3] += v3
}
result[k2] = was
}
return result
}
func (balances Balances) KindaGroup(group Group) Balances {
ks := []string{}
for k := range balances {
ks = append(ks, k)
}
sort.Strings(ks)
result := make(Balances)
for _, k := range ks {
v := balances[k]
k2 := k
if k3 := group(v.kinda(k)).Name; k2 != k3 {
k2 = k3
}
if _, ok := result[k2]; !ok {
result[k2] = make(Balance)
}
for k3, v3 := range v {
result[k2][k3] += v3
}
}
return result
}
func (balances Balances) WithBPIs(bpis BPIs) Balances {
return balances.WithBPIsAt(bpis, "9")
}
func (balances Balances) WithBPIsAt(bpis BPIs, date string) Balances {
ks := []string{}
for k := range balances {
ks = append(ks, k)
}
sort.Strings(ks)
result := make(Balances)
for k, v := range balances {
for _, k := range ks {
v := balances[k]
if _, ok := result[k]; !ok {
result[k] = make(Balance)
}
for k2, v2 := range v {
scalar := 1.0
if k2 != USD {
scalar = bpis[k2].Lookup(date)
if k2 == USD {
result[k][USD] = result[k][USD] + v2
} else if scalar := bpis[k2].Lookup(date); scalar != nil {
result[k][USD] = result[k][USD] + v2*(*scalar)
} else {
result[k][k2] = result[k][k2] + v2
}
result[k][USD] += v2 * scalar
}
}
return result
@@ -64,6 +191,39 @@ func (balances Balances) Push(d Delta) {
balances[d.Name].Push(d)
}
func (balance Balance) Sub(other Balance) Balance {
return balance.Sum(other.Invert())
}
func (balance Balance) Sum(other Balance) Balance {
result := make(Balance)
for k, v := range balance {
result[k] += v
}
for k, v := range other {
result[k] += v
}
return result
}
func (balance Balance) Nonzero() Balance {
result := make(Balance)
for k, v := range balance {
if v != 0 {
result[k] = v
}
}
return result
}
func (balance Balance) Invert() Balance {
result := make(Balance)
for k, v := range balance {
result[k] = v * -1.0
}
return result
}
func (balance Balance) Push(d Delta) {
if _, ok := balance[d.Currency]; !ok {
balance[d.Currency] = 0
@@ -90,3 +250,14 @@ func (balance Balance) Debug() string {
}
return strings.Join(result, " + ")
}
func (balances Balances) Normalize(n Normalizer, date string) Balances {
result := make(Balances)
for name, balance := range balances {
result[name] = make(Balance)
for currency, value := range balance {
result[name][currency] = value / n.NormalizeFactor(name, date)
}
}
return result
}

View File

@@ -51,4 +51,38 @@ func TestBalances(t *testing.T) {
t.Error("didnt sum other", b["ab"])
}
})
t.Run("like", func(t *testing.T) {
was := make(Balances)
was.Push(Delta{Name: "a", Currency: USD, Value: 0.1})
was.Push(Delta{Name: "ab", Currency: USD, Value: 1.2})
got := was.Like(`^ab$`)
if len(got) != 1 || got["ab"][USD] != 1.2 {
t.Error(got)
}
got = was.NotLike(`^ab$`)
if len(got) != 1 || got["a"][USD] != 0.1 {
t.Error(got)
}
})
t.Run("group", func(t *testing.T) {
was := make(Balances)
was.Push(Delta{Name: "a:1", Currency: USD, Value: 0.1})
was.Push(Delta{Name: "a:2", Currency: USD, Value: 1.2})
was.Push(Delta{Name: "b:1", Currency: USD, Value: 2.2})
got := was.Group(`^[^:]*`)
if len(got) != 2 {
t.Error(got)
}
if got["a"][USD] != 1.3 {
t.Error(got)
}
if got["b"][USD] != 2.2 {
t.Error(got)
}
})
}

View File

@@ -50,7 +50,7 @@ func NewBPIs(p string) (BPIs, error) {
}
}
func (bpi BPI) Lookup(date string) float64 {
func (bpi BPI) Lookup(date string) *float64 {
var closestWithoutGoingOver string
for k := range bpi {
if k <= date && k > closestWithoutGoingOver {
@@ -58,7 +58,8 @@ func (bpi BPI) Lookup(date string) float64 {
}
}
if closestWithoutGoingOver == "" {
return 0
return nil
}
return bpi[closestWithoutGoingOver]
f := bpi[closestWithoutGoingOver]
return &f
}

View File

@@ -157,16 +157,16 @@ P 2023-10-22 07:33:56 GME $17.18
t.Fatal(err)
}
if got := got["USDC"].Lookup("2099-01-01"); got != 0 {
if got := got["USDC"].Lookup("2099-01-01"); got != nil {
t.Error("default got != 0:", got)
}
if got := got["GME"].Lookup("2099-01-01"); got != 17.18 {
if got := got["GME"].Lookup("2099-01-01"); got == nil || *got != 17.18 {
t.Errorf("shouldve returned latest but got %v", got)
}
if got := got["GME"].Lookup("2023-09-19"); got != 18.22 {
if got := got["GME"].Lookup("2023-09-19"); got == nil || *got != 18.22 {
t.Errorf("shouldve returned one day before but got %v", got)
}
if got := got["GME"].Lookup("2023-09-18"); got != 18.22 {
if got := got["GME"].Lookup("2023-09-18"); got == nil || *got != 18.22 {
t.Errorf("shouldve returned day of but got %v", got)
}
t.Log(got)

View File

@@ -14,25 +14,52 @@ type Delta struct {
Value float64
Currency Currency
Description string
isSet bool
Transaction string
Payee bool
isSet bool
fileName string
lineNo int
with []Delta
}
func newDelta(d, desc, name string, v float64, c string, isSet bool) Delta {
func newDelta(transaction string, payee bool, d, desc, name string, v float64, c string, isSet bool, fileName string, lineNo int) Delta {
return Delta{
Date: d,
Name: name,
Value: v,
Currency: Currency(c),
Description: desc,
isSet: isSet,
Transaction: transaction,
Payee: payee,
isSet: isSet,
fileName: fileName,
lineNo: lineNo,
}
}
func (delta Delta) withWith(other Delta) Delta {
other.with = nil
delta.with = append(delta.with, other)
return delta
}
func (delta Delta) equivalent(other Delta) bool {
delta.fileName = ""
delta.lineNo = 0
delta.with = nil
other.fileName = ""
other.lineNo = 0
other.with = nil
return fmt.Sprintf("%+v", delta) == fmt.Sprintf("%+v", other)
}
func (delta Delta) Debug() string {
return fmt.Sprintf("{@%s %s:\"%s\" %s%.2f %s}", delta.Date, delta.Name, delta.Description, func() string {
return fmt.Sprintf("{@%s %s(payee=%v):\"%s\" %s%.2f %s @%s#%d}", delta.Date, delta.Name, delta.Payee, delta.Description, func() string {
if !delta.isSet {
return ""
}
return "= "
}(), delta.Value, delta.Currency)
}(), delta.Value, delta.Currency, delta.fileName, delta.lineNo)
}

View File

@@ -6,8 +6,14 @@ import (
func TestDelta(t *testing.T) {
d := "2099-08-07"
delta := newDelta(d, "", "name", 34.56, "$", false)
delta := newDelta("x", true, d, "", "name", 34.56, "$", false, "", 0)
if delta.Transaction != "x" {
t.Error(delta.Transaction)
}
if !delta.Payee {
t.Error(delta.Payee)
}
if delta.Date != d {
t.Error(delta.Date)
}

View File

@@ -57,12 +57,8 @@ func (deltas Deltas) Balances() Balances {
}
result[delta.Name][delta.Currency] += delta.Value
if result[delta.Name][delta.Currency] < 0.000000001 && result[delta.Name][delta.Currency] > -0.000000001 {
delete(result[delta.Name], delta.Currency)
if len(result[delta.Name]) == 0 {
delete(result, delta.Name)
}
result[delta.Name][delta.Currency] = 0
}
}
return result
}

View File

@@ -38,9 +38,12 @@ func TestDeltas(t *testing.T) {
}
balances := deltas.Balances()
if len(balances) != 1 {
if len(balances) != 2 {
t.Error(len(balances), balances)
}
if balances["a"][""] != 0 {
t.Error(balances["a"])
}
if balances["b"][""] != 1.3 {
t.Error(balances["b"])
}

View File

@@ -1,15 +1,13 @@
package ledger
import (
"bufio"
"bytes"
"fmt"
"io"
"io/fs"
"io/ioutil"
"os"
"path"
"path/filepath"
"slices"
"sort"
"strings"
"unicode"
@@ -25,87 +23,44 @@ func NewFiles(p string, q ...string) (Files, error) {
return f, err
}
func (files Files) TempGetLastNLines(n int) ([]string, error) {
p := files.paths()[0]
f, err := os.Open(p)
if err != nil {
return nil, err
func (files Files) Amend(old, now Delta) error {
if now.isSet {
return fmt.Errorf("cannot ammend: immutable isSet is set")
}
defer f.Close()
return peekLastNLines(io.Discard, bufio.NewReader(f), n)
}
func (files Files) TempSetLastNLines(n int, lines []string) error {
p := files.paths()[0]
newFile, err := func() (string, error) {
w, err := ioutil.TempFile(os.TempDir(), path.Base(p))
if err != nil {
return "", err
}
defer w.Close()
r, err := os.Open(p)
if err != nil {
return "", err
}
defer r.Close()
if _, err := peekLastNLines(w, bufio.NewReader(r), n); err != nil {
return "", err
}
for i := range lines {
if len(strings.TrimSpace(lines[i])) == 0 {
continue
}
if _, err := fmt.Fprintln(w, lines[i]); err != nil {
return "", err
}
}
if err := w.Close(); err != nil {
return "", err
}
return w.Name(), nil
}()
xactions, err := files.transactions()
if err != nil {
return err
}
r, err := os.Open(newFile)
if err != nil {
return err
}
defer r.Close()
w, err := os.Create(p)
if err != nil {
return err
}
defer w.Close()
_, err = io.Copy(w, r)
return err
}
func peekLastNLines(w io.Writer, r *bufio.Reader, n int) ([]string, error) {
lastNLines := make([]string, 0, n)
for {
line, err := r.ReadBytes('\n')
if len(bytes.TrimSpace(line)) > 0 {
lastNLines = append(lastNLines, string(bytes.TrimRight(line, "\n")))
for i := 0; i < len(lastNLines)-n; i++ {
fmt.Fprintln(w, lastNLines[i])
}
lastNLines = lastNLines[max(0, len(lastNLines)-n):]
var transaction transaction
for _, xaction := range xactions {
if xaction.name != old.Transaction {
continue
}
if err == io.EOF {
break
transaction = xaction
break
}
if transaction.payee == old.Name {
if len(transaction.recipients) != 1 {
return fmt.Errorf("cannot amend: modifying original payee, but many recipients cant share new value")
}
if err != nil {
return nil, err
transaction.payee, transaction.recipients[0].name, transaction.recipients[0].value = transaction.recipients[0].name, transaction.payee, transaction.recipients[0].value*-1.0
}
idx := -1
for i, recipient := range transaction.recipients {
if recipient.name == old.Name && recipient.value == old.Value {
idx = i
}
}
return lastNLines, nil
if idx == -1 {
return fmt.Errorf("cannot amend: no recipient with name %q value %.2f found in %+v to set new value", old.Name, old.Value, transaction)
}
old.Value *= -1
return files.Add(transaction.payee, []Delta{old, now}...)
}
func (files Files) paths() []string {
@@ -130,7 +85,16 @@ func (files Files) paths() []string {
return result
}
func (files Files) Add(payee string, delta Delta) error {
func (files Files) Add(payee string, deltas ...Delta) error {
for _, delta := range deltas {
if err := files.add(payee, delta); err != nil {
return err
}
}
return nil
}
func (files Files) add(payee string, delta Delta) error {
currencyValue := fmt.Sprintf("%s%.2f", delta.Currency, delta.Value)
if delta.Currency != USD {
currencyValue = fmt.Sprintf("%.2f %s", delta.Value, delta.Currency)
@@ -143,11 +107,13 @@ func (files Files) Add(payee string, delta Delta) error {
}
func (files Files) append(s string) error {
if err := files.trimTrainlingWhitespace(); err != nil {
p := path.Join(path.Dir(files.paths()[0]), "inbox.txt")
if err := files.trimTrailingWhitespace(p); err != nil {
return err
}
f, err := os.OpenFile(string(files.paths()[0]), os.O_APPEND|os.O_CREATE|os.O_WRONLY, os.ModePerm)
f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, os.ModePerm)
if err != nil {
return err
}
@@ -157,8 +123,8 @@ func (files Files) append(s string) error {
return f.Close()
}
func (files Files) trimTrainlingWhitespace() error {
idx, err := files._lastNonWhitespacePos()
func (files Files) trimTrailingWhitespace(p string) error {
idx, err := files._lastNonWhitespacePos(p)
if err != nil {
return err
}
@@ -166,7 +132,7 @@ func (files Files) trimTrainlingWhitespace() error {
return nil
}
f, err := os.OpenFile(string(files.paths()[0]), os.O_CREATE|os.O_WRONLY, os.ModePerm)
f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY, os.ModePerm)
if err != nil {
return err
}
@@ -175,8 +141,8 @@ func (files Files) trimTrainlingWhitespace() error {
return f.Truncate(int64(idx + 1))
}
func (files Files) _lastNonWhitespacePos() (int, error) {
f, err := os.Open(string(files.paths()[0]))
func (files Files) _lastNonWhitespacePos(p string) (int, error) {
f, err := os.Open(p)
if os.IsNotExist(err) {
return -1, nil
}
@@ -208,38 +174,14 @@ func (files Files) Deltas(like ...Like) (Deltas, error) {
result := make(Deltas, 0, len(transactions)*2)
for _, transaction := range transactions {
sums := map[string]float64{}
for _, recipient := range transaction.recipients {
sums[recipient.currency] += recipient.value
delta := newDelta(
transaction.date,
transaction.description,
recipient.name,
recipient.value,
recipient.currency,
recipient.isSet,
)
result = append(result, delta)
}
for currency, value := range sums {
if value == 0 {
continue
}
if transaction.payee == "" {
//return nil, fmt.Errorf("didnt find net zero and no dumping ground payee set: %+v", transaction)
} else {
delta := newDelta(
transaction.date,
transaction.description,
transaction.payee,
-1.0*value,
currency,
false,
)
result = append(result, delta)
}
}
result = append(result, transaction.deltas()...)
}
slices.SortFunc(result, func(a, b Delta) int {
if str := strings.Compare(a.Date+a.fileName, b.Date+b.fileName); str != 0 {
return str
}
return a.lineNo - b.lineNo
})
balances := make(Balances)
for i := range result {

View File

@@ -1,15 +1,166 @@
package ledger
import (
"bytes"
"encoding/base64"
"fmt"
"os"
"path"
"path/filepath"
"testing"
)
func TestFileAmend(t *testing.T) {
cases := map[string]struct {
from string
old Delta
now Delta
want string
}{
"multi recipient": {
from: `
2006-01-02 description
recipientA $3.45
recipientB $6.45
payee
`,
old: Delta{
Date: "2006-01-02",
Name: "recipientB",
Value: 6.45,
Currency: "$",
Description: "description",
},
now: Delta{
Date: "2106-11-12",
Name: "recipientC",
Value: 16.45,
Currency: "T",
Description: "1description",
},
want: `
2006-01-02 description
recipientB $-6.45
payee
2106-11-12 1description
recipientC 16.45 T
payee`,
},
"recipient": {
from: `
2006-01-02 description
recipient $3.45
payee $-3.45
`,
old: Delta{
Date: "2006-01-02",
Name: "recipient",
Value: 3.45,
Currency: "$",
Description: "description",
},
now: Delta{
Date: "2106-11-12",
Name: "1recipient",
Value: 13.45,
Currency: "T",
Description: "1description",
},
want: `
2006-01-02 description
recipient $-3.45
payee
2106-11-12 1description
1recipient 13.45 T
payee`,
},
"payee": {
from: `
2006-01-02 description
recipient $3.45
payee
`,
old: Delta{
Date: "2006-01-02",
Name: "payee",
Value: -3.45,
Currency: "$",
Description: "description",
},
now: Delta{
Date: "2106-11-12",
Name: "1payee",
Value: -13.45,
Currency: "T",
Description: "1description",
},
want: `
2006-01-02 description
payee $3.45
recipient
2106-11-12 1description
1payee -13.45 T
recipient`,
},
"was set": {
from: `
2006-01-02 description
recipient $3.45
payee
`,
old: Delta{
Date: "2006-01-02",
Name: "recipient",
Value: 3.45,
Currency: "$",
Description: "description",
},
now: Delta{
Date: "2106-11-12",
Name: "1recipient",
Value: 13.45,
Currency: "T",
Description: "1description",
},
want: `
2006-01-02 description
recipient $-3.45
payee
2106-11-12 1description
1recipient 13.45 T
payee`,
},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
p := path.Join(t.TempDir(), "dat")
if err := os.WriteFile(p, []byte(c.from), os.ModePerm); err != nil {
t.Fatal(err)
}
files, err := NewFiles(p)
if err != nil {
t.Fatal(err)
} else if deltas, err := files.Deltas(); err != nil {
t.Fatal(err)
} else if filtered := deltas.Like(func(d Delta) bool {
c.old.Transaction = d.Transaction
c.old.Payee = d.Payee
return d.equivalent(c.old)
}); len(filtered) != 1 {
t.Fatalf("input \n\t%s \ndidnt include old \n\t%+v \nin \n\t%+v: \n\t%+v", c.from, c.old, deltas, filtered)
}
if err := files.Amend(c.old, c.now); err != nil {
t.Fatal(err)
} else if b, err := os.ReadFile(path.Join(path.Dir(p), "inbox.txt")); err != nil {
t.Fatal(err)
} else if string(b) != c.want {
t.Fatalf("expected \n\t%q\nbut got\n\t%q\n\t%s", c.want, b, b)
}
})
}
}
func TestFileAdd(t *testing.T) {
filesAppendDelim = " "
payee := "name:3"
@@ -70,7 +221,7 @@ func TestFileAdd(t *testing.T) {
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
p := path.Join(t.TempDir(), "input")
p := path.Join(t.TempDir(), "inbox.txt")
if c.given != nil {
if err := os.WriteFile(p, []byte(c.given), os.ModePerm); err != nil {
t.Fatal(err)
@@ -221,6 +372,10 @@ func TestFileDeltas(t *testing.T) {
Value: -97.92,
Currency: USD,
Description: "Electricity / Power Bill TG2PJ-2PLP5",
Payee: true,
fileName: "",
lineNo: 0,
},
{
Date: "2022-12-12",
@@ -228,6 +383,9 @@ func TestFileDeltas(t *testing.T) {
Value: 97.92,
Currency: USD,
Description: "Electricity / Power Bill TG2PJ-2PLP5",
fileName: "",
lineNo: 0,
},
{
Date: "2022-12-12",
@@ -235,6 +393,10 @@ func TestFileDeltas(t *testing.T) {
Value: -1.00,
Currency: USD,
Description: "Test pay chase TG32S-BT2FF",
Payee: true,
fileName: "",
lineNo: 0,
},
{
Date: "2022-12-12",
@@ -242,6 +404,9 @@ func TestFileDeltas(t *testing.T) {
Value: 1.00,
Currency: USD,
Description: "Test pay chase TG32S-BT2FF",
fileName: "",
lineNo: 0,
},
}
@@ -254,7 +419,8 @@ func TestFileDeltas(t *testing.T) {
for name, d := range cases {
want := d
t.Run(name, func(t *testing.T) {
f, err := NewFiles("./testdata/" + name + ".dat")
fileName := "./testdata/" + name + ".dat"
f, err := NewFiles(fileName)
if err != nil {
t.Fatal(err)
}
@@ -268,10 +434,16 @@ func TestFileDeltas(t *testing.T) {
t.Error(len(deltas))
}
for i := range want {
want[i].fileName = fileName
deltas[i].lineNo = 0
if i >= len(deltas) {
break
}
if want[i] != deltas[i] {
if deltas[i].Transaction == "" {
t.Error(deltas[i].Transaction)
}
deltas[i].Transaction = ""
if !want[i].equivalent(deltas[i]) {
t.Errorf("[%d] \n\twant=%s, \n\t got=%s", i, want[i].Debug(), deltas[i].Debug())
}
}
@@ -293,148 +465,3 @@ func TestFilesOfDir(t *testing.T) {
t.Error(paths)
}
}
func TestFilesTempGetLastNLines(t *testing.T) {
cases := map[string]struct {
input string
n int
want []string
}{
"empty": {},
"get n lines from empty file": {
input: "",
n: 5,
want: []string{},
},
"get 0 lines from file": {
input: "#a\n#b",
n: 0,
want: []string{},
},
"get 3 lines from 2 line file": {
input: "#a\n#b",
n: 3,
want: []string{"#a", "#b"},
},
"get 2 lines from 3 line file": {
input: "#a\n#b\n#c",
n: 2,
want: []string{"#b", "#c"},
},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
p := path.Join(t.TempDir(), base64.URLEncoding.EncodeToString([]byte(t.Name())))
os.WriteFile(p, []byte(c.input), os.ModePerm)
files, err := NewFiles(p)
if err != nil {
panic(err)
}
if got, err := files.TempGetLastNLines(c.n); err != nil {
t.Fatal(err)
} else if fmt.Sprint(got) != fmt.Sprint(c.want) {
for i := range c.want {
t.Logf("want[%d] = %q", i, c.want[i])
}
for i := range got {
t.Logf(" got[%d] = %q", i, got[i])
}
t.Errorf("wanted\n\t%+v, got\n\t%+v", c.want, got)
}
})
}
}
func TestFilesTempSetLastNLines(t *testing.T) {
cases := map[string]struct {
given string
input []string
n int
want string
}{
"empty": {},
"append to empty": {
input: []string{"hello", "world"},
n: 100,
want: "hello\nworld\n",
},
"replace last 10 of 1 lines with 2": {
given: "ohno",
input: []string{"hello", "world"},
n: 10,
want: "hello\nworld\n",
},
"replace last 1 of 1 lines with 2": {
given: "ohno",
input: []string{"hello", "world"},
n: 1,
want: "hello\nworld\n",
},
"replace last 0 of 1 lines with 2": {
given: "ohno",
input: []string{"hello", "world"},
n: 0,
want: "ohno\nhello\nworld\n",
},
"replace last 1 of 1 lines with 0": {
given: "ohno",
input: []string{},
n: 1,
want: "",
},
"replace last 1 of 2 lines with 1": {
given: "ohno\nhaha",
input: []string{"replaced"},
n: 1,
want: "ohno\nreplaced\n",
},
"replace last 1 of 2 lines with 2": {
given: "ohno\nhaha",
input: []string{"replac", "ed"},
n: 1,
want: "ohno\nreplac\ned\n",
},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
p := path.Join(t.TempDir(), base64.URLEncoding.EncodeToString([]byte(t.Name())))
realp := p + ".real"
os.WriteFile(realp, []byte(c.given), os.ModePerm)
if err := os.Symlink(realp, p); err != nil {
t.Fatal(err)
}
if stat, err := os.Lstat(p); err != nil {
t.Error(err)
} else if stat.Mode().IsRegular() {
t.Error("p is already a regular file")
}
files := Files([]string{p})
if err := files.TempSetLastNLines(c.n, c.input); err != nil {
t.Fatal(err)
}
got, _ := os.ReadFile(p)
if string(got) != c.want {
t.Errorf("want\n%s, got\n%s", c.want, got)
}
realb, _ := os.ReadFile(realp)
b, _ := os.ReadFile(realp)
if !bytes.Equal(b, realb) {
t.Errorf("%s no longer links to %s", p, realp)
}
if stat, err := os.Lstat(p); err != nil {
t.Error(err)
} else if stat.Mode().IsRegular() {
t.Error("p is now a regular file")
}
})
}
}

View File

@@ -1,6 +1,8 @@
package ledger
import "regexp"
import (
"regexp"
)
type Group func(Delta) Delta
@@ -17,6 +19,9 @@ func GroupDate(pattern string) Group {
p := regexp.MustCompile(pattern)
return func(d Delta) Delta {
d.Date = p.FindString(d.Date)
for i := range d.with {
d.with[i].Date = p.FindString(d.with[i].Date)
}
return d
}
}
@@ -25,6 +30,9 @@ func GroupName(pattern string) Group {
p := regexp.MustCompile(pattern)
return func(d Delta) Delta {
d.Name = p.FindString(d.Name)
for i := range d.with {
d.with[i].Name = p.FindString(d.with[i].Name)
}
return d
}
}

View File

@@ -8,6 +8,36 @@ type Like func(Delta) bool
type Likes []Like
func LikeWith(pattern string) Like {
l := LikeName(pattern)
return func(d Delta) bool {
if l(d) {
return true
}
for _, d := range d.with {
if l(d) {
return true
}
}
return false
}
}
func LikeTransactions(deltas ...Delta) Like {
return func(d Delta) bool {
for i := range deltas {
if deltas[i].Transaction == d.Transaction {
return true
}
}
return false
}
}
func LikeTransaction(delta Delta) Like {
return LikeTransactions(delta)
}
func LikeBefore(date string) Like {
return func(d Delta) bool {
return date >= d.Date
@@ -20,6 +50,22 @@ func LikeAfter(date string) Like {
}
}
func LikeNotName(pattern string) Like {
return func(d Delta) bool {
return !like(pattern, d.Name)
}
}
func LikeNot(like Like) Like {
return func(d Delta) bool {
return !like(d)
}
}
func NotLikeName(pattern string) Like {
return LikeNot(LikeName(pattern))
}
func LikeName(pattern string) Like {
return func(d Delta) bool {
return like(pattern, d.Name)

View File

@@ -36,3 +36,20 @@ func TestLikesAll(t *testing.T) {
t.Error(likes.All(delta))
}
}
func TestLikeWith(t *testing.T) {
delta := Delta{
Name: "x",
with: []Delta{
Delta{Name: "y"},
Delta{Name: "z"},
},
}
if !LikeWith("x")(delta) {
t.Error("like with self not caught")
}
if !LikeWith("z")(delta) {
t.Error("like with reverse not caught")
}
}

View File

@@ -51,6 +51,26 @@ func (register Register) Names() []string {
return result
}
func (register Register) Like(pattern string) Register {
result := make(Register)
for k, v := range register {
if got := v.Like(pattern); len(got) > 0 {
result[k] = got
}
}
return result
}
func (register Register) NotLike(pattern string) Register {
result := make(Register)
for k, v := range register {
if got := v.NotLike(pattern); len(got) > 0 {
result[k] = got
}
}
return result
}
func (register Register) Dates() []string {
result := make([]string, 0, len(register))
for k := range register {

View File

@@ -3,19 +3,116 @@ package ledger
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"regexp"
"slices"
"strconv"
"strings"
"unicode"
)
type Transaction Deltas
type Transactions []Transaction
func (transactions Transactions) Deltas() Deltas {
result := make(Deltas, 0, len(transactions))
for _, transaction := range transactions {
result = append(result, transaction...)
}
return result
}
func (deltas Deltas) Transactions() Transactions {
m := make(map[string]Transaction)
for _, d := range deltas {
m[d.Transaction] = append(m[d.Transaction], d)
}
result := make(Transactions, 0, len(m))
for _, v := range m {
result = append(result, v)
}
slices.SortFunc(result, func(a, b Transaction) int {
if a[0].Date == b[0].Date {
if a[0].Description == b[0].Description {
return strings.Compare(a[0].Transaction, b[0].Transaction)
}
return strings.Compare(a[0].Description, b[0].Description)
}
return strings.Compare(a[0].Date, b[0].Date)
})
return result
}
func (transactions Transactions) NotLike(like ...Like) Transactions {
result := make(Transactions, 0, len(transactions))
for _, transaction := range transactions {
if matching := (Deltas)(transaction).Like(like...); len(matching) == 0 {
result = append(result, transaction)
}
}
return result
}
func (transactions Transactions) Like(like ...Like) Transactions {
result := make(Transactions, 0, len(transactions))
for _, transaction := range transactions {
if matching := (Deltas)(transaction).Like(like...); len(matching) > 0 {
result = append(result, transaction)
}
}
return result
}
func (transaction Transaction) Payee() string {
balances := Deltas(transaction).Balances()
candidates := []string{}
for name, balance := range balances {
deltas := Deltas(transaction).Like(LikeName(`^` + name + `$`))
if len(deltas) != 1 {
continue
}
everyoneElse := balances.NotLike(`^` + name + `$`).Group(`^`)[""]
matches := true
for currency, value := range balance {
matches = matches && everyoneElse[currency]*-1 == value
}
if matches {
candidates = append(candidates, name)
}
}
slices.Sort(candidates)
if len(candidates) == 0 {
panic(balances)
}
for _, candidate := range candidates {
if strings.HasPrefix(candidate, "Withdrawal") {
return candidate
}
}
return candidates[len(candidates)-1]
}
type transaction struct {
date string
description string
payee string
recipients []transactionRecipient
name string
fileName string
lineNo int
}
func (t transaction) empty() bool {
@@ -29,6 +126,57 @@ type transactionRecipient struct {
isSet bool
}
func (t transaction) deltas() Deltas {
result := []Delta{}
sums := map[string]float64{}
for i, recipient := range t.recipients {
sums[recipient.currency] += recipient.value
result = append(result, newDelta(
t.name,
true,
t.date,
t.description,
recipient.name,
recipient.value,
recipient.currency,
recipient.isSet,
t.fileName,
t.lineNo+i,
))
}
for currency, value := range sums {
if value == 0 {
continue
}
if t.payee == "" {
//return nil, fmt.Errorf("didnt find net zero and no dumping ground payee set: %+v", transaction)
} else {
result = append(result, newDelta(
t.name,
false,
t.date,
t.description,
t.payee,
-1.0*value,
currency,
false,
t.fileName,
t.lineNo,
))
}
}
for i := range result {
for j := range result {
if i != j {
result[i] = result[i].withWith(result[j])
}
}
}
return result
}
func (t transactionRecipient) empty() bool {
return t == (transactionRecipient{})
}
@@ -59,8 +207,11 @@ func (files Files) _transactions(file string) ([]transaction, error) {
result := make([]transaction, 0)
for {
one, err := readTransaction(r)
name := fmt.Sprintf("%s/%d", file, len(result))
one, err := readTransaction(name, r)
if !one.empty() {
one.fileName = file
one.lineNo = len(result)
result = append(result, one)
}
if err == io.EOF {
@@ -72,24 +223,63 @@ func (files Files) _transactions(file string) ([]transaction, error) {
}
}
func readTransaction(r *bufio.Reader) (transaction, error) {
result, err := _readTransaction(r)
if err != nil {
func readTransaction(name string, r *bufio.Reader) (transaction, error) {
result, err := _readTransaction(name, r)
if err != nil && !errors.Is(err, io.EOF) {
return result, err
}
if result.empty() {
return result, nil
}
if result.payee == "" && len(result.recipients) < 2 {
return result, fmt.Errorf("found a transaction with no payee and less than 2 recipeints: %+v", result)
return result, err
}
if result.payee != "" && len(result.recipients) < 1 {
return result, fmt.Errorf("found a transaction with payee but no recipeints: %+v", result)
}
return result, nil
if result.payee == "" {
if len(result.recipients) < 2 {
return result, fmt.Errorf("found a transaction with no payee and less than 2 recipeints: %+v", result)
}
func() {
sumPerRecipient := map[string]float64{}
recipients := []string{}
for _, recipient := range result.recipients {
recipients = append(recipients, recipient.name)
sumPerRecipient[recipient.name] += recipient.value
}
slices.Sort(recipients)
for _, k := range recipients {
n := 0
for i := range result.recipients {
if result.recipients[i].name == k {
n += 1
}
}
if n != 1 {
continue
}
v := sumPerRecipient[k]
everyoneElse := 0.0
for j := range sumPerRecipient {
if k != j {
everyoneElse += sumPerRecipient[j]
}
}
if -1.0*v == everyoneElse {
result.payee = k
result.recipients = slices.DeleteFunc(result.recipients, func(recipient transactionRecipient) bool {
return recipient.name == k
})
return
}
}
return
}()
}
return result, err
}
func _readTransaction(r *bufio.Reader) (transaction, error) {
func _readTransaction(name string, r *bufio.Reader) (transaction, error) {
readTransactionLeadingWhitespace(r)
firstLine, err := readTransactionLine(r)
@@ -107,6 +297,7 @@ func _readTransaction(r *bufio.Reader) (transaction, error) {
result := transaction{
date: string(dateDescriptionMatches[0][1]),
description: string(dateDescriptionMatches[0][2]),
name: name,
}
for {
@@ -193,6 +384,7 @@ func readTransactionName(r *bufio.Reader) (string, float64, string, bool, error)
return "", 0, "", false, nil
}
line = bytes.Split(line, []byte(";"))[0] // comment-free
fields := bytes.Fields(line)
isSet := false

View File

@@ -8,6 +8,38 @@ import (
"testing"
)
func TestTransactionPayee(t *testing.T) {
given := Transaction{
Delta{Name: "x", Transaction: "a", Value: 9},
Delta{Name: "Withdrawal:z", Transaction: "a", Value: -3},
Delta{Name: "Withdrawal:z", Transaction: "a", Value: -6},
}
got := given.Payee()
if got != "x" {
t.Error(got)
}
}
func TestDeltasTransactions(t *testing.T) {
given := Deltas{
Delta{Date: "2", Name: "x", Transaction: "a"},
Delta{Date: "2", Name: "y", Transaction: "a"},
Delta{Date: "1", Name: "z", Transaction: "b"},
}
got := given.Transactions()
if len(got) != 2 {
t.Error(len(got))
}
if len(got[0]) != 1 {
t.Error("first xaction is not earliest date", len(got[0]))
}
if len(got[1]) != 2 {
t.Error("second xaction is not latest date", len(got[1]))
}
}
func TestReadTransaction(t *testing.T) {
cases := map[string]struct {
input string
@@ -33,7 +65,28 @@ func TestReadTransaction(t *testing.T) {
want: transaction{
date: "2003-04-05",
description: "Reasoning here",
payee: "",
payee: "A:B",
recipients: []transactionRecipient{
{
name: "C:D",
value: -1.0,
currency: "$",
},
},
},
err: io.EOF,
},
"multi send": {
input: `
2003-04-05 Reasoning here
A:B $1.00
A:B $2.00
C:D
`,
want: transaction{
date: "2003-04-05",
description: "Reasoning here",
payee: "C:D",
recipients: []transactionRecipient{
{
name: "A:B",
@@ -41,8 +94,8 @@ func TestReadTransaction(t *testing.T) {
currency: "$",
},
{
name: "C:D",
value: -1.0,
name: "A:B",
value: 2.0,
currency: "$",
},
},
@@ -55,7 +108,7 @@ func TestReadTransaction(t *testing.T) {
c := d
t.Run(name, func(t *testing.T) {
r := bufio.NewReader(strings.NewReader(c.input))
got, err := readTransaction(r)
got, err := readTransaction("", r)
if err != c.err {
t.Error(err)
}

160
src/view/chart.go Normal file
View File

@@ -0,0 +1,160 @@
package view
import (
"fmt"
"io"
"slices"
"strings"
"github.com/go-echarts/go-echarts/v2/charts"
"github.com/go-echarts/go-echarts/v2/opts"
)
type Chart interface {
AddX(interface{})
AddY(string, []int)
Render(io.Writer) error
Overlap(Chart)
}
func NewChart(name string) Chart {
switch name {
case "line":
return NewLine()
case "bar":
return NewBar()
case "stack":
return NewStack()
case "pie":
return NewPie()
default:
panic("bad chart name " + name)
}
}
type Line struct {
*charts.Line
}
func NewLine() Line {
return Line{Line: charts.NewLine()}
}
func (line Line) AddX(v interface{}) {
line.SetXAxis(v)
}
func (line Line) AddY(name string, v []int) {
y := make([]opts.LineData, len(v))
for i := range y {
y[i].Value = v[i]
}
line.AddSeries(name, y).
SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{
Stack: "stackB",
}))
}
func (line Line) Overlap(other Chart) {
overlapper, ok := other.(charts.Overlaper)
if !ok {
panic(fmt.Sprintf("cannot overlap %T", other))
}
line.Line.Overlap(overlapper)
}
type Bar struct {
*charts.Bar
}
func NewBar() Bar {
return Bar{Bar: charts.NewBar()}
}
func (bar Bar) AddX(v interface{}) {
bar.SetXAxis(v)
}
func (bar Bar) AddY(name string, v []int) {
y := make([]opts.BarData, len(v))
for i := range v {
y[i].Value = v[i]
}
bar.AddSeries(name, y)
}
func (bar Bar) Overlap(other Chart) {
overlapper, ok := other.(charts.Overlaper)
if !ok {
panic(fmt.Sprintf("cannot overlap %T", other))
}
bar.Bar.Overlap(overlapper)
}
type Stack struct {
Bar
}
func NewStack() Stack {
bar := NewBar()
bar.SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{Stack: "x"}))
return Stack{Bar: bar}
}
func (stack Stack) AddY(name string, v []int) {
y := make([]opts.BarData, len(v))
for i := range v {
y[i].Value = v[i]
}
stack.AddSeries(name, y).
SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{
Stack: "stackA",
}))
}
type Pie struct {
*charts.Pie
series []opts.PieData
}
func NewPie() *Pie {
return &Pie{Pie: charts.NewPie()}
}
func (pie *Pie) AddX(v interface{}) {
}
func (pie *Pie) Render(w io.Writer) error {
slices.SortFunc(pie.series, func(a, b opts.PieData) int {
return strings.Compare(a.Name, b.Name)
})
commonPrefixLen := -1
for i := 0; i < len(pie.series[0].Name) && i < len(pie.series[len(pie.series)-1].Name); i++ {
if pie.series[0].Name[i] != pie.series[len(pie.series)-1].Name[i] {
break
}
commonPrefixLen = i
}
for i := range pie.series {
pie.series[i].Name = pie.series[i].Name[commonPrefixLen+1:]
}
pie.AddSeries("", pie.series)
pie.SetGlobalOptions(charts.WithLegendOpts(opts.Legend{
Show: false,
}))
return pie.Pie.Render(w)
}
func (pie *Pie) AddY(name string, v []int) {
for _, v := range v {
pie.series = append(pie.series, opts.PieData{
Name: fmt.Sprintf("%s ($%d)", name, v),
Value: v,
})
}
}
func (pie *Pie) Overlap(other Chart) {
panic("nope")
}

View File

@@ -1,6 +1,15 @@
todo:
- cd including run
- clean ui for /transactions.html
- |
scale by salaries for proportional house contributions
if bel makes 190k and contributes $190
equal to z making 140k and contri $140
so contrib/sal = %of_me_contributed
... is that fair tho?
i think it is--if we both give N%, then 100% of house is covered, so we were hit equally
- ui can create transactions
- combine transactions in /transactions.html
- amend /transactions.html
- cicd including run
scheduled: []
done:
- todo: balances over time window

18
vendor/github.com/guptarohit/asciigraph/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,18 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# ide
.idea
# build dirs
*dist*

View File

@@ -0,0 +1,74 @@
# Build customization
builds:
- env:
- CGO_ENABLED=0
main: ./cmd/asciigraph/main.go
ldflags: '-s -w'
# GOOS list to build in.
# For more info refer to https://golang.org/doc/install/source#environment
goos:
- linux
- darwin
- windows
# GOARCH to build in.
# For more info refer to https://golang.org/doc/install/source#environment
goarch:
- 386
- amd64
- arm
- arm64
ignore:
- goos: darwin
goarch: 386
checksum:
name_template: '{{ .ProjectName }}_{{ .Version }}_sha512-checksums.txt'
algorithm: sha512
# Archive customization
archives:
- id: tar
format: tar.gz
files:
- LICENSE
- README.md
name_template: >-
{{- .ProjectName }}_
{{- .Version }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end -}}
format_overrides:
- goos: windows
format: zip
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
release:
github:
owner: guptarohit
name: asciigraph
# If set to true, will not auto-publish the release.
# Default is false.
draft: true
dockers:
- image_templates:
- 'ghcr.io/guptarohit/asciigraph:{{ .Version }}'
- 'ghcr.io/guptarohit/asciigraph:{{ .Tag }}'
- 'ghcr.io/guptarohit/asciigraph:v{{ .Major }}'
- 'ghcr.io/guptarohit/asciigraph:v{{ .Major }}.{{ .Minor }}'
- 'ghcr.io/guptarohit/asciigraph:latest'
dockerfile: goreleaser.dockerfile
build_flag_templates:
- '--label=org.opencontainers.image.title={{ .ProjectName }}'
- '--label=org.opencontainers.image.name={{ .ProjectName }}'
- '--label=org.opencontainers.image.description=Go package to make lightweight line graphs ╭┈╯ in CLI'
- '--label=org.opencontainers.image.url=https://github.com/guptarohit/asciigraph'
- '--label=org.opencontainers.image.source=https://github.com/guptarohit/asciigraph'
- '--label=org.opencontainers.image.version={{ .Version }}'
- '--label=org.opencontainers.image.created={{ .Date }}'
- '--label=org.opencontainers.image.revision={{ .FullCommit }}'
- '--label=org.opencontainers.image.licenses=BSD-3-Clause'

113
vendor/github.com/guptarohit/asciigraph/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,113 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.7.3] - 2024-10-26
### Fixed
- Incorrect plot height calculation for small value ranges (#59)
## [0.7.2] - 2024-08-12
### Fixed
- Unintended modification of input data (#55)
## [0.7.1] - 2024-03-30
### Added
- CLI: Option to specify legends for series (`sl`)
## [0.7.0] - 2024-03-30
### Added
- CLI: Options to specify delimiter (`d`) and number of series (`sn`)
### Changed
- CLI: Option (`sc`) to specify series colors
## [0.6.0] - 2024-03-25
### Added
- Option to add legends for colored graphs
## [0.5.6] - 2023-06-24
### Added
- Options to set upper & lower bound of graph
## [0.5.5] - 2022-05-03
### Added
- Ansi colors support for graphs
## [0.5.4] - 2022-05-03
### Added
- Option to plot multiple series together (#34)
- Dockerfile file support (#33)
## [0.5.3] - 2022-02-20
### Fixed
- Handled NaN first value (#32)
- Fixed incorrect y-axis start value tick (#31)
## [0.5.2] - 2021-03-28
### Added
- added support to set custom precision of data point labels along the y-axis
- added go module support
### Changed
- updated README to markdown format
## [0.5.1] - 2020-09-14
### Added
- added support for NaN values in series
- added option to control fps of plot rendering via cli for real-time data
### Changed
- removed use of append() method
- make caption centered
- removed trailing spaces from plot
## [0.5.0] - 2020-06-28
### Added
- added support for the realtime plot of data points (from stdin) for CLI.
## [0.4.2] - 2020-06-07
### Fixed
- Prevent panics when data is flat. (#8)
- Prevent BADPREC issue when maximum and minimum values in a series are 0. (#10)
[0.7.2]: https://github.com/guptarohit/asciigraph/releases/tag/v0.7.2
[0.7.1]: https://github.com/guptarohit/asciigraph/releases/tag/v0.7.1
[0.7.0]: https://github.com/guptarohit/asciigraph/releases/tag/v0.7.0
[0.6.0]: https://github.com/guptarohit/asciigraph/releases/tag/v0.6.0
[0.5.6]: https://github.com/guptarohit/asciigraph/releases/tag/v0.5.6
[0.5.5]: https://github.com/guptarohit/asciigraph/releases/tag/v0.5.5
[0.5.4]: https://github.com/guptarohit/asciigraph/releases/tag/v0.5.4
[0.5.3]: https://github.com/guptarohit/asciigraph/releases/tag/v0.5.3
[0.5.2]: https://github.com/guptarohit/asciigraph/releases/tag/v0.5.2
[0.5.1]: https://github.com/guptarohit/asciigraph/releases/tag/v0.5.1
[0.5.0]: https://github.com/guptarohit/asciigraph/releases/tag/v0.5.0
[0.4.2]: https://github.com/guptarohit/asciigraph/releases/tag/v0.4.2

View File

@@ -0,0 +1,133 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
https://github.com/guptarohit.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

10
vendor/github.com/guptarohit/asciigraph/Dockerfile generated vendored Normal file
View File

@@ -0,0 +1,10 @@
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY cmd ./cmd
COPY go.mod ./
COPY *.go ./
RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /app/asciigraph ./cmd/asciigraph/main.go
FROM scratch
COPY --from=builder /app/asciigraph /asciigraph
ENTRYPOINT ["/asciigraph"]

29
vendor/github.com/guptarohit/asciigraph/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2018, Rohit Gupta
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

292
vendor/github.com/guptarohit/asciigraph/README.md generated vendored Normal file
View File

@@ -0,0 +1,292 @@
# asciigraph
[![Build status][]][1] [![Go Report Card][]][2] [![Coverage Status][]][3] [![GoDoc][]][4] [![License][]][5] [![Mentioned in Awesome Go][]][6]
Go package to make lightweight ASCII line graphs ╭┈╯.
![image][]
## Installation
```bash
go get -u github.com/guptarohit/asciigraph@latest
```
## Usage
### Basic graph
```go
package main
import (
"fmt"
"github.com/guptarohit/asciigraph"
)
func main() {
data := []float64{3, 4, 9, 6, 2, 4, 5, 8, 5, 10, 2, 7, 2, 5, 6}
graph := asciigraph.Plot(data)
fmt.Println(graph)
}
```
Running this example would render the following graph:
```bash
10.00 ┤ ╭╮
9.00 ┤ ╭╮ ││
8.00 ┤ ││ ╭╮││
7.00 ┤ ││ ││││╭╮
6.00 ┤ │╰╮ ││││││ ╭
5.00 ┤ │ │ ╭╯╰╯│││╭╯
4.00 ┤╭╯ │╭╯ ││││
3.00 ┼╯ ││ ││││
2.00 ┤ ╰╯ ╰╯╰╯
```
### Multiple Series
```go
package main
import (
"fmt"
"github.com/guptarohit/asciigraph"
)
func main() {
data := [][]float64{{0, 1, 2, 3, 3, 3, 2, 0}, {5, 4, 2, 1, 4, 6, 6}}
graph := asciigraph.PlotMany(data)
fmt.Println(graph)
}
```
Running this example would render the following graph:
```bash
6.00 ┤ ╭─
5.00 ┼╮ │
4.00 ┤╰╮ ╭╯
3.00 ┤ │╭│─╮
2.00 ┤ ╰╮│ ╰╮
1.00 ┤╭╯╰╯ │
0.00 ┼╯ ╰
```
### Colored graphs
```go
package main
import (
"fmt"
"github.com/guptarohit/asciigraph"
)
func main() {
data := make([][]float64, 4)
for i := 0; i < 4; i++ {
for x := -20; x <= 20; x++ {
v := math.NaN()
if r := 20 - i; x >= -r && x <= r {
v = math.Sqrt(math.Pow(float64(r), 2)-math.Pow(float64(x), 2)) / 2
}
data[i] = append(data[i], v)
}
}
graph := asciigraph.PlotMany(data, asciigraph.Precision(0), asciigraph.SeriesColors(
asciigraph.Red,
asciigraph.Yellow,
asciigraph.Green,
asciigraph.Blue,
))
fmt.Println(graph)
}
```
Running this example would render the following graph:
![colored_graph_image][]
### Legends for colored graphs
The graph can include legends for each series, making it easier to interpret.
```go
package main
import (
"fmt"
"github.com/guptarohit/asciigraph"
"math"
)
func main() {
data := make([][]float64, 3)
for i := 0; i < 3; i++ {
for x := -12; x <= 12; x++ {
v := math.NaN()
if r := 12 - i; x >= -r && x <= r {
v = math.Sqrt(math.Pow(float64(r), 2)-math.Pow(float64(x), 2)) / 2
}
data[i] = append(data[i], v)
}
}
graph := asciigraph.PlotMany(data,
asciigraph.Precision(0),
asciigraph.SeriesColors(asciigraph.Red, asciigraph.Green, asciigraph.Blue),
asciigraph.SeriesLegends("Red", "Green", "Blue"),
asciigraph.Caption("Series with legends"))
fmt.Println(graph)
}
```
Running this example would render the following graph:
![graph_with_legends_image][]
## CLI Installation
This package also brings a small utility for command line usage.
Assuming `$GOPATH/bin` is in your `$PATH`, install CLI with following command:
```bash
go install github.com/guptarohit/asciigraph/cmd/asciigraph@latest
```
or pull Docker image:
```bash
docker pull ghcr.io/guptarohit/asciigraph:latest
```
or download binaries from the [releases][] page.
## CLI Usage
```bash ✘ 0|125  16:19:23
> asciigraph --help
Usage of asciigraph:
asciigraph [options]
Options:
-ac axis color
y-axis color of the plot
-b buffer
data points buffer when realtime graph enabled, default equal to `width`
-c caption
caption for the graph
-cc caption color
caption color of the plot
-d delimiter
data delimiter for splitting data points in the input stream (default ",")
-f fps
set fps to control how frequently graph to be rendered when realtime graph enabled (default 24)
-h height
height in text rows, 0 for auto-scaling
-lb lower bound
lower bound set the minimum value for the vertical axis (ignored if series contains lower values) (default +Inf)
-lc label color
y-axis label color of the plot
-o offset
offset in columns, for the label (default 3)
-p precision
precision of data point labels along the y-axis (default 2)
-r realtime
enables realtime graph for data stream
-sc series colors
comma-separated series colors corresponding to each series
-sl series legends
comma-separated series legends corresponding to each series
-sn number of series
number of series (columns) in the input data (default 1)
-ub upper bound
upper bound set the maximum value for the vertical axis (ignored if series contains larger values) (default -Inf)
-w width
width in columns, 0 for auto-scaling
asciigraph expects data points from stdin. Invalid values are logged to stderr.
```
Feed it data points via stdin:
```bash
seq 1 72 | asciigraph -h 10 -c "plot data from stdin"
```
or use Docker image:
```bash
seq 1 72 | docker run -i --rm ghcr.io/guptarohit/asciigraph -h 10 -c "plot data from stdin"
```
Output:
```bash
72.00 ┤ ╭────
64.90 ┤ ╭──────╯
57.80 ┤ ╭──────╯
50.70 ┤ ╭──────╯
43.60 ┤ ╭──────╯
36.50 ┤ ╭───────╯
29.40 ┤ ╭──────╯
22.30 ┤ ╭──────╯
15.20 ┤ ╭──────╯
8.10 ┤ ╭──────╯
1.00 ┼──╯
plot data from stdin
```
Example of **real-time graph** for data points stream via stdin:
<a href="https://asciinema.org/a/382383" target="_blank"><img width="500" alt="Realtime graph for data points via stdin (google ping) using asciigraph" src="https://asciinema.org/a/382383.svg" /></a>
<details>
<summary>command for above graph</summary>
```sh
ping -i.2 google.com | grep -oP '(?<=time=).*(?=ms)' --line-buffered | asciigraph -r -h 10 -w 40 -c "realtime plot data (google ping in ms) from stdin"
```
</details>
Example of **multi-series real-time graph** for data points stream via stdin:
<a href="https://asciinema.org/a/649906" target="_blank"><img width="500" alt="Ping latency comparison: Google (Blue) vs. DuckDuckGo (Red) with asciigraph" src="https://asciinema.org/a/649906.svg" /></a>
<details>
<summary>command for above graph</summary>
```sh
{unbuffer paste -d, <(ping -i 0.4 google.com | sed -u -n -E 's/.*time=(.*)ms.*/\1/p') <(ping -i 0.4 duckduckgo.com | sed -u -n -E 's/.*time=(.*)ms.*/\1/p') } | asciigraph -r -h 15 -w 60 -sn 2 -sc "blue,red" -c "Ping Latency Comparison" -sl "Google, DuckDuckGo"
```
</details>
## Acknowledgement
This package started as golang port of [asciichart][].
## Contributing
Feel free to make a pull request! :octocat:
[Build status]: https://github.com/guptarohit/asciigraph/actions/workflows/test.yml/badge.svg
[1]: https://github.com/guptarohit/asciigraph/actions/workflows/test.yml
[Go Report Card]: https://goreportcard.com/badge/github.com/guptarohit/asciigraph
[2]: https://goreportcard.com/report/github.com/guptarohit/asciigraph
[Coverage Status]: https://coveralls.io/repos/github/guptarohit/asciigraph/badge.svg?branch=master
[3]: https://coveralls.io/github/guptarohit/asciigraph?branch=master
[GoDoc]: https://godoc.org/github.com/guptarohit/asciigraph?status.svg
[4]: https://godoc.org/github.com/guptarohit/asciigraph
[License]: https://img.shields.io/badge/licence-BSD-blue.svg
[5]: https://github.com/guptarohit/asciigraph/blob/master/LICENSE
[Mentioned in Awesome Go]: https://awesome.re/mentioned-badge-flat.svg
[6]: https://github.com/avelino/awesome-go#advanced-console-uis
[image]: https://user-images.githubusercontent.com/7895001/41509956-b1b2b3d0-7279-11e8-9d19-d7dea17d5e44.png
[colored_graph_image]: https://user-images.githubusercontent.com/7895001/166443444-40ad8113-2c0f-46d7-9c75-1cf08435ce15.png
[releases]: https://github.com/guptarohit/asciigraph/releases
[asciichart]: https://github.com/kroitor/asciichart
[graph_with_legends_image]: https://github.com/guptarohit/asciigraph/assets/7895001/4066ee95-55ca-42a4-8a03-e73ce20df5d3

265
vendor/github.com/guptarohit/asciigraph/asciigraph.go generated vendored Normal file
View File

@@ -0,0 +1,265 @@
package asciigraph
import (
"bytes"
"fmt"
"math"
"strings"
)
// Plot returns ascii graph for a series.
func Plot(series []float64, options ...Option) string {
return PlotMany([][]float64{series}, options...)
}
// PlotMany returns ascii graph for multiple series.
func PlotMany(data [][]float64, options ...Option) string {
var logMaximum float64
config := configure(config{
Offset: 3,
Precision: 2,
}, options)
// Create a deep copy of the input data
dataCopy := make([][]float64, len(data))
for i, series := range data {
dataCopy[i] = make([]float64, len(series))
copy(dataCopy[i], series)
}
data = dataCopy
lenMax := 0
for i := range data {
if l := len(data[i]); l > lenMax {
lenMax = l
}
}
if config.Width > 0 {
for i := range data {
for j := len(data[i]); j < lenMax; j++ {
data[i] = append(data[i], math.NaN())
}
data[i] = interpolateArray(data[i], config.Width)
}
lenMax = config.Width
}
minimum, maximum := math.Inf(1), math.Inf(-1)
for i := range data {
minVal, maxVal := minMaxFloat64Slice(data[i])
if minVal < minimum {
minimum = minVal
}
if maxVal > maximum {
maximum = maxVal
}
}
if config.LowerBound != nil && *config.LowerBound < minimum {
minimum = *config.LowerBound
}
if config.UpperBound != nil && *config.UpperBound > maximum {
maximum = *config.UpperBound
}
interval := math.Abs(maximum - minimum)
if config.Height <= 0 {
config.Height = calculateHeight(interval)
}
if config.Offset <= 0 {
config.Offset = 3
}
var ratio float64
if interval != 0 {
ratio = float64(config.Height) / interval
} else {
ratio = 1
}
min2 := round(minimum * ratio)
max2 := round(maximum * ratio)
intmin2 := int(min2)
intmax2 := int(max2)
rows := int(math.Abs(float64(intmax2 - intmin2)))
width := lenMax + config.Offset
type cell struct {
Text string
Color AnsiColor
}
plot := make([][]cell, rows+1)
// initialise empty 2D grid
for i := 0; i < rows+1; i++ {
line := make([]cell, width)
for j := 0; j < width; j++ {
line[j].Text = " "
line[j].Color = Default
}
plot[i] = line
}
precision := config.Precision
logMaximum = math.Log10(math.Max(math.Abs(maximum), math.Abs(minimum))) //to find number of zeros after decimal
if minimum == float64(0) && maximum == float64(0) {
logMaximum = float64(-1)
}
if logMaximum < 0 {
// negative log
if math.Mod(logMaximum, 1) != 0 {
// non-zero digits after decimal
precision += uint(math.Abs(logMaximum))
} else {
precision += uint(math.Abs(logMaximum) - 1.0)
}
} else if logMaximum > 2 {
precision = 0
}
maxNumLength := len(fmt.Sprintf("%0.*f", precision, maximum))
minNumLength := len(fmt.Sprintf("%0.*f", precision, minimum))
maxWidth := int(math.Max(float64(maxNumLength), float64(minNumLength)))
// axis and labels
for y := intmin2; y < intmax2+1; y++ {
var magnitude float64
if rows > 0 {
magnitude = maximum - (float64(y-intmin2) * interval / float64(rows))
} else {
magnitude = float64(y)
}
label := fmt.Sprintf("%*.*f", maxWidth+1, precision, magnitude)
w := y - intmin2
h := int(math.Max(float64(config.Offset)-float64(len(label)), 0))
plot[w][h].Text = label
plot[w][h].Color = config.LabelColor
plot[w][config.Offset-1].Text = "┤"
plot[w][config.Offset-1].Color = config.AxisColor
}
for i := range data {
series := data[i]
color := Default
if i < len(config.SeriesColors) {
color = config.SeriesColors[i]
}
var y0, y1 int
if !math.IsNaN(series[0]) {
y0 = int(round(series[0]*ratio) - min2)
plot[rows-y0][config.Offset-1].Text = "┼" // first value
plot[rows-y0][config.Offset-1].Color = config.AxisColor
}
for x := 0; x < len(series)-1; x++ { // plot the line
d0 := series[x]
d1 := series[x+1]
if math.IsNaN(d0) && math.IsNaN(d1) {
continue
}
if math.IsNaN(d1) && !math.IsNaN(d0) {
y0 = int(round(d0*ratio) - float64(intmin2))
plot[rows-y0][x+config.Offset].Text = "╴"
plot[rows-y0][x+config.Offset].Color = color
continue
}
if math.IsNaN(d0) && !math.IsNaN(d1) {
y1 = int(round(d1*ratio) - float64(intmin2))
plot[rows-y1][x+config.Offset].Text = "╶"
plot[rows-y1][x+config.Offset].Color = color
continue
}
y0 = int(round(d0*ratio) - float64(intmin2))
y1 = int(round(d1*ratio) - float64(intmin2))
if y0 == y1 {
plot[rows-y0][x+config.Offset].Text = "─"
} else {
if y0 > y1 {
plot[rows-y1][x+config.Offset].Text = "╰"
plot[rows-y0][x+config.Offset].Text = "╮"
} else {
plot[rows-y1][x+config.Offset].Text = "╭"
plot[rows-y0][x+config.Offset].Text = "╯"
}
start := int(math.Min(float64(y0), float64(y1))) + 1
end := int(math.Max(float64(y0), float64(y1)))
for y := start; y < end; y++ {
plot[rows-y][x+config.Offset].Text = "│"
}
}
start := int(math.Min(float64(y0), float64(y1)))
end := int(math.Max(float64(y0), float64(y1)))
for y := start; y <= end; y++ {
plot[rows-y][x+config.Offset].Color = color
}
}
}
// join columns
var lines bytes.Buffer
for h, horizontal := range plot {
if h != 0 {
lines.WriteRune('\n')
}
// remove trailing spaces
lastCharIndex := 0
for i := width - 1; i >= 0; i-- {
if horizontal[i].Text != " " {
lastCharIndex = i
break
}
}
c := Default
for _, v := range horizontal[:lastCharIndex+1] {
if v.Color != c {
c = v.Color
lines.WriteString(c.String())
}
lines.WriteString(v.Text)
}
if c != Default {
lines.WriteString(Default.String())
}
}
// add caption if not empty
if config.Caption != "" {
lines.WriteRune('\n')
lines.WriteString(strings.Repeat(" ", config.Offset+maxWidth))
if len(config.Caption) < lenMax {
lines.WriteString(strings.Repeat(" ", (lenMax-len(config.Caption))/2))
}
if config.CaptionColor != Default {
lines.WriteString(config.CaptionColor.String())
}
lines.WriteString(config.Caption)
if config.CaptionColor != Default {
lines.WriteString(Default.String())
}
}
if len(config.SeriesLegends) > 0 {
addLegends(&lines, config, lenMax, config.Offset+maxWidth)
}
return lines.String()
}

312
vendor/github.com/guptarohit/asciigraph/color.go generated vendored Normal file
View File

@@ -0,0 +1,312 @@
package asciigraph
import "fmt"
type AnsiColor byte
var (
Default AnsiColor = 0
AliceBlue AnsiColor = 255
AntiqueWhite AnsiColor = 255
Aqua AnsiColor = 14
Aquamarine AnsiColor = 122
Azure AnsiColor = 15
Beige AnsiColor = 230
Bisque AnsiColor = 224
Black AnsiColor = 188 // dummy value
BlanchedAlmond AnsiColor = 230
Blue AnsiColor = 12
BlueViolet AnsiColor = 92
Brown AnsiColor = 88
BurlyWood AnsiColor = 180
CadetBlue AnsiColor = 73
Chartreuse AnsiColor = 118
Chocolate AnsiColor = 166
Coral AnsiColor = 209
CornflowerBlue AnsiColor = 68
Cornsilk AnsiColor = 230
Crimson AnsiColor = 161
Cyan AnsiColor = 14
DarkBlue AnsiColor = 18
DarkCyan AnsiColor = 30
DarkGoldenrod AnsiColor = 136
DarkGray AnsiColor = 248
DarkGreen AnsiColor = 22
DarkKhaki AnsiColor = 143
DarkMagenta AnsiColor = 90
DarkOliveGreen AnsiColor = 59
DarkOrange AnsiColor = 208
DarkOrchid AnsiColor = 134
DarkRed AnsiColor = 88
DarkSalmon AnsiColor = 173
DarkSeaGreen AnsiColor = 108
DarkSlateBlue AnsiColor = 60
DarkSlateGray AnsiColor = 238
DarkTurquoise AnsiColor = 44
DarkViolet AnsiColor = 92
DeepPink AnsiColor = 198
DeepSkyBlue AnsiColor = 39
DimGray AnsiColor = 242
DodgerBlue AnsiColor = 33
Firebrick AnsiColor = 124
FloralWhite AnsiColor = 15
ForestGreen AnsiColor = 28
Fuchsia AnsiColor = 13
Gainsboro AnsiColor = 253
GhostWhite AnsiColor = 15
Gold AnsiColor = 220
Goldenrod AnsiColor = 178
Gray AnsiColor = 8
Green AnsiColor = 2
GreenYellow AnsiColor = 155
Honeydew AnsiColor = 15
HotPink AnsiColor = 205
IndianRed AnsiColor = 167
Indigo AnsiColor = 54
Ivory AnsiColor = 15
Khaki AnsiColor = 222
Lavender AnsiColor = 254
LavenderBlush AnsiColor = 255
LawnGreen AnsiColor = 118
LemonChiffon AnsiColor = 230
LightBlue AnsiColor = 152
LightCoral AnsiColor = 210
LightCyan AnsiColor = 195
LightGoldenrodYellow AnsiColor = 230
LightGray AnsiColor = 252
LightGreen AnsiColor = 120
LightPink AnsiColor = 217
LightSalmon AnsiColor = 216
LightSeaGreen AnsiColor = 37
LightSkyBlue AnsiColor = 117
LightSlateGray AnsiColor = 103
LightSteelBlue AnsiColor = 152
LightYellow AnsiColor = 230
Lime AnsiColor = 10
LimeGreen AnsiColor = 77
Linen AnsiColor = 255
Magenta AnsiColor = 13
Maroon AnsiColor = 1
MediumAquamarine AnsiColor = 79
MediumBlue AnsiColor = 20
MediumOrchid AnsiColor = 134
MediumPurple AnsiColor = 98
MediumSeaGreen AnsiColor = 72
MediumSlateBlue AnsiColor = 99
MediumSpringGreen AnsiColor = 48
MediumTurquoise AnsiColor = 80
MediumVioletRed AnsiColor = 162
MidnightBlue AnsiColor = 17
MintCream AnsiColor = 15
MistyRose AnsiColor = 224
Moccasin AnsiColor = 223
NavajoWhite AnsiColor = 223
Navy AnsiColor = 4
OldLace AnsiColor = 230
Olive AnsiColor = 3
OliveDrab AnsiColor = 64
Orange AnsiColor = 214
OrangeRed AnsiColor = 202
Orchid AnsiColor = 170
PaleGoldenrod AnsiColor = 223
PaleGreen AnsiColor = 120
PaleTurquoise AnsiColor = 159
PaleVioletRed AnsiColor = 168
PapayaWhip AnsiColor = 230
PeachPuff AnsiColor = 223
Peru AnsiColor = 173
Pink AnsiColor = 218
Plum AnsiColor = 182
PowderBlue AnsiColor = 152
Purple AnsiColor = 5
Red AnsiColor = 9
RosyBrown AnsiColor = 138
RoyalBlue AnsiColor = 63
SaddleBrown AnsiColor = 94
Salmon AnsiColor = 210
SandyBrown AnsiColor = 215
SeaGreen AnsiColor = 29
SeaShell AnsiColor = 15
Sienna AnsiColor = 131
Silver AnsiColor = 7
SkyBlue AnsiColor = 117
SlateBlue AnsiColor = 62
SlateGray AnsiColor = 66
Snow AnsiColor = 15
SpringGreen AnsiColor = 48
SteelBlue AnsiColor = 67
Tan AnsiColor = 180
Teal AnsiColor = 6
Thistle AnsiColor = 182
Tomato AnsiColor = 203
Turquoise AnsiColor = 80
Violet AnsiColor = 213
Wheat AnsiColor = 223
White AnsiColor = 15
WhiteSmoke AnsiColor = 255
Yellow AnsiColor = 11
YellowGreen AnsiColor = 149
)
var ColorNames = map[string]AnsiColor{
"default": Default,
"aliceblue": AliceBlue,
"antiquewhite": AntiqueWhite,
"aqua": Aqua,
"aquamarine": Aquamarine,
"azure": Azure,
"beige": Beige,
"bisque": Bisque,
"black": Black,
"blanchedalmond": BlanchedAlmond,
"blue": Blue,
"blueviolet": BlueViolet,
"brown": Brown,
"burlywood": BurlyWood,
"cadetblue": CadetBlue,
"chartreuse": Chartreuse,
"chocolate": Chocolate,
"coral": Coral,
"cornflowerblue": CornflowerBlue,
"cornsilk": Cornsilk,
"crimson": Crimson,
"cyan": Cyan,
"darkblue": DarkBlue,
"darkcyan": DarkCyan,
"darkgoldenrod": DarkGoldenrod,
"darkgray": DarkGray,
"darkgreen": DarkGreen,
"darkkhaki": DarkKhaki,
"darkmagenta": DarkMagenta,
"darkolivegreen": DarkOliveGreen,
"darkorange": DarkOrange,
"darkorchid": DarkOrchid,
"darkred": DarkRed,
"darksalmon": DarkSalmon,
"darkseagreen": DarkSeaGreen,
"darkslateblue": DarkSlateBlue,
"darkslategray": DarkSlateGray,
"darkturquoise": DarkTurquoise,
"darkviolet": DarkViolet,
"deeppink": DeepPink,
"deepskyblue": DeepSkyBlue,
"dimgray": DimGray,
"dodgerblue": DodgerBlue,
"firebrick": Firebrick,
"floralwhite": FloralWhite,
"forestgreen": ForestGreen,
"fuchsia": Fuchsia,
"gainsboro": Gainsboro,
"ghostwhite": GhostWhite,
"gold": Gold,
"goldenrod": Goldenrod,
"gray": Gray,
"green": Green,
"greenyellow": GreenYellow,
"honeydew": Honeydew,
"hotpink": HotPink,
"indianred": IndianRed,
"indigo": Indigo,
"ivory": Ivory,
"khaki": Khaki,
"lavender": Lavender,
"lavenderblush": LavenderBlush,
"lawngreen": LawnGreen,
"lemonchiffon": LemonChiffon,
"lightblue": LightBlue,
"lightcoral": LightCoral,
"lightcyan": LightCyan,
"lightgoldenrodyellow": LightGoldenrodYellow,
"lightgray": LightGray,
"lightgreen": LightGreen,
"lightpink": LightPink,
"lightsalmon": LightSalmon,
"lightseagreen": LightSeaGreen,
"lightskyblue": LightSkyBlue,
"lightslategray": LightSlateGray,
"lightsteelblue": LightSteelBlue,
"lightyellow": LightYellow,
"lime": Lime,
"limegreen": LimeGreen,
"linen": Linen,
"magenta": Magenta,
"maroon": Maroon,
"mediumaquamarine": MediumAquamarine,
"mediumblue": MediumBlue,
"mediumorchid": MediumOrchid,
"mediumpurple": MediumPurple,
"mediumseagreen": MediumSeaGreen,
"mediumslateblue": MediumSlateBlue,
"mediumspringgreen": MediumSpringGreen,
"mediumturquoise": MediumTurquoise,
"mediumvioletred": MediumVioletRed,
"midnightblue": MidnightBlue,
"mintcream": MintCream,
"mistyrose": MistyRose,
"moccasin": Moccasin,
"navajowhite": NavajoWhite,
"navy": Navy,
"oldlace": OldLace,
"olive": Olive,
"olivedrab": OliveDrab,
"orange": Orange,
"orangered": OrangeRed,
"orchid": Orchid,
"palegoldenrod": PaleGoldenrod,
"palegreen": PaleGreen,
"paleturquoise": PaleTurquoise,
"palevioletred": PaleVioletRed,
"papayawhip": PapayaWhip,
"peachpuff": PeachPuff,
"peru": Peru,
"pink": Pink,
"plum": Plum,
"powderblue": PowderBlue,
"purple": Purple,
"red": Red,
"rosybrown": RosyBrown,
"royalblue": RoyalBlue,
"saddlebrown": SaddleBrown,
"salmon": Salmon,
"sandybrown": SandyBrown,
"seagreen": SeaGreen,
"seashell": SeaShell,
"sienna": Sienna,
"silver": Silver,
"skyblue": SkyBlue,
"slateblue": SlateBlue,
"slategray": SlateGray,
"snow": Snow,
"springgreen": SpringGreen,
"steelblue": SteelBlue,
"tan": Tan,
"teal": Teal,
"thistle": Thistle,
"tomato": Tomato,
"turquoise": Turquoise,
"violet": Violet,
"wheat": Wheat,
"white": White,
"whitesmoke": WhiteSmoke,
"yellow": Yellow,
"yellowgreen": YellowGreen,
}
func (c AnsiColor) String() string {
if c == Default {
return "\x1b[0m"
}
if c == Black {
c = 0
}
if c <= Silver {
// 3-bit color
return fmt.Sprintf("\x1b[%dm", 30+byte(c))
}
if c <= White {
// 4-bit color
return fmt.Sprintf("\x1b[%dm", 82+byte(c))
}
// 8-bit color
return fmt.Sprintf("\x1b[38;5;%dm", byte(c))
}

View File

@@ -0,0 +1,3 @@
FROM scratch
COPY asciigraph /asciigraph
ENTRYPOINT ["/asciigraph"]

45
vendor/github.com/guptarohit/asciigraph/legend.go generated vendored Normal file
View File

@@ -0,0 +1,45 @@
package asciigraph
import (
"bytes"
"fmt"
"strings"
"unicode/utf8"
)
// Create legend item as a colored box and text
func createLegendItem(text string, color AnsiColor) (string, int) {
return fmt.Sprintf(
"%s■%s %s",
color.String(),
Default.String(),
text,
),
// Can't use len() because of AnsiColor, add 2 for box and space
utf8.RuneCountInString(text) + 2
}
// Add legend for each series added to the graph
func addLegends(lines *bytes.Buffer, config *config, lenMax int, leftPad int) {
lines.WriteString("\n\n")
lines.WriteString(strings.Repeat(" ", leftPad))
var legendsText string
var legendsTextLen int
rightPad := 3
for i, text := range config.SeriesLegends {
item, itemLen := createLegendItem(text, config.SeriesColors[i])
legendsText += item
legendsTextLen += itemLen
if i < len(config.SeriesLegends)-1 {
legendsText += strings.Repeat(" ", rightPad)
legendsTextLen += rightPad
}
}
if legendsTextLen < lenMax {
lines.WriteString(strings.Repeat(" ", (lenMax-legendsTextLen)/2))
}
lines.WriteString(legendsText)
}

126
vendor/github.com/guptarohit/asciigraph/options.go generated vendored Normal file
View File

@@ -0,0 +1,126 @@
package asciigraph
import (
"strings"
)
// Option represents a configuration setting.
type Option interface {
apply(c *config)
}
// config holds various graph options
type config struct {
Width, Height int
LowerBound, UpperBound *float64
Offset int
Caption string
Precision uint
CaptionColor AnsiColor
AxisColor AnsiColor
LabelColor AnsiColor
SeriesColors []AnsiColor
SeriesLegends []string
}
// An optionFunc applies an option.
type optionFunc func(*config)
// apply implements the Option interface.
func (of optionFunc) apply(c *config) { of(c) }
func configure(defaults config, options []Option) *config {
for _, o := range options {
o.apply(&defaults)
}
return &defaults
}
// Width sets the graphs width. By default, the width of the graph is
// determined by the number of data points. If the value given is a
// positive number, the data points are interpolated on the x axis.
// Values <= 0 reset the width to the default value.
func Width(w int) Option {
return optionFunc(func(c *config) {
if w > 0 {
c.Width = w
} else {
c.Width = 0
}
})
}
// Height sets the graphs height.
func Height(h int) Option {
return optionFunc(func(c *config) {
if h > 0 {
c.Height = h
} else {
c.Height = 0
}
})
}
// LowerBound sets the graph's minimum value for the vertical axis. It will be ignored
// if the series contains a lower value.
func LowerBound(min float64) Option {
return optionFunc(func(c *config) { c.LowerBound = &min })
}
// UpperBound sets the graph's maximum value for the vertical axis. It will be ignored
// if the series contains a bigger value.
func UpperBound(max float64) Option {
return optionFunc(func(c *config) { c.UpperBound = &max })
}
// Offset sets the graphs offset.
func Offset(o int) Option {
return optionFunc(func(c *config) { c.Offset = o })
}
// Precision sets the graphs precision.
func Precision(p uint) Option {
return optionFunc(func(c *config) { c.Precision = p })
}
// Caption sets the graphs caption.
func Caption(caption string) Option {
return optionFunc(func(c *config) {
c.Caption = strings.TrimSpace(caption)
})
}
// CaptionColor sets the caption color.
func CaptionColor(ac AnsiColor) Option {
return optionFunc(func(c *config) {
c.CaptionColor = ac
})
}
// AxisColor sets the axis color.
func AxisColor(ac AnsiColor) Option {
return optionFunc(func(c *config) {
c.AxisColor = ac
})
}
// LabelColor sets the axis label color.
func LabelColor(ac AnsiColor) Option {
return optionFunc(func(c *config) {
c.LabelColor = ac
})
}
// SeriesColors sets the series colors.
func SeriesColors(ac ...AnsiColor) Option {
return optionFunc(func(c *config) {
c.SeriesColors = ac
})
}
// SeriesLegends sets the legend text for the corresponding series.
func SeriesLegends(text ...string) Option {
return optionFunc(func(c *config) {
c.SeriesLegends = text
})
}

105
vendor/github.com/guptarohit/asciigraph/utils.go generated vendored Normal file
View File

@@ -0,0 +1,105 @@
package asciigraph
import (
"fmt"
"log"
"math"
"os"
"os/exec"
"runtime"
)
func minMaxFloat64Slice(v []float64) (min, max float64) {
min = math.Inf(1)
max = math.Inf(-1)
if len(v) == 0 {
panic("Empty slice")
}
for _, e := range v {
if e < min {
min = e
}
if e > max {
max = e
}
}
return
}
func round(input float64) float64 {
if math.IsNaN(input) {
return math.NaN()
}
sign := 1.0
if input < 0 {
sign = -1
input *= -1
}
_, decimal := math.Modf(input)
var rounded float64
if decimal >= 0.5 {
rounded = math.Ceil(input)
} else {
rounded = math.Floor(input)
}
return rounded * sign
}
func linearInterpolate(before, after, atPoint float64) float64 {
return before + (after-before)*atPoint
}
func interpolateArray(data []float64, fitCount int) []float64 {
var interpolatedData []float64
springFactor := float64(len(data)-1) / float64(fitCount-1)
interpolatedData = append(interpolatedData, data[0])
for i := 1; i < fitCount-1; i++ {
spring := float64(i) * springFactor
before := math.Floor(spring)
after := math.Ceil(spring)
atPoint := spring - before
interpolatedData = append(interpolatedData, linearInterpolate(data[int(before)], data[int(after)], atPoint))
}
interpolatedData = append(interpolatedData, data[len(data)-1])
return interpolatedData
}
// clear terminal screen
var Clear func()
func init() {
platform := runtime.GOOS
if platform == "windows" {
Clear = func() {
cmd := exec.Command("cmd", "/c", "cls")
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
} else {
Clear = func() {
fmt.Print("\033[2J\033[H")
}
}
}
func calculateHeight(interval float64) int {
if interval >= 1 {
return int(interval)
}
scaleFactor := math.Pow(10, math.Floor(math.Log10(interval)))
scaledDelta := interval / scaleFactor
if scaledDelta < 2 {
return int(math.Ceil(scaledDelta))
}
return int(math.Floor(scaledDelta))
}

27
vendor/golang.org/x/crypto/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,27 @@
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

22
vendor/golang.org/x/crypto/PATENTS generated vendored Normal file
View File

@@ -0,0 +1,22 @@
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Google as part of the Go project.
Google hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section)
patent license to make, have made, use, offer to sell, sell, import,
transfer and otherwise run, modify and propagate the contents of this
implementation of Go, where such license applies only to those patent
claims, both currently owned or controlled by Google and acquired in
the future, licensable by Google that are necessarily infringed by this
implementation of Go. This grant does not include claims that would be
infringed only as a consequence of further modification of this
implementation. If you or your agent or exclusive licensee institute or
order or agree to the institution of patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging
that this implementation of Go or any code incorporated within this
implementation of Go constitutes direct or contributory patent
infringement, or inducement of patent infringement, then any patent
rights granted to you under this License for this implementation of Go
shall terminate as of the date such litigation is filed.

76
vendor/golang.org/x/crypto/ssh/terminal/terminal.go generated vendored Normal file
View File

@@ -0,0 +1,76 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package terminal provides support functions for dealing with terminals, as
// commonly found on UNIX systems.
//
// Deprecated: this package moved to golang.org/x/term.
package terminal
import (
"io"
"golang.org/x/term"
)
// EscapeCodes contains escape sequences that can be written to the terminal in
// order to achieve different styles of text.
type EscapeCodes = term.EscapeCodes
// Terminal contains the state for running a VT100 terminal that is capable of
// reading lines of input.
type Terminal = term.Terminal
// NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is
// a local terminal, that terminal must first have been put into raw mode.
// prompt is a string that is written at the start of each input line (i.e.
// "> ").
func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
return term.NewTerminal(c, prompt)
}
// ErrPasteIndicator may be returned from ReadLine as the error, in addition
// to valid line data. It indicates that bracketed paste mode is enabled and
// that the returned line consists only of pasted data. Programs may wish to
// interpret pasted data more literally than typed data.
var ErrPasteIndicator = term.ErrPasteIndicator
// State contains the state of a terminal.
type State = term.State
// IsTerminal returns whether the given file descriptor is a terminal.
func IsTerminal(fd int) bool {
return term.IsTerminal(fd)
}
// ReadPassword reads a line of input from a terminal without local echo. This
// is commonly used for inputting passwords and other sensitive data. The slice
// returned does not include the \n.
func ReadPassword(fd int) ([]byte, error) {
return term.ReadPassword(fd)
}
// MakeRaw puts the terminal connected to the given file descriptor into raw
// mode and returns the previous state of the terminal so that it can be
// restored.
func MakeRaw(fd int) (*State, error) {
return term.MakeRaw(fd)
}
// Restore restores the terminal connected to the given file descriptor to a
// previous state.
func Restore(fd int, oldState *State) error {
return term.Restore(fd, oldState)
}
// GetState returns the current state of a terminal which may be useful to
// restore the terminal after a signal.
func GetState(fd int) (*State, error) {
return term.GetState(fd)
}
// GetSize returns the dimensions of the given terminal.
func GetSize(fd int) (width, height int, err error) {
return term.GetSize(fd)
}

27
vendor/golang.org/x/sys/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,27 @@
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

22
vendor/golang.org/x/sys/PATENTS generated vendored Normal file
View File

@@ -0,0 +1,22 @@
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Google as part of the Go project.
Google hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section)
patent license to make, have made, use, offer to sell, sell, import,
transfer and otherwise run, modify and propagate the contents of this
implementation of Go, where such license applies only to those patent
claims, both currently owned or controlled by Google and acquired in
the future, licensable by Google that are necessarily infringed by this
implementation of Go. This grant does not include claims that would be
infringed only as a consequence of further modification of this
implementation. If you or your agent or exclusive licensee institute or
order or agree to the institution of patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging
that this implementation of Go or any code incorporated within this
implementation of Go constitutes direct or contributory patent
infringement, or inducement of patent infringement, then any patent
rights granted to you under this License for this implementation of Go
shall terminate as of the date such litigation is filed.

8
vendor/golang.org/x/sys/plan9/asm.s generated vendored Normal file
View File

@@ -0,0 +1,8 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "textflag.h"
TEXT ·use(SB),NOSPLIT,$0
RET

30
vendor/golang.org/x/sys/plan9/asm_plan9_386.s generated vendored Normal file
View File

@@ -0,0 +1,30 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "textflag.h"
//
// System call support for 386, Plan 9
//
// Just jump to package syscall's implementation for all these functions.
// The runtime may know about them.
TEXT ·Syscall(SB),NOSPLIT,$0-32
JMP syscall·Syscall(SB)
TEXT ·Syscall6(SB),NOSPLIT,$0-44
JMP syscall·Syscall6(SB)
TEXT ·RawSyscall(SB),NOSPLIT,$0-28
JMP syscall·RawSyscall(SB)
TEXT ·RawSyscall6(SB),NOSPLIT,$0-40
JMP syscall·RawSyscall6(SB)
TEXT ·seek(SB),NOSPLIT,$0-36
JMP syscall·seek(SB)
TEXT ·exit(SB),NOSPLIT,$4-4
JMP syscall·exit(SB)

30
vendor/golang.org/x/sys/plan9/asm_plan9_amd64.s generated vendored Normal file
View File

@@ -0,0 +1,30 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "textflag.h"
//
// System call support for amd64, Plan 9
//
// Just jump to package syscall's implementation for all these functions.
// The runtime may know about them.
TEXT ·Syscall(SB),NOSPLIT,$0-64
JMP syscall·Syscall(SB)
TEXT ·Syscall6(SB),NOSPLIT,$0-88
JMP syscall·Syscall6(SB)
TEXT ·RawSyscall(SB),NOSPLIT,$0-56
JMP syscall·RawSyscall(SB)
TEXT ·RawSyscall6(SB),NOSPLIT,$0-80
JMP syscall·RawSyscall6(SB)
TEXT ·seek(SB),NOSPLIT,$0-56
JMP syscall·seek(SB)
TEXT ·exit(SB),NOSPLIT,$8-8
JMP syscall·exit(SB)

25
vendor/golang.org/x/sys/plan9/asm_plan9_arm.s generated vendored Normal file
View File

@@ -0,0 +1,25 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "textflag.h"
// System call support for plan9 on arm
// Just jump to package syscall's implementation for all these functions.
// The runtime may know about them.
TEXT ·Syscall(SB),NOSPLIT,$0-32
JMP syscall·Syscall(SB)
TEXT ·Syscall6(SB),NOSPLIT,$0-44
JMP syscall·Syscall6(SB)
TEXT ·RawSyscall(SB),NOSPLIT,$0-28
JMP syscall·RawSyscall(SB)
TEXT ·RawSyscall6(SB),NOSPLIT,$0-40
JMP syscall·RawSyscall6(SB)
TEXT ·seek(SB),NOSPLIT,$0-36
JMP syscall·exit(SB)

70
vendor/golang.org/x/sys/plan9/const_plan9.go generated vendored Normal file
View File

@@ -0,0 +1,70 @@
package plan9
// Plan 9 Constants
// Open modes
const (
O_RDONLY = 0
O_WRONLY = 1
O_RDWR = 2
O_TRUNC = 16
O_CLOEXEC = 32
O_EXCL = 0x1000
)
// Rfork flags
const (
RFNAMEG = 1 << 0
RFENVG = 1 << 1
RFFDG = 1 << 2
RFNOTEG = 1 << 3
RFPROC = 1 << 4
RFMEM = 1 << 5
RFNOWAIT = 1 << 6
RFCNAMEG = 1 << 10
RFCENVG = 1 << 11
RFCFDG = 1 << 12
RFREND = 1 << 13
RFNOMNT = 1 << 14
)
// Qid.Type bits
const (
QTDIR = 0x80
QTAPPEND = 0x40
QTEXCL = 0x20
QTMOUNT = 0x10
QTAUTH = 0x08
QTTMP = 0x04
QTFILE = 0x00
)
// Dir.Mode bits
const (
DMDIR = 0x80000000
DMAPPEND = 0x40000000
DMEXCL = 0x20000000
DMMOUNT = 0x10000000
DMAUTH = 0x08000000
DMTMP = 0x04000000
DMREAD = 0x4
DMWRITE = 0x2
DMEXEC = 0x1
)
const (
STATMAX = 65535
ERRMAX = 128
STATFIXLEN = 49
)
// Mount and bind flags
const (
MREPL = 0x0000
MBEFORE = 0x0001
MAFTER = 0x0002
MORDER = 0x0003
MCREATE = 0x0004
MCACHE = 0x0010
MMASK = 0x0017
)

212
vendor/golang.org/x/sys/plan9/dir_plan9.go generated vendored Normal file
View File

@@ -0,0 +1,212 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Plan 9 directory marshalling. See intro(5).
package plan9
import "errors"
var (
ErrShortStat = errors.New("stat buffer too short")
ErrBadStat = errors.New("malformed stat buffer")
ErrBadName = errors.New("bad character in file name")
)
// A Qid represents a 9P server's unique identification for a file.
type Qid struct {
Path uint64 // the file server's unique identification for the file
Vers uint32 // version number for given Path
Type uint8 // the type of the file (plan9.QTDIR for example)
}
// A Dir contains the metadata for a file.
type Dir struct {
// system-modified data
Type uint16 // server type
Dev uint32 // server subtype
// file data
Qid Qid // unique id from server
Mode uint32 // permissions
Atime uint32 // last read time
Mtime uint32 // last write time
Length int64 // file length
Name string // last element of path
Uid string // owner name
Gid string // group name
Muid string // last modifier name
}
var nullDir = Dir{
Type: ^uint16(0),
Dev: ^uint32(0),
Qid: Qid{
Path: ^uint64(0),
Vers: ^uint32(0),
Type: ^uint8(0),
},
Mode: ^uint32(0),
Atime: ^uint32(0),
Mtime: ^uint32(0),
Length: ^int64(0),
}
// Null assigns special "don't touch" values to members of d to
// avoid modifying them during plan9.Wstat.
func (d *Dir) Null() { *d = nullDir }
// Marshal encodes a 9P stat message corresponding to d into b
//
// If there isn't enough space in b for a stat message, ErrShortStat is returned.
func (d *Dir) Marshal(b []byte) (n int, err error) {
n = STATFIXLEN + len(d.Name) + len(d.Uid) + len(d.Gid) + len(d.Muid)
if n > len(b) {
return n, ErrShortStat
}
for _, c := range d.Name {
if c == '/' {
return n, ErrBadName
}
}
b = pbit16(b, uint16(n)-2)
b = pbit16(b, d.Type)
b = pbit32(b, d.Dev)
b = pbit8(b, d.Qid.Type)
b = pbit32(b, d.Qid.Vers)
b = pbit64(b, d.Qid.Path)
b = pbit32(b, d.Mode)
b = pbit32(b, d.Atime)
b = pbit32(b, d.Mtime)
b = pbit64(b, uint64(d.Length))
b = pstring(b, d.Name)
b = pstring(b, d.Uid)
b = pstring(b, d.Gid)
b = pstring(b, d.Muid)
return n, nil
}
// UnmarshalDir decodes a single 9P stat message from b and returns the resulting Dir.
//
// If b is too small to hold a valid stat message, ErrShortStat is returned.
//
// If the stat message itself is invalid, ErrBadStat is returned.
func UnmarshalDir(b []byte) (*Dir, error) {
if len(b) < STATFIXLEN {
return nil, ErrShortStat
}
size, buf := gbit16(b)
if len(b) != int(size)+2 {
return nil, ErrBadStat
}
b = buf
var d Dir
d.Type, b = gbit16(b)
d.Dev, b = gbit32(b)
d.Qid.Type, b = gbit8(b)
d.Qid.Vers, b = gbit32(b)
d.Qid.Path, b = gbit64(b)
d.Mode, b = gbit32(b)
d.Atime, b = gbit32(b)
d.Mtime, b = gbit32(b)
n, b := gbit64(b)
d.Length = int64(n)
var ok bool
if d.Name, b, ok = gstring(b); !ok {
return nil, ErrBadStat
}
if d.Uid, b, ok = gstring(b); !ok {
return nil, ErrBadStat
}
if d.Gid, b, ok = gstring(b); !ok {
return nil, ErrBadStat
}
if d.Muid, b, ok = gstring(b); !ok {
return nil, ErrBadStat
}
return &d, nil
}
// pbit8 copies the 8-bit number v to b and returns the remaining slice of b.
func pbit8(b []byte, v uint8) []byte {
b[0] = byte(v)
return b[1:]
}
// pbit16 copies the 16-bit number v to b in little-endian order and returns the remaining slice of b.
func pbit16(b []byte, v uint16) []byte {
b[0] = byte(v)
b[1] = byte(v >> 8)
return b[2:]
}
// pbit32 copies the 32-bit number v to b in little-endian order and returns the remaining slice of b.
func pbit32(b []byte, v uint32) []byte {
b[0] = byte(v)
b[1] = byte(v >> 8)
b[2] = byte(v >> 16)
b[3] = byte(v >> 24)
return b[4:]
}
// pbit64 copies the 64-bit number v to b in little-endian order and returns the remaining slice of b.
func pbit64(b []byte, v uint64) []byte {
b[0] = byte(v)
b[1] = byte(v >> 8)
b[2] = byte(v >> 16)
b[3] = byte(v >> 24)
b[4] = byte(v >> 32)
b[5] = byte(v >> 40)
b[6] = byte(v >> 48)
b[7] = byte(v >> 56)
return b[8:]
}
// pstring copies the string s to b, prepending it with a 16-bit length in little-endian order, and
// returning the remaining slice of b..
func pstring(b []byte, s string) []byte {
b = pbit16(b, uint16(len(s)))
n := copy(b, s)
return b[n:]
}
// gbit8 reads an 8-bit number from b and returns it with the remaining slice of b.
func gbit8(b []byte) (uint8, []byte) {
return uint8(b[0]), b[1:]
}
// gbit16 reads a 16-bit number in little-endian order from b and returns it with the remaining slice of b.
func gbit16(b []byte) (uint16, []byte) {
return uint16(b[0]) | uint16(b[1])<<8, b[2:]
}
// gbit32 reads a 32-bit number in little-endian order from b and returns it with the remaining slice of b.
func gbit32(b []byte) (uint32, []byte) {
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24, b[4:]
}
// gbit64 reads a 64-bit number in little-endian order from b and returns it with the remaining slice of b.
func gbit64(b []byte) (uint64, []byte) {
lo := uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
hi := uint32(b[4]) | uint32(b[5])<<8 | uint32(b[6])<<16 | uint32(b[7])<<24
return uint64(lo) | uint64(hi)<<32, b[8:]
}
// gstring reads a string from b, prefixed with a 16-bit length in little-endian order.
// It returns the string with the remaining slice of b and a boolean. If the length is
// greater than the number of bytes in b, the boolean will be false.
func gstring(b []byte) (string, []byte, bool) {
n, b := gbit16(b)
if int(n) > len(b) {
return "", b, false
}
return string(b[:n]), b[n:], true
}

31
vendor/golang.org/x/sys/plan9/env_plan9.go generated vendored Normal file
View File

@@ -0,0 +1,31 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Plan 9 environment variables.
package plan9
import (
"syscall"
)
func Getenv(key string) (value string, found bool) {
return syscall.Getenv(key)
}
func Setenv(key, value string) error {
return syscall.Setenv(key, value)
}
func Clearenv() {
syscall.Clearenv()
}
func Environ() []string {
return syscall.Environ()
}
func Unsetenv(key string) error {
return syscall.Unsetenv(key)
}

50
vendor/golang.org/x/sys/plan9/errors_plan9.go generated vendored Normal file
View File

@@ -0,0 +1,50 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package plan9
import "syscall"
// Constants
const (
// Invented values to support what package os expects.
O_CREAT = 0x02000
O_APPEND = 0x00400
O_NOCTTY = 0x00000
O_NONBLOCK = 0x00000
O_SYNC = 0x00000
O_ASYNC = 0x00000
S_IFMT = 0x1f000
S_IFIFO = 0x1000
S_IFCHR = 0x2000
S_IFDIR = 0x4000
S_IFBLK = 0x6000
S_IFREG = 0x8000
S_IFLNK = 0xa000
S_IFSOCK = 0xc000
)
// Errors
var (
EINVAL = syscall.NewError("bad arg in system call")
ENOTDIR = syscall.NewError("not a directory")
EISDIR = syscall.NewError("file is a directory")
ENOENT = syscall.NewError("file does not exist")
EEXIST = syscall.NewError("file already exists")
EMFILE = syscall.NewError("no free file descriptors")
EIO = syscall.NewError("i/o error")
ENAMETOOLONG = syscall.NewError("file name too long")
EINTR = syscall.NewError("interrupted")
EPERM = syscall.NewError("permission denied")
EBUSY = syscall.NewError("no free devices")
ETIMEDOUT = syscall.NewError("connection timed out")
EPLAN9 = syscall.NewError("not supported by plan 9")
// The following errors do not correspond to any
// Plan 9 system messages. Invented to support
// what package os and others expect.
EACCES = syscall.NewError("access permission denied")
EAFNOSUPPORT = syscall.NewError("address family not supported by protocol")
)

150
vendor/golang.org/x/sys/plan9/mkall.sh generated vendored Normal file
View File

@@ -0,0 +1,150 @@
#!/usr/bin/env bash
# Copyright 2009 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
# The plan9 package provides access to the raw system call
# interface of the underlying operating system. Porting Go to
# a new architecture/operating system combination requires
# some manual effort, though there are tools that automate
# much of the process. The auto-generated files have names
# beginning with z.
#
# This script runs or (given -n) prints suggested commands to generate z files
# for the current system. Running those commands is not automatic.
# This script is documentation more than anything else.
#
# * asm_${GOOS}_${GOARCH}.s
#
# This hand-written assembly file implements system call dispatch.
# There are three entry points:
#
# func Syscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr);
# func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr);
# func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr);
#
# The first and second are the standard ones; they differ only in
# how many arguments can be passed to the kernel.
# The third is for low-level use by the ForkExec wrapper;
# unlike the first two, it does not call into the scheduler to
# let it know that a system call is running.
#
# * syscall_${GOOS}.go
#
# This hand-written Go file implements system calls that need
# special handling and lists "//sys" comments giving prototypes
# for ones that can be auto-generated. Mksyscall reads those
# comments to generate the stubs.
#
# * syscall_${GOOS}_${GOARCH}.go
#
# Same as syscall_${GOOS}.go except that it contains code specific
# to ${GOOS} on one particular architecture.
#
# * types_${GOOS}.c
#
# This hand-written C file includes standard C headers and then
# creates typedef or enum names beginning with a dollar sign
# (use of $ in variable names is a gcc extension). The hardest
# part about preparing this file is figuring out which headers to
# include and which symbols need to be #defined to get the
# actual data structures that pass through to the kernel system calls.
# Some C libraries present alternate versions for binary compatibility
# and translate them on the way in and out of system calls, but
# there is almost always a #define that can get the real ones.
# See types_darwin.c and types_linux.c for examples.
#
# * zerror_${GOOS}_${GOARCH}.go
#
# This machine-generated file defines the system's error numbers,
# error strings, and signal numbers. The generator is "mkerrors.sh".
# Usually no arguments are needed, but mkerrors.sh will pass its
# arguments on to godefs.
#
# * zsyscall_${GOOS}_${GOARCH}.go
#
# Generated by mksyscall.pl; see syscall_${GOOS}.go above.
#
# * zsysnum_${GOOS}_${GOARCH}.go
#
# Generated by mksysnum_${GOOS}.
#
# * ztypes_${GOOS}_${GOARCH}.go
#
# Generated by godefs; see types_${GOOS}.c above.
GOOSARCH="${GOOS}_${GOARCH}"
# defaults
mksyscall="go run mksyscall.go"
mkerrors="./mkerrors.sh"
zerrors="zerrors_$GOOSARCH.go"
mksysctl=""
zsysctl="zsysctl_$GOOSARCH.go"
mksysnum=
mktypes=
run="sh"
case "$1" in
-syscalls)
for i in zsyscall*go
do
sed 1q $i | sed 's;^// ;;' | sh > _$i && gofmt < _$i > $i
rm _$i
done
exit 0
;;
-n)
run="cat"
shift
esac
case "$#" in
0)
;;
*)
echo 'usage: mkall.sh [-n]' 1>&2
exit 2
esac
case "$GOOSARCH" in
_* | *_ | _)
echo 'undefined $GOOS_$GOARCH:' "$GOOSARCH" 1>&2
exit 1
;;
plan9_386)
mkerrors=
mksyscall="go run mksyscall.go -l32 -plan9 -tags plan9,386"
mksysnum="./mksysnum_plan9.sh /n/sources/plan9/sys/src/libc/9syscall/sys.h"
mktypes="XXX"
;;
plan9_amd64)
mkerrors=
mksyscall="go run mksyscall.go -l32 -plan9 -tags plan9,amd64"
mksysnum="./mksysnum_plan9.sh /n/sources/plan9/sys/src/libc/9syscall/sys.h"
mktypes="XXX"
;;
plan9_arm)
mkerrors=
mksyscall="go run mksyscall.go -l32 -plan9 -tags plan9,arm"
mksysnum="./mksysnum_plan9.sh /n/sources/plan9/sys/src/libc/9syscall/sys.h"
mktypes="XXX"
;;
*)
echo 'unrecognized $GOOS_$GOARCH: ' "$GOOSARCH" 1>&2
exit 1
;;
esac
(
if [ -n "$mkerrors" ]; then echo "$mkerrors |gofmt >$zerrors"; fi
case "$GOOS" in
plan9)
syscall_goos="syscall_$GOOS.go"
if [ -n "$mksyscall" ]; then echo "$mksyscall $syscall_goos |gofmt >zsyscall_$GOOSARCH.go"; fi
;;
esac
if [ -n "$mksysctl" ]; then echo "$mksysctl |gofmt >$zsysctl"; fi
if [ -n "$mksysnum" ]; then echo "$mksysnum |gofmt >zsysnum_$GOOSARCH.go"; fi
if [ -n "$mktypes" ]; then echo "$mktypes types_$GOOS.go |gofmt >ztypes_$GOOSARCH.go"; fi
) | $run

246
vendor/golang.org/x/sys/plan9/mkerrors.sh generated vendored Normal file
View File

@@ -0,0 +1,246 @@
#!/usr/bin/env bash
# Copyright 2009 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
# Generate Go code listing errors and other #defined constant
# values (ENAMETOOLONG etc.), by asking the preprocessor
# about the definitions.
unset LANG
export LC_ALL=C
export LC_CTYPE=C
CC=${CC:-gcc}
uname=$(uname)
includes='
#include <sys/types.h>
#include <sys/file.h>
#include <fcntl.h>
#include <dirent.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <netinet/tcp.h>
#include <errno.h>
#include <sys/signal.h>
#include <signal.h>
#include <sys/resource.h>
'
ccflags="$@"
# Write go tool cgo -godefs input.
(
echo package plan9
echo
echo '/*'
indirect="includes_$(uname)"
echo "${!indirect} $includes"
echo '*/'
echo 'import "C"'
echo
echo 'const ('
# The gcc command line prints all the #defines
# it encounters while processing the input
echo "${!indirect} $includes" | $CC -x c - -E -dM $ccflags |
awk '
$1 != "#define" || $2 ~ /\(/ || $3 == "" {next}
$2 ~ /^E([ABCD]X|[BIS]P|[SD]I|S|FL)$/ {next} # 386 registers
$2 ~ /^(SIGEV_|SIGSTKSZ|SIGRT(MIN|MAX))/ {next}
$2 ~ /^(SCM_SRCRT)$/ {next}
$2 ~ /^(MAP_FAILED)$/ {next}
$2 !~ /^ETH_/ &&
$2 !~ /^EPROC_/ &&
$2 !~ /^EQUIV_/ &&
$2 !~ /^EXPR_/ &&
$2 ~ /^E[A-Z0-9_]+$/ ||
$2 ~ /^B[0-9_]+$/ ||
$2 ~ /^V[A-Z0-9]+$/ ||
$2 ~ /^CS[A-Z0-9]/ ||
$2 ~ /^I(SIG|CANON|CRNL|EXTEN|MAXBEL|STRIP|UTF8)$/ ||
$2 ~ /^IGN/ ||
$2 ~ /^IX(ON|ANY|OFF)$/ ||
$2 ~ /^IN(LCR|PCK)$/ ||
$2 ~ /(^FLU?SH)|(FLU?SH$)/ ||
$2 ~ /^C(LOCAL|READ)$/ ||
$2 == "BRKINT" ||
$2 == "HUPCL" ||
$2 == "PENDIN" ||
$2 == "TOSTOP" ||
$2 ~ /^PAR/ ||
$2 ~ /^SIG[^_]/ ||
$2 ~ /^O[CNPFP][A-Z]+[^_][A-Z]+$/ ||
$2 ~ /^IN_/ ||
$2 ~ /^LOCK_(SH|EX|NB|UN)$/ ||
$2 ~ /^(AF|SOCK|SO|SOL|IPPROTO|IP|IPV6|ICMP6|TCP|EVFILT|NOTE|EV|SHUT|PROT|MAP|PACKET|MSG|SCM|MCL|DT|MADV|PR)_/ ||
$2 == "ICMPV6_FILTER" ||
$2 == "SOMAXCONN" ||
$2 == "NAME_MAX" ||
$2 == "IFNAMSIZ" ||
$2 ~ /^CTL_(MAXNAME|NET|QUERY)$/ ||
$2 ~ /^SYSCTL_VERS/ ||
$2 ~ /^(MS|MNT)_/ ||
$2 ~ /^TUN(SET|GET|ATTACH|DETACH)/ ||
$2 ~ /^(O|F|FD|NAME|S|PTRACE|PT)_/ ||
$2 ~ /^LINUX_REBOOT_CMD_/ ||
$2 ~ /^LINUX_REBOOT_MAGIC[12]$/ ||
$2 !~ "NLA_TYPE_MASK" &&
$2 ~ /^(NETLINK|NLM|NLMSG|NLA|IFA|IFAN|RT|RTCF|RTN|RTPROT|RTNH|ARPHRD|ETH_P)_/ ||
$2 ~ /^SIOC/ ||
$2 ~ /^TIOC/ ||
$2 !~ "RTF_BITS" &&
$2 ~ /^(IFF|IFT|NET_RT|RTM|RTF|RTV|RTA|RTAX)_/ ||
$2 ~ /^BIOC/ ||
$2 ~ /^RUSAGE_(SELF|CHILDREN|THREAD)/ ||
$2 ~ /^RLIMIT_(AS|CORE|CPU|DATA|FSIZE|NOFILE|STACK)|RLIM_INFINITY/ ||
$2 ~ /^PRIO_(PROCESS|PGRP|USER)/ ||
$2 ~ /^CLONE_[A-Z_]+/ ||
$2 !~ /^(BPF_TIMEVAL)$/ &&
$2 ~ /^(BPF|DLT)_/ ||
$2 !~ "WMESGLEN" &&
$2 ~ /^W[A-Z0-9]+$/ {printf("\t%s = C.%s\n", $2, $2)}
$2 ~ /^__WCOREFLAG$/ {next}
$2 ~ /^__W[A-Z0-9]+$/ {printf("\t%s = C.%s\n", substr($2,3), $2)}
{next}
' | sort
echo ')'
) >_const.go
# Pull out the error names for later.
errors=$(
echo '#include <errno.h>' | $CC -x c - -E -dM $ccflags |
awk '$1=="#define" && $2 ~ /^E[A-Z0-9_]+$/ { print $2 }' |
sort
)
# Pull out the signal names for later.
signals=$(
echo '#include <signal.h>' | $CC -x c - -E -dM $ccflags |
awk '$1=="#define" && $2 ~ /^SIG[A-Z0-9]+$/ { print $2 }' |
grep -v 'SIGSTKSIZE\|SIGSTKSZ\|SIGRT' |
sort
)
# Again, writing regexps to a file.
echo '#include <errno.h>' | $CC -x c - -E -dM $ccflags |
awk '$1=="#define" && $2 ~ /^E[A-Z0-9_]+$/ { print "^\t" $2 "[ \t]*=" }' |
sort >_error.grep
echo '#include <signal.h>' | $CC -x c - -E -dM $ccflags |
awk '$1=="#define" && $2 ~ /^SIG[A-Z0-9]+$/ { print "^\t" $2 "[ \t]*=" }' |
grep -v 'SIGSTKSIZE\|SIGSTKSZ\|SIGRT' |
sort >_signal.grep
echo '// mkerrors.sh' "$@"
echo '// Code generated by the command above; DO NOT EDIT.'
echo
go tool cgo -godefs -- "$@" _const.go >_error.out
cat _error.out | grep -vf _error.grep | grep -vf _signal.grep
echo
echo '// Errors'
echo 'const ('
cat _error.out | grep -f _error.grep | sed 's/=\(.*\)/= Errno(\1)/'
echo ')'
echo
echo '// Signals'
echo 'const ('
cat _error.out | grep -f _signal.grep | sed 's/=\(.*\)/= Signal(\1)/'
echo ')'
# Run C program to print error and syscall strings.
(
echo -E "
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <string.h>
#include <signal.h>
#define nelem(x) (sizeof(x)/sizeof((x)[0]))
enum { A = 'A', Z = 'Z', a = 'a', z = 'z' }; // avoid need for single quotes below
int errors[] = {
"
for i in $errors
do
echo -E ' '$i,
done
echo -E "
};
int signals[] = {
"
for i in $signals
do
echo -E ' '$i,
done
# Use -E because on some systems bash builtin interprets \n itself.
echo -E '
};
static int
intcmp(const void *a, const void *b)
{
return *(int*)a - *(int*)b;
}
int
main(void)
{
int i, j, e;
char buf[1024], *p;
printf("\n\n// Error table\n");
printf("var errors = [...]string {\n");
qsort(errors, nelem(errors), sizeof errors[0], intcmp);
for(i=0; i<nelem(errors); i++) {
e = errors[i];
if(i > 0 && errors[i-1] == e)
continue;
strcpy(buf, strerror(e));
// lowercase first letter: Bad -> bad, but STREAM -> STREAM.
if(A <= buf[0] && buf[0] <= Z && a <= buf[1] && buf[1] <= z)
buf[0] += a - A;
printf("\t%d: \"%s\",\n", e, buf);
}
printf("}\n\n");
printf("\n\n// Signal table\n");
printf("var signals = [...]string {\n");
qsort(signals, nelem(signals), sizeof signals[0], intcmp);
for(i=0; i<nelem(signals); i++) {
e = signals[i];
if(i > 0 && signals[i-1] == e)
continue;
strcpy(buf, strsignal(e));
// lowercase first letter: Bad -> bad, but STREAM -> STREAM.
if(A <= buf[0] && buf[0] <= Z && a <= buf[1] && buf[1] <= z)
buf[0] += a - A;
// cut trailing : number.
p = strrchr(buf, ":"[0]);
if(p)
*p = '\0';
printf("\t%d: \"%s\",\n", e, buf);
}
printf("}\n\n");
return 0;
}
'
) >_errors.c
$CC $ccflags -o _errors _errors.c && $GORUN ./_errors && rm -f _errors.c _errors _const.go _error.grep _signal.grep _error.out

23
vendor/golang.org/x/sys/plan9/mksysnum_plan9.sh generated vendored Normal file
View File

@@ -0,0 +1,23 @@
#!/bin/sh
# Copyright 2009 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
COMMAND="mksysnum_plan9.sh $@"
cat <<EOF
// $COMMAND
// MACHINE GENERATED BY THE ABOVE COMMAND; DO NOT EDIT
package plan9
const(
EOF
SP='[ ]' # space or tab
sed "s/^#define${SP}\\([A-Z0-9_][A-Z0-9_]*\\)${SP}${SP}*\\([0-9][0-9]*\\)/SYS_\\1=\\2/g" \
< $1 | grep -v SYS__
cat <<EOF
)
EOF

21
vendor/golang.org/x/sys/plan9/pwd_go15_plan9.go generated vendored Normal file
View File

@@ -0,0 +1,21 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.5
package plan9
import "syscall"
func fixwd() {
syscall.Fixwd()
}
func Getwd() (wd string, err error) {
return syscall.Getwd()
}
func Chdir(path string) error {
return syscall.Chdir(path)
}

23
vendor/golang.org/x/sys/plan9/pwd_plan9.go generated vendored Normal file
View File

@@ -0,0 +1,23 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !go1.5
package plan9
func fixwd() {
}
func Getwd() (wd string, err error) {
fd, err := open(".", O_RDONLY)
if err != nil {
return "", err
}
defer Close(fd)
return Fd2path(fd)
}
func Chdir(path string) error {
return chdir(path)
}

30
vendor/golang.org/x/sys/plan9/race.go generated vendored Normal file
View File

@@ -0,0 +1,30 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build plan9 && race
package plan9
import (
"runtime"
"unsafe"
)
const raceenabled = true
func raceAcquire(addr unsafe.Pointer) {
runtime.RaceAcquire(addr)
}
func raceReleaseMerge(addr unsafe.Pointer) {
runtime.RaceReleaseMerge(addr)
}
func raceReadRange(addr unsafe.Pointer, len int) {
runtime.RaceReadRange(addr, len)
}
func raceWriteRange(addr unsafe.Pointer, len int) {
runtime.RaceWriteRange(addr, len)
}

25
vendor/golang.org/x/sys/plan9/race0.go generated vendored Normal file
View File

@@ -0,0 +1,25 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build plan9 && !race
package plan9
import (
"unsafe"
)
const raceenabled = false
func raceAcquire(addr unsafe.Pointer) {
}
func raceReleaseMerge(addr unsafe.Pointer) {
}
func raceReadRange(addr unsafe.Pointer, len int) {
}
func raceWriteRange(addr unsafe.Pointer, len int) {
}

22
vendor/golang.org/x/sys/plan9/str.go generated vendored Normal file
View File

@@ -0,0 +1,22 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build plan9
package plan9
func itoa(val int) string { // do it here rather than with fmt to avoid dependency
if val < 0 {
return "-" + itoa(-val)
}
var buf [32]byte // big enough for int64
i := len(buf) - 1
for val >= 10 {
buf[i] = byte(val%10 + '0')
i--
val /= 10
}
buf[i] = byte(val + '0')
return string(buf[i:])
}

109
vendor/golang.org/x/sys/plan9/syscall.go generated vendored Normal file
View File

@@ -0,0 +1,109 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build plan9
// Package plan9 contains an interface to the low-level operating system
// primitives. OS details vary depending on the underlying system, and
// by default, godoc will display the OS-specific documentation for the current
// system. If you want godoc to display documentation for another
// system, set $GOOS and $GOARCH to the desired system. For example, if
// you want to view documentation for freebsd/arm on linux/amd64, set $GOOS
// to freebsd and $GOARCH to arm.
//
// The primary use of this package is inside other packages that provide a more
// portable interface to the system, such as "os", "time" and "net". Use
// those packages rather than this one if you can.
//
// For details of the functions and data types in this package consult
// the manuals for the appropriate operating system.
//
// These calls return err == nil to indicate success; otherwise
// err represents an operating system error describing the failure and
// holds a value of type syscall.ErrorString.
package plan9 // import "golang.org/x/sys/plan9"
import (
"bytes"
"strings"
"unsafe"
)
// ByteSliceFromString returns a NUL-terminated slice of bytes
// containing the text of s. If s contains a NUL byte at any
// location, it returns (nil, EINVAL).
func ByteSliceFromString(s string) ([]byte, error) {
if strings.IndexByte(s, 0) != -1 {
return nil, EINVAL
}
a := make([]byte, len(s)+1)
copy(a, s)
return a, nil
}
// BytePtrFromString returns a pointer to a NUL-terminated array of
// bytes containing the text of s. If s contains a NUL byte at any
// location, it returns (nil, EINVAL).
func BytePtrFromString(s string) (*byte, error) {
a, err := ByteSliceFromString(s)
if err != nil {
return nil, err
}
return &a[0], nil
}
// ByteSliceToString returns a string form of the text represented by the slice s, with a terminating NUL and any
// bytes after the NUL removed.
func ByteSliceToString(s []byte) string {
if i := bytes.IndexByte(s, 0); i != -1 {
s = s[:i]
}
return string(s)
}
// BytePtrToString takes a pointer to a sequence of text and returns the corresponding string.
// If the pointer is nil, it returns the empty string. It assumes that the text sequence is terminated
// at a zero byte; if the zero byte is not present, the program may crash.
func BytePtrToString(p *byte) string {
if p == nil {
return ""
}
if *p == 0 {
return ""
}
// Find NUL terminator.
n := 0
for ptr := unsafe.Pointer(p); *(*byte)(ptr) != 0; n++ {
ptr = unsafe.Pointer(uintptr(ptr) + 1)
}
return string(unsafe.Slice(p, n))
}
// Single-word zero for use when we need a valid pointer to 0 bytes.
// See mksyscall.pl.
var _zero uintptr
func (ts *Timespec) Unix() (sec int64, nsec int64) {
return int64(ts.Sec), int64(ts.Nsec)
}
func (tv *Timeval) Unix() (sec int64, nsec int64) {
return int64(tv.Sec), int64(tv.Usec) * 1000
}
func (ts *Timespec) Nano() int64 {
return int64(ts.Sec)*1e9 + int64(ts.Nsec)
}
func (tv *Timeval) Nano() int64 {
return int64(tv.Sec)*1e9 + int64(tv.Usec)*1000
}
// use is a no-op, but the compiler cannot see that it is.
// Calling use(p) ensures that p is kept live until that point.
//
//go:noescape
func use(p unsafe.Pointer)

361
vendor/golang.org/x/sys/plan9/syscall_plan9.go generated vendored Normal file
View File

@@ -0,0 +1,361 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Plan 9 system calls.
// This file is compiled as ordinary Go code,
// but it is also input to mksyscall,
// which parses the //sys lines and generates system call stubs.
// Note that sometimes we use a lowercase //sys name and
// wrap it in our own nicer implementation.
package plan9
import (
"bytes"
"syscall"
"unsafe"
)
// A Note is a string describing a process note.
// It implements the os.Signal interface.
type Note string
func (n Note) Signal() {}
func (n Note) String() string {
return string(n)
}
var (
Stdin = 0
Stdout = 1
Stderr = 2
)
// For testing: clients can set this flag to force
// creation of IPv6 sockets to return EAFNOSUPPORT.
var SocketDisableIPv6 bool
func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.ErrorString)
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.ErrorString)
func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr)
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
func atoi(b []byte) (n uint) {
n = 0
for i := 0; i < len(b); i++ {
n = n*10 + uint(b[i]-'0')
}
return
}
func cstring(s []byte) string {
i := bytes.IndexByte(s, 0)
if i == -1 {
i = len(s)
}
return string(s[:i])
}
func errstr() string {
var buf [ERRMAX]byte
RawSyscall(SYS_ERRSTR, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0)
buf[len(buf)-1] = 0
return cstring(buf[:])
}
// Implemented in assembly to import from runtime.
func exit(code int)
func Exit(code int) { exit(code) }
func readnum(path string) (uint, error) {
var b [12]byte
fd, e := Open(path, O_RDONLY)
if e != nil {
return 0, e
}
defer Close(fd)
n, e := Pread(fd, b[:], 0)
if e != nil {
return 0, e
}
m := 0
for ; m < n && b[m] == ' '; m++ {
}
return atoi(b[m : n-1]), nil
}
func Getpid() (pid int) {
n, _ := readnum("#c/pid")
return int(n)
}
func Getppid() (ppid int) {
n, _ := readnum("#c/ppid")
return int(n)
}
func Read(fd int, p []byte) (n int, err error) {
return Pread(fd, p, -1)
}
func Write(fd int, p []byte) (n int, err error) {
return Pwrite(fd, p, -1)
}
var ioSync int64
//sys fd2path(fd int, buf []byte) (err error)
func Fd2path(fd int) (path string, err error) {
var buf [512]byte
e := fd2path(fd, buf[:])
if e != nil {
return "", e
}
return cstring(buf[:]), nil
}
//sys pipe(p *[2]int32) (err error)
func Pipe(p []int) (err error) {
if len(p) != 2 {
return syscall.ErrorString("bad arg in system call")
}
var pp [2]int32
err = pipe(&pp)
if err == nil {
p[0] = int(pp[0])
p[1] = int(pp[1])
}
return
}
// Underlying system call writes to newoffset via pointer.
// Implemented in assembly to avoid allocation.
func seek(placeholder uintptr, fd int, offset int64, whence int) (newoffset int64, err string)
func Seek(fd int, offset int64, whence int) (newoffset int64, err error) {
newoffset, e := seek(0, fd, offset, whence)
if newoffset == -1 {
err = syscall.ErrorString(e)
}
return
}
func Mkdir(path string, mode uint32) (err error) {
fd, err := Create(path, O_RDONLY, DMDIR|mode)
if fd != -1 {
Close(fd)
}
return
}
type Waitmsg struct {
Pid int
Time [3]uint32
Msg string
}
func (w Waitmsg) Exited() bool { return true }
func (w Waitmsg) Signaled() bool { return false }
func (w Waitmsg) ExitStatus() int {
if len(w.Msg) == 0 {
// a normal exit returns no message
return 0
}
return 1
}
//sys await(s []byte) (n int, err error)
func Await(w *Waitmsg) (err error) {
var buf [512]byte
var f [5][]byte
n, err := await(buf[:])
if err != nil || w == nil {
return
}
nf := 0
p := 0
for i := 0; i < n && nf < len(f)-1; i++ {
if buf[i] == ' ' {
f[nf] = buf[p:i]
p = i + 1
nf++
}
}
f[nf] = buf[p:]
nf++
if nf != len(f) {
return syscall.ErrorString("invalid wait message")
}
w.Pid = int(atoi(f[0]))
w.Time[0] = uint32(atoi(f[1]))
w.Time[1] = uint32(atoi(f[2]))
w.Time[2] = uint32(atoi(f[3]))
w.Msg = cstring(f[4])
if w.Msg == "''" {
// await() returns '' for no error
w.Msg = ""
}
return
}
func Unmount(name, old string) (err error) {
fixwd()
oldp, err := BytePtrFromString(old)
if err != nil {
return err
}
oldptr := uintptr(unsafe.Pointer(oldp))
var r0 uintptr
var e syscall.ErrorString
// bind(2) man page: If name is zero, everything bound or mounted upon old is unbound or unmounted.
if name == "" {
r0, _, e = Syscall(SYS_UNMOUNT, _zero, oldptr, 0)
} else {
namep, err := BytePtrFromString(name)
if err != nil {
return err
}
r0, _, e = Syscall(SYS_UNMOUNT, uintptr(unsafe.Pointer(namep)), oldptr, 0)
}
if int32(r0) == -1 {
err = e
}
return
}
func Fchdir(fd int) (err error) {
path, err := Fd2path(fd)
if err != nil {
return
}
return Chdir(path)
}
type Timespec struct {
Sec int32
Nsec int32
}
type Timeval struct {
Sec int32
Usec int32
}
func NsecToTimeval(nsec int64) (tv Timeval) {
nsec += 999 // round up to microsecond
tv.Usec = int32(nsec % 1e9 / 1e3)
tv.Sec = int32(nsec / 1e9)
return
}
func nsec() int64 {
var scratch int64
r0, _, _ := Syscall(SYS_NSEC, uintptr(unsafe.Pointer(&scratch)), 0, 0)
// TODO(aram): remove hack after I fix _nsec in the pc64 kernel.
if r0 == 0 {
return scratch
}
return int64(r0)
}
func Gettimeofday(tv *Timeval) error {
nsec := nsec()
*tv = NsecToTimeval(nsec)
return nil
}
func Getpagesize() int { return 0x1000 }
func Getegid() (egid int) { return -1 }
func Geteuid() (euid int) { return -1 }
func Getgid() (gid int) { return -1 }
func Getuid() (uid int) { return -1 }
func Getgroups() (gids []int, err error) {
return make([]int, 0), nil
}
//sys open(path string, mode int) (fd int, err error)
func Open(path string, mode int) (fd int, err error) {
fixwd()
return open(path, mode)
}
//sys create(path string, mode int, perm uint32) (fd int, err error)
func Create(path string, mode int, perm uint32) (fd int, err error) {
fixwd()
return create(path, mode, perm)
}
//sys remove(path string) (err error)
func Remove(path string) error {
fixwd()
return remove(path)
}
//sys stat(path string, edir []byte) (n int, err error)
func Stat(path string, edir []byte) (n int, err error) {
fixwd()
return stat(path, edir)
}
//sys bind(name string, old string, flag int) (err error)
func Bind(name string, old string, flag int) (err error) {
fixwd()
return bind(name, old, flag)
}
//sys mount(fd int, afd int, old string, flag int, aname string) (err error)
func Mount(fd int, afd int, old string, flag int, aname string) (err error) {
fixwd()
return mount(fd, afd, old, flag, aname)
}
//sys wstat(path string, edir []byte) (err error)
func Wstat(path string, edir []byte) (err error) {
fixwd()
return wstat(path, edir)
}
//sys chdir(path string) (err error)
//sys Dup(oldfd int, newfd int) (fd int, err error)
//sys Pread(fd int, p []byte, offset int64) (n int, err error)
//sys Pwrite(fd int, p []byte, offset int64) (n int, err error)
//sys Close(fd int) (err error)
//sys Fstat(fd int, edir []byte) (n int, err error)
//sys Fwstat(fd int, edir []byte) (err error)

284
vendor/golang.org/x/sys/plan9/zsyscall_plan9_386.go generated vendored Normal file
View File

@@ -0,0 +1,284 @@
// go run mksyscall.go -l32 -plan9 -tags plan9,386 syscall_plan9.go
// Code generated by the command above; see README.md. DO NOT EDIT.
//go:build plan9 && 386
package plan9
import "unsafe"
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func fd2path(fd int, buf []byte) (err error) {
var _p0 unsafe.Pointer
if len(buf) > 0 {
_p0 = unsafe.Pointer(&buf[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_FD2PATH, uintptr(fd), uintptr(_p0), uintptr(len(buf)))
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func pipe(p *[2]int32) (err error) {
r0, _, e1 := Syscall(SYS_PIPE, uintptr(unsafe.Pointer(p)), 0, 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func await(s []byte) (n int, err error) {
var _p0 unsafe.Pointer
if len(s) > 0 {
_p0 = unsafe.Pointer(&s[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_AWAIT, uintptr(_p0), uintptr(len(s)), 0)
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func open(path string, mode int) (fd int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_OPEN, uintptr(unsafe.Pointer(_p0)), uintptr(mode), 0)
fd = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func create(path string, mode int, perm uint32) (fd int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_CREATE, uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(perm))
fd = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func remove(path string) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_REMOVE, uintptr(unsafe.Pointer(_p0)), 0, 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func stat(path string, edir []byte) (n int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
var _p1 unsafe.Pointer
if len(edir) > 0 {
_p1 = unsafe.Pointer(&edir[0])
} else {
_p1 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_STAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir)))
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func bind(name string, old string, flag int) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(name)
if err != nil {
return
}
var _p1 *byte
_p1, err = BytePtrFromString(old)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_BIND, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(flag))
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func mount(fd int, afd int, old string, flag int, aname string) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(old)
if err != nil {
return
}
var _p1 *byte
_p1, err = BytePtrFromString(aname)
if err != nil {
return
}
r0, _, e1 := Syscall6(SYS_MOUNT, uintptr(fd), uintptr(afd), uintptr(unsafe.Pointer(_p0)), uintptr(flag), uintptr(unsafe.Pointer(_p1)), 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func wstat(path string, edir []byte) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
var _p1 unsafe.Pointer
if len(edir) > 0 {
_p1 = unsafe.Pointer(&edir[0])
} else {
_p1 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_WSTAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir)))
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func chdir(path string) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_CHDIR, uintptr(unsafe.Pointer(_p0)), 0, 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Dup(oldfd int, newfd int) (fd int, err error) {
r0, _, e1 := Syscall(SYS_DUP, uintptr(oldfd), uintptr(newfd), 0)
fd = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Pread(fd int, p []byte, offset int64) (n int, err error) {
var _p0 unsafe.Pointer
if len(p) > 0 {
_p0 = unsafe.Pointer(&p[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_PREAD, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0)
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Pwrite(fd int, p []byte, offset int64) (n int, err error) {
var _p0 unsafe.Pointer
if len(p) > 0 {
_p0 = unsafe.Pointer(&p[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_PWRITE, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0)
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Close(fd int) (err error) {
r0, _, e1 := Syscall(SYS_CLOSE, uintptr(fd), 0, 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fstat(fd int, edir []byte) (n int, err error) {
var _p0 unsafe.Pointer
if len(edir) > 0 {
_p0 = unsafe.Pointer(&edir[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_FSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir)))
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fwstat(fd int, edir []byte) (err error) {
var _p0 unsafe.Pointer
if len(edir) > 0 {
_p0 = unsafe.Pointer(&edir[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_FWSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir)))
if int32(r0) == -1 {
err = e1
}
return
}

284
vendor/golang.org/x/sys/plan9/zsyscall_plan9_amd64.go generated vendored Normal file
View File

@@ -0,0 +1,284 @@
// go run mksyscall.go -l32 -plan9 -tags plan9,amd64 syscall_plan9.go
// Code generated by the command above; see README.md. DO NOT EDIT.
//go:build plan9 && amd64
package plan9
import "unsafe"
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func fd2path(fd int, buf []byte) (err error) {
var _p0 unsafe.Pointer
if len(buf) > 0 {
_p0 = unsafe.Pointer(&buf[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_FD2PATH, uintptr(fd), uintptr(_p0), uintptr(len(buf)))
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func pipe(p *[2]int32) (err error) {
r0, _, e1 := Syscall(SYS_PIPE, uintptr(unsafe.Pointer(p)), 0, 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func await(s []byte) (n int, err error) {
var _p0 unsafe.Pointer
if len(s) > 0 {
_p0 = unsafe.Pointer(&s[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_AWAIT, uintptr(_p0), uintptr(len(s)), 0)
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func open(path string, mode int) (fd int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_OPEN, uintptr(unsafe.Pointer(_p0)), uintptr(mode), 0)
fd = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func create(path string, mode int, perm uint32) (fd int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_CREATE, uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(perm))
fd = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func remove(path string) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_REMOVE, uintptr(unsafe.Pointer(_p0)), 0, 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func stat(path string, edir []byte) (n int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
var _p1 unsafe.Pointer
if len(edir) > 0 {
_p1 = unsafe.Pointer(&edir[0])
} else {
_p1 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_STAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir)))
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func bind(name string, old string, flag int) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(name)
if err != nil {
return
}
var _p1 *byte
_p1, err = BytePtrFromString(old)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_BIND, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(flag))
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func mount(fd int, afd int, old string, flag int, aname string) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(old)
if err != nil {
return
}
var _p1 *byte
_p1, err = BytePtrFromString(aname)
if err != nil {
return
}
r0, _, e1 := Syscall6(SYS_MOUNT, uintptr(fd), uintptr(afd), uintptr(unsafe.Pointer(_p0)), uintptr(flag), uintptr(unsafe.Pointer(_p1)), 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func wstat(path string, edir []byte) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
var _p1 unsafe.Pointer
if len(edir) > 0 {
_p1 = unsafe.Pointer(&edir[0])
} else {
_p1 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_WSTAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir)))
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func chdir(path string) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_CHDIR, uintptr(unsafe.Pointer(_p0)), 0, 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Dup(oldfd int, newfd int) (fd int, err error) {
r0, _, e1 := Syscall(SYS_DUP, uintptr(oldfd), uintptr(newfd), 0)
fd = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Pread(fd int, p []byte, offset int64) (n int, err error) {
var _p0 unsafe.Pointer
if len(p) > 0 {
_p0 = unsafe.Pointer(&p[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_PREAD, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0)
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Pwrite(fd int, p []byte, offset int64) (n int, err error) {
var _p0 unsafe.Pointer
if len(p) > 0 {
_p0 = unsafe.Pointer(&p[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_PWRITE, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0)
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Close(fd int) (err error) {
r0, _, e1 := Syscall(SYS_CLOSE, uintptr(fd), 0, 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fstat(fd int, edir []byte) (n int, err error) {
var _p0 unsafe.Pointer
if len(edir) > 0 {
_p0 = unsafe.Pointer(&edir[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_FSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir)))
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fwstat(fd int, edir []byte) (err error) {
var _p0 unsafe.Pointer
if len(edir) > 0 {
_p0 = unsafe.Pointer(&edir[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_FWSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir)))
if int32(r0) == -1 {
err = e1
}
return
}

284
vendor/golang.org/x/sys/plan9/zsyscall_plan9_arm.go generated vendored Normal file
View File

@@ -0,0 +1,284 @@
// go run mksyscall.go -l32 -plan9 -tags plan9,arm syscall_plan9.go
// Code generated by the command above; see README.md. DO NOT EDIT.
//go:build plan9 && arm
package plan9
import "unsafe"
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func fd2path(fd int, buf []byte) (err error) {
var _p0 unsafe.Pointer
if len(buf) > 0 {
_p0 = unsafe.Pointer(&buf[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_FD2PATH, uintptr(fd), uintptr(_p0), uintptr(len(buf)))
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func pipe(p *[2]int32) (err error) {
r0, _, e1 := Syscall(SYS_PIPE, uintptr(unsafe.Pointer(p)), 0, 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func await(s []byte) (n int, err error) {
var _p0 unsafe.Pointer
if len(s) > 0 {
_p0 = unsafe.Pointer(&s[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_AWAIT, uintptr(_p0), uintptr(len(s)), 0)
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func open(path string, mode int) (fd int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_OPEN, uintptr(unsafe.Pointer(_p0)), uintptr(mode), 0)
fd = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func create(path string, mode int, perm uint32) (fd int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_CREATE, uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(perm))
fd = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func remove(path string) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_REMOVE, uintptr(unsafe.Pointer(_p0)), 0, 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func stat(path string, edir []byte) (n int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
var _p1 unsafe.Pointer
if len(edir) > 0 {
_p1 = unsafe.Pointer(&edir[0])
} else {
_p1 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_STAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir)))
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func bind(name string, old string, flag int) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(name)
if err != nil {
return
}
var _p1 *byte
_p1, err = BytePtrFromString(old)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_BIND, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(flag))
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func mount(fd int, afd int, old string, flag int, aname string) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(old)
if err != nil {
return
}
var _p1 *byte
_p1, err = BytePtrFromString(aname)
if err != nil {
return
}
r0, _, e1 := Syscall6(SYS_MOUNT, uintptr(fd), uintptr(afd), uintptr(unsafe.Pointer(_p0)), uintptr(flag), uintptr(unsafe.Pointer(_p1)), 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func wstat(path string, edir []byte) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
var _p1 unsafe.Pointer
if len(edir) > 0 {
_p1 = unsafe.Pointer(&edir[0])
} else {
_p1 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_WSTAT, uintptr(unsafe.Pointer(_p0)), uintptr(_p1), uintptr(len(edir)))
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func chdir(path string) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
r0, _, e1 := Syscall(SYS_CHDIR, uintptr(unsafe.Pointer(_p0)), 0, 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Dup(oldfd int, newfd int) (fd int, err error) {
r0, _, e1 := Syscall(SYS_DUP, uintptr(oldfd), uintptr(newfd), 0)
fd = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Pread(fd int, p []byte, offset int64) (n int, err error) {
var _p0 unsafe.Pointer
if len(p) > 0 {
_p0 = unsafe.Pointer(&p[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_PREAD, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0)
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Pwrite(fd int, p []byte, offset int64) (n int, err error) {
var _p0 unsafe.Pointer
if len(p) > 0 {
_p0 = unsafe.Pointer(&p[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_PWRITE, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(offset), uintptr(offset>>32), 0)
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Close(fd int) (err error) {
r0, _, e1 := Syscall(SYS_CLOSE, uintptr(fd), 0, 0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fstat(fd int, edir []byte) (n int, err error) {
var _p0 unsafe.Pointer
if len(edir) > 0 {
_p0 = unsafe.Pointer(&edir[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_FSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir)))
n = int(r0)
if int32(r0) == -1 {
err = e1
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fwstat(fd int, edir []byte) (err error) {
var _p0 unsafe.Pointer
if len(edir) > 0 {
_p0 = unsafe.Pointer(&edir[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_FWSTAT, uintptr(fd), uintptr(_p0), uintptr(len(edir)))
if int32(r0) == -1 {
err = e1
}
return
}

49
vendor/golang.org/x/sys/plan9/zsysnum_plan9.go generated vendored Normal file
View File

@@ -0,0 +1,49 @@
// mksysnum_plan9.sh /opt/plan9/sys/src/libc/9syscall/sys.h
// MACHINE GENERATED BY THE ABOVE COMMAND; DO NOT EDIT
package plan9
const (
SYS_SYSR1 = 0
SYS_BIND = 2
SYS_CHDIR = 3
SYS_CLOSE = 4
SYS_DUP = 5
SYS_ALARM = 6
SYS_EXEC = 7
SYS_EXITS = 8
SYS_FAUTH = 10
SYS_SEGBRK = 12
SYS_OPEN = 14
SYS_OSEEK = 16
SYS_SLEEP = 17
SYS_RFORK = 19
SYS_PIPE = 21
SYS_CREATE = 22
SYS_FD2PATH = 23
SYS_BRK_ = 24
SYS_REMOVE = 25
SYS_NOTIFY = 28
SYS_NOTED = 29
SYS_SEGATTACH = 30
SYS_SEGDETACH = 31
SYS_SEGFREE = 32
SYS_SEGFLUSH = 33
SYS_RENDEZVOUS = 34
SYS_UNMOUNT = 35
SYS_SEMACQUIRE = 37
SYS_SEMRELEASE = 38
SYS_SEEK = 39
SYS_FVERSION = 40
SYS_ERRSTR = 41
SYS_STAT = 42
SYS_FSTAT = 43
SYS_WSTAT = 44
SYS_FWSTAT = 45
SYS_MOUNT = 46
SYS_AWAIT = 47
SYS_PREAD = 50
SYS_PWRITE = 51
SYS_TSEMACQUIRE = 52
SYS_NSEC = 53
)

2
vendor/golang.org/x/sys/unix/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
_obj/
unix.test

184
vendor/golang.org/x/sys/unix/README.md generated vendored Normal file
View File

@@ -0,0 +1,184 @@
# Building `sys/unix`
The sys/unix package provides access to the raw system call interface of the
underlying operating system. See: https://godoc.org/golang.org/x/sys/unix
Porting Go to a new architecture/OS combination or adding syscalls, types, or
constants to an existing architecture/OS pair requires some manual effort;
however, there are tools that automate much of the process.
## Build Systems
There are currently two ways we generate the necessary files. We are currently
migrating the build system to use containers so the builds are reproducible.
This is being done on an OS-by-OS basis. Please update this documentation as
components of the build system change.
### Old Build System (currently for `GOOS != "linux"`)
The old build system generates the Go files based on the C header files
present on your system. This means that files
for a given GOOS/GOARCH pair must be generated on a system with that OS and
architecture. This also means that the generated code can differ from system
to system, based on differences in the header files.
To avoid this, if you are using the old build system, only generate the Go
files on an installation with unmodified header files. It is also important to
keep track of which version of the OS the files were generated from (ex.
Darwin 14 vs Darwin 15). This makes it easier to track the progress of changes
and have each OS upgrade correspond to a single change.
To build the files for your current OS and architecture, make sure GOOS and
GOARCH are set correctly and run `mkall.sh`. This will generate the files for
your specific system. Running `mkall.sh -n` shows the commands that will be run.
Requirements: bash, go
### New Build System (currently for `GOOS == "linux"`)
The new build system uses a Docker container to generate the go files directly
from source checkouts of the kernel and various system libraries. This means
that on any platform that supports Docker, all the files using the new build
system can be generated at once, and generated files will not change based on
what the person running the scripts has installed on their computer.
The OS specific files for the new build system are located in the `${GOOS}`
directory, and the build is coordinated by the `${GOOS}/mkall.go` program. When
the kernel or system library updates, modify the Dockerfile at
`${GOOS}/Dockerfile` to checkout the new release of the source.
To build all the files under the new build system, you must be on an amd64/Linux
system and have your GOOS and GOARCH set accordingly. Running `mkall.sh` will
then generate all of the files for all of the GOOS/GOARCH pairs in the new build
system. Running `mkall.sh -n` shows the commands that will be run.
Requirements: bash, go, docker
## Component files
This section describes the various files used in the code generation process.
It also contains instructions on how to modify these files to add a new
architecture/OS or to add additional syscalls, types, or constants. Note that
if you are using the new build system, the scripts/programs cannot be called normally.
They must be called from within the docker container.
### asm files
The hand-written assembly file at `asm_${GOOS}_${GOARCH}.s` implements system
call dispatch. There are three entry points:
```
func Syscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr)
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr)
```
The first and second are the standard ones; they differ only in how many
arguments can be passed to the kernel. The third is for low-level use by the
ForkExec wrapper. Unlike the first two, it does not call into the scheduler to
let it know that a system call is running.
When porting Go to a new architecture/OS, this file must be implemented for
each GOOS/GOARCH pair.
### mksysnum
Mksysnum is a Go program located at `${GOOS}/mksysnum.go` (or `mksysnum_${GOOS}.go`
for the old system). This program takes in a list of header files containing the
syscall number declarations and parses them to produce the corresponding list of
Go numeric constants. See `zsysnum_${GOOS}_${GOARCH}.go` for the generated
constants.
Adding new syscall numbers is mostly done by running the build on a sufficiently
new installation of the target OS (or updating the source checkouts for the
new build system). However, depending on the OS, you may need to update the
parsing in mksysnum.
### mksyscall.go
The `syscall.go`, `syscall_${GOOS}.go`, `syscall_${GOOS}_${GOARCH}.go` are
hand-written Go files which implement system calls (for unix, the specific OS,
or the specific OS/Architecture pair respectively) that need special handling
and list `//sys` comments giving prototypes for ones that can be generated.
The mksyscall.go program takes the `//sys` and `//sysnb` comments and converts
them into syscalls. This requires the name of the prototype in the comment to
match a syscall number in the `zsysnum_${GOOS}_${GOARCH}.go` file. The function
prototype can be exported (capitalized) or not.
Adding a new syscall often just requires adding a new `//sys` function prototype
with the desired arguments and a capitalized name so it is exported. However, if
you want the interface to the syscall to be different, often one will make an
unexported `//sys` prototype, and then write a custom wrapper in
`syscall_${GOOS}.go`.
### types files
For each OS, there is a hand-written Go file at `${GOOS}/types.go` (or
`types_${GOOS}.go` on the old system). This file includes standard C headers and
creates Go type aliases to the corresponding C types. The file is then fed
through godef to get the Go compatible definitions. Finally, the generated code
is fed though mkpost.go to format the code correctly and remove any hidden or
private identifiers. This cleaned-up code is written to
`ztypes_${GOOS}_${GOARCH}.go`.
The hardest part about preparing this file is figuring out which headers to
include and which symbols need to be `#define`d to get the actual data
structures that pass through to the kernel system calls. Some C libraries
preset alternate versions for binary compatibility and translate them on the
way in and out of system calls, but there is almost always a `#define` that can
get the real ones.
See `types_darwin.go` and `linux/types.go` for examples.
To add a new type, add in the necessary include statement at the top of the
file (if it is not already there) and add in a type alias line. Note that if
your type is significantly different on different architectures, you may need
some `#if/#elif` macros in your include statements.
### mkerrors.sh
This script is used to generate the system's various constants. This doesn't
just include the error numbers and error strings, but also the signal numbers
and a wide variety of miscellaneous constants. The constants come from the list
of include files in the `includes_${uname}` variable. A regex then picks out
the desired `#define` statements, and generates the corresponding Go constants.
The error numbers and strings are generated from `#include <errno.h>`, and the
signal numbers and strings are generated from `#include <signal.h>`. All of
these constants are written to `zerrors_${GOOS}_${GOARCH}.go` via a C program,
`_errors.c`, which prints out all the constants.
To add a constant, add the header that includes it to the appropriate variable.
Then, edit the regex (if necessary) to match the desired constant. Avoid making
the regex too broad to avoid matching unintended constants.
### internal/mkmerge
This program is used to extract duplicate const, func, and type declarations
from the generated architecture-specific files listed below, and merge these
into a common file for each OS.
The merge is performed in the following steps:
1. Construct the set of common code that is identical in all architecture-specific files.
2. Write this common code to the merged file.
3. Remove the common code from all architecture-specific files.
## Generated files
### `zerrors_${GOOS}_${GOARCH}.go`
A file containing all of the system's generated error numbers, error strings,
signal numbers, and constants. Generated by `mkerrors.sh` (see above).
### `zsyscall_${GOOS}_${GOARCH}.go`
A file containing all the generated syscalls for a specific GOOS and GOARCH.
Generated by `mksyscall.go` (see above).
### `zsysnum_${GOOS}_${GOARCH}.go`
A list of numeric constants for all the syscall number of the specific GOOS
and GOARCH. Generated by mksysnum (see above).
### `ztypes_${GOOS}_${GOARCH}.go`
A file containing Go types for passing into (or returning from) syscalls.
Generated by godefs and the types file (see above).

86
vendor/golang.org/x/sys/unix/affinity_linux.go generated vendored Normal file
View File

@@ -0,0 +1,86 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// CPU affinity functions
package unix
import (
"math/bits"
"unsafe"
)
const cpuSetSize = _CPU_SETSIZE / _NCPUBITS
// CPUSet represents a CPU affinity mask.
type CPUSet [cpuSetSize]cpuMask
func schedAffinity(trap uintptr, pid int, set *CPUSet) error {
_, _, e := RawSyscall(trap, uintptr(pid), uintptr(unsafe.Sizeof(*set)), uintptr(unsafe.Pointer(set)))
if e != 0 {
return errnoErr(e)
}
return nil
}
// SchedGetaffinity gets the CPU affinity mask of the thread specified by pid.
// If pid is 0 the calling thread is used.
func SchedGetaffinity(pid int, set *CPUSet) error {
return schedAffinity(SYS_SCHED_GETAFFINITY, pid, set)
}
// SchedSetaffinity sets the CPU affinity mask of the thread specified by pid.
// If pid is 0 the calling thread is used.
func SchedSetaffinity(pid int, set *CPUSet) error {
return schedAffinity(SYS_SCHED_SETAFFINITY, pid, set)
}
// Zero clears the set s, so that it contains no CPUs.
func (s *CPUSet) Zero() {
for i := range s {
s[i] = 0
}
}
func cpuBitsIndex(cpu int) int {
return cpu / _NCPUBITS
}
func cpuBitsMask(cpu int) cpuMask {
return cpuMask(1 << (uint(cpu) % _NCPUBITS))
}
// Set adds cpu to the set s.
func (s *CPUSet) Set(cpu int) {
i := cpuBitsIndex(cpu)
if i < len(s) {
s[i] |= cpuBitsMask(cpu)
}
}
// Clear removes cpu from the set s.
func (s *CPUSet) Clear(cpu int) {
i := cpuBitsIndex(cpu)
if i < len(s) {
s[i] &^= cpuBitsMask(cpu)
}
}
// IsSet reports whether cpu is in the set s.
func (s *CPUSet) IsSet(cpu int) bool {
i := cpuBitsIndex(cpu)
if i < len(s) {
return s[i]&cpuBitsMask(cpu) != 0
}
return false
}
// Count returns the number of CPUs in the set s.
func (s *CPUSet) Count() int {
c := 0
for _, b := range s {
c += bits.OnesCount64(uint64(b))
}
return c
}

13
vendor/golang.org/x/sys/unix/aliases.go generated vendored Normal file
View File

@@ -0,0 +1,13 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
package unix
import "syscall"
type Signal = syscall.Signal
type Errno = syscall.Errno
type SysProcAttr = syscall.SysProcAttr

17
vendor/golang.org/x/sys/unix/asm_aix_ppc64.s generated vendored Normal file
View File

@@ -0,0 +1,17 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build gc
#include "textflag.h"
//
// System calls for ppc64, AIX are implemented in runtime/syscall_aix.go
//
TEXT ·syscall6(SB),NOSPLIT,$0-88
JMP syscall·syscall6(SB)
TEXT ·rawSyscall6(SB),NOSPLIT,$0-88
JMP syscall·rawSyscall6(SB)

27
vendor/golang.org/x/sys/unix/asm_bsd_386.s generated vendored Normal file
View File

@@ -0,0 +1,27 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build (freebsd || netbsd || openbsd) && gc
#include "textflag.h"
// System call support for 386 BSD
// Just jump to package syscall's implementation for all these functions.
// The runtime may know about them.
TEXT ·Syscall(SB),NOSPLIT,$0-28
JMP syscall·Syscall(SB)
TEXT ·Syscall6(SB),NOSPLIT,$0-40
JMP syscall·Syscall6(SB)
TEXT ·Syscall9(SB),NOSPLIT,$0-52
JMP syscall·Syscall9(SB)
TEXT ·RawSyscall(SB),NOSPLIT,$0-28
JMP syscall·RawSyscall(SB)
TEXT ·RawSyscall6(SB),NOSPLIT,$0-40
JMP syscall·RawSyscall6(SB)

Some files were not shown because too many files have changed in this diff Show More