WidgetCalendar

Demonstrates monthly calendar attributes with interactive cycling.

Dates are complex. Rendering them in a grid requires calculation of leap years, month lengths, and day-of-week offsets. Use this widget to skip the boilerplate.

This demo showcases the Calendar widget. It provides an interactive playground where you can toggle headers, weekday labels, and event highlights in real-time.

Use it to understand how to render time-based data grids efficiently.

Example

Run the demo from the terminal:

ruby examples/widget_calendar/app.rb

Source Code

# frozen_string_literal: true

#--
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
# SPDX-License-Identifier: MIT-0
#++

$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
require "ratatui_ruby"

# Demonstrates monthly calendar attributes with interactive cycling.
#
# Dates are complex. Rendering them in a grid requires calculation of leap years, month lengths, and day-of-week offsets.
# Use this widget to skip the boilerplate.
#
# This demo showcases the <tt>Calendar</tt> widget. It provides an interactive playground where you can toggle headers, weekday labels, and event highlights in real-time.
#
# Use it to understand how to render time-based data grids efficiently.
#
# === Example
#
# Run the demo from the terminal:
#
#   ruby examples/widget_calendar/app.rb
#
# rdoc-image:/doc/images/widget_calendar.png
class WidgetCalendar
  def initialize(date: nil)
    @date = date
  end

  def run
    RatatuiRuby.run do |tui|
      show_header = true
      show_weekdays = true
      show_surrounding = false
      show_events = true
      hotkey_style = tui.style(modifiers: [:bold])

      loop do
        now = @date || Time.now
        surrounding_style = show_surrounding ? tui.style(fg: "gray", modifiers: [:dim]) : nil

        events_map = if show_events
          {
            now => tui.style(fg: "green", modifiers: [:bold]),
            (now + (86400 * 2)) => tui.style(fg: "red", modifiers: [:underlined]),
            (now - (86400 * 5)) => tui.style(fg: "blue", bg: "white"),
          }
        else
          {}
        end

        calendar = tui.calendar(
          year: now.year,
          month: now.month,
          events: events_map,
          header_style: tui.style(fg: "yellow", modifiers: [:bold]),
          show_month_header: show_header,
          show_weekdays_header: show_weekdays,
          show_surrounding: surrounding_style,
          block: tui.block(borders: [:top, :left, :right])
        )

        controls = tui.paragraph(
          text: [
            tui.text_line(spans: [
              tui.text_span(content: " h/w/s/e", style: hotkey_style),
              tui.text_span(content: ": Toggle Header/Weekdays/Surrounding/Events  "),
              tui.text_span(content: "q", style: hotkey_style),
              tui.text_span(content: ": Quit"),
            ]),
            tui.text_line(spans: [
              tui.text_span(content: " Events: ", style: hotkey_style),
              tui.text_span(content: "Today (Green), +2d (Red), -5d (Blue) (#{show_events ? 'On' : 'Off'})"),
            ]),
          ],
          block: tui.block(title: " Controls ", borders: [:all])
        )

        tui.draw do |frame|
          calendar_area, controls_area = tui.layout_split(
            frame.area,
            direction: :vertical,
            constraints: [
              tui.constraint_min(0),
              tui.constraint_length(4),
            ]
          )
          frame.render_widget(calendar, calendar_area)
          frame.render_widget(controls, controls_area)
        end

        case tui.poll_event
        in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
          break
        in type: :key, code: "h"
          show_header = !show_header
        in type: :key, code: "w"
          show_weekdays = !show_weekdays
        in type: :key, code: "s"
          show_surrounding = !show_surrounding
        in type: :key, code: "e"
          show_events = !show_events
        else
          nil
        end

        sleep 0.05
      end
    end
  end
end

WidgetCalendar.new.run if __FILE__ == $PROGRAM_NAME