<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Stanislav (Stas) Katkov</title>
    <description></description>
    <link>https://www.skatkov.com/</link>
    <atom:link href="https://www.skatkov.com/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Mon, 13 Jul 2026 11:29:57 +0000</pubDate>
    <lastBuildDate>Mon, 13 Jul 2026 11:29:57 +0000</lastBuildDate>
    <generator>Jekyll v4.4.1</generator>
    
      <item>
        <title>Mutation testing in the age of LLMs</title>
        <description>&lt;p&gt;The last conference I attended was &lt;a href=&quot;https://wrocloverb.com/&quot;&gt;Wroclaw RB&lt;/a&gt;. The conference had workshops in the first part, and I was really excited about the mutation testing workshop.&lt;/p&gt;

&lt;p&gt;Google, in &lt;a href=&quot;https://arxiv.org/pdf/2103.07189&quot;&gt;one of their research papers&lt;/a&gt;, claimed that mutation testing might have prevented 7 out of 10 production accidents if this testing practice was widely used.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Mutants are coupled with 70% of high-priority bugs, for which mutation testing would have reported a live, fault-coupled mutant on the bug-introducing change.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you are a small business owner like me, you will take that edge over competitors any time.&lt;/p&gt;

&lt;p&gt;My experience with mutation testing in other languages has been rather unstable. It felt like a great idea, and you could surface some improvements, but it was not something that I could run reliably on CI or even locally. It was even harder to sell the idea of 100% mutant coverage to the team in the pre-LLM era. A lot of teams struggled with automated tests in general and were very far away from such a tough metric.&lt;/p&gt;

&lt;p&gt;But the author of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mutant&lt;/code&gt; gem, &lt;a href=&quot;schirp-dso.com&quot;&gt;Markus&lt;/a&gt;, has been using mutation testing in production for years. And there have been a lot of rumors about Ruby teams that practice mutation testing with his gem as well.&lt;/p&gt;

&lt;p&gt;It felt like, in the age of LLMs, running full mutation coverage for projects should be easier. And Markus’s workshop proved it.&lt;/p&gt;

&lt;p&gt;At first we tried mutation testing manually, and after that did the same, but with LLMs. The LLM was capable of doing all the reasoning to effectively kill mutants, with very little input from my side.&lt;/p&gt;

&lt;p&gt;I came home energized, with a feeling that I had found a magic pill for my LLM-powered development. I relied on LLMs a lot, but never trusted their output. I meticulously went over each line they generated, and my main responsibility was to delete as much code as possible (and I removed a lot).&lt;/p&gt;

&lt;p&gt;Markus showed us toy examples. But how would it work on real Ruby projects? I had 3 open source projects to test this on before making a “go or no-go” decision for commercial projects.&lt;/p&gt;

&lt;h2 id=&quot;mutating-oss&quot;&gt;Mutating OSS&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://poshtui.com&quot;&gt;My side-project&lt;/a&gt; heavily depends on 3 open-source Ruby projects, so I picked them to experiment with the mutant gem.&lt;/p&gt;

&lt;h3 id=&quot;webhukhs-gem&quot;&gt;webhukhs gem&lt;/h3&gt;
&lt;p&gt;Here is the PR: &lt;a href=&quot;https://github.com/skatkov/webhukhs/pull/13&quot;&gt;https://github.com/skatkov/webhukhs/pull/13&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;webhukhs&lt;/code&gt; gem is a webhooks processing engine for Rails. This is a very stable gem that had already been tested on multiple projects.&lt;/p&gt;

