<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Fan Zheng</title>
    <description>The latest articles on Forem by Fan Zheng (@fan_zheng_29f1f5278f3b37c).</description>
    <link>https://forem.com/fan_zheng_29f1f5278f3b37c</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3868172%2F6a3eafe3-cd96-4074-a16a-9182c151b409.jpg</url>
      <title>Forem: Fan Zheng</title>
      <link>https://forem.com/fan_zheng_29f1f5278f3b37c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/fan_zheng_29f1f5278f3b37c"/>
    <language>en</language>
    <item>
      <title>70 Years of Typing</title>
      <dc:creator>Fan Zheng</dc:creator>
      <pubDate>Mon, 13 Apr 2026 17:52:08 +0000</pubDate>
      <link>https://forem.com/fan_zheng_29f1f5278f3b37c/70-years-of-typing-3e0g</link>
      <guid>https://forem.com/fan_zheng_29f1f5278f3b37c/70-years-of-typing-3e0g</guid>
      <description>&lt;p&gt;Many years later, as he watched an AI build and debug a mobile app from a TTY terminal, the former IBMer would remember that distant afternoon when he entered his fourteenth consecutive day configuring a React Native development environment in Eclipse.&lt;/p&gt;

&lt;p&gt;This is the story about code typing. It all started from — forget the punch tape — the keyboard.&lt;/p&gt;

&lt;p&gt;Fingers on keys, eyes on screen. The first programmers typed into the void and the void typed back in blinking green.&lt;/p&gt;

&lt;p&gt;Then came the editor wars. Vi saved every keystroke like the world was ending. Emacs consumed every keystroke like it was building one. Neither side won. Both sides are still fighting.&lt;/p&gt;

&lt;p&gt;Then the GUI arrived and promised to save us from typing. Eclipse rose as king — &lt;em&gt;launch everything from one place&lt;/em&gt;, they said at IBM. And they meant it. You didn't just code inside Eclipse. You coded Eclipse itself. Plugins became the language. Plugins became the culture. A plugin to manage your plugins. A team whose entire job was writing plugins for other teams' plugins. The platform had eaten its own builders, and the builders called it architecture. The snake ate itself, and was satisfied.&lt;/p&gt;

&lt;p&gt;It was magnificent. It was also four gigabytes and a five-minute startup time, held together by dependency trees nobody fully understood, maintained by teams that no longer existed.&lt;/p&gt;

&lt;p&gt;IntelliJ watched all of this and made the opposite bet. Opinionated. Integrated. Fast enough to think in. It charged money and people paid, gladly, just to escape. Eclipse didn't fall — it was abandoned, one migration at a time, until there was nothing left to abandon.&lt;/p&gt;

&lt;p&gt;Then VS Code arrived and rewrote the terms again. Free. Fast. An extension protocol so clean that the whole community poured in overnight. Not an IDE — an editor that knew its place, and grew into everything else. It isn't dead. It may never die. It is, at this moment, open in ten million windows.&lt;/p&gt;

&lt;p&gt;And then Electron. The dream was honest: build desktop apps with web technology, ship everywhere. It worked. Slack worked. VS Code itself worked. But then came the imitators — a hundred apps that were just websites nailed to a window frame, each carrying a full Chromium instance like a sailor carries an anchor.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How many RAMs must an IDE eat up, before you call it garbage?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer, my friend, is still loading.&lt;/p&gt;

&lt;p&gt;So we fled back to the terminal. Vim. Neovim. A text file and a thought.&lt;/p&gt;

&lt;p&gt;And then, quietly, they came.&lt;/p&gt;

&lt;p&gt;Aider, Cursor, Copilot, Gemini, Claude — different voices, the same instinct. Not a trend. Not a product cycle. Something older than that. The industry, without agreeing to, had remembered something it once knew: that the terminal was never primitive. It was just waiting for the other side to get smart enough.&lt;/p&gt;

&lt;p&gt;The tide doesn't ask permission. It just comes in.&lt;/p&gt;

&lt;p&gt;And then one day, something in the terminal typed back.&lt;/p&gt;

&lt;p&gt;Not green phosphor. Not a compiler error. Something that understood.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Neo, is that you?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The former IBMer looked at the screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it supplies plugins.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>history</category>
      <category>claudecode</category>
      <category>terminal</category>
    </item>
    <item>
      <title>Wake me up, when cronjob ends</title>
      <dc:creator>Fan Zheng</dc:creator>
      <pubDate>Wed, 08 Apr 2026 16:17:11 +0000</pubDate>
      <link>https://forem.com/fan_zheng_29f1f5278f3b37c/wake-me-up-when-cronjob-ends-52f9</link>
      <guid>https://forem.com/fan_zheng_29f1f5278f3b37c/wake-me-up-when-cronjob-ends-52f9</guid>
      <description>&lt;h1&gt;
  
  
  Wake me up, when cronjob ends
&lt;/h1&gt;




&lt;p&gt;Every morning at 9:03, a cron job scans my GitHub notifications, writes a summary to disk, and exits. The file sits in &lt;code&gt;gh-pending.md&lt;/code&gt;. Nobody opens it.&lt;/p&gt;

