calendar

[github]

this month like cal, today bracketed, time ticking below

ui 32×13 8 fps 3.0 kB reads the clock

use it

html

<script type="module" src="https://ascii.rest/ascii.js"></script>

<ascii-art piece="calendar" options='{"monday":false,"time":true}'></ascii-art>

No install and no build step. ascii.js defines the <ascii-art> tag, loads the piece from ascii.rest, plays it while it is on screen, and holds the first frame for anyone who prefers reduced motion.

react

// npm install github:bas3line/ascii
import { Ascii } from "ascii.rest/react";
import { calendar } from "ascii.rest/pieces";

<Ascii piece={calendar} options={{"monday":false,"time":true}} />

A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="calendar", to fetch the piece only when it mounts.

astro

---
// npm install github:bas3line/ascii
import Ascii from "ascii.rest/astro";
---

<Ascii piece="calendar" options={{"monday":false,"time":true}} />

Renders the first frame on the server, so the page is whole before any script runs, then plays the piece once it loads.

typescript

// npm install github:bas3line/ascii
import { mount } from "ascii.rest";
import { calendar } from "ascii.rest/pieces";

const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, calendar, {"monday":false,"time":true});

mount plays a piece in a <pre>, or on a <canvas> for the coloured ones, and returns a function that stops it. Every piece, option and frame is typed.

calendar.ts

[js] [github]
/*
 * calendar: this month laid out like the cal command, today in brackets,
 * with the weekday and the running time along the bottom.
 */
import type { Frame, Meta } from "../types.ts";

export interface CalendarOptions {
  [key: string]: unknown;
  monday: boolean;
  time: boolean;
}

export const meta = {
  name: "calendar",
  category: "ui",
  note: "this month like cal, today bracketed, time ticking below",
  cols: 32,
  rows: 13,
  fps: 8,
  options: { monday: false, time: true },
  clock: true,
} satisfies Meta<CalendarOptions>;

const DAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
const MONTHS = ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"];
const L = 2; // left margin; the grid is 7 cells of 4

export default function calendar({ monday = false, time = true }: Partial<CalendarOptions> = {}): Frame {
  const { cols, rows } = meta;
  const pad = (s: string) => (" ".repeat(L) + s).padEnd(cols).slice(0, cols);
  const centre = (s: string) => pad(" ".repeat(Math.max(0, Math.floor((28 - s.length) / 2))) + s);
  const order = monday ? [1, 2, 3, 4, 5, 6, 0] : [0, 1, 2, 3, 4, 5, 6];

  // The month only changes at midnight, so the grid is built once a day.
  let key = "";
  let page: string[] = [];
  const build = (y: number, m: number, d: number): string[] => {
    const lead = (new Date(y, m, 1).getDay() - order[0] + 7) % 7;
    const count = new Date(y, m + 1, 0).getDate();
    const weeks: string[] = [];
    for (let w = 0; w < 6; w++) {
      let line = "";
      for (let c = 0; c < 7; c++) {
        const n = w * 7 + c - lead + 1;
        if (n < 1 || n > count) line += "    ";
        else if (n === d) line += n < 10 ? " [" + n + "]" : "[" + n + "]";
        else line += " " + String(n).padStart(2) + " ";
      }
      weeks.push(pad(line));
    }
    return [
      " ".repeat(cols),
      centre(MONTHS[m] + " " + y),
      pad("─".repeat(28)),
      pad(order.map((i) => " " + DAYS[i].slice(0, 2) + " ").join("")),
      ...weeks,
      pad("─".repeat(28)),
    ];
  };

  // Play time ticks the clock so it steps evenly between frames; the wall
  // clock pulls it back whenever play was paused or fell behind.
  let offset: number | null = null;
  return (t) => {
    const wall = Date.now() / 1000;
    if (offset === null || wall - (offset + t) > 0.25 || offset + t - wall > 60) offset = wall - t;
    const now = new Date((offset + t) * 1000);
    const y = now.getFullYear(), m = now.getMonth(), d = now.getDate();
    if (key !== y + "-" + m + "-" + d) {
      key = y + "-" + m + "-" + d;
      page = build(y, m, d);
    }
    const two = (n: number) => String(n).padStart(2, "0");
    const day = DAYS[now.getDay()] + " " + d;
    const clock = time ? two(now.getHours()) + ":" + two(now.getMinutes()) + ":" + two(now.getSeconds()) : "";
    const foot = pad(" " + day + clock.padStart(26 - day.length));
    return [...page, foot, " ".repeat(cols)].slice(0, rows).join("\n");
  };
}