package main import ( "fmt" "strings" "time" "github.com/charmbracelet/lipgloss" "github.com/dustin/go-humanize" ) type calendarTime struct { time.Time } // June 2025 // Su Mo Tu We Th Fr Sa // 1 2 3 4 5 6 7 // 8 9 10 11 12 13 14 // 15 16 17 18 19 20 21 // 22 23 24 25 26 27 28 // 29 30 var dayCols = map[int]time.Weekday{ 0: time.Monday, 1: time.Tuesday, 2: time.Wednesday, 3: time.Thursday, 4: time.Friday, 5: time.Saturday, 6: time.Sunday, } const brailleStart = 0x2800 const brailleEnd = 0x28FF var baseStyle = lipgloss.NewStyle().Width(7 * 3) var weekdayStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#2222f2")) var weekendStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#ff0022")) var monthNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00dd80")) var yearStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#a0dd10")) var calendarTitle = baseStyle.AlignHorizontal(lipgloss.Center) var dayStyle = lipgloss.NewStyle() var dayStartStyle = dayStyle.Background(lipgloss.Color("#555555")) var highlightStyle = dayStyle.Foreground(lipgloss.Color("#000000")).Background(lipgloss.Color("#ffffff")).Blink(true) // calendar takes three calendarTime's, the calendar month to show, the "start" calendar time // (shown with a underline) and the highlighted calendarTime. func calendar(t calendarTime, start calendarTime, hl calendarTime, styles map[string]lipgloss.Style) string { f, _ := t.firstLastDays() out := calendarTitle.Render(fmt.Sprintf("%s %s", monthNameStyle.Render(f.Month().String()), yearStyle.Render(fmt.Sprint(f.Year())))) out += "\n" out += weekdayStyle.Render(" Mo Tu We Th Fr ") out += weekendStyle.Render("Sa Su") out += "\n" at := f col := 0 lines := 0 for { if at.Weekday() == dayCols[col] { // codePoint := brailleStart + rand.Intn(brailleEnd-brailleStart+1) // brailleChar := rune(codePoint) // if rand.Intn(100) < 80 { // brailleChar = ' ' // } // pre := lipgloss.NewStyle().Foreground(lipgloss.Color("#d00010")).Render(string(brailleChar)) dayStyle := dayStyle specialStyle, ok := styles[at.Format(time.DateOnly)] if ok { dayStyle = specialStyle } dayString := fmt.Sprintf("%2d", at.Day()) padding := 3 - len(fmt.Sprint(at.Day())) padding = 1 out += strings.Repeat(" ", padding) if at.dayEqual(hl) { out += highlightStyle.Render(dayString) } else if at.dayEqual(start) { out += dayStartStyle.Render(dayString) } else { out += dayStyle.Render(dayString) } } else { col++ out += " " continue } at = calendarTime{at.AddDate(0, 0, 1)} if at.Month() != f.Month() { break } col++ if col > 6 { out += "\n" col = 0 lines++ } } for lines < 6 { out += "\n" lines++ } out += "\n" return out } func (ct calendarTime) dayEqual(t calendarTime) bool { y1, m1, d1 := ct.Date() y2, m2, d2 := t.Date() if y1 == y2 && m1 == m2 && d1 == d2 { return true } return false } func (ct calendarTime) firstLastDays() (calendarTime, calendarTime) { loc := ct.Location() y, m, _ := ct.Date() first := time.Date(y, m, 1, 0, 0, 0, 0, loc) m++ if m > 12 { y++ } last := time.Date(y, m, 1, 0, 0, 0, 0, loc).AddDate(0, 0, -1) return calendarTime{first}, calendarTime{last} } func (ct calendarTime) addDay() calendarTime { return calendarTime{ct.AddDate(0, 0, 1)} } func (ct calendarTime) subDay() calendarTime { return calendarTime{ct.AddDate(0, 0, -1)} } func (ct calendarTime) addWeek() calendarTime { return calendarTime{ct.AddDate(0, 0, 7)} } func (ct calendarTime) subWeek() calendarTime { return calendarTime{ct.AddDate(0, 0, -7)} } func (ct calendarTime) addMonth() calendarTime { return calendarTime{ct.AddDate(0, 1, 0)} } func (ct calendarTime) subMonth() calendarTime { return calendarTime{ct.AddDate(0, -1, 0)} } func (ct calendarTime) to(t calendarTime) string { from := t.Time to := ct.Time rel := humanize.RelTime(t.Time, ct.Time, "earlier", "later") days := int(from.Sub(to).Hours() / 24) return fmt.Sprintf("%s / %d days", rel, days) }