&lt;p&gt;The terminal window doesn't open. There's no sound. I make coffee and forget the thing exists. Then I remember, open the file, and find yesterday's summary sitting there having waited patiently for eight hours. This is not a workflow. This is a file.&lt;/p&gt;

&lt;p&gt;This assumes you already have a Claude Code cron skill — a slash command that runs non-interactively, writes output to disk, and exits. The one used here is &lt;a href="https://github.com/easyfan/gh-review" rel="noopener noreferrer"&gt;gh-review&lt;/a&gt;, which scans GitHub notifications and writes a pending-items file. If you don't have one, there's nothing here to notify you about.&lt;/p&gt;

&lt;p&gt;I built a fix: a shell + AppleScript pipeline that fires a persistent modal alert when the cron skill finishes, and when you click "Open CC," opens iTerm2 pre-loaded with context and ready to go.&lt;/p&gt;




&lt;p&gt;Building it required five choices that weren't obvious in advance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;display alert&lt;/code&gt;, not Notification Center.&lt;/strong&gt; Banners auto-dismiss. Modal alerts require a click. &lt;code&gt;giving up after 86400&lt;/code&gt; ensures it won't block forever — one day is enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iTerm2, not Ghostty.&lt;/strong&gt; Windows created via &lt;code&gt;open -na Ghostty&lt;/code&gt; don't register with macOS's Accessibility layer. System Events can't find them. I tried AXUIElement traversal, highest-PID strategy, &lt;code&gt;window-x/y&lt;/code&gt; flags — all failed with the same result: &lt;code&gt;System Events got an error: Can't get window 1 of process "Ghostty".&lt;/code&gt; iTerm2 has a complete, stable AppleScript dictionary. Ghostty stays on the taskbar. iTerm2 gets one job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The temp script trick.&lt;/strong&gt; iTerm2's &lt;code&gt;command&lt;/code&gt; parameter accepts a single binary path — compound shell commands get truncated at the first space. Fix: write the full launch command to &lt;code&gt;/tmp/cc-launch.sh&lt;/code&gt;, pass only that path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/cc-launch.sh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
#!/bin/bash
source ~/.nvm/nvm.sh
cd ~/writer
claude --resume "&lt;/span&gt;&lt;span class="nv"&gt;$context_file&lt;/span&gt;&lt;span class="sh"&gt;"
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also solves the cron nvm problem for free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explicit &lt;code&gt;--mode=cron&lt;/code&gt;.&lt;/strong&gt; Early versions inferred run mode from session history — fragile because session files can be missing, truncated, or from a half-finished interactive run, any of which tips the inference toward "interactive."&lt;/p&gt;

&lt;p&gt;The failure mode looked like success. The skill ran, exited clean, logged nothing anomalous. It just didn't write the summary file. I checked the script, checked permissions, re-ran it manually — worked fine. Ran it via cron — worked fine. Three mornings in a row the file sat empty, and three mornings in a row I assumed I'd looked too early or the scan had found nothing. On the fourth morning I added a debug log line and watched the skill enter the interactive code path, print a prompt, and wait — at 9:03 AM, in an unattended terminal, for a keypress that was never going to come.&lt;/p&gt;

&lt;p&gt;Just pass the flag. The skill reads it and behaves accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context injection based on draft state.&lt;/strong&gt; Before launching Claude, check &lt;code&gt;gh-pending.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pending_count&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;claude &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="s2"&gt;"You have &lt;/span&gt;&lt;span class="nv"&gt;$pending_count&lt;/span&gt;&lt;span class="s2"&gt; pending items. Run /gh-review."&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"/gh-review"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is a continuous transition: notification fires, you click, Claude opens already briefed — first message something like "You have 4 pending items from this morning's scan. Ready when you are." The &lt;code&gt;else&lt;/code&gt; branch handles the no-pending case — Claude runs a fresh scan rather than opening to an empty briefing.&lt;/p&gt;

&lt;p&gt;It works, except when the count is stale. Walk in, Claude announces twelve pending items; you cleared ten of them yesterday. It's a confident briefing about old news. The count is frozen at cron time, not now. Close enough for a morning habit.&lt;/p&gt;




&lt;p&gt;Known limitations: macOS-only. Requires iTerm2 Accessibility permission. &lt;code&gt;/tmp/cc-open-skill.txt&lt;/code&gt; is a shared file — two concurrent notifications would clobber each other (not a real problem: tasks run 5+ hours apart). crontab hardcodes the nvm path.&lt;/p&gt;

&lt;p&gt;The cron skill ran this morning at 9:03. The alert appeared at 9:04. I clicked "Open CC," iTerm2 opened, Claude was already briefed.&lt;/p&gt;

&lt;p&gt;The job ran while I wasn't paying attention, and when it needed me, it asked.&lt;/p&gt;




&lt;p&gt;Same logic, I'm told, applies to flossing. I'll let you know how that goes.&lt;/p&gt;

</description>
      <category>devtools</category>
      <category>macos</category>
      <category>automation</category>
      <category>claudecode</category>
    </item>
  </channel>
</rss>
