Lantern

Code Block.

A code panel with a filename bar, language badge, copy button, optional line numbers and highlighted lines. No syntax highlighter required.

startup.lualua
-- startup.lua: runs when the computer bootslocal modem = peripheral.find("modem")rednet.open(peripheral.getName(modem)) print("Lantern hub online")while true do  local id, message = rednet.receive("lantern")  print(("[%d] %s"):format(id, message))end

Installation#

With the CLI

$npx lanterncn add code-block

This also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/code-block. See Installation if your project is not set up yet.

Manually

  1. Install the dependencies

    npm install lucide-react
  2. Copy the source into your project

    components/ui/code-block.tsx
    "use client";
    
    import * as React from "react";
    import { Check, Copy } from "lucide-react";
    
    import { cn } from "@/lib/utils";
    
    function CodeBlockCopyButton({
      value,
      className,
      ...props
    }: Omit<React.ComponentProps<"button">, "value" | "onClick"> & { value: string | (() => string) }) {
      const [copied, setCopied] = React.useState(false);
      const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
    
      React.useEffect(() => () => {
        if (timer.current) clearTimeout(timer.current);
      }, []);
    
      async function copy() {
        try {
          await navigator.clipboard.writeText(typeof value === "function" ? value() : value);
          setCopied(true);
          if (timer.current) clearTimeout(timer.current);
          timer.current = setTimeout(() => setCopied(false), 1500);
        } catch {
          // Clipboard access can be blocked; leave the button in its idle state.
        }
      }
    
      return (
        <button
          type="button"
          data-slot="code-block-copy"
          data-copied={copied || undefined}
          aria-label={copied ? "Copied" : "Copy code"}
          onClick={copy}
          className={cn(
            "inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md border border-border bg-background/80 text-muted-foreground transition-colors outline-none hover:border-input hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-terminal [&_svg]:size-3.5",
            className,
          )}
          {...props}
        >
          {copied ? <Check className="text-success" /> : <Copy />}
          <span className="sr-only" aria-live="polite">
            {copied ? "Copied to clipboard" : ""}
          </span>
        </button>
      );
    }
    
    type CodeBlockProps = Omit<React.ComponentProps<"div">, "title"> & {
      /** Plain source text. Rendered line by line with the default Lantern text color. */
      code?: string;
      /** Filename or label shown in the title bar. */
      title?: React.ReactNode;
      /** Language badge in the title bar, e.g. "lua". */
      language?: string;
      /** Show line numbers. Applies to `code`. */
      showLineNumbers?: boolean;
      /** 1-based line numbers to mark with an orange bar and tint. Applies to `code`. */
      highlightLines?: number[];
      /** Max height of the scroll area, e.g. 320 or "20rem". */
      maxHeight?: number | string;
      /** Text copied by the copy button. Defaults to `code`, or the rendered text of `children`. */
      copyValue?: string;
      /** Hide the copy button. */
      copyable?: boolean;
    };
    
    /**
     * A code panel without a highlighter dependency. Pass `code` for plain text, or `children`
     * for content you highlighted yourself (for example shiki HTML).
     */
    function CodeBlock({
      code,
      title,
      language,
      showLineNumbers = false,
      highlightLines,
      maxHeight,
      copyValue,
      copyable = true,
      className,
      children,
      ...props
    }: CodeBlockProps) {
      const bodyRef = React.useRef<HTMLDivElement>(null);
      const hasHeader = title != null || language != null;
      const highlighted = React.useMemo(() => new Set(highlightLines ?? []), [highlightLines]);
      const lines = React.useMemo(() => (code === undefined ? null : code.replace(/\n$/, "").split("\n")), [code]);
    
      const getCopyValue = () => copyValue ?? code ?? bodyRef.current?.innerText ?? "";
    
      const copyButton = copyable ? <CodeBlockCopyButton value={getCopyValue} /> : null;
    
      return (
        <div
          data-slot="code-block"
          className={cn("relative min-w-0 overflow-hidden rounded-lg border bg-terminal text-terminal-foreground", className)}
          {...props}
        >
          {hasHeader && (
            <div
              data-slot="code-block-header"
              className="flex min-h-11 items-center justify-between gap-3 border-b bg-[#16211c] py-2 pr-2 pl-4"
            >
              <span className="min-w-0 truncate font-mono text-[11px] text-[#a8b89b]">{title}</span>
              <span className="flex shrink-0 items-center gap-2">
                {language && (
                  <span
                    data-slot="code-block-language"
                    className="rounded-sm border border-border px-1.5 py-1 font-mono text-[9px] leading-none tracking-[0.16em] text-muted-foreground uppercase"
                  >
                    {language}
                  </span>
                )}
                {copyButton}
              </span>
            </div>
          )}
          {!hasHeader && copyButton && <div className="absolute top-2 right-2 z-10">{copyButton}</div>}
          <div
            ref={bodyRef}
            data-slot="code-block-body"
            role="region"
            aria-label={typeof title === "string" ? title : "Code"}
            tabIndex={0}
            style={maxHeight !== undefined ? { maxHeight } : undefined}
            className={cn(
              "overflow-auto font-mono text-[13px] leading-[1.7] outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-inset",
              "[scrollbar-color:var(--color-input)_transparent] [scrollbar-width:thin]",
              "[&_pre]:m-0 [&_pre]:bg-transparent! [&_pre]:font-mono",
            )}
          >
            {lines ? (
              <pre className="py-4">
                <code className="grid w-max min-w-full">
                  {lines.map((line, i) => {
                    const n = i + 1;
                    const isHighlighted = highlighted.has(n);
                    return (
                      <span
                        key={i}
                        data-line={n}
                        data-highlighted={isHighlighted || undefined}
                        className={cn(
                          "flex border-l-2 border-transparent pr-12 pl-[14px]",
                          isHighlighted && "border-primary bg-primary/10",
                        )}
                      >
                        {showLineNumbers && (
                          <span
                            aria-hidden="true"
                            className={cn(
                              "mr-4 inline-block w-[3ch] shrink-0 text-right text-[#5f6f5a] select-none",
                              isHighlighted && "text-primary",
                            )}
                          >
                            {n}
                          </span>
                        )}
                        <span className="whitespace-pre">{line || " "}</span>
                      </span>
                    );
                  })}
                </code>
              </pre>
            ) : (
              <div className="[&_pre]:p-4">{children}</div>
            )}
          </div>
        </div>
      );
    }
    
    export { CodeBlock, CodeBlockCopyButton };
  3. Update the import paths to match your project

    The source imports cn from @/lib/utils.

