package add import ( "errors" "fmt" "log" "os" "strings" "time" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" "github.com/tgrosinger/ledger-tui/pkg/tui/currencyinput" "github.com/tgrosinger/ledger-tui/pkg/tui/dateinput" "github.com/tgrosinger/ledger-tui/pkg/tui/postingsinput" "github.com/tgrosinger/ledger-tui/pkg/tui/suggestions" ) var AddCmd = &cobra.Command{ Use: "add", Short: "Add a new transaction to a ledger file", Args: cobra.NoArgs, Run: executeAddTUI, } 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(f), help: help.New(), keymap: keymap{ nextField: key.NewBinding( key.WithKeys("tab"), key.WithHelp("tab", "Next")), prevField: key.NewBinding( key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "Previous")), quit: key.NewBinding( key.WithKeys("esc", "ctrl+c"), key.WithHelp("esc", "Quit")), }, } addTUI.description.Placeholder = "Transaction Description" addTUI.description.Validate = descriptionValidator addTUI.description.Prompt = "" p := tea.NewProgram(addTUI, tea.WithAltScreen()) if err := p.Start(); err != nil { fmt.Printf("Alas, there's been an error: %v", err) os.Exit(1) } } func descriptionValidator(s string) error { // The description must not have a line break if strings.Contains(s, "\n") { return errors.New("description must not contain a new line") } return nil } type focus int const ( none focus = iota date description total lines ) type keymap struct { nextField key.Binding prevField key.Binding //increment key.Binding //decrement key.Binding quit key.Binding } type AddTxTUI struct { // Add New Tx Form focused focus furthestFocus focus // Fields date dateinput.Model description textinput.Model total currencyinput.Model suggestionBox suggestions.Model keymap keymap help help.Model // halfFrame is a style which can fit two side by side, separated by a // simple border. Will be resized when the window resizes. halfFrame lipgloss.Style } func (m AddTxTUI) Init() tea.Cmd { return tea.Batch( m.date.Init(), m.total.Init(), m.suggestionBox.Init(), ) } func (m AddTxTUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds := make([]tea.Cmd, 3) switch msg := msg.(type) { case tea.WindowSizeMsg: m.halfFrame = lipgloss.NewStyle(). Width((msg.Width/2)-2). Height(msg.Height-4). Border(lipgloss.NormalBorder(), true) case dateinput.Msg: switch msg { case dateinput.NextInputMsg: if m.focused == date { log.Println("Focusing description") m.focused = description } case dateinput.PrevInputMsg: // No input field before date, so ignore } case postingsinput.Msg: switch msg { case postingsinput.NextInputMsg: if m.focused == lines { // TODO: Focus on save button } case postingsinput.PrevInputMsg: if m.focused == lines { m.focused = total } } case tea.KeyMsg: switch msg.Type { // These keys should exit the program. case tea.KeyCtrlC, tea.KeyEsc: return m, tea.Quit // Navigate between fields case tea.KeyTab: if m.focused == description { m.focused = total } else if m.focused == total { m.focused = lines } case tea.KeyShiftTab: if m.focused == description { m.focused = date } else if m.focused == total { m.focused = description } } } m.setFocus() var cmd tea.Cmd m.date, cmd = m.date.Update(msg) cmds = append(cmds, cmd) m.description, cmd = m.description.Update(msg) 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...) } func (m *AddTxTUI) setFocus() { if m.focused != date && m.date.Focused() { m.date.Blur() } if m.focused != description && m.description.Focused() { m.description.Blur() } if m.focused != total && m.total.Focused() { m.total.Blur() } // TODO: Blur accounts switch m.focused { case date: m.date.Focus() if m.furthestFocus < date { m.furthestFocus = date } case description: m.description.Focus() if m.furthestFocus < description { m.furthestFocus = description } case total: m.total.Focus() if m.furthestFocus < total { m.furthestFocus = total } case lines: // TODO: Focus accounts if m.furthestFocus < lines { m.furthestFocus = lines } } } func (m AddTxTUI) helpView() string { return "\n" + m.help.ShortHelpView([]key.Binding{ m.keymap.nextField, m.keymap.prevField, m.keymap.quit, }) } func (m AddTxTUI) View() string { output := strings.Builder{} output.WriteString("Date: " + m.date.View()) if m.furthestFocus >= description { output.WriteString("\nDesc: " + m.description.View()) } if m.furthestFocus >= total { output.WriteString("\nTotal: " + m.total.View()) } return lipgloss.JoinVertical(lipgloss.Left, lipgloss.JoinHorizontal(lipgloss.Top, m.halfFrame.Render(output.String()), m.halfFrame.Render(m.suggestionBox.View())), m.helpView(), ) }