<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://gierdo.astounding.technology/feed.xml" rel="self" type="application/atom+xml" /><link href="https://gierdo.astounding.technology/" rel="alternate" type="text/html" /><updated>2026-08-08T12:35:42+00:00</updated><id>https://gierdo.astounding.technology/feed.xml</id><title type="html">gierdo’s blog</title><subtitle>Software is weird.</subtitle><author><name>Dominikus Gierlach</name></author><entry><title type="html">AI: Let your agent drive neovim</title><link href="https://gierdo.astounding.technology/blog/2026/08/08/nvim-editor-mcp" rel="alternate" type="text/html" title="AI: Let your agent drive neovim" /><published>2026-08-08T00:00:00+00:00</published><updated>2026-08-08T00:00:00+00:00</updated><id>https://gierdo.astounding.technology/blog/2026/08/08/nvim-editor-mcp</id><content type="html" xml:base="https://gierdo.astounding.technology/blog/2026/08/08/nvim-editor-mcp"><![CDATA[<script src="https://asciinema.org/a/1262569.js" id="asciicast-1262569" async="true"></script>

<p>In my <a href="/blog/2026/04/23/agentic-vim">previous post about agentic vim</a>, I wrote about how to integrate AI agents
into my <code class="language-plaintext highlighter-rouge">nvim</code> environment using the Agent Client Protocol (ACP) and
<code class="language-plaintext highlighter-rouge">codecompanion</code>. In that setup, <code class="language-plaintext highlighter-rouge">nvim</code> acts as the client, prompting the agent
and bringing the responses back into the editor buffers.</p>

<p>But what if we turn things around?</p>

<p>What if the AI agent is the driver, and we want to let it interact with our
running editor session? E.g., if I’m running an external agent CLI or a
pair-programming assistant, and I want it to be able to open a file at a
specific line, check LSP diagnostics to see if its changes compile, populate my
quickfix list, or send me notifications and let my entire vim flash by toggling
color schemes?</p>

<p>I didn’t find a solution, so I built
<a href="https://github.com/gierdo/nvim-editor-mcp"><code class="language-plaintext highlighter-rouge">nvim-editor-mcp</code></a>.</p>

<h2 id="the-problem">The Problem</h2>

<ul>
  <li>External AI agents or CLI assistants operate blindly, isolated from my
running editor session.</li>
  <li>When working on shared coding tasks, the agent cannot see what I see. It
doesn’t know my active buffer, cursor position, or the LSP diagnostics
currently flashing on my screen.</li>
  <li>Instead of a shared, interactive coding session, the workflow becomes
disjointed: the agent outputs file paths and line numbers in the terminal,
and I have to manually navigate to them.</li>
  <li>I want a seamless, bi-directional partnership where the agent can interact
directly with the Vim API to inspect my context in real time and guide me
through the code.</li>
  <li>And, of course, I want this integration to be loose and lightweight, without
forcing the entire agent runner to live inside a heavy <code class="language-plaintext highlighter-rouge">nvim</code> plugin.</li>
</ul>

<h2 id="solution">Solution</h2>

<p>An MCP server that exposes <code class="language-plaintext highlighter-rouge">nvim</code> operations as tools, bridging the agent’s
requests to <code class="language-plaintext highlighter-rouge">nvim</code> via RPC.</p>

<p>I created <a href="https://github.com/gierdo/nvim-editor-mcp"><code class="language-plaintext highlighter-rouge">nvim-editor-mcp</code></a>. It is a lightweight Python server.</p>

<p>Building it was pretty straightforward, mainly because of two very useful
abstractions.</p>

<ul>
  <li><strong><a href="https://github.com/jlonge4/fastmcp"><code class="language-plaintext highlighter-rouge">fastmcp</code></a></strong> abstracts away the entire
MCP boilerplate. It handles JSON-RPC serialization, protocol lifecycles, and
tool schema generation. You define your tools with a simple <code class="language-plaintext highlighter-rouge">@mcp.tool</code>
decorator, and <code class="language-plaintext highlighter-rouge">fastmcp</code> takes care of the rest.</li>
  <li><strong><a href="https://github.com/neovim/pynvim"><code class="language-plaintext highlighter-rouge">pynvim</code></a></strong> provides the Python
interface to <code class="language-plaintext highlighter-rouge">nvim</code>’s Msgpack-RPC API. Instead of worrying about socket-level
message passing, it gives us a clean, high-level API to command the editor,
run Lua code, and query buffer/diagnostic states directly.</li>
</ul>

<p>Together, they make bridging the agent protocol and editor state a breeze.</p>

<h3 id="how-it-connects">How it connects</h3>

<p><code class="language-plaintext highlighter-rouge">nvim</code> communicates over a Unix socket. When you open a terminal buffer inside
<code class="language-plaintext highlighter-rouge">nvim</code>, it automatically sets the <code class="language-plaintext highlighter-rouge">$NVIM</code> environment variable containing the
path to that socket.</p>

<p><code class="language-plaintext highlighter-rouge">nvim-editor-mcp</code> is designed to be zero-config:</p>

<ol>
  <li>If started from inside <code class="language-plaintext highlighter-rouge">nvim</code> (e.g., via a terminal buffer or an integrated
runner), it automatically reads <code class="language-plaintext highlighter-rouge">$NVIM</code>.</li>
  <li>If started externally, it falls back to connecting to a specified socket,
which can be identified by the agent</li>
</ol>

<h3 id="the-tools">The Tools</h3>

<p>Once connected, the server exposes the following tools to the AI agent:</p>

<table>
  <thead>
    <tr>
      <th>Tool</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">attach_nvim</code></td>
      <td>Connect to a <code class="language-plaintext highlighter-rouge">nvim</code> instance by socket path</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">open_file</code></td>
      <td>Open a file at a specific line</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">set_quickfix</code></td>
      <td>Populate the quickfix list</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">get_diagnostics</code></td>
      <td>Fetch LSP diagnostics</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">nvim_exec_lua</code></td>
      <td>Execute arbitrary Lua in <code class="language-plaintext highlighter-rouge">nvim</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">notify</code></td>
      <td>Show a notification in <code class="language-plaintext highlighter-rouge">nvim</code></td>
    </tr>
  </tbody>
</table>

<p>Because we expose <code class="language-plaintext highlighter-rouge">nvim_exec_lua</code>, the agent has a powerful fallback to do
basically anything it wants inside our editor session. For example, to check
the path of the current buffer, the agent can call <code class="language-plaintext highlighter-rouge">nvim_exec_lua</code> with:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">return</span> <span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_buf_get_name</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="client-configuration">Client Configuration</h3>

<p>To hook it up to an MCP client like Claude Desktop or any other MCP-enabled AI
CLI runner, just add it to your configuration file:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"mcpServers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"nvim-editor-mcp"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"nvim-editor-mcp"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>(Ensure <code class="language-plaintext highlighter-rouge">nvim-editor-mcp</code> is on your <code class="language-plaintext highlighter-rouge">$PATH</code>, which you can do easily by
installing it with <code class="language-plaintext highlighter-rouge">uv tool install
git+https://github.com/gierdo/nvim-editor-mcp.git</code>).</p>

<h2 id="tldr">tl;dr</h2>

<ul>
  <li>Exposing local development environments to AI agents via MCP enables
real-time, interactive pair-programming sessions between you and your agent.</li>
  <li><code class="language-plaintext highlighter-rouge">nvim-editor-mcp</code> lets external agents drive your editor and see what you see
by querying the <code class="language-plaintext highlighter-rouge">nvim</code> API directly (buffers, cursors, diagnostics).</li>
  <li><code class="language-plaintext highlighter-rouge">fastmcp</code> and <code class="language-plaintext highlighter-rouge">pynvim</code> act as clean abstractions, making it simple to bridge
the Model Context Protocol to <code class="language-plaintext highlighter-rouge">nvim</code>’s RPC interface.</li>
  <li>Zero-config connection discovery over local Unix sockets handles the plumbing
automatically.</li>
</ul>]]></content><author><name>Dominikus Gierlach</name></author><category term="programming" /><category term="vim" /><category term="ai" /><category term="mcp" /><category term="python" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Sh*t with git: worktrees</title><link href="https://gierdo.astounding.technology/blog/2026/07/22/git-worktrees" rel="alternate" type="text/html" title="Sh*t with git: worktrees" /><published>2026-07-22T00:00:00+00:00</published><updated>2026-07-22T00:00:00+00:00</updated><id>https://gierdo.astounding.technology/blog/2026/07/22/git-worktrees</id><content type="html" xml:base="https://gierdo.astounding.technology/blog/2026/07/22/git-worktrees"><![CDATA[<h1 id="sht-with-git-worktrees">Sh*t with <code class="language-plaintext highlighter-rouge">git</code>: worktrees</h1>

<p>Since about the first year of my studies I’ve been using <code class="language-plaintext highlighter-rouge">git</code>. I manage and
version my personal documents, letters, system configuration, notes, tax
declaration (oioi, I should really finish last year’s taxes soon..), basically
my digital life with <code class="language-plaintext highlighter-rouge">git</code>, using a little server with <code class="language-plaintext highlighter-rouge">gitolite</code> as
home-local server and synchronization point where needed.</p>

<p>Additionally, of course, I’ve been working with <code class="language-plaintext highlighter-rouge">git</code>. Basically every project I
worked with in the last many years was versioned with <code class="language-plaintext highlighter-rouge">git</code>, with the exeption
of <code class="language-plaintext highlighter-rouge">perforce</code> being used by my first employer (🤮, not the employer, great
people. I had a lot of fun there and learned a lot. But perforce…)</p>

<p>Anyway, what I am going for is, I have used <code class="language-plaintext highlighter-rouge">git</code> for a long time. I have
understood <code class="language-plaintext highlighter-rouge">git</code> for a bit less time, I have been confused by <code class="language-plaintext highlighter-rouge">git</code> many times
and I spent a lot of time trying to resolve merge conflicts. I think I have a
moderately solid understanding of the basic concepts of <code class="language-plaintext highlighter-rouge">git</code>, I know
implementation details and I can explain the difference between a <code class="language-plaintext highlighter-rouge">git merge</code>
and a <code class="language-plaintext highlighter-rouge">git rebase</code>. For the last few years, I have just simply used <code class="language-plaintext highlighter-rouge">git</code>
without thinking about it and adapting my basic workflow.</p>

<p>Until about 3 weeks ago.</p>

<p>I have heard of <a href="https://git-scm.com/docs/git-worktree">worktree</a> before, but
never passed the stage of “Huh, interesting, I have to look into that one day.”</p>

<p>Whenever I wanted to quickly fix something on main, wanted to review a specific
branch or for whatever other reason do something outside of the context of the
branch I am currently working on, I would stash / commit my current changes,
switch branches, do the thing, switch back and either pop the stash or reset my
branch / amend to the temporary commit.</p>

<p>Whenever I needed parallel states of a repository, e.g. because binary files
have to be examined or for a bit of crude copy paste action or parallel work or
whatever, I would create physical copies of the repository in the same top
level directory, à <code class="language-plaintext highlighter-rouge">my_repo</code> and <code class="language-plaintext highlighter-rouge">my_repo_copy</code>. Which is a bit sh*t, for
several reasons. Nobody needs several identical index directories, and it’s
just confusing as well as a lot of manual action.</p>

<h2 id="the-problem">The Problem</h2>

<p>I have to or want to quickly work on a different branch than my current working
state, or I want to work with several branches at the same time.</p>

<p>I don’t want to create intermediate commits, stash and pop, manually copy paste
and create error prone workflows that need manual cleanup.</p>

<h2 id="solution">Solution</h2>

<script src="https://asciinema.org/a/1261378.js" id="asciicast-1261378" async="true"></script>

<p><code class="language-plaintext highlighter-rouge">git worktree</code>!</p>

<p>Manage multiple working trees attached to the same repository.</p>

<p>A <code class="language-plaintext highlighter-rouge">git</code> repository can support multiple working trees, allowing you to check
out more than one branch at a time. With <code class="language-plaintext highlighter-rouge">git worktree add</code> a new working tree
is associated with the repository, along with additional metadata that
differentiates that working tree from others in the same repository. The
working tree, along with this metadata, is called a “worktree”.</p>

<p>What does that mean?</p>

<p>I have a repository, e.g. the one with this content from
<a href="https://github.com/gierdo/gierdo.github.io">github</a>.</p>

<p>It’s cloned locally in <code class="language-plaintext highlighter-rouge">gierdo.github.io</code> and checked out to <code class="language-plaintext highlighter-rouge">master</code>. Now, I
want to work on the new article about worktrees in the branch <code class="language-plaintext highlighter-rouge">worktrees</code>. I
could run a <code class="language-plaintext highlighter-rouge">git checkout -b worktrees</code> or <code class="language-plaintext highlighter-rouge">git checkout worktrees</code> and switch
branches on my “main worktree”, which is, well, the main worktree you always
get when you clone or init a repository.</p>

<p>Or I could checkout the branch to a “linked worktree” under a specified
directory, reusing my entire rest of the setup and the already present <code class="language-plaintext highlighter-rouge">.git/</code>,
with <code class="language-plaintext highlighter-rouge">git worktree add -b worktrees place_for_my_worktrees/worktrees</code> or <code class="language-plaintext highlighter-rouge">git
worktree add worktrees place_for_my_worktrees/worktrees</code></p>

<p>This creates a directory <code class="language-plaintext highlighter-rouge">place_for_my_worktrees/worktrees</code>, which contains the
(you guessed it) linked worktree.</p>

<p>Pretty handy!</p>

<p>To make it even more handy for me and support shell completion including fuzzy
searching, I configured a bit of sugar around it.</p>

<p>In my global gitconfig, there are two <em>alias</em> configurations, allowing me to
make sure that all worktrees are added under the same repository-local
directory (<code class="language-plaintext highlighter-rouge">worktrees/</code>):</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[alias]</span>
  <span class="py">worktree-add</span> <span class="p">=</span> <span class="s">"!f() { git worktree add -b </span><span class="se">\"</span><span class="s">$1</span><span class="se">\"</span><span class="s"> </span><span class="se">\"</span><span class="s">worktrees/$1</span><span class="se">\"</span><span class="s">; }; f"</span>
  <span class="py">worktree-checkout</span> <span class="p">=</span> <span class="s">"!f() { git worktree add </span><span class="se">\"</span><span class="s">worktrees/$1</span><span class="se">\"</span><span class="s"> </span><span class="se">\"</span><span class="s">$1</span><span class="se">\"</span><span class="s">; }; f"</span>
</code></pre></div></div>

<p>My global gitignore contains the <code class="language-plaintext highlighter-rouge">woktrees</code> directory, as my worktrees are
in-tree of the parent “main tree”. Yes, this is potentially a bit dangerous,
but also very convenient. I’m a bit undecided about this, but in all 3 weeks of
experience with it I haven’t encountered any issues, and it’s easy enough to
change if needs be..</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>...
tags
.tool-versions
.vim
worktrees
...
</code></pre></div></div>

<p>My custom zsh completion configuration contains the completion definition to
complete branches for the git worktree checkout, which makes it behave like a
normal git checkout, including fuzzy completion if configured correctly.</p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#compdef git-worktree-checkout</span>

