<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Blunders n Builds]]></title><description><![CDATA[Mistakes, terminal setups, and tools that actually stuck — a learning log.]]></description><link>https://blundersnbuilds.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a8e5ca47ae97031fd65aead/291596d7-ef74-4420-a98d-febbfc882872.png</url><title>Blunders n Builds</title><link>https://blundersnbuilds.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 01 Sep 2026 16:14:01 GMT</lastBuildDate><atom:link href="https://blundersnbuilds.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[OCR Gives You Eyes, Not a Brain]]></title><description><![CDATA[Why reading text off a page and understanding what that text means turned out to be two completely different problems.
I went into a course on document extraction assuming modern OCR was basically an ]]></description><link>https://blundersnbuilds.hashnode.dev/ocr-gives-you-eyes-not-a-brain</link><guid isPermaLink="true">https://blundersnbuilds.hashnode.dev/ocr-gives-you-eyes-not-a-brain</guid><category><![CDATA[OCR ]]></category><category><![CDATA[Model]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[reasoning]]></category><category><![CDATA[Pipeline]]></category><dc:creator><![CDATA[Sushanth Kamabathula]]></dc:creator><pubDate>Tue, 01 Sep 2026 01:44:44 GMT</pubDate><content:encoded><![CDATA[<p><em>Why reading text off a page and understanding what that text means turned out to be two completely different problems.</em></p>
<p>I went into a course on document extraction assuming modern OCR was basically an improved version of old OCR. That's wrong: they're two unrelated methodologies that solve the same problem. The old approach, Tesseract is the standard example, is a hand-engineered pipeline, fixed rules for finding lines, then fixed rules for classifying characters. It's cheap to run, and still the right tool for a clean scanned document, a scanned novel doesn't need anything fancier than that. The new approach is fully data-driven, a model trained on examples instead of programmed with rules, and it's what makes real-world text (curved text, receipts, signage) tractable at all.</p>
<p>Even the good version of OCR has a limit though, and the phrase that stuck with me was "eyes without a brain." OCR can see that a cluster of pixels spells the word TOTAL. It has no idea that TOTAL is the field that actually matters, or which of the six numbers on the page belongs to it. Perception and cognition are two completely separate problems.</p>
<p>The fix is putting a reasoning layer on top instead of expecting the OCR model to somehow get smarter. The specific pattern is called ReAct, reason then act, and it's a loop: Thought, what do I need to do next, Action, call a tool like running OCR on a specific region, Observation, look at what came back, then loop again until it's done. What I liked about it was every thought gets logged, so you can actually see why the model made a call instead of just getting a final answer and having to trust it.</p>
<p>The framing that made the whole thing click was thinking of it as a stack instead of a single tool: pixels turn into text, text turns into structure, structure turns into something an agent can actually reason over. I'd been thinking of OCR as the hard part and everything after it as free, but it's actually the opposite, OCR is the easy layer. The part that's still hard is everything you build on top of it.</p>
]]></content:encoded></item><item><title><![CDATA[A Dashboard Shortcut I Built for a Class I Was Taking]]></title><description><![CDATA[One keybinding to start a Quarto preview, and cleanup code to make sure it never leaves a mess behind.
I was taking a class that used Quarto for basically everything, problem sets, write-ups, the work]]></description><link>https://blundersnbuilds.hashnode.dev/a-dashboard-shortcut-i-built-for-a-class-i-was-taking</link><guid isPermaLink="true">https://blundersnbuilds.hashnode.dev/a-dashboard-shortcut-i-built-for-a-class-i-was-taking</guid><category><![CDATA[neovim]]></category><category><![CDATA[quarto]]></category><category><![CDATA[markdown]]></category><category><![CDATA[terminal]]></category><dc:creator><![CDATA[Sushanth Kamabathula]]></dc:creator><pubDate>Mon, 31 Aug 2026 01:26:39 GMT</pubDate><content:encoded><![CDATA[<p><em>One keybinding to start a Quarto preview, and cleanup code to make sure it never leaves a mess behind.</em></p>
<p>I was taking a class that used Quarto for basically everything, problem sets, write-ups, the works, which meant constantly running <code>quarto preview file.qmd</code> in a terminal, tabbing over to check the rendered output, then remembering to kill the process when I was done. Except I never remembered. I'd close the terminal instead, and Quarto would keep running in the background, still watching a file I'd stopped editing.</p>
<p>So I wrote a function that does the whole cycle from inside the editor: one keybinding starts the preview, and it doesn't just fire off the process, it tracks the job per buffer so a second call knows to kill the first one instead of stacking previews on top of each other.</p>
<pre><code class="language-lua">local job_id = vim.fn.jobstart({ "quarto", "preview", file, "--timeout", "5" }, {
  on_exit = function(_, code)
    quarto_jobs[buf] = nil
    quarto_cleanup(file)
    if code ~= 0 and code ~= 143 then
      vim.notify("Quarto preview exited with code " .. code, vim.log.levels.ERROR)
    end
  end,
})
</code></pre>
<p>The <code>--timeout 5</code> flag does something I didn't expect the first time I read the Quarto docs: it makes the process self-exit once no browser client is still connected, which means closing the preview tab is enough to trigger cleanup. I don't have to remember to do anything on my end at all.</p>
<p>Cleanup itself was its own problem. Quarto doesn't just render to stdout, it writes an HTML file, a <code>_files</code> directory full of assets, and a <code>.quarto</code> cache, all sitting next to my source file. I didn't want any of that showing up in a git status I'd have to think about, so <code>on_exit</code> calls a cleanup function that deletes all three, every time the job ends.</p>
<p>The bug that took the longest to notice: none of this fired if I just closed the buffer inside Neovim instead of quitting Neovim entirely. The job kept running, the generated files kept sitting there, and I only caught it because I opened a preview, closed the buffer, and later found a <code>.quarto</code> folder from a file I hadn't touched in a week. Fixed with one more autocmd on <code>BufUnload</code> instead of only relying on Neovim actually exiting.</p>
<p>One argument-order thing that cost me more debugging time than it should have: the file path has to come before the flags in the <code>jobstart</code> table, or Quarto reads the whole thing as "preview the current directory" instead of "preview this file." I found it by process of elimination.</p>
]]></content:encoded></item><item><title><![CDATA[The Plugin That Replaced Eight Others]]></title><description><![CDATA[One config file, one plugin, and a feature I'd had switched off for months on an assumption.
At some point my Neovim config had a picker plugin, a dashboard plugin, a notification plugin, a terminal p]]></description><link>https://blundersnbuilds.hashnode.dev/the-plugin-that-replaced-eight-others</link><guid isPermaLink="true">https://blundersnbuilds.hashnode.dev/the-plugin-that-replaced-eight-others</guid><category><![CDATA[neovim]]></category><category><![CDATA[Lazygit]]></category><category><![CDATA[plugins]]></category><category><![CDATA[plugin]]></category><category><![CDATA[snacks]]></category><category><![CDATA[snacks.nvim]]></category><dc:creator><![CDATA[Sushanth Kamabathula]]></dc:creator><pubDate>Sat, 29 Aug 2026 02:31:49 GMT</pubDate><content:encoded><![CDATA[<p><em>One config file, one plugin, and a feature I'd had switched off for months on an assumption.</em></p>
<p>At some point my Neovim config had a picker plugin, a dashboard plugin, a notification plugin, a terminal plugin, a file explorer plugin, a zen-mode plugin, and a scratch-buffer plugin, each with its own config file, its own keymap conventions, its own way of doing borders and window styling. Snacks.nvim replaced all of it. I swapped it in over a weekend and deleted six files.</p>
<p>Fewer files is nice but it's not a story. The interesting part is a comment I left in my own config a while back that I finally circled back to:</p>
<pre><code class="language-lua">-- Was disabled on the assumption magick wasn't installed; :checkhealth
-- reports ImageMagick 7.1.2 present and Ghostty detected as a supported
-- terminal (it speaks the kitty graphics protocol), so both requirements
-- are actually met.
image = { enabled = true },
</code></pre>
<p>I'd turned off inline image rendering in markdown buffers months earlier because I thought I didn't have ImageMagick installed, and never actually checked. It sat disabled through however many <code>:Lazy sync</code> runs, until I ran <code>:checkhealth</code> for an unrelated reason and saw ImageMagick sitting there, and my terminal already speaking the right graphics protocol. The feature had been available the entire time I was telling myself it wasn't.</p>
<p>The rest of the plugin follows the same shape everywhere: check first, enable conditionally, don't assume. The lazygit integration does the same thing but out loud instead of silently:</p>
<pre><code class="language-lua">function()
  if vim.fn.executable("lazygit") == 1 then
    Snacks.lazygit()
  else
    vim.notify("lazygit not installed. Run: brew install lazygit", vim.log.levels.WARN)
  end
