wip - changes from last time I worked on this
This commit is contained in:
parent
25bfb521a6
commit
f48f3a98ce
@ -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)
|
||||||
|
9
pkg/collections/collections.go
Normal file
9
pkg/collections/collections.go
Normal 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
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
|
@ -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...)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user