&lt;p&gt;I mostly expected to see increased test coverage. But I was surprised to find a lot of great simplifications that I had completely missed.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.is_a?()&lt;/code&gt; can return true for an instance of a class or any ancestor. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;instance_of?()&lt;/code&gt; will only return true for an exact class match. So in my case, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;instance_of?&lt;/code&gt; is more precise.&lt;/li&gt;
  &lt;li&gt;Type casting was unneeded in a lot of places (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.to_s&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.to_json&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.force_encoding(Encoding::BINARY)&lt;/code&gt; was replaced with the simpler &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.b&lt;/code&gt;. I didn’t like this particular simplification. I prefer the explicit version. But I guess it’s very easy to mess up attributes here? So I went along with it, but it would be awesome if such cases could be configured.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash[:key]&lt;/code&gt; was replaced with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash.fetch(:key)&lt;/code&gt;. This is what I do myself all the time, but I completely missed one case.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rescue&lt;/code&gt; didn’t require assignment to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e&lt;/code&gt; variable. Great one, fewer variable allocations.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Rails.error.report(e, handled: true, severity: :error)&lt;/code&gt; was simplified to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Rails.error.report(e, severity: :error)&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;handled: true&lt;/code&gt; is a default value, so there was no need to provide this attribute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even though we didn’t surface any bugs, it still felt like a good enough improvement! So I decided to proceed further and see if mutant would bring more benefit in other projects.&lt;/p&gt;

&lt;h3 id=&quot;yard-markdown-gem&quot;&gt;yard-markdown gem&lt;/h3&gt;
&lt;p&gt;Here is the PR: &lt;a href=&quot;https://github.com/skatkov/yard-markdown/pull/33&quot;&gt;https://github.com/skatkov/yard-markdown/pull/33&lt;/a&gt;
(there are more PRs that followed after)&lt;/p&gt;

&lt;p&gt;Contrary to the previous polished gem, this was hacked together to “just work”. A proof of concept, in a way. There was no design or architecture; most of the code was in a single file that came with a template and glue code that made it work together.&lt;/p&gt;

&lt;p&gt;There could be so many data permutations that this plugin could break in various unexpected ways. And I already knew that there were at least two bugs in this gem, but even with decent tests it was scary to touch. Would mutant find them?&lt;/p&gt;

&lt;p&gt;In my LLM prompt, I did say: “It would be great if the code had better organization and this gem became easier to maintain long term.” I was not expecting much, but tried to be upfront about the desired results.&lt;/p&gt;

&lt;p&gt;And I lost track of all the small code simplifications that this brought. The LLM, with mutant assistance, stopped only twice to ask for my help. In both cases it was because it had found a logic bug. The LLM found all the bugs I knew about.&lt;/p&gt;

&lt;p&gt;Not only that, the code was neatly organized. It was more maintainable. I was not afraid to touch it anymore. I threw in yard-lint to improve code comments! This was a natural “oh, shit, what just happened?” moment.&lt;/p&gt;

&lt;p&gt;I could not believe my own eyes: mutant had surfaced bugs and the LLM had properly signaled that something funny was going on. This was a success! I couldn’t wait to onboard another similar project and couldn’t go to sleep without doing that.&lt;/p&gt;

&lt;h3 id=&quot;rdoc-markdown-gem&quot;&gt;rdoc-markdown gem&lt;/h3&gt;
&lt;p&gt;Here is a PR: &lt;a href=&quot;https://github.com/skatkov/rdoc-markdown/pull/58&quot;&gt;https://github.com/skatkov/rdoc-markdown/pull/58&lt;/a&gt;
(this had multiple follow-up PRs)&lt;/p&gt;

&lt;p&gt;While the code for this gem was rather simple, conversion to markdown is rather hacky: HTML is getting converted into markdown. This entire thing is riddled with bugs, but I currently see no other way to integrate with rdoc and achieve a more stable solution.&lt;/p&gt;

&lt;p&gt;On some gems this plugin just threw errors…&lt;/p&gt;

&lt;p&gt;Mutant caught around 8 bugs and I managed to resolve 6. There are no gems that just fail after 100% coverage anymore. Yet there are more bugs left… mutant made a lot of them visible (but not all, though).&lt;/p&gt;

&lt;p&gt;Unfortunately, there is not much that I can do in the rdoc-markdown gem itself to resolve all issues. RDoc itself requires more work.&lt;/p&gt;

&lt;p&gt;At this stage I was sold, and immediately wrote to Markus that I needed that license.&lt;/p&gt;

&lt;h2 id=&quot;lessons-learned&quot;&gt;Lessons learned&lt;/h2&gt;
&lt;p&gt;I have on-boarded 6 Ruby projects into mutation testing at the time of writing. It roughly took me two days to onboard each project into mutation testing, but with bigger rails project it took my couple of weeks of gradual conversion.&lt;/p&gt;

&lt;p&gt;Mutant is now a staple of my workflow when I deal with Ruby projects. It helps with LLM fatigue. I still look into code LLM’s produce, but more to get a feeling for what architecture we’re building here.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;In the pre-LLM era there was an argument that the costs of maintaining 100% mutant test coverage were too high. But with LLMs, this argument is completely void. LLMs can make the right call about where to simplify and where to add more tests. In some hairy spots, the LLM gives up, and this is where I pay attention, as this is most likely a bug.&lt;/li&gt;
  &lt;li&gt;At first, the LLM powered by mutant went overboard with adding tests. A lot of new tests felt repetitive and excessive. I still have not found a perfect way to deal with it to my satisfaction, but there are a couple of ways to improve this:
    &lt;ul&gt;
      &lt;li&gt;minitest/rails seemed too lax with assertions. The fact that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nil&lt;/code&gt; could be typecasted into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;false&lt;/code&gt; value allowed a lot of live mutations that needed killing with additional tests. As it turns out, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;minitest-strict&lt;/code&gt; solves exactly that problem and allowed me to eliminate a lot of new tests.&lt;/li&gt;
      &lt;li&gt;I can ask the LLM to attempt to delete some repetitive tests, while keeping a close eye on coverage.&lt;/li&gt;
      &lt;li&gt;Maybe in the future we can have mutation testing for non-production code?&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;I got a little anxious seeing so much existing production code changed by LLMs. They were removing guard clauses, some checks… “this surely will backfire somehow”, I thought to myself. But I haven’t seen any new bugs introduced after mutant-powered refactoring did its job. ZERO issues!&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Emboldened by the success in Ruby, I tried mutation testing in other languages. And I understood that writing a generic mutation testing framework is not that hard, but making it usable is really hard. It’s also computationally heavy. Incremental mutation testing of only code that is being changed is a killer feature of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mutant&lt;/code&gt; gem. So that $30/month fee for the mutant gem? Worth it! I don’t curse at every random error and don’t have the urge to fix the mutation testing framework itself.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;As a web developer, I have never really seen all my CPUs work to their full potential. The mutant gem changes that, to the point that the browser started fighting for resources. Parallelization of agents with mutant has to be done carefully; one has to be sure not to execute multiple mutants at the same time. Getting a beefy server and running mutant there seems like a good idea that I haven’t tried so far. Relying on GitHub Actions is not enough now either.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Sometimes I’m in prototyping mode, trying to figure out whether a library or approach is workable, and in that moment mutant slows everything down and derails the entire process. In my head, “mutant” is a final polish step, and to draw this distinction I’ve created a “prototype” mode in my opencode, with explicit instructions not to execute any mutation testing.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;I’ve been following Markus, the mutant gem author, and his work for some time. He is one of the original developer thinkers, not someone who just blindly follows “best practices”. And he goes out of his way to provide support and share his experience. It was an enriching experience for me personally just to have a couple of random chats with him. I’m standing here and questioning a lot of my personal choices as a result (and hopefully that makes me a slightly better developer now).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a conclusion, I wanted to share a single thought with the world. &lt;strong&gt;No matter what kind of language you’re dealing with, take a day off and try a mutation testing framework.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I have continued my experiments with advanced testing approaches: I’m experimenting with fuzz testing and property testing. Especially &lt;a href=&quot;https://github.com/antithesishq/bombadil&quot;&gt;bombadil&lt;/a&gt;’s experimental support for TUIs makes me excited. But let’s leave this part for future articles.&lt;/p&gt;
</description>
        <pubDate>Wed, 01 Jul 2026 09:38:00 +0000</pubDate>
        <link>https://www.skatkov.com/posts/2026-07-01-mutate-all-things</link>
        <guid isPermaLink="true">https://www.skatkov.com/posts/2026-07-01-mutate-all-things</guid>
        
        
      </item>
    
      <item>
        <title>Signing and notarizing a Mac app on Linux</title>
        <description>&lt;p&gt;I had to sign and notarize the &lt;a href=&quot;https://www.poshtui.com?utm=skatkov.com&quot;&gt;Posh TUI&lt;/a&gt; Mac app. The problem is that our household moved away from Apple. My last MacBook (Intel-based) was converted into an Omarchy machine for my son. But all the documentation assumed access to a MacBook and Keychain.app.&lt;/p&gt;

&lt;p&gt;So what should I do? Rent a Mac in the cloud? Borrow someone’s MacBook for a couple of hours? There is nothing inherently Apple-specific about signing apps.&lt;/p&gt;

&lt;p&gt;According to the &lt;a href=&quot;https://goreleaser.com/customization/sign/notarize/#cross-platform&quot;&gt;GoReleaser documentation&lt;/a&gt;, these things are required for signing and notarization.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MACOS_SIGN_P12&lt;/code&gt; - base64-encoded Developer ID Application &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.p12&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MACOS_SIGN_PASSWORD&lt;/code&gt; - password for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.p12&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MACOS_NOTARY_KEY&lt;/code&gt; - base64-encoded App Store Connect &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.p8&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MACOS_NOTARY_KEY_ID&lt;/code&gt; - App Store Connect key ID&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MACOS_NOTARY_ISSUER_ID&lt;/code&gt; - App Store Connect issuer UUID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s how to get all of these on Linux (Fedora, in my case). This guide assumes that you already have an &lt;a href=&quot;https://developer.apple.com/&quot;&gt;Apple Developer Account&lt;/a&gt; and have paid the $99/year fee.&lt;/p&gt;

&lt;h2 id=&quot;developer-id-application-aka-p12&quot;&gt;Developer ID Application (aka &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.p12&lt;/code&gt;)&lt;/h2&gt;

&lt;p&gt;The first stage is application signing, and it’s the tricky part. This requires some OpenSSL kung fu to get all the certificates and keys right.&lt;/p&gt;

&lt;h3 id=&quot;1-generate-a-private-key&quot;&gt;1. Generate a private key&lt;/h3&gt;

&lt;p&gt;Do this if you don’t have one already. Store it safely.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;openssl genrsa &lt;span class=&quot;nt&quot;&gt;-out&lt;/span&gt; mykey.key 2048
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;2-generate-a-certificate-signing-request-csr&quot;&gt;2. Generate a Certificate Signing Request (CSR)&lt;/h3&gt;

&lt;p&gt;After this operation, you will have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CertificateSigningRequest.certSigningRequest&lt;/code&gt; file.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;openssl req &lt;span class=&quot;nt&quot;&gt;-new&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-key&lt;/span&gt; mykey.key &lt;span class=&quot;nt&quot;&gt;-out&lt;/span&gt; CertificateSigningRequest.certSigningRequest &lt;span class=&quot;nt&quot;&gt;-subj&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/emailAddress=krooni@skatkov.com/CN=Stanislav Katkov/C=NL&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;3-upload-the-csr-to-apples-developer-portal&quot;&gt;3. Upload the CSR to Apple’s Developer Portal&lt;/h3&gt;

&lt;p&gt;Go to your Apple Developer account and create a new certificate.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/Pasted%20image%2020260623134629.png&quot; alt=&quot;Apple Developer certificate creation screen&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is where you should upload that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CertificateSigningRequest.certSigningRequest&lt;/code&gt; file. Apple website will give you a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.cer&lt;/code&gt; file in return: the actual certificate, signed by Apple.&lt;/p&gt;
&lt;h2 id=&quot;steps-to-create-the-p12&quot;&gt;Steps to Create the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.p12&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;Convert Apple’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.cer&lt;/code&gt; file (DER format) to PEM format:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;openssl x509 &lt;span class=&quot;nt&quot;&gt;-inform&lt;/span&gt; DER &lt;span class=&quot;nt&quot;&gt;-in&lt;/span&gt; developerID_application.cer &lt;span class=&quot;nt&quot;&gt;-out&lt;/span&gt; developer_id.pem
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Combine the certificate and your private key into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.p12&lt;/code&gt; file:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;openssl pkcs12 &lt;span class=&quot;nt&quot;&gt;-export&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-out&lt;/span&gt; Certificates.p12 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-inkey&lt;/span&gt; mykey.key &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-in&lt;/span&gt; developer_id.pem
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;During export, OpenSSL will ask you to set a password for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.p12&lt;/code&gt; certificate. You can skip it, but I suggest setting one. Store it in your password manager.&lt;/p&gt;

&lt;p&gt;Now we have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Certificates.p12&lt;/code&gt; certificate on our hands. And we can proceed to next step.&lt;/p&gt;

&lt;h2 id=&quot;getting-the-p8-key&quot;&gt;Getting the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.p8&lt;/code&gt; key&lt;/h2&gt;

&lt;p&gt;Gathering everything required for notarization is the easiest part. It requires downloading one file and copying a couple of values.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Go to the &lt;a href=&quot;https://appstoreconnect.apple.com&quot;&gt;App Store Connect portal&lt;/a&gt; and log in&lt;/li&gt;
  &lt;li&gt;After logging in, open the &lt;strong&gt;Users and Access&lt;/strong&gt; section, then click the &lt;strong&gt;Integrations&lt;/strong&gt; tab&lt;/li&gt;
  &lt;li&gt;On the Integrations page, you’ll find the &lt;strong&gt;Issuer ID&lt;/strong&gt; and the option to create a new API key. Click the &lt;strong&gt;+&lt;/strong&gt; button to create one&lt;/li&gt;
  &lt;li&gt;The new key needs to have at least the &lt;strong&gt;App Manager&lt;/strong&gt; access level&lt;/li&gt;
  &lt;li&gt;Click &lt;strong&gt;Download API Key&lt;/strong&gt; to download the key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It will be saved as a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.p8&lt;/code&gt; file named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AuthKey_[key_id]&lt;/code&gt;. Besides this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.p8&lt;/code&gt; key, we need &lt;strong&gt;2 more things&lt;/strong&gt; from this page:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Item&lt;/th&gt;
      &lt;th&gt;Where to find it&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Key ID&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Shown in the key list&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Issuer ID&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Shown above the “Active” table&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;assembling-all-of-this-together&quot;&gt;Assembling all of this together&lt;/h2&gt;

&lt;p&gt;Now for the signing process.&lt;/p&gt;

&lt;p&gt;In my case, I had to make 3 tools work together.&lt;/p&gt;

&lt;h3 id=&quot;goreleaser&quot;&gt;GoReleaser&lt;/h3&gt;

&lt;p&gt;Your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.goreleaser.yaml&lt;/code&gt; should look like this:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
&lt;span class=&quot;na&quot;&gt;notarize&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;macos&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;isEnvSet&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;MACOS_SIGN_P12&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;}}&apos;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;sign&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;certificate&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{.Env.MACOS_SIGN_P12}}&quot;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# base64 encoded .p12&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{.Env.MACOS_SIGN_PASSWORD}}&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;notarize&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;issuer_id&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{.Env.MACOS_NOTARY_ISSUER_ID}}&quot;&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;key_id&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{.Env.MACOS_NOTARY_KEY_ID}}&quot;&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{.Env.MACOS_NOTARY_KEY}}&quot;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# base64 encoded .p8&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;github-actions&quot;&gt;GitHub Actions&lt;/h3&gt;

