Compare commits

..

No commits in common. "6f367619c33f163552e157ec1bd41b89bb5571db" and "0ec0e422105715889d6c2885dd7ee0314a37125b" have entirely different histories.

11 changed files with 130 additions and 4401 deletions

BIN
12pom Executable file

Binary file not shown.

33
README
View file

@ -1,33 +0,0 @@
= poms =
A very opinionated companion for taskwarrior, with notes and the pomodoro technique.
Designed to help you overcome being overwhelmed
= workflow =
- You pick a list of tasks you think you can achieve in the time you have alotted.
- A good reference for this is [cblgh's 12pom days](https://cblgh.org/12%20pomodoro%20workday/)
- Each of the tasks you list get placed into a text file.
- This textfile is free to use as you see fit.
- I make plan textfiles, and I generally link my task-note into that, and write about the specific task in there.
- Once you feel like you've got an adequate plan for each of your tasks, you can exit your editor.
- You'll get to choose which task to work on next
- Now you're in Pomodoro mode. Do the task! Take more notes with a shortcut.
- Now it's break time!
- Rinse, repeat until you're done!
= Roadmap / Issues =
- Implement state machine to manage state (See design docs)
- Implement timer
- It should have big full screen countdown MM:SS
- Ideally the individual digits are ascii art made from the digit itself
- TaskList Active tasks should _always_ display, and then let the cursor indexing kick in
- Dynamic column spacing based on WindowResizeMsg
- When going from TaskList to nvim, don't overwrite! Ask!
- Show hints about actions / shortcuts on screen
- Refactor extra functions out of `main.go` and into `task.go`
- Make sure task loading happens through `tea.Cmd`
= Design Docs =
- [Excalidraw drawings](https://excalidraw.com/#json=Tucrdp__TOwmtOEvNnbVv,ep_J-9-vB3BRVjnd0Y2H0w)

9
STATUS
View file

@ -1,9 +0,0 @@
= Sticky Active Index =
I tried to use an `activeView` like `headerView` but I had issues:
- Getting the styling the same as the rest of the rows
- Being able to get the cursor back up to selected view port
I'm getting the sense that I may have to fork the viewport?
Or at least learn how the viewport works

File diff suppressed because it is too large Load diff

2
go.mod
View file

@ -1,4 +1,4 @@
module git.sealight.xyz/aynish/poms
module 12pom
go 1.21.6

176
main.go
View file

@ -2,48 +2,119 @@ package main
import (
"fmt"
table "git.sealight.xyz/aynish/poms/table"
task "git.sealight.xyz/aynish/poms/task"
timer "git.sealight.xyz/aynish/poms/timer"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"log"
"os"
"os/exec"
"encoding/json"
"sort"
"log"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/key"
"os"
"strconv"
"strings"
"time"
)
// Task representation.
type Task struct {
Id int `json:"id"`
Description string `json:"description"`
Tags []string `json:"tags"`
Project string `json:"project"`
Status string `json:"status"`
Uuid string `json:"uuid"`
Urgency float32 `json:"urgency"`
Priority string `json:"priority"`
Due string `json:"due"`
End string `json:"end"`
Entry string `json:"entry"`
Modified string `json:"modified"`
}
func Tasks() []Task {
out, err := exec.Command("task", "(status:pending -backlog -BLOCKED -idea)", "export").Output()
if err != nil {
log.Fatal(err)
}
var tasks []Task
err = json.Unmarshal([]byte(out), &tasks)
if err != nil {
log.Fatal(err)
}
sort.Slice(tasks, func(i, j int) bool {
return tasks[i].Urgency > tasks[j].Urgency
})
return tasks
}
var (
tasks = task.GetTasks()
tasks = Tasks()
columns = []table.Column{
{Title: "✅?", Width: 3},
{Title: "ID", Width: 4},
{Title: "Project", Width: 20},
{Title: "Tags", Width: 20},
{Title: "Description", Width: 35},
{Title: "Project", Width: 10},
{Title: "Tags", Width: 10},
{Title: "Description", Width: 20},
// {Title: "Urgency", Width: 10},
}
baseStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240"))
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240"))
)
type model struct {
state string
taskList table.Model
all table.Model
}
func TasksToRows(tasks []task.Task) []table.Row {
func TasksToRows(tasks []Task) []table.Row {
rows := make([]table.Row, len(tasks))
for i := 0; i < len(tasks); i++ {
for i := 0; i < len(tasks); i++ {
t := tasks[i]
rows[i] = []string{strconv.Itoa(t.Id), t.Project, strings.Join(t.Tags[:], " "), t.Description} // fmt.Sprintf("%f", t.Urgency)
rows[i] = []string{"❌", strconv.Itoa(t.Id), t.Project, strings.Join(t.Tags[:], " "), t.Description} // fmt.Sprintf("%f", t.Urgency)
}
return rows
}
// We needed to overwrite "space" being page down
func NewKeyMap() table.KeyMap {
return table.KeyMap{
LineUp: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑/k", "up"),
),
LineDown: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("↓/j", "down"),
),
PageUp: key.NewBinding(
key.WithKeys("b", "pgup"),
key.WithHelp("b/pgup", "page up"),
),
PageDown: key.NewBinding(
key.WithKeys("f", "pgdown"),
key.WithHelp("f/pgdn", "page down"),
),
HalfPageUp: key.NewBinding(
key.WithKeys("u", "ctrl+u"),
key.WithHelp("u", "½ page up"),
),
HalfPageDown: key.NewBinding(
key.WithKeys("d", "ctrl+d"),
key.WithHelp("d", "½ page down"),
),
GotoTop: key.NewBinding(
key.WithKeys("home", "g"),
key.WithHelp("g/home", "go to start"),
),
GotoBottom: key.NewBinding(
key.WithKeys("end", "G"),
key.WithHelp("G/end", "go to end"),
),
}
}
func CreateTable() table.Model {
var t table.Model
rows := TasksToRows(tasks)
@ -52,6 +123,7 @@ func CreateTable() table.Model {
table.WithRows(rows),
table.WithFocused(true),
table.WithHeight(15),
table.WithKeyMap(NewKeyMap()),
)
s := table.DefaultStyles()
@ -64,22 +136,32 @@ func CreateTable() table.Model {
Foreground(lipgloss.Color("229")).
Background(lipgloss.Color("57")).
Bold(false)
s.Active = s.Active.
Foreground(lipgloss.Color("0")).
Background(lipgloss.Color("120"))
t.SetStyles(s)
return t
}
func (m model) SwitchRow() table.Model {
selected := m.all.SelectedRow()
if (selected[0] == "❌") {
selected[0] = "✅"
} else {
selected[0] = "❌"
}
// m.all.SetRows(allRows)
m.all.UpdateViewport()
return m.all
}
type editorFinishedMsg struct{ err error }
func (m model) rowsToFile() []string {
var rows []string
for index, row := range m.taskList.Rows() {
if index < m.taskList.ActiveIndex() {
for _, row := range m.all.Rows() {
if row[0] == "✅" {
// get uuid
id := row[0]
uuid_byte, err := exec.Command("task", "_get", id+".uuid").Output()
id := row[1]
uuid_byte, err := exec.Command("task", "_get", id + ".uuid").Output()
if err != nil {
log.Fatal(err)
}
@ -93,20 +175,18 @@ func (m model) rowsToFile() []string {
log.Fatal(err)
}
path := time.Now().Format(time.DateOnly)
tags := strings.Replace(row[2], " ", ":", -1)
tags = ":" + row[1] + ":" + tags + ":"
fmt.Fprintln(f, "%title "+row[3])
tags := strings.Replace(row[3], " ", ":", -1)
tags = ":" + row[2] + ":" + tags + ":"
fmt.Fprintln(f, "%title " + row[4])
fmt.Fprintln(f, tags)
fmt.Fprintln(f, "%date "+path)
fmt.Fprintln(f, "%date " + path)
fmt.Fprintln(f, "")
f.Close()
}
// link to task-note from pom-note
rowClone := make(table.Row, len(row))
_ = copy(rowClone, row)
rowClone[3] = "[[../task-notes/" + uuid + ".wiki|" + row[3] + "]]"
rows = append(rows, strings.Join(row[1:], " - "))
row[4] = "[[../task-notes/" + uuid + ".wiki|" + row[4] + "]]"
rows = append(rows, strings.Join(row[2:], " - "))
}
}
return rows
@ -126,12 +206,12 @@ func (m model) writeFile() string {
defer f.Close()
fmt.Fprintln(f, "%title ")
fmt.Fprintln(f, ":pom:priv:plan:")
fmt.Fprintln(f, "%date "+path)
fmt.Fprintln(f, "%date " + path)
fmt.Fprintln(f, "")
rows := m.rowsToFile()
for _, value := range rows {
fmt.Fprintln(f, value) // print values to f, one per line
fmt.Fprintln(f, value) // print values to f, one per line
}
return full_path
}
@ -148,8 +228,6 @@ func (m model) openEditor() tea.Cmd {
})
}
// TODO
// This is where the data should be loaded...
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@ -157,28 +235,32 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
case "esc":
if m.all.Focused() {
m.all.Blur()
} else {
m.all.Focus()
}
case "q", "ctrl+c":
return m, tea.Quit
case " ":
m.all = m.SwitchRow()
return m, nil
case "enter":
return m, m.openEditor()
}
case tea.WindowSizeMsg:
//top, right, bottom, left := baseStyle.GetMargin()
m.taskList.SetWidth(msg.Width - 5)
m.taskList.SetHeight(msg.Height - 5)
}
m.taskList, cmd = m.taskList.Update(msg)
m.all, cmd = m.all.Update(msg)
return m, cmd
}
func (m model) View() string {
return baseStyle.Render(m.taskList.View())
return baseStyle.Render(m.all.View())
}
func main() {
var m model
m.taskList = CreateTable()
if _, err := tea.NewProgram(m).Run(); err != nil {
m := model{CreateTable()}
if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}

BIN
poms

Binary file not shown.

View file

@ -1,528 +0,0 @@
package table
import (
"strings"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/mattn/go-runewidth"
)
// Model defines a state for the table widget.
type Model struct {
KeyMap KeyMap
cols []Column
rows []Row
cursor int
focus bool
styles Styles
styleFunc StyleFunc
viewport viewport.Model
start int
activeIndex int
end int
}
// Row represents one line in the table.
type Row []string
// Column defines the table structure.
type Column struct {
Title string
Width int
}
// KeyMap defines keybindings. It satisfies to the help.KeyMap interface, which
// is used to render the menu.
type KeyMap struct {
LineUp key.Binding
LineDown key.Binding
PageUp key.Binding
PageDown key.Binding
HalfPageUp key.Binding
HalfPageDown key.Binding
GotoTop key.Binding
GotoBottom key.Binding
Select key.Binding
}
// ShortHelp implements the KeyMap interface.
func (km KeyMap) ShortHelp() []key.Binding {
return []key.Binding{km.LineUp, km.LineDown}
}
// FullHelp implements the KeyMap interface.
func (km KeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{km.LineUp, km.LineDown, km.GotoTop, km.GotoBottom},
{km.PageUp, km.PageDown, km.HalfPageUp, km.HalfPageDown},
}
}
// DefaultKeyMap returns a default set of keybindings.
// We needed to overwrite "space" being page down
func DefaultKeyMap() KeyMap {
return KeyMap{
LineUp: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑/k", "up"),
),
LineDown: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("↓/j", "down"),
),
PageUp: key.NewBinding(
key.WithKeys("b", "pgup"),
key.WithHelp("b/pgup", "page up"),
),
PageDown: key.NewBinding(
key.WithKeys("f", "pgdown"),
key.WithHelp("f/pgdn", "page down"),
),
HalfPageUp: key.NewBinding(
key.WithKeys("u", "ctrl+u"),
key.WithHelp("u", "½ page up"),
),
HalfPageDown: key.NewBinding(
key.WithKeys("d", "ctrl+d"),
key.WithHelp("d", "½ page down"),
),
GotoTop: key.NewBinding(
key.WithKeys("home", "g"),
key.WithHelp("g/home", "go to start"),
),
GotoBottom: key.NewBinding(
key.WithKeys("end", "G"),
key.WithHelp("G/end", "go to end"),
),
Select: key.NewBinding(
key.WithKeys(" "),
key.WithHelp("spacebar", "select task"),
),
}
}
// Styles contains style definitions for this list component. By default, these
// values are generated by DefaultStyles.
type Styles struct {
Header lipgloss.Style
Cell lipgloss.Style
Selected lipgloss.Style
Active lipgloss.Style
}
// DefaultStyles returns a set of default style definitions for this table.
func DefaultStyles() Styles {
return Styles{
Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")),
Active: lipgloss.NewStyle().Bold(true).Background(lipgloss.Color("x20")),
Header: lipgloss.NewStyle().Bold(true).Padding(0, 1),
Cell: lipgloss.NewStyle().Padding(0, 1),
}
}
// SetStyles sets the table styles.
func (m *Model) SetStyles(s Styles) {
m.styles = s
m.UpdateViewport()
}
// Option is used to set options in New. For example:
//
// table := New(WithColumns([]Column{{Title: "ID", Width: 10}}))
type Option func(*Model)
// New creates a new model for the table widget.
func New(opts ...Option) Model {
m := Model{
cursor: 0,
activeIndex: 0,
viewport: viewport.New(0, 20),
KeyMap: DefaultKeyMap(),
styles: DefaultStyles(),
}
for _, opt := range opts {
opt(&m)
}
m.UpdateViewport()
return m
}
// WithColumns sets the table columns (headers).
func WithColumns(cols []Column) Option {
return func(m *Model) {
m.cols = cols
}
}
// WithRows sets the table rows (data).
func WithRows(rows []Row) Option {
return func(m *Model) {
m.rows = rows
}
}
// WithHeight sets the height of the table.
func WithHeight(h int) Option {
return func(m *Model) {
m.viewport.Height = h - lipgloss.Height(m.headersView())
}
}
// WithWidth sets the width of the table.
func WithWidth(w int) Option {
return func(m *Model) {
m.viewport.Width = w
}
}
// WithFocused sets the focus state of the table.
func WithFocused(f bool) Option {
return func(m *Model) {
m.focus = f
}
}
// WithStyles sets the table styles.
func WithStyles(s Styles) Option {
return func(m *Model) {
m.styles = s
}
}
// WithStyleFunc sets the table style func which can determine a cell style per column, row, and selected state.
func WithStyleFunc(f StyleFunc) Option {
return func(m *Model) {
m.styleFunc = f
}
}
// WithKeyMap sets the key map.
func WithKeyMap(km KeyMap) Option {
return func(m *Model) {
m.KeyMap = km
}
}
// Update is the Bubble Tea update loop.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
if !m.focus {
return m, nil
}
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, m.KeyMap.LineUp):
m.MoveUp(1)
case key.Matches(msg, m.KeyMap.LineDown):
m.MoveDown(1)
case key.Matches(msg, m.KeyMap.PageUp):
m.MoveUp(m.viewport.Height)
case key.Matches(msg, m.KeyMap.PageDown):
m.MoveDown(m.viewport.Height)
case key.Matches(msg, m.KeyMap.HalfPageUp):
m.MoveUp(m.viewport.Height / 2)
case key.Matches(msg, m.KeyMap.HalfPageDown):
m.MoveDown(m.viewport.Height / 2)
case key.Matches(msg, m.KeyMap.LineDown):
m.MoveDown(1)
case key.Matches(msg, m.KeyMap.GotoTop):
m.GotoTop()
case key.Matches(msg, m.KeyMap.GotoBottom):
m.GotoBottom()
case key.Matches(msg, m.KeyMap.Select):
m.SwitchRow()
}
}
return m, nil
}
// Focused returns the focus state of the table.
func (m Model) Focused() bool {
return m.focus
}
// Focus focuses the table, allowing the user to move around the rows and
// interact.
func (m *Model) Focus() {
m.focus = true
m.UpdateViewport()
}
// Blur blurs the table, preventing selection or movement.
func (m *Model) Blur() {
m.focus = false
m.UpdateViewport()
}
// View renders the component.
func (m Model) View() string {
return m.headersView() + "\n" + m.viewport.View()
}
// UpdateViewport updates the list content based on the previously defined
// columns and rows.
func (m *Model) UpdateViewport() {
renderedRows := make([]string, 0, len(m.rows))
// Render only rows from: m.cursor-m.viewport.Height to: m.cursor+m.viewport.Height
// Constant runtime, independent of number of rows in a table.
// Limits the number of renderedRows to a maximum of 2*m.viewport.Height
if m.activeIndex > 0 {
for i := 0; i < m.activeIndex; i++ {
renderedRows = append(renderedRows, m.renderRow(i))
}
}
if m.cursor >= 0 {
m.start = clamp(m.cursor-m.viewport.Height, 0, m.cursor) + m.activeIndex
} else {
m.start = 0 + m.activeIndex
}
m.end = clamp(m.cursor+m.viewport.Height, m.cursor, len(m.rows))
for i := m.start; i < m.end; i++ {
renderedRows = append(renderedRows, m.renderRow(i))
}
m.viewport.SetContent(
lipgloss.JoinVertical(lipgloss.Left, renderedRows...),
)
}
// SelectedRow returns the selected row.
// You can cast it to your own implementation.
func (m Model) SelectedRow() Row {
if m.cursor < 0 || m.cursor >= len(m.rows) {
return nil
}
return m.rows[m.cursor]
}
// Rows returns the current rows.
func (m Model) Rows() []Row {
return m.rows
}
// Columns returns the current columns.
func (m Model) Columns() []Column {
return m.cols
}
// SetRows sets a new rows state.
func (m *Model) SetRows(r []Row) {
m.rows = r
m.UpdateViewport()
}
// SetColumns sets a new columns state.
func (m *Model) SetColumns(c []Column) {
m.cols = c
m.UpdateViewport()
}
// Update row to selected
func (m *Model) SwitchRow() {
// Get selected row
selectedRow := m.SelectedRow()
removedRows := remove(m.rows, m.cursor)
// If cursor outside selection (adding selection)
// We check equals because if it's on the border, we just moved into selection
// and are still adding
if m.cursor >= m.activeIndex {
newSelection := make([]Row, len(removedRows)+1)
newSelection = append(removedRows[:m.activeIndex+1], removedRows[m.activeIndex:]...)
newSelection[m.activeIndex] = selectedRow
m.rows = newSelection
m.activeIndex += 1
} else {
m.activeIndex -= 1
newSelection := make([]Row, len(removedRows)+1)
newSelection = append(removedRows[:m.activeIndex+1], removedRows[m.activeIndex:]...)
newSelection[m.activeIndex] = selectedRow
m.rows = newSelection
}
m.UpdateViewport()
}
// SetWidth sets the width of the viewport of the table.
func (m *Model) SetWidth(w int) {
m.viewport.Width = w
m.UpdateViewport()
}
// SetHeight sets the height of the viewport of the table.
func (m *Model) SetHeight(h int) {
m.viewport.Height = h - lipgloss.Height(m.headersView())
m.UpdateViewport()
}
func (m *Model) ActiveIndex() int {
return m.activeIndex
}
// Height returns the viewport height of the table.
func (m Model) Height() int {
return m.viewport.Height
}
// Width returns the viewport width of the table.
func (m Model) Width() int {
return m.viewport.Width
}
// Cursor returns the index of the selected row.
func (m Model) Cursor() int {
return m.cursor
}
// SetCursor sets the cursor position in the table.
func (m *Model) SetCursor(n int) {
m.cursor = clamp(n, 0, len(m.rows)-1)
m.UpdateViewport()
}
// MoveUp moves the selection up by any number of rows.
// It can not go above the first row.
func (m *Model) MoveUp(n int) {
m.cursor = clamp(m.cursor-n, 0, len(m.rows)-1)
switch {
case m.start == 0:
m.viewport.SetYOffset(clamp(m.viewport.YOffset, 0, m.cursor))
//case m.start < m.viewport.Height:
// m.viewport.YOffset = (clamp(clamp(m.viewport.YOffset+n, 0, m.cursor), 0, m.viewport.Height))
//case m.viewport.YOffset >= 1:
// m.viewport.YOffset = clamp(m.viewport.YOffset+n, 1, m.viewport.Height)
}
m.UpdateViewport()
}
// MoveDown moves the selection down by any number of rows.
// It can not go below the last row.
func (m *Model) MoveDown(n int) {
m.cursor = clamp(m.cursor+n, 0, len(m.rows)-1)
m.UpdateViewport()
switch {
case m.end == len(m.rows) && m.viewport.YOffset > 0:
m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.viewport.Height))
case m.cursor > (m.end-m.start)/2 && m.viewport.YOffset > 0:
m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.cursor))
case m.viewport.YOffset > 1:
case m.cursor > m.viewport.YOffset+m.viewport.Height-1:
m.viewport.SetYOffset(clamp(m.viewport.YOffset+1, 0, 1))
}
}
// GotoTop moves the selection to the first row.
func (m *Model) GotoTop() {
m.MoveUp(m.cursor)
}
// GotoBottom moves the selection to the last row.
func (m *Model) GotoBottom() {
m.MoveDown(len(m.rows))
}
// FromValues create the table rows from a simple string. It uses `\n` by
// default for getting all the rows and the given separator for the fields on
// each row.
func (m *Model) FromValues(value, separator string) {
rows := []Row{}
for _, line := range strings.Split(value, "\n") {
r := Row{}
for _, field := range strings.Split(line, separator) {
r = append(r, field)
}
rows = append(rows, r)
}
m.SetRows(rows)
}
// StyleFunc is a function that can be used to customize the style of a table cell based on the row and column index.
type StyleFunc func(row, col int, value string) lipgloss.Style
func (m Model) headersView() string {
var s = make([]string, 0, len(m.cols))
for _, col := range m.cols {
if col.Width <= 0 {
continue
}
style := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true)
renderedCell := style.Render(runewidth.Truncate(col.Title, col.Width, "…"))
s = append(s, m.styles.Header.Render(renderedCell))
}
return lipgloss.JoinHorizontal(lipgloss.Left, s...)
}
func (m *Model) renderRow(r int) string {
var s = make([]string, 0, len(m.cols))
for i, value := range m.rows[r] {
if m.cols[i].Width <= 0 {
continue
}
var cellStyle lipgloss.Style
if m.styleFunc != nil {
cellStyle = m.styleFunc(r, i, value)
if r == m.cursor {
cellStyle.Inherit(m.styles.Selected)
} else if r < m.activeIndex {
cellStyle.Inherit(m.styles.Active)
}
} else {
cellStyle = m.styles.Cell
}
style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true)
renderedCell := cellStyle.Render(style.Render(runewidth.Truncate(value, m.cols[i].Width, "…")))
s = append(s, renderedCell)
}
row := lipgloss.JoinHorizontal(lipgloss.Left, s...)
if r == m.cursor {
return m.styles.Selected.Render(row)
}
if r < m.activeIndex {
return m.styles.Active.Render(row)
}
return row
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func clamp(v, low, high int) int {
return min(max(v, low), high)
}
func remove(slice []Row, s int) []Row {
return append(slice[:s], slice[s+1:]...)
}

