2026年4月13日星期一

70 Years of Typing

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.

This is the story about code typing. It all started from — forget the punch tape — the keyboard.

Fingers on keys, eyes on screen. The first programmers typed into the void and the void typed back in blinking green.

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.

Then the GUI arrived and promised to save us from typing. Eclipse rose as king — launch everything from one place, 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.

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.

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.

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.

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.

How many RAMs must an IDE eat up, before you call it garbage?

The answer, my friend, is still loading.

So we fled back to the terminal. Vim. Neovim. A text file and a thought.

And then, quietly, they came.

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.

The tide doesn't ask permission. It just comes in.

And then one day, something in the terminal typed back.

Not green phosphor. Not a compiler error. Something that understood.

Neo, is that you?

The former IBMer looked at the screen.

And it supplies plugins.

2026年4月7日星期二

Wake me up, when cronjob ends


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

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.

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 gh-review, which scans GitHub notifications and writes a pending-items file. If you don't have one, there's nothing here to notify you about.

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.


Building it required five choices that weren't obvious in advance.

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

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

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

cat > /tmp/cc-launch.sh << EOF
#!/bin/bash
source ~/.nvm/nvm.sh
cd ~/writer
claude --resume "$context_file"
EOF

This also solves the cron nvm problem for free.

Explicit --mode=cron. 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."

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.

Just pass the flag. The skill reads it and behaves accordingly.

Context injection based on draft state. Before launching Claude, check gh-pending.md:

if [ "$pending_count" -gt 0 ]; then
  claude --system "You have $pending_count pending items. Run /gh-review."
else
  claude -p "/gh-review"
fi

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 else branch handles the no-pending case — Claude runs a fresh scan rather than opening to an empty briefing.

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.


Known limitations: macOS-only. Requires iTerm2 Accessibility permission. /tmp/cc-open-skill.txt 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.

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.

The job ran while I wasn't paying attention, and when it needed me, it asked.


Same logic, I'm told, applies to flossing. I'll let you know how that goes.

2015年9月21日星期一

nodejs-persistable-scheduler: A nodejs based, persist by mysql, timely and repeatable task framework

In the past months, I were working in a volunteer project, which target is to create a P2P parking slot sharing platform for IBMers in Beijing. You must know that parking slots is what kind of cherish if you have ever living here.
As I am the back-end service architect of said platform, I was responsible to design/implement most part of the back-end services and transactions.
Inside the development, one of my technical requirement is a timely scheduler framework in NODEJS, which should has the following capabilities:
  • Customizable task functions;
  • Repeatable with customized frequency;
  • Persist the task data into a mysql DB;
  • Task executing status recording;
  • Fallback after restart the service;
  • Easy to use;
By going through the public modules inside NPM, I got nothing perfect candidate.
So finally I decide to build one. Now the module is finished. Following is the introduction of said module nodejs-persistable-scheduler:
A nodejs based, persist by mysql, timely and repeatable task framework;
==============================
A nodejs based, persist by mysql, timely task framework;
$ npm install nodejs-persistable-scheduler
  1. q: A library for promises (CommonJS/Promises/A,B,D);
  2. mysql: A node.js driver for mysql. It is written in JavaScript, does not require compiling, and is 100% MIT licensed.
This module is a nodejs based, persist by mysql, timely task framework;
There are 4 components that compose the whole module:
Which is repsonsible to doing the initialization works for making the functions of whole module available. In general, the initialization works are sperated into 2 parts: A system initialization task which will create 2 database tables named "fixtimetask" and "scanrecord" in the DB reference that discribed in the configurations; and a customized initialization task which will add maintaining level tasks(not user event triggered tasks, on some other words) if you need any.
This component will be executed ONLY ONCE (Well, actually, you can execute it more than once within some tricky way, referring to the (Best Prectice)[#tests:] please);
Which is responsible to doing 2 steps of fallback work evertime the current instance of nodejs-persistable-scheduler startup: 1. Find out the in-complete executing initialization and maintainess tasks in the last running from the persistance, and run them again; And 2 start the tasks maintainess monitor;
Which is a framework for registering/executing/delaying/hastening/cancelling/pausing/restarting tasks from customers;
Which is responsible to recording the initialization/maintainess task status into persistance while executing.

var scheduler = require('nodejs-persistable-scheduler');
var q = require('q');
var methodArr = [];
/***************************
 ***************************
  DONT CHANGE THE ORDER of mehtodArr!!!!
  JUST PUSH THE NEW ADDED ONES AT THE TAIL!!!
  **************************
  **************************/
method1 = function(param) {
  var defer = Q.defer();
  //functional codes for method1...
  return defer.promise;
};

methodArr.push({name:'METHOD1',method1});

method2 = function(param) {
  var defer = Q.defer();
  //functional codes for method2...
  return defer.promise;
};
methodArr.push({name:'METHOD2',method2});

initialTask = function() {
  var defer = Q.defer();
  var AM = new Date();
  AM.setHours(12,0,0);
  scheduler.registerTask("daily",AM,'METHOD1',null,null).then(function(ID) {
    if (ID)
    {
      defer.resolve(true);
    }
  },function(err) {
    defer.reject(err);
  });
  return defer.promise;
};

var schedulerConfig = {
  initFunc: initialTask, 
  scanInterval: 24*60*60*1000//scan interval time, in MS
};

var initialConfig = {
  db:
  {
    host: '',
    user: '',
    password: '',
    database:'',
    port: 
  },//DB config
  methods:methodArr,
  scheduler:schedulerConfig
};

scheduler.initialize(initialConfig);
Caution that:
  1. Please keep available of the DB naming 'fixtimetask' and 'scanrecord' for module 'nodejs-persistable-scheduler';
  2. Inside one NODEJS instance, or multiple NODEJS instances that sharing a same DB, only 1 'nodejs-persistable-scheduler' instance allowed.
  3. For different NODEJS instances that are not sharing a same DB, there could be multiple 'nodejs-persistable-scheduler' instances.
Tips:
If you want to re-initial the whole 'nodejs-persistable-scheduler' module (run the customized initialization module more than once), you can clean up the record in DB table 'scanrecord' with recordType equals to 0;
Refer the comments inside nodejs-persistable-scheduler/index.js please;

2008年11月11日星期二

Only a start...

I will record some technicial experiences in this BLOG. For my personal review, and for sharing with you, too.