end,
</code></pre>
<p>Instead of a "command not found" from a shell-out gone wrong, missing lazygit just tells you what to do about it. Small thing, but it's the difference between a plugin that fails politely and one that fails like a stack trace.</p>
<p>None of this needed eight separate plugins to begin with, which is the lesson. A stack of single-purpose plugins feels more modular until you're maintaining eight sets of keymap conventions and eight border styles that don't match. One plugin that does all of it consistently, written by people who use it themselves, ends up being less config to reason about even though its own opts table is longer than any plugin's used to be. I didn't consolidate for the file count. I consolidated because the eight-plugin version kept breaking in ways a single well-maintained one doesn't.</p>
]]></content:encoded></item><item><title><![CDATA[Keybindings That Only Exist When They're Needed]]></title><description><![CDATA[Code lens, document color, linked editing range — three LSP features Neovim ships but never turns on, and what makes it safe to turn them on.
Neovim 0.12 shipped native support for a handful of LSP fe]]></description><link>https://blundersnbuilds.hashnode.dev/keybindings-that-only-exist-when-they-re-needed</link><guid isPermaLink="true">https://blundersnbuilds.hashnode.dev/keybindings-that-only-exist-when-they-re-needed</guid><category><![CDATA[neovim]]></category><category><![CDATA[nvim, ]]></category><category><![CDATA[terminal]]></category><category><![CDATA[learning]]></category><category><![CDATA[insights]]></category><dc:creator><![CDATA[Sushanth Kamabathula]]></dc:creator><pubDate>Fri, 28 Aug 2026 03:17:39 GMT</pubDate><content:encoded><![CDATA[<p><em>Code lens, document color, linked editing range — three LSP features Neovim ships but never turns on, and what makes it safe to turn them on.</em></p>
<p>Neovim 0.12 shipped native support for a handful of LSP features that almost nobody uses, because none of them are switched on by default and the servers that support them are not consistent about it. I found out they existed by reading the LSP client source instead of the docs.</p>
<p>Code lens is the clickable "2 references" or "Run Test" annotation you see above a function in VS Code. Document color swaps a hex code or <code>rgb()</code> value for an actual color swatch, as long as the server understands color, not just a plugin doing regex on the text. Linked editing range means renaming an opening HTML tag updates the closing one automatically, which nvim-ts-autotag never did on its own: autotag creates the closing tag, it never tried to keep the pair in sync afterward.</p>
<p>None of these are safe to always turn on, because asking a server for something it doesn't implement is an error. So the actual code is a small table of method-name-to-module pairs, checked against what the client claims to support:</p>
<pre><code class="language-lua">local capability_features = {
  { "textDocument/codeLens", vim.lsp.codelens },
  { "textDocument/documentColor", vim.lsp.document_color },
  { "textDocument/linkedEditingRange", vim.lsp.linked_editing_range },
}