_git-worktree-checkout<span class="o">()</span> <span class="o">{</span>
    _values <span class="s1">'branches'</span> <span class="k">${${</span><span class="p">(f)</span><span class="s2">"</span><span class="si">$(</span>git <span class="k">for</span><span class="nt">-each-ref</span> <span class="nt">--format</span><span class="o">=</span><span class="s1">'%(refname:short)'</span> refs/heads refs/remotes 2&gt;/dev/null<span class="si">)</span><span class="s2">"</span><span class="k">}:-</span><span class="s2">""</span><span class="k">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>If you are interested in the full configuration, it’s all <a href="https://github.com/gierdo/dotfiles">here</a>.</p>

<h2 id="tldr">tl;dr</h2>

<ul>
  <li>sometimes you need several branches or you have to quickly switch branches</li>
  <li>workarounds are possible</li>
  <li>workarounds are workarounds</li>
  <li><code class="language-plaintext highlighter-rouge">git worktree</code> might be better than the alternatives</li>
</ul>]]></content><author><name>Dominikus Gierlach</name></author><category term="programming" /><category term="git" /><category term="worktrees" /><summary type="html"><![CDATA[Sh*t with git: worktrees]]></summary></entry><entry><title type="html">AI: Agentic vim - local, cloudy, losely coupled?</title><link href="https://gierdo.astounding.technology/blog/2026/04/23/agentic-vim" rel="alternate" type="text/html" title="AI: Agentic vim - local, cloudy, losely coupled?" /><published>2026-04-23T00:00:00+00:00</published><updated>2026-04-23T00:00:00+00:00</updated><id>https://gierdo.astounding.technology/blog/2026/04/23/agentic-vim</id><content type="html" xml:base="https://gierdo.astounding.technology/blog/2026/04/23/agentic-vim"><![CDATA[<h1 id="ai-agentic-vim---local-cloudy-losely-coupled">AI: Agentic vim - local, cloudy, losely coupled?</h1>

<p>I have been playing around with / working productively with <em>AI</em> in vim for a
while now. For religious and practical reasons <a href="/blog/2024/03/02/llama-igpu-amd-container">with local llms</a>, but also with <em>AI agents</em>,
namely <a href="https://kiro.dev/cli/"><code class="language-plaintext highlighter-rouge">kiro-cli</code></a>,
<a href="https://opencode.ai/de"><code class="language-plaintext highlighter-rouge">opencode</code></a> and <a href="https://geminicli.com/"><code class="language-plaintext highlighter-rouge">gemini
cli</code></a>, integrated into my workflow and environment with
<a href="https://github.com/folke/sidekick.nvim"><code class="language-plaintext highlighter-rouge">sidekick</code></a>.</p>

<p>Through <code class="language-plaintext highlighter-rouge">sidekick</code>, the different AI agents were integrated in a separate
buffer but with common and uniform keybindings, context integration (file
content, diagnostics, ..) etc.</p>

<p>Still, every AI agent has its own interaction model, UI details, look and feel
etc, and environment integration by <code class="language-plaintext highlighter-rouge">sidekick</code>, while impressive and mostly
surprisingly stable, was rather hacky.</p>

<p>For my personal environment, I’ve been using <code class="language-plaintext highlighter-rouge">gemini</code>. My employer only
provides <code class="language-plaintext highlighter-rouge">kiro-cli</code> and (limited) <code class="language-plaintext highlighter-rouge">opencode</code>, and I want to have an as-uniform
experience as possible. So, for a while I would have wanted to have an
abstraction over the cli agents in my environment.</p>

<p>Theoretically, that’s what
<a href="https://agentclientprotocol.com/get-started/introduction"><em>ACP</em></a> is for.
However, <em>ACP</em> support in <code class="language-plaintext highlighter-rouge">kiro-cli</code> has not been present from the start, and I
became rather familiar with the setup I had in the meantime.</p>

<p>Until I had a bit of time at my hands to touch the setup again last week.</p>

<h2 id="the-problem">The Problem</h2>

<ul>
  <li>I use <code class="language-plaintext highlighter-rouge">nvim</code></li>
  <li>I (have to) use different AI agents (<code class="language-plaintext highlighter-rouge">kiro-cli</code>, <code class="language-plaintext highlighter-rouge">opencode</code>, <code class="language-plaintext highlighter-rouge">gemini</code>)</li>
  <li>I want a uniform look and feel, configuration, capabilities, etc. in my
<code class="language-plaintext highlighter-rouge">nvim</code> environment, no matter which AI agent powers my AI workflow</li>
  <li>In most cases, the environment integration is more important to me than the
agent specific capabilities and advantages that might be lost with it,
especially when it comes to agent autonomy and orchestration</li>
</ul>

<h2 id="possible-solution">Possible solution</h2>

<p>The Agent Client Protocol
<a href="https://agentclientprotocol.com/get-started/introduction">(<em>ACP</em>)</a>
standardizes communication between code editors/IDEs and coding agents.</p>

<p>If all used agents support the <em>ACP</em>, a single <em>ACP</em> client configuration could
provide the solution I want.</p>

<p>Sadly, when I started the fun with “my” main agent <code class="language-plaintext highlighter-rouge">kiro-cli</code>, it has not
supported <em>ACP</em> yet.</p>

<h2 id="solution">Solution</h2>

<p>A while ago, <code class="language-plaintext highlighter-rouge">kiro-cli</code> added support for <em>ACP</em> 🥳!</p>

<p>No more reason not to try it out.</p>

<p>After a bit of research, I decided to use
<a href="https://codecompanion.olimorris.dev/"><em>codecompanion</em></a> to replace <code class="language-plaintext highlighter-rouge">sidekick</code>
for the AI integration.</p>

<p>The full configuration can be found
<a href="https://github.com/gierdo/dotfiles/blob/8992099423a5cbea84bc48d51918e50ea826955d/.config/nvim/lua/plugins/ai.lua">here</a>,
but I will write about a few details of the configuration.</p>

<h3 id="keybindings">Keybindings</h3>

<p>I had the following <code class="language-plaintext highlighter-rouge">sidekick</code> keybindings:</p>