Usage#

import { CodeBlock, CodeBlockCopyButton } from "@/components/ui/code-block";
<CodeBlock title="startup.lua" language="lua" code={source} showLineNumbers highlightLines={[2, 3]} />

Pass code as a plain string and it renders in the default terminal text color. To use your own highlighter, render its output as children instead, for example a div with shiki HTML, and set copyValue to the raw source.

Line numbers and highlighted lines apply to the code prop. Set maxHeight to cap the panel and scroll the rest.

Examples#

Line numbers and highlighted lines

empty.lualua
local turtle = require("turtle") for i = 1, 16 do  turtle.select(i)  turtle.dropDown()end print("Inventory emptied into the chest below")

Long file with max height

miner.lualua
-- miner.lua: dig a 3x3 tunnel and come homelocal length = tonumber(...) or 32local fuelNeeded = length * 2 + 10 local function refuel()  if turtle.getFuelLevel() >= fuelNeeded then return true end  for slot = 1, 16 do    turtle.select(slot)    if turtle.refuel(0) then      turtle.refuel()      if turtle.getFuelLevel() >= fuelNeeded then return true end    end  end  return falseend local function digColumn()  turtle.digUp()  turtle.digDown()end local function step()  while not turtle.forward() do    turtle.dig()    turtle.attack()  endend if not refuel() then  print("Not enough fuel, need " .. fuelNeeded)  returnend for i = 1, length do  turtle.dig()  step()  digColumn()  turtle.turnLeft()  turtle.dig()  turtle.turnRight()  turtle.turnRight()  turtle.dig()  turtle.turnLeft()end turtle.turnLeft()turtle.turnLeft()for i = 1, length do  step()end print("Tunnel finished, " .. length .. " blocks")