&lt;p&gt;The same ENV variables need to be added as secrets to your GitHub repository. With the following small change, you can expose them as ENV variables for GoReleaser.&lt;/p&gt;

&lt;div class=&quot;language-yml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
&lt;span class=&quot;na&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;goreleaser&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;fetch-depth&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;goreleaser/goreleaser-action@v7&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;distribution&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;goreleaser&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;~&amp;gt;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;v2&quot;&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;release --clean&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;MACOS_SIGN_P12&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;${{ secrets.MACOS_SIGN_P12 }}&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;MACOS_SIGN_PASSWORD&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;${{ secrets.MACOS_SIGN_PASSWORD }}&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;MACOS_NOTARY_KEY&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;${{ secrets.MACOS_NOTARY_KEY }}&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;MACOS_NOTARY_KEY_ID&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;${{ secrets.MACOS_NOTARY_KEY_ID }}&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;MACOS_NOTARY_ISSUER_ID&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;${{ secrets.MACOS_NOTARY_ISSUER_ID }}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;mise&quot;&gt;Mise&lt;/h3&gt;

&lt;p&gt;For a local release, put the same values in an uncommitted &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mise.local.toml&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-toml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;redactions&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;MACOS_*&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;MACOS_SIGN_P12&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/absolute/path/to/Certificates.p12&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;MACOS_SIGN_PASSWORD&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;p12-password&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;MACOS_NOTARY_KEY&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/absolute/path/to/ApiKey_AAABBBCCC.p8&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;MACOS_NOTARY_KEY_ID&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;AAABBBCCC&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;MACOS_NOTARY_ISSUER_ID&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;issuer-uuid&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;testing&quot;&gt;Testing&lt;/h3&gt;

&lt;p&gt;Testing the entire process without issuing a new release is easy with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--snapshot&lt;/code&gt; attribute for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;goreleaser release&lt;/code&gt; command. It skips many steps, like publishing and announcements.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;goreleaser release --clean --snapshot&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The first notarization process took a lot of time (more than 12 hours), so I installed &lt;a href=&quot;https://github.com/anchore/quill&quot;&gt;quill&lt;/a&gt; locally to monitor the submission status.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mise exec -- sh -c &apos;quill submission status &amp;lt;submission-id&amp;gt; --notary-issuer &quot;$MACOS_NOTARY_ISSUER_ID&quot; --notary-key-id &quot;$MACOS_NOTARY_KEY_ID&quot; --notary-key &quot;$MACOS_NOTARY_KEY&quot;&apos;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Once the status changes from &lt;strong&gt;Pending&lt;/strong&gt; to &lt;strong&gt;Accepted&lt;/strong&gt;, you know that everything is good to go.&lt;/p&gt;

&lt;h2 id=&quot;closing-notes&quot;&gt;Closing notes&lt;/h2&gt;
&lt;p&gt;Everyone tells me that there are so many things you can’t do on Linux. But I’ve successfully dodged the need to buy another Windows/macOS machine for the last 8 years. This is another “Nope, Linux is enough” post.&lt;/p&gt;

&lt;p&gt;Feel free to try the latest release of &lt;a href=&quot;https://poshtui.com?utm=skatkov.com&quot;&gt;POSH TUI&lt;/a&gt; as proof that it’s possible to sign and notarize a macOS app without touching an Apple laptop.&lt;/p&gt;
</description>
        <pubDate>Thu, 25 Jun 2026 17:04:00 +0000</pubDate>
        <link>https://www.skatkov.com/posts/2026-06-25-signing-and-notarizing-mac-app-on-linux</link>
        <guid isPermaLink="true">https://www.skatkov.com/posts/2026-06-25-signing-and-notarizing-mac-app-on-linux</guid>
        
        
      </item>
    
      <item>
        <title>Let it rip</title>
        <description>&lt;p&gt;Huge LLM user here. I didn’t become one a couple of months ago — I’ve been experimenting with LLMs for a while. But it’s hard to find the courage to write about this experience. Every couple of weeks the LLM-powered workflow shifts — people keep coming up with great ways to use it.&lt;/p&gt;

&lt;p&gt;One thing that never changed for me was that an LLM was only a handy assistant, nothing more than that. But this time, I gave the LLM a boring maintenance loop and it actually shipped a lot meaningful improvements without any guidance at all.&lt;/p&gt;

&lt;h2 id=&quot;yardrdoc-markdown-gems&quot;&gt;(yard|rdoc)-markdown gems&lt;/h2&gt;

&lt;p&gt;This year, all my time has been going towards one project — &lt;a href=&quot;https://poshtui.com/&quot;&gt;Posh TUI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A Markdown renderer is a big part of an application like this. I’m using the &lt;a href=&quot;https://github.com/charmbracelet/glamour&quot;&gt;glamour&lt;/a&gt; library from the &lt;a href=&quot;https://charm.land/&quot;&gt;Charm team&lt;/a&gt;. Thank god I don’t have to figure out how to render Markdown in a terminal — it’s a crazy complicated problem.&lt;/p&gt;

&lt;p&gt;But I &lt;em&gt;do&lt;/em&gt; have to figure out how to convert Ruby source code into Markdown documentation. The main issue is that it has to be done reliably. Quick workarounds don’t cut it here. So that led me to write a couple of libraries — &lt;a href=&quot;https://github.com/skatkov/yard-markdown&quot;&gt;yard-markdown&lt;/a&gt; and &lt;a href=&quot;https://github.com/skatkov/rdoc-markdown&quot;&gt;rdoc-markdown&lt;/a&gt; — to make that possible.&lt;/p&gt;