<p><code class="language-plaintext highlighter-rouge">A-a</code> to open/toggle the agent window, including the selection as context, and
<code class="language-plaintext highlighter-rouge">A-i</code> to toggle with the predefined prompt catalogue.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">keys</span> <span class="o">=</span> <span class="p">{</span>
    <span class="p">{</span>
      <span class="s2">"&lt;A-a&gt;"</span><span class="p">,</span>
      <span class="k">function</span><span class="p">()</span>
        <span class="nb">require</span><span class="p">(</span><span class="s2">"sidekick.cli"</span><span class="p">).</span><span class="n">toggle</span><span class="p">({</span> <span class="n">filter</span> <span class="o">=</span> <span class="p">{</span> <span class="n">installed</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">})</span>
      <span class="k">end</span><span class="p">,</span>
      <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Sidekick Toggle"</span><span class="p">,</span>
      <span class="n">mode</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"n"</span><span class="p">,</span> <span class="s2">"t"</span><span class="p">,</span> <span class="s2">"i"</span> <span class="p">},</span>
    <span class="p">},</span>
    <span class="p">{</span>
      <span class="s2">"&lt;A-a&gt;"</span><span class="p">,</span>
      <span class="k">function</span><span class="p">()</span>
        <span class="nb">require</span><span class="p">(</span><span class="s2">"sidekick.cli"</span><span class="p">).</span><span class="n">send</span><span class="p">({</span>
          <span class="n">filter</span> <span class="o">=</span> <span class="p">{</span> <span class="n">installed</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">},</span>
          <span class="n">msg</span> <span class="o">=</span> <span class="s2">"The current file is {file}, this is the current selection: \n\n```\n{selection}\n```"</span><span class="p">,</span>
        <span class="p">})</span>
      <span class="k">end</span><span class="p">,</span>
      <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Sidekick Toggle with context"</span><span class="p">,</span>
      <span class="n">mode</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"x"</span> <span class="p">},</span>
    <span class="p">},</span>
    <span class="p">{</span>
      <span class="s2">"&lt;A-i&gt;"</span><span class="p">,</span>
      <span class="k">function</span><span class="p">()</span>
        <span class="nb">require</span><span class="p">(</span><span class="s2">"sidekick.cli"</span><span class="p">).</span><span class="n">prompt</span><span class="p">({</span> <span class="n">filter</span> <span class="o">=</span> <span class="p">{</span> <span class="n">installed</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">})</span>
      <span class="k">end</span><span class="p">,</span>
      <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Sidekick Select Prompt"</span><span class="p">,</span>
      <span class="n">mode</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"n"</span><span class="p">,</span> <span class="s2">"t"</span><span class="p">,</span> <span class="s2">"i"</span><span class="p">,</span> <span class="s2">"x"</span> <span class="p">},</span>
    <span class="p">},</span>
  <span class="p">},</span>
</code></pre></div></div>

<p>For <code class="language-plaintext highlighter-rouge">codecompanion</code>, the same can be achieved with these keybindings:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">({</span> <span class="s2">"n"</span><span class="p">,</span> <span class="s2">"t"</span><span class="p">,</span> <span class="s2">"i"</span> <span class="p">},</span> <span class="s2">"&lt;A-a&gt;"</span><span class="p">,</span> <span class="s2">"&lt;cmd&gt;CodeCompanionChat Toggle&lt;cr&gt;"</span><span class="p">,</span> <span class="p">{</span> <span class="n">desc</span> <span class="o">=</span> <span class="s2">"CodeCompanion Toggle"</span> <span class="p">})</span>
      <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">({</span> <span class="s2">"x"</span> <span class="p">},</span> <span class="s2">"&lt;A-a&gt;"</span><span class="p">,</span> <span class="s2">"&lt;cmd&gt;CodeCompanionChat Add&lt;cr&gt;"</span><span class="p">,</span> <span class="p">{</span> <span class="n">desc</span> <span class="o">=</span> <span class="s2">"CodeCompanion add selection"</span> <span class="p">})</span>
      <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">(</span>
        <span class="p">{</span> <span class="s2">"n"</span><span class="p">,</span> <span class="s2">"t"</span><span class="p">,</span> <span class="s2">"i"</span><span class="p">,</span> <span class="s2">"x"</span> <span class="p">},</span>
        <span class="s2">"&lt;A-i&gt;"</span><span class="p">,</span>
        <span class="s2">"&lt;cmd&gt;CodeCompanionActions&lt;cr&gt;"</span><span class="p">,</span>
        <span class="p">{</span> <span class="n">desc</span> <span class="o">=</span> <span class="s2">"CodeCompanion Actions"</span> <span class="p">}</span>
      <span class="p">)</span>
</code></pre></div></div>

<h3 id="buffer-guard">Buffer guard</h3>

<p>The <code class="language-plaintext highlighter-rouge">codecompanion</code> buffer behaves like a normal buffer. Which means that if I
jump in the jumplist while being in it, it would be replaced. Or if a custom /
weird LSP action is triggered. Or…</p>

<p>So, I wanted to protect the <code class="language-plaintext highlighter-rouge">codecompanion</code> buffer, as identified through its
virtual filetype.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">init</span> <span class="o">=</span> <span class="k">function</span><span class="p">()</span>
      <span class="c1">-- If new buffers are opened or jumps being triggered while focus is on codecompanion, the action should be triggered outside of the codecompanion buffer</span>
      <span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_create_autocmd</span><span class="p">(</span><span class="s2">"FileType"</span><span class="p">,</span> <span class="p">{</span>
        <span class="n">pattern</span> <span class="o">=</span> <span class="s2">"codecompanion"</span><span class="p">,</span>
        <span class="n">callback</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">args</span><span class="p">)</span>
          <span class="n">vim</span><span class="p">.</span><span class="n">bo</span><span class="p">[</span><span class="n">args</span><span class="p">.</span><span class="n">buf</span><span class="p">].</span><span class="n">buflisted</span> <span class="o">=</span> <span class="kc">false</span>
          <span class="kd">local</span> <span class="n">win</span> <span class="o">=</span> <span class="n">vim</span><span class="p">.</span><span class="n">fn</span><span class="p">.</span><span class="n">bufwinid</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">buf</span><span class="p">)</span>
          <span class="k">if</span> <span class="n">win</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span> <span class="k">then</span>
            <span class="k">return</span>
          <span class="k">end</span>
          <span class="c1">-- Guard this window: redirect any non-cc buffer that lands here</span>
          <span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_create_autocmd</span><span class="p">(</span><span class="s2">"BufWinEnter"</span><span class="p">,</span> <span class="p">{</span>
            <span class="n">callback</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">ev</span><span class="p">)</span>
              <span class="k">if</span> <span class="ow">not</span> <span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_win_is_valid</span><span class="p">(</span><span class="n">win</span><span class="p">)</span> <span class="k">then</span>
                <span class="k">return</span> <span class="kc">true</span>
              <span class="k">end</span>
              <span class="k">if</span> <span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_get_current_win</span><span class="p">()</span> <span class="o">~=</span> <span class="n">win</span> <span class="k">then</span>
                <span class="k">return</span>
              <span class="k">end</span>
              <span class="k">if</span> <span class="n">ev</span><span class="p">.</span><span class="n">buf</span> <span class="o">==</span> <span class="n">args</span><span class="p">.</span><span class="n">buf</span> <span class="k">then</span>
                <span class="k">return</span>
              <span class="k">end</span>
              <span class="c1">-- Restore cc buffer in this window, move new buffer elsewhere</span>
              <span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_win_set_buf</span><span class="p">(</span><span class="n">win</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">buf</span><span class="p">)</span>
              <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">w</span> <span class="k">in</span> <span class="nb">ipairs</span><span class="p">(</span><span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_tabpage_list_wins</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span> <span class="k">do</span>
                <span class="k">if</span> <span class="n">w</span> <span class="o">~=</span> <span class="n">win</span> <span class="ow">and</span> <span class="n">vim</span><span class="p">.</span><span class="n">bo</span><span class="p">[</span><span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_win_get_buf</span><span class="p">(</span><span class="n">w</span><span class="p">)].</span><span class="n">filetype</span> <span class="o">~=</span> <span class="s2">"codecompanion"</span> <span class="k">then</span>
                  <span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_win_set_buf</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">ev</span><span class="p">.</span><span class="n">buf</span><span class="p">)</span>
                  <span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_set_current_win</span><span class="p">(</span><span class="n">w</span><span class="p">)</span>
                  <span class="k">return</span>
                <span class="k">end</span>
              <span class="k">end</span>
              <span class="n">vim</span><span class="p">.</span><span class="n">cmd</span><span class="p">(</span><span class="s2">"split | buffer "</span> <span class="o">..</span> <span class="n">ev</span><span class="p">.</span><span class="n">buf</span><span class="p">)</span>
            <span class="k">end</span><span class="p">,</span>
          <span class="p">})</span>
        <span class="k">end</span><span class="p">,</span>
      <span class="p">})</span>
    <span class="k">end</span>
</code></pre></div></div>

<h2 id="tldr">tl;dr</h2>

<ul>
  <li><em>ACP</em> is nice for Agent/Client integration</li>
  <li>setting up an work environment to use agent integration through ACP allows
for the definition of common behaviour and user experience, even with
different agents underneath it all</li>
  <li>nvim can be set up to use ACP with plugins, e.g. codecompanion</li>
  <li>?</li>
  <li>profit</li>
</ul>]]></content><author><name>Dominikus Gierlach</name></author><category term="linux" /><category term="programming" /><category term="vim" /><category term="ai" /><category term="acp" /><category term="agent" /><summary type="html"><![CDATA[AI: Agentic vim - local, cloudy, losely coupled?]]></summary></entry><entry><title type="html">LSP mix and match - cherry-pick capabilities from different LSP servers</title><link href="https://gierdo.astounding.technology/blog/2025/12/14/lsp-mix-and-match" rel="alternate" type="text/html" title="LSP mix and match - cherry-pick capabilities from different LSP servers" /><published>2025-12-14T00:00:00+00:00</published><updated>2025-12-14T00:00:00+00:00</updated><id>https://gierdo.astounding.technology/blog/2025/12/14/lsp-mix-and-match</id><content type="html" xml:base="https://gierdo.astounding.technology/blog/2025/12/14/lsp-mix-and-match"><![CDATA[<h1 id="lsp-mix-and-match---cherry-pick-capabilities-from-different-lsp-servers">LSP mix and match - cherry-pick capabilities from different LSP servers</h1>

<blockquote>
  <p>Note: Recently, <code class="language-plaintext highlighter-rouge">pyrefly</code> has gained the capabilities that were missing for
me to be able to fully switch to it as sole lsp. My current setup doesn’t
make use of cherry-picking LSP capabilities anymore.</p>
</blockquote>

<p>As I have written about <a href="/blog/2024/09/19/nvim-lsp">here</a>, for
most of my file edititing / programming / note-taking / … I use <a href="https://neovim.io">vim
(neovim)</a>.</p>

<p>Not a bare-bones neovim, though. I use many plugins, and I have even more of
them set up. Too many? Maybe. Probably. Especially considering the ever growing
number of software supply chain attacks..</p>

<p>Anyways, that is a different topic for a different day.</p>

<p>Besides convenient plugins, the main building block for developer experience
enhancement and efficiency are <a href="https://microsoft.github.io/language-server-protocol/implementors/servers/"><em>language
servers</em></a>,
which provide stuff like <em>diagnostics</em>, <em>code completion</em>, <em>navigation
enhancements</em> and more.</p>

<p>Besides playing around with its config, I try to actually use vim for
productive work, including programming in <code class="language-plaintext highlighter-rouge">python</code>.</p>

<p>My go-to language server for python has been
<a href="https://github.com/DetachHead/basedpyright"><code class="language-plaintext highlighter-rouge">basedpyright</code></a>, a fork of
microsoft’s <a href="https://github.com/microsoft/pyright"><code class="language-plaintext highlighter-rouge">pyright</code></a>, which basically
adds the functionality of the proprietary <code class="language-plaintext highlighter-rouge">pylance</code> extension to <code class="language-plaintext highlighter-rouge">pyright</code> and
adds some more bells and whistles.</p>

<p>While <code class="language-plaintext highlighter-rouge">basedpyright</code> is stable, helpful, convenient and generally excellent,
there are a few new kids on the block:</p>

<p>There is a new generation of <code class="language-plaintext highlighter-rouge">python</code> language servers, written in <code class="language-plaintext highlighter-rouge">Rust</code>,
targeting high performance and productivity in large and complex code bases:</p>

<p>There is Astral’s <a href="https://docs.astral.sh/ty/"><code class="language-plaintext highlighter-rouge">ty</code></a>, Meta’s
<a href="https://pyrefly.org/"><code class="language-plaintext highlighter-rouge">pyrefly</code></a> and David Halter’s (Jedi’s original author)
<a href="https://zubanls.com"><code class="language-plaintext highlighter-rouge">zuban</code></a>, and probably some more.</p>

<p>I have been intrigued by them, mainly hoping for a smoother and more performant
workflow in large and growing codebases. So, I have played around with them for
a while now, eventually ending up using <code class="language-plaintext highlighter-rouge">ty</code> only for type checking while still
having to rely on <code class="language-plaintext highlighter-rouge">basedpyright</code> for completion, auto-import, symbol renaming,
navigation etc.</p>

<p><em>Why?</em></p>

<p>In single capabilities, none of the new lsp servers match <code class="language-plaintext highlighter-rouge">basedpyright</code> in my
workflow. Yet.</p>

<p>E.g.:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">pyrefly</code> has great completion, navigation and even auto-import, but it’s
really bad in renaming symbols. It can rename variables, but no functions.</li>
  <li><code class="language-plaintext highlighter-rouge">ty</code> has limited support of <code class="language-plaintext highlighter-rouge">pydantic</code>, which I use a lot, completion is not
as powerful as <code class="language-plaintext highlighter-rouge">pyrefly</code>’s, but renaming works great.</li>
</ul>

<p><em>Why not use both?</em></p>

<p>If you enable several language servers with the (nominally) same capabilities
for the same file type, you get duplicate results/actions for every capability.
Go to definition? Select where you want to jump to from these two identical
alternatives!</p>

<p>My goal was to mix-and-match, e.g. use <code class="language-plaintext highlighter-rouge">ty</code> for renaming and <code class="language-plaintext highlighter-rouge">pyrefly</code> for
everything else. However, while especially <code class="language-plaintext highlighter-rouge">pyrefly</code>’s configuration is very
flexible, the lsps don’t support tailored enabling and disabling of select
capabilities.</p>

<p>I tried for an unsuccessful while to configure the lsp server by restricting
the handed-over client capabilities, but that didn’t go anywhere.</p>

<p>The only almost compromise that worked for me with the configuration through
the lsp servers themselves was <em>disabling</em> <code class="language-plaintext highlighter-rouge">basedpyright</code>’s type checking and
<em>disabling</em> <code class="language-plaintext highlighter-rouge">ty</code>’s language server capabilities, besides type checking. Like
this, at least a little bit of the heavy lifting could be offloaded from
<code class="language-plaintext highlighter-rouge">basedpyright</code> to <code class="language-plaintext highlighter-rouge">ty</code>, but I was not happy.</p>

<p>Now, I sort of am happy! I found out how to disable specific lsp capabilities
for specific lsp servers through <a href="https://neovim.io/doc/user/lsp.html"><code class="language-plaintext highlighter-rouge">nvim</code>’s lsp
configuration</a>. Instead of trying to get
the lsp server to not provide the functionality, the <em>client</em> ignores it!</p>

<h2 id="the-problem">The Problem</h2>

<p>I like working with Python, also with larger and growing code bases. There are
LSP servers that offer excellent performance. However, none match the full
functionality of <code class="language-plaintext highlighter-rouge">basedpyright</code> when used independently. Unfortunately,
<code class="language-plaintext highlighter-rouge">basedpyright</code> lacks in performance. Combining multiple LSP servers introduces
duplicate functionality and a bit of a mess. Such combination could be feasible
if we could selectively disable or enable conflicting capabilities. However, it
is not possible to instruct LSP servers to withhold certain functionalities
unless they provide an interface for that, e.g. with dedicated configuration
options.</p>

<h2 id="the-solution">The solution</h2>

<p>Enable conflicting lsp servers, but tell the lsp client (<em>nvim</em>) that it should
not use specific lsp server’s capabilities through an <code class="language-plaintext highlighter-rouge">on_init</code> hook 🥳!</p>

<p>The configuration is probably not fully final and will change, but this is the
general thing.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">local</span> <span class="n">lsp_capabilities</span> <span class="o">=</span> <span class="nb">require</span><span class="p">(</span><span class="s2">"blink.cmp"</span><span class="p">).</span><span class="n">get_lsp_capabilities</span><span class="p">(</span><span class="kc">nil</span><span class="p">,</span> <span class="kc">true</span><span class="p">)</span>

  <span class="c1">---@param client vim.lsp.Client</span>
  <span class="kd">local</span> <span class="n">ty_on_init</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">client</span><span class="p">)</span>
    <span class="n">client</span><span class="p">.</span><span class="n">server_capabilities</span><span class="p">.</span><span class="n">callHierarchyProvider</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="n">client</span><span class="p">.</span><span class="n">server_capabilities</span><span class="p">.</span><span class="n">completionProvider</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="n">client</span><span class="p">.</span><span class="n">server_capabilities</span><span class="p">.</span><span class="n">inlineCompletionProvider</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="n">client</span><span class="p">.</span><span class="n">server_capabilities</span><span class="p">.</span><span class="n">inlineValueProvider</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="n">client</span><span class="p">.</span><span class="n">server_capabilities</span><span class="p">.</span><span class="n">declarationProvider</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="n">client</span><span class="p">.</span><span class="n">server_capabilities</span><span class="p">.</span><span class="n">definitionProvider</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="n">client</span><span class="p">.</span><span class="n">server_capabilities</span><span class="p">.</span><span class="n">implementationProvider</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="n">client</span><span class="p">.</span><span class="n">server_capabilities</span><span class="p">.</span><span class="n">inlayHintProvider</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="n">client</span><span class="p">.</span><span class="n">server_capabilities</span><span class="p">.</span><span class="n">signatureHelpProvider</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="n">client</span><span class="p">.</span><span class="n">server_capabilities</span><span class="p">.</span><span class="n">hoverProvider</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="n">client</span><span class="p">.</span><span class="n">server_capabilities</span><span class="p">.</span><span class="n">referencesProvider</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="n">client</span><span class="p">.</span><span class="n">server_capabilities</span><span class="p">.</span><span class="n">codeActionProvider</span> <span class="o">=</span> <span class="kc">false</span>
  <span class="k">end</span>
  <span class="n">vim</span><span class="p">.</span><span class="n">lsp</span><span class="p">.</span><span class="n">config</span><span class="p">(</span><span class="s2">"ty"</span><span class="p">,</span> <span class="p">{</span>
    <span class="n">capabilities</span> <span class="o">=</span> <span class="n">lsp_capabilities</span><span class="p">,</span>
    <span class="n">on_init</span> <span class="o">=</span> <span class="p">{</span> <span class="n">ty_on_init</span> <span class="p">},</span>
    <span class="n">settings</span> <span class="o">=</span> <span class="p">{</span>
      <span class="n">ty</span> <span class="o">=</span> <span class="p">{</span>
        <span class="n">disableLanguageServices</span> <span class="o">=</span> <span class="kc">false</span><span class="p">,</span>
        <span class="n">diagnosticMode</span> <span class="o">=</span> <span class="s2">"workspace"</span><span class="p">,</span>
        <span class="n">experimental</span> <span class="o">=</span> <span class="p">{</span>
          <span class="n">rename</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
        <span class="p">},</span>
      <span class="p">},</span>
    <span class="p">},</span>
  <span class="p">})</span>

  <span class="c1">---@param client vim.lsp.Client</span>
  <span class="kd">local</span> <span class="n">pyrefly_on_init</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">client</span><span class="p">)</span>
    <span class="n">client</span><span class="p">.</span><span class="n">server_capabilities</span><span class="p">.</span><span class="n">renameProvider</span> <span class="o">=</span> <span class="kc">false</span>
  <span class="k">end</span>
  <span class="n">vim</span><span class="p">.</span><span class="n">lsp</span><span class="p">.</span><span class="n">config</span><span class="p">(</span><span class="s2">"pyrefly"</span><span class="p">,</span> <span class="p">{</span>
    <span class="n">capabilities</span> <span class="o">=</span> <span class="n">lsp_capabilities</span><span class="p">,</span>
    <span class="n">on_init</span> <span class="o">=</span> <span class="p">{</span> <span class="n">pyrefly_on_init</span> <span class="p">},</span>
    <span class="n">settings</span> <span class="o">=</span> <span class="p">{</span>
      <span class="n">python</span> <span class="o">=</span> <span class="p">{</span>
        <span class="n">pyrefly</span> <span class="o">=</span> <span class="p">{</span>
          <span class="n">disableLanguageServices</span> <span class="o">=</span> <span class="kc">false</span><span class="p">,</span>
          <span class="n">displayTypeErrors</span> <span class="o">=</span> <span class="s2">"force-on"</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="n">analysis</span> <span class="o">=</span> <span class="p">{</span>
          <span class="n">diagnosticMode</span> <span class="o">=</span> <span class="s2">"workspace"</span><span class="p">,</span>
          <span class="n">inlayHints</span> <span class="o">=</span> <span class="p">{</span>
            <span class="n">callArgumentNames</span> <span class="o">=</span> <span class="s2">"all"</span><span class="p">,</span>
            <span class="n">variableTypes</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
            <span class="n">functionReturnTypes</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
            <span class="n">pytestParameters</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
          <span class="p">},</span>
        <span class="p">},</span>
      <span class="p">},</span>
    <span class="p">},</span>
  <span class="p">})</span>

</code></pre></div></div>

<h2 id="tldr">tl;dr</h2>

<ul>
  <li>Some lsp servers are not powerful enough to fully replace alternatives but
would be great enhancements or replacements for single capabilities</li>
  <li><em>nvim</em>’s lsp config API allows for specifying different hooks, <code class="language-plaintext highlighter-rouge">on_init</code> and
<code class="language-plaintext highlighter-rouge">on_attach</code> etc.</li>
  <li>It’s possible to disable specific capabilities in those hooks</li>
</ul>]]></content><author><name>Dominikus Gierlach</name></author><category term="linux" /><category term="programming" /><category term="vim" /><category term="lsp" /><category term="configuration" /><category term="capabilities" /><category term="ty" /><category term="python" /><category term="pyrefly" /><summary type="html"><![CDATA[LSP mix and match - cherry-pick capabilities from different LSP servers]]></summary></entry><entry><title type="html">update: ‘secure’ automatic system decryption - tpm2 + LUKS + systemd-cryptenroll</title><link href="https://gierdo.astounding.technology/blog/2025/07/05/tpm2-luks-systemd" rel="alternate" type="text/html" title="update: ‘secure’ automatic system decryption - tpm2 + LUKS + systemd-cryptenroll" /><published>2025-07-05T00:00:00+00:00</published><updated>2025-07-05T00:00:00+00:00</updated><id>https://gierdo.astounding.technology/blog/2025/07/05/tpm2-luks-systemd</id><content type="html" xml:base="https://gierdo.astounding.technology/blog/2025/07/05/tpm2-luks-systemd"><![CDATA[<h1 id="secure-automatic-system-decrpytion-with-tpm-and-luks---update">‘Secure’ automatic system decrpytion with tpm and LUKS - update</h1>

<p>As I have written about <a href="/blog/2023/12/10/tpm2-luks">here</a>, I
sort of have accepted a compromise in security for added convenience,
auto-unlocking my <a href="https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup">LUKS</a>
encrypted root partition making use of my system’s built in TPM in order to
only auto-unlock the system under specific (secure) conditions.</p>

<p>I’ve used <code class="language-plaintext highlighter-rouge">clevis</code> so far to manage the setup, using the trusty
<code class="language-plaintext highlighter-rouge">initramfs-tools</code> to configure and bake the <em>initramfs</em> with the required
modules and configuration, relying on <code class="language-plaintext highlighter-rouge">cryptsetup</code> to manage and reset the
specifics of the configuration, e.g. whenever a bios update invalidated a <em>PCR
measurement</em>.</p>

<p>Not that elegant. And there are new, more elegant and capable mechanisms
available!</p>

<ul>
  <li>There is <code class="language-plaintext highlighter-rouge">systemd-cryptenroll</code> to enroll physical tokens to LUKS2 encrypted
volumes, simplifying the setup and management of this part of the process</li>
  <li>There is <code class="language-plaintext highlighter-rouge">dracut</code> for the configuration and management of <em>initramfs</em> images,
the designated successor of <code class="language-plaintext highlighter-rouge">initramfs-tools</code></li>
</ul>

<p>While my previous setup worked, I wanted to try setting it up with the
(designated) successor systems.</p>

<p>Well. To be honest, first I just wanted to try out <code class="language-plaintext highlighter-rouge">systemd-cryptenroll</code>
instead of <code class="language-plaintext highlighter-rouge">clevis</code>, realized that the intended way of configuring
auto-unlocking didn’t work with <code class="language-plaintext highlighter-rouge">initramfs-tools</code>, realized that (due to that,
amongst other things) there have been discussions about replacing
<code class="language-plaintext highlighter-rouge">initramfs-tools</code> with <code class="language-plaintext highlighter-rouge">dracut</code> as default and then decided to redo the whole
process.</p>

<h2 id="the-problem">The Problem</h2>

<p>Well, it’s not really a new problem, it’s an itch and a mental problem, I guess
😅. The original challenge, shifting the compromise of convenience vs. security
a little by auto-unlocking the encrypted system with the help of the TPM has
been working for a while.</p>

<p>But:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">systemd-cryptenroll</code> exists, potential successor to <code class="language-plaintext highlighter-rouge">clevis</code></li>
  <li><code class="language-plaintext highlighter-rouge">dracut</code> exists, potential successor to <code class="language-plaintext highlighter-rouge">initramfs-tools</code></li>
  <li>Both of those systems are very capable</li>
  <li>My setup is using their (potential) predecessors</li>
</ul>

<h2 id="the-solution">The solution</h2>

<h3 id="systemd-cryptenroll">systemd-cryptenroll</h3>

