<?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: Slee Woo</title>
    <description>The latest articles on Forem by Slee Woo (@sleewoo).</description>
    <link>https://forem.com/sleewoo</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%2F90673%2F6b66f289-4920-466b-abab-f36af2372191.png</url>
      <title>Forem: Slee Woo</title>
      <link>https://forem.com/sleewoo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/sleewoo"/>
    <language>en</language>
    <item>
      <title>Finally learned how to close Neovim. Still can't leave</title>
      <dc:creator>Slee Woo</dc:creator>
      <pubDate>Fri, 01 May 2026 11:07:42 +0000</pubDate>
      <link>https://forem.com/sleewoo/finally-learned-how-to-close-neovim-still-cant-leave-17g6</link>
      <guid>https://forem.com/sleewoo/finally-learned-how-to-close-neovim-still-cant-leave-17g6</guid>
      <description>&lt;p&gt;There's a famous joke: &lt;em&gt;I've been using Vim for years because I don't know how to close it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now I finally learned how to close it.&lt;/p&gt;

&lt;p&gt;But still can't leave.&lt;/p&gt;




&lt;h2&gt;
  
  
  A brief museum of editors I loved
&lt;/h2&gt;

&lt;p&gt;Before Neovim there were others. There are always others.&lt;/p&gt;

&lt;p&gt;Zend Studio, for the veterans who remember. The IDE that came with a PHP license and a sense of seriousness. It knew what a project was before "project" meant a folder with a &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Eclipse, the cathedral next door - workspaces, perspectives, a plugin for every problem and three more for every solution. If you were coding in the 2000s, Eclipse wasn't your editor; it was your operating system.&lt;/p&gt;

&lt;p&gt;RubyMine, when Ruby was the future and autocomplete that actually understood your models felt like a small miracle. JetBrains in general - heavy, opinionated, smarter than you, willing to tell you so.&lt;/p&gt;

&lt;p&gt;Sublime. The editor that taught a generation of us that startup time is a feature. Command-P was a religious experience in 2013.&lt;/p&gt;

&lt;p&gt;Then the Electron wave. Atom first, then VS Code. Suddenly your editor was a browser, your browser was an editor, and somehow we all just accepted that 400MB of RAM was fine for typing.&lt;/p&gt;

&lt;p&gt;I used all of them. I liked all of them. I was, briefly, each of those people.&lt;/p&gt;

&lt;p&gt;Wonder how things would've gone if Zed had been around in 2015.&lt;/p&gt;




&lt;h2&gt;
  
  
  The plugins, sharpened over years
&lt;/h2&gt;

&lt;p&gt;Here's what nobody tells you about a decade in one editor: you don't accumulate plugins, you accumulate &lt;em&gt;positions&lt;/em&gt;. A plugin isn't a feature you installed, it's a decision you made about how you want to work, frozen into a line of config.&lt;/p&gt;

&lt;p&gt;Every keymap is a small vote you cast against your past self.&lt;/p&gt;

&lt;p&gt;Every leader-key combination is a sentence you taught your hands to speak.&lt;/p&gt;

&lt;p&gt;Ten years of that and the editor stops being software. It becomes a dialect.&lt;/p&gt;

&lt;p&gt;People quote that line - &lt;em&gt;perfection is achieved not when there is nothing more to add, but when there is nothing left to take away.&lt;/em&gt; And the older I get, the more I think the real version is even quieter: an ideal tool is one where there is nothing left to drop. Everything that's still there earned its way back, after you tried to remove it and missed it.&lt;/p&gt;

&lt;p&gt;My Neovim config is like that now. Not because I designed it. Because time did.&lt;/p&gt;




&lt;h2&gt;
  
  
  Five attempts
&lt;/h2&gt;

&lt;p&gt;I tried to switch to Zed about five times in the last year.&lt;/p&gt;

&lt;p&gt;Zed is genuinely great. Fast in the way Sublime was fast, but with the modern stack underneath. And the v1 release, with the agents finally something you can switch off and ignore - that removed the last excuse I had.&lt;/p&gt;

&lt;p&gt;Every time I opened it, wanted the switch to work.&lt;/p&gt;

&lt;p&gt;Every time, after a few hours, I was back in Neovim. Not because Zed did anything wrong. Because my hands kept reaching for things that weren't there in the same shape, and the small constant friction of that translation just... wore me down.&lt;/p&gt;