&lt;p&gt;It seems like I picked the right approach. But to make it work, I went into this loop to generate → compare → tweak → test → repeat (500x) and got tired of it. The libraries got stuck in a “it doesn’t break; looks legit for now” state.&lt;/p&gt;

&lt;p&gt;But this is part of the core experience for users — these libraries matter. I just didn’t have enough drive to solve it once and for all.&lt;/p&gt;

&lt;h2 id=&quot;let-it-rip&quot;&gt;Let it rip&lt;/h2&gt;

&lt;p&gt;One weekend I asked an LLM to work on the &lt;strong&gt;rdoc-markdown&lt;/strong&gt; gem. But this time my prompt started with: &lt;em&gt;“Don’t ask any questions. Make your best guess and proceed without assistance.”&lt;/em&gt; The plan was to run this session in “yolo” mode.&lt;/p&gt;

&lt;p&gt;I laid out all the context:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;How to generate output&lt;/li&gt;
  &lt;li&gt;That the output should be as close to HTML as possible&lt;/li&gt;
  &lt;li&gt;That there might be bugs&lt;/li&gt;
  &lt;li&gt;Which gems it might use as good test subjects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After that was out of the way, I suggested it might be a good idea to do this in a loop.&lt;/p&gt;

&lt;p&gt;With that, I made sure my laptop wouldn’t automatically go to sleep. And I let it rip.&lt;/p&gt;

&lt;h2 id=&quot;pupil-outdid-its-master&quot;&gt;Pupil outdid its master&lt;/h2&gt;

&lt;p&gt;It took it a couple of hours to do all this boring, repetitive work. I checked on it a couple of times before going to sleep, but I never assisted. There wasn’t a specific goal — it used its best judgment and just kept going.&lt;/p&gt;

&lt;p&gt;In the morning, the results were in.&lt;/p&gt;

&lt;p&gt;It produced decent Markdown documentation for Rails, then for Minitest, and then for Ruby itself. I couldn’t believe my eyes — it squashed all the bugs I knew about. More than that, it looked great.&lt;/p&gt;

&lt;p&gt;Of course, I couldn’t let this slide — I jumped into the code and found some bugs. Now you see who’s the boss around here?&lt;/p&gt;

&lt;p&gt;Jokes aside: I ran a similar procedure on the &lt;strong&gt;yard-markdown&lt;/strong&gt; gem the next evening, and the results were similarly good.&lt;/p&gt;

&lt;p&gt;Now I’m sitting here relieved that I probably saved myself at least a month of part-time work. But at the same time I’m wondering: would I have found enough motivation to do the same tedious job at the same level of execution? I’m not sure I would.&lt;/p&gt;

&lt;p&gt;I didn’t even bother guiding anything in this process. And for a brief moment, the compute became its own master and executed the work the way it saw fit. Bravo, young padawan.&lt;/p&gt;
</description>
        <pubDate>Sat, 14 Feb 2026 19:37:00 +0000</pubDate>
        <link>https://www.skatkov.com/posts/2026-02-14-let-it-rip</link>
        <guid isPermaLink="true">https://www.skatkov.com/posts/2026-02-14-let-it-rip</guid>
        
        <category>LLM</category>
        
        
      </item>
    
      <item>
        <title>Warning about deprecations in Ruby</title>
        <description>&lt;p&gt;I’ve been looking into ways how libraries and rails engines communicate deprecation warnings and found couple of surprising things.&lt;/p&gt;

&lt;p&gt;Everyone working with Rails probably heard or seen &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActiveSupport::Deprecation&lt;/code&gt; in action. But what about gems that don’t depend on any rails dependencies? &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Kernel.warn&lt;/code&gt; might surprise you here.&lt;/p&gt;
&lt;h2 id=&quot;activesupportdeprecation&quot;&gt;ActiveSupport::Deprecation&lt;/h2&gt;
&lt;p&gt;Rails offers a no thrills way to emit deprecation warnings, that everyone already knows about.&lt;/p&gt;

&lt;p&gt;Before Rails 7.1 it would look like this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;ActiveSupport&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Deprecation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Method will be removed in next version&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# or&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;deprecator&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveSupport&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Deprecation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;MyGem&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;deprecator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Method will be removed in next version&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But starting with Rails 7.1 explicit deprecators registry became a thing. So things became even simpler.&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deprecator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Method will be removed in next version&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# or for specific gems&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deprecators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:my_gem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Method will be removed in next version&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;How these deprecations are communicated can be configured with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config.active_support.deprecation&lt;/code&gt; setting .&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Value&lt;/th&gt;
      &lt;th&gt;Result&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;:raise&lt;/td&gt;
      &lt;td&gt;Raise ActiveSupport::DeprecationException&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;:stderr&lt;/td&gt;
      &lt;td&gt;Logs all deprecation warnings to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$stderr&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;:log&lt;/td&gt;
      &lt;td&gt;Logs all deprecation warnings to Rails.logger&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;:notify&lt;/td&gt;
      &lt;td&gt;Use ActiveSupport::Notifications to notify &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deprecation.rails&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;:report&lt;/td&gt;
      &lt;td&gt;Use ActiveSupport::ErrorReporter to report deprecations&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;:silence&lt;/td&gt;
      &lt;td&gt;Do nothing&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;There is also a way to include stack trace with every warning if you enable debug mode.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deprecators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;debug&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Everything seems pretty clear and simple here. But what if you don’t depend on active support?&lt;/p&gt;
&lt;h2 id=&quot;kernelwarn&quot;&gt;Kernel.warn&lt;/h2&gt;
&lt;p&gt;Turns out, that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Kernel.warn&lt;/code&gt; is not just a generic method to emit warnings. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Kernel.warn&lt;/code&gt; doubles down as a way to communicate deprecation’s, experimental features or performance improvements.&lt;/p&gt;

&lt;p&gt;There are two downsides though:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;It’s not very well documented, so hardly anyone knows.&lt;/li&gt;
  &lt;li&gt;Deprecation warnings are silenced by default&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But here is way to emit a deprecation warning:&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Method is deprecated&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;category: :deprecated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Problem is that nobody is likely to be listening. Since default values for each category are as follows:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Warning[:deprecated] # =&amp;gt; false&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Warning[:experimental] # =&amp;gt; true&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Warning[:performance] # =&amp;gt; false&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Warning and performance categories are silenced by default. But off course this could be changed by setting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Warning[:deprecated] = true&lt;/code&gt; or using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-W&lt;/code&gt; argument in RUBYOPTS.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;export RUBYOPT=&quot;-W1:deprecated&quot;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Or you can ask ruby to be more verbose and this will enable all warning categories:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;export RUBYOPTS=&quot;-W2&quot;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I’m hunting for deprecation’s, but performance improvement warnings can give a lot of nice hints and are worth exploring too.&lt;/p&gt;
</description>
        <pubDate>Fri, 17 Oct 2025 22:44:00 +0000</pubDate>
        <link>https://www.skatkov.com/posts/2025-10-18-warning-about-deprecations-in-ruby</link>
        <guid isPermaLink="true">https://www.skatkov.com/posts/2025-10-18-warning-about-deprecations-in-ruby</guid>
        
        
      </item>
    
      <item>
        <title>Ordering Estonian ID on Linux!</title>
        <description>&lt;p&gt;I’m an Estonian passport holder, not living in Estonia for a long time. I don’t get to interact with Estonian government services much, even though I kind of miss the simplicity of that interaction. I have never in my life had to run around different Estonian government buildings to get things resolved. Things are always resolved digitally from the comfort of my home.&lt;/p&gt;

&lt;p&gt;This is something you don’t appreciate until you travel around and see how far ahead Estonia is in its digitization, even compared to richer countries like the Netherlands. And don’t get me started on countries like Thailand, where there’s an entire service industry that helps you deal with the annoyances of government-required paperwork—it’s a lucrative business for a reason.&lt;/p&gt;