<p>Installing <code class="language-plaintext highlighter-rouge">systemd-cryptenroll</code> on Debian is easy enough. It comes with
<code class="language-plaintext highlighter-rouge">systemd-cryptsetup</code>, which might be on your system already?
If not:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>systemd-cryptsetup
</code></pre></div></div>

<p>Then, enrolling the TPM with the encrypted partition is pretty straightforward:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemd-cryptenroll <span class="nt">--tpm2-device</span><span class="o">=</span>auto /dev/nvme0n1p3
</code></pre></div></div>

<p>Depending on your needs/level-of-paranoia, you might want to add other PCRs, as
<code class="language-plaintext highlighter-rouge">systemd-cryptenroll</code> per default only uses PCR 7, maybe you even want to use
TPM + PIN:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemd-cryptenroll <span class="nt">--tpm2-device</span><span class="o">=</span>auto <span class="nt">--tpm2-pcrs</span><span class="o">=</span>7+11 <span class="nt">--tpm2-with-pin</span><span class="o">=</span><span class="nb">yes</span> /dev/nvme0n1p3
</code></pre></div></div>

<p>You follow the guided process, and then can verify success. Hopefully.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemd-cryptenroll /dev/nvme0n1p3
SLOT TYPE
   0 password
   1 tpm2
</code></pre></div></div>

<p>Configuration should (™️) work by configuring the tpm in <code class="language-plaintext highlighter-rouge">/etc/crypttab</code> and
rebuilding the <em>initramfs</em> with it:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /etc/crypttab
nvme0n1p3_crypt <span class="nv">UUID</span><span class="o">=</span>e010d6c1-6016-4f2d-bd05-1be8f383ada9 none tpm2-device<span class="o">=</span>auto,luks,discard
</code></pre></div></div>

<h3 id="building-the-initramfs-with-initramfs-tools-fail">building the initramfs with initramfs-tools (fail)</h3>

<p>But, alas, rebuilding the initial ram disk fails on Debian and should also fail
on Ubuntu:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>update-initramfs <span class="nt">-u</span> <span class="nt">-k</span> all
...
cryptsetup: WARNING: nvme0n1p3_crypt: ignoring unknown option <span class="s1">'tpm2-device'</span>
...
</code></pre></div></div>

<p>After a bit of research: <code class="language-plaintext highlighter-rouge">initramfs-tools</code> doesn’t support tpm2 devices. There
is a <a href="https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1031254">bug report</a>
and even an open <a href="https://salsa.debian.org/cryptsetup-team/cryptsetup/-/merge_requests/39">merge request with a
fix</a>,
but the situation appears to be stale.</p>

<p>However, there is the designated successor of <code class="language-plaintext highlighter-rouge">initramfs-tools</code>:
<a href="https://en.wikipedia.org/wiki/Dracut_\(software\)"><code class="language-plaintext highlighter-rouge">dracut</code></a>, which has
replaced <code class="language-plaintext highlighter-rouge">initramfs-tools</code> in a bunch of linux distributions already.</p>

<h3 id="building-the-initramfs-with-dracut">building the initramfs with dracut</h3>

<p>On debian and Ubuntu, it can be installed from the package sources like this,
replacing <code class="language-plaintext highlighter-rouge">initramfs-tools</code> in the process:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>dracut
</code></pre></div></div>

