KeyCaster.spoon

February 12, 2026 · View on GitHub

Display your recent keystrokes on screen — perfect for screen recording, live demos, and tutorials.

KeyCaster shows a tasteful overlay that follows the monitor under your mouse, with a “KC” menubar icon while active. It supports two display modes, a drag-anywhere anchor, and pixel-accurate, word-safe grouping so labels never break awkwardly.

⚠️ Privacy: KeyCaster only visualizes keystrokes in real time. It does not store, send, or log what you type.


You might like these tools as well; CursorScope FocusMode


🎥 Demo

Column

Line

Drag


What’s new

  • Drag to move (⌘⌥ + left-drag) — place the overlay anywhere on screen.
  • Deterministic across displays — position stays in the same relative spot when you move to another monitor.
  • Menubar menu (KC) — switch Column / Line from the menu; Start/Stop entry included.
  • Column mode: pixel-measured fill (no early wraps), hard grouping (labels aren’t split), configurable groupJoiner.
  • Line mode: default overflow behavior (no time fade; old segments drop only when off-box), optional joiner.
  • Friendly glyphs for special keys (↩︎ ⎋ ⌫ arrows, F-keys) and modifiers (⌘⌥⌃⇧).

Requirements

  • Hammerspoon (recent versions)
  • macOS with Accessibility permissions granted to Hammerspoon → System Settings → Privacy & Security → Accessibility → enable Hammerspoon.

Installation

Option A — Clone into your Spoons directory

mkdir -p ~/.hammerspoon/Spoons
cd ~/.hammerspoon/Spoons
# Replace the URL below with your repo URL once you publish it
git clone https://github.com/YOURNAME/KeyCaster.spoon KeyCaster.spoon

Option B — Manual

Create the folder and copy init.lua into it:

mkdir -p ~/.hammerspoon/Spoons/KeyCaster.spoon
# Put init.lua here → ~/.hammerspoon/Spoons/KeyCaster.spoon/init.lua

Reload Hammerspoon after installation.


Quick Start

Add to ~/.hammerspoon/init.lua:

if hs.loadSpoon("KeyCaster") then
  spoon.KeyCaster
    :configure({
      -- Core
      mode = "column",               -- "column" | "line"
      fadingDuration = 2.0,
      maxVisible = 5,
      minAlphaWhileVisible = 0.35,
      followInterval = 0.40,
      ignoreAutoRepeat = true,

      -- Free placement (drag with ⌘⌥ to move)
      positionFree = { x = 20, y = 80 },  -- top-left anchor (px)

      -- Appearance
      font = { name = "Menlo", size = 18 },
      colors = {
        bg     = { red=0, green=0, blue=0, alpha=0.78 },
        text   = { red=1, green=1, blue=1, alpha=0.98 },
        stroke = { red=1, green=1, blue=1, alpha=0.15 },
        shadow = { red=0, green=0, blue=0, alpha=0.6 },
      },

      -- Column mode
      box = { w = 260, h = 36, spacing = 8, corner = 10 },
      column = {
        newBoxOnPause = 0.70,
        fillMode      = "measure",  -- pixel-based packing
        fillFactor    = 0.96,       -- new box when measured width > 96% of usable width
        hardGrouping  = true,       -- never split labels across boxes
        groupJoiner   = "",         -- "" for tight (e.g. ⌘C), " " or " " for spacing
        -- maxCharsPerBox is used only if you set fillMode="chars"
      },

      -- Line mode
      line = {
        box = { w = 520, h = 36, corner = 10 },
        maxSegments = 60,
        gap = 6,
        fadeMode = "overflow",      -- "overflow" = no time fade; trim when off-box, or "time"
        joiner = nil,                -- nil = reuse column.groupJoiner; "" or " " to override
      },

      -- Optional safety & filters
      respectSecureInput = true,    -- suppress while macOS secure input is active
      appFilter = nil,              -- e.g., { mode="deny", bundleIDs={"com.agilebits.onepassword7"} }
      showModifierOnly = false,     -- if true, show pure modifier chords (e.g., ⌘⇧)
      showMouse = { enabled = false, radius = 14, fade = 0.6, strokeAlpha = 0.35 }, -- click ripples
    })
    :bindHotkeys(spoon.KeyCaster.defaultHotkeys)
    :start() -- optional: start immediately
end

Hotkeys

  • Start: ⌃⌥⌘K
  • Stop: ⌃⌥⌘F

You’ll see KC in the menubar while KeyCaster is active. Click it to switch Column/Line or to Stop.


Usage Tips

  • Move the overlay: hold ⌘⌥ and drag the box. The stack grows down if the anchor is in the top half, or up if it’s in the bottom half.

  • Multi-display: the anchor is stored normalized, so it appears in the same relative spot when you move between monitors.

  • Grouping:

    • Column mode uses pixel-measured packing and hard grouping so labels aren’t split.
    • Set column.groupJoiner = " " for a spaced look, or " " (thin space) for subtle separation.
    • Line mode prefixes joiner before every segment except the first (defaults to column.groupJoiner).

Configuration Reference

Call spoon.KeyCaster:configure({...}) with any of the options below.

Core

KeyTypeDefaultDescription
modestring"column""column" or "line".
positionFree.x/ynumber20/80Free placement anchor (px). Drag with ⌘⌥ to update.
fadingDurationnumber2.0Seconds to fade items (used when time-based fading is active).
maxVisibleinteger5Newest N items won’t fade below minAlphaWhileVisible until they age out.
minAlphaWhileVisiblenumber0.35Minimum opacity for items in the newest maxVisible.
followIntervalnumber0.40How often (s) to re-lay out on the display under the mouse.
ignoreAutoRepeatbooleantrueIgnore key autorepeat events.