&lt;p&gt;But let’s get back to Estonia and the main reason I’m writing this blog post. I have to change my passport and ID every 5 years—it’s possible to do digitally, but only if one manages to securely identify themselves.&lt;/p&gt;

&lt;p&gt;In my case, the only option is the ID card. This requires a special device to read the smart card. As a long-time Linux user, I often scratch my head figuring out how to make such devices work, since Linux support is rarely advertised and the documentation has zero information related to Linux.&lt;/p&gt;

&lt;p&gt;But I wanted to avoid wasting two days traveling to The Hague (to request a new document and to actually receive it) and request a new ID card online.&lt;/p&gt;

&lt;p&gt;So, how do you read an Estonian ID card on a Manjaro-powered Linux machine? The &lt;a href=&quot;https://www.id.ee&quot;&gt;id.ee&lt;/a&gt; website offers a &lt;a href=&quot;https://www.id.ee/en/article/install-id-software/&quot;&gt;basic manual on how to proceed&lt;/a&gt;. They claim to support Linux, but most of their guides are focused on Debian-based distros (primarily Ubuntu), and I found that some steps are missing.&lt;/p&gt;
&lt;h2 id=&quot;getting-a-smart-card-reader-to-work&quot;&gt;Getting a smart card reader to work&lt;/h2&gt;
&lt;p&gt;I don’t know much about smart card readers and the differences between them. Most readers I’ve used worked just fine on all Linux machines, even if they don’t market their Linux support.&lt;/p&gt;

&lt;p&gt;After a quick search on Amazon, I ordered a &lt;a href=&quot;https://www.lindy.eu/USB-2-0-Smart-Card-Reader.htm?websale8=ld0101.ld021102&amp;amp;pi=42768&quot;&gt;Lindy USB 2.0 Smart Card Reader&lt;/a&gt;. Linux support is not advertised, but I was pretty sure it would work (or else I’d just return it).&lt;/p&gt;

&lt;p&gt;Once I plugged it in, the lights came on, which was a good sign. DigiDoc4 client is used to read data from ID cards. This package is available through Flatpak on most Linux distributions.&lt;/p&gt;

&lt;p&gt;But once I opened the DigiDoc client, I was met with a surprising error.&lt;/p&gt;
&lt;h3 id=&quot;the-pscs-service-required-for-using-the-id-card-is-not-working&quot;&gt;“The PSCS service, required for using the ID-card, is not working”&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.id.ee/en/article/issues-with-the-id-card-reader/&quot;&gt;Issues with the ID-card reader&lt;/a&gt; page gave me a useful tip, that I just need to enable pcscd server via systemctl.&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;systemctl enable pcscd.socket  
sudo systemctl start pcscd.socket
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now the error was gone, but DigiDoc couldn’t identify my card reader and showed another interesting error.&lt;/p&gt;
&lt;h3 id=&quot;no-card-readers-found&quot;&gt;“No card readers found”&lt;/h3&gt;
&lt;p&gt;The smart card reader was definitely there—it was blinking with red/blue lights, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lsusb&lt;/code&gt; command actually showed the device in the list.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 1050:0407 Yubico.com Yubikey 4/5 OTP+U2F+CCID
Bus 001 Device 003: ID 0489:e0d8 Foxconn / Hon Hai Bluetooth 5.2 Adapter [MediaTek MT7922]
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 003 Device 002: ID 13d3:5293 IMC Networks Integrated Camera
Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 005 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 006 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 007 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 007 Device 008: ID 0bda:0165 Realtek Semiconductor Corp. Smart Card Reader Interface
Bus 008 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It turned out I was missing some packages that would allow the DigiDoc client to find and use the smart card reader.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo pacman -S ccid opensc libp11 engine_pkcs11
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;At this point, restart the DigiDoc4 client, and personal data should show up (I’m not adding any screenshots here, as it’s private). This ensures that the card reader is operational and the ID card is not damaged and can be used.&lt;/p&gt;

&lt;p&gt;But the goal here is to sign/verify things through the browser so I can request a new document online.&lt;/p&gt;

&lt;h2 id=&quot;integrating-the-card-reader-with-firefox&quot;&gt;Integrating the card reader with Firefox&lt;/h2&gt;
&lt;p&gt;I’m using Firefox for this example, and according to the manual from id.ee, it requires just two extensions to work:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://addons.mozilla.org/en-US/firefox/addon/web-eid-webextension/&quot;&gt;Web eID&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://addons.mozilla.org/en-US/firefox/addon/pkcs11-module-loader/&quot;&gt;PKCS11 module loader&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But nothing worked. The police website told me that my software was outdated and I needed to update.&lt;/p&gt;

&lt;p&gt;The old web authorization method for smart cards had a lot of stability issues in the past. So it turns out that now Estonian government services rely on &lt;a href=&quot;https://web-eid.eu/&quot;&gt;web-eid&lt;/a&gt;, and it requires a native client called web-eid on the machine for authorization to work.&lt;/p&gt;

&lt;p&gt;First, import certificates to ensure we’re using the correct source:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg --keyserver keyserver.ubuntu.com --recv-keys 1282B0F8809D0DC632C85A3F86B611CE24492160
gpg --keyserver keyserver.ubuntu.com --recv-keys D1EBC666EFCBFBD3CFC2EBAA90C0B5E75C3B195D
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then, install the native client and the WebExtension wrapper that communicates with the browser:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo pamac -S web-eid-native  web-eid-firefox&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://web-eid.eu&quot;&gt;web-eid.eu&lt;/a&gt; page has a way to test that both signing and authorization work as expected—and it did.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;As often happens with documentation, id.ee has slightly outdated documentation and forgot to mention a couple of crucial steps. It’s not surprising because they have to maintain documentation in three different languages—easy to forget things.&lt;/p&gt;

&lt;p&gt;This small manual isn’t that interesting for most people around the globe, just for the million or so Estonians who have to use eGovernment services. But I’m sure I’ll be thankful for publishing this after 5 years (by the time I need to change some documents again).&lt;/p&gt;
</description>
        <pubDate>Fri, 26 Sep 2025 16:09:00 +0000</pubDate>
        <link>https://www.skatkov.com/posts/2025-09-26-using-estonian-id-s-on-linux</link>
        <guid isPermaLink="true">https://www.skatkov.com/posts/2025-09-26-using-estonian-id-s-on-linux</guid>
        
        
      </item>
    
      <item>
        <title>From zsh to fish shell</title>
        <description>&lt;p&gt;Always wanted to move from zsh to fish, but never found a good excuse to spend time on that. Recently a new motherboard for the laptop arrived, this event created a need to wipe entire system clean so we can start over.&lt;/p&gt;

&lt;p&gt;That’s a good excuse not to install zsh and use &lt;a href=&quot;https://fishshell.com/&quot;&gt;fish&lt;/a&gt;!&lt;/p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;/h2&gt;

&lt;p&gt;Often people complain that zsh takes a while to boot for every new terminal. This is indeed annoying, but not zsh’s fault. The main culprit here is widely used &lt;a href=&quot;https://ohmyz.sh/&quot;&gt;oh-my-zsh&lt;/a&gt; , but &lt;a href=&quot;https://github.com/sorin-ionescu/prezto&quot;&gt;prezto&lt;/a&gt; solves this issue for good.  And I’ve been using it for years!&lt;/p&gt;

&lt;p&gt;My main complaints have been about other, smaller issues:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;While prezto is great, the installation and update process is not enjoyable. I’ve gone through the setup at least 10 times in the last 3 years, and I still mess something up at first try.&lt;/li&gt;
  &lt;li&gt;ZSH’s strict attributes parser has forced me to ‘quote’ and escape some things better. It’s a well-known quirk at this point, but it still gets on my nerves every time. Most of these “zsh bites me when I forget to quote” problems either do not exist in fish or are greatly reduced, because fish’s grammar is simpler: no history expansion, no glob qualifiers, no K-style extended globs, no parameter flags, no arithmetic in $((…)), etc.&lt;/li&gt;
  &lt;li&gt;Configuration for bash and zsh is hectic and scattered across multiple files (~/.bashrc, ~/.bash_profile, ~/.zshrc, etc). There are multitudes of ways to organize and maintain these better, non of those approaches left me satisfied. All fish config files could be found in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.config/fish/&lt;/code&gt; folder, that’s a great default!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;nothing-fish-y-here&quot;&gt;Nothing fish-y here&lt;/h2&gt;
