L23646QEMZPB366O52BQACSTIXR5K5ARMCTCT3IWL7JIMSFMQB4AC
package main
import (
"context"
"fmt"
db "github.com/Asfolny/protastim/internal/database"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/bubbles/spinner"
)
type taskModel struct {
id int64
config *config
data *db.Task
modal tea.Model
err error
loading bool
editing bool
spinner spinner.Model
}
func newTask(config *config, id int64) taskModel {
m := taskModel{
id: id,
config: config,
loading: true,
spinner: config.newSpinner(),
}
return m
}
func (m taskModel) fetchTask() tea.Msg {
row, err := m.config.queries.GetTask(context.Background(), m.id)
if err != nil {
return errMsg{err}
}
return row
}
func (m taskModel) Init() tea.Cmd {
return tea.Batch(m.fetchTask, m.spinner.Tick)
}
func (m taskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case db.Task:
m.data = &msg
m.loading = false
return m, nil
case errMsg:
m.err = msg
m.loading = false
return m, tea.Quit
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
case tea.KeyMsg:
switch msg.String() {
case "ctrl+n":
m.modal = newTaskEdit(m.config, nil)
m.editing = true
return m, m.modal.Init()
}
}
if m.editing {
var cmd tea.Cmd
m.modal, cmd = m.modal.Update(msg)
return m, cmd
}
return m, nil
}
func (m taskModel) View() string {
if m.loading {
return fmt.Sprintf("\n%s Loading\n\n", m.spinner.View())
}
if m.err != nil {
return fmt.Sprintf("\nFailed fetching task: %v\n\n", m.err)
}
if m.editing {
return m.modal.View()
}
return fmt.Sprintf(
"Name: %s\nDescription:\n%s\nStatus: %s\n",
m.data.Name,
m.data.Description.String,
m.data.Status,
)
}
package main
import (
"context"
"fmt"
"strconv"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type taskListModel struct {
config *config
table table.Model
err error
loading bool
spinner spinner.Model
}
func newTaskList(config *config) taskListModel {
m := taskListModel{
config: config,
loading: true,
spinner: config.newSpinner(),
}
t := table.New(
table.WithColumns([]table.Column{
{Title: "ID", Width: 4},
{Title: "Name", Width: 10},
{Title: "Status", Width: 8},
}),
table.WithFocused(true),
table.WithHeight(8),
)
ts := table.DefaultStyles()
ts.Header = ts.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
BorderBottom(true).
Bold(false)
ts.Selected = ts.Selected.
Foreground(lipgloss.Color("229")).
Background(lipgloss.Color("57")).
Bold(false)
t.SetStyles(ts)
m.table = t
return m
}
func (m taskListModel) fetchTasks() tea.Msg {
m.config.mu.RLock()
defer m.config.mu.RUnlock()
row, err := m.config.queries.ListTasks(context.Background())
if err != nil {
return errMsg{err}
}
tableRows := make([]table.Row, len(row), len(row))
for i, e := range row {
tableRows[i] = table.Row{strconv.FormatInt(e.ID, 10), e.Name, e.Status}
}
return rows{tableRows}
}
func (m taskListModel) Init() tea.Cmd {
return tea.Batch(m.fetchTasks, m.spinner.Tick)
}
func (m taskListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case rows:
m.table.SetRows(msg.data)
m.loading = false
return m, nil
case errMsg:
m.err = msg
m.loading = false
return m, tea.Quit
case tea.KeyMsg:
switch msg.String() {
case "ctrl+n":
return m, changeView(newTaskEdit(m.config, nil))
case "enter":
id, _ := strconv.ParseInt(m.table.SelectedRow()[0], 10, 64)
return m, changeView(newTask(m.config, id))
}
}
m.table, cmd = m.table.Update(msg)
return m, cmd
}
func (m taskListModel) View() string {
if m.loading {
return fmt.Sprintf("\n%s Loading\n\n", m.spinner.View())
}
if m.err != nil {
return fmt.Sprintf("\nFailed fetching project: %v\n\n", m.err)
}
return m.config.styles.BaseTable.Render(m.table.View()) + "\n"
}
package main
import (
"context"
"database/sql"
"fmt"
"strings"
db "github.com/Asfolny/protastim/internal/database"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
)
type taskEditModel struct {
config *config
form *huh.Form
width int
create bool
}
func newTaskEdit(config *config, task *db.Task) tea.Model {
m := taskEditModel{config: config, create: task == nil}
confirmDefault := true
var name string
var desc string
var status string
if task != nil {
name = task.Name
desc = task.Description.String
status = task.Status
}
m.form = huh.NewForm(
huh.NewGroup(
huh.NewInput().
Key("name").
Title("Name").
Prompt("> ").
Validate(huh.ValidateNotEmpty()).
Value(&name),
huh.NewText().
Key("description").
Title("Description").
Value(&desc),
huh.NewSelect[string]().
Key("status").
Options(huh.NewOptions("N", "P", "C", "H", "D")...).
Title("Status ").
Inline(true).
Value(&status),
// TODO make a select between "create and open" and "only create"
// TODO select project
huh.NewConfirm().
Validate(func(v bool) error { // Should SHOULD close
if !v {
return fmt.Errorf("Welp, finish up then")
}
return nil
}).
Affirmative("done").
Negative("cancel").
Inline(true).
Value(&confirmDefault),
),
).
WithWidth(45).
WithShowHelp(false).
WithShowErrors(false)
return m
}
func (m taskEditModel) Init() tea.Cmd {
return m.form.Init()
}
func (m taskEditModel) createTaskCmd() func() tea.Msg {
m.config.mu.Lock()
defer m.config.mu.Unlock()
desc := sql.NullString{String: m.form.GetString("description")}
if desc.String != "" {
desc.Valid = true
}
dataset := db.CreateTaskParams{
Name: m.form.GetString("name"),
Description: desc,
Status: m.form.GetString("status"),
}
return func() tea.Msg {
m.config.queries.CreateTask(context.Background(), dataset)
return editingDone(true)
}
}
func (m taskEditModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = min(msg.Width, maxWidth) - m.config.styles.Base.GetHorizontalFrameSize()
case editingDone:
return m, changeView(newTaskList(m.config))
}
var cmds []tea.Cmd
// Process the form
form, cmd := m.form.Update(msg)
if f, ok := form.(*huh.Form); ok {
m.form = f
cmds = append(cmds, cmd)
}
if m.form.State == huh.StateCompleted {
// TODO handle abort
// TODO handle updating an existing
cmds = append(cmds, m.createTaskCmd())
}
return m, tea.Batch(cmds...)
}
func (m taskEditModel) View() string {
var status string
s := m.config.styles
v := strings.TrimSuffix(m.form.View(), "\n\n")
form := m.config.lg.NewStyle().Margin(1, 0).Render(v)
errors := m.form.Errors()
header := m.appBoundaryView("Charm Employment Application")
if len(errors) > 0 {
header = m.appErrorBoundaryView(m.errorView())
}
body := lipgloss.JoinHorizontal(lipgloss.Top, form, status)
footer := m.appBoundaryView(m.form.Help().ShortHelpView(m.form.KeyBinds()))
if len(errors) > 0 {
footer = m.appErrorBoundaryView("")
}
return s.Base.Render(header + "\n" + body + "\n\n" + footer)
}
func (m taskEditModel) errorView() string {
var s string
for _, err := range m.form.Errors() {
s += err.Error()
}
return s
}
func (m taskEditModel) appBoundaryView(text string) string {
return lipgloss.PlaceHorizontal(
m.width,
lipgloss.Left,
m.config.styles.HeaderText.Render(text),
lipgloss.WithWhitespaceChars("/"),
lipgloss.WithWhitespaceForeground(indigo),
)
}
func (m taskEditModel) appErrorBoundaryView(text string) string {
return lipgloss.PlaceHorizontal(
m.width,
lipgloss.Left,
m.config.styles.ErrorHeaderText.Render(text),
lipgloss.WithWhitespaceChars("/"),
lipgloss.WithWhitespaceForeground(red),
)
}