&lt;p&gt;One attempt is curiosity. Five is something else. Five means some part of me keeps pulling me back, and I can't quite negotiate with it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Vim-mode is not Vim
&lt;/h2&gt;

&lt;p&gt;Yes, Zed has a vim mode. Yes, it's technically excellent. The keybindings work. The motions are there. On paper it's a perfect bridge.&lt;/p&gt;

&lt;p&gt;Emotionally it's a different room.&lt;/p&gt;

&lt;p&gt;It's the difference between speaking a language and being translated into it. The surface matches. The underneath doesn't. You can feel the seam, even when you can't point at it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part I can't transfer
&lt;/h2&gt;

&lt;p&gt;Here's the sad part - and I don't even know if "sad" is the right word, I still can't quite name this feeling.&lt;/p&gt;

&lt;p&gt;None of this transfers.&lt;/p&gt;

&lt;p&gt;Ten years of muscle memory, ten years of config decisions, ten years of "oh I fixed that annoyance in 2019 and forgot it was ever annoying" - there's no export button for that. You can't &lt;code&gt;git clone&lt;/code&gt; a decade of embodiment into a new editor.&lt;/p&gt;

&lt;p&gt;And that's what it is, really. The editor isn't an application I use anymore, it's a prism I think through. Switching isn't learning a new tool, it's relearning the angle at which my thoughts come out.&lt;/p&gt;

&lt;p&gt;It's not age. I don't think it's age. A kid who's been deep in one editor for three years has the same thing in miniature. It's just what happens when a tool stops being external.&lt;/p&gt;




&lt;h2&gt;
  
  
  So
&lt;/h2&gt;

&lt;p&gt;I still use Neovim.&lt;/p&gt;

&lt;p&gt;Zed is on my machine. I open it sometimes. I'll probably try again - a sixth attempt, a seventh.&lt;/p&gt;

&lt;p&gt;Both editors are great. That part I'm sure of. The rest of it, the why-can't-I-just-switch part, I'm still sitting with.&lt;/p&gt;

&lt;p&gt;If you've felt this about any tool - an editor, a shell, a keyboard layout, a language you can't quite leave - I'd love to hear it. I suspect this isn't really a Neovim post. I suspect it's a post about how the things we use long enough start using us back.&lt;/p&gt;

&lt;p&gt;And not sure that's really a problem.&lt;/p&gt;

</description>
      <category>neovim</category>
      <category>zed</category>
      <category>vim</category>
      <category>discuss</category>
    </item>
    <item>
      <title>In the AI Agents Era, Why Waste Time Building a Framework?</title>
      <dc:creator>Slee Woo</dc:creator>
      <pubDate>Wed, 11 Mar 2026 14:44:40 +0000</pubDate>
      <link>https://forem.com/sleewoo/in-the-ai-agents-era-why-waste-time-building-a-framework-oni</link>
      <guid>https://forem.com/sleewoo/in-the-ai-agents-era-why-waste-time-building-a-framework-oni</guid>
      <description>&lt;p&gt;There is no answer to that question. Because the question is about the result - a pragmatically calculated, measurable, expected result.&lt;/p&gt;

&lt;p&gt;But building is about the process.&lt;/p&gt;

&lt;p&gt;It's about that zen. That quiet delight when you finally solve something that's been annoying you for years. Maybe it annoys only you. Maybe you're solving a problem that doesn't exist for anyone else. But now it's solved, and you get your couple minutes of glory.&lt;/p&gt;

&lt;p&gt;Now you can ditch all those validation libs - each with its own syntax, its own limitations - and just write TypeScript that gets validated at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineRoute&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;POST&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TRefine&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TRefine&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// validated before reaching here - no lib syntax, just TypeScript&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the breeze seems here to stay. But no - another annoyance starts to rise from the depths.&lt;/p&gt;

&lt;p&gt;After your 100th route, looking at a file-based routing tree is like a nightmare.&lt;br&gt;
What are all these files? They belong to what? Which is the master (handler), which is the servant (helper)?&lt;/p&gt;

&lt;p&gt;And the answer comes in the form of a question: why not organize routes as directories, with a handler file inside? Wait, but that's a lot of folders!&lt;/p&gt;

&lt;p&gt;Ok, let's sketch how it would look compared to the actual mess...&lt;/p&gt;