<p>Configuration of dracut happens in <code class="language-plaintext highlighter-rouge">/etc/dracut.conf.d/*.conf</code>.</p>

<p>I configured dracut to run the host-only initramfs creation with tpm2 support
and compress with <code class="language-plaintext highlighter-rouge">lz4</code>, all in separate files:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /etc/dracut.conf.d/compression.conf
<span class="nv">compress</span><span class="o">=</span><span class="s2">"lz4"</span>

<span class="nb">cat</span> /etc/dracut.conf.d/hostonly.conf
<span class="nv">hostonly</span><span class="o">=</span><span class="s2">"yes"</span>
<span class="nv">hostonly_cmdline</span><span class="o">=</span><span class="s2">"no"</span>

<span class="nb">cat</span> /etc/dracut.conf.d/tpm.conf
add_dracutmodules+<span class="o">=</span><span class="s2">" tpm2-tss crypt "</span>
add_drivers+<span class="o">=</span><span class="s2">" tpm tpm_tis_core tpm_tis tpm_crb "</span>
</code></pre></div></div>

<p>With that configuration in place, the initramfs can be (re)built with</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dracut <span class="nt">-f</span>
</code></pre></div></div>

<p>After that, a reboot should end up with an  automatically unlocked encrypted
partition.</p>

<h2 id="tldr">tl;dr</h2>

<p>If you trust your system firmware and tpm enough, you can set your system up to
decrypt automatically without providing a passphrase using
<code class="language-plaintext highlighter-rouge">systemd-cryptenroll</code> and <code class="language-plaintext highlighter-rouge">dracut</code>.</p>

<p>There are also <a href="/blog/2023/12/10/tpm2-luks">other methods</a>, but
this one appears to be the most future proof and easy to set-up and maintain.</p>]]></content><author><name>Dominikus Gierlach</name></author><category term="linux" /><category term="security" /><category term="tpm 2.0" /><category term="systemd-cryptenroll" /><summary type="html"><![CDATA[‘Secure’ automatic system decrpytion with tpm and LUKS - update]]></summary></entry><entry><title type="html">Amazon Q in vim</title><link href="https://gierdo.astounding.technology/blog/2025/05/25/amazon-q-vim" rel="alternate" type="text/html" title="Amazon Q in vim" /><published>2025-05-25T00:00:00+00:00</published><updated>2025-05-25T00:00:00+00:00</updated><id>https://gierdo.astounding.technology/blog/2025/05/25/amazon-q-vim</id><content type="html" xml:base="https://gierdo.astounding.technology/blog/2025/05/25/amazon-q-vim"><![CDATA[<h1 id="amazon-q---assistant-in-vim">Amazon Q - Assistant in vim</h1>

<h2 id="the-problem">The Problem</h2>

<p>My employer is a typical German large scale corporation. One of the attributes
that come with that is that we are, of course, interested in generative AI, but
also very sceptical and cautious.</p>

<p>Slowly, however, more and more AI services become available to us, but
basically only web-interface chat solutions without APIs and IDE/development
related integrations.</p>

<p>I was moderately happy when recently it was announced that we can use <a href="https://aws.amazon.com/q/developer/"><em>Amazon
Q Developer</em></a>! Awesome! Finally a
development focused solution!</p>

<p>There are extensions for JetBrains IDEs, VS Code, Visual Studio and a preview
for Eclipse, and there is a <em>cli</em>.</p>

<p>That should be enough, right?</p>

<p>…</p>

<p>Well. Not for all of us neovim users.</p>

<p>I want to have integration to the full potential of generative AI, adhering to
applicable compliance restrictions, and still remain in the tool environment
that makes me as productive and makes my work as enjoyable as possible.</p>

<h2 id="possible-solution">Possible solution</h2>

<p>There are many pretty excellent neovim AI plugins out there, e.g.
<a href="https://github.com/yetone/avante.nvim">Avante</a> or <a href="https://github.com/CopilotC-Nvim/CopilotChat.nvim">Copilot
Chat</a>.</p>

<p>While those are pretty nice and powerful, most of them are taylored around a
number of the big AI API providers, and are pretty big to work with.</p>

<p><em>Amazon Q</em> does not provide a nice API, especially not an API that is
compatible with existing clients and integrations. I spent a few minutes
looking through Avante and did not see a quick path to extend the existing (web
API centric) model adaption with Q.</p>

<p>I have been tinkering with <a href="https://github.com/gierdo/neoai.nvim">a fork</a> of
<a href="https://github.com/Bryley/neoai.nvim">neoai.nvim</a>, extending it a little for
my usecases, focussing on low-overhead integration of local language models
with llama.cpp. I’ve been very happy with it, especially because the plugin is
still so small that I don’t have a hard time understanding what it does and how
to extend it without having to spend too much time. I’m lazy..</p>

<p>So, I decided to simply extend the plugin I’m most familiar with, <a href="https://github.com/gierdo/neoai.nvim">my
fork</a>.</p>

<h2 id="solution">Solution</h2>

<p>As written above, <em>Amazon Q</em> does not provide a nice API, but has a cli, called
<code class="language-plaintext highlighter-rouge">q</code>. I decided to keep it simple and simply dump my prompt into <code class="language-plaintext highlighter-rouge">q</code> in
<em>non-interactive</em> mode, parsing its <code class="language-plaintext highlighter-rouge">stdout</code> output. I hoped that it would
allow me to use the existing way of providing persistent chat sessions and
context, as well as having a simple way to apply existing functionality, e.g.
code recognition for easy pasting with <code class="language-plaintext highlighter-rouge">:put c</code> etc.</p>

<p>It was surprisingly easy, the core of it all is fifty-ish lines of <code class="language-plaintext highlighter-rouge">lua</code>:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="n">utils</span> <span class="o">=</span> <span class="nb">require</span><span class="p">(</span><span class="s2">"neoai.utils"</span><span class="p">)</span>

<span class="c1">--- This model definition supports Amazon Q CLI integration</span>
<span class="c1">---@type ModelModule</span>
<span class="kd">local</span> <span class="n">M</span> <span class="o">=</span> <span class="p">{}</span>

<span class="n">M</span><span class="p">.</span><span class="n">name</span> <span class="o">=</span> <span class="s2">"Amazon Q"</span>

<span class="n">M</span><span class="p">.</span><span class="n">_chunks</span> <span class="o">=</span> <span class="p">{}</span>

<span class="n">M</span><span class="p">.</span><span class="n">get_current_output</span> <span class="o">=</span> <span class="k">function</span><span class="p">()</span>
 <span class="k">return</span> <span class="nb">table.concat</span><span class="p">(</span><span class="n">M</span><span class="p">.</span><span class="n">_chunks</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1">---@param chunk string</span>
<span class="c1">---@param on_stdout_chunk fun(chunk: string) Function to call whenever a stdout chunk occurs</span>
<span class="n">M</span><span class="p">.</span><span class="n">_recieve_chunk</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">chunk</span><span class="p">,</span> <span class="n">on_stdout_chunk</span><span class="p">)</span>
 <span class="c1">-- Split the input chunk by newlines</span>
 <span class="n">on_stdout_chunk</span><span class="p">(</span><span class="n">chunk</span><span class="p">)</span>
 <span class="nb">table.insert</span><span class="p">(</span><span class="n">M</span><span class="p">.</span><span class="n">_chunks</span><span class="p">,</span> <span class="n">chunk</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1">---@param chat_history ChatHistory</span>
<span class="c1">---@param on_stdout_chunk fun(chunk: string) Function to call whenever a stdout chunk occurs</span>
<span class="c1">---@param on_complete fun(err?: string, output?: string) Function to call when model has finished</span>
<span class="n">M</span><span class="p">.</span><span class="n">send_to_model</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">chat_history</span><span class="p">,</span> <span class="n">on_stdout_chunk</span><span class="p">,</span> <span class="n">on_complete</span><span class="p">)</span>
 <span class="c1">-- Format messages for Amazon Q CLI</span>
 <span class="kd">local</span> <span class="n">messages_json</span> <span class="o">=</span> <span class="n">vim</span><span class="p">.</span><span class="n">json</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">chat_history</span><span class="p">.</span><span class="n">messages</span><span class="p">)</span>

 <span class="n">chunks</span> <span class="o">=</span> <span class="p">{}</span>

 <span class="kd">local</span> <span class="n">command</span> <span class="o">=</span> <span class="p">{}</span>
 <span class="n">command</span> <span class="o">=</span> <span class="p">{</span>
  <span class="s2">"chat"</span><span class="p">,</span>
  <span class="s2">"--no-interactive"</span><span class="p">,</span>
 <span class="p">}</span>
 <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">v</span> <span class="k">in</span> <span class="nb">ipairs</span><span class="p">(</span><span class="n">chat_history</span><span class="p">.</span><span class="n">params</span><span class="p">)</span> <span class="k">do</span>
  <span class="nb">table.insert</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
 <span class="k">end</span>
 <span class="nb">table.insert</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="n">messages_json</span><span class="p">)</span>

 <span class="c1">-- Execute the Amazon Q CLI command</span>
 <span class="n">utils</span><span class="p">.</span><span class="n">exec</span><span class="p">(</span><span class="s2">"q"</span><span class="p">,</span> <span class="n">command</span><span class="p">,</span> <span class="k">function</span><span class="p">(</span><span class="n">chunk</span><span class="p">)</span>
  <span class="n">M</span><span class="p">.</span><span class="n">_recieve_chunk</span><span class="p">(</span><span class="n">chunk</span><span class="p">,</span> <span class="n">on_stdout_chunk</span><span class="p">)</span>
 <span class="k">end</span><span class="p">,</span> <span class="k">function</span><span class="p">(</span><span class="n">err</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span>
  <span class="c1">-- Clean up the temporary file</span>

  <span class="n">on_complete</span><span class="p">(</span><span class="n">err</span><span class="p">,</span> <span class="n">M</span><span class="p">.</span><span class="n">get_current_output</span><span class="p">())</span>
 <span class="k">end</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">return</span> <span class="n">M</span>
</code></pre></div></div>

<p><em>Q</em> behaves a bit weird, it only marks source-code it produces with colors,
using <em>ansi</em> codes, instead of using the de-facto standard backtics. Luckily it
appears to be consistent, so code snippet extraction works like this:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">extract_code_snippets</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
  <span class="kd">local</span> <span class="n">matches</span> <span class="o">=</span> <span class="p">{}</span>
  <span class="c1">-- Amazon Q marks code by color-coding it green with ansi codes.</span>
  <span class="c1">-- Everything between "start green" and "reset color" is assumed to be code.</span>
  <span class="k">for</span> <span class="n">match</span> <span class="k">in</span> <span class="nb">string.gmatch</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="s2">"</span><span class="se">\27</span><span class="s2">%[38;5;%d+m(.-)\27%[0m"</span><span class="p">)</span> <span class="k">do</span>
    <span class="nb">table.insert</span><span class="p">(</span><span class="n">matches</span><span class="p">,</span> <span class="n">match</span><span class="p">)</span>
  <span class="k">end</span>
  <span class="k">return</span> <span class="nb">table.concat</span><span class="p">(</span><span class="n">matches</span><span class="p">,</span> <span class="s2">"</span><span class="se">\n\n</span><span class="s2">"</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I run environments that do not have <code class="language-plaintext highlighter-rouge">q</code> available, so I made my config
sensitive to its availability.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">config</span> <span class="o">=</span> <span class="k">function</span><span class="p">()</span>
      <span class="kd">local</span> <span class="n">q_available</span> <span class="o">=</span> <span class="n">vim</span><span class="p">.</span><span class="n">fn</span><span class="p">.</span><span class="n">executable</span><span class="p">(</span><span class="s2">"q"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span>

      <span class="kd">local</span> <span class="n">models</span> <span class="o">=</span> <span class="p">{}</span>
      <span class="kd">local</span> <span class="n">extract_code_snippets</span>

      <span class="k">if</span> <span class="n">q_available</span> <span class="k">then</span>
        <span class="n">extract_code_snippets</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
          <span class="kd">local</span> <span class="n">matches</span> <span class="o">=</span> <span class="p">{}</span>
          <span class="c1">-- Amazon Q marks code by color-coding it green with ansi codes.</span>
          <span class="c1">-- Everything between "start green" and "reset color" is assumed to be code.</span>
          <span class="k">for</span> <span class="n">match</span> <span class="k">in</span> <span class="nb">string.gmatch</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="s2">"</span><span class="se">\27</span><span class="s2">%[38;5;%d+m(.-)\27%[0m"</span><span class="p">)</span> <span class="k">do</span>
            <span class="nb">table.insert</span><span class="p">(</span><span class="n">matches</span><span class="p">,</span> <span class="n">match</span><span class="p">)</span>
          <span class="k">end</span>
          <span class="k">return</span> <span class="nb">table.concat</span><span class="p">(</span><span class="n">matches</span><span class="p">,</span> <span class="s2">"</span><span class="se">\n\n</span><span class="s2">"</span><span class="p">)</span>
        <span class="k">end</span>

        <span class="nb">table.insert</span><span class="p">(</span><span class="n">models</span><span class="p">,</span> <span class="p">{</span>
          <span class="n">name</span> <span class="o">=</span> <span class="s2">"q"</span><span class="p">,</span>
          <span class="n">model</span> <span class="o">=</span> <span class="s2">"q"</span><span class="p">,</span>
          <span class="n">params</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"--trust-all-tools"</span> <span class="p">},</span>
        <span class="p">})</span>
      <span class="k">else</span>
        <span class="n">startLlama</span><span class="p">()</span>

        <span class="n">extract_code_snippets</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
          <span class="kd">local</span> <span class="n">matches</span> <span class="o">=</span> <span class="p">{}</span>
          <span class="k">for</span> <span class="n">match</span> <span class="k">in</span> <span class="nb">string.gmatch</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="s2">"```%w*\n(.-)```"</span><span class="p">)</span> <span class="k">do</span>
            <span class="nb">table.insert</span><span class="p">(</span><span class="n">matches</span><span class="p">,</span> <span class="n">match</span><span class="p">)</span>
          <span class="k">end</span>

          <span class="c1">-- Next part matches any code snippets that are incomplete</span>
          <span class="kd">local</span> <span class="n">count</span> <span class="o">=</span> <span class="nb">select</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="nb">string.gsub</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="s2">"```"</span><span class="p">,</span> <span class="s2">"```"</span><span class="p">))</span>
          <span class="k">if</span> <span class="n">count</span> <span class="o">%</span> <span class="mi">2</span> <span class="o">==</span> <span class="mi">1</span> <span class="k">then</span>
            <span class="kd">local</span> <span class="n">pattern</span> <span class="o">=</span> <span class="s2">"```%w*\n([^`]-)$"</span>
            <span class="kd">local</span> <span class="n">match</span> <span class="o">=</span> <span class="nb">string.match</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">pattern</span><span class="p">)</span>
            <span class="nb">table.insert</span><span class="p">(</span><span class="n">matches</span><span class="p">,</span> <span class="n">match</span><span class="p">)</span>
          <span class="k">end</span>
          <span class="k">return</span> <span class="nb">table.concat</span><span class="p">(</span><span class="n">matches</span><span class="p">,</span> <span class="s2">"</span><span class="se">\n\n</span><span class="s2">"</span><span class="p">)</span>
        <span class="k">end</span>

        <span class="nb">table.insert</span><span class="p">(</span><span class="n">models</span><span class="p">,</span> <span class="p">{</span>
          <span class="n">name</span> <span class="o">=</span> <span class="s2">"openai"</span><span class="p">,</span>
          <span class="n">model</span> <span class="o">=</span> <span class="s2">"llama 3"</span><span class="p">,</span>
          <span class="n">params</span> <span class="o">=</span> <span class="kc">nil</span><span class="p">,</span>
        <span class="p">})</span>
      <span class="k">end</span>

      <span class="nb">require</span><span class="p">(</span><span class="s2">"neoai"</span><span class="p">).</span><span class="n">setup</span><span class="p">({</span> <span class="c1">---@diagnostic disable-line: missing-fields</span>
        <span class="n">ui</span> <span class="o">=</span> <span class="p">{</span>
          <span class="n">output_popup_text</span> <span class="o">=</span> <span class="s2">"AI"</span><span class="p">,</span>
          <span class="n">input_popup_text</span> <span class="o">=</span> <span class="s2">"Prompt"</span><span class="p">,</span>
          <span class="n">width</span> <span class="o">=</span> <span class="mi">45</span><span class="p">,</span> <span class="c1">-- As percentage eg. 45%</span>
          <span class="n">output_popup_height</span> <span class="o">=</span> <span class="mi">80</span><span class="p">,</span> <span class="c1">-- As percentage eg. 80%</span>
          <span class="n">submit</span> <span class="o">=</span> <span class="s2">"&lt;Enter&gt;"</span><span class="p">,</span> <span class="c1">-- Key binding to submit the prompt</span>
        <span class="p">},</span>
        <span class="n">models</span> <span class="o">=</span> <span class="n">models</span><span class="p">,</span>
        <span class="n">register_output</span> <span class="o">=</span> <span class="p">{</span>
          <span class="p">[</span><span class="s2">"a"</span><span class="p">]</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">output</span><span class="p">)</span>
            <span class="k">return</span> <span class="n">output</span>
          <span class="k">end</span><span class="p">,</span>
          <span class="p">[</span><span class="s2">"c"</span><span class="p">]</span> <span class="o">=</span> <span class="n">extract_code_snippets</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="n">inject</span> <span class="o">=</span> <span class="p">{</span>
          <span class="n">cutoff_width</span> <span class="o">=</span> <span class="mi">75</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="n">prompts</span> <span class="o">=</span> <span class="p">{</span>
          <span class="n">context_prompt</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">context</span><span class="p">)</span>
            <span class="k">return</span> <span class="s2">"Please only follow instructions or answer to questions. Be concise. "</span>
              <span class="o">..</span> <span class="p">(</span><span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_buf_get_name</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="o">~=</span> <span class="s2">""</span> <span class="ow">and</span> <span class="s2">"This is my currently opened file: "</span> <span class="o">..</span> <span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_buf_get_name</span><span class="p">(</span>
                <span class="mi">0</span>
              <span class="p">)</span> <span class="ow">or</span> <span class="s2">""</span><span class="p">)</span>
              <span class="o">..</span> <span class="s2">"I'd like to provide some context for future "</span>
              <span class="o">..</span> <span class="s2">"messages. Here is the code/text that I want to refer "</span>
              <span class="o">..</span> <span class="s2">"to in our upcoming conversations:\n\n"</span>
              <span class="o">..</span> <span class="n">context</span>
          <span class="k">end</span><span class="p">,</span>
          <span class="n">default_prompt</span> <span class="o">=</span> <span class="k">function</span><span class="p">()</span>
            <span class="k">return</span> <span class="s2">"Please only follow instructions or answer to questions. Be concise. "</span>
              <span class="o">..</span> <span class="p">(</span>
                <span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_buf_get_name</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="o">~=</span> <span class="s2">""</span>
                  <span class="ow">and</span> <span class="s2">"This is my currently opened file: "</span> <span class="o">..</span> <span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_buf_get_name</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
                <span class="ow">or</span> <span class="s2">""</span>
              <span class="p">)</span>
          <span class="k">end</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="n">mappings</span> <span class="o">=</span> <span class="p">{</span>
          <span class="p">[</span><span class="s2">"select_up"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"&lt;C-k&gt;"</span><span class="p">,</span>
          <span class="p">[</span><span class="s2">"select_down"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"&lt;C-j&gt;"</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="n">open_ai</span> <span class="o">=</span> <span class="p">{</span>
          <span class="n">url</span> <span class="o">=</span> <span class="s2">"http://127.0.0.1:9741/v1/chat/completions"</span><span class="p">,</span>
          <span class="n">display_name</span> <span class="o">=</span> <span class="s2">"llama.cpp"</span><span class="p">,</span>
          <span class="n">api_key</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">---@diagnostic disable-line: missing-fields</span>
            <span class="n">value</span> <span class="o">=</span> <span class="kc">nil</span><span class="p">,</span>
            <span class="n">get</span> <span class="o">=</span> <span class="k">function</span><span class="p">()</span>
              <span class="k">return</span> <span class="s2">""</span>
            <span class="k">end</span><span class="p">,</span>
          <span class="p">},</span>
        <span class="p">},</span>
      <span class="p">})</span>
    <span class="k">end</span><span class="p">,</span>

</code></pre></div></div>

<h2 id="secret-agents">Secret Agents</h2>

<p>Reading through the config you might see a dangerous option: <code class="language-plaintext highlighter-rouge">params = {
"--trust-all-tools" },</code>.</p>

<p>This allows <code class="language-plaintext highlighter-rouge">q</code> to execute all commands that are available to it in the current
context.</p>

<p>Yes, it’s dangerous, probably also a bit stupid. But really powerful, as well.
And a lot of fun, to be honest.</p>

<p>First I was annoyed by <em>Amazon Q</em> not providing a web API, but using the <code class="language-plaintext highlighter-rouge">cli</code>
as API makes the tool integration agentic, basically for free!</p>

<p>I give the currently open file as context in the system prompt.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">..</span> <span class="p">(</span>
  <span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_buf_get_name</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="o">~=</span> <span class="s2">""</span>
    <span class="ow">and</span> <span class="s2">"This is my currently opened file: "</span> <span class="o">..</span> <span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_buf_get_name</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
  <span class="ow">or</span> <span class="s2">""</span>
<span class="p">)</span>
</code></pre></div></div>

<p>With this, I can refer to the current file and let <em>q</em> interact with it.</p>

<p>Or even the entire project!</p>

<p>I was pretty amazed by pressing <code class="language-plaintext highlighter-rouge">&lt;Alt-a&gt;</code> in my editor and watching the
integration perform a full <em>Please run the tests in this project and tell me if
they were a success.</em> and things like <em>Create a feature branch hinting that I
created a new UI, commit the latest changes and push them.</em></p>

<h2 id="tldr">tl;dr</h2>

<ul>
  <li>Amazon Q is a developer centric AI by AWS</li>
  <li>Amazon Q does not provide a classic web API</li>
  <li>Amazon Q provides a <code class="language-plaintext highlighter-rouge">cli</code></li>
  <li>The <code class="language-plaintext highlighter-rouge">cli</code> can be integrated in <code class="language-plaintext highlighter-rouge">neovim</code></li>
  <li>The integration provides powerful assistance, through context-handover and
chat response or by interacting with entire projects through shell command
execution and interacting with files</li>
</ul>]]></content><author><name>Dominikus Gierlach</name></author><category term="programming" /><category term="linux" /><category term="vim" /><category term="amazon" /><category term="aws" /><category term="ai" /><category term="Q" /><summary type="html"><![CDATA[Amazon Q - Assistant in vim]]></summary></entry><entry><title type="html">Secure(ish) environment setup - direnv + gnome-keyring</title><link href="https://gierdo.astounding.technology/blog/2025/05/08/secureish-environment-setup" rel="alternate" type="text/html" title="Secure(ish) environment setup - direnv + gnome-keyring" /><published>2025-05-08T00:00:00+00:00</published><updated>2025-05-08T00:00:00+00:00</updated><id>https://gierdo.astounding.technology/blog/2025/05/08/secureish-environment-setup</id><content type="html" xml:base="https://gierdo.astounding.technology/blog/2025/05/08/secureish-environment-setup"><![CDATA[<h1 id="how-and-why-to-set-up-automated-environment-secret-stuff">How and why to set up automated environment secret stuff</h1>

<h2 id="the-problem">The Problem</h2>

<p>When working on several projects in different environments, interacting with
distributed and relatively loosely coupled <em>systems</em> and <em>platforms</em>, such as
different <em>Jira</em>, <em>GitLab</em>, <em>…</em> instances and other <em>API</em> providers, e.g.
<em>OpenAI</em> or an <em>Artifactory</em> or whatever.</p>

<p>I want to be able to <em>consume</em> these systems from my development environment,
which is basically my shell with dedicated <em>command line interfaces</em> and
<em>integrations</em> in <code class="language-plaintext highlighter-rouge">vim</code>.</p>

<p>How can I now configure all the endpoints and other <em>environmental aspects</em>,
including <em>API tokens</em> and other <em>secrets</em>, independently for different project
contexts, without having to copy-paste secrets or hard-code secrets somewhere,
e.g. putting clear-text tokens into <code class="language-plaintext highlighter-rouge">.env</code> files?</p>

<p>The solution should handle</p>

<ul>
  <li>Automation and convenience. Whenever I enter a specific directory that
specifies a context, e.g. a project or group of projects, I want the
environment to automagically be set up correctly. When I leave the
environment or change context, I want the environment to be teared down
again.</li>
  <li>Centralization. <em>Don’t repeat yourself</em> (DRY) is not just a good practice for
programming. I don’t want to have 27 places to touch when I changed my GitHub
API token.</li>
  <li>Security. <em>No</em> clear-text secrets stored somewhere, ever.</li>
</ul>

<h2 id="possible-solution">Possible Solution</h2>

<p>In pretty much every Linux environment that is not KDE, you should have the
<code class="language-plaintext highlighter-rouge">gnome-keyring</code> available. The <code class="language-plaintext highlighter-rouge">gnome-keyring</code> is basically a local database
that is <em>encrypted</em> with the user’s login password (-&gt; automatically decrypted
on login through pam), which provides a convenient centralised interface for
secrets/keys. <code class="language-plaintext highlighter-rouge">gnome-keyring</code> supports gpg and ssh keys, which it can unlock
and act as a key agent for, and generic passwords.</p>

<p>All passwords can then be retrieved by libsecret and the according tooling.
There is a built-in git integration (built into git, but you still have to
configure it!), and many applications use it already, e.g. the network manager,
Chrome, Chromium, Edge, …</p>

<p>This opens a solution path, if it would be possible to automatically <em>retrieve</em>
secrets from the <code class="language-plaintext highlighter-rouge">gnome-keyring</code> and create <em>environment variables</em> etc.</p>

<h2 id="solution">Solution</h2>

<p>There is a <em>cli</em>, <code class="language-plaintext highlighter-rouge">secret-tool</code>, which allows you to store and retrieve generic
secrets from/into the <code class="language-plaintext highlighter-rouge">gnome-keyring</code>. In combination with your shell setup
through <code class="language-plaintext highlighter-rouge">direnv</code>, this allows for pretty nice and still secure automation,
fulfilling all the goals.</p>

<h3 id="setup">Setup</h3>

<h4 id="gnome-keyring">gnome-keyring</h4>

<p>Well. You need the <code class="language-plaintext highlighter-rouge">gnome-keyring</code> to be set up, ideally unlocked automatically
on login. If you’re on gnome, that should come as a given. If you are using a
different environment, it might be a given.</p>

<h4 id="secret-tool">secret-tool</h4>

<p>Install the <code class="language-plaintext highlighter-rouge">secret-tool</code>. On <em>Debian</em> based systems, which includes all the
<em>Ubuntu</em> flavours, that’s done with</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>libsecret-tools
</code></pre></div></div>

<h4 id="direnv">direnv</h4>

<p>Install <code class="language-plaintext highlighter-rouge">direnv</code>, e.g., if you are on <em>Debian</em> based systems, with</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>direnv
</code></pre></div></div>

<p>And make sure to set it up and integrate it into your shell.
I am using zsh, so you will find the configuration in the <a href="https://github.com/gierdo/dotfiles/blob/c8877582a3f8fce10e5c78f4e9dc70b8e6a6e9fc/.zshrc#L148"><code class="language-plaintext highlighter-rouge">.zshrc</code></a></p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>...
<span class="k">if </span><span class="nb">command</span> <span class="nt">-v</span> direnv 1&gt;/dev/null 2&gt;&amp;1<span class="p">;</span> <span class="k">then
  </span>_evalcache direnv hook zsh
<span class="k">fi</span>
...
</code></pre></div></div>

<h3 id="how-does-it-work">How does it work?</h3>

<p>For example, I want to have a gitlab API token in all my repositories for
<a href="https://private-gitlab.com">https://private-gitlab.com</a>, so that I can quickly create new projects or open
/ interact with merge requests with the <em>gitlab cli</em>, <code class="language-plaintext highlighter-rouge">glab</code>. So, I have a file
<code class="language-plaintext highlighter-rouge">~/workspace/private-gitlab.com/.envrc</code> with the following content:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">GITLAB_TOKEN</span><span class="o">=</span><span class="si">$(</span>secret-tool lookup protocol https server private-gitlab.com user &lt;bob@secret.com&gt;<span class="si">)</span>
<span class="nb">export </span><span class="nv">GITLAB_URI</span><span class="o">=</span><span class="s2">"https://private-gitlab.com"</span>
<span class="nb">export </span><span class="nv">GITHUB_TOKEN</span><span class="o">=</span><span class="si">$(</span>secret-tool lookup protocol https server github.com user &lt;dominik.gierlach@gmail.com&gt;<span class="si">)</span>
</code></pre></div></div>

<p>Together with <code class="language-plaintext highlighter-rouge">direnv</code> set up and integrated in to the <code class="language-plaintext highlighter-rouge">shell</code>, as can be seen
im my <a href="https://github.com/gierdo/dotfiles">dotfiles.</a>, this retrieves a token
from the keyring whenever I enter a repository in
<code class="language-plaintext highlighter-rouge">~/workspace/private-gitlab.com/...</code> and sets it up as local environment
variable in this context.</p>

<h3 id="add--change-secrets">Add / change secrets</h3>

<p>The secrets themselves can be set up like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>secret-tool store <span class="nt">--label</span><span class="o">=</span><span class="s1">'Git: &lt;https://github.com/&gt;'</span> schema org.gnome.keyring.NetworkPassword protocol https user &lt;dominik.gierlach@gmail.com&gt; server github.com
</code></pre></div></div>

<p>Or by using <code class="language-plaintext highlighter-rouge">seahorse</code>, a graphical manager for the <code class="language-plaintext highlighter-rouge">gnome-keyring</code>.</p>

<h3 id="nested-environments">Nested environments</h3>

<p>With <code class="language-plaintext highlighter-rouge">direnv</code>, it’s possible to nest environments in order to extend them.</p>

<p>Assume a directory structure like this</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/workspace/github.com/..
  - .envrc
+ - project_group_1
  + - project_a
  + - project_b
+ - project_group_2
    - .envrc
  + - project_c
  + - project_d
</code></pre></div></div>

<p>I want to be able to use the environment configuration that is part of
<code class="language-plaintext highlighter-rouge">~/workspace/github.com/.envrc</code> in every project, but I want to change/extend it
for all projects in <code class="language-plaintext highlighter-rouge">~/workspace/github.com/project_group_2</code>.</p>

<p>That is possible by referring to the overarching <code class="language-plaintext highlighter-rouge">.envrc</code> from
<code class="language-plaintext highlighter-rouge">~/workspace/github.com/project_group_1/.envrc</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">source</span> ~/workspace/github.com/.envrc
<span class="nb">export </span><span class="nv">AWS_PROFILE</span><span class="o">=</span>PROJECT_GROUP_2_AWS
</code></pre></div></div>

<h2 id="bonus-setup-git-credential-integration-with-gnome-keyring">Bonus: Setup git credential integration with <code class="language-plaintext highlighter-rouge">gnome-keyring</code></h2>

<p>As written above, <code class="language-plaintext highlighter-rouge">gnome-keyring</code> can also store <code class="language-plaintext highlighter-rouge">git</code> credentials, when using
<em>https</em> authentication. While this is somewhat orthogonal to setting up shell
environments with secrets, it’s connected and important.</p>

<p><code class="language-plaintext highlighter-rouge">git</code> can use <a href="https://git-scm.com/docs/gitcredentials"><em>credential helpers</em></a>
to store and automagically use credentials for different repositories. The
trivial and built-in <em>credential helper</em> is <code class="language-plaintext highlighter-rouge">store</code>, which simply stores the
credentials on disk in <em>clear text</em>, so let’s not go down that path.</p>

<p>The <em>credential helper</em> to integrate with <code class="language-plaintext highlighter-rouge">gnome-keyring</code> on linux is
<code class="language-plaintext highlighter-rouge">git-credential-libsecret</code>.</p>

<h3 id="install-git-credential-libsecret">Install <code class="language-plaintext highlighter-rouge">git-credential-libsecret</code></h3>

<p>On Debian based systems, including Ubuntu, the credential helper is not
available as compiled binary, but the sourcecode is part of the <code class="language-plaintext highlighter-rouge">git</code> package.</p>

<p>Install it e.g. like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>libsecret-1-dev
<span class="nb">mkdir</span> <span class="nt">-p</span> ~/workspace/other/libsecret
<span class="nb">cd</span> ~/workspace/other/libsecret
<span class="nb">cp</span> <span class="nt">-r</span> /usr/share/doc/git/contrib/credential/libsecret/<span class="k">*</span> ./
make
<span class="nb">mkdir</span> <span class="nt">-p</span> ~/.local/bin
<span class="nb">cp </span>git-credential-libsecret ~/.local/bin
</code></pre></div></div>

<h3 id="set-up-git-credential-libsecret">Set up <code class="language-plaintext highlighter-rouge">git-credential-libsecret</code></h3>

<p>After it has been installed, let’s make git use it.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git config <span class="nt">--global</span> credential.helper libsecret
</code></pre></div></div>

<h3 id="automatic-setup-with-dotfiles-and-tuning">Automatic setup with dotfiles and tuning</h3>

<p>Yes, that was annoyingly complex, especially for a thing that should not be
forgotten whenever a new machine is set up.</p>

<p>That’s why I integrated the setup into my
<a href="https://github.com/gierdo/dotfiles">dotfiles.</a>
I don’t want to have to remember (= look up and re-understand) how to set it up
whenever I set up a new machine, and I don’t want to forget to set it up at
all.</p>

<p>The git configuration is part of the <a href="https://github.com/gierdo/dotfiles/blob/c8877582a3f8fce10e5c78f4e9dc70b8e6a6e9fc/.gitconfig#L31">global
<code class="language-plaintext highlighter-rouge">.gitconfig</code></a>,
installation is automated through
<a href="https://github.com/gierdo/dotfiles/blob/c8877582a3f8fce10e5c78f4e9dc70b8e6a6e9fc/tuning/programs.toml#L175">tuning</a>.</p>

<h2 id="tldr">tl;dr</h2>

<ul>
  <li>Sometimes secrets are needed in (shell based) environments, to be set up as
environment variables</li>
  <li>Tokens can be stored centrally and encrypted in the <code class="language-plaintext highlighter-rouge">gnome-keyring</code></li>
  <li>Tokens can be retrieved and exported as environment variables with
<code class="language-plaintext highlighter-rouge">secret-tool</code></li>
  <li>Environment specific automation thereof can be done with <code class="language-plaintext highlighter-rouge">direnv</code></li>
</ul>]]></content><author><name>Dominikus Gierlach</name></author><category term="programming" /><category term="linux" /><category term="security" /><category term="direnv" /><category term="environment" /><summary type="html"><![CDATA[How and why to set up automated environment secret stuff]]></summary></entry><entry><title type="html">neovim config - native lsp support instead of coc</title><link href="https://gierdo.astounding.technology/blog/2024/09/19/nvim-lsp" rel="alternate" type="text/html" title="neovim config - native lsp support instead of coc" /><published>2024-09-19T00:00:00+00:00</published><updated>2024-09-19T00:00:00+00:00</updated><id>https://gierdo.astounding.technology/blog/2024/09/19/nvim-lsp</id><content type="html" xml:base="https://gierdo.astounding.technology/blog/2024/09/19/nvim-lsp"><![CDATA[<h1 id="migrating-language-server-linting-and-formatting-away-from-coc">Migrating language server, linting and formatting away from <a href="https://github.com/neoclide/coc.nvim">coc</a></h1>

<h2 id="context">Context</h2>

<p>I am a big <a href="https://neovim.io">vim (neovim)</a> fan and do basically all my
programming and writing in it, the exception being work things that need MS
Office. Luckily, most of those things are possible with the browser-based
version of office, the exception being macros and obscure things like
Powerpoint footer changes. Anyways, that is a completely different story.</p>

<p>Naked, vanilla vim is nice and present in basically every environment. So, it’s
good to be able to use it, especially for remote
configuration/documentation/troubleshooting work. The real power is unleashed
with more advanced capabilities and extensions, which extend the barebone
editor with modern creature comforts, such as <em>linting</em>, <em>automatic fixing</em>,
<em>code completion</em>, <em>auto formatting</em>, <em>syntax highlighting</em>, <em>test execution</em>,
<em>debugging</em> and <em>intelligent navigation</em>.</p>

<p>My <em>code completion</em> and <em>intelligent navigation</em> setup started with
<a href="https://github.com/ycm-core/YouCompleteMe">YouCompleteMe</a> in 2017. I needed it
mainly for <code class="language-plaintext highlighter-rouge">C</code>, and it did a good job.</p>

<p>A bit later, Microsoft’s <em>Language Server Protocol</em> gained much more traction,
with more and more really usable <a href="https://microsoft.github.io/language-server-protocol/implementors/servers/"><em>language
servers</em></a>
being really usable and taking off, mainly n the context of <em>visual studio
code</em>. Most of the Language servers where installed and integrated into
<em>vscode</em> through dedicated <em>plugins</em>.</p>

<p>In comes the neovim plugin <a href="https://github.com/neoclide/coc.nvim">coc (conquer of
completion)</a>. This plugin acts as a
<em>language server client</em>, meaning that it can use any <em>language server</em> that
implements the lsp standard, and it can integrate slightly adapted <em>vscode
pluins</em> as <em>coc plugins</em>. This meant that the managed installation of language
servers and the integration thereof became really simple and straightforward,
with a very active ecosystem and community around it. I migrated to <em>coc</em> in
2019, adding support and better integration for many more languages into my
setup.</p>

<p><em>coc</em> with a set of plugins and a handful of “manually” integrated language
servers as well as with a set of linters and formatters, integrated with
<a href="https://github.com/iamcco/diagnostic-languageserver">diagnostic-languageserver</a>,
was my default setup for 5 years. I changed the supported languages all the
time, based on what I needed, but besides that and <a href="/blog/2023/12/01/nvim-config-lua">rewriting the config to
lua</a>, my setup stayed like
this for 5 years.</p>

<p>In the meantime, <em>neovim</em> gained native <em>language server client</em> support,
removing the need for a dedicated plugin for lsp support. However, <em>coc</em> still
provided the easy integration and awesome out of the box functionality.</p>

<p>The awesome and very capable community embraced the feature, though. There is
<a href="https://github.com/williamboman/mason.nvim">mason</a>, an nvim native package/lsp
manager, and many integration layer plugins between neovim and mason, making
the integration of language servers slimmer (no extra layer), extremely
flexible and manageable, with concise and powerful configuration.</p>

<p>Powered by all of that, projects like <a href="https://www.lazyvim.org/">lazyvim</a>
started, providing much more out of the box functionality and ease of use than
coc had. Lazyvim is basically a full-featured one size fits all
preconfiguration of nvim, with very sane defaults and a smart plugin and
configuration system that makes it feature rich without bloat. I know a few
people who recently went down the rabbithole of vim, and they started with
lazyvim right away. I respect it a lot, even though I want to have a bit more
control over my setup, so not for me. The ecosystem grew and grew and is now
more active and powerful than coc’s ecosystem.</p>

<p>I was very interested in the principle of the powerful and manageable
modularization, but still, the normal out of the box experience was a lot worse
than with coc, as it was basically not a box, but a collection of elements you
had to build your box with yourself. The capabilities of coc are split into
separate and independent components for completion (with lsp as source amongst
others), dedicated components for formatters, linters, dedicated configuration
of all components and system integration.</p>

<p>Interesting, but it’s a lot. Especially because my current setup just worked
and did mostly what I wanted it to do, and I had no real functioning reason to
migrate besides the itch. So, I put a migration on the back of my to do list,
to do some other time.</p>

<h2 id="some-other-time-is-now">Some other time is <em>now</em></h2>

<p>That is until for the first time ever I had to do things in <code class="language-plaintext highlighter-rouge">C#</code>. There is a
coc plugin for <code class="language-plaintext highlighter-rouge">C#</code>,
<a href="https://github.com/coc-extensions/coc-omnisharp">coc-omnisharp</a>, which in the
README greets you with the message:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>⛔ This project is lacking proper maintanence. I would recommend csharp-ls at this moment.
</code></pre></div></div>

<p>This leaves manual integration of the
<a href="https://github.com/razzmatazz/csharp-language-server">csharp-language-server</a>.
Awesome, I did it right away.</p>

<p>But there was a problem.</p>

<p>With the coc integration, I was unable to find a way to replace the default
sync handler for system libraries only available as binaries, used e.g. for
navigation, such as <em>go_to_definition</em>, with a buffer showing the decompiled
sourcecode. This is explicitly (and easily) possible with the flexible
configuration of native language servers in nvim, as
<a href="https://github.com/Decodetalkers/csharpls-extended-lsp.nvim">documented</a>.</p>

<p>So, last weekend, I did it.</p>

<p>I migrated my configuration.</p>

<h3 id="structure">Structure</h3>

<p>I use <a href="https://github.com/folke/lazy.nvim">lazy</a> as plugin manager, which makes
it really simple to split up plugin configuration. I decided to split my
language server config into a dedicated file for the abstract definition of the
behaviour of lsps, e.g. with key mappings, sources of the completion system and
so on, a dedicated file for the specific definition of supported languages, and
a dedicated file for the separate configuration of linters and fixers, while
also revamping the test execution and debugging setup.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/home/gierdo/.dotfiles/.config/nvim/lua/plugins

❯ tree
.
├── debugging.lua
├── filetree.lua
├── fuzzyness.lua
├── git.lua
├── init.lua
├── linters.lua
├── lsp-behaviour.lua
├── lsp-languages.lua
├── neoai.lua
├── testing.lua
├── theme.lua
└── treesitter.lua
</code></pre></div></div>

<p>Copy-pasting the few hundred lines of lua here probably makes this post even
less readable, but I’ll point out some things here that are not just obvious
configuration.</p>

<p>If you want to see the full configuration, check out my
<a href="https://github.com/gierdo/dotfiles/tree/master/.config/nvim/lua/plugins">dotfiles.</a></p>

<h4 id="the-original-motivation-c-decompiler-integration">The original motivation: C# decompiler integration</h4>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="kd">local</span> <span class="n">cs_config</span> <span class="o">=</span> <span class="p">{</span>
        <span class="n">capabilities</span> <span class="o">=</span> <span class="n">lsp_capabilities</span><span class="p">,</span>
        <span class="n">handlers</span> <span class="o">=</span> <span class="p">{</span>
          <span class="p">[</span><span class="s2">"textDocument/definition"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">require</span><span class="p">(</span><span class="s2">"csharpls_extended"</span><span class="p">).</span><span class="n">handler</span><span class="p">,</span>
          <span class="p">[</span><span class="s2">"textDocument/typeDefinition"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">require</span><span class="p">(</span><span class="s2">"csharpls_extended"</span><span class="p">).</span><span class="n">handler</span><span class="p">,</span>
        <span class="p">},</span>
      <span class="p">}</span>
      <span class="n">lspconfig</span><span class="p">.</span><span class="n">csharp_ls</span><span class="p">.</span><span class="n">setup</span><span class="p">(</span><span class="n">cs_config</span><span class="p">)</span>
</code></pre></div></div>

<h4 id="fuzzy-and-fancy-selection-for-multiple-navigation-target-hits">Fuzzy and fancy selection for multiple navigation target hits</h4>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code>          <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">(</span>
            <span class="s2">"n"</span><span class="p">,</span>
            <span class="s2">"gd"</span><span class="p">,</span>
            <span class="nb">require</span><span class="p">(</span><span class="s1">'telescope.builtin'</span><span class="p">).</span><span class="n">lsp_definitions</span><span class="p">,</span>
            <span class="p">{</span> <span class="n">buffer</span> <span class="o">=</span> <span class="n">event</span><span class="p">.</span><span class="n">buf</span><span class="p">,</span> <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Go to definition."</span> <span class="p">}</span>
          <span class="p">)</span>
          <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">(</span>
            <span class="s2">"n"</span><span class="p">,</span>
            <span class="s2">"gi"</span><span class="p">,</span>
            <span class="nb">require</span><span class="p">(</span><span class="s1">'telescope.builtin'</span><span class="p">).</span><span class="n">lsp_implementations</span><span class="p">,</span>
            <span class="p">{</span> <span class="n">buffer</span> <span class="o">=</span> <span class="n">event</span><span class="p">.</span><span class="n">buf</span><span class="p">,</span> <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Go to implementation."</span> <span class="p">}</span>
          <span class="p">)</span>
          <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">(</span>
            <span class="s2">"n"</span><span class="p">,</span>
            <span class="s2">"go"</span><span class="p">,</span>
            <span class="nb">require</span><span class="p">(</span><span class="s1">'telescope.builtin'</span><span class="p">).</span><span class="n">lsp_type_definitions</span><span class="p">,</span>
            <span class="p">{</span> <span class="n">buffer</span> <span class="o">=</span> <span class="n">event</span><span class="p">.</span><span class="n">buf</span><span class="p">,</span> <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Go to type definition."</span> <span class="p">}</span>
          <span class="p">)</span>
          <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">(</span>
            <span class="s2">"n"</span><span class="p">,</span>
            <span class="s2">"gr"</span><span class="p">,</span>
            <span class="nb">require</span><span class="p">(</span><span class="s1">'telescope.builtin'</span><span class="p">).</span><span class="n">lsp_references</span><span class="p">,</span>
            <span class="p">{</span> <span class="n">buffer</span> <span class="o">=</span> <span class="n">event</span><span class="p">.</span><span class="n">buf</span><span class="p">,</span> <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Show references."</span> <span class="p">}</span>
          <span class="p">)</span>
</code></pre></div></div>

<h4 id="extended-json--and-yaml-schema-support">Extended json- and yaml schema support</h4>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="n">lspconfig</span><span class="p">.</span><span class="n">jsonls</span><span class="p">.</span><span class="n">setup</span><span class="p">({</span>
        <span class="n">capabilities</span> <span class="o">=</span> <span class="n">lsp_capabilities</span><span class="p">,</span>
        <span class="n">settings</span> <span class="o">=</span> <span class="p">{</span>
          <span class="n">json</span> <span class="o">=</span> <span class="p">{</span>
            <span class="n">schemas</span> <span class="o">=</span> <span class="nb">require</span><span class="p">(</span><span class="s2">"schemastore"</span><span class="p">).</span><span class="n">json</span><span class="p">.</span><span class="n">schemas</span><span class="p">(),</span>
            <span class="n">validate</span> <span class="o">=</span> <span class="p">{</span> <span class="n">enable</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">},</span>
            <span class="n">schemaDownload</span> <span class="o">=</span> <span class="p">{</span> <span class="n">enable</span> <span class="o">=</span> <span class="kc">false</span> <span class="p">},</span>
          <span class="p">},</span>
        <span class="p">},</span>
      <span class="p">})</span>

      <span class="n">lspconfig</span><span class="p">.</span><span class="n">yamlls</span><span class="p">.</span><span class="n">setup</span><span class="p">({</span>
        <span class="n">capabilities</span> <span class="o">=</span> <span class="n">lsp_capabilities</span><span class="p">,</span>
        <span class="n">settings</span> <span class="o">=</span> <span class="p">{</span>
          <span class="n">yaml</span> <span class="o">=</span> <span class="p">{</span>
            <span class="n">schemaStore</span> <span class="o">=</span> <span class="p">{</span>
              <span class="n">enable</span> <span class="o">=</span> <span class="kc">false</span><span class="p">,</span>
              <span class="n">url</span> <span class="o">=</span> <span class="s2">""</span><span class="p">,</span>
            <span class="p">},</span>
            <span class="n">schemas</span> <span class="o">=</span> <span class="nb">require</span><span class="p">(</span><span class="s2">"schemastore"</span><span class="p">).</span><span class="n">yaml</span><span class="p">.</span><span class="n">schemas</span><span class="p">({</span>
              <span class="n">extra</span> <span class="o">=</span> <span class="p">{</span>
                <span class="p">{</span>
                  <span class="n">name</span> <span class="o">=</span> <span class="s2">"Cloudformation"</span><span class="p">,</span>
                  <span class="n">description</span> <span class="o">=</span> <span class="s2">"Cloudformation Template"</span><span class="p">,</span>
                  <span class="n">fileMatch</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"*.template.y*ml"</span><span class="p">,</span> <span class="s2">"*-template.y*ml"</span> <span class="p">},</span>
                  <span class="n">url</span> <span class="o">=</span> <span class="s2">"https://raw.githubusercontent.com/awslabs/goformation/master/schema/cloudformation.schema.json"</span><span class="p">,</span>
                <span class="p">},</span>
              <span class="p">},</span>
            <span class="p">}),</span>
            <span class="n">customTags</span> <span class="o">=</span> <span class="p">{</span>
              <span class="c1">-- Cloudformation tags</span>
              <span class="s2">"!And scalar"</span><span class="p">,</span>
              <span class="s2">"!If scalar"</span><span class="p">,</span>
              <span class="s2">"!Not"</span><span class="p">,</span>
              <span class="s2">"!Equals scalar"</span><span class="p">,</span>
              <span class="s2">"!Or scalar"</span><span class="p">,</span>
              <span class="s2">"!FindInMap scalar"</span><span class="p">,</span>
              <span class="s2">"!Base64"</span><span class="p">,</span>
              <span class="s2">"!Cidr"</span><span class="p">,</span>
              <span class="s2">"!Ref"</span><span class="p">,</span>
              <span class="s2">"!Sub"</span><span class="p">,</span>
              <span class="s2">"!GetAtt sequence"</span><span class="p">,</span>
              <span class="s2">"!GetAZs"</span><span class="p">,</span>
              <span class="s2">"!ImportValue sequence"</span><span class="p">,</span>
              <span class="s2">"!Select sequence"</span><span class="p">,</span>
              <span class="s2">"!Split sequence"</span><span class="p">,</span>
              <span class="s2">"!Join sequence"</span><span class="p">,</span>
            <span class="p">},</span>
          <span class="p">},</span>
        <span class="p">},</span>
      <span class="p">})</span>
</code></pre></div></div>

<h4 id="imho-smart-test-execution-and-debugging-keymaps">imho smart test execution and debugging keymaps</h4>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="kd">local</span> <span class="n">wk</span> <span class="o">=</span> <span class="nb">require</span><span class="p">(</span><span class="s2">"which-key"</span><span class="p">)</span>

      <span class="n">wk</span><span class="p">.</span><span class="n">add</span><span class="p">({</span>
        <span class="p">{</span> <span class="s2">"&lt;leader&gt;t"</span><span class="p">,</span> <span class="n">group</span> <span class="o">=</span> <span class="s2">"   Test"</span> <span class="p">},</span>
        <span class="p">{</span> <span class="s2">"&lt;leader&gt;tr"</span><span class="p">,</span> <span class="n">neotest</span><span class="p">.</span><span class="n">run</span><span class="p">.</span><span class="n">run</span><span class="p">,</span> <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Run nearest test."</span> <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;tf"</span><span class="p">,</span>
          <span class="k">function</span><span class="p">()</span>
            <span class="n">neotest</span><span class="p">.</span><span class="n">run</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">vim</span><span class="p">.</span><span class="n">fn</span><span class="p">.</span><span class="n">expand</span><span class="p">(</span><span class="s2">"%"</span><span class="p">))</span>
          <span class="k">end</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Run current file."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;td"</span><span class="p">,</span>
          <span class="k">function</span><span class="p">()</span>
            <span class="n">neotest</span><span class="p">.</span><span class="n">run</span><span class="p">.</span><span class="n">run</span><span class="p">({</span> <span class="n">strategy</span> <span class="o">=</span> <span class="s2">"dap"</span> <span class="p">})</span>
          <span class="k">end</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Debug nearest test."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;ts"</span><span class="p">,</span>
          <span class="n">neotest</span><span class="p">.</span><span class="n">summary</span><span class="p">.</span><span class="n">toggle</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Show/hide summary."</span><span class="p">,</span>
        <span class="p">},</span>
      <span class="p">})</span>

      <span class="n">wk</span><span class="p">.</span><span class="n">add</span><span class="p">({</span>
        <span class="p">{</span> <span class="s2">"&lt;leader&gt;d"</span><span class="p">,</span> <span class="n">group</span> <span class="o">=</span> <span class="s2">"   Debug"</span> <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;dd"</span><span class="p">,</span>
          <span class="n">telescope</span><span class="p">.</span><span class="n">extensions</span><span class="p">.</span><span class="n">dap</span><span class="p">.</span><span class="n">commands</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Show debug commands."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;dt"</span><span class="p">,</span>
          <span class="n">dap</span><span class="p">.</span><span class="n">toggle_breakpoint</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Toggle line breakpoint on the current line."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;dbs"</span><span class="p">,</span>
          <span class="n">dap</span><span class="p">.</span><span class="n">set_breakpoint</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Set breakpoint."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;dbl"</span><span class="p">,</span>
          <span class="n">telescope</span><span class="p">.</span><span class="n">extensions</span><span class="p">.</span><span class="n">dap</span><span class="p">.</span><span class="n">list_breakpoints</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"List breakpoints."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;dv"</span><span class="p">,</span>
          <span class="n">telescope</span><span class="p">.</span><span class="n">extensions</span><span class="p">.</span><span class="n">dap</span><span class="p">.</span><span class="n">variables</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Show variables."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;dbc"</span><span class="p">,</span>
          <span class="n">dap</span><span class="p">.</span><span class="n">clear_breakpoints</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Clear breakpoints."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;dbe"</span><span class="p">,</span>
          <span class="n">dap</span><span class="p">.</span><span class="n">set_exception_breakpoints</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Set Exception breakpoints."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;dc"</span><span class="p">,</span>
          <span class="n">dap</span><span class="p">.</span><span class="n">continue</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"When debugging, continue. Otherwise start debugging."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;dfd"</span><span class="p">,</span>
          <span class="n">dap</span><span class="p">.</span><span class="n">down</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Move down a frame in the current call stack."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;dfu"</span><span class="p">,</span>
          <span class="n">dap</span><span class="p">.</span><span class="n">up</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Move up a frame in the current call stack."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">{</span> <span class="s2">"&lt;leader&gt;dp"</span><span class="p">,</span> <span class="n">dap</span><span class="p">.</span><span class="n">pause</span><span class="p">,</span> <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Pause debuggee."</span> <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;dr"</span><span class="p">,</span>
          <span class="n">dap</span><span class="p">.</span><span class="n">run_to_cursor</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Continues execution to the current cursor."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">{</span>
          <span class="s2">"&lt;leader&gt;dre"</span><span class="p">,</span>
          <span class="n">dap</span><span class="p">.</span><span class="n">restart</span><span class="p">,</span>
          <span class="n">desc</span> <span class="o">=</span> <span class="s2">"Restart debugging with the same configuration."</span><span class="p">,</span>
        <span class="p">},</span>
      <span class="p">})</span>
</code></pre></div></div>

<h2 id="tldr">tl;dr</h2>

<ul>
  <li>language servers and such + vim are great</li>
  <li>there are many ways to integrate vim with language servers</li>
  <li><a href="https://github.com/neoclide/coc.nvim">coc</a> is not that slim and flexible
with good out of the box experience</li>
  <li>native nvim lsp support is slim and flexible with worse out of the box
experience</li>
  <li>with existing plugins and thanks to the community, benefiting from the
advantages of native nvim lsp support is possible in a sane way</li>
</ul>]]></content><author><name>Dominikus Gierlach</name></author><category term="programming" /><category term="linux" /><category term="vim" /><summary type="html"><![CDATA[Migrating language server, linting and formatting away from coc]]></summary></entry><entry><title type="html">atuin server on a Raspberry PI - Cross compiling Rust</title><link href="https://gierdo.astounding.technology/blog/2024/07/07/atuin-pi" rel="alternate" type="text/html" title="atuin server on a Raspberry PI - Cross compiling Rust" /><published>2024-07-07T00:00:00+00:00</published><updated>2024-07-07T00:00:00+00:00</updated><id>https://gierdo.astounding.technology/blog/2024/07/07/atuin-pi</id><content type="html" xml:base="https://gierdo.astounding.technology/blog/2024/07/07/atuin-pi"><![CDATA[<h1 id="atuin-server-on-a-raspberry-pi---cross-compiling-rust">atuin server on a Raspberry PI - Cross compiling Rust</h1>

<p>I’m a big fan of <a href="https://docs.atuin.sh/">atuin</a>, a shell history manager,
which replaces the flat command history file with a local database, fully
searcheable and with builtin synchronization support.</p>

<p>My setup of atuin,
<a href="https://github.com/gierdo/dotfiles/blob/master/tuning/programs.toml#L162">installation</a>
and
<a href="https://github.com/gierdo/dotfiles/blob/master/.config/atuin/config.toml">configuration</a>,
can be seen in my <a href="https://github.com/gierdo/dotfiles">dotfiles</a>. But the
<a href="https://docs.atuin.sh/guide/installation/">documentation</a> is excellent.</p>

<p>In case you are wondering why the sync configuration is not in my config: I try
to keep all specific configuration of stuff that is not publicly available and
might not be needed in all my environments outside of the dotfiles. I set
<code class="language-plaintext highlighter-rouge">$ATUIN_SYNC_ADDRESS</code> in my <code class="language-plaintext highlighter-rouge">~/.profile.local</code>.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export ATUIN_SYNC_ADDRESS=http://raspberrypi.local:8888
</code></pre></div></div>

<p>Atuin offers a free synchronization server at <a href="https://api.atuin.sh">https://api.atuin.sh</a>. Yes, it’s
not a good idea to show and open up your command history to everybody. Which is
why the synchronization is completely end-to-end encrypted by atuin. The
synchronization server only sees gibberish, and all your clients have to be set
up with a passkey to enable sync.</p>

<p>If you, like me, still don’t want to trust that data to a sync service, you can
also run your own <a href="https://docs.atuin.sh/self-hosting/server-setup/">synchronization
server</a>, which is pretty
straightforward, assuming you know a bit about services and postgresql.</p>

<p>Of course that means that you have to run a server somewhere…</p>

<p>I happen to have a rasperry pi 3 in my home network, which mainly acts as an
always-on member of <a href="https://syncthing.net/">syncthing</a>-synchronized
directories, and as a git repository server for private repositories with
<a href="https://gitolite.com/gitolite/index.html">gitolite</a>. I don’t expose the Pi at
all to the public internet, it is only available internally, resolveable
through <code class="language-plaintext highlighter-rouge">mDNS</code>. I wanted to set it up as atuin synchronization server. It’s
already there, it is available for all devices in my local home network. This,
of course, means that I can only synchronize shell history with my machines at
home. But I don’t have a problem with this limitation at all.</p>

<h2 id="the-problem">The problem</h2>

<p>Atuin is not installable through the raspbian package sources. This is also
true for Debian at the time of writing. On my machine, I install it with cargo,
orchestrated with <code class="language-plaintext highlighter-rouge">tuning</code>.</p>

<p>So, how do you install atuin on my raspberry pi, which uses an ARM CPU?
Especially if you don’t want to compile it on the pi itself, wait for ages and
fill your pi with stuff you don’t really need?</p>

<h2 id="the-solution">The solution</h2>

<p><a href="https://github.com/atuinsh/atuin">Atuin</a> is a program written in Rust. This
means that</p>

<ol>
  <li>Programs are compiled to architecture specific binaries</li>
  <li>Programs can be built pretty easily with Rust’s awesome package manager
<a href="https://doc.rust-lang.org/cargo/"><code class="language-plaintext highlighter-rouge">cargo</code></a></li>
  <li><a href="https://rust-lang.github.io/rustup/cross-compilation.html">Cross
compilation</a> is
pretty straightforward, thanks to Rust.</li>
</ol>

<p>As usual, the devil is in the details, however.</p>

<h2 id="the-details">The details</h2>

<h3 id="setting-up-rust-for-cross-compilation">Setting up Rust for cross-compilation</h3>

<p>With <code class="language-plaintext highlighter-rouge">rustup</code> as rust toolchain manager, it’s easy to install the rust specific
target and toolchain for the raspberry pi.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ rustup target add armv7-unknown-linux-gnueabihf
</code></pre></div></div>

<p>This is not enough, however. For successful cross compilation, rust also needs
the target specific gcc toolchain to link against C libraries and sysroot.</p>

<h3 id="setting-up-the-system">Setting up the system</h3>

<p>You need gcc for cross-compilation with the <code class="language-plaintext highlighter-rouge">armhf</code> target, as well as the
<code class="language-plaintext highlighter-rouge">armhf</code> specific libraries for dynamic linking.</p>

<p>On a Debian system, this can be installed like so.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo apt-get install gcc-arm-linux-gnueabihf libgcc-13-dev-armhf-cross
</code></pre></div></div>

<p>The installation of gcc and the sysroot is on a system level, independent from
the local rust setup. This means that cargo has to be set up correctly so that
it uses the correct linker.</p>

<h3 id="configuring-cargo">Configuring cargo</h3>

<p>Target specific attributes can be configured in <code class="language-plaintext highlighter-rouge">~/.cargo/config.toml</code></p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[target.armv7-unknown-linux-gnueabihf]</span>
<span class="py">linker</span> <span class="p">=</span> <span class="s">"arm-linux-gnueabihf-gcc"</span>
</code></pre></div></div>

<h2 id="compilation">Compilation</h2>

<p>After everything has been set up, cargo can be built in the local directory with this command:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cargo install --target=armv7-unknown-linux-gnueabihf --target-dir=$(pwd) --root=$(pwd) atuin
</code></pre></div></div>

<p>This will download the sources of <code class="language-plaintext highlighter-rouge">atuin</code> and all dependencies, cross-compile
it and copy it to a <code class="language-plaintext highlighter-rouge">bin</code> subdirectory.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ readelf -h bin/atuin
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Position-Independent Executable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0xd363d
  Start of program headers:          52 (bytes into file)
  Start of section headers:          32358908 (bytes into file)
  Flags:                             0x5000400, Version5 EABI, hard-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         10
  Size of section headers:           40 (bytes)
  Number of section headers:         44
  Section header string table index: 43
</code></pre></div></div>

<h2 id="installation">Installation</h2>

<p>Copy the binary onto your raspberry pi, e.g. to <code class="language-plaintext highlighter-rouge">/usr/local/bin/atuin</code>, and
you’re good to go!</p>

<h2 id="tldr">tl;dr</h2>

<ul>
  <li>atuin is great to manage shell history</li>
  <li>atuin sync is great and pretty safe</li>
  <li>atuin sync on a raspberry pi under your control is even safer</li>
  <li>cross-compilation with Rust is relatively easy</li>
  <li>the devil is in the details</li>
</ul>]]></content><author><name>Dominikus Gierlach</name></author><category term="programming" /><category term="linux" /><category term="rust" /><category term="build" /><category term="atuin" /><summary type="html"><![CDATA[atuin server on a Raspberry PI - Cross compiling Rust]]></summary></entry><entry><title type="html">Local AI assistant - llama3</title><link href="https://gierdo.astounding.technology/blog/2024/05/20/llama3" rel="alternate" type="text/html" title="Local AI assistant - llama3" /><published>2024-05-20T00:00:00+00:00</published><updated>2024-05-20T00:00:00+00:00</updated><id>https://gierdo.astounding.technology/blog/2024/05/20/llama3</id><content type="html" xml:base="https://gierdo.astounding.technology/blog/2024/05/20/llama3"><![CDATA[<h1 id="update-local-ai-assistant---llama-3">Update: Local AI assistant - llama 3</h1>

<p>You might have read my previous posts about <a href="/blog/2023/11/24/llama-vim">using a local codellama for a vim
coding assistant</a>, setting it up to
use <a href="/blog/2023/12/16/llama-vim-igpu-amd">my laptop’s GPU for inference</a> and the <a href="/blog/2023/12/17/llama-vim-igpu-amd-performance">performance evaluation</a> and so on.</p>

<p>Also, my previous <a href="/blog/2023/12/28/codellama-vs-mistral">evaluation of different models</a>, which formed the base of my
selection of which model to run locally.</p>

<p>Since quite a bit of time, there has been a new kid on the block: Meta’s <em>llama
3</em> is here! I’ve been running it since April, and it basically replaced my
previous local model.</p>

<p>I will not repeat the evaluation of the previous models, but apply the same
methods to <em>llama 3</em>. For the others, please take a look at the previous
article. Or, even better, try them out yourself.</p>

<h2 id="environment">Environment</h2>

<p>The general setup is described already in previous articles, mainly <a href="/blog/2023/12/16/llama-vim-igpu-amd">the
performance evaluation of GPU assisted inference</a></p>

<p>Here is the short summary:</p>

<ul>
  <li>Lenovo Thinkpad t14s gen 4</li>
  <li>llama.cpp with ROCm on AMD Ryzen 7 PRO 7840U w/ Radeon 780M Graphics, 8G VRAM</li>
</ul>

<p>This results an boundary conditions for the models to chose from: The models
have to be usable with llama.cpp and cope with 8GB of VRAM.</p>

<p><em>llama 3</em> fits that requirement nicely in the quanticised 8B instruct variant:
<code class="language-plaintext highlighter-rouge">Meta-Llama-3-8B-Instruct.Q4_K_M.gguf</code></p>

<h2 id="method">Method</h2>

<p>I gave each model a specific task:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Please create a c function for an embedded platform (no stl or time.h, no
POSIX), which returns the unix timestamp for a given time.
</code></pre></div></div>

<p>With the created code in the context, I want to test each model’s ability to
work with given code, e.g. for comments and improvement.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>What is happening here?

What are potential problems with the given code?

Please give me a well-documented version of this code. Please follow best
practices and do not comment overly verbosely.
</code></pre></div></div>

<h3 id="llama-3">llama 3</h3>

<p>Llama 3 happy starts right away, ignoring significant parts of the instruction,
however: The given value is not unix timestamp compliant.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>--------

Here is a simple C function that calculates the Unix timestamp (seconds since
January 1, 1970, 00:00:00 UTC) from a given year, month, day, hour, minute and
second:

```c
#include &lt;stdint.h&gt;

uint32_t unix_timestamp(int year, int month, int day, int hour, int min, int sec) {
    uint32_t timestamp = 0;

    // Calculate the seconds since January 1, 1970, 00:00:00 UTC
    timestamp += (year - 1970) * 31536000; // 31536000 is the number of seconds in a year

    // Adjust for leap years
    if ((year % 4 == 0 &amp;&amp; year % 100 != 0) || year % 400 == 0)
        timestamp++; // Add one second for February 29th

    int month_days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    timestamp += (month - 1) * 2628000 + day * 86400; // Add days and hours
    timestamp += (hour * 3600); // Add hours
    timestamp += (min * 60); // Add minutes
    timestamp += sec;

    return timestamp;
}
```

Note: This function assumes that the input values are valid. It does not
perform any error checking or handling.

--------

</code></pre></div></div>

<p>The good thing is that llama is aware of it when asked:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>--------

Are you sure this function returns a unix compliant timestamp?

--------

I made a mistake. The function I provided earlier does not correctly calculate
the Unix timestamp.

A Unix timestamp is the number of seconds that have elapsed since January 1,
1970, at midnight in Coordinated Universal Time (UTC). To correctly calculate
this, we need to consider the following:

* The epoch date (January 1, 1970)
* The leap years
* The daylight saving time

Here's a revised version of the function that calculates the Unix timestamp
more accurately:
```c
#include &lt;stdint.h&gt;

uint32_t unix_timestamp(int y, int m, int d, int h, int mi, int s) {
    uint32_t timestamp = 0;

    // Calculate the seconds since January 1, 1970, 00:00:00 UTC
    timestamp += (y - 1970) * 31536000; // 31536000 is the number of seconds in a year

    // Adjust for leap years
    if ((y % 4 == 0 &amp;&amp; y % 100 != 0) || y % 400 == 0)
        timestamp++; // Add one second for February 29th

    int month_days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    timestamp += (m - 1) * 2628000 + d * 86400; // Add days and hours
    timestamp += h * 3600; // Add hours
    timestamp += mi * 60; // Add minutes
    timestamp += s;

    return timestamp;
}
```
This function takes into account the leap years, but it still does not consider
daylight saving time. If you need to account for daylight saving time, you
would need a more complex algorithm.

Keep in mind that this function is still not fully compliant with the Unix
timestamp standard, as it does not take into account the start and end times of
daylight saving time.

--------
</code></pre></div></div>

<p>Now, this is pretty nice. And we are all just humans, after all.</p>

<p>Now, let’s see if it understands what it has created.</p>

<p>With the (revised) generated function in the context, let’s ask it what is
happening, and what potential issues could be:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>What is happening here?

--------

This is a C function named `unix_timestamp` that takes six integer arguments:
year (`y`), month (`m`), day (`d`), hour (`h`), minute (`mi`), and second
(`s`). It calculates the Unix timestamp (number of seconds since January 1,
1970, 00:00:00 UTC) based on these input values.

--------

What are potential problems with the given code?

--------

1. The function does not handle invalid inputs such as dates that do not exist
   (e.g., February 30, 29 or April 31), negative months, or times outside of
   the range 0-23 for hours and 0-59 for minutes.

2. It does not consider daylight saving time (DST) changes, which can affect
   the accuracy of the timestamp calculation.

3. The function assumes that the system's clock is set to UTC, but if the
   system is in a different timezone, the calculated timestamp will be off by
   the difference between the local timezone and UTC.

4. It does not handle overflow when calculating the timestamp. If the sum of
   seconds exceeds 2^32-1 (the maximum value for a uint32_t), the calculation
   will wrap around, resulting in an incorrect timestamp.

5. The function can be improved to use more descriptive variable names instead
   of single-letter variable names like 'y', 'm', 'd', 'h', 'mi', 's'.

6. It does not consider the fact that month days are different for February and
   it's not correct to subtract 1 from month, because January is also a month.

7. The function should return a time_t type instead of uint32_t.

8. The code can be simplified by removing the conditional statements in the
   calculation of leap year.

9. The variable `month_days` is not necessary and can be removed.

10. The comment for the line `timestamp  += (y - 1970) * 31536000;` should be
    updated to accurately describe what it does.

---------

Can you give me an improved version?

--------

Here is a revised version of your code:

```c
#include &lt;stdint.h&gt;

time_t unix_timestamp(int year, int month, int day, int hour, int minute, int second) {
    if (month &lt; 1 || month &gt; 12)
        return -1;

    time_t timestamp = 0;

    // Calculate the seconds since January 1, 1970
    int total_seconds_in_year = 31536000; // 31536000 is the number of seconds in a year
    if ((year % 4 == 0 &amp;&amp; year % 100 != 0) || year % 400 == 0)
        ++total_seconds_in_year;

    int[] month_days = {31,28,31,30,31,30,31,31,30,31,30,31};
    int day_of_year = 0;
    for(int i = 1; i &lt; month; i++) {
        day_of_year += month_days[i-1];
    }
    day_of_year += day - 1;

    timestamp = (year - 1970) * total_seconds_in_year + day_of_year * 86400 + hour * 3600 + minute * 60 + second;

    return timestamp;
}
```

This version includes error checking for the month, adds a comment to describe what each section of code does, and simplifies some calculations.

--------

Please give me a well-documented version of this code. Please follow best
practices and do not comment overly verbosely.


--------

Here is the code with clear comments:

```c
#include &lt;stdint.h&gt;

// Convert a date in the format (year, month, day, hour, minute, second) to a Unix timestamp.
time_t unix_timestamp(int year, int month, int day, int hour, int minute, int second) {
    // Check if the month is within valid range (1-12)
    if (month &lt; 1 || month &gt; 12) {
        return -1; // Return an error
    }

    time_t timestamp = 0;

    // Calculate the number of seconds in a year, taking into account leap years
    int total_seconds_in_year = 31536000;
    if ((year % 4 == 0 &amp;&amp; year % 100 != 0) || year % 400 == 0) {
        ++total_seconds_in_year; // Leap year adjustment
    }

    // Array of days in each month (excluding February)
    int month_days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    int day_of_year = 0;

    // Calculate the total number of days before the given month
    for (int i = 1; i &lt; month; i++) {
        day_of_year += month_days[i - 1];
    }
    day_of_year += day - 1; // Adjust for 0-based indexing

    // Calculate the Unix timestamp
    timestamp = (year - 1970) * total_seconds_in_year + day_of_year * 86400 + hour * 3600 + minute * 60 + second;

    return timestamp;
}
```

--------
</code></pre></div></div>

<p>Yes! nice. It also tries to improve it’s code.</p>

<h4 id="verdict">Verdict</h4>

<p><em>Llama 3</em> understands the given task but is lacking a bit of attention to
detail. However, it can improve upon itsolf on reflection.</p>

<h2 id="general-non-coding-tasks">General non-coding tasks</h2>

<p>Ignore the content of the given task, it is merely a repetition of the task
from the previous evaluation.</p>

<h3 id="method-1">Method</h3>

<p>I put the following bullet points in the context of each model, and asked the
modell to write the given points in full sentences for a short tech article.</p>

<ul>
  <li>mistral is okay with coding tasks</li>
  <li>mistral appears to be good at general language tasks</li>
  <li>codellama appears to be good at coding tasks, with good knowledge of libraries</li>
  <li>codellama appears to be lacking at general language tasks</li>
</ul>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Please write the given points in full sentences for a short tech article.


--------

Here is the rewritten text:

Mistral is capable and competent when it comes to tackling coding tasks.
Additionally, Mistral demonstrates proficiency in general language-related
tasks. Codellama, on the other hand, excels at coding tasks, showcasing its
strong understanding of various libraries. However, Codellama's abilities
dwindle when it comes to general language tasks, indicating a specific area
where it requires improvement.

--------

Please elaborate a bit more.

--------

Here is an elaboration on the text:

Mistral's strengths lie in its ability to tackle coding tasks with ease and
proficiency. Whether it's working on a complex algorithm or debugging a piece
of code, Mistral is well-equipped to handle a wide range of programming
challenges.

In addition to its coding prowess, Mistral also demonstrates a strong
foundation in general language skills. It can understand and communicate
effectively through written text, making it an asset in any team that values
clear communication.

Codellama, on the other hand, stands out for its exceptional abilities in
coding tasks. Its deep understanding of various programming libraries and
frameworks allows it to create efficient, effective, and scalable solutions.

However, Codellama's strengths are largely limited to coding-related areas.
When it comes to general language tasks, such as writing coherent sentences or
conversing naturally, Codellama struggles. This highlights an area where it
requires improvement, potentially through exposure to more diverse linguistic
contexts and training exercises.

Overall, Mistral's versatility in both coding and language skills makes it a
valuable asset, while Codellama's specialized expertise in coding is noteworthy
but may benefit from further development in other areas.

--------

</code></pre></div></div>

<h3 id="summary">Summary</h3>

<p>The expanded text, especially the elaborative version, is pretty much spot-on
with the given information.</p>

<h2 id="verdict-1">Verdict</h2>

<p>I switched to llama. It yielded very nice results, while (this is not covered
in this particular article, maybe I’ll write another one?) being very
performant and being able to handle a relatively large context window locally
in my environment, which is a large contributing factor to the decision.</p>]]></content><author><name>Dominikus Gierlach</name></author><category term="programming" /><category term="linux" /><category term="openai" /><category term="ai" /><category term="llama" /><category term="AMD" /><summary type="html"><![CDATA[Update: Local AI assistant - llama 3]]></summary></entry></feed>