Posts tagged with “tools”

kitty showed huge gaps between characters — `monospace` was resolving to a CJK font

Every character in kitty had a massive gap after it, like the cell width was double what it should be. The obvious first move — swap font_family in kitty.conf — did nothing. Neither did the version a second pair of hands tried. Two failed attempts, and the config looked completely normal.

The config wasn't the problem. monospace is just a fontconfig alias, and on this machine fontconfig was handing it to a CJK font:

$ fc-match monospace
NotoSansCJK-Regular.ttc: "Noto Sans Mono CJK SC" "Regular"

kitty bases its cell width on the primary font, and a CJK font's cell is full-width — so narrow ASCII glyphs end up stranded in the middle of wide blank cells. The same kitty.conf was perfectly fine on another machine, because over there fc-match monospace returns DejaVu Sans Mono.

What was poisoning the alias was a file Ubuntu's language-selector drops in:

$ grep -A6 '<family>monospace</family>' \
    ~/.config/fontconfig/conf.d/64-language-selector-prefer.conf
		<family>monospace</family>
		<prefer>
			<family>Noto Sans Mono CJK SC</family>
			<family>Noto Sans Mono CJK JP</family>
			...

It prepends the Noto CJK mono fonts to the monospace alias, so they outrank every Latin mono. Comment out that one <alias> block and monospace goes back to DejaVu:

$ fc-match monospace
DejaVuSansMono.ttf: "DejaVu Sans Mono" "Book"

The reload trap that made this take an hour

Here's the part that hurts. After fixing fontconfig, pressing ctrl+shift+F5 to reload kitty... still showed the gaps. It looked exactly like the fix hadn't worked — the same dead end that had already wasted two attempts.

kitty caches its fontconfig resolution in-process. ctrl+shift+F5 re-reads kitty.conf, but it never re-queried fontconfig, so a change to how monospace resolves never reached the running kitty. Only quitting and reopening kitty — a fresh process — picked it up.

So the rule, painfully earned: when a kitty font change "doesn't work," before you conclude the fix is wrong, fully restart kitty and re-check. The fix may have been right all along; you were just looking at a cached font. And when the symptom is uniformly wide gaps across every character, run fc-match monospace before touching kitty.conf at all.

Pinning a gh account with `GH_TOKEN="$(gh auth token --user X)"`? Check it's non-empty

A common pattern for scripts that must always hit GitHub as a specific account, regardless of whatever account is currently active, is:

GH_TOKEN="$(gh auth token --user work-account)" gh workflow run ...

This works — until gh auth token --user work-account fails and returns an empty string. GH_TOKEN="" is not treated as "unset" by your shell, but it is treated as unset by gh itself, which then silently falls back to whatever account is currently active in the keyring. If that's your personal account and the target repo belongs to an org it can't see, you get a confusing HTTP 404: Not Found — which reads like a missing-repo problem, not an auth problem.

Guard the lookup instead of trusting it inline:

TOKEN="$(gh auth token --user work-account)"
if [ -z "$TOKEN" ]; then
    echo "Error: could not obtain gh token for work-account" >&2
    exit 1
fi
GH_TOKEN="$TOKEN" gh workflow run ...

Same fix applies to any wrapper that routes gh calls by repo owner: resolve the token first, fail loudly if it's empty, and only then invoke the real gh binary. Silent fallback to the active account is the failure mode to design out.

Excalidraw's hand-drawn look is free — Excalidraw+ only buys you cloud collab

I almost passed on the cute hand-drawn flowchart style, assuming it sat behind a subscription. It doesn't. The hand-drawn aesthetic is Excalidraw's default and only style, and it's completely free. The paid tier, Excalidraw+, is team cloud collaboration and version history — nothing to do with how the diagrams look.

mermaid.live is the same story: the official open-source editor for Mermaid.js, free, no credits, no pay-per-render. The "credit-based" impression usually comes from third-party SaaS tools that generate Mermaid from a prompt.

So going from a flowchart to a hand-drawn version costs nothing. Get your flowchart as Mermaid (write it, or hand a vision-capable AI a photo of your sketch), then paste it straight onto the Excalidraw canvas — it detects the syntax and pops a "Parse as Mermaid" import dialog. There is no Insert → Mermaid item in the hamburger menu or toolbar (those only have Open / Save / Export / Live Collaboration); the other entry point is the command palette (Ctrl/Cmd + /) → search Mermaid.

Vagrant Terminal always opens as Administrator? Turn UAC back on

On Windows, when you open Terminal in a Vagrant VM, it sometimes runs in Administrator mode regardless of which shell you choose — even when you explicitly selected normal user mode.

The fix is to restore User Account Control (UAC) prompts:

  1. Open Control Panel → System and Security
  2. Under "Security and Maintenance", click "Change User Account Control settings"
  3. Move the slider back to the default: "Notify me only when apps try to make changes to my computer"
  4. Click OK, then confirm with Yes

When UAC is fully disabled (slider at "Never notify"), Windows bypasses the elevation prompt and grants administrative privileges silently — which causes every Terminal session to launch as Administrator. Restoring UAC to its default level re-enables the elevation check and lets normal user sessions work normally again.

tg-relay 能否驱动非 tmux 的 Claude Code session?

tg-relay 的 inbound 路由依赖 tmux pane ID(%NN)——收到 Telegram 消息后,它调 tmux send-keys/tmp/tg-*.md 注入对应 pane。mux.driver local 跑的是前台进程,没有 tmux pane,所以 relay 找不到投递目标,直接失败。

Outbound 没问题:notify_shuke 不依赖 tmux,local driver 下照常发 TG,reply index 里用 MUX_DRIVER_SLUG 代替 %NN 记录身份。问题只在 inbound。

可行路径:named FIFO + Stop hook exit 2

每个 local session 启动时用 slug 创建一个 named FIFO:

mkfifo /tmp/mux-tg-${MUX_DRIVER_SLUG}.fifo

tg-relay 看到 reply index 里的 pane ID 是 slug 而非 %NN,就写这个 FIFO 而非调 tmux:

echo "$message" > /tmp/mux-tg-${slug}.fifo

Claude Code 的 Stop hook 在每个 turn 结束时检查这条 FIFO:

# Stop hook
fifo="/tmp/mux-tg-${MUX_DRIVER_SLUG}.fifo"
if read -t 0.1 msg < "$fifo" 2>/dev/null; then
    echo "$msg"
    exit 2   # 拦住 stop,把消息作为 additionalContext 注入
fi
exit 0

exit 2 的语义:Claude 不停止,stdout 作为 additionalContext(system feedback)注入同一个 turn,Claude 继续处理。技术上不是新的 user message,是 same-turn 的 system context,但效果上 Claude 会读到并响应 TG 消息。

局限

  • hook 只在 turn 边界触发。FIFO 里的消息要等当前 turn 结束才被拉取。如果turn结束时FIFO里是空的,那这个turn就正常结束了。没有机会再接到后续FIFO的内容。这是一个致命缺陷。让整个方案变得不再可行。
  • additionalContext ≠ user message:conversation history 里这不是一条用户消息,边角行为可能和正常 TG 路由有差异。
  • FIFO 阻塞:写端无读端时 echo > fifo 会阻塞,relay 需要用 O_NONBLOCK 或超时保护。

不完美,但架构上可行,不需要改动 Claude Code 本身。