View file

@ -1,40 +0,0 @@
package task
import (
"encoding/json"
"sort"
"log"
"os/exec"
)
// Task representation.
type Task struct {
Id int `json:"id"`
Description string `json:"description"`
Tags []string `json:"tags"`
Project string `json:"project"`
Status string `json:"status"`
Uuid string `json:"uuid"`
Urgency float32 `json:"urgency"`
Priority string `json:"priority"`
Due string `json:"due"`
End string `json:"end"`
Entry string `json:"entry"`
Modified string `json:"modified"`
}
func GetTasks() []Task {
out, err := exec.Command("task", "(status:pending -backlog -BLOCKED -idea)", "export").Output()
if err != nil {
log.Fatal(err)
}
var tasks []Task
err = json.Unmarshal([]byte(out), &tasks)
if err != nil {
log.Fatal(err)
}
sort.Slice(tasks, func(i, j int) bool {
return tasks[i].Urgency > tasks[j].Urgency
})
return tasks
}

View file

@ -1,122 +0,0 @@
package timer
import (
"strconv"
// "os"
// "time"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
)
var (
colon = []string{" ", " ", " __ ", " |__| ", " __ ", " |__| ", " ", " "}
one = []string{" _____ ", " | _ | ", " |.| | ", " `-|. | ", " |: | ", " |::.| ", " `---' "}
two = []string{" _______ ", " | |", " |___| |", " / ___/ ", " |: 1 \\ ", " |::.. . |", " '-------'"}
three = []string{" _______ ", " | _ |", " |___| |", " _(__ |", " |: 1 |", " |::.. . |", " '-------'"}
four = []string{" ___ ___ ", " | Y |", " | | |", " |____ |", " |: |", " |::.|", " '---'"}
five = []string{" _______ ", " | _ |", " | 1___|", " |____ |", " |: 1 |", " |::.. . |", " '-------'"}
six = []string{" _______ ", " | _ |", " | 1___|", " |. ", " |: 1 |", " |::.. . |", " '-------'"}
seven = []string{" _______ ", " | _ |", " |___| |", " / / ", " | | ", " | | ", " '---' "}
eight = []string{" _______ ", " | _ |", " |. | |", " |. _ |", " |: 1 |", " |::.. . |", " '-------'"}
nine = []string{" _______ ", " | _ |", " | | |", " \\___ |", " |: 1 |", " |::.. . |", " '-------'"}
zero = []string{" _______ ", " | _ |", " |. | |", " |. | |", " |: 1 |", " |::.. . |", " '-------'"}
numbers = [][]string{zero, one, two, three, four, five, six, seven, eight, nine}
)
type model struct {
timer Model
keymap keymap
help help.Model
}
type keymap struct {
start key.Binding
stop key.Binding
reset key.Binding
quit key.Binding
}
func (m model) Init() tea.Cmd {
return m.timer.Init()
}
func printIntegers(num float64) string {
var s string
digits := strconv.FormatFloat(num, 'f', -1, 64)
for j := 0; j < 7; j++ {
for _, val := range digits {
n, _ := strconv.Atoi(string(val))
s += numbers[n][j] + " "
}
s += "\n"
}
return s
}
func PrintTime(min float64, sec float64) string {
var s string
mins := strconv.FormatFloat(min, 'f', 0, 64)
secs := strconv.FormatFloat(sec, 'f', 0, 64)
time := mins+secs
for j := 0; j < 7; j++ {
for i, val := range time {
if i == 2 {
s += colon[j]
}
n, _ := strconv.Atoi(string(val))
s += numbers[n][j]
}
s += "\n"
}
return s
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case TickMsg:
var cmd tea.Cmd
m.timer, cmd = m.timer.Update(msg)
return m, cmd
case StartStopMsg:
var cmd tea.Cmd
m.timer, cmd = m.timer.Update(msg)
m.keymap.stop.SetEnabled(m.timer.Running())
m.keymap.start.SetEnabled(!m.timer.Running())
return m, cmd
case TimeoutMsg:
// m.quitting = true
return m, tea.Quit
case tea.KeyMsg:
switch {
case key.Matches(msg, m.keymap.quit):
// m.quitting = true
return m, tea.Quit
case key.Matches(msg, m.keymap.reset):
//m.timer.Timeout = timeout
case key.Matches(msg, m.keymap.start, m.keymap.stop):
return m, m.timer.Toggle()
}
}
return m, nil
}
func (m model) helpView() string {
return "\n" + m.help.ShortHelpView([]key.Binding{
m.keymap.start,
m.keymap.stop,
m.keymap.reset,
m.keymap.quit,
})
}
func (m model) View() string {
return PrintTime(m.timer.Minutes(), m.timer.Seconds())
}