Appearance

KeyTypeDefaultDescription
font.namestring"Menlo"Font name. The Spoon resolves to an available monospace if missing.
font.sizenumber18Font size (pt).
colors.bgrgba{0,0,0,0.78}Background color.
colors.textrgba{1,1,1,0.98}Text color.
colors.strokergba{1,1,1,0.15}Border color.
colors.shadowrgba{0,0,0,0.6}Drop shadow color.

Column Mode

KeyTypeDefaultDescription
box.w/h/spacingnumber260/36/8Box width/height; vertical spacing (px).
box.cornernumber10Corner radius (px).
column.fillModestring"measure""measure" uses pixel width; "chars" uses maxCharsPerBox.
column.fillFactornumber0.96Start a new box when measured width exceeds this fraction of usable width.
column.newBoxOnPausenumber0.70Start a new box after this many seconds of inactivity.
column.hardGroupingbooleantrueDo not split labels across boxes.
column.groupJoinerstring""Joiner between labels when appending: "", " ", or " ".
column.maxCharsPerBoxint14Only used when fillMode="chars".

Line Mode

KeyTypeDefaultDescription
line.box.w/hnumber520/36Box width/height (px).
line.box.cornernumber10Corner radius (px).
line.maxSegmentsinteger60Max segments kept in memory.
line.gapnumber6Horizontal gap (px) between segments.
line.fadeModestring"overflow""overflow" (no time fade; drop when off-box) or "time" (fades by fadingDuration).
line.joinerstring|nilnilPrefix joiner before every segment except the first. nil → reuse column.groupJoiner.

Safety & Filters

KeyTypeDefaultDescription
respectSecureInputbooleantrueSuppress output when macOS Secure Keyboard Entry is active.
appFiltertable|nilnile.g., { mode="deny", bundleIDs={"com.agilebits.onepassword7"} }.
showModifierOnlybooleanfalseIf true, show pure modifier chords (e.g., ⌘⇧) when pressed alone.
showMousetablesee QSClick ripples: { enabled, radius, fade, strokeAlpha }.

Hotkeys

-- Defaults
spoon.KeyCaster.defaultHotkeys = {
  start = { {"ctrl","alt","cmd"}, "K" },
  stop  = { {"ctrl","alt","cmd"}, "F" },
}

-- Use defaults
spoon.KeyCaster:bindHotkeys(spoon.KeyCaster.defaultHotkeys)

-- Or customize
spoon.KeyCaster:bindHotkeys({
  start = { {"ctrl","alt","cmd"}, "K" },
  stop  = { {"ctrl","alt","cmd"}, "F" },
})

Customizing Key Symbols

KeyCaster lets you override the displayed symbol for any key by modifying spoon.KeyCaster.specialKeys or spoon.KeyCaster.punctuationKeys before calling :start().

hs.loadSpoon("KeyCaster")

-- Override default symbols
spoon.KeyCaster.specialKeys["return"] = "⏎"   -- change ↩︎ to alternative
spoon.KeyCaster.specialKeys["left"]   = "◀"   -- triangle arrow
spoon.KeyCaster.specialKeys["right"]  = "▶"

-- Add keys not in defaults
spoon.KeyCaster.specialKeys["f19"]  = "✧"     -- Hyper key
spoon.KeyCaster.specialKeys["kana"] = "あ"    -- JIS keyboard

spoon.KeyCaster:start()

Available tables:

TableKeys
specialKeysreturn, enter, escape, tab, space, delete, arrows, page up/down, home, end
punctuationKeyscomma, period, slash, backslash, grave, quote, semicolon, minus, equal, brackets

Both tables can be freely modified to match your preferences or keyboard layout.


Examples

1) Column mode with tight grouping (default)

spoon.KeyCaster:configure({
  mode = "column",
  column = { groupJoiner = "", fillMode = "measure", fillFactor = 0.96 },
})

2) Column mode with a thin space joiner

spoon.KeyCaster:configure({
  mode = "column",
  column = { groupJoiner = " " }, -- U+2009 THIN SPACE
})

3) Line mode, spaced segments, no gap

spoon.KeyCaster:configure({
  mode = "line",
  line = { joiner = " ", gap = 0, fadeMode = "overflow" },
})

4) Time-fade line mode

spoon.KeyCaster:configure({
  mode = "line",
  line = { fadeMode = "time" },  -- uses fadingDuration
  fadingDuration = 1.6,
})

Troubleshooting

  • Nothing appears

    • Check Accessibility permission for Hammerspoon.
    • Ensure the Spoon path is ~/.hammerspoon/Spoons/KeyCaster.spoon/init.lua.
    • Check Hammerspoon Console (⌘`) for errors.
  • Menubar icon missing

    • The KC icon shows while active. Start with ⌃⌥⌘K or spoon.KeyCaster:start().
  • Overlay on wrong display

    • The overlay follows the display under your mouse. Move the mouse to the target display.
  • Font errors

    • Set font.name to an installed font (e.g., "Menlo"). The Spoon falls back gracefully.

Contributing

PRs welcome! Please update examples and the configuration table when adding features, and test both modes across single/multi-display setups.

License

MIT License — see LICENSE.


Credits

Built on the awesome Hammerspoon ecosystem. Thanks to the community for ideas and prior art around keystroke viewers.