diff --git a/README.md b/README.md index 3c03060..dc27e85 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,4 @@ Iterates over note cards. Currently just to learn BubbleTea, may turn into a rea * To change selection use arrow keys (or j/k). * To enable a card to be selected, press enter or space. * To switch to draw mode, press D. +* To create a card, press C. diff --git a/go.mod b/go.mod index 7d338e7..7eb042c 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,11 @@ go 1.19 require ( github.com/charmbracelet/bubbles v0.14.0 github.com/charmbracelet/bubbletea v0.22.1 + github.com/charmbracelet/lipgloss v0.5.0 ) require ( github.com/atotto/clipboard v0.1.4 // indirect - github.com/charmbracelet/lipgloss v0.5.0 // indirect 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.16 // indirect @@ -20,6 +20,7 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect github.com/rivo/uniseg v0.4.2 // indirect + github.com/sahilm/fuzzy v0.1.0 // indirect golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect diff --git a/go.sum b/go.sum index 9364331..137ad8c 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,7 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 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= diff --git a/gui/create.go b/gui/create.go index aaa9c07..5ed1e32 100644 --- a/gui/create.go +++ b/gui/create.go @@ -2,6 +2,7 @@ package gui import ( "fmt" + "gitea.tyrel.dev/tyrel/itor/models" tea "github.com/charmbracelet/bubbletea" ) @@ -20,14 +21,20 @@ func (m *model) createCurrent() { front := m.createInputs[0].Value() back := m.createInputs[1].Value() + if front == "" || back == "" { + return + } + // Create a new card - card := Card{ + card := models.Card{ Front: front, Back: back, } // Add it to the deck m.deck = append(m.deck, card) + m.selectList.InsertItem(len(m.selectList.Items())+1, card) + // Clear the inputs for i, _ := range m.createInputs { m.createInputs[i].Reset() diff --git a/gui/draw.go b/gui/draw.go index 6daf76c..3283184 100644 --- a/gui/draw.go +++ b/gui/draw.go @@ -1,6 +1,15 @@ package gui -import tea "github.com/charmbracelet/bubbletea" +import ( + "fmt" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +const hotPink = lipgloss.Color("#FF06B7") + +var hotPinkInput = lipgloss.NewStyle().Foreground(hotPink) func (m *model) updateDraw(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { @@ -16,12 +25,28 @@ func (m *model) updateDraw(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, nil } -func (m *model) viewDraw() string { - s := "Draw Mode:\n\n" - s += m.cardListToString() +func (m *model) viewDraw() string { + s := fmt.Sprintf("Draw Mode (%d cards):\n\n", len(m.selected)) + + s += fmt.Sprintf("[%s] [%s] [%s] [%s]\n", + hotPinkInput.Width(7).Render("1: View"), + hotPinkInput.Width(10).Render("2: Correct"), + hotPinkInput.Width(12).Render("3: Incorrect"), + hotPinkInput.Width(7).Render("4: Skip"), + ) s += "\nPress [s] to go back to select\nPress [q] to quit." return s } + +func (m *model) cardListToString() string { + s := "" + for i, card := range m.deck { + if _, ok := m.selected[i]; ok { + s += fmt.Sprintf("%s [%s]\n", card.Front, card.Back) + } + } + return s +} diff --git a/gui/gui.go b/gui/gui.go index 80b5dc2..a7476f7 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -17,7 +17,8 @@ const ( func (m *model) Init() tea.Cmd { // Just return `nil`, which means "no I/O right now, please." - return nil + cmd := tea.EnterAltScreen + return cmd } func (m *model) switchMode(mode Mode) { @@ -32,24 +33,14 @@ func (m *model) switchMode(mode Mode) { } } -func (m *model) cardListToString() string { - s := "" - for i, card := range m.deck { - if _, ok := m.selected[i]; ok { - s += fmt.Sprintf("%s [%s]\n", card.Front, card.Back) - } - } - return s -} - func (m *model) View() string { if m.mode == Draw { return m.viewDraw() } else if m.mode == Create { return m.viewCreate() + } else { + return m.viewSelect() } - // Select is default mode - return m.viewSelect() } func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -57,9 +48,9 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.updateDraw(msg) } else if m.mode == Create { return m.updateCreate(msg) + } else { + return m.updateSelect(msg) } - // Select is default mode - return m.updateSelect(msg) } func Run() { initial := initialModel() diff --git a/gui/model.go b/gui/model.go index 834ed05..fc25846 100644 --- a/gui/model.go +++ b/gui/model.go @@ -1,25 +1,23 @@ package gui import ( - "time" - + "gitea.tyrel.dev/tyrel/itor/models" + "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textinput" ) -type Card struct { - Front string - Back string - LastCorrect time.Time -} - type model struct { - deck []Card // items on the to-do list - selectCardIndex int // which to-do list item our selectCardIndex is pointing at - selected map[int]struct{} // which to-do items are selected - createInputs []textinput.Model - createdInputIndex int - mode Mode - err error + deck []models.Card // items on the to-do list + + selectCardIndex int // which to-do list item our selectCardIndex is pointing at + selected map[int]struct{} // which to-do items are selected + selectList list.Model + + createInputs []textinput.Model // Inputs for front and back, is an array because we change position by an index + createdInputIndex int // holds an index for which input is active editing in create mode + + mode Mode + err error } func initialModel() model { @@ -34,12 +32,31 @@ func initialModel() model { back.CharLimit = 256 back.Width = 80 + deck := []models.Card{ + models.NewCard("Hello (JP)", "Konnichi wa"), + models.NewCard("Hello (SP)", "Hola"), + models.NewCard("Hello (DE)", "Guten Tag"), + } + + var items []list.Item + + for _, card := range deck { + items = append(items, card) + } + + selectedList := list.New(items, itemDelegate{}, 80, 8) + selectedList.Title = "Card selection" + selectedList.SetShowStatusBar(false) + selectedList.SetFilteringEnabled(false) + selectedList.SetShowHelp(false) + selectedList.SetFilteringEnabled(false) + selectedList.Styles.Title = titleStyle + selectedList.Styles.PaginationStyle = paginationStyle + selectedList.Styles.HelpStyle = helpStyle + return model{ - deck: []Card{ - {Front: "Hello (JP)", Back: "Konnichi wa"}, - {Front: "Hello (SP)", Back: "Hola"}, - {Front: "Hello (DE)", Back: "Guten Tag"}, - }, + deck: deck, + selectList: selectedList, createInputs: []textinput.Model{front, back}, selected: make(map[int]struct{}), mode: Select, diff --git a/gui/select.go b/gui/select.go index 5af11d3..e431682 100644 --- a/gui/select.go +++ b/gui/select.go @@ -2,11 +2,64 @@ package gui import ( "fmt" + "gitea.tyrel.dev/tyrel/itor/models" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "io" ) +var ( + titleStyle = lipgloss.NewStyle().MarginLeft(2) + itemStyle = lipgloss.NewStyle().PaddingLeft(4) + selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) + paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) + helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) + quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) +) + +type itemDelegate struct{} + +func (d itemDelegate) Height() int { + return 1 +} + +func (d itemDelegate) Spacing() int { + return 0 +} + +func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { + return nil +} + +func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(models.Card) + if !ok { + return + } + + checked := " " // not selected + if i.Selected { + checked = "✓" // selected! + } + + str := fmt.Sprintf("[%s] %d. %s", checked, index+1, i.ToString()) + + fn := itemStyle.Render + if index == m.Index() { + fn = func(s string) string { + return selectedItemStyle.Render("> " + s) + } + } + + fmt.Fprint(w, fn(str)) +} + func (m *model) updateSelect(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.selectList.SetWidth(msg.Width) + return m, nil // Is it a key press? case tea.KeyMsg: @@ -35,43 +88,62 @@ func (m *model) updateSelect(msg tea.Msg) (tea.Model, tea.Cmd) { // The "enter" key and the spacebar (a literal space) toggle // the selected state for the item that the selectCardIndex is pointing at. case "enter", " ": - _, ok := m.selected[m.selectCardIndex] + card, ok := m.selectList.SelectedItem().(models.Card) if ok { - delete(m.selected, m.selectCardIndex) - } else { - m.selected[m.selectCardIndex] = struct{}{} + idx := SliceIndex(len(m.deck), func(i int) bool { return m.deck[i].Id == card.Id }) + fmt.Printf("%d IS THE CARD: %s", idx, card.Front) + if _, ok := m.selected[idx]; ok { + delete(m.selected, idx) + m.deck[idx].Selected = false + } else { + m.selected[idx] = struct{}{} + m.deck[idx].Selected = true + } + m.selectList.SetItem(idx, m.deck[idx]) } } } - return m, nil + var cmd tea.Cmd + m.selectList, cmd = m.selectList.Update(msg) + return m, cmd } func (m *model) viewSelect() string { - s := "Select deck to query:\n\n" + s := "Select cards to draw:\n\n" - // Iterate over our choices - for i, choice := range m.deck { - - // Is the selectCardIndex pointing at this choice? - cursor := " " // no selectCardIndex - if m.selectCardIndex == i { - cursor = ">" // selectCardIndex! - } - - // Is this choice selected? - checked := " " // not selected - if _, ok := m.selected[i]; ok { - checked = "✓" // selected! - } - - // Render the row - s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice.Front) - } + //// Iterate over our choices + //for i, choice := range m.deck { + // + // // Is the selectCardIndex pointing at this choice? + // cursor := " " // no selectCardIndex + // if m.selectCardIndex == i { + // cursor = ">" // selectCardIndex! + // } + // + // // Is this choice selected? + // checked := " " // not selected + // if _, ok := m.selected[i]; ok { + // checked = "✓" // selected! + // } + // + // // Render the row + // s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice.Front) + //} + s += m.selectList.View() // The footer - s += "\nPress [up/down/j/k] to Move up and down.\nPress [enter/space] to toggle selection.\n" + s += "\nPress [↑/k] move up.\nPress [↓/j] move down.\nPress [enter/space] to toggle selection.\n" s += "\nPress [c] to Create.\nPress [d] to Draw.\nPress [q] to quit.\n" // Send the UI for rendering return s } + +func SliceIndex(limit int, predicate func(i int) bool) int { + for i := 0; i < limit; i++ { + if predicate(i) { + return i + } + } + return -1 +} diff --git a/models/card.go b/models/card.go new file mode 100644 index 0000000..8790470 --- /dev/null +++ b/models/card.go @@ -0,0 +1,34 @@ +package models + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "time" +) + +type Card struct { + Id string + Front string + Back string + LastCorrect time.Time + Selected bool +} + +func NewCard(front, back string) Card { + id := GetMD5Hash(fmt.Sprintf("%s|%s", front, back)) + return Card{Front: front, Back: back, Id: id} +} + +func (c Card) FilterValue() string { + return "" +} + +func (c Card) ToString() string { + return c.Front +} + +func GetMD5Hash(text string) string { + hash := md5.Sum([]byte(text)) + return hex.EncodeToString(hash[:]) +}