&lt;p&gt;Fish has been a blast so far!&lt;/p&gt;

&lt;p&gt;It comes with syntax highlighting, suggestions, and completions based on man pages—out of the box, no plugins needed. But we can cherry-pick the plugins if anything extra was needed.&lt;/p&gt;

&lt;p&gt;So I decided to make a default shell right away. On Fedora this takes exactly three commands to accomplish.&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;dnf &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;fish
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;dnf &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;util-linux-user &lt;span class=&quot;c&quot;&gt;# fedora needs this package to install default shells.&lt;/span&gt;
chsh &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; /usr/bin/fish
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But things only get better from here with just couple of more plugins. And fish comes with a awesome package manager called - &lt;strong&gt;Fisher&lt;/strong&gt;: A proper package manager, can you imagine that?&lt;/p&gt;

&lt;p&gt;It will get handy, so let’s install Fisher without any further ado.&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-sL&lt;/span&gt; https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish | &lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; fisher &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;jorgebucaran/fisher
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;powerline-prompt&quot;&gt;Powerline prompt&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/fish-bobthefish.png&quot; alt=&quot;Bobthefish theme for Fish&quot; /&gt;
One of the main selling features of zsh is a git-aware &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;powerline&lt;/code&gt; prompt. Fish comes with a pretty decent port of this theme.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;fisher oh-my-fish/theme-bobthefish
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Every &lt;a href=&quot;https://www.nordtheme.com/&quot;&gt;Nord theme&lt;/a&gt; fanatic will need to insert one line into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.config/fish/config.fish&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;set -g theme_color_scheme nord
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;directory-jumper&quot;&gt;Directory jumper&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cd&lt;/code&gt; is the most popular command used in a terminal by far, so since forever I relied on &lt;a href=&quot;https://github.com/rupa/z&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rupa/z&lt;/code&gt;&lt;/a&gt; to jump through directories. There are modern alternatives, but original has never failed me.&lt;/p&gt;

&lt;p&gt;But &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rupa/z&lt;/code&gt; was a bit of a pain to get working during setup. So fish port of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Z&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fisher&lt;/code&gt; package manager made this a breeze!&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fisher install jethrokuan/z&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;fzfhistory-search&quot;&gt;fzf/history search&lt;/h3&gt;
&lt;p&gt;fish comes with built-in history search functionality available through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+r&lt;/code&gt;. This would previously require a plugin in zsh, but this is built in. Adding fzf to this turns this already powerful feature into an overpowered one.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;fisher &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;PatrickF1/fzf.fish
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/fish-search.png&quot; alt=&quot;Fish search feature powered by fzf&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;All in all - Fish is a great improvement over ZSH. It might not be worth it for people that don’t touch terminal as much as I do. But for me, it’s a small productivity boost that keeps on giving every day.&lt;/p&gt;
</description>
        <pubDate>Wed, 30 Jul 2025 12:56:00 +0000</pubDate>
        <link>https://www.skatkov.com/posts/2025-07-30-moving-from-zsh-to-fish-shell</link>
        <guid isPermaLink="true">https://www.skatkov.com/posts/2025-07-30-moving-from-zsh-to-fish-shell</guid>
        
        
      </item>
    
      <item>
        <title>Software License management with Polar.sh</title>
        <description>&lt;p&gt;This article would have been incredibly helpful to me just a month ago—so here it is, primarily for other developers who, like me, don’t usually write software that’s meant to run in non-server environments.&lt;/p&gt;

&lt;p&gt;I just introduced licenses to one of my apps—&lt;a href=&quot;https://devtui.com&quot;&gt;DevTUI&lt;/a&gt;. There’s another app that suffers from my code, &lt;a href=&quot;https://poshtui.com&quot;&gt;PoshTUI&lt;/a&gt;, and it might require something similar soon. I suspect all of this will come in handy again in the future.&lt;/p&gt;

&lt;h2 id=&quot;brief-idea&quot;&gt;Brief idea&lt;/h2&gt;
&lt;p&gt;Not all licensing features have been rolled out yet, so let me briefly explain the overall vision.&lt;/p&gt;

&lt;p&gt;DevTUI will be available to everyone as a free product, with no limitations on functionality. However, it will include a nagging popup prompting users to support further development. Users can remove this nag screen by paying a (reasonable) one-time fee.&lt;/p&gt;

&lt;p&gt;The inspiration for this payment model comes from Postico and Sublime Text — two products I personally support and admire.&lt;/p&gt;

&lt;p&gt;Since DevTUI doesn’t incur any recurring costs, I’m okay with people using it for free. That said, I still want to encourage users to contribute financially to the project rather than relying on the usual “begging for open source funding” approach.&lt;/p&gt;
&lt;h2 id=&quot;requirements-for-licensing-solution&quot;&gt;Requirements for licensing solution&lt;/h2&gt;
&lt;p&gt;Starting out, I only knew what needed to be built: a licensing solution for a one-time payment product. A solution should support the following requirements:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Ability to issue free licenses to early supporters of &lt;a href=&quot;https://poshtui.com&quot;&gt;PoshTUI&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Ability to limit each license to three machines (or less)&lt;/li&gt;
  &lt;li&gt;Allow customers to manage their licenses without a need to contact support(e.g. deactivate licenses or view receipts)&lt;/li&gt;
  &lt;li&gt;Should work offline; license validation should not run every time the app starts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;licensing-server&quot;&gt;Licensing Server&lt;/h2&gt;
&lt;p&gt;I’m using &lt;a href=&quot;https://polar.sh&quot;&gt;Polar.sh&lt;/a&gt; as my payment and licensing provider. I’m skipping the part about how I ended up with them—no interest in badmouthing any competitors.&lt;/p&gt;

&lt;p&gt;Rolling your own solution on top of Stripe is also possible. But that wasn’t an option worth pursuing for me. A 4% cut of future revenue seemed like a fair trade-off to avoid dealing with payment handling and license infrastructure.&lt;/p&gt;

&lt;p&gt;Polar team has been incredibly helpful. When I first contacted them, they didn’t even have a Go SDK, but they published one within two days. They also responded with useful tips and shipped fixes—again, all within 48 hours of my initial email.&lt;/p&gt;

&lt;p&gt;Polar.sh provides me with all the server side niceties.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Offer a way to define a product that can be paid one time&lt;/li&gt;
  &lt;li&gt;There is also  Discounts for those early supporters and friends&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://polar.sh/krooni/portal/request&quot;&gt;Customer portal&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://buy.polar.sh/polar_cl_JPBTnQKWsNBC8lA7tpR1uZYne5hMuW40xqTRI3P9WcH&quot;&gt;Checkout links&lt;/a&gt; for a product, even one with a 100% discount&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the client-side implementation is up to me.&lt;/p&gt;

&lt;h2 id=&quot;license-activation&quot;&gt;License Activation&lt;/h2&gt;

&lt;p&gt;The app can do three license related actions - &lt;strong&gt;active&lt;/strong&gt;, &lt;strong&gt;validate&lt;/strong&gt; and &lt;strong&gt;deactivate&lt;/strong&gt;. All these action are currently implemented in a CLI interface.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Commands &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;activating, validating, and deactivating licenses  
  
Usage:  
 devtui license &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;  
  
Available Commands:  
 activate    Activate a license  
 deactivate  Deactivate a license  
 validate    Validate a license  
  
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But I expect that most users will probably invoke only one command - &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;activate&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;activate&quot;&gt;Activate&lt;/h3&gt;
&lt;p&gt;This is how user can activate a license&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;devtui license activate --key=DEVTUI-2CA57A34-E191-4290-A394-XXXXXX&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I’m intentionally not using any ENV variables — this would require an additional library, and I don’t want users polluting their &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.zshrc&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.bashrc&lt;/code&gt; with extra variables.&lt;/p&gt;

&lt;p&gt;After activation, the key and related info are stored in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;license.json&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;During activation with an API call to Polar.sh i’m passing a “conditions” field. In our case, we use the MAC address to associate the license with a specific machine. The licensing server enforces a maximum of 3 machines per license.&lt;/p&gt;

