diff --git a/constants.go b/constants.go deleted file mode 100644 index 90239d0..0000000 --- a/constants.go +++ /dev/null @@ -1,10 +0,0 @@ -package main - -//Table definition constants -const ( - numRows = 6 - numCols = 7 - cellW = 10 - cellH = 1 -) - diff --git a/go.sum b/go.sum index 48041bc..73a85ed 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= @@ -14,8 +12,6 @@ github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2ll github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= diff --git a/main.go b/main.go index 08217df..72afb3f 100644 --- a/main.go +++ b/main.go @@ -1,31 +1,73 @@ +// main.go package main +// A simple program demonstrating the spinner component from the Bubbles +// component library. + import ( "fmt" - "os" - "time" - + "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "os" ) -func main() { - // Get the current datetime - now := time.Now() - // Set the start date used for opening the current month instead of 1/1/1970 - start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) - // This gets the weekday of the first of the month so that we have the correct day with the correct date - offset := int(start.Weekday()) - p := tea.NewProgram(model{ - year: now.Year(), - monthIndex: int(now.Month()) - 1, - cursorRow: offset / numCols, - cursorCol: offset % numCols, - startOffset: offset, - mode: monthView, - }) - if err := p.Start(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) +type errMsg error + +type model struct { + spinner spinner.Model + quitting bool + err error +} + +func initialModel() model { + s := spinner.New() + s.Spinner = spinner.Dot + s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + return model{spinner: s} +} + +func (m model) Init() tea.Cmd { + return m.spinner.Tick +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q", "esc", "ctrl+c": + m.quitting = true + return m, tea.Quit + default: + return m, nil + } + + case errMsg: + m.err = msg + return m, nil + + default: + var cmd tea.Cmd + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd } } +func (m model) View() string { + if m.err != nil { + return m.err.Error() + } + str := fmt.Sprintf("\n\n %s Loading forever...press q to quit\n\n", m.spinner.View()) + if m.quitting { + return str + "\n" + } + return str +} + +func main() { + p := tea.NewProgram(initialModel()) + if _, err := p.Run(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/model.go b/model.go deleted file mode 100644 index eadacba..0000000 --- a/model.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import tea "github.com/charmbracelet/bubbletea" -// viewMode is used to store which view is being currently seen (i.e. month or hour) -type viewMode int - -const ( - monthView viewMode = iota - hourlyView -) - -type model struct { - cursorRow int // which row the cursor is sitting on used for cursor highlight - cursorCol int // which col the cursor is sitiing on used for cursor highlight - monthIndex int // what month 1-12 that is being displayed - year int // the current year being displayed - startOffset int // weekday offset where day 1 starts - daysInMonth int // a count of how many days there are in the month to account for 30,31,or 28 - - mode viewMode - - // For hourly view - selectedDay int // which day we are looking at in hourly view - hourCursor int // which hour (0-23) is selected in hourly view -} - -//No init state -func (m model) Init() tea.Cmd { - return nil -} - diff --git a/readme.md b/readme.md index 190b6a6..7710b0f 100644 --- a/readme.md +++ b/readme.md @@ -1,49 +1 @@ -# GoCal - -This is a terminal-based calendar and hourly schedule viewer built using [Bubble Tea](https://github.com/charmbracelet/bubbletea) and styled with [Lipgloss](https://github.com/charmbracelet/lipgloss). - ---- - -## Project Structure - -| File | Description | -|----------------|-------------------------------------------------------------------| -| `main.go` | Entry point of the application. Initializes the program and sets the initial model state (current year/month and cursor position). | -| `constants.go` | Defines global constants such as calendar grid size and cell dimensions used throughout the app. | -| `styles.go` | Contains all the Lipgloss style definitions for calendar cells, headers, and hourly schedule views. | -| `model.go` | Defines the data model struct (`model`) that holds the application state, and the `viewMode` type representing current UI mode. Also contains the `Init` function for Bubble Tea. | -| `update.go` | Implements all update logic for handling user input and updating the model state in both month and hourly views. Includes the fix to properly update cursor position when switching months. | -| `view.go` | Contains all rendering logic that converts the current model state into styled terminal output for both month and hourly views. | - ---- - -## Features - -- Month view with navigable calendar grid. -- Highlighted current day. -- Cursor highlighting with keyboard navigation. -- Enter key switches to hourly schedule view for the selected day. -- Hourly view with current hour highlighted. -- Switch back to month view with ESC. - ---- - -## Usage - -Run the program: - -```bash -go run . -``` -Controls: - -- [a] / [d]: Navigate previous / next month. - -- Arrow keys: Move cursor around days. - -- [Enter]: Switch to hourly view for selected day. - -- [Esc]: Return to month view. - -- [q] / Ctrl+C: Quit. - +# GoCal \ No newline at end of file diff --git a/styles.go b/styles.go deleted file mode 100644 index 85d4a10..0000000 --- a/styles.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import "github.com/charmbracelet/lipgloss" - -// Styles for calendar cells and headers -var ( - // this is the style thats used as the default style for the month view - cellStyle = lipgloss.NewStyle().Width(cellW).Height(cellH).Align(lipgloss.Center) - // this is the style that sets the current selected hours bg to orange and text to white - selectedStyle = cellStyle.Copy().Bold(true).Background(lipgloss.Color("12")).Foreground(lipgloss.Color("15")) - // this is the style that sets the color of cells that are not in use for a given month - unselectedStyle = cellStyle.Copy().Background(lipgloss.Color("236")).Foreground(lipgloss.Color("250")) - // this is the style that sets the color of todays cell to purple bg white text - todayStyle = cellStyle.Copy().Background(lipgloss.Color("99")).Foreground(lipgloss.Color("15")).Bold(true) - // this is the style that defines the header where the current months name goes - headerStyle = lipgloss.NewStyle().Width(numCols * cellW).Align(lipgloss.Center).Bold(true) - // this is the style that defines the DoW header where mon-sun go - daysOfWeekStyle = lipgloss.NewStyle().Width(cellW).Align(lipgloss.Center).Bold(true) - - // Styles for hourly view cells - // this is the style thats used as the default for the hours view - hourCellStyle = lipgloss.NewStyle().Width(cellW * 3).Height(cellH).Align(lipgloss.Left).PaddingLeft(1) - // This is the style that sets the current selected hours style to orange bg white text - hourSelectedStyle = hourCellStyle.Copy().Bold(true).Background(lipgloss.Color("12")).Foreground(lipgloss.Color("15")) - // this is the style that sets the current hour to purple bg and white text - currentHourStyle = hourCellStyle.Copy().Background(lipgloss.Color("99")).Foreground(lipgloss.Color("15")).Bold(true) -) - diff --git a/update.go b/update.go deleted file mode 100644 index fe720d3..0000000 --- a/update.go +++ /dev/null @@ -1,125 +0,0 @@ -package main - -import ( - "time" - - tea "github.com/charmbracelet/bubbletea" -) - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch m.mode { - case monthView: - return m.updateMonthView(msg) - case hourlyView: - return m.updateHourlyView(msg) - default: - return m, nil - } -} - -func (m model) updateMonthView(msg tea.Msg) (tea.Model, tea.Cmd) { - firstDay := time.Date(m.year, time.Month(m.monthIndex+1), 1, 0, 0, 0, 0, time.UTC) - m.startOffset = int(firstDay.Weekday()) - m.daysInMonth = time.Date(m.year, time.Month(m.monthIndex+2), 0, 0, 0, 0, 0, time.UTC).Day() - - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "ctrl+c", "q": - return m, tea.Quit - case "a": // Previous month - if m.monthIndex == 0 { - m.monthIndex = 11 - m.year-- - } else { - m.monthIndex-- - } - - first := time.Date(m.year, time.Month(m.monthIndex+1), 1, 0, 0, 0, 0, time.UTC) - offset := int(first.Weekday()) - - m.startOffset = offset - m.daysInMonth = time.Date(m.year, time.Month(m.monthIndex+2), 0, 0, 0, 0, 0, time.UTC).Day() - - m.cursorRow = offset / numCols - m.cursorCol = offset % numCols - - return m, nil - case "d": // Next month - if m.monthIndex == 11 { - m.monthIndex = 0 - m.year++ - } else { - m.monthIndex++ - } - - first := time.Date(m.year, time.Month(m.monthIndex+1), 1, 0, 0, 0, 0, time.UTC) - offset := int(first.Weekday()) - - m.startOffset = offset - m.daysInMonth = time.Date(m.year, time.Month(m.monthIndex+2), 0, 0, 0, 0, 0, time.UTC).Day() - - m.cursorRow = offset / numCols - m.cursorCol = offset % numCols - - return m, nil - case "up": - if m.cursorRow > 0 { - m.cursorRow-- - } - case "down": - if m.cursorRow < numRows-1 { - m.cursorRow++ - } - case "left": - if m.cursorCol > 0 { - m.cursorCol-- - } - case "right": - if m.cursorCol < numCols-1 { - m.cursorCol++ - } - case "enter": - // Calculate selected day based on cursor - dayIndex := m.cursorRow*numCols + m.cursorCol - if dayIndex >= m.startOffset && dayIndex < m.startOffset+m.daysInMonth { - m.selectedDay = dayIndex - m.startOffset + 1 - m.hourCursor = 0 - m.mode = hourlyView - } - } - - // Clamp cursor to valid days - dayIndex := m.cursorRow*numCols + m.cursorCol - if dayIndex < m.startOffset || dayIndex >= m.startOffset+m.daysInMonth { - m.cursorRow = m.startOffset / numCols - m.cursorCol = m.startOffset % numCols - } - return m, nil - default: - return m, nil - } -} - -func (m model) updateHourlyView(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "esc": - m.mode = monthView - return m, nil - case "ctrl+c", "q": - return m, tea.Quit - case "up": - if m.hourCursor > 0 { - m.hourCursor-- - } - case "down": - if m.hourCursor < 23 { - m.hourCursor++ - } - } - } - return m, nil -} - diff --git a/view.go b/view.go deleted file mode 100644 index 3f91ac4..0000000 --- a/view.go +++ /dev/null @@ -1,89 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -func (m model) View() string { - switch m.mode { - case monthView: - return m.viewMonth() - case hourlyView: - return m.viewHourly() - default: - return "" - } -} - -func (m model) viewMonth() string { - var out string - - monthTime := time.Date(m.year, time.Month(m.monthIndex+1), 1, 0, 0, 0, 0, time.UTC) - header := headerStyle.Render(monthTime.Format("January 2006")) - out += header + "\n" - - weekdays := []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} - for _, day := range weekdays { - out += daysOfWeekStyle.Render(day) - } - out += "\n" - - day := 1 - for r := 0; r < numRows; r++ { - for c := 0; c < numCols; c++ { - cellNum := r*numCols + c - var content string - - if cellNum >= m.startOffset && day <= m.daysInMonth { - content = fmt.Sprintf("%2d", day) - if r == m.cursorRow && c == m.cursorCol { - out += selectedStyle.Render(content) - } else { - today := time.Now() - isToday := m.year == today.Year() && - m.monthIndex == int(today.Month())-1 && - day == today.Day() - if isToday { - out += todayStyle.Render(content) - } else { - out += unselectedStyle.Render(content) - } - } - day++ - } else { - out += cellStyle.Render("") - } - } - out += "\n" - } - - out += "\n[a]/[d] to change month, arrows to move, enter to select a day, q to quit." - return out -} - -func (m model) viewHourly() string { - var out string - - dateHeader := fmt.Sprintf("Schedule for %04d-%02d-%02d", m.year, m.monthIndex+1, m.selectedDay) - out += headerStyle.Render(dateHeader) + "\n\n" - - nowHour := time.Now().Hour() - - for hour := 0; hour < 24; hour++ { - label := fmt.Sprintf("%02d:00 - %02d:00", hour, hour+1) - - switch { - case hour == m.hourCursor: - out += hourSelectedStyle.Render(label) + "\n" - case hour == nowHour: - out += currentHourStyle.Render(label) + "\n" - default: - out += hourCellStyle.Render(label) + "\n" - } - } - - out += "\nPress ESC to return to month view, q to quit." - return out -} -