Initial commit
Description, date, and total fields are functional. Next step is to start adding structure to the form and adding a window to the side which will show previous similar transactions and autocomplete suggestions. Arrow keys will be able to auto-fill the current field from the selected suggestion.
This commit is contained in:
commit
461812eace
15
.devcontainer/Dockerfile
Normal file
15
.devcontainer/Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
FROM ghcr.io/tgrosinger/hledger-multiarch:1.26.1 as hledger
|
||||
|
||||
FROM docker.io/library/golang:1.18-bullseye
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
# [Optional] Uncomment the next lines to use go get to install anything else you need
|
||||
# USER vscode
|
||||
# RUN go get -x <your-dependency-or-tool>
|
||||
|
||||
COPY --from=hledger /usr/bin/hledger /usr/bin/hledger
|
||||
|
||||
ENTRYPOINT bash
|
27
.devcontainer/devcontainer.json
Normal file
27
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,27 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/go
|
||||
{
|
||||
"name": "Go",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
},
|
||||
"runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"go.toolsManagement.checkForUpdates": "local",
|
||||
"go.useLanguageServer": true,
|
||||
"go.gopath": "/go"
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"golang.Go"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
ledger-tui
|
||||
debug.log
|
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
FROM library/golang:1.18-bullseye as builder
|
||||
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
# TODO: Add cross compiling and multiple binary distributions.
|
||||
RUN go build cmd/ledger-tui/ledger-tui.go
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /src/ledger-tui /ledger-tui
|
||||
ENTRYPOINT /ledger-tui
|
24
examples/hledger-add.txt
Normal file
24
examples/hledger-add.txt
Normal file
@ -0,0 +1,24 @@
|
||||
$ hledger add
|
||||
Adding transactions to journal file main.journal
|
||||
Any command line arguments will be used as defaults.
|
||||
Use tab key to complete, readline keys to edit, enter to accept defaults.
|
||||
An optional (CODE) may follow transaction dates.
|
||||
An optional ; COMMENT may follow descriptions or amounts.
|
||||
If you make a mistake, enter < at any prompt to go one step backward.
|
||||
To end a transaction, enter . when prompted.
|
||||
To quit, enter . at a date prompt or press control-d or control-c.
|
||||
Date [2022-02-08]: 2/15
|
||||
Description: market
|
||||
Account 1: expenses:food
|
||||
Amount 1: $50
|
||||
Account 2: assets:cash
|
||||
Amount 2 [$-50]:
|
||||
Account 3 (or . or enter to finish this transaction):
|
||||
2022-02-15 market
|
||||
expenses:food $50
|
||||
assets:cash $-50
|
||||
|
||||
Save this transaction to the journal ? [y]:
|
||||
Saved.
|
||||
Starting the next transaction (. or ctrl-D/ctrl-C to quit)
|
||||
Date [2022-02-15]:
|
457
examples/simple.json
Normal file
457
examples/simple.json
Normal file
@ -0,0 +1,457 @@
|
||||
[
|
||||
{
|
||||
"tcode": "",
|
||||
"tcomment": "",
|
||||
"tdate": "2008-01-01",
|
||||
"tdate2": null,
|
||||
"tdescription": "income",
|
||||
"tindex": 1,
|
||||
"tpostings": [
|
||||
{
|
||||
"paccount": "assets:bank:checking",
|
||||
"pamount": [
|
||||
{
|
||||
"acommodity": "$",
|
||||
"aprice": null,
|
||||
"aquantity": {
|
||||
"decimalMantissa": 150,
|
||||
"decimalPlaces": 2,
|
||||
"floatingPoint": 1.5
|
||||
},
|
||||
"astyle": {
|
||||
"ascommodityside": "L",
|
||||
"ascommodityspaced": false,
|
||||
"asdecimalpoint": ".",
|
||||
"asdigitgroups": null,
|
||||
"asprecision": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"pbalanceassertion": null,
|
||||
"pcomment": "",
|
||||
"pdate": null,
|
||||
"pdate2": null,
|
||||
"poriginal": null,
|
||||
"pstatus": "Unmarked",
|
||||
"ptags": [],
|
||||
"ptransaction_": "1",
|
||||
"ptype": "RegularPosting"
|
||||
},
|
||||
{
|
||||
"paccount": "income:salary",
|
||||
"pamount": [
|
||||
{
|
||||
"acommodity": "$",
|
||||
"aprice": null,
|
||||
"aquantity": {
|
||||
"decimalMantissa": -150,
|
||||
"decimalPlaces": 2,
|
||||
"floatingPoint": -1.5
|
||||
},
|
||||
"astyle": {
|
||||
"ascommodityside": "L",
|
||||
"ascommodityspaced": false,
|
||||
"asdecimalpoint": ".",
|
||||
"asdigitgroups": null,
|
||||
"asprecision": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"pbalanceassertion": null,
|
||||
"pcomment": "",
|
||||
"pdate": null,
|
||||
"pdate2": null,
|
||||
"poriginal": null,
|
||||
"pstatus": "Unmarked",
|
||||
"ptags": [],
|
||||
"ptransaction_": "1",
|
||||
"ptype": "RegularPosting"
|
||||
}
|
||||
],
|
||||
"tprecedingcomment": "",
|
||||
"tsourcepos": [
|
||||
{
|
||||
"sourceColumn": 1,
|
||||
"sourceLine": 1,
|
||||
"sourceName": "/workspaces/ledger-tui/examples/simple.ledger"
|
||||
},
|
||||
{
|
||||
"sourceColumn": 1,
|
||||
"sourceLine": 4,
|
||||
"sourceName": "/workspaces/ledger-tui/examples/simple.ledger"
|
||||
}
|
||||
],
|
||||
"tstatus": "Unmarked",
|
||||
"ttags": []
|
||||
},
|
||||
{
|
||||
"tcode": "",
|
||||
"tcomment": "",
|
||||
"tdate": "2008-06-01",
|
||||
"tdate2": null,
|
||||
"tdescription": "gift",
|
||||
"tindex": 2,
|
||||
"tpostings": [
|
||||
{
|
||||
"paccount": "assets:bank:checking",
|
||||
"pamount": [
|
||||
{
|
||||
"acommodity": "$",
|
||||
"aprice": null,
|
||||
"aquantity": {
|
||||
"decimalMantissa": 2,
|
||||
"decimalPlaces": 0,
|
||||
"floatingPoint": 2
|
||||
},
|
||||
"astyle": {
|
||||
"ascommodityside": "L",
|
||||
"ascommodityspaced": false,
|
||||
"asdecimalpoint": ".",
|
||||
"asdigitgroups": null,
|
||||
"asprecision": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"pbalanceassertion": null,
|
||||
"pcomment": "",
|
||||
"pdate": null,
|
||||
"pdate2": null,
|
||||
"poriginal": null,
|
||||
"pstatus": "Unmarked",
|
||||
"ptags": [],
|
||||
"ptransaction_": "2",
|
||||
"ptype": "RegularPosting"
|
||||
},
|
||||
{
|
||||
"paccount": "income:gifts",
|
||||
"pamount": [
|
||||
{
|
||||
"acommodity": "$",
|
||||
"aprice": null,
|
||||
"aquantity": {
|
||||
"decimalMantissa": -2,
|
||||
"decimalPlaces": 0,
|
||||
"floatingPoint": -2
|
||||
},
|
||||
"astyle": {
|
||||
"ascommodityside": "L",
|
||||
"ascommodityspaced": false,
|
||||
"asdecimalpoint": ".",
|
||||
"asdigitgroups": null,
|
||||
"asprecision": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"pbalanceassertion": null,
|
||||
"pcomment": "",
|
||||
"pdate": null,
|
||||
"pdate2": null,
|
||||
"poriginal": null,
|
||||
"pstatus": "Unmarked",
|
||||
"ptags": [],
|
||||
"ptransaction_": "2",
|
||||
"ptype": "RegularPosting"
|
||||
}
|
||||
],
|
||||
"tprecedingcomment": "",
|
||||
"tsourcepos": [
|
||||
{
|
||||
"sourceColumn": 1,
|
||||
"sourceLine": 5,
|
||||
"sourceName": "/workspaces/ledger-tui/examples/simple.ledger"
|
||||
},
|
||||
{
|
||||
"sourceColumn": 1,
|
||||
"sourceLine": 8,
|
||||
"sourceName": "/workspaces/ledger-tui/examples/simple.ledger"
|
||||
}
|
||||
],
|
||||
"tstatus": "Unmarked",
|
||||
"ttags": []
|
||||
},
|
||||
{
|
||||
"tcode": "",
|
||||
"tcomment": "",
|
||||
"tdate": "2008-06-02",
|
||||
"tdate2": null,
|
||||
"tdescription": "save",
|
||||
"tindex": 3,
|
||||
"tpostings": [
|
||||
{
|
||||
"paccount": "assets:bank:saving",
|
||||
"pamount": [
|
||||
{
|
||||
"acommodity": "$",
|
||||
"aprice": null,
|
||||
"aquantity": {
|
||||
"decimalMantissa": 345,
|
||||
"decimalPlaces": 2,
|
||||
"floatingPoint": 3.45
|
||||
},
|
||||
"astyle": {
|
||||
"ascommodityside": "L",
|
||||
"ascommodityspaced": false,
|
||||
"asdecimalpoint": ".",
|
||||
"asdigitgroups": null,
|
||||
"asprecision": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"pbalanceassertion": null,
|
||||
"pcomment": "",
|
||||
"pdate": null,
|
||||
"pdate2": null,
|
||||
"poriginal": null,
|
||||
"pstatus": "Unmarked",
|
||||
"ptags": [],
|
||||
"ptransaction_": "3",
|
||||
"ptype": "RegularPosting"
|
||||
},
|
||||
{
|
||||
"paccount": "assets:bank:checking",
|
||||
"pamount": [
|
||||
{
|
||||
"acommodity": "$",
|
||||
"aprice": null,
|
||||
"aquantity": {
|
||||
"decimalMantissa": -345,
|
||||
"decimalPlaces": 2,
|
||||
"floatingPoint": -3.45
|
||||
},
|
||||
"astyle": {
|
||||
"ascommodityside": "L",
|
||||
"ascommodityspaced": false,
|
||||
"asdecimalpoint": ".",
|
||||
"asdigitgroups": null,
|
||||
"asprecision": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"pbalanceassertion": null,
|
||||
"pcomment": "",
|
||||
"pdate": null,
|
||||
"pdate2": null,
|
||||
"poriginal": null,
|
||||
"pstatus": "Unmarked",
|
||||
"ptags": [],
|
||||
"ptransaction_": "3",
|
||||
"ptype": "RegularPosting"
|
||||
}
|
||||
],
|
||||
"tprecedingcomment": "",
|
||||
"tsourcepos": [
|
||||
{
|
||||
"sourceColumn": 1,
|
||||
"sourceLine": 9,
|
||||
"sourceName": "/workspaces/ledger-tui/examples/simple.ledger"
|
||||
},
|
||||
{
|
||||
"sourceColumn": 1,
|
||||
"sourceLine": 12,
|
||||
"sourceName": "/workspaces/ledger-tui/examples/simple.ledger"
|
||||
}
|
||||
],
|
||||
"tstatus": "Unmarked",
|
||||
"ttags": []
|
||||
},
|
||||
{
|
||||
"tcode": "",
|
||||
"tcomment": "",
|
||||
"tdate": "2008-06-03",
|
||||
"tdate2": null,
|
||||
"tdescription": "eat & shop",
|
||||
"tindex": 4,
|
||||
"tpostings": [
|
||||
{
|
||||
"paccount": "expenses:food",
|
||||
"pamount": [
|
||||
{
|
||||
"acommodity": "$",
|
||||
"aprice": null,
|
||||
"aquantity": {
|
||||
"decimalMantissa": 125,
|
||||
"decimalPlaces": 2,
|
||||
"floatingPoint": 1.25
|
||||
},
|
||||
"astyle": {
|
||||
"ascommodityside": "L",
|
||||
"ascommodityspaced": false,
|
||||
"asdecimalpoint": ".",
|
||||
"asdigitgroups": null,
|
||||
"asprecision": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"pbalanceassertion": null,
|
||||
"pcomment": "",
|
||||
"pdate": null,
|
||||
"pdate2": null,
|
||||
"poriginal": null,
|
||||
"pstatus": "Unmarked",
|
||||
"ptags": [],
|
||||
"ptransaction_": "4",
|
||||
"ptype": "RegularPosting"
|
||||
},
|
||||
{
|
||||
"paccount": "expenses:supplies",
|
||||
"pamount": [
|
||||
{
|
||||
"acommodity": "$",
|
||||
"aprice": null,
|
||||
"aquantity": {
|
||||
"decimalMantissa": 475,
|
||||
"decimalPlaces": 2,
|
||||
"floatingPoint": 4.75
|
||||
},
|
||||
"astyle": {
|
||||
"ascommodityside": "L",
|
||||
"ascommodityspaced": false,
|
||||
"asdecimalpoint": ".",
|
||||
"asdigitgroups": null,
|
||||
"asprecision": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"pbalanceassertion": null,
|
||||
"pcomment": "",
|
||||
"pdate": null,
|
||||
"pdate2": null,
|
||||
"poriginal": null,
|
||||
"pstatus": "Unmarked",
|
||||
"ptags": [],
|
||||
"ptransaction_": "4",
|
||||
"ptype": "RegularPosting"
|
||||
},
|
||||
{
|
||||
"paccount": "assets:cash",
|
||||
"pamount": [
|
||||
{
|
||||
"acommodity": "$",
|
||||
"aprice": null,
|
||||
"aquantity": {
|
||||
"decimalMantissa": -600,
|
||||
"decimalPlaces": 2,
|
||||
"floatingPoint": -6
|
||||
},
|
||||
"astyle": {
|
||||
"ascommodityside": "L",
|
||||
"ascommodityspaced": false,
|
||||
"asdecimalpoint": ".",
|
||||
"asdigitgroups": null,
|
||||
"asprecision": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"pbalanceassertion": null,
|
||||
"pcomment": "",
|
||||
"pdate": null,
|
||||
"pdate2": null,
|
||||
"poriginal": null,
|
||||
"pstatus": "Unmarked",
|
||||
"ptags": [],
|
||||
"ptransaction_": "4",
|
||||
"ptype": "RegularPosting"
|
||||
}
|
||||
],
|
||||
"tprecedingcomment": "",
|
||||
"tsourcepos": [
|
||||
{
|
||||
"sourceColumn": 1,
|
||||
"sourceLine": 13,
|
||||
"sourceName": "/workspaces/ledger-tui/examples/simple.ledger"
|
||||
},
|
||||
{
|
||||
"sourceColumn": 1,
|
||||
"sourceLine": 17,
|
||||
"sourceName": "/workspaces/ledger-tui/examples/simple.ledger"
|
||||
}
|
||||
],
|
||||
"tstatus": "Cleared",
|
||||
"ttags": []
|
||||
},
|
||||
{
|
||||
"tcode": "",
|
||||
"tcomment": "",
|
||||
"tdate": "2008-12-31",
|
||||
"tdate2": null,
|
||||
"tdescription": "pay off",
|
||||
"tindex": 5,
|
||||
"tpostings": [
|
||||
{
|
||||
"paccount": "liabilities:debts",
|
||||
"pamount": [
|
||||
{
|
||||
"acommodity": "$",
|
||||
"aprice": null,
|
||||
"aquantity": {
|
||||
"decimalMantissa": 1,
|
||||
"decimalPlaces": 0,
|
||||
"floatingPoint": 1
|
||||
},
|
||||
"astyle": {
|
||||
"ascommodityside": "L",
|
||||
"ascommodityspaced": false,
|
||||
"asdecimalpoint": ".",
|
||||
"asdigitgroups": null,
|
||||
"asprecision": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"pbalanceassertion": null,
|
||||
"pcomment": "",
|
||||
"pdate": null,
|
||||
"pdate2": null,
|
||||
"poriginal": null,
|
||||
"pstatus": "Unmarked",
|
||||
"ptags": [],
|
||||
"ptransaction_": "5",
|
||||
"ptype": "RegularPosting"
|
||||
},
|
||||
{
|
||||
"paccount": "assets:bank:checking",
|
||||
"pamount": [
|
||||
{
|
||||
"acommodity": "$",
|
||||
"aprice": null,
|
||||
"aquantity": {
|
||||
"decimalMantissa": -1,
|
||||
"decimalPlaces": 0,
|
||||
"floatingPoint": -1
|
||||
},
|
||||
"astyle": {
|
||||
"ascommodityside": "L",
|
||||
"ascommodityspaced": false,
|
||||
"asdecimalpoint": ".",
|
||||
"asdigitgroups": null,
|
||||
"asprecision": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"pbalanceassertion": null,
|
||||
"pcomment": "",
|
||||
"pdate": null,
|
||||
"pdate2": null,
|
||||
"poriginal": null,
|
||||
"pstatus": "Unmarked",
|
||||
"ptags": [],
|
||||
"ptransaction_": "5",
|
||||
"ptype": "RegularPosting"
|
||||
}
|
||||
],
|
||||
"tprecedingcomment": "",
|
||||
"tsourcepos": [
|
||||
{
|
||||
"sourceColumn": 1,
|
||||
"sourceLine": 18,
|
||||
"sourceName": "/workspaces/ledger-tui/examples/simple.ledger"
|
||||
},
|
||||
{
|
||||
"sourceColumn": 25,
|
||||
"sourceLine": 20,
|
||||
"sourceName": "/workspaces/ledger-tui/examples/simple.ledger"
|
||||
}
|
||||
],
|
||||
"tstatus": "Cleared",
|
||||
"ttags": []
|
||||
}
|
||||
]
|
20
examples/simple.ledger
Normal file
20
examples/simple.ledger
Normal file
@ -0,0 +1,20 @@
|
||||
2008/01/01 income
|
||||
assets:bank:checking $1.50
|
||||
income:salary
|
||||
|
||||
2008/06/01 gift
|
||||
assets:bank:checking $2
|
||||
income:gifts
|
||||
|
||||
2008/06/02 save
|
||||
assets:bank:saving $3.45
|
||||
assets:bank:checking
|
||||
|
||||
2008/06/03 * eat & shop
|
||||
expenses:food $1.25
|
||||
expenses:supplies $4.75
|
||||
assets:cash
|
||||
|
||||
2008/12/31 * pay off
|
||||
liabilities:debts $1
|
||||
assets:bank:checking
|
29
go.mod
Normal file
29
go.mod
Normal file
@ -0,0 +1,29 @@
|
||||
module github.com/tgrosinger/ledger-tui
|
||||
|
||||
go 1.18
|
||||
|
||||
require github.com/charmbracelet/bubbletea v0.22.0
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/charmbracelet/lipgloss v0.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/lrstanley/bubblezone v0.0.0-20220729154607-e408d1dc3890 // indirect
|
||||
github.com/spf13/cobra v1.5.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v0.13.0
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
||||
github.com/muesli/cancelreader v0.2.1 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
)
|
58
go.sum
Normal file
58
go.sum
Normal file
@ -0,0 +1,58 @@
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/charmbracelet/bubbles v0.13.0 h1:zP/ROH3wJEBqZWKIsD50ZKKlx3ydLInq3LdD/Nrlb8w=
|
||||
github.com/charmbracelet/bubbles v0.13.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
|
||||
github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
|
||||
github.com/charmbracelet/bubbletea v0.22.0 h1:E1BTNSE3iIrq0G0X6TjGAmrQ32cGCbFDPcIuImikrUc=
|
||||
github.com/charmbracelet/bubbletea v0.22.0/go.mod h1:aoVIwlNlr5wbCB26KhxfrqAn0bMp4YpJcoOelbxApjs=
|
||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
|
||||
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lrstanley/bubblezone v0.0.0-20220729154607-e408d1dc3890 h1:mAJcJhjnJiwT3w9q1whqbnvDfezUSR+ECVb+B1j3D64=
|
||||
github.com/lrstanley/bubblezone v0.0.0-20220729154607-e408d1dc3890/go.mod h1:CxaUrg7Y6DmnquTpb1Rgxib+u+NcRxrDi8m/mR1poTM=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/cancelreader v0.2.1 h1:Xzd1B4U5bWQOuSKuN398MyynIGTNT89dxzpEDsalXZs=
|
||||
github.com/muesli/cancelreader v0.2.1/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
|
||||
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
69
pkg/ledger/hledger/hledger.go
Normal file
69
pkg/ledger/hledger/hledger.go
Normal file
@ -0,0 +1,69 @@
|
||||
package hledger
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
tx "github.com/tgrosinger/ledger-tui/pkg/transaction"
|
||||
)
|
||||
|
||||
// HLedger implements the Ledger interface. It uses the hledger binary to pull
|
||||
// data from the ledger file.
|
||||
type HLedger struct {
|
||||
filepath string
|
||||
}
|
||||
|
||||
func (hl HLedger) GetTransactions() ([]tx.Transaction, error) {
|
||||
return nil, errors.New("Method not implemented")
|
||||
}
|
||||
|
||||
func (hl HLedger) AddTransaction(newTx tx.Transaction) error {
|
||||
return errors.New("Method not implemented")
|
||||
|
||||
}
|
||||
|
||||
// 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"`
|
||||
Tdescription string `json:"tdescription"`
|
||||
Tindex int `json:"tindex"`
|
||||
Tpostings []struct {
|
||||
Paccount string `json:"paccount"`
|
||||
Pamount []struct {
|
||||
Acommodity string `json:"acommodity"`
|
||||
Aprice interface{} `json:"aprice"`
|
||||
Aquantity struct {
|
||||
DecimalMantissa int `json:"decimalMantissa"`
|
||||
DecimalPlaces int `json:"decimalPlaces"`
|
||||
FloatingPoint float64 `json:"floatingPoint"`
|
||||
} `json:"aquantity"`
|
||||
Astyle struct {
|
||||
Ascommodityside string `json:"ascommodityside"`
|
||||
Ascommodityspaced bool `json:"ascommodityspaced"`
|
||||
Asdecimalpoint string `json:"asdecimalpoint"`
|
||||
Asdigitgroups interface{} `json:"asdigitgroups"`
|
||||
Asprecision int `json:"asprecision"`
|
||||
} `json:"astyle"`
|
||||
} `json:"pamount"`
|
||||
Pbalanceassertion interface{} `json:"pbalanceassertion"`
|
||||
Pcomment string `json:"pcomment"`
|
||||
Pdate interface{} `json:"pdate"`
|
||||
Pdate2 interface{} `json:"pdate2"`
|
||||
Poriginal interface{} `json:"poriginal"`
|
||||
Pstatus string `json:"pstatus"`
|
||||
Ptags []interface{} `json:"ptags"`
|
||||
Ptransaction string `json:"ptransaction_"`
|
||||
Ptype string `json:"ptype"`
|
||||
} `json:"tpostings"`
|
||||
Tprecedingcomment string `json:"tprecedingcomment"`
|
||||
Tsourcepos []struct {
|
||||
SourceColumn int `json:"sourceColumn"`
|
||||
SourceLine int `json:"sourceLine"`
|
||||
SourceName string `json:"sourceName"`
|
||||
} `json:"tsourcepos"`
|
||||
Tstatus string `json:"tstatus"`
|
||||
Ttags []interface{} `json:"ttags"`
|
||||
}
|
8
pkg/ledger/ledger.go
Normal file
8
pkg/ledger/ledger.go
Normal file
@ -0,0 +1,8 @@
|
||||
package ledger
|
||||
|
||||
import "github.com/tgrosinger/ledger-tui/pkg/transaction"
|
||||
|
||||
type Ledger interface {
|
||||
GetTransactions() ([]transaction.Transaction, error)
|
||||
AddTransaction(newTx transaction.Transaction) error
|
||||
}
|
1
pkg/transaction/collection.go
Normal file
1
pkg/transaction/collection.go
Normal file
@ -0,0 +1 @@
|
||||
package transaction
|
29
pkg/transaction/transaction.go
Normal file
29
pkg/transaction/transaction.go
Normal file
@ -0,0 +1,29 @@
|
||||
package transaction
|
||||
|
||||
import "time"
|
||||
|
||||
// Transaction contains the information stored in a ledger file about a single
|
||||
// transaction.
|
||||
type Transaction struct {
|
||||
Index int
|
||||
Date time.Time
|
||||
Description string
|
||||
Comment string
|
||||
PreceedingComment string
|
||||
Postings []Posting
|
||||
Status string // TODO: Enum?
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type Posting struct {
|
||||
Amount float64
|
||||
Commodity string
|
||||
Account string
|
||||
Status string // TODO: Enum?
|
||||
Type string // TODO: Enum?
|
||||
Tags []string
|
||||
}
|
||||
|
||||
func (t Transaction) string() string {
|
||||
return "TODO"
|
||||
}
|
17
pkg/tui/cmds.go
Normal file
17
pkg/tui/cmds.go
Normal file
@ -0,0 +1,17 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
//
|
||||
// Bubbletea Commands
|
||||
//
|
||||
|
||||
func NextInput() tea.Msg {
|
||||
return NextInputMsg
|
||||
}
|
||||
|
||||
func PrevInput() tea.Msg {
|
||||
return PrevInputMsg
|
||||
}
|
230
pkg/tui/commands/add/add.go
Normal file
230
pkg/tui/commands/add/add.go
Normal file
@ -0,0 +1,230 @@
|
||||
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/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"
|
||||
)
|
||||
|
||||
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) {
|
||||
addTUI := AddTxTUI{
|
||||
focused: date,
|
||||
furthestFocus: date,
|
||||
date: dateinput.New(time.Now(), true),
|
||||
description: textinput.New(),
|
||||
total: currencyinput.New("$"),
|
||||
help: help.New(),
|
||||
keymap: keymap{
|
||||
nextField: key.NewBinding(
|
||||
key.WithKeys("tab"),
|
||||
key.WithHelp("tab", "Next Field")),
|
||||
prevField: key.NewBinding(
|
||||
key.WithKeys("shift+tab"),
|
||||
key.WithHelp("shift+tab", "Previous Field")),
|
||||
quit: key.NewBinding(
|
||||
key.WithKeys("esc", "ctrl+c"),
|
||||
key.WithHelp("esc", "Quit")),
|
||||
},
|
||||
}
|
||||
|
||||
addTUI.description.Placeholder = "Transaction Description"
|
||||
addTUI.description.Validate = descriptionValidator
|
||||
|
||||
p := tea.NewProgram(addTUI)
|
||||
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
|
||||
date dateinput.Model
|
||||
description textinput.Model
|
||||
total currencyinput.Model
|
||||
|
||||
keymap keymap
|
||||
help help.Model
|
||||
}
|
||||
|
||||
func (m AddTxTUI) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m AddTxTUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
cmds := make([]tea.Cmd, 3)
|
||||
|
||||
switch msg := msg.(type) {
|
||||
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)
|
||||
|
||||
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 {
|
||||
log.Println("Rendering LedgerTUI")
|
||||
|
||||
output := strings.Builder{}
|
||||
output.WriteString("Tx Date: " + m.date.View())
|
||||
|
||||
if m.furthestFocus >= description {
|
||||
output.WriteString("\nDescription: " + m.description.View())
|
||||
}
|
||||
|
||||
if m.furthestFocus >= total {
|
||||
output.WriteString("\nTotal: " + m.total.View())
|
||||
|
||||
}
|
||||
|
||||
// TODO: When accounts is focused, only show the account view.
|
||||
// The account view should have a text box at the top, and a list of
|
||||
// suggestions below that can be selected.
|
||||
// Maybe use a "next" button or something to make it less jarring.
|
||||
|
||||
output.WriteString(m.helpView())
|
||||
|
||||
return output.String()
|
||||
}
|
74
pkg/tui/currencyinput/currencyinput.go
Normal file
74
pkg/tui/currencyinput/currencyinput.go
Normal file
@ -0,0 +1,74 @@
|
||||
package currencyinput
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
amount textinput.Model
|
||||
}
|
||||
|
||||
func New(prefix string) Model {
|
||||
m := Model{
|
||||
amount: textinput.New(),
|
||||
}
|
||||
|
||||
m.amount.Prompt = prefix
|
||||
m.amount.Validate = numberValidator
|
||||
m.amount.Placeholder = "00.00"
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func numberValidator(s string) error {
|
||||
// Should be a number with a maximum of two decimal places
|
||||
_, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decimalIndex := strings.Index(s, ".")
|
||||
if decimalIndex == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
decimalPlaces := (len(s) - 1) - decimalIndex
|
||||
log.Println(decimalPlaces)
|
||||
if decimalPlaces > 2 {
|
||||
return errors.New("currency may only have two decimal places")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
m.amount, cmd = m.amount.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
return m.amount.View()
|
||||
}
|
||||
|
||||
func (m *Model) Focused() bool {
|
||||
return m.amount.Focused()
|
||||
}
|
||||
|
||||
func (m *Model) Focus() {
|
||||
m.amount.Focus()
|
||||
}
|
||||
|
||||
func (m *Model) Blur() {
|
||||
m.amount.Blur()
|
||||
}
|
18
pkg/tui/dateinput/cmds.go
Normal file
18
pkg/tui/dateinput/cmds.go
Normal file
@ -0,0 +1,18 @@
|
||||
package dateinput
|
||||
|
||||
import tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
type Msg int
|
||||
|
||||
const (
|
||||
NextInputMsg Msg = iota
|
||||
PrevInputMsg
|
||||
)
|
||||
|
||||
func NextInput() tea.Msg {
|
||||
return NextInputMsg
|
||||
}
|
||||
|
||||
func PrevInput() tea.Msg {
|
||||
return PrevInputMsg
|
||||
}
|
386
pkg/tui/dateinput/dateInput.go
Normal file
386
pkg/tui/dateinput/dateInput.go
Normal file
@ -0,0 +1,386 @@
|
||||
package dateinput
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// TODO: This could probably be its own repo
|
||||
|
||||
// focus denotes which field in this input is focused.
|
||||
type focus int
|
||||
|
||||
const (
|
||||
none focus = iota
|
||||
year
|
||||
month
|
||||
day
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
year textinput.Model
|
||||
month textinput.Model
|
||||
day textinput.Model
|
||||
|
||||
focused focus
|
||||
}
|
||||
|
||||
func New(defaultDate time.Time, focused bool) Model {
|
||||
d := Model{
|
||||
year: textinput.New(),
|
||||
month: textinput.New(),
|
||||
day: textinput.New(),
|
||||
}
|
||||
|
||||
d.year.Placeholder = "YYYY"
|
||||
d.year.Prompt = ""
|
||||
d.year.Width = 4
|
||||
d.year.SetValue(strconv.Itoa(defaultDate.Year()))
|
||||
d.year.CharLimit = 4
|
||||
d.year.Validate = yearValidator
|
||||
|
||||
d.month.Placeholder = "MM"
|
||||
d.month.Prompt = ""
|
||||
d.month.Width = 2
|
||||
d.month.SetValue(defaultDate.Format("01"))
|
||||
d.month.CharLimit = 2
|
||||
d.month.Validate = monthValidator
|
||||
|
||||
d.day.Placeholder = "DD"
|
||||
d.day.Prompt = ""
|
||||
d.day.Width = 2
|
||||
d.day.SetValue(defaultDate.Format("02"))
|
||||
d.day.CharLimit = 2
|
||||
d.day.Validate = dayValidator
|
||||
|
||||
if focused {
|
||||
d.log("Setting initial focus to year")
|
||||
d.focused = year
|
||||
d.year.Focus()
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
func yearValidator(s string) error {
|
||||
// The input already restricts to 4 digits, so we just need to ensure the
|
||||
// value is greater than 0 (technically).
|
||||
val, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if val <= 0 {
|
||||
return errors.New("year must be positive")
|
||||
}
|
||||
|
||||
// TODO: Ensure that the month-day combination is still valid.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func monthValidator(s string) error {
|
||||
// The input already restricts to 4 digits, so we just need to ensure the
|
||||
// value is between 1 and 12.
|
||||
val, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if val < 1 || val > 12 {
|
||||
return errors.New("month must be between 1 and 12")
|
||||
}
|
||||
|
||||
// TODO: Ensure that the month-day combination is valid.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dayValidator(s string) error {
|
||||
// The input already restricts to 4 digits, so we just need to ensure the
|
||||
// value is between 1 and 31.
|
||||
val, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if val < 1 || val > 31 {
|
||||
return errors.New("day must be between 1 and 31")
|
||||
}
|
||||
|
||||
// TODO: Ensure that the month-day combination is valid.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
cmds := make([]tea.Cmd, 3)
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.Type {
|
||||
|
||||
// These keys should exit the program.
|
||||
case tea.KeyCtrlC, tea.KeyEsc:
|
||||
return m, tea.Quit
|
||||
|
||||
// Navigate the input fields
|
||||
case tea.KeyTab:
|
||||
cmds = append(cmds, m.nextInput(true))
|
||||
case tea.KeyShiftTab:
|
||||
cmds = append(cmds, m.prevInput(true))
|
||||
case tea.KeyRight:
|
||||
// Only advance fields if cursor is at the end of this field
|
||||
cmds = append(cmds, m.nextInput(false))
|
||||
case tea.KeyLeft:
|
||||
// Only advance fields if cursor is at the start of this field
|
||||
cmds = append(cmds, m.prevInput(false))
|
||||
|
||||
// Modify the focused input field with arrow keys
|
||||
case tea.KeyUp:
|
||||
m.incrementField()
|
||||
case tea.KeyDown:
|
||||
m.decrementField()
|
||||
}
|
||||
|
||||
m.setFocus()
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.year, cmd = m.year.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
m.month, cmd = m.month.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
m.day, cmd = m.day.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
return fmt.Sprintf(`%s- %s- %s`,
|
||||
m.year.View(),
|
||||
m.month.View(),
|
||||
m.day.View())
|
||||
}
|
||||
|
||||
func (m *Model) Focused() bool {
|
||||
return m.focused != none
|
||||
}
|
||||
|
||||
// Focus gives focus to the first field in this model.
|
||||
func (m *Model) Focus() {
|
||||
if m.focused == none {
|
||||
m.focused = year
|
||||
m.setFocus()
|
||||
}
|
||||
}
|
||||
|
||||
// Blur removes focus from all fields contained in this model.
|
||||
func (m *Model) Blur() {
|
||||
if m.focused != none {
|
||||
m.focused = none
|
||||
m.setFocus()
|
||||
}
|
||||
}
|
||||
|
||||
// AtEnd returns true if the cursor is at the end of the last field in this
|
||||
// model. If ignoreCursor it will return true if the cursor is anywhere in the
|
||||
// last field.
|
||||
func (m *Model) AtEnd(ignoreCursor bool) bool {
|
||||
if m.focused != day {
|
||||
return false
|
||||
}
|
||||
return ignoreCursor || m.day.Cursor() == 3
|
||||
}
|
||||
|
||||
// AtStart returns true if the cursor is at the start of the first field in this
|
||||
// model. If ignoreCursor it will return true if the cursor is anywhere in the
|
||||
// first field.
|
||||
func (m *Model) AtStart(ignoreCursor bool) bool {
|
||||
if m.focused != year {
|
||||
return false
|
||||
}
|
||||
return ignoreCursor || m.year.Cursor() == 0
|
||||
}
|
||||
|
||||
// setFocus updates the contained fields to ensure only the correct one has
|
||||
// focus.
|
||||
func (m *Model) setFocus() {
|
||||
if m.focused != year && m.year.Focused() {
|
||||
m.year.Blur()
|
||||
}
|
||||
if m.focused != month && m.month.Focused() {
|
||||
m.month.Blur()
|
||||
}
|
||||
if m.focused != day && m.day.Focused() {
|
||||
m.day.Blur()
|
||||
}
|
||||
|
||||
switch m.focused {
|
||||
case year:
|
||||
m.year.Focus()
|
||||
case month:
|
||||
m.month.Focus()
|
||||
case day:
|
||||
m.day.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
// nextInput changes the focus in this model to the next field. Does nothing if
|
||||
// already on the last field. Also places the cursor in that field at the end.
|
||||
func (m *Model) nextInput(force bool) tea.Cmd {
|
||||
if !force {
|
||||
switch m.focused {
|
||||
case year:
|
||||
pos := m.year.Cursor()
|
||||
if pos != 4 {
|
||||
return nil
|
||||
}
|
||||
case month:
|
||||
pos := m.month.Cursor()
|
||||
if pos != 2 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.log(fmt.Sprintf("Switching to next input from %d", m.focused))
|
||||
// Setting the cursor to the end of the text box so that backspace is easily available.
|
||||
switch m.focused {
|
||||
case year:
|
||||
m.focused = month
|
||||
m.month.CursorEnd()
|
||||
case month:
|
||||
m.focused = day
|
||||
m.day.CursorEnd()
|
||||
default:
|
||||
// On the last input already
|
||||
m.focused = none
|
||||
return NextInput
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// prevInput changes the focus in this model to the previous field. Does nothing
|
||||
// if already on the first field. Also places the cursor in that field at the
|
||||
// end.
|
||||
func (m *Model) prevInput(force bool) tea.Cmd {
|
||||
if !force {
|
||||
switch m.focused {
|
||||
case year:
|
||||
pos := m.year.Cursor()
|
||||
if pos != 0 {
|
||||
return nil
|
||||
}
|
||||
case month:
|
||||
pos := m.month.Cursor()
|
||||
if pos != 0 {
|
||||
return nil
|
||||
}
|
||||
case day:
|
||||
pos := m.day.Cursor()
|
||||
if pos != 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.log(fmt.Sprintf("Switching to prev input from %d", m.focused))
|
||||
switch m.focused {
|
||||
case month:
|
||||
m.focused = year
|
||||
m.year.CursorEnd()
|
||||
case day:
|
||||
m.focused = month
|
||||
m.month.CursorEnd()
|
||||
// TODO: Reset blink if needed
|
||||
default:
|
||||
// On the first input already
|
||||
m.focused = none
|
||||
return PrevInput
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// incrementField intelligently increments the value in the selected field. Will
|
||||
// roll over other fields if necessary to keep the date valid.
|
||||
func (m *Model) incrementField() {
|
||||
m.log(fmt.Sprintf("Incrementing %d field", m.focused))
|
||||
|
||||
currentDate, err := m.currentDate()
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
switch m.focused {
|
||||
case year:
|
||||
currentDate = currentDate.AddDate(1, 0, 0)
|
||||
case month:
|
||||
currentDate = currentDate.AddDate(0, 1, 0)
|
||||
case day:
|
||||
currentDate = currentDate.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
m.setDate(currentDate)
|
||||
}
|
||||
|
||||
// decrementField intelligently decrements the value in the selected field. Will
|
||||
// roll over other fields if necessary to keep the date valid.
|
||||
func (m *Model) decrementField() {
|
||||
m.log(fmt.Sprintf("Decrementing %d field", m.focused))
|
||||
|
||||
currentDate, err := m.currentDate()
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
switch m.focused {
|
||||
case year:
|
||||
currentDate = currentDate.AddDate(-1, 0, 0)
|
||||
case month:
|
||||
currentDate = currentDate.AddDate(0, -1, 0)
|
||||
case day:
|
||||
currentDate = currentDate.AddDate(0, 0, -1)
|
||||
}
|
||||
|
||||
m.setDate(currentDate)
|
||||
|
||||
}
|
||||
|
||||
// setDate takes a time.Time and sets the values of the three date fields accordingly.
|
||||
func (m *Model) setDate(date time.Time) {
|
||||
m.log("Setting date to " + date.Format("2006-01-02"))
|
||||
m.year.SetValue(date.Format("2006"))
|
||||
m.month.SetValue(date.Format("01"))
|
||||
m.day.SetValue(date.Format("02"))
|
||||
}
|
||||
|
||||
// currentDate retrieves the values from the three date fields and constructs
|
||||
// them into a time.Time object if possible. Returns an error if the values are
|
||||
// not valid and cannot be turned into a date.
|
||||
func (m *Model) currentDate() (time.Time, error) {
|
||||
parsed, err := time.Parse("2006-01-02",
|
||||
fmt.Sprintf("%s-%s-%s", m.year.Value(), m.month.Value(), m.day.Value()))
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("failed to parse the currently configured time: %w", err)
|
||||
}
|
||||
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func (m *Model) log(msg string) {
|
||||
log.Println("DateInput: " + msg)
|
||||
}
|
5
pkg/tui/fuzzyinput/fuzzyInput.go
Normal file
5
pkg/tui/fuzzyinput/fuzzyInput.go
Normal file
@ -0,0 +1,5 @@
|
||||
package tui
|
||||
|
||||
// TODO: This could probably be its own repo
|
||||
|
||||
// Use https://github.com/lithammer/fuzzysearch
|
8
pkg/tui/msgs.go
Normal file
8
pkg/tui/msgs.go
Normal file
@ -0,0 +1,8 @@
|
||||
package tui
|
||||
|
||||
type TUIMsg int
|
||||
|
||||
const (
|
||||
NextInputMsg TUIMsg = iota
|
||||
PrevInputMsg
|
||||
)
|
18
pkg/tui/postingsinput/cmds.go
Normal file
18
pkg/tui/postingsinput/cmds.go
Normal file
@ -0,0 +1,18 @@
|
||||
package postingsinput
|
||||
|
||||
import tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
type Msg int
|
||||
|
||||
const (
|
||||
NextInputMsg Msg = iota
|
||||
PrevInputMsg
|
||||
)
|
||||
|
||||
func NextInput() tea.Msg {
|
||||
return NextInputMsg
|
||||
}
|
||||
|
||||
func PrevInput() tea.Msg {
|
||||
return PrevInputMsg
|
||||
}
|
1
pkg/tui/postingsinput/postingsInput.go
Normal file
1
pkg/tui/postingsinput/postingsInput.go
Normal file
@ -0,0 +1 @@
|
||||
package postingsinput
|
Reference in New Issue
Block a user