diff --git a/cmd/canal/main.go b/cmd/canal/main.go index 568ed01..ea7d5d1 100644 --- a/cmd/canal/main.go +++ b/cmd/canal/main.go @@ -7,7 +7,9 @@ import ( ) func main() { - if err := ui.App(); err != nil { - fmt.Printf("Error: %s", err) + canal := ui.NewApp() + + if err := canal.Run(); err != nil { + fmt.Printf("Error: an error occurred while running Canal, %s", err) } } diff --git a/go.mod b/go.mod index faa6e57..831abe5 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module forge.dananglin.me.uk/code/dananglin/canal go 1.16 require ( - github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807 + github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 github.com/magefile/mage v1.11.0 + github.com/rivo/tview v0.0.0-20210923051754-2cb20002bc4c go.etcd.io/bbolt v1.3.6 ) diff --git a/go.sum b/go.sum index c09086e..49d2d96 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,27 @@ -github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807 h1:jdjd5e68T4R/j4PWxfZqcKY8KtT9oo8IPNVuV4bSXDQ= -github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807/go.mod h1:Xoiu5VdKMvbRgHuY7+z64lhu/7lvax/22nzASF6GrO8= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM= +github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04= +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/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls= github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +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/rivo/tview v0.0.0-20210923051754-2cb20002bc4c h1:ye4bWm8SafYmr0DADOKSfeVZ1Swzm9aLW+baCOcHDWE= +github.com/rivo/tview v0.0.0-20210923051754-2cb20002bc4c/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d h1:L/IKR6COd7ubZrs2oTnTi73IhgqJ71c9s80WsQnh0Es= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/board/args.go b/internal/board/args.go new file mode 100644 index 0000000..342cd1f --- /dev/null +++ b/internal/board/args.go @@ -0,0 +1,17 @@ +package board + +type CardArgs struct { + NewTitle string + NewContent string +} + +type UpdateCardArgs struct { + CardID int + CardArgs +} + +type UpdateCardStatusArgs struct { + CardID int + OldStatusID int + NewStatusID int +} diff --git a/internal/board/board.go b/internal/board/board.go index 9e03e81..d8b1462 100644 --- a/internal/board/board.go +++ b/internal/board/board.go @@ -10,8 +10,8 @@ import ( bolt "go.etcd.io/bbolt" ) -// LoadBoard reads the board from the database. If no board exists then a new one will be created. -func LoadBoard(path string) (*bolt.DB, error) { +// OpenProject reads the project from the database. If no board exists then a new one will be created. +func OpenProject(path string) (*bolt.DB, error) { db, err := database.OpenDatabase(path) if err != nil { return nil, fmt.Errorf("unable to open the database, %w", err) @@ -31,7 +31,7 @@ func LoadBoard(path string) (*bolt.DB, error) { boltItems[i] = &newStatusList[i] } - if err := database.WriteMany(db, database.StatusBucket, boltItems); err != nil { + if _, err := database.WriteMany(db, database.StatusBucket, boltItems); err != nil { return nil, fmt.Errorf("unable to save the default status list to the database, %w", err) } } @@ -88,7 +88,7 @@ func DeleteStatus(db *bolt.DB) error { } // CreateCard creates a card in the database. -func CreateCard(db *bolt.DB, title, content string) error { +func CreateCard(db *bolt.DB, args CardArgs) error { statusList, err := ReadStatusList(db) if err != nil { return fmt.Errorf("unable to read the status list, %w", err) @@ -100,8 +100,8 @@ func CreateCard(db *bolt.DB, title, content string) error { card := Card{ ID: -1, - Title: title, - Content: content, + Title: args.NewTitle, + Content: args.NewContent, } cardID, err := database.Write(db, database.CardBucket, &card) @@ -140,19 +140,46 @@ func ReadCard(db *bolt.DB, id int) (Card, error) { return card, nil } +// ReadCardList returns a list of Card values from the database. +// TODO: function needs testing. +func ReadCardList(db *bolt.DB, ids []int) ([]Card, error) { + data, err := database.ReadMany(db, database.CardBucket, ids) + if err != nil { + return nil, fmt.Errorf("unable to read card list from the database, %w", err) + } + + cards := make([]Card, len(data)) + + for i, d := range data { + buf := bytes.NewBuffer(d) + + decoder := gob.NewDecoder(buf) + + var c Card + + if err := decoder.Decode(&c); err != nil { + return nil, fmt.Errorf("unable to decode data, %w", err) + } + + cards[i] = c + } + + return cards, nil +} + // UpdateCard modifies an existing card and saves the modification to the database. -func UpdateCard(db *bolt.DB, id int, title, content string) error { - card, err := ReadCard(db, id) +func UpdateCard(db *bolt.DB, args UpdateCardArgs) error { + card, err := ReadCard(db, args.CardID) if err != nil { return err } - if len(title) > 0 { - card.Title = title + if len(args.NewTitle) > 0 { + card.Title = args.NewTitle } - if len(content) > 0 { - card.Content = content + if len(args.NewContent) > 0 { + card.Content = args.NewContent } if _, err := database.Write(db, database.CardBucket, &card); err != nil { @@ -162,9 +189,9 @@ func UpdateCard(db *bolt.DB, id int, title, content string) error { return nil } -// MoveCard moves a card between statuses. +// UpdateCardStatus moves a card between statuses. // TODO: finish implementation. -func MoveCard(db *bolt.DB, fromStatusID, toStatusID int) error { +func UpdateCardStatus(db *bolt.DB, args UpdateCardStatusArgs) error { return nil } diff --git a/internal/board/board_test.go b/internal/board/board_test.go index c4d685b..e41776c 100644 --- a/internal/board/board_test.go +++ b/internal/board/board_test.go @@ -22,7 +22,7 @@ func TestCardLifecycle(t *testing.T) { testDBPath := filepath.Join(projectDir, "test", "databases", "Board_TestCardLifecycle.db") os.Remove(testDBPath) - db, err := board.LoadBoard(testDBPath) + db, err := board.OpenProject(testDBPath) if err != nil { t.Fatalf("Unable to open the test database %s, %s.", testDBPath, err) } diff --git a/internal/database/database.go b/internal/database/database.go index 80862bf..0b0503e 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -51,55 +51,57 @@ func OpenDatabase(path string) (*bolt.DB, error) { return db, nil } -// WriteMany saves one or more statuses to the status bucket. -func WriteMany(db *bolt.DB, bucketName string, items []BoltItem) error { - if len(items) == 0 { - return nil - } - +// Read retrieves a Bolt item from a specified bucket and returns the data in bytes. +func Read(db *bolt.DB, bucketName string, id int) ([]byte, error) { bucket := []byte(bucketName) - err := db.Update(func(tx *bolt.Tx) error { + var data []byte + + if err := db.View(func(tx *bolt.Tx) error { b := tx.Bucket(bucket) if b == nil { return bucketNotExistError{bucket: string(bucket)} } - for _, i := range items { - var err error + data = b.Get([]byte(strconv.Itoa(id))) - if i.Id() < 1 { - var id uint64 - if id, err = b.NextSequence(); err != nil { - return fmt.Errorf("unable to generate ID, %w", err) - } - i.UpdateId(int(id)) - } + return nil + }); err != nil { + return []byte{}, fmt.Errorf("error while reading the Bolt item from the database, %w", err) + } - buf := new(bytes.Buffer) - encoder := gob.NewEncoder(buf) - if err = encoder.Encode(i); err != nil { - return fmt.Errorf("unable to encode data, %w", err) - } + return data, nil +} - if err = b.Put([]byte(strconv.Itoa(i.Id())), buf.Bytes()); err != nil { - return fmt.Errorf("unable to add the status to the bucket, %w", err) - } +// ReadMany reads one or more Bolt items from the specified bucket. +func ReadMany(db *bolt.DB, bucketName string, ids []int) ([][]byte, error) { + bucket := []byte(bucketName) + + var output [][]byte + + err := db.View(func(tx *bolt.Tx) error { + b := tx.Bucket(bucket) + + if b == nil { + return bucketNotExistError{bucket: bucketName} + } + + for _, v := range ids { + data := b.Get([]byte(strconv.Itoa(v))) + output = append(output, data) } return nil }) if err != nil { - return fmt.Errorf("error while saving the statuses to the database, %w", err) + return output, fmt.Errorf("error while retrieving the data from the database, %w", err) } - return nil + return output, nil } -// TODO: Create ReadMany - -// ReadAll retrieves all the statuses from the status bucket. +// ReadAll retrieves all the Bolt Items from the specified bucket. func ReadAll(db *bolt.DB, bucketName string) ([][]byte, error) { bucket := []byte(bucketName) @@ -109,7 +111,7 @@ func ReadAll(db *bolt.DB, bucketName string) ([][]byte, error) { b := tx.Bucket(bucket) if b == nil { - return bucketNotExistError{bucket: string(bucket)} + return bucketNotExistError{bucket: bucketName} } if err := b.ForEach(func(_, v []byte) error { @@ -168,27 +170,54 @@ func Write(db *bolt.DB, bucketName string, item BoltItem) (int, error) { return item.Id(), nil } -// Read retrieves a Bolt item from a specified bucket and returns the data in bytes. -func Read(db *bolt.DB, bucketName string, id int) ([]byte, error) { +// WriteMany saves one or more Bolt items to the status bucket. +func WriteMany(db *bolt.DB, bucketName string, items []BoltItem) ([]int, error) { + if len(items) == 0 { + return []int{}, nil + } + + ids := make([]int, len(items)) + bucket := []byte(bucketName) - var data []byte - - if err := db.View(func(tx *bolt.Tx) error { + err := db.Update(func(tx *bolt.Tx) error { b := tx.Bucket(bucket) if b == nil { return bucketNotExistError{bucket: string(bucket)} } - data = b.Get([]byte(strconv.Itoa(id))) + for i, v := range items { + var err error + + if v.Id() < 1 { + var id uint64 + if id, err = b.NextSequence(); err != nil { + return fmt.Errorf("unable to generate ID, %w", err) + } + v.UpdateId(int(id)) + } + + buf := new(bytes.Buffer) + encoder := gob.NewEncoder(buf) + if err = encoder.Encode(v); err != nil { + return fmt.Errorf("unable to encode data, %w", err) + } + + if err = b.Put([]byte(strconv.Itoa(v.Id())), buf.Bytes()); err != nil { + return fmt.Errorf("unable to add the Bolt Item to the %s bucket, %w", bucketName, err) + } + + ids[i] = v.Id() + } return nil - }); err != nil { - return []byte{}, fmt.Errorf("error while reading the Bolt item from the database, %w", err) + }) + if err != nil { + return nil, fmt.Errorf("error while saving the Bolt Items to the database, %w", err) } - return data, nil + return ids, nil } // dbPath returns the path to the database file. If a path is given then that is returned. Otherwise the default path is returned. diff --git a/internal/database/database_test.go b/internal/database/database_test.go index d7576d3..235547b 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -112,7 +112,7 @@ func testWriteStatusList(t *testing.T, db *bolt.DB) { boltItems[i] = &newStatusList[i] } - if err := database.WriteMany(db, database.StatusBucket, boltItems); err != nil { + if _, err := database.WriteMany(db, database.StatusBucket, boltItems); err != nil { t.Fatalf("An error occurred whilst writing the initial status list to the database, %s", err) } } @@ -175,7 +175,7 @@ func testReadStatusList(t *testing.T, db *bolt.DB) { } } -func TestReadAndWriteCard(t *testing.T) { +func TestReadAndWriteCards(t *testing.T) { t.Parallel() var db *bolt.DB @@ -198,20 +198,41 @@ func TestReadAndWriteCard(t *testing.T) { _ = db.Close() }() - cardID := testWriteCard(t, db) - testReadCard(t, db, cardID) -} - -func testWriteCard(t *testing.T, db *bolt.DB) int { - t.Helper() - - newCard := board.Card{ + singleCard := board.Card{ ID: -1, Title: "A test task.", Content: "This task should be completed.", } - cardID, err := database.Write(db, database.CardBucket, &newCard) + singleCardID := testWriteOneCard(t, db, singleCard) + testReadOneCard(t, db, singleCardID) + + manyCards := []board.Card{ + { + ID: -1, + Title: "Test card A.", + Content: "This is test card A.", + }, + { + ID: -1, + Title: "Test card B.", + Content: "This is test card B.", + }, + { + ID: -1, + Title: "Test card C.", + Content: "This is test card C.", + }, + } + + manyCardIDs := testWriteManyCard(t, db, manyCards) + testReadManyCards(t, db, manyCardIDs) +} + +func testWriteOneCard(t *testing.T, db *bolt.DB, card board.Card) int { + t.Helper() + + cardID, err := database.Write(db, database.CardBucket, &card) if err != nil { t.Fatalf("An error occurred whilst writing the card to the database, %s", err) } @@ -219,7 +240,7 @@ func testWriteCard(t *testing.T, db *bolt.DB) int { return cardID } -func testReadCard(t *testing.T, db *bolt.DB, cardID int) { +func testReadOneCard(t *testing.T, db *bolt.DB, cardID int) { t.Helper() data, err := database.Read(db, database.CardBucket, cardID) @@ -250,6 +271,72 @@ func testReadCard(t *testing.T, db *bolt.DB, cardID int) { } } +func testWriteManyCard(t *testing.T, db *bolt.DB, cards []board.Card) []int { + t.Helper() + + boltItems := make([]database.BoltItem, len(cards)) + + for i := range cards { + boltItems[i] = &cards[i] + } + + ids, err := database.WriteMany(db, database.CardBucket, boltItems) + if err != nil { + t.Fatalf("An error occurred whilst writing many cards to the database, %s", err) + } + + return ids +} + +func testReadManyCards(t *testing.T, db *bolt.DB, cardIDs []int) { + t.Helper() + + data, err := database.ReadMany(db, database.CardBucket, cardIDs) + if err != nil { + t.Fatalf("An error occurred whilst reading the data from the database, %s", err) + } + + got := make([]board.Card, len(data)) + + for i, d := range data { + buf := bytes.NewBuffer(d) + + decoder := gob.NewDecoder(buf) + + var c board.Card + + if err := decoder.Decode(&c); err != nil { + t.Fatalf("An error occurred whilst decoding data, %s", err) + } + + got[i] = c + } + + want := []board.Card{ + { + ID: 2, + Title: "Test card A.", + Content: "This is test card A.", + }, + { + ID: 3, + Title: "Test card B.", + Content: "This is test card B.", + }, + { + ID: 4, + Title: "Test card C.", + Content: "This is test card C.", + }, + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("Unexpected list of cards read from the database: got %+v, want %+v", got, want) + } else { + t.Logf("Expected list of cards read from the database: got %+v", got) + } +} + func projectRoot() (string, error) { cwd, err := os.Getwd() if err != nil { diff --git a/internal/ui/app.go b/internal/ui/app.go new file mode 100644 index 0000000..a4df026 --- /dev/null +++ b/internal/ui/app.go @@ -0,0 +1,154 @@ +package ui + +import ( + "fmt" + + "forge.dananglin.me.uk/code/dananglin/canal/internal/board" + "github.com/rivo/tview" + bolt "go.etcd.io/bbolt" +) + +type shiftDirection int + +const ( + shiftLeft shiftDirection = iota + shiftRight +) + +const ( + mainPage string = "main" + quitPage string = "quit" + addPage string = "add" +) + +// App does some magical stuff. +type App struct { + *tview.Application + + columns []column + flex *tview.Flex + pages *tview.Pages + focusedColumn int + db *bolt.DB +} + +// shutdown shuts down the application. +func (a *App) shutdown() { + a.closeDB() + a.Stop() +} + +// closeDB closes the BoltDB database. +func (a *App) closeDB() { + if a.db != nil { + _ = a.db.Close() + } +} + +// openProject opens the kanban project. +func (a *App) openProject(path string) error { + if a.db != nil && len(a.db.Path()) > 0 { + a.db.Close() + } + + db, err := board.OpenProject(path) + if err != nil { + return fmt.Errorf("unable to load board, %w", err) + } + + a.db = db + + if err = a.refresh(); err != nil { + return err + } + + return nil +} + +// refresh refreshes the UI. +func (a *App) refresh() error { + statusList, err := board.ReadStatusList(a.db) + if err != nil { + return fmt.Errorf("unable to get the status list, %w", err) + } + + a.updateBoard(statusList) + + a.setColumnFocus() + + return nil +} + +func (a *App) updateBoard(statusList []board.Status) error { + a.flex.Clear() + columns := make([]column, len(statusList)) + + for i := range statusList { + columns[i] = a.newColumn(statusList[i].ID, statusList[i].Name) + + if len(statusList[i].CardIds) > 0 { + cards, err := board.ReadCardList(a.db, statusList[i].CardIds) + if err != nil { + return fmt.Errorf("unable to get the card list. %w", err) + } + + for _, c := range cards { + columns[i].cards.AddItem(fmt.Sprintf("[%d] %s", c.Id(), c.Title), "", 0, nil) + } + } + + a.flex.AddItem(columns[i].cards, 0, 1, false) + } + + a.columns = columns + + return nil +} + +func (a *App) shiftColumnFocus(s shiftDirection) { + switch s { + case shiftRight: + if a.focusedColumn == len(a.columns)-1 { + a.focusedColumn = 0 + } else { + a.focusedColumn++ + } + case shiftLeft: + if a.focusedColumn == 0 { + a.focusedColumn = len(a.columns) - 1 + } else { + a.focusedColumn-- + } + } + + a.setColumnFocus() +} + +func (a *App) setColumnFocus() { + a.SetFocus(a.columns[a.focusedColumn].cards) +} + +// newCard creates a new card and saves it to the database. +func (a *App) newCard(title, content string) error { + args := board.CardArgs{ + NewTitle: title, + NewContent: content, + } + + if err := board.CreateCard(a.db, args); err != nil { + return fmt.Errorf("unable to create card, %w", err) + } + + a.refresh() + + return nil +} + +// TODO: Move 'Add' to the centre of the app +// TODO: Customize list primitive or create a new one +// TODO: If customizing exisiing list primitive, wrap list around a column type. Add statusID to it. +// TODO: Update card status (card ID, oldStatus, newStatus) + +//func viewCard() error { +//return nil +//} diff --git a/internal/ui/column.go b/internal/ui/column.go new file mode 100644 index 0000000..e30a023 --- /dev/null +++ b/internal/ui/column.go @@ -0,0 +1,41 @@ +package ui + +import ( + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +type column struct { + statusID int + statusName string + cards *tview.List +} + +func (a *App) newColumn(statusID int, statusName string) column { + l := tview.NewList() + + l.SetBorder(true) + l.ShowSecondaryText(false) + l.SetTitle(" " + statusName + " ") + l.SetHighlightFullLine(true) + l.SetSelectedFocusOnly(true) + l.SetWrapAround(false) + + l.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Rune() == 'h' || event.Key() == tcell.KeyLeft { + a.shiftColumnFocus(shiftLeft) + } else if event.Rune() == 'l' || event.Key() == tcell.KeyRight { + a.shiftColumnFocus(shiftRight) + } + + return event + }) + + c := column{ + statusID: statusID, + statusName: statusName, + cards: l, + } + + return c +} diff --git a/internal/ui/init.go b/internal/ui/init.go new file mode 100644 index 0000000..d691d4e --- /dev/null +++ b/internal/ui/init.go @@ -0,0 +1,100 @@ +package ui + +import ( + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +// NewApp returns a new App. +func NewApp() App { + a := App{ + Application: tview.NewApplication(), + pages: tview.NewPages(), + flex: tview.NewFlex(), + focusedColumn: 0, + } + + initApp(&a) + + return a +} + +// initApp initialises App +func initApp(a *App) { + a.pages.AddPage(mainPage, a.flex, true, true) + + quit := newQuitModal(a) + a.pages.AddPage(quitPage, quit, false, false) + + add := newAddForm(a) + a.pages.AddPage(addPage, add, false, false) + + a.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Rune() == 'q' { + a.pages.ShowPage(quitPage) + a.SetFocus(quit) + } else if event.Rune() == 'o' { + a.openProject("") + } else if event.Rune() == 'a' { + a.pages.ShowPage(addPage) + a.SetFocus(add) + } + + return event + }) + + a.SetRoot(a.pages, true) +} + +// newQuitModal returns a new modal for the user's confirmation +// for quitting the application. +func newQuitModal(a *App) *tview.Modal { + + quit := *tview.NewModal() + + quitDoneFunc := func(_ int, buttonLabel string) { + switch buttonLabel { + case "Quit": + a.shutdown() + default: + a.pages.SwitchToPage("main") + a.setColumnFocus() + } + } + + quit.SetText("Do you want to quit the application?"). + AddButtons([]string{"Quit", "Cancel"}). + SetDoneFunc(quitDoneFunc) + + return &quit +} + +// newAddForm creates a new Form primitive for creating a new card. +func newAddForm(a *App) *tview.Form { + add := tview.NewForm() + + titleField := "Title" + + add.AddInputField(titleField, "", 0, nil, nil) + + add.AddButton("Save", func() { + title := add.GetFormItemByLabel(titleField).(*tview.InputField).GetText() + // TODO: error value needs handling + _ = a.newCard(title, "") + add.GetFormItemByLabel(titleField).(*tview.InputField).SetText("") + a.pages.SwitchToPage(mainPage) + a.setColumnFocus() + }) + + add.AddButton("Cancel", func() { + a.pages.SwitchToPage(mainPage) + add.GetFormItemByLabel(titleField).(*tview.InputField).SetText("") + a.setColumnFocus() + }) + + add.SetBorder(true) + + add.SetTitle(" New Card ") + + return add +} diff --git a/internal/ui/ui.go b/internal/ui/ui.go deleted file mode 100644 index 8bddc19..0000000 --- a/internal/ui/ui.go +++ /dev/null @@ -1,136 +0,0 @@ -// Right now this will be a very simple, scuffed interface. -package ui - -import ( - "fmt" - - "forge.dananglin.me.uk/code/dananglin/canal/internal/board" - "github.com/eiannone/keyboard" - bolt "go.etcd.io/bbolt" -) - -func App() error { - var db *bolt.DB - - var err error - - keysEvents, err := keyboard.GetKeys(10) - if err != nil { - return fmt.Errorf("unable to create the keysEvent channel, %w", err) - } - - defer func() { - _ = keyboard.Close() - }() - - fmt.Println(usage()) - -app: - for { - event := <-keysEvents - if event.Err != nil { - return fmt.Errorf("keys event error: %w", event.Err) - } - - switch event.Rune { - case 'q': - break app - case 'r': - if err = refresh(db); err != nil { - fmt.Printf("Error: Unable to refresh board, %s\n", err) - } - case 'a': - if err = newCard(db); err != nil { - fmt.Printf("Error: Unable to add a card, %s\n", err) - } - case 'v': - if err = viewCard(db, 1); err != nil { - fmt.Printf("Error: Unable to view card, %s\n", err) - } - case 'o': - // TODO: How do we close the db? - db, err = openProject("") - if err != nil { - fmt.Printf("Error: Unable to open the project, %s\n", err) - } - default: - fmt.Println("Error: Unknown key event.") - } - } - - db.Close() - - return nil -} - -func refresh(db *bolt.DB) error { - statusList, err := board.ReadStatusList(db) - if err != nil { - return fmt.Errorf("unable to get the status list, %w", err) - } - - fmt.Printf("--------------------\n") - - for _, s := range statusList { - fmt.Printf("Status ID: %d\nStatus Name: \"%s\"\nCard IDs: %v\n\n", s.ID, s.Name, s.CardIds) - } - - fmt.Printf("--------------------\n\n\n") - - return nil -} - -func newCard(db *bolt.DB) error { - title := "A card title" - - content := "As a user, this is a ticket for me.\nAs a user, I want to close it." - - if err := board.CreateCard(db, title, content); err != nil { - return fmt.Errorf("unable to create card, %w", err) - } - - fmt.Println("Sample card created successfully.") - - return nil -} - -func viewCard(db *bolt.DB, id int) error { - card, err := board.ReadCard(db, id) - if err != nil { - return fmt.Errorf("unable to read card, %w", err) - } - - fmt.Printf("====================\n") - fmt.Printf("[%d] %s\n", card.ID, card.Title) - fmt.Printf("--------------------\n") - fmt.Println(card.Content) - fmt.Printf("====================\n") - - return nil -} - -func openProject(path string) (*bolt.DB, error) { - db, err := board.LoadBoard(path) - if err != nil { - return nil, fmt.Errorf("unable to load board, %w", err) - } - - if err = refresh(db); err != nil { - return nil, err - } - - return db, nil -} - -func usage() string { - usage := ` -Press 'o' to open the project -Press 'r' to refresh the board -Press 'a' to add a sample card -Press 'v' to view the first sample card -Press 'q' to quit - -` - - return usage -}