wip - changes from last time I worked on this
This commit is contained in:
parent
25bfb521a6
commit
f48f3a98ce
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@ -23,6 +24,8 @@ var rootCmd = &cobra.Command{
|
||||
var debugLogging bool
|
||||
var debugLoggingFile *os.File
|
||||
|
||||
var ledgerFilePath string
|
||||
|
||||
func configureLogging(_ *cobra.Command, _ []string) {
|
||||
if debugLogging {
|
||||
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() {
|
||||
rootCmd.PersistentFlags().BoolVar(&debugLogging, "debug", false,
|
||||
"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(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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os/exec"
|
||||
|
||||
"github.com/tgrosinger/ledger-tui/pkg/transaction"
|
||||
tx "github.com/tgrosinger/ledger-tui/pkg/transaction"
|
||||
)
|
||||
|
||||
@ -12,8 +16,30 @@ type HLedger struct {
|
||||
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) {
|
||||
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 {
|
||||
@ -21,13 +47,57 @@ 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
|
||||
// when executing `hledger print -O json`.
|
||||
type Transaction struct {
|
||||
Tcode string `json:"tcode"`
|
||||
Tcomment string `json:"tcomment"`
|
||||
Tdate string `json:"tdate"`
|
||||
Tdate2 interface{} `json:"tdate2"`
|
||||
Tdate2 string `json:"tdate2"`
|
||||
Tdescription string `json:"tdescription"`
|
||||
Tindex int `json:"tindex"`
|
||||
Tpostings []struct {
|
||||
@ -54,7 +124,7 @@ type Transaction struct {
|
||||
Pdate2 interface{} `json:"pdate2"`
|
||||
Poriginal interface{} `json:"poriginal"`
|
||||
Pstatus string `json:"pstatus"`
|
||||
Ptags []interface{} `json:"ptags"`
|
||||
Ptags []string `json:"ptags"`
|
||||
Ptransaction string `json:"ptransaction_"`
|
||||
Ptype string `json:"ptype"`
|
||||
} `json:"tpostings"`
|
||||
@ -65,5 +135,24 @@ type Transaction struct {
|
||||
SourceName string `json:"sourceName"`
|
||||
} `json:"tsourcepos"`
|
||||
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
|
||||
|
||||
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.
|
||||
@ -24,6 +29,29 @@ type Posting struct {
|
||||
Tags []string
|
||||
}
|
||||
|
||||
func (t Transaction) string() string {
|
||||
func (t Transaction) String() string {
|
||||
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) {
|
||||
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{
|
||||
focused: date,
|
||||
furthestFocus: date,
|
||||
date: dateinput.New(time.Now(), true),
|
||||
description: textinput.New(),
|
||||
total: currencyinput.New("$"),
|
||||
suggestionBox: suggestions.New(),
|
||||
suggestionBox: suggestions.New(f),
|
||||
help: help.New(),
|
||||
keymap: keymap{
|
||||
nextField: key.NewBinding(
|
||||
@ -91,6 +97,8 @@ type AddTxTUI struct {
|
||||
// Add New Tx Form
|
||||
focused focus
|
||||
furthestFocus focus
|
||||
|
||||
// Fields
|
||||
date dateinput.Model
|
||||
description textinput.Model
|
||||
total currencyinput.Model
|
||||
@ -106,7 +114,11 @@ type AddTxTUI struct {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -171,6 +183,8 @@ func (m AddTxTUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
cmds = append(cmds, cmd)
|
||||
m.total, cmd = m.total.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
m.suggestionBox, cmd = m.suggestionBox.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ func numberValidator(s string) error {
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
// Sub-components do not have Init functions to call.
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
|
@ -120,6 +120,7 @@ func dayValidator(s string) error {
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
// Sub-components do not have Init functions to call.
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
|
@ -6,3 +6,11 @@ const (
|
||||
NextInputMsg TUIMsg = iota
|
||||
PrevInputMsg
|
||||
)
|
||||
|
||||
type ErrMsg struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ErrMsg) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
@ -1,19 +1,67 @@
|
||||
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 {
|
||||
ledgerFilename string
|
||||
transactions []transaction.Transaction
|
||||
}
|
||||
|
||||
func New() Model {
|
||||
return Model{}
|
||||
func New(ledgerFilename string) Model {
|
||||
return Model{
|
||||
ledgerFilename: ledgerFilename,
|
||||
}
|
||||
}
|
||||
|
||||
func Init() tea.Cmd {
|
||||
return nil
|
||||
// loadTransactions returns a bubbletea command which can be executed
|
||||
// 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) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user