View file

@ -1,200 +0,0 @@
package timer
import (
"sync"
"time"
tea "github.com/charmbracelet/bubbletea"
)
var (
lastID int
idMtx sync.Mutex
)
func nextID() int {
idMtx.Lock()
defer idMtx.Unlock()
lastID++
return lastID
}
// Authors note with regard to start and stop commands:
//
// Technically speaking, sending commands to start and stop the timer in this
// case is extraneous. To stop the timer we'd just need to set the 'running'
// property on the model to false which cause logic in the update function to
// stop responding to TickMsgs. To start the model we'd set 'running' to true
// and fire off a TickMsg. Helper functions would look like:
//
// func (m *model) Start() tea.Cmd
// func (m *model) Stop()
//
// The danger with this approach, however, is that order of operations becomes
// important with helper functions like the above. Consider the following:
//
// // Would not work
// return m, m.timer.Start()
//
// // Would work
// cmd := m.timer.start()
// return m, cmd
//
// Thus, because of potential pitfalls like the ones above, we've introduced
// the extraneous StartStopMsg to simplify the mental model when using this
// package. Bear in mind that the practice of sending commands to simply
// communicate with other parts of your application, such as in this package,
// is still not recommended.
// StartStopMsg is used to start and stop the timer.
type StartStopMsg struct {
ID int
running bool
}
// TickMsg is a message that is sent on every timer tick.
type TickMsg struct {
// ID is the identifier of the timer that sends the message. This makes
// it possible to determine which timer a tick belongs to when there
// are multiple timers running.
//
// Note, however, that a timer will reject ticks from other timers, so
// it's safe to flow all TickMsgs through all timers and have them still
// behave appropriately.
ID int
// Timeout returns whether or not this tick is a timeout tick. You can
// alternatively listen for TimeoutMsg.
Timeout bool
}
// TimeoutMsg is a message that is sent once when the timer times out.
//
// It's a convenience message sent alongside a TickMsg with the Timeout value
// set to true.
type TimeoutMsg struct {
ID int
}
// Model of the timer component.
type Model struct {
// How long until the timer expires.
Timeout time.Duration
// How long to wait before every tick. Defaults to 1 second.
Interval time.Duration
id int
running bool
}
// NewWithInterval creates a new timer with the given timeout and tick interval.
func NewWithInterval(timeout, interval time.Duration) Model {
return Model{
Timeout: timeout,
Interval: interval,
running: true,
id: nextID(),
}
}
// New creates a new timer with the given timeout and default 1s interval.
func New(timeout time.Duration) Model {
return NewWithInterval(timeout, time.Second)
}
// ID returns the model's identifier. This can be used to determine if messages
// belong to this timer instance when there are multiple timers.
func (m Model) ID() int {
return m.id
}
// Running returns whether or not the timer is running. If the timer has timed
// out this will always return false.
func (m Model) Running() bool {
if m.Timedout() || !m.running {
return false
}
return true
}
// Timedout returns whether or not the timer has timed out.
func (m Model) Timedout() bool {
return m.Timeout <= 0
}
// Init starts the timer.
func (m Model) Init() tea.Cmd {
return m.tick()
}
// Update handles the timer tick.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case StartStopMsg:
if msg.ID != 0 && msg.ID != m.id {
return m, nil
}
m.running = msg.running
return m, m.tick()
case TickMsg:
if !m.Running() || (msg.ID != 0 && msg.ID != m.id) {
break
}
m.Timeout -= m.Interval
return m, tea.Batch(m.tick(), m.timedout())
}
return m, nil
}
// View of the timer component.
func (m Model) View() string {
return m.Timeout.String()
}
func (m Model) Seconds() float64 {
return m.Timeout.Seconds()
}
func (m Model) Minutes() float64 {
return m.Timeout.Minutes()
}
// Start resumes the timer. Has no effect if the timer has timed out.
func (m *Model) Start() tea.Cmd {
return m.startStop(true)
}
// Stop pauses the timer. Has no effect if the timer has timed out.
func (m *Model) Stop() tea.Cmd {
return m.startStop(false)
}
// Toggle stops the timer if it's running and starts it if it's stopped.
func (m *Model) Toggle() tea.Cmd {
return m.startStop(!m.Running())
}
func (m Model) tick() tea.Cmd {
return tea.Tick(m.Interval, func(_ time.Time) tea.Msg {
return TickMsg{ID: m.id, Timeout: m.Timedout()}
})
}
func (m Model) timedout() tea.Cmd {
if !m.Timedout() {
return nil
}
return func() tea.Msg {
return TimeoutMsg{ID: m.id}
}
}
func (m Model) startStop(v bool) tea.Cmd {
return func() tea.Msg {
return StartStopMsg{ID: m.id, running: v}
}
}