&lt;h3 id=&quot;validate&quot;&gt;Validate&lt;/h3&gt;
&lt;p&gt;This action uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;license.json&lt;/code&gt; file created during activation. While the CLI command simply checks that the license is active, this validation logic is also embedded directly in the app.&lt;/p&gt;

&lt;p&gt;Here’s a rough outline of the validation logic:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;It’s checking that hash sum matches expectations&lt;/li&gt;
  &lt;li&gt;If it doesn’t match -&amp;gt; We re-validate license with server&lt;/li&gt;
  &lt;li&gt;If it does match -&amp;gt; We check if it’s time to check license with a server.&lt;/li&gt;
  &lt;li&gt;During a license check with server, if MAC address is not similar to one we used during activation - validation will fail. This would prevent people from just moving &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;license.json&lt;/code&gt; file to another machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Software attempts to validate license with a server every week, this time frame is completely arbitrary.&lt;/p&gt;
&lt;h3 id=&quot;deactivate&quot;&gt;Deactivate&lt;/h3&gt;
&lt;p&gt;This also uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;license.json&lt;/code&gt; file created during activation.&lt;/p&gt;

&lt;p&gt;While it’s already possible to deactivate a license via the customer portal, having it available in the CLI felt important. Two use cases come to mind:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Running the app in CI environments where machines constantly get recycled&lt;/li&gt;
  &lt;li&gt;Switching from one machine to another&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;identifying-a-machine&quot;&gt;Identifying a Machine&lt;/h2&gt;
&lt;p&gt;To my knowledge, most software uniquely identify a machine by its MAC address. It’s important to try and find the physical, manufacturer-assigned MAC address, use it during license activation, and later rely on it during validation.&lt;/p&gt;

&lt;p&gt;However, not all MAC addresses are created equal. There are also Locally Administered Addresses (LAAs), these are not the same because they could be:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Manually assigned by a network administrator&lt;/li&gt;
  &lt;li&gt;Often used in virtual machines&lt;/li&gt;
  &lt;li&gt;Common in network virtualization scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s possible to identify those by checking the second least significant bit of the first octet:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If the bit is 1, it’s locally administered&lt;/li&gt;
  &lt;li&gt;If it’s 0, it’s universally administered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;02:00:00:00:00:00 → locally administered&lt;/li&gt;
  &lt;li&gt;00:1A:2B:3C:4D:5E → universally administered&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;license-file&quot;&gt;License File&lt;/h2&gt;

&lt;p&gt;We use the &lt;a href=&quot;github.com/adrg/xdg&quot;&gt;github.com/adrg/xdg&lt;/a&gt; library to determine where to store the license file.&lt;/p&gt;

&lt;p&gt;This library implements the &lt;a href=&quot;&quot;&gt;XDG Base Directory and XDG User Directory specifications&lt;/a&gt;, offering a standard mechanism for storing application state, data, or configuration across multiple OSes—Windows, Linux, Plan 9, and macOS.&lt;/p&gt;

&lt;p&gt;In the app’s case, we rely on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;XDG_DATA_HOME&lt;/code&gt;, or fallbacks as per OS:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Unix&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;macOS&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Plan 9&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Windows&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;~/.local/share&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;~/Library/Application Support&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;$home/lib&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;LocalAppData&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Example ‘license.json’:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;hash&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;7394991a704a054096f4484d8a19f9ac66e3e8c98b68603652feadc785a364f2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;license_key_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;DEVTUI-2CA57A34-E191-4290-A394-XXXXXX&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;activation_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2ee71107-2ecb-4172-aff7-ceaa6b2f7cef&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;next_check_time&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2025-05-12T23:18:12.7724912+02:00&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;last_verified_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2025-05-05T23:18:12.772488825+02:00&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The hash is generated from multiple values and is used to verify that the license data hasn’t been tampered with. If the hash doesn’t match, the app treats the license as invalid.&lt;/p&gt;

&lt;p&gt;I won’t go into the full details of what goes into the hash, but in general:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;MAC address&lt;/li&gt;
  &lt;li&gt;License key&lt;/li&gt;
  &lt;li&gt;Activation time&lt;/li&gt;
  &lt;li&gt;Next check timestamp&lt;/li&gt;
  &lt;li&gt;A salt value (random string)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mechanism isn’t hacker-proof, but the effort required to crack it should outweigh the $40 one-time price tag.&lt;/p&gt;
&lt;h2 id=&quot;final-notes&quot;&gt;Final notes&lt;/h2&gt;
&lt;p&gt;This is my first time implementing client-side license logic for an app, so I’ve probably missed some edge cases.  But if anyone has some feedback or recommendations, please reach out.&lt;/p&gt;
</description>
        <pubDate>Sun, 11 May 2025 16:38:00 +0000</pubDate>
        <link>https://www.skatkov.com/posts/2025-05-11-software-license-management-for-dummies</link>
        <guid isPermaLink="true">https://www.skatkov.com/posts/2025-05-11-software-license-management-for-dummies</guid>
        
        
      </item>
    
      <item>
        <title>Strange thing about happiness.</title>
        <description>&lt;p&gt;Once, my mom showed me a video where I was having water fun with my kid after I finished my wake boarding session. My mother captured a joyful moment, but guy on a video was shirtless, which saddened the video for me. That guy on a video looked visibly fat (over 100 kilos) with hanging breasts.&lt;/p&gt;

&lt;p&gt;It was clear  - that “guy” had let himself loose. Even though, nobody ever said it out loud. That guy decided that he has higher standards for himself and body shape needed to be rectified.&lt;/p&gt;

&lt;p&gt;Wake boarding is definitely fun, but it’s not enough in Dutch climate. Luckily, I found a kickboxing gym near by house, and trainer seemed really good. I already had experience with muay thai before, and knew that dutch kickboxing was dominating. So the dutch kickboxing journey started.&lt;/p&gt;

&lt;p&gt;For the first month, I would vomit from the intensity of the workouts. After a month I became a bit more confident and started participating in sparing sessions. It was not always light and my body already was not as conditioned - so injuries started creeping in into my day to day life. It is intense, and I came back exhausted  - couldn’t do anything else but sleep. (bummer, if you planned to work on a side-project that evening)&lt;/p&gt;

&lt;p&gt;It was only after about 8 months that I could get through workouts without major upheavals. It became a routine - train, come home, take a shower, and continue with my daily activities. Injuries are also a thing of the past, and even bruises hardly appear anymore.&lt;/p&gt;

&lt;p&gt;My weight is now 97 kg, much less than before. BMI is on the border of normal. Muscles and ABS could be spotted on my body once again. Kickboxing have become my outlet - when I feel bad, I go straight to the gym. People are already worried when I don’t come for a long time. How can I not go now and let the guys down?&lt;/p&gt;

&lt;p&gt;I found my sport in the Netherlands, and of course, wake boarding when it’s warm. My son was inspired and he goes twice a week to same kickboxing gym. Those bullies should be really careful around my son now - he can already kick ass.&lt;/p&gt;

&lt;p&gt;Sport really increases happiness levels. And a lot of water.&lt;/p&gt;

</description>
        <pubDate>Thu, 28 Nov 2024 13:43:00 +0000</pubDate>
        <link>https://www.skatkov.com/posts/2024-11-28-strange-thing-about-happiness</link>
        <guid isPermaLink="true">https://www.skatkov.com/posts/2024-11-28-strange-thing-about-happiness</guid>
        
        
      </item>
    
      <item>
        <title>Coworking in Eindhoven</title>
        <description>&lt;p&gt;I’m permanently based in Eindhoven these days. We tried to make nomad life work, but decided not to pursue this lifestyle any longer. I’m able to work remotely and still do, but:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;kid needs a stable school and circle of friends around him&lt;/li&gt;
  &lt;li&gt;wife can’t sit straight for hours at a computer, so she needs a job that doesn’t require that&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m OK with staying put, but biggest thing I miss about “digital nomads” is a cheerful and supportive community of laptop workers that support each other in their work and life.&lt;/p&gt;

&lt;p&gt;Local coworking spaces are not about community and don’t look that cheerful either. They are more like offices. That’s not an issue with Eindhoven, but the Netherlands per se. It might be even a wider issue in Europe, because I haven’t found anything like what I experienced in Asia’s coworking spaces anywhere else here.&lt;/p&gt;