vim.api.nvim_create_autocmd("LspAttach", {
  callback = function(ev)
    local client = vim.lsp.get_client_by_id(ev.data.client_id)
    for _, feature in ipairs(capability_features) do
      local method, mod = feature[1], feature[2]
      if client:supports_method(method) then
        mod.enable(true, { bufnr = ev.buf })
      end
    end
  end,
})
</code></pre>
<p>Every server that attaches to a buffer runs through the same check, and only gets the features it supports: clangd gets code lens, jsonls doesn't, and nothing breaks either way.</p>
<p>The other piece that mattered more: capabilities aren't set per server, they're set once on <code>'*'</code>, a config every server inherits before its own table gets merged on top. I almost put capabilities in each server's config instead, which would've worked for the servers I set up by hand. The problem is mason-lspconfig auto-enables any server the moment you install it, so a future <code>:MasonInstall gopls</code> would've quietly come up with Neovim's default capabilities instead of blink.cmp's — no snippets, no proper completion resolution, no folding range for ufo. Setting it once on the wildcard means every server gets the same baseline whether I remembered to configure it individually or not.</p>
<p>I added the wildcard fix after noticing a newly installed server behaving worse than the others, and only found the three LSP capabilities afterward, because I went looking for what else Neovim might be sitting on that I hadn't used yet.</p>
]]></content:encoded></item><item><title><![CDATA[jk Instead of Escape, and Other Muscle Memory I Had to Rewire]]></title><description><![CDATA[Insert mode maps, a leader key, and the small habits that make Neovim actually feel fast.
Escape is in the worst possible spot on the keyboard. Top left corner, a hand-stretch away from home row, and ]]></description><link>https://blundersnbuilds.hashnode.dev/jk-instead-of-escape-and-other-muscle-memory-i-had-to-rewire</link><guid isPermaLink="true">https://blundersnbuilds.hashnode.dev/jk-instead-of-escape-and-other-muscle-memory-i-had-to-rewire</guid><category><![CDATA[neovim]]></category><category><![CDATA[leader]]></category><category><![CDATA[terminal]]></category><category><![CDATA[setup]]></category><category><![CDATA[Shortcuts]]></category><dc:creator><![CDATA[Sushanth Kamabathula]]></dc:creator><pubDate>Thu, 27 Aug 2026 02:56:38 GMT</pubDate><content:encoded><![CDATA[<p>Insert mode maps, a leader key, and the small habits that make Neovim actually feel fast.</p>
<p>Escape is in the worst possible spot on the keyboard. Top left corner, a hand-stretch away from home row, and you hit it constantly in Vim, every single time you want to leave insert mode. After the mouse was gone (see the last post), Escape became the next obvious issue. So I remapped <code>jk</code>, typed together in insert mode, to act as Escape. Hit <code>j</code> then <code>k</code> fast enough and you're back in normal mode without your hands ever leaving home row.</p>
<p>The problem is obvious if you've ever typed the word "junk" or a sentence with "just kidding" in it. For about two weeks I'd occasionally get kicked out of insert mode mid-word for typing a totally normal <code>jk</code> sequence, and have to retype. It's a rare combination in real writing that it stopped being a problem once I got used to it, but it wasn't zero-cost the way I'd thought before actually using it.</p>
<p>Around the same time I started using the leader key properly. Vim gives you this modifier key, usually mapped to space, that does nothing on its own but becomes the prefix for basically every custom shortcut you build. <code>&lt;leader&gt;ff</code> to find files, <code>&lt;leader&gt;gs</code> for git status, that kind of thing. The appeal isn't any single mapping, it's that space is right under your thumb and never conflicts with an existing Vim command, so you get a second layer of shortcuts without stepping on anything.</p>
<p>The one that actually surprised me was auto-centering scroll. Search for something with <code>/</code> and jump to it, and by default the match lands wherever it happens to land on screen, sometimes right at the bottom edge with barely any context around it. I mapped the search-jump commands to append <code>zz</code>, which recenters the current line in the middle of the window.</p>
<p>None of these are impressive. A remapped key, a modifier, a scroll tweak. What actually changed my editor was doing about a dozen of these small rewires back to back until the friction that used to interrupt every few seconds mostly disappeared. I didn't notice it happening in the moment. I noticed it a month later when I sat down at someone else's default Vim setup and felt like I was working in mud.</p>
]]></content:encoded></item><item><title><![CDATA[Why My Mouse Doesn't Work In My Editor Anymore]]></title><description><![CDATA[Somewhere in my Neovim config there's a single line, opt.mouse = "", that turns the mouse off completely. Click anywhere in the editor and nothing happens. I added it mostly as an experiment to myself]]></description><link>https://blundersnbuilds.hashnode.dev/why-my-mouse-doesn-t-work-in-my-editor-anymore</link><guid isPermaLink="true">https://blundersnbuilds.hashnode.dev/why-my-mouse-doesn-t-work-in-my-editor-anymore</guid><category><![CDATA[neovim]]></category><category><![CDATA[mouse]]></category><category><![CDATA[keyboard]]></category><category><![CDATA[trackpad]]></category><dc:creator><![CDATA[Sushanth Kamabathula]]></dc:creator><pubDate>Thu, 27 Aug 2026 02:52:23 GMT</pubDate><content:encoded><![CDATA[<p>Somewhere in my Neovim config there's a single line, <code>opt.mouse = ""</code>, that turns the mouse off completely. Click anywhere in the editor and nothing happens. I added it mostly as an experiment to myself, expecting to revert it within a week once it got annoying.</p>
<p>That was months ago.</p>
<p>The logic was pretty simple: if the mouse works at all, I'll use it, and if I use it, I'll never get fast at the keyboard equivalent. Clicking to a specific line is easier than thinking about a motion, right until you've done it thousand times and never learned the motion. So I removed the easy option and forced myself to figure out the actual way to do things.</p>
<p>The first week was hard. Not the big stuff: jumping around a file with search and line numbers was easy to adapt to. It was the small stuff: clicking into a specific word to start editing; dragging to select a block of text I wanted to delete; clicking a tab to switch buffers. All of that had to be re-learnt as a keyboard motion, and for a few days I was slower than before, which is an annoying thing when the whole point was supposedly efficiency.</p>
<p>Once <code>f</code> and <code>t</code> motions to jump to a character, visual mode for selections, and buffer-switching keymaps became automatic, I stopped thinking about them at all, which is the whole goal of muscle memory in the first place.</p>
<p>The part I didn't expect: I now notice the problem in other editors. Reaching for a trackpad in VS Code to click somewhere feels like a problem now, not the default. That's probably the real reason that this was worth doing: not that I got faster (I did, eventually) but that the keyboard-only way stopped feeling harder.</p>
<p>I'm not going to pretend this works with everyone. If you're working with someone, or doing anything visual-heavy, a mouse is just correct. But for the kind of solo coding I do most of the day, cutting off the easy path turned out to be the thing that taught me the hard one.</p>
]]></content:encoded></item></channel></rss>