wip - changes from last time I worked on this

This commit is contained in:
Tony Grosinger 2022-12-12 08:26:10 -08:00
parent 25bfb521a6
commit f48f3a98ce
9 changed files with 262 additions and 30 deletions

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -23,6 +24,8 @@ var rootCmd = &cobra.Command{
var debugLogging bool var debugLogging bool
var debugLoggingFile *os.File var debugLoggingFile *os.File
var ledgerFilePath string
func configureLogging(_ *cobra.Command, _ []string) { func configureLogging(_ *cobra.Command, _ []string) {
if debugLogging { if debugLogging {
var err error var err error
@ -43,9 +46,40 @@ func loggingCleanup(_ *cobra.Command, _ []string) {
} }
} }
func verifyLedgerFile() {
f, err := rootCmd.Flags().GetString("file")
if err != nil {
fmt.Fprintln(os.Stderr, "Error loading value of the file argument: "+err.Error())
os.Exit(1)
}
if f == "" {
var ok bool
f, ok = os.LookupEnv("LEDGER_FILE")
if !ok {
fmt.Fprintln(os.Stderr,
"You must specify the -f option or set the LEDGER_FILE environment variable")
fmt.Fprintln(os.Stderr, "Try 'ledger-tui --help' for more information.")
os.Exit(1)
}
rootCmd.Flags().Set("file", f)
}
if _, err := os.Stat(f); errors.Is(err, os.ErrNotExist) {
fmt.Fprintln(os.Stderr, "Ledger file specified does not exist.")
os.Exit(1)
}
}
func main() { func main() {
rootCmd.PersistentFlags().BoolVar(&debugLogging, "debug", false, rootCmd.PersistentFlags().BoolVar(&debugLogging, "debug", false,
"Output debug logs to debug.log file") "Output debug logs to debug.log file")
rootCmd.PersistentFlags().StringVarP(&ledgerFilePath, "file", "f", "",
"Path to the ledger file that should be loaded. If empty, the value "+
"from the LEDGER_FILE environment variable is used.")
cobra.OnInitialize(verifyLedgerFile)
rootCmd.AddCommand(add.AddCmd) rootCmd.AddCommand(add.AddCmd)
rootCmd.AddCommand(license.LicenseCmd) rootCmd.AddCommand(license.LicenseCmd)

View File

@ -0,0 +1,9 @@
package collections
func Map[T any, M any](a []T, f func(T) M) []M {
n := make([]M, len(a))
for i, e := range a {
n[i] = f(e)
}
return n
}

View File

@ -1,8 +1,12 @@
package hledger package hledger
import ( import (
"bytes"
"encoding/json"
"errors" "errors"
"os/exec"
"github.com/tgrosinger/ledger-tui/pkg/transaction"
tx "github.com/tgrosinger/ledger-tui/pkg/transaction" tx "github.com/tgrosinger/ledger-tui/pkg/transaction"
) )
@ -12,8 +16,30 @@ type HLedger struct {
filepath string filepath string
} }
func New(filepath string) HLedger {
return HLedger{
filepath: filepath,
}
}
// GetTransactions returns a slice of transactions loaded from the configured
// ledger file.
func (hl HLedger) GetTransactions() ([]tx.Transaction, error) { func (hl HLedger) GetTransactions() ([]tx.Transaction, error) {
return nil, errors.New("Method not implemented") out, err := hl.exec("print", "-O", "json")
if err != nil {
return nil, err
}
var results []Transaction
err = json.Unmarshal([]byte(out), &results)
if err != nil {
return nil, err
}
converted, errors := hl.convertTransactions(results)
return converted, transaction.TransactionParseErrors{
Errors: errors,
}
} }
func (hl HLedger) AddTransaction(newTx tx.Transaction) error { func (hl HLedger) AddTransaction(newTx tx.Transaction) error {
@ -21,15 +47,59 @@ func (hl HLedger) AddTransaction(newTx tx.Transaction) error {
} }
// exec runs an hledger command against the configured ledger file.
func (hl HLedger) exec(args ...string) (string, error) {
allArgs := make([]string, 0, len(args)+2)
allArgs = append(allArgs, "-f", hl.filepath)
allArgs = append(allArgs, args...)
cmd := exec.Command("hledger", allArgs...)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return "", err
}
return out.String(), nil
}
// convertTransaction converts the transactions as loaded from hledger into a
// friendlier and simplified format used by this application.
func (hl HLedger) convertTransactions(txs []Transaction) ([]transaction.Transaction, []transaction.TransactionParseError) {
output := make([]transaction.Transaction, 0, len(txs))
errors := make([]transaction.TransactionParseError, 0)
for _, tx := range txs {
converted, err := tx.convert()
if err != nil {
position := -1
if len(tx.Tsourcepos) > 0 {
position = tx.Tsourcepos[0].SourceLine
}
errors = append(errors, transaction.TransactionParseError{
Err: err,
Date: tx.Tdate,
Description: tx.Tdescription,
Line: position,
})
} else {
output = append(output, converted)
}
}
return output, errors
}
// Transaction is the data format for transactions as they come from hledger // Transaction is the data format for transactions as they come from hledger
// when executing `hledger print -O json`. // when executing `hledger print -O json`.
type Transaction struct { type Transaction struct {
Tcode string `json:"tcode"` Tcode string `json:"tcode"`
Tcomment string `json:"tcomment"` Tcomment string `json:"tcomment"`
Tdate string `json:"tdate"` Tdate string `json:"tdate"`
Tdate2 interface{} `json:"tdate2"` Tdate2 string `json:"tdate2"`
Tdescription string `json:"tdescription"` Tdescription string `json:"tdescription"`
Tindex int `json:"tindex"` Tindex int `json:"tindex"`
Tpostings []struct { Tpostings []struct {
Paccount string `json:"paccount"` Paccount string `json:"paccount"`
Pamount []struct { Pamount []struct {
@ -48,15 +118,15 @@ type Transaction struct {
Asprecision int `json:"asprecision"` Asprecision int `json:"asprecision"`
} `json:"astyle"` } `json:"astyle"`
} `json:"pamount"` } `json:"pamount"`
Pbalanceassertion interface{} `json:"pbalanceassertion"` Pbalanceassertion interface{} `json:"pbalanceassertion"`
Pcomment string `json:"pcomment"` Pcomment string `json:"pcomment"`
Pdate interface{} `json:"pdate"` Pdate interface{} `json:"pdate"`
Pdate2 interface{} `json:"pdate2"` Pdate2 interface{} `json:"pdate2"`
Poriginal interface{} `json:"poriginal"` Poriginal interface{} `json:"poriginal"`
Pstatus string `json:"pstatus"` Pstatus string `json:"pstatus"`
Ptags []interface{} `json:"ptags"` Ptags []string `json:"ptags"`
Ptransaction string `json:"ptransaction_"` Ptransaction string `json:"ptransaction_"`
Ptype string `json:"ptype"` Ptype string `json:"ptype"`
} `json:"tpostings"` } `json:"tpostings"`
Tprecedingcomment string `json:"tprecedingcomment"` Tprecedingcomment string `json:"tprecedingcomment"`
Tsourcepos []struct { Tsourcepos []struct {
@ -64,6 +134,25 @@ type Transaction struct {
SourceLine int `json:"sourceLine"` SourceLine int `json:"sourceLine"`
SourceName string `json:"sourceName"` SourceName string `json:"sourceName"`
} `json:"tsourcepos"` } `json:"tsourcepos"`
Tstatus string `json:"tstatus"` Tstatus string `json:"tstatus"`
Ttags []interface{} `json:"ttags"` Ttags []string `json:"ttags"`
}
func (t *Transaction) convert() (transaction.Transaction, error) {
tx := transaction.Transaction{
Index: t.Tindex,
Date: t.Tdate,
Description: t.Tdescription,
Comment: t.Tcomment,
PreceedingComment: t.Tprecedingcomment,
Postings: make([]transaction.Posting, len(t.Tpostings)),
Status: t.Tstatus,
Tags: t.Ttags,
}
for _, posting := range t.Tpostings {
tx.Postings = append(tx.Postings, posting.convert())
}
return tx, nil
} }

View File

@ -1,6 +1,11 @@
package transaction package transaction
import "time" import (
"strings"
"time"
"github.com/tgrosinger/ledger-tui/pkg/collections"
)
// Transaction contains the information stored in a ledger file about a single // Transaction contains the information stored in a ledger file about a single
// transaction. // transaction.
@ -24,6 +29,29 @@ type Posting struct {
Tags []string Tags []string
} }
func (t Transaction) string() string { func (t Transaction) String() string {
return "TODO" return "TODO"
} }
type TransactionParseError struct {
Err error
Date string
Description string
Line int
}
func (e TransactionParseError) Error() string {
return e.Err.Error()
}
type TransactionParseErrors struct {
Errors []TransactionParseError
}
func (e TransactionParseErrors) Error() string {
return strings.Join(collections.Map(
e.Errors,
func(t TransactionParseError) string {
return t.Error()
}), "\n")
}

View File

@ -29,13 +29,19 @@ var AddCmd = &cobra.Command{
} }
func executeAddTUI(cmd *cobra.Command, args []string) { func executeAddTUI(cmd *cobra.Command, args []string) {
f, err := cmd.Flags().GetString("file")
if err != nil {
log.Fatalf("Missing ledger file path: %s", err.Error())
}
log.Println("Using ledger file in " + f)
addTUI := AddTxTUI{ addTUI := AddTxTUI{
focused: date, focused: date,
furthestFocus: date, furthestFocus: date,
date: dateinput.New(time.Now(), true), date: dateinput.New(time.Now(), true),
description: textinput.New(), description: textinput.New(),
total: currencyinput.New("$"), total: currencyinput.New("$"),
suggestionBox: suggestions.New(), suggestionBox: suggestions.New(f),
help: help.New(), help: help.New(),
keymap: keymap{ keymap: keymap{
nextField: key.NewBinding( nextField: key.NewBinding(
@ -91,9 +97,11 @@ type AddTxTUI struct {
// Add New Tx Form // Add New Tx Form
focused focus focused focus
furthestFocus focus furthestFocus focus
date dateinput.Model
description textinput.Model // Fields
total currencyinput.Model date dateinput.Model
description textinput.Model
total currencyinput.Model
suggestionBox suggestions.Model suggestionBox suggestions.Model
@ -106,7 +114,11 @@ type AddTxTUI struct {
} }
func (m AddTxTUI) Init() tea.Cmd { func (m AddTxTUI) Init() tea.Cmd {
return textinput.Blink return tea.Batch(
m.date.Init(),
m.total.Init(),
m.suggestionBox.Init(),
)
} }
func (m AddTxTUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m AddTxTUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@ -171,6 +183,8 @@ func (m AddTxTUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
m.total, cmd = m.total.Update(msg) m.total, cmd = m.total.Update(msg)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
m.suggestionBox, cmd = m.suggestionBox.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
} }

View File

@ -46,6 +46,7 @@ func numberValidator(s string) error {
} }
func (m Model) Init() tea.Cmd { func (m Model) Init() tea.Cmd {
// Sub-components do not have Init functions to call.
return textinput.Blink return textinput.Blink
} }

View File

@ -120,6 +120,7 @@ func dayValidator(s string) error {
} }
func (m Model) Init() tea.Cmd { func (m Model) Init() tea.Cmd {
// Sub-components do not have Init functions to call.
return textinput.Blink return textinput.Blink
} }

View File

@ -6,3 +6,11 @@ const (
NextInputMsg TUIMsg = iota NextInputMsg TUIMsg = iota
PrevInputMsg PrevInputMsg
) )
type ErrMsg struct {
Err error
}
func (e ErrMsg) Error() string {
return e.Err.Error()
}

View File

@ -1,19 +1,67 @@
package suggestions package suggestions
import tea "github.com/charmbracelet/bubbletea" import (
"fmt"
"log"
"os"
tea "github.com/charmbracelet/bubbletea"
"github.com/tgrosinger/ledger-tui/pkg/ledger/hledger"
"github.com/tgrosinger/ledger-tui/pkg/transaction"
"github.com/tgrosinger/ledger-tui/pkg/tui"
)
// TransactionsMsg indicates that transactions were successfully loaded and are
// now available for use by the suggestions engine.
type TransactionsMsg []transaction.Transaction
type SuggestionErrMsg tui.ErrMsg
type Model struct { type Model struct {
ledgerFilename string
transactions []transaction.Transaction
} }
func New() Model { func New(ledgerFilename string) Model {
return Model{} return Model{
ledgerFilename: ledgerFilename,
}
} }
func Init() tea.Cmd { // loadTransactions returns a bubbletea command which can be executed
return nil // asynchronously to load transactions from a specified ledger file.
func loadTransactions(filepath string) func() tea.Msg {
return func() tea.Msg {
log.Println("Loading ledger file at " + filepath)
ledger := hledger.New(filepath)
txs, err := ledger.GetTransactions()
if err != nil {
return SuggestionErrMsg{Err: err}
}
return TransactionsMsg(txs)
}
}
func (m Model) Init() tea.Cmd {
return loadTransactions(m.ledgerFilename)
} }
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case TransactionsMsg:
log.Println("Transactions loaded")
m.transactions = msg
case SuggestionErrMsg:
// TODO: This error is not visible to the user anywhere
fmt.Fprintln(os.Stderr, "Encountered an error: "+msg.Err.Error())
log.Println("Encountered an error: " + msg.Err.Error())
return m, tea.Quit
}
return m, nil return m, nil
} }