diff --git a/src_code/go_src/cmd/tui/tui_create_author.go b/src_code/go_src/cmd/tui/tui_create_author.go new file mode 100644 index 0000000..a3cb08b --- /dev/null +++ b/src_code/go_src/cmd/tui/tui_create_author.go @@ -0,0 +1,226 @@ +package tui + +// A simple example demonstrating the use of multiple text input components +// from the Bubbles component library. + +import ( + "fmt" + "os" + "strings" + + //"github.com/charmbracelet/bubbles/cursor" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var ( + focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) + cursorStyle = focusedStyle + noStyle = lipgloss.NewStyle() + cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) + focusedButton = focusedStyle.Render("[ Submit ]") + focusedExclude = focusedStyle.Render("[ Exclude ]") + blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Submit")) + excludeButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Exclude")) +) + +type model_ca struct { + focusIndex int + inputs []textinput.Model + quitting bool + exclude bool +} + +func initialModel() model_ca { + m := model_ca{ + inputs: make([]textinput.Model, 5), + } + + var t textinput.Model + for i := range m.inputs { + t = textinput.New() + t.Cursor.Style = cursorStyle + t.CharLimit = 32 + + switch i { + case 0: + t.Placeholder = "shortname (e.g. jo)" + t.Focus() + t.PromptStyle = focusedStyle + t.TextStyle = focusedStyle + case 1: + t.Placeholder = "Long name (e.g. JohnDoe)" + case 2: + t.Placeholder = "Username (e.g. JohnDoe-gh)" + case 3: + t.Placeholder = "Email (e.g. JohnDoe@domain.do" + case 4: + t.Placeholder = "Group tags (e.g. gr1|gr2)" + } + + m.inputs[i] = t + } + + return m +} + +func (m model_ca) Init() tea.Cmd { + return textinput.Blink +} + +func (m model_ca) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "esc": + m.inputs = nil + return m, tea.Quit + + // Set focus to next input + case "tab", "shift+tab", "enter", "up", "down": + s := msg.String() + + // Did the user press enter while the submit button was focused? + // If so, exit. + if s == "enter" && m.focusIndex == len(m.inputs)+1 { + m.quitting = true + return m, tea.Quit + } else if s == "enter" && m.focusIndex == len(m.inputs) { + // toggle exclude + m.exclude = !m.exclude + return m, nil + } + + // Cycle indexes + if s == "up" || s == "shift+tab" { + m.focusIndex-- + } else { + m.focusIndex++ + } + + if m.focusIndex > len(m.inputs)+1 { + m.focusIndex = 0 + } else if m.focusIndex < 0 { + m.focusIndex = len(m.inputs) + } + + cmds := make([]tea.Cmd, len(m.inputs)) + for i := 0; i <= len(m.inputs)-1; i++ { + if i == m.focusIndex { + // Set focused state + cmds[i] = m.inputs[i].Focus() + m.inputs[i].PromptStyle = focusedStyle + m.inputs[i].TextStyle = focusedStyle + continue + } + // Remove focused state + m.inputs[i].Blur() + m.inputs[i].PromptStyle = noStyle + m.inputs[i].TextStyle = noStyle + } + + return m, tea.Batch(cmds...) + } + } + + // Handle character input and blinking + cmd := m.updateInputs(msg) + + return m, cmd +} + +func (m *model_ca) updateInputs(msg tea.Msg) tea.Cmd { + cmds := make([]tea.Cmd, len(m.inputs)) + + // Only text inputs with Focus() set will respond, so it's safe to simply + // update all of them here without any further logic. + for i := range m.inputs { + m.inputs[i], cmds[i] = m.inputs[i].Update(msg) + } + + return tea.Batch(cmds...) +} + +func (m model_ca) View() string { + if m.quitting { + return "" + } + + var b strings.Builder + + for i := range m.inputs { + b.WriteString(m.inputs[i].View()) + if i < len(m.inputs)-1 { + b.WriteRune('\n') + } + } + + button := &blurredButton + if m.focusIndex == len(m.inputs)+1 { + button = &focusedButton + } + exclude := &excludeButton + if m.focusIndex == len(m.inputs) { + exclude = &focusedExclude + } + + if m.exclude { + fmt.Fprintf(&b, "\n\n%s: [X]\n\n", *exclude) + } else { + fmt.Fprintf(&b, "\n\n%s: [ ]\n\n", *exclude) + } + + fmt.Fprintf(&b, "\n\n%s\n\n", *button) + + b.WriteString(cursorModeHelpStyle.Render()) + + return b.String() +} + +func Entry_CA() { + m, err := tea.NewProgram(initialModel()).Run() + if err != nil { + fmt.Printf("could not start program: %s\n", err) + os.Exit(1) + } + + + + if len(m.(model_ca).inputs) > 0 && + m.(model_ca).inputs[0].Value() != "" && + m.(model_ca).inputs[1].Value() != "" && + m.(model_ca).inputs[2].Value() != "" && + m.(model_ca).inputs[3].Value() != "" { + f, err := os.OpenFile("author_file", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + panic(err) + } + + defer f.Close() + + sb := strings.Builder{} + + sb.WriteString(fmt.Sprintf("%s|%s|%s|%s", + m.(model_ca).inputs[0].Value(), + m.(model_ca).inputs[1].Value(), + m.(model_ca).inputs[2].Value(), + m.(model_ca).inputs[3].Value())) + + if m.(model_ca).exclude { + sb.WriteString(fmt.Sprintf("|%s", "ex")) + } + + if m.(model_ca).inputs[4].Value() != "" { + sb.WriteString(fmt.Sprintf(";;%s", m.(model_ca).inputs[4].Value())) + } + + sb.WriteRune('\n') + + if _, err = f.WriteString(sb.String()); err != nil { + panic(err) + } + } + +} \ No newline at end of file diff --git a/src_code/go_src/cmd/tui/tui_list.go b/src_code/go_src/cmd/tui/tui_list.go new file mode 100644 index 0000000..6c68b7c --- /dev/null +++ b/src_code/go_src/cmd/tui/tui_list.go @@ -0,0 +1,278 @@ +package tui + +import ( + "fmt" + "io" + "main/src_code/go_src/cmd/utils" + "os" + "sort" + "strings" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +const listHeight = 14 + +var ( + titleStyle = lipgloss.NewStyle().MarginLeft(2) + itemStyle = lipgloss.NewStyle().PaddingLeft(4) + selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) + highlightStyle = lipgloss.NewStyle().PaddingLeft(4).Background(lipgloss.Color("236")).Foreground(lipgloss.Color("17")) + selectedHighlightStyle = lipgloss.NewStyle().PaddingLeft(2).Background(lipgloss.Color("236")).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 item string + +var selected = map[string]item{} + +var negation = false + +type listKeyMap struct { + selectAll key.Binding + negation key.Binding + groupSelect key.Binding + selectOne key.Binding + createAuthor key.Binding +} + +func newListKeyMap() *listKeyMap { + return &listKeyMap{ + selectAll: key.NewBinding( + key.WithKeys("A"), + key.WithHelp("A", "Add all authors"), + ), + negation: key.NewBinding( + key.WithKeys("n"), + key.WithHelp("n", "Toggle negation and select author"), + ), + groupSelect: key.NewBinding( + key.WithKeys("f"), + key.WithHelp("f", "Select group"), + ), + selectOne: key.NewBinding( + key.WithKeys(" "), + key.WithHelp("space", "Select author"), + ), + createAuthor: key.NewBinding( + key.WithKeys("C"), + key.WithHelp("C", "Create new author"), + ), + } +} + + +//TODO: Try and add filtering later down the line +func (i item) FilterValue() string { return string(i) } + +type itemDelegate struct{} + +func (d itemDelegate) Height() int { return 1 } +func (d itemDelegate) Spacing() int { return 0 } +func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } +func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(item) + if !ok { + return + } + + + str := fmt.Sprintf("%d. %s", index+1, i) + + //TODO: add negation style where all items are flipped in selection + + fn := itemStyle.Render + if _, ok := selected[string(i)]; ok { + fn = func(s ...string) string { + base := strings.Join(s, " ") + if negation { + base = base + " ^" + } + if index == m.Index() { + return selectedHighlightStyle.Render("> " + base + " [X]") + } else { + return highlightStyle.Render(base + " [X]") + } + } + } else { + if index == m.Index() { + fn = func(s ...string) string { + return selectedItemStyle.Render("> " + strings.Join(s, " ")) + } + } + } + + + fmt.Fprint(w, fn(str)) +} + +type model struct { + list list.Model + keys *listKeyMap + quitting bool +} + +func (m model) Init() tea.Cmd { + return nil +} + +func selectToggle(i item) { + if _, ok := selected[string(i)]; ok { + delete(selected, string(i)) + toggleNegation() + } else { + selected[string(i)] = i + } +} + +func toggleNegation() { + if len(selected) == 0 { + negation = false + } +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.list.SetWidth(msg.Width) + return m, nil + + // If filtering is enabled, skip key handling + case tea.KeyMsg: + if m.list.FilterState() == list.Filtering { + break + } + // Handle keys from keyList (help menu) + switch { + case key.Matches(msg, m.keys.negation): + i, ok := m.list.SelectedItem().(item) + if ok { + negation = true + selectToggle(i) + } + + case key.Matches(msg, m.keys.selectOne): + i, ok := m.list.SelectedItem().(item) + if ok { + selectToggle(i) + } + + case key.Matches(msg, m.keys.selectAll): + //TODO: maybe look at behavior of this when auth are already selected + negation = false + for _, i := range m.list.Items() { + selectToggle(i.(item)) + } + + case key.Matches(msg, m.keys.groupSelect): + // group code goes here + + case key.Matches(msg, m.keys.createAuthor): + Entry_CA() + return m, tea.ClearScreen + } + // extra key options + switch keypress := msg.String(); keypress { + case "q", "ctrl+c", "esc": + m.quitting = true + selected = nil + return m, tea.Quit + + case "enter": + m.quitting = true + return m, tea.Quit + } + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +func (m model) View() string { + if m.quitting { + return "" //quitTextStyle.Render(strings.Join(m.choice, " ")) + } + + return "\n" + m.list.View() +} + +//TODO: pass list in as a param to allow for group selection using same template +func Entry() []string { + items := []list.Item{} + dupProtect := map[string]string{} + + listKeys := newListKeyMap() + + // Add items to the list + for short, user := range utils.Users { + // if items already contains the user, skip it + str_user := user.Username + " - " + user.Email + if _, ok := dupProtect[str_user]; ok { + continue + } + items = append(items, item(str_user)) + dupProtect[str_user] = short + } + + sort.Slice(items, func(i, j int) bool { + return items[i].(item) < items[j].(item) + }) + + const defaultWidth = 20 + + l := list.New(items, itemDelegate{}, defaultWidth, listHeight) + l.Title = "Select authors to add to commit" + l.SetShowStatusBar(false) + l.SetFilteringEnabled(true) // Enable filtering + l.Styles.Title = titleStyle + l.Styles.PaginationStyle = paginationStyle + l.AdditionalShortHelpKeys = // Add help keys (main page) + func() []key.Binding { + return []key.Binding{ + listKeys.selectOne, + } + } + l.AdditionalFullHelpKeys = // Add help keys (help menu) + func() []key.Binding { + return []key.Binding{ + listKeys.selectAll, + listKeys.negation, + listKeys.groupSelect, + listKeys.createAuthor, + } + } + l.Styles.HelpStyle = helpStyle + + m := model{list: l, keys: listKeys} + + f, err := tea.NewProgram(m).Run() + if err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } + + // Assert the final tea.Model to our local model and print the choice. + + output := []string{} + + for i := range selected { + short := dupProtect[i] + if negation { + short = "^" + short + } + + output = append(output, short) + } + + if _, ok := f.(model); ok && len(output) > 0 { + return output + } + return nil +} \ No newline at end of file