&lt;p&gt;So this is my attempt to bring a cheerful and encouraging coworking vibe to Eindhoven. Now there is a small &lt;a href=&quot;https://t.me/+dZkamGqH9XtjMzFl&quot;&gt;Telegram group&lt;/a&gt; with 24 members as of date. It so happened that the first active members spoke Russian, so the main language of communication is Russian, but everyone can easily switch to English. So if you’re living here or passing by, no matter who you are - feel free to join.&lt;/p&gt;

&lt;p&gt;We have explored a lot of places in Eindhoven where one can work with a laptop and have a decent cup of coffee in the process. And here is a list of places that our community approves.&lt;/p&gt;

&lt;h2 id=&quot;lucifer-coffee&quot;&gt;Lucifer Coffee&lt;/h2&gt;
&lt;p&gt;This place comes with decent coffee, separate laptop tables equipped with power sockets and a great atmosphere. My personal favorite is their “daily soups”, I’m a big fan of soups and they often have really decent ones.&lt;/p&gt;

&lt;p&gt;There are two of them in the city:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://maps.app.goo.gl/AntumVe6mreBDb7Q6&quot;&gt;One next to the central station&lt;/a&gt; - this one is the biggest and has a lot of space. By far my favorite spot to work at, there are also a lot of food choices within walking distance.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://maps.app.goo.gl/bytmrD6vJTRB7n4y9&quot;&gt;Another one in the heart of city&lt;/a&gt; - a bit dark and you are only allowed to work on one big table. But a decent option nevertheless.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;zwartwit-koffie&quot;&gt;Zwartwit Koffie&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://maps.app.goo.gl/yRdrzL2Mr4ztryHb6&quot;&gt;https://maps.app.goo.gl/yRdrzL2Mr4ztryHb6&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Coffee place that started it all. This is where I met first group members and most of my first friends in town. By far the friendliest place in town – visitors are happy to have a chit-chat, even staff stick around longer to ask how you are doing.&lt;/p&gt;

&lt;p&gt;Unfortunately, this place has switched owners and coffee quality varies these days. They switch recipes and beans a lot, sometimes it’s great, sometimes not at all.&lt;/p&gt;

&lt;h2 id=&quot;hubble-community-café&quot;&gt;Hubble Community Café&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://maps.app.goo.gl/oxkcorPRa6d5GXnb6&quot;&gt;https://maps.app.goo.gl/oxkcorPRa6d5GXnb6&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a university coffee place, but it’s open to the general public. One thing about universities is that they usually have the cheapest food on premises. It doesn’t get any cheaper than here.&lt;/p&gt;

&lt;p&gt;Coffee is decent and cheap as well.&lt;/p&gt;

&lt;h2 id=&quot;eindhoven-public-library&quot;&gt;Eindhoven Public Library&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://maps.app.goo.gl/1M3Pbj8wUbMMQLhQ9&quot;&gt;https://maps.app.goo.gl/1M3Pbj8wUbMMQLhQ9&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Eindhoven has a fantastic library, works really late too (until 19:00). So if all the coffee places are closed and you don’t feel like going home - this is a fantastic place.&lt;/p&gt;

&lt;p&gt;Usually it’s really quiet here, so it’s a perfect place to take a call. Not the case when there is a school break - it gets overrun by kids.&lt;/p&gt;

&lt;h2 id=&quot;seats2meet&quot;&gt;Seats2meet&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://seats2meet.com&quot;&gt;https://seats2meet.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’ve been bad mouthing coworking places, but this one is the closest to social coworking places I’ve found in Europe. If you pre-register, they will most likely have a free spot for you and free coffee. Even though the coffee is free, quality is pretty decent.&lt;/p&gt;

&lt;h2 id=&quot;douwe-egberts-café&quot;&gt;Douwe Egberts Café&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://maps.app.goo.gl/cboG4K6CW98q1dx37&quot;&gt;https://maps.app.goo.gl/cboG4K6CW98q1dx37&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My least favorite coffee place, because I absolutely don’t like their coffee, my wife loves it though. Everyone’s tastes are different, so you might like it. I usually compensate with their smoothies or tea, those are great. Also decent selection of snacks.&lt;/p&gt;

&lt;p&gt;But the biggest advantage here is two huge floors with a lot of sitting places. When every other place is full, you can always find a spot here.&lt;/p&gt;

&lt;p&gt;If you know of any other spaces that we should check out – let me know. Otherwise will be happy to see you join our small group.&lt;/p&gt;
</description>
        <pubDate>Fri, 22 Nov 2024 11:26:00 +0000</pubDate>
        <link>https://www.skatkov.com/posts/2024-11-22-coworking-in-eindhoven</link>
        <guid isPermaLink="true">https://www.skatkov.com/posts/2024-11-22-coworking-in-eindhoven</guid>
        
        
      </item>
    
      <item>
        <title>Nothing Phone 2a</title>
        <description>&lt;p&gt;Someone stepped on my trusted OnePlus Nord 2T at the gym, and the screen broke in 3 different places. It was my mistake in the first place, so after reflecting on my bad habit of throwing things around the gym, I decided to buy another phone.&lt;/p&gt;

&lt;p&gt;I never buy flagship phones and stay within the medium-top range category. It’s not that I can’t afford a more expensive phone; there’s just not much justification for me to have a phone that costs more than 400 EUR.&lt;/p&gt;

&lt;p&gt;There’s a new mobile maker underdog in Europe that offers phones in that range - Nothing.tech! So, I bought a &lt;strong&gt;Nothing Phone 2a&lt;/strong&gt; to support a European hardware startup. Building phones in our day and age is not easy, especially for a company in Europe - we should support such underdogs.&lt;/p&gt;

&lt;p&gt;It’s been a week since the Nothing Phone arrived at our house.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/nothing-phone.jpg&quot; alt=&quot;Nothing Phone 2a&quot; /&gt;&lt;/p&gt;

&lt;p&gt;There are things that threw me off immediately about the phone, but after a week together, I’m coming to terms with those quirks.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;It charges slower than my previous phone (80W versus 45W). As I understand, charging speeds over 60W are not great for long-term battery health. After a couple of years, 80W charging started to show on my Nord 2T; it barely lasted a day. The Nothing Phone lasts me a couple of days on a single charge, and I assume with the compromise on charging speed, this nice feature would last longer.&lt;/li&gt;
  &lt;li&gt;The phone’s back is uncompromisingly plastic without any attempts to hide it. Nothing made it look cool, but the tactile feeling is so unusual that it still feels like a kid’s toy in my hands. But I’m getting used to this feeling!&lt;/li&gt;
  &lt;li&gt;The official case makes the phone rather bulky and heavy; the phone without it is lighter and thinner. I absolutely regret buying that case. It feels like an afterthought and an extra they make money on.&lt;/li&gt;
  &lt;li&gt;Most Android phones have a bottom bar with a couple of buttons that allow you to go back or see all active apps. Well, Nothing completely ditched that, replacing it with gestures. This gives more screen estate, but it still takes me a couple of attempts to get those gestures right. If only they had a setting to bring that bar back!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These 4 are my main complaints; everything else is praise.&lt;/p&gt;

&lt;p&gt;I love this monochrome theme! The screen seems really good. Everything feels snappy. Their camera placement is actually very convenient; I hardly ever touch it with my fingers by accident. And the phone is running Android 15, while Nord still uses Android 12 (so much for promised updates, huh?).&lt;/p&gt;

&lt;p&gt;But the strangest thing is that you can submit feedback, and the company actively answers it. More than that, they suggest how to resolve those complaints. I’m very actively submitting bugs and feedback about hardware and software in use, but Nothing is the only hardware company that actually responds!&lt;/p&gt;

&lt;p&gt;I have little experience with flagship phones. I could probably get a better processor or a camera that would cost me 4x as much - but I absolutely don’t need any of that. If I numb my tactile feelings and forget that I’m holding a piece of plastic in my hands, the Nothing Phone 2a does feel premium!&lt;/p&gt;
</description>
        <pubDate>Mon, 14 Oct 2024 22:09:00 +0000</pubDate>
        <link>https://www.skatkov.com/posts/2024-10-15-nothing-phone-2a</link>
        <guid isPermaLink="true">https://www.skatkov.com/posts/2024-10-15-nothing-phone-2a</guid>
        
        
      </item>
    
  </channel>
</rss>