&lt;p&gt;Ten minutes later: &lt;em&gt;holy structure&lt;/em&gt;. What the order!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api/
  users/
    [id]/
      index.ts      ← handler for /api/users/:id
      helpers.ts    ← clearly not a route
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of weekends of spare time later: another annoyance is behind. Another minute of glory.&lt;/p&gt;

&lt;p&gt;So, is the breeze here forever? For a couple of hours perhaps, but...&lt;/p&gt;

&lt;p&gt;Why the heck are routes limited to monolithic segments like &lt;code&gt;posts/:id&lt;/code&gt;?&lt;br&gt;
How do you cover simple paths like &lt;code&gt;posts.json&lt;/code&gt; or &lt;code&gt;posts/1.json&lt;/code&gt; in a single route?&lt;br&gt;
Should you create a separate route for each? That's crazy!&lt;/p&gt;

&lt;p&gt;Time for a new zen. Turns out &lt;code&gt;path-to-regexp&lt;/code&gt; v8 is finally state-of-the-art in routing - so flexible, so delightful to integrate. And here it is, the new Power Syntax for routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;book{-:id}-info           ➜ /book-info or /book-123-info
locale{-:lang{-:country}} ➜ /locale, /locale-en, /locale-en-US
api/{v:version}/users     ➜ /api/users or /api/v2/users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So far so good.&lt;/p&gt;

&lt;p&gt;Now, finally, a comfortable weekend! Or not?&lt;br&gt;
Wait - forgot to wire auth middleware into the latest routes.&lt;br&gt;
A couple of Neovim strokes and we're good.&lt;/p&gt;

&lt;p&gt;A couple of Neovim strokes later: wait, how could I forget about auth middleware, it's essential!&lt;br&gt;
And how is it that in the 21st century you still have to manually wire middleware into each route?&lt;br&gt;
That's nonsense, that's anti-progress!&lt;/p&gt;

&lt;p&gt;One wasted weekend later: who said stylesheets can be cascading but middleware cannot?&lt;br&gt;
It's that easy - create a &lt;code&gt;use.ts&lt;/code&gt; in any folder and all underlying routes automatically wire the exported middleware. No imports. No repetition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api/
  admin/
    use.ts          ← auth runs for every route under /admin
    users/
      index.ts      ← inherits automatically
      [id]/
        index.ts    ← inherits automatically
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more wasted weekends because someone forgot to wire something.&lt;br&gt;
Now they'll be wasted for far more reasonable reasons.&lt;/p&gt;

&lt;p&gt;And one more annoyance has been harming the breeze for years.&lt;br&gt;
Whatever tricks or hacks tried to get a clean, type-safe, validated round-trip from client to server - none fully satisfies.&lt;/p&gt;

&lt;p&gt;Then a perfectly legal question hits: once there are typed validation targets on the server, why not use them to generate typed clients? Wait, even better - TypeBox runs perfectly in the browser, so why not use the same validation routines on the client?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fetchClients&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_/front/fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// fully typed, validated client-side before the request is even sent&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetchClients&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users/[id]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nc"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of weekends later: so productive now with type-safe, client-side validated fetch clients!&lt;/p&gt;

&lt;p&gt;Time for some rest, finally. Wait - a customer asks for an OpenAPI spec for their API.&lt;br&gt;
Ok, let's quickly wrap a script that gets all routes and generates the spec...&lt;/p&gt;

&lt;p&gt;Multiple "quickly wrap a script" iterations later: how can such a simple task need so much manual work?&lt;br&gt;
No way, there has to be an automated solution. And another zen is on the road - taking the AST-parsed routes with their params, payloads, and responses, and gluing them together into an automated OpenAPI 3.1 spec generator.&lt;/p&gt;

&lt;p&gt;And that zen doesn't come alone. It's accompanied by the sincere &lt;em&gt;wow&lt;/em&gt; of customers who discover they got detailed OpenAPI for free.&lt;/p&gt;

&lt;p&gt;And that's pretty much a lot - much more than any pragmatically calculated result expected from an apparently pointless effort.&lt;/p&gt;

&lt;p&gt;Because the real result is the sum of all those micro-achievements that push you forward.&lt;/p&gt;

&lt;p&gt;And no - the breeze isn't supposed to arrive once and stay forever.&lt;br&gt;
It lives in the movement, not the stillness.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>node</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
