package main import ( "log" "os" "strconv" "time" "unicode/utf8" "github.com/gdamore/tcell/v2" ) const ( dayWidth = 2 smallGap = 1 largeGap = 3 maxMonthWidth = 7*dayWidth + 6*smallGap maxMonthHeight = (31+6+(7-1))/7 + 2 /* maxMonthHeight is the height of a 31-day month starting on Sunday (month name and weekdays included) */ titleHeight = smallGap + 1 ) var ( months = [...]string{"", "Јануар", "Фебруар", "Март", "Април", "Мај", "Јун", "Јул", "Август", "Септембар", "Октобар", "Новембар", "Децембар"} weekdays = [...]string{"Недеља", "Понедељак", "Уторак", "Среда", "Четвртак", "Петак", "Субота"} ) var ( wall tcell.Screen defStyle tcell.Style todayStyle tcell.Style selStyle tcell.Style monthsWide int monthsHigh int monthsDrawn int ) var ( selTime time.Time today time.Time ) 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 date(year int, month time.Month, day int) time.Time { return time.Date(year, month, day, 0, 0, 0, 0, time.Local) } func lastDay(m time.Month) time.Time { return date(selTime.Year(), m+1, 0) } func monthHeight(m time.Month) int { return (offset(m) + lastDay(m).Day() - 1) / 7 } func centeredText(s string, x, y, width int, scr tcell.Screen, style tcell.Style) { start := x + max((width-utf8.RuneCountInString(s))/2, 0) i := 0 for _, char := range s { if i >= width { break } scr.SetContent(start+i, y, char, nil, style) i++ } } func wrappedText(s string, scr tcell.Screen, style tcell.Style) { x := 0 y := 0 w, h := scr.Size() for _, char := range s { if x >= w { if y >= h { break } x = 0 y++ } scr.SetContent(x, y, char, nil, style) x++ } } func bottomOfMonth(m time.Month, w time.Weekday) (time time.Time) { last := lastDay(m) day := last.Day() if w > last.Weekday() { day -= 7 } day += int(w - last.Weekday()) time = date(selTime.Year(), m, day) return } func topOfMonth(m time.Month, w time.Weekday) (time time.Time) { first := date(selTime.Year(), m, 1) day := 1 if w < first.Weekday() { day += 7 } day += int(w - first.Weekday()) time = date(selTime.Year(), m, day) return } func moveUp() { weekday := selTime.Weekday() newTime := selTime.AddDate(0, 0, -7) if newTime.Month() == selTime.Month() { selTime = newTime } else if int(selTime.Month()) > monthsWide { newMonth := selTime.Month() - time.Month(monthsWide) selTime = bottomOfMonth(newMonth, weekday) } } func moveDown() { weekday := selTime.Weekday() newTime := selTime.AddDate(0, 0, +7) if newTime.Month() == selTime.Month() { selTime = newTime } else if int(selTime.Month()) <= (12-1)/monthsWide*monthsWide { newMonth := selTime.Month() + time.Month(monthsWide) selTime = topOfMonth(newMonth, weekday) } } func offset(m time.Month) int { return (int(date(selTime.Year(), m, 1).Weekday()) + 6) % 7 } func rowInMonth(m time.Month, day int) int { return (day + offset(m) - 1) / 7 } func leftmostInRow(m time.Month, row int) time.Time { if row == 0 { return date(selTime.Year(), m, 1) } if row > monthHeight(m) { row = monthHeight(m) } day := row*7 + 1 - offset(m) return date(selTime.Year(), m, day) } func rightmostInRow(m time.Month, row int) time.Time { if row >= monthHeight(m) { return lastDay(m) } day := row*7 + 7 - offset(m) return date(selTime.Year(), m, day) } func moveRight() { if selTime.Weekday() != time.Sunday && selTime.Day() != lastDay(selTime.Month()).Day() { selTime = selTime.AddDate(0, 0, 1) } else if int(selTime.Month())%monthsWide != 0 { selTime = leftmostInRow(selTime.Month()+1, rowInMonth(selTime.Month(), selTime.Day())) } } func moveLeft() { if selTime.Weekday() != time.Monday && selTime.Day() != 1 { selTime = selTime.AddDate(0, 0, -1) } else if int(selTime.Month())%monthsWide != 1 && monthsWide != 1 { // special case, if monthsWide // is 1, (x%monthsWide) is 0 selTime = rightmostInRow(selTime.Month()-1, rowInMonth(selTime.Month(), selTime.Day())) } } func drawMonth(m time.Month, x, y int) { centeredText(months[m], x, y, maxMonthWidth, wall, defStyle) for i := 1; i <= 7; i++ { centeredText(weekdays[i%7], x+(i-1)*(dayWidth+smallGap), y+1, dayWidth, wall, defStyle) } dayNum := date(selTime.Year(), m+1, 0).Day() for i := 1; i <= dayNum; i++ { weekday := int(date(selTime.Year(), m, i).Weekday()) style := defStyle if today.Day() == i && today.Month() == m { style = todayStyle } else if selTime.Day() == i && selTime.Month() == m { style = selStyle } centeredText( strconv.Itoa(i), x+((weekday-1+7)%7)*(dayWidth+smallGap), y+2, 2, wall, style) if weekday == 0 { y++ } } } func drawWall() { wall.Clear() w, h := wall.Size() if w < maxMonthWidth+2*smallGap || h < maxMonthHeight+2*smallGap+titleHeight { wrappedText("Екран је премали за календар.", wall, defStyle) return } monthsWide = (w - 2*smallGap + largeGap) / (maxMonthWidth + largeGap) monthsHigh = (h - smallGap - titleHeight) / (maxMonthHeight + smallGap) monthsDrawn = min(monthsHigh*monthsWide, 12) startingMonth := time.Month((int(selTime.Month())-1)/monthsDrawn*monthsDrawn + 1) endingMonth := time.Month(min(12, int(startingMonth)+monthsDrawn-1)) centerVert := (w - monthsWide*maxMonthWidth - (monthsWide-1)*largeGap) / 2 for i, m := 0, startingMonth; i < monthsHigh; i++ { for j := 0; j < monthsWide; j++ { drawMonth(m, centerVert+j*(maxMonthWidth+largeGap), titleHeight+smallGap+i*(maxMonthHeight+smallGap)) if m >= endingMonth { return } m++ } } } func main() { today = time.Now() selTime = today var err error wall, err = tcell.NewScreen() if err != nil { log.Fatalf("%+v", err) } if err := wall.Init(); err != nil { log.Fatalf("%+v", err) } defStyle = tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset) todayStyle = tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.GetColor("blue")).Bold(true).Reverse(true) selStyle = tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.GetColor("green")).Bold(true) wall.SetStyle(defStyle) wall.Clear() drawWall() quit := func() { wall.Fini() os.Exit(0) } for { // Update screen wall.Show() // Poll event ev := wall.PollEvent() // Process event switch ev := ev.(type) { case *tcell.EventResize: case *tcell.EventKey: if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC || ev.Rune() == 'Q' || ev.Rune() == 'q' { quit() } switch ev.Rune() { case 'h': moveLeft() case 'j': moveDown() case 'k': moveUp() case 'l': moveRight() } } drawWall() wall.Sync() } }