diff --git a/main.go b/main.go index c77d1af..6c9c9b1 100644 --- a/main.go +++ b/main.go @@ -6,245 +6,8 @@ import ( "time" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" ) -const ( - numRows = 6 - numCols = 7 - cellW = 10 - cellH = 1 -) - -var ( - cellStyle = lipgloss.NewStyle().Width(cellW).Height(cellH).Align(lipgloss.Center) - selectedStyle = cellStyle.Copy().Bold(true).Background(lipgloss.Color("12")).Foreground(lipgloss.Color("15")) // Blue bg - unselectedStyle = cellStyle.Copy().Background(lipgloss.Color("236")).Foreground(lipgloss.Color("250")) - todayStyle = cellStyle.Copy().Background(lipgloss.Color("99")).Foreground(lipgloss.Color("15")).Bold(true) // Purple bg + white text - headerStyle = lipgloss.NewStyle().Width(numCols * cellW).Align(lipgloss.Center).Bold(true) - daysOfWeekStyle = lipgloss.NewStyle().Width(cellW).Align(lipgloss.Center).Bold(true) - - hourCellStyle = lipgloss.NewStyle().Width(cellW * 3).Height(cellH).Align(lipgloss.Left).PaddingLeft(1) - hourSelectedStyle = hourCellStyle.Copy().Bold(true).Background(lipgloss.Color("12")).Foreground(lipgloss.Color("15")) - // New style for current hour highlight (purple background, white text) - currentHourStyle = hourCellStyle.Copy().Background(lipgloss.Color("99")).Foreground(lipgloss.Color("15")).Bold(true) -) - -type viewMode int - -const ( - monthView viewMode = iota - hourlyView -) - -type model struct { - cursorRow int - cursorCol int - monthIndex int - year int - startOffset int // weekday offset where day 1 starts - daysInMonth int - - mode viewMode - - // For hourly view - selectedDay int - hourCursor int // which hour (0-23) is selected in hourly view -} - -func (m model) Init() tea.Cmd { - return nil -} - -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.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.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 -} - -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: - // selected hour gets blue highlight - out += hourSelectedStyle.Render(label) + "\n" - case hour == nowHour: - // current hour gets purple highlight - out += currentHourStyle.Render(label) + "\n" - default: - out += hourCellStyle.Render(label) + "\n" - } - } - - out += "\nPress ESC to return to month view, q to quit." - return out -} - func main() { now := time.Now() start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) diff --git a/readme.md b/readme.md index 7710b0f..7cc29c4 100644 --- a/readme.md +++ b/readme.md @@ -1 +1,48 @@ -# GoCal \ No newline at end of file +# 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.