H4X3MEXPAMD7EFT4SH5QR4SVCH5WBBALEKOKRNGT4GNNC22LCXOQC GIA6TLIRNBTNNDBC6KBNY6U4OKNSIIEEZAW47AELL3I67N6V5CIQC BOQ7THDGYO62LXAK73I6YEIBPMDEQZ7J5JU4EQZMJVW2WZVK4VHAC JNU4ZGY5RAJSPIPDS63KLPQM4AXRYCHF72BRAZAVZRCV4FK7TGVAC BNYCTEJNONO7ECZMUSRAMBIZ6H344H2ZGSPAUCTPK2TVFNYPLGYAC 75KUI4W7LOCQRLGVL76AJ7NCDEUZD2RIJQMWR7XHEYU2RNHOHWQAC XRWBP4HXCO44YAXOZJPKYC2CLRPOXUPPS7TIZXEDFIMVHJ7GI7FAC WSHSUKJNQS5LEC6DEY52MUH4LQHAMQFSNYNACZTM6EK357TDB3FAC L23646QEMZPB366O52BQACSTIXR5K5ARMCTCT3IWL7JIMSFMQB4AC BCLBZMCTFR2ESWVVMGOPM7QSUD5TXV6ADTGBB5PHL7CPIYQIIY6AC ONTPUCESSETHTD6BIP6M5LEO3KMTNNCTPTV3SAH2HOQW5H2Q2DQQC MRP2IYLEDZ74P6364EIGB6FS6LP6SSCGZZIDBODPXIRGPB7CPWHAC COGZHHYVN54XZHQ7RNDUW3IESENXC4WQD5EKKJDHBZTGMI45HASQC SMK755RWW5HQAETGZ37IP7WUL7P4AC3VXGLWO6TINE23HJHFMLRAC package mainimport ("context""fmt"db "github.com/Asfolny/protastim/internal/database"tea "github.com/charmbracelet/bubbletea""github.com/charmbracelet/bubbles/spinner")type taskModel struct {id int64config *configdata *db.Taskmodal tea.Modelerr errorloading boolediting boolspinner 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 = &msgm.loading = falsereturn m, nilcase errMsg:m.err = msgm.loading = falsereturn m, tea.Quitcase spinner.TickMsg:var cmd tea.Cmdm.spinner, cmd = m.spinner.Update(msg)return m, cmdcase tea.KeyMsg:switch msg.String() {case "ctrl+n":m.editing = truereturn m, m.modal.Init()}}if m.editing {var cmd tea.Cmdm.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,)}m.modal = newTaskEdit(m.config, nil, "")
package mainimport ("context""fmt"db "github.com/Asfolny/protastim/internal/database"tea "github.com/charmbracelet/bubbletea")type projectModel struct {}func newProject(config *config, id int64) projectModel {m := projectModel{}t := table.New(table.WithColumns([]table.Column{{Title: "ID", Width: 4},{Title: "Name"},{Title: "Status", Width: 8},}),table.WithFocused(true),)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.taskList = treturn m}func (m projectModel) fetchProject() tea.Msg {if err != nil {return errMsg{err}}}func (m projectModel) Init() tea.Cmd {}func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {switch msg := msg.(type) {case rows:m.loadingTasks = falsem.taskList.SetRows(msg.data)return m, nilcase db.Project:m.data = &msgreturn m, nilcase errMsg:m.err = msgreturn m, tea.Quitcase spinner.TickMsg:var cmd tea.Cmdm.spinner, cmd = m.spinner.Update(msg)return m, cmdcase tea.KeyMsg:switch msg.String() {case "tab":switch m.focus {case focusDesc:m.focus = focusTasksm.taskList.Focus()case focusTasks:m.focus = focusDescm.taskList.Blur()}case "enter":if m.focus == focusTasks {id, _ := strconv.ParseInt(m.taskList.SelectedRow()[0], 10, 64)return m, changeView(newTask(m.config, id))}case "ctrl+n":}}}}func (m projectModel) View() string {return fmt.Sprintf("\n%s Loading\n\n", m.spinner.View())}if m.err != nil {}}m.taskList.SetColumns(cols)m.taskList.SetHeight(desc.Height)m.taskList.SetWidth(desc.Width)}descOut := lg.NewStyle().BorderStyle(lipgloss.NormalBorder()).Render(desc.View())table := lg.NewStyle().BorderStyle(lipgloss.NormalBorder()).Render(m.taskList.View())sb.WriteString(title)sb.WriteString(status)sb.WriteString(lipgloss.JoinHorizontal(lipgloss.Bottom, descOut, table))return sb.String()var sb strings.Builderlg := m.config.lgtitleStyle := lg.NewStyle().AlignHorizontal(lipgloss.Center).Width(m.config.getInnerWidth()).MarginTop(2).MarginBottom(1).Bold(true)title := titleStyle.Render(m.data.Name + "\n")status := m.config.styles.Base.Render(fmt.Sprintf("Status: %s\n", m.data.Status))// TODO more task/desc layoutsdesc := m.descVwdesc.SetContent(m.data.Description.String)desc.Width = (m.config.getInnerWidth() / 2) - 4desc.Height = m.config.getInnerHeight() - lipgloss.Height(title) - lipgloss.Height(status)cols := m.taskList.Columns()for i, col := range cols {if col.Title == "Name" {col.Width = desc.Width - 4 - 8cols[i] = col}return fmt.Sprintf("\nFailed fetching project or tasks: %v\n\n", m.err)if m.loadingProject {return m, cmdvar cmd tea.Cmdswitch m.focus {case focusDesc:m.descVw, cmd = m.descVw.Update(msg)case focusTasks:m.taskList, cmd = m.taskList.Update(msg)return m, changeView(newProjectEdit(m.config, nil))case "ctrl+e":return m, changeView(newProjectEdit(m.config, m.data))case "ctrl+t":return m, changeView(newTaskEdit(m.config, nil, m.data.Name))m.loadingProject = falsem.loadingTasks = falsem.loadingProject = falsem.descVw.SetContent(m.data.Description.String)return tea.Batch(m.fetchProject, m.fetchTasks, m.spinner.Tick)tableRows := make([]table.Row, len(tasks), len(tasks))for i, e := range tasks {tableRows[i] = table.Row{strconv.FormatInt(e.ID, 10), e.Name, e.Status}}return rows{tableRows}project, err := m.config.queries.GetProject(context.Background(), m.id)if err != nil {return errMsg{err}}return project}func (m projectModel) fetchTasks() tea.Msg {tasks, err := m.config.queries.ListTasksByProject(context.Background(), m.id)id: id,config: config,loadingProject: true,loadingTasks: true,spinner: config.newSpinner(),descVw: viewport.New(0, 0),focus: focusTasks,id int64config *configdata *db.Projecterr errorloadingProject boolloadingTasks boolspinner spinner.ModeldescVw viewport.ModeltaskList table.Modelfocus projectViewFocusElement"github.com/charmbracelet/lipgloss")type projectViewFocusElement intconst (focusDesc projectViewFocusElement = iotafocusTasks"github.com/charmbracelet/bubbles/spinner""github.com/charmbracelet/bubbles/table""github.com/charmbracelet/bubbles/viewport""strconv""strings"
package mainimport ("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 projectEditModel struct {config *configform *huh.Formwidth intcreate bool}func newProjectEdit(config *config, project *db.Project) tea.Model {m := projectEditModel{config: config, create: project == nil}confirmDefault := truevar name stringvar desc stringvar status stringif project != nil {name = project.Namedesc = project.Description.Stringstatus = project.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")...).Inline(true).Value(&status),// TODO make a select between "create and open" and "only create"huh.NewConfirm().Validate(func(v bool) error { // Should SHOULD closeif !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 projectEditModel) Init() tea.Cmd {return m.form.Init()}func (m projectEditModel) createProjectCmd() 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.CreateProjectParams{Name: m.form.GetString("name"),Description: desc,Status: m.form.GetString("status"),}return func() tea.Msg {m.config.queries.CreateProject(context.Background(), dataset)}}func (m projectEditModel) 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()return m, changeView(newProjectList(m.config))}var cmds []tea.Cmd// Process the formform, cmd := m.form.Update(msg)if f, ok := form.(*huh.Form); ok {m.form = fcmds = append(cmds, cmd)}if m.form.State == huh.StateCompleted {// TODO handle abort// TODO handle updating an existingcmds = append(cmds, m.createProjectCmd())}return m, tea.Batch(cmds...)}func (m projectEditModel) View() string {var status strings := m.config.stylesv := 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 projectEditModel) errorView() string {var s stringfor _, err := range m.form.Errors() {s += err.Error()}return s}func (m projectEditModel) appBoundaryView(text string) string {return lipgloss.PlaceHorizontal(m.width,lipgloss.Left,m.config.styles.HeaderText.Render(text),lipgloss.WithWhitespaceChars("/"),lipgloss.WithWhitespaceForeground(indigo),)}func (m projectEditModel) appErrorBoundaryView(text string) string {return lipgloss.PlaceHorizontal(m.width,lipgloss.Left,m.config.styles.ErrorHeaderText.Render(text),lipgloss.WithWhitespaceChars("/"),lipgloss.WithWhitespaceForeground(red),)}case editingDone:return editingDone(true)Title("Status").
package mainimport ("context""fmt""strconv""github.com/charmbracelet/bubbles/spinner""github.com/charmbracelet/bubbles/table"tea "github.com/charmbracelet/bubbletea""github.com/charmbracelet/lipgloss")type projectListModel struct {config *configtable table.Modelerr errorloading boolspinner spinner.Model}func newProjectList(config *config) projectListModel {m := projectListModel{config: config,loading: true,spinner: config.newSpinner(),}t := table.New(table.WithColumns([]table.Column{{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 = treturn m}func (m projectListModel) fetchProjects() tea.Msg {m.config.mu.RLock()defer m.config.mu.RUnlock()row, err := m.config.queries.ListProjects(context.Background())if err != nil {return errMsg{err}}tableRows := make([]table.Row, len(row), len(row))for i, e := range row {}return rows{tableRows}}func (m projectListModel) Init() tea.Cmd {return tea.Batch(m.fetchProjects, m.spinner.Tick)}func (m projectListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {var cmd tea.Cmdswitch msg := msg.(type) {case rows:m.table.SetRows(msg.data)m.loading = falsereturn m, nilcase errMsg:m.err = msgm.loading = falsereturn m, tea.Quitcase tea.KeyMsg:switch msg.String() {case "ctrl+n":return m, changeView(newProjectEdit(m.config, nil))case "enter":id, _ := strconv.ParseInt(m.table.SelectedRow()[0], 10, 64)return m, changeView(newProject(m.config, id))}}m.table, cmd = m.table.Update(msg)return m, cmd}func (m projectListModel) 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)}}lg := m.config.lgreturn lg.NewStyle().PaddingTop(1).Render(m.table.View())// TODO this specific handling should be done within Update insteadcols := m.table.Columns()for i, col := range cols {if col.Title == "Name" {col.Width = m.config.getInnerWidth() - 4 - 8 - 2cols[i] = col}}m.table.SetColumns(cols)m.table.SetHeight(m.config.getInnerHeight())m.table.SetWidth(m.config.size.Width)tableRows[i] = table.Row{strconv.FormatInt(e.ID, 10), e.Status, e.Name}{Title: "Name"},{Title: "ID", Width: 2},
package mainimport ("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 *configtable table.Modelerr errorloading boolspinner 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 = treturn 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.Cmdswitch msg := msg.(type) {case rows:m.table.SetRows(msg.data)m.loading = falsereturn m, nilcase errMsg:m.err = msgm.loading = falsereturn m, tea.Quitcase tea.KeyMsg:switch msg.String() {case "ctrl+n":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"}return m, changeView(newTaskEdit(m.config, nil, ""))
package mainimport ("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 *configform *huh.Formwidth intcreate bool}confirmDefault := truevar name stringvar desc stringvar status stringif task != nil {name = task.Namedesc = task.Description.Stringstatus = 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),huh.NewConfirm().Validate(func(v bool) error { // Should SHOULD closeif !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 {}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:}var cmds []tea.Cmd// Process the formform, cmd := m.form.Update(msg)if f, ok := form.(*huh.Form); ok {m.form = fcmds = append(cmds, cmd)}if m.form.State == huh.StateCompleted {// TODO handle abort// TODO handle updating an existingcmds = append(cmds, m.createTaskCmd())}return m, tea.Batch(cmds...)}func (m taskEditModel) View() string {var status strings := m.config.stylesv := strings.TrimSuffix(m.form.View(), "\n\n")form := m.config.lg.NewStyle().Margin(1, 0).Render(v)errors := m.form.Errors()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 stringfor _, 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),)}var header stringif m.create {header = m.appBoundaryView("Create Task")} else {header = m.appBoundaryView("Update Task")}return m, changeView(newProject(m.config, m.getProjectId()))case []db.Project:*m.projects = msg*m.loading = falsereturn m, nilProjectID: m.getProjectId(),return tea.Batch(m.form.Init(), m.fetchProjects)func (m taskEditModel) getProjectId() int64 {for _, e := range *m.projects {if e.Name == m.form.GetString("project") {return e.ID}}return 0}}func (m taskEditModel) projectsAsOptions() []string {l := make([]string, len(*m.projects) + 1)l[0] = "Choose a project"for i, e := range *m.projects {l[i+1] = e.Namei++}return l}func (m taskEditModel) fetchProjects() tea.Msg {row, err := m.config.queries.ListProjects(context.Background())if err != nil {return errMsg{err}}return rowhuh.NewSelect[string]().Key("project").Title("Project").OptionsFunc(func() []huh.Option[string] {if *m.loading {empty := make([]string, 2)empty[0] = "Loading..."empty[1] = m.projectreturn huh.NewOptions(empty...)}return huh.NewOptions(m.projectsAsOptions()...)}, m.loading).Validate(func(val string) error {if (val == "Choose a project") {return errors.New("Must choose a project for task")}return nil}).Value(&m.project),func newTaskEdit(config *config, task *db.Task, project string) tea.Model {t := truevar p []db.Projectm := taskEditModel{config: config, create: task == nil, loading: &t, projects: &p, project: project}loading *boolprojects *[]db.Projectproject string"errors"