<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>David Mohundro - Developer, Architect, Learner, Dad and more</title>
        <link>https://mohundro.com</link>
        <description>David Mohundro is a developer, architect, continuous learner, dad, husband and more.</description>
        <lastBuildDate>Tue, 07 Apr 2026 18:10:45 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en-us</language>
        <image>
            <title>David Mohundro - Developer, Architect, Learner, Dad and more</title>
            <url>https://mohundro.com/static/images/me.jpeg</url>
            <link>https://mohundro.com</link>
        </image>
        <copyright>All rights reserved 2026, David Mohundro</copyright>
        <atom:link href="https://mohundro.com/rss.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Tuesday - April 7, 2026]]></title>
            <link>https://mohundro.com/fragments/2026-04-07/</link>
            <guid isPermaLink="false">https://mohundro.com/fragments/2026-04-07/</guid>
            <pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I am <em>loving</em> the Artemis II news. When I was a kid, some of my favorite memories were seeing photos from Voyager II of the outer planets. When my mom would take me to the library, I would beeline to the science section... either astronomy or dinosaurs. My kids are starting to share a lot of my interests and I took two of them to watch Project Hail Mary this past weekend - we loved it.</p>
<hr />
<p>There are a multitude of sites out there to help you level up your shell skills. And almost every time I find one shared, I learn at least something new. See <a href="https://blog.hofstede.it/shell-tricks-that-actually-make-life-easier-and-save-your-sanity/">https://blog.hofstede.it/shell-tricks-that-actually-make-life-easier-and-save-your-sanity/</a>. On this one, it was mostly the "backspace replacement" section. While I am a VIM keybinding fan, the one place I never had gotten used to it is on the terminal, so I've still stuck with readline defaults.</p>
<hr />
<p>Leadership.Garden has a treasure trove of great content. Here are a couple of great posts from the last few weeks:</p>
<ul>
<li><a href="https://leadership.garden/the-finger-is-not-the-moon/">https://leadership.garden/the-finger-is-not-the-moon/</a></li>
<li><a href="https://leadership.garden/what-actually-breaks-at-30-people/">https://leadership.garden/what-actually-breaks-at-30-people/</a></li>
</ul>
<p>I'm not going to share my own comments here, just check them out. Great insights into organizations and leadership.</p>
<hr />
<p>This site just made me laugh - see <a href="https://www.theuncomfortable.com/#">the Uncomfortable</a>. I had to share this one with my kids just to hear their reactions.</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[New Blog Redesign]]></title>
            <link>https://mohundro.com/blog/2026-03-14-new-redesign/</link>
            <guid isPermaLink="false">https://mohundro.com/blog/2026-03-14-new-redesign/</guid>
            <pubDate>Sat, 14 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Well, it was time for another blog redesign. I'd been wanting to do something with this for a while, because I had still been coasting on the Tailwind Next.js...]]></description>
            <content:encoded><![CDATA[<p>Well, it was time for another blog redesign.</p>
<p>I'd been wanting to do something with this for a while, because I had still been coasting on the <a href="https://github.com/timlrx/tailwind-nextjs-starter-blog">Tailwind Next.js design</a> for a few years.</p>
<p>I'd also liked some of the experimentation around tweaks to the traditional blog format (e.g. <a href="/tags/fragment">fragments</a>, <a href="/til">til</a>, or <a href="/now">now</a>) so... I added them all as part of this. If nothing else, my hope is that this will encourage me to engage more here than on social media.</p>
<p>I've also set up <a href="https://posseparty.com/">POSSE Party</a> so that, even though I don't really plan to engage with social media, my posts will still go up there.</p>
<h2>Fragments</h2>
<p>If you want to know what fragments are, the idea is really from <a href="https://martinfowler.com/articles/writing-fragments.html">Martin Fowler</a>. I'm not doing anything original here.</p>
<p>If anything, this is a lot more like my really old posts. I used to drop completely random posts that, at this point, are more on the embarrassing side of things. But I'm keeping them up.</p>
<p>I'd like to post these on a semi-regular cadence, though.</p>
<h2>TIL</h2>
<p>The TIL (i.e. "Today I Learned") posts idea has been around for a while, but these are just snippets that are things I didn't know about. I've been tracking these in Obsidian (when I remember to write them down), so I just wrote a little snippet of code with the <a href="https://help.obsidian.md/cli">Obsidian CLI</a> to pull them over here.</p>
<p>Some of these things are on really old topics, mostly because as we help our clients at work, they sometimes need help with really old things. Will this be relevant to you? Probably not. But it is there, too.</p>
<h2>Now</h2>
<p>The "now" idea is comes entirely from <a href="https://sive.rs/nowff">Derek Sivers</a>. You can see examples via <a href="/now">https://nownownow.com/about</a>.</p>
<p>In theory, this will be my somewhat up to date status, assuming I keep it up to date.</p>
<h2>Other</h2>
<p>Besides those things, it is redesigned. Before you ask, <em>yes</em> I did use LLMs to help. I both enjoy using these tools and also miss the craft of coding. In this case, though, it did help me come up with a lot of ideas regarding the design.</p>
<p>This post, though, was written entirely by me.</p>
<p>Enjoy!</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[Friday - March 13, 2026]]></title>
            <link>https://mohundro.com/fragments/2026-03-13/</link>
            <guid isPermaLink="false">https://mohundro.com/fragments/2026-03-13/</guid>
            <pubDate>Fri, 13 Mar 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Welcome to the first fragment! Credit for this idea goes primarily to <a href="https://martinfowler.com/articles/writing-fragments.html">Martin Fowler</a>, though <a href="https://simonwillison.net/notes/">Simon Willison</a> has a similar idea, too.</p>
<p>This is mostly a means of avoiding social media... whereas I used to post entirely random ideas on Twitter or BlueSky, now I'll post them here. I might also post them on social media, but I might not. If you see <em>really</em> old posts on my blog, you'll see I used to do this back then <em>before social media</em>. Yes, I'm old.</p>
<hr />
<p>I was super grateful for Claude Code this week as we released a major project at work for one of our clients. We were able to respond far more quickly because of Claude than we would have 2-3 years ago.</p>
<p>Not sure if that makes me a "make-it-go" person vs. a "craft" person or not. I honestly love both. I mean, I spent months learning how to use Vim because I saw a video showing how fast someone could code with it... <em>but</em> I love to deliver something new, too. I guess I'm not sure how I feel.</p>
<p>(see <a href="https://blog.lmorchard.com/2026/03/11/grief-and-the-ai-split/">https://blog.lmorchard.com/2026/03/11/grief-and-the-ai-split/</a> for good thoughts here)</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[Parallels Login Prompt]]></title>
            <link>https://mohundro.com/til/2026-02-02-parallels-login-prompt/</link>
            <guid isPermaLink="false">https://mohundro.com/til/2026-02-02-parallels-login-prompt/</guid>
            <pubDate>Mon, 02 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>How to get to the login prompt in Parallels:</p>
<p>Press <code>Ctrl + Alt + F3</code> (on a Mac keyboard in Parallels, you might need <code>Fn + Control + Option + F3</code>).</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[ripgrep and the rgignore file]]></title>
            <link>https://mohundro.com/blog/2026-01-07-rgignore/</link>
            <guid isPermaLink="false">https://mohundro.com/blog/2026-01-07-rgignore/</guid>
            <pubDate>Wed, 07 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[I'm a huge fan of ripgrep. And that's not an exaggeration. It's literally my number one used tool on the terminal. So, to learn something new about it makes...]]></description>
            <content:encoded><![CDATA[<p>I'm a huge fan of ripgrep. And that's not an exaggeration. It's literally my number one used tool on the terminal.</p>
<pre><code>$ history --search | string trim | string replace -r ' .*' '' | sort | uniq -c | sort -nr | head -n 10
2913 rg
1005 git
 369 cd
 322 ls
 287 cat
 285 rm
 226 fd
 188 brew
 168 j
 162 gh
</code></pre>
<p>So, to learn something new about it makes for a special occasion.</p>
<p>ripgrep already has built in support for ignoring files based on your <code>.gitignore</code> file. I've known this for a while. And I've <em>read</em> this line in the README before, too...</p>
<blockquote>
<p>Namely, ripgrep won't search files ignored by your <code>.gitignore</code>/<code>.ignore</code>/<code>.rgignore</code> files, it won't search hidden files and it won't search binary files. Automatic filtering can be disabled with <code>rg -uuu</code>.</p>
</blockquote>
<p>So, I <em>knew</em> ripgrep had its own ignore file. But I never knew why I might care. It felt weird. Then I realized why.</p>
<p>Check out this snippet:</p>
<pre><code>$ rg FirstName
src/lib/Foo.Lib/Data/Migrations/20251001172535_SomeRandomEnhancements.Designer.cs
2850:                    b.Property&lt;string&gt;("FirstName")
4295:                    b.Property&lt;string&gt;("FirstName")
5326:                    b.Property&lt;string&gt;("FatherFirstName")
5349:                    b.Property&lt;string&gt;("FirstName")
5418:                    b.Property&lt;string&gt;("MotherFirstName")
5555:                    b.Property&lt;string&gt;("SpouseFirstName")
5690:                    b.Property&lt;string&gt;("FirstName")
8135:                    b.Property&lt;string&gt;("FirstName")
12923:                    b.Property&lt;string&gt;("FirstName")

src/lib/Foo.Lib/Models/PersonModel.cs
8:    public string FirstName { get; set; } = string.Empty;

src/lib/Foo.Lib/Data/Migrations/20251106142304_OtherMigration.Designer.cs
2853:                    b.Property&lt;string&gt;("FirstName")
4298:                    b.Property&lt;string&gt;("FirstName")
5329:                    b.Property&lt;string&gt;("FatherFirstName")
5352:                    b.Property&lt;string&gt;("FirstName")
5421:                    b.Property&lt;string&gt;("MotherFirstName")
5558:                    b.Property&lt;string&gt;("SpouseFirstName")
5693:                    b.Property&lt;string&gt;("FirstName")
8146:                    b.Property&lt;string&gt;("FirstName")
12960:                    b.Property&lt;string&gt;("FirstName")
</code></pre>
<p>The migration designer file is <em>pure noise</em> when I'm looking for usage. Multiply this times every migration file in the source. Or, if you still have a system with minified javascript in source (not every project has switched to <code>package.json</code>, <em>still</em>)? Even worse.</p>
<p>If I add a new <code>.rgignore</code> file, though, that has this line:</p>
<pre><code>src/lib/Foo.Lib/Data/Migrations/*.Designer.cs
*.min.js
*.min.css
*.js.map
</code></pre>
<p>It now will only show the match I <em>care</em> about:</p>
<pre><code>$ rg FirstName
src/lib/Foo.Lib/Models/PersonModel.cs
8:    public string FirstName { get; set; } = string.Empty;
</code></pre>
<p>Disclaimer - there is a risk that you'll miss matches with this. But that's why ripgrep has support to disable the filters, too.</p>
<p>Hope this helps someone out there.</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[How to Extract Claude Code Transcripts]]></title>
            <link>https://mohundro.com/til/2026-01-07-extracting-claude-code-transcripts/</link>
            <guid isPermaLink="false">https://mohundro.com/til/2026-01-07-extracting-claude-code-transcripts/</guid>
            <pubDate>Wed, 07 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><a href="https://simonwillison.net/2025/Dec/25/claude-code-transcripts/">Simon Willison has a tool for this</a>...</p>
<p>Just run:</p>
<pre><code>uvx claude-code-transcripts
</code></pre>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[Git Wrapped for 2025]]></title>
            <link>https://mohundro.com/blog/2026-01-05-git-wrapped/</link>
            <guid isPermaLink="false">https://mohundro.com/blog/2026-01-05-git-wrapped/</guid>
            <pubDate>Mon, 05 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[I don't recall pulling up my Git Wrapped before, but I saw someone link theirs today, so I decided to check it out. You can see mine at...]]></description>
            <content:encoded><![CDATA[<p>I don't recall pulling up my <a href="https://git-wrapped.com">Git Wrapped</a> before, but I saw someone link theirs today, so I decided to check it out.</p>
<p>You can see mine at <a href="https://git-wrapped.com/profiles/drmohundro">git-wrapped.com/profiles/drmohundro</a> or embedded below:</p>
<p><img src="/static/images/blog/2025-git-wrapped-drmohundro.png" alt="my git wrapped for 2025" /></p>
<p>I'm <em>most</em> surprised that Friday is my most active day. I would have never guessed that... but then again, that is the least likely day to have calls or meetings.</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[2025 Year in Review]]></title>
            <link>https://mohundro.com/blog/2026-01-01-2025-year-in-review/</link>
            <guid isPermaLink="false">https://mohundro.com/blog/2026-01-01-2025-year-in-review/</guid>
            <pubDate>Thu, 01 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Well, 2025 is a wrap and we're in 2026. As I've tried to do over the last few years, I want to reflect on how the year went and on some of the things I've...]]></description>
            <content:encoded><![CDATA[<p>Well, 2025 is a wrap and we're in 2026. As I've tried to do over the last few years, I want to reflect on how the year went and on some of the things I've learned and the highlights.</p>
<p>First off, the definite highlight for me was celebrating twenty years of marriage. When we got married, I was living in a different state early in my career - I didn't know much of anything. I mean, if you go back and look at my blog content, I was just throwing junk out there (though in fairness, this was before social media existed... yes, I'm old...). Now, we've got four kids, I've been at Clear Function for 10 years working as a consultant, and I'd like to think I'm a lot wiser.</p>
<p>To celebrate our anniversary, we took a trip with some good friends to do a Biblical tour in Turkey and Greece visiting the sites for the seven churches of Revelation. We ended up getting to see most of the sites for Paul's first missionary journey as well. I didn't really know what to expect, but it was amazing. We took over 2000 photos and I took pages of notes.</p>
<h2>Reflections on AI</h2>
<p>On the software engineering front, you can't talk about 2025 without talking about AI... or generative AI and LLMs anyway. I've been using AI tools for 2-3 years now, but they've become a daily driver for me... (whether I like it or not).</p>
<p>There have been plenty of posts talking about the pros and cons of using LLMs to help with development. Do LLMs speed up development? Do LLMs make you feel faster, but you're actually slower? Do LLMs infringe on copyright laws? I don't know. For good or bad, we're using it because our clients will be asking about it and we have to know and understand it.</p>
<p>Do I enjoy using it? If I'm honest... sometimes. For greenfield projects, or especially for spikes or proof of concepts, I've been impressed. I've built multiple small projects for personal use as well as UI mockups that have helped us at work with communicating with clients in ways that would have been a lot more costly before. For brownfield projects, it still feels hit or miss.</p>
<p>I started the year off mostly using Windsurf with some Copilot usage in VSCode, but I've ended the year mostly using Claude Code in the terminal. In my mind, Claude Code has really taken the lead in terms of development tools. I'll admit, I still use Gemini as a Google replacement (which is ironic considering Gemini is from Google...) - I guess I should instead say I use Gemini as a search engine replacement. I'm still using DuckDuckGo as my primary search engine.</p>
<h2>Conferences</h2>
<p>This year my team only attended one conference - <a href="https://2025.allthingsopen.org/">All Things Open 2025</a>. I enjoyed it quite a bit, though I <em>still</em> wish we could find a technical conference that is full of 300 and 400 level sessions. It is frustrating to go to a session and either get a sales pitch or what we could get from the introductory docs for a tool or library. I guess one difference is that, when I first started going to conferences earlier in my career, most of the good content was only available in books, so conferences could be invaluable... today, most everything is online.</p>
<h2>Books</h2>
<p>I didn't read nearly as many books as I might have liked this year, but I will call a few out. I read the Silo books, partially based on a recommendation from a friend and partially out of curiosity based on seeing clips from the TV show. I enjoyed the books quite a bit, though I remember telling my friend that the first book actually felt <em>claustrophobic</em> to me, as so much of it was set in a single silo. I wanted to "see" more.</p>
<p>Another book I read was <em>Absolution</em>, the fourth book in the Southern Reach series. I really enjoyed the earlier three books, though they're very strange and disorienting. I was sort of hoping for some answers in this fourth book. Just to clear things up... there aren't any answers in the fourth book, just more questions. I really enjoyed the first 2/3rds of the book, though. The last 3rd... <em>ouch</em>. I really struggled with it. I can't say more... but if you've read it, you'll know what I mean. Sorry for being cryptic otherwise.</p>
<p>The last book I want to mention is <em>Jonathan Strange and Mr. Norrell</em>. I mostly read this one because I loved <em>Piranesi</em> so much a few years back. This one was great, but it took me a long time to get into it. It's a long read and I think it takes a while to build out the world... once I was in, though, I loved it.</p>
<h2>Games</h2>
<p>I don't normally add anything about games I played, but 2025 had some remarkable games (some of which came out earlier). The first one I want to talk about is <em>Baldur's Gate 3</em>... and yes, it came out in 2023. I just never got around to it. I'm sure glad I finally was able to play it. Having not played a CRPG in a while, it was difficult to get used to. The story is great and the number of ways you can approach a problem is enormous. It is just a bit overwhelming. But I loved it.</p>
<p><em>Indiana Jones and the Great Circle</em> was one of my favorite games. I still remember playing the intro level and having a huge grin on my face. When you play this game, YOU ARE Indiana Jones. I really hope they keep this series going.</p>
<p>Another great game was <em>Avowed</em>. The world building in this game was amazing. Also, there were some absolutely hilarious moments... just side comments from companions in reaction to choices... I actually laughed out loud at times.</p>
<p>My personal game of the year, though, is <em>Clair Obscur: Expedition 33</em>... and most everyone else it seems based on the awards results. I have <em>never</em> seen any medium engage with grief as well as this game. It was an amazing experience.</p>
<h2>Wrapping Up</h2>
<p>Overall, 2025 was a good year for me. There were plenty of things to get upset about, but I really am trying to look at the positives. I'm looking forward to seeing what comes in 2026!</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[Ten Years at Clear Function]]></title>
            <link>https://mohundro.com/blog/2025-09-01-ten-years-at-clear-function/</link>
            <guid isPermaLink="false">https://mohundro.com/blog/2025-09-01-ten-years-at-clear-function/</guid>
            <pubDate>Mon, 01 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Today, September 1, marks ten years for me working at Clear Function. Prior to CF, the longest I had worked anywhere was 7 years, so this is a landmark for me....]]></description>
            <content:encoded><![CDATA[<p>Today, September 1, marks ten years for me working at Clear Function. Prior to CF, the longest I had worked anywhere was 7 years, so this is a landmark for me.</p>
<h2>Reflections on the past Decade at Work</h2>
<p>I was joking with coworkers that, when I started at CF, I only had 2 kids and now I have 4! Obviously, number of children has nothing to do with my work, but it more shows just how much has changed in the last decade.</p>
<p>On the work side, Clear Function has been in two different offices with four different spaces. We went from largely in-person to a very hybrid model with half of our team being remote now. I still prefer working in the office most of the time, because I 1) find that I can focus better on work there and 2) I like hanging out with my coworkers! I do work from home when I need to, though, and that flexibility is a huge difference maker.</p>
<p>It would be hard to estimate how many clients and projects I've worked on in that period of time... the types of technologies has grown, but not as much as I might have thought. The biggest changes I've seen have honestly been on the frontend side, with projects ranging from old jQuery projects to modern React or Vue projects and everything in between. I've met new friends at various clients and I'm confident that most of them would love the opportunity to work with me again, in whatever fashion that might look like.</p>
<h2>Reflections at Home</h2>
<p>Like I said, life at home has changed a lot in the last ten years, too. Two of my children have been born since then, one is a teenager now and one is <em>really</em> close.</p>
<p>I started my fitness journey about 7 years ago and have now run at least 5 half marathons. I'm certainly healthier than I was when I started!</p>
<h2>Reflections on Technology</h2>
<p>Who would have thought that AI would have become so huge over this time? I remember trying out ChatGPT when it first went public and everyone had it make songs or whatever and were blown away by it to now we're using Claude Code pretty regularly at work. I'm very much in a "love/hate" relationship with LLMs - I like that it can help with "busy-work" code and I hate what it is doing to college graduates. It shouldn't surprise me that so many businesses are so short-sighted, though.</p>
<h2>Wrapping Up</h2>
<p>This is mostly been a meandering post, but I just wanted to drop some brief reflections. Back to hanging out with the kids on our day off!</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[Report Server - Finding embedded datasets]]></title>
            <link>https://mohundro.com/til/2025-08-07-report-server/</link>
            <guid isPermaLink="false">https://mohundro.com/til/2025-08-07-report-server/</guid>
            <pubDate>Thu, 07 Aug 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Finding where embedded datasets (sprocs) are used across reports:</p>
<pre><code>WITH XMLNAMESPACES
   (DEFAULT 'http://schemas.microsoft.com/sqlserver/reporting/2016/01/reportdefinition',
    'http://schemas.microsoft.com/SQLServer/reporting/reportdesigner' AS rd
   )
SELECT
    C.Name AS ReportName,
    DS.value('@Name', 'VARCHAR(255)') AS DataSetName,
    Q.value('CommandText[1]', 'VARCHAR(MAX)') AS CommandText
FROM
    dbo.Catalog AS C
CROSS APPLY
    (SELECT CAST(CAST(C.Content AS VARBINARY(MAX)) AS XML)) AS ReportXML(X)
CROSS APPLY
    ReportXML.X.nodes('/Report/DataSets/DataSet') AS DataSets(DS)
CROSS APPLY
    DataSets.DS.nodes('Query') AS Queries(Q)
WHERE
    C.Type = 2 -- Type 2 indicates a Report
    AND Q.value('CommandText[1]', 'VARCHAR(MAX)') LIKE '%' + 'spRptYourProcName' + '%';
</code></pre>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[Brief intro into JetBrains.Annotations]]></title>
            <link>https://mohundro.com/blog/2025-07-23-jetbrains-annotations/</link>
            <guid isPermaLink="false">https://mohundro.com/blog/2025-07-23-jetbrains-annotations/</guid>
            <pubDate>Wed, 23 Jul 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[I'm a longtime fan of JetBrains tooling... I've been using them since at least 2007 (see my first post about TDD where I first mention ReSharper). Today, my...]]></description>
            <content:encoded><![CDATA[<p>I'm a longtime fan of JetBrains tooling... I've been using them since <em>at least</em> 2007 (see <a href="https://mohundro.com/blog/2007-11-14-successful-first-baby-steps-with-test-driven-development/">my first post about TDD</a> where I first mention ReSharper). Today, my primary editor is JetBrains Rider (or some other flavor of JetBrains IDE depending on the project and stack) on MacOS. I wanted to share about the <a href="https://www.nuget.org/packages/JetBrains.Annotations/"><code>JetBrains.Annotations</code> NuGet package</a>.</p>
<p>Per the README:</p>
<blockquote>
<p>ReSharper Annotations help reduce false positive warnings, explicitly declare purity and nullability in your code, deal with implicit usages of types and members, support special semantics of the APIs in ASP.NET and XAML frameworks and otherwise increase the accuracy of the code inspections in JetBrains .NET IDEs.</p>
</blockquote>
<p>Here's how I'd say it... the package provides a collection of .NET attributes that you can use to decorate your code and communicate intent <em>specifically to your IDE</em> about how the code should be used.</p>
<p>For example, Rider can track usages across your system; however, if you access an interface via reflection, it can't tell via static analysis. But if you add the <code>UsedImplicitly</code> attribute, you're letting Rider/ReSharper know that the interface <em>is used</em> and to not mark it as safe to remove.</p>
<pre><code>[UsedImplicitly]
public interface IFoo { }
</code></pre>
<p>A few that I just recently learned about are the AspMvc* attributes (e.g. <code>AspMvcActionAttribute</code>, <code>AspMvcAreaAttribute</code>, <code>AspMvcControllerAttribute</code>, etc.).</p>
<p>Here's a recent example of usage that I brought in:</p>
<pre><code>[HtmlTargetElement("li", Attributes = "is-selected-area")]
[HtmlTargetElement("ul", Attributes = "is-selected-area")]
public class IsSelectedTagHelper : TagHelper
{
    [AspMvcController]
    [HtmlAttributeName("is-selected-controller")]
    public string? Controller { get; set; }

    [AspMvcController]
    [HtmlAttributeName("is-selected-controllers")]
    public string[] Controllers { get; set; } = [];

    [AspMvcAction]
    [HtmlAttributeName("is-selected-action")]
    public string? Action { get; set; }

    [AspMvcArea]
    [HtmlAttributeName("is-selected-area")]
    public string? Area { get; set; }

    [AspMvcAction]
    [HtmlAttributeName("is-selected-actions")]
    public string[] Actions { get; set; } = [];

    [HtmlAttributeName("is-selected-css-class")]
    public string CssClass { get; set; } = "active";

    [ViewContext]
    [HtmlAttributeNotBound]
    public ViewContext ViewContext { get; set; } = null!;

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        var currentController = ViewContext.RouteData.Values["controller"] as string;
        var currentAction = ViewContext.RouteData.Values["action"] as string;
        var currentArea = ViewContext.RouteData.Values["area"] as string;

        if (
            (!string.IsNullOrEmpty(Controller) || Controllers.Any())
            &amp;&amp; !(
                (
                    !string.IsNullOrEmpty(Controller)
                    &amp;&amp; Controller.Equals(currentController, StringComparison.OrdinalIgnoreCase)
                ) || Controllers.Contains(currentController, StringComparer.OrdinalIgnoreCase)
            )
        )
        {
            return;
        }

        var areaToCompare = Area;
        var currentAreaToCompare = currentArea;

        if (areaToCompare == "Dashboard" &amp;&amp; currentAreaToCompare == null)
        {
            areaToCompare = "";
            currentAreaToCompare = "";
        }

        if (
            !string.IsNullOrEmpty(Area)
            &amp;&amp; !(areaToCompare ?? string.Empty).Equals(
                currentAreaToCompare ?? string.Empty,
                StringComparison.OrdinalIgnoreCase
            )
        )
        {
            return;
        }

        var actions = new List&lt;string&gt;();
        if (!string.IsNullOrEmpty(Action))
        {
            actions.Add(Action);
        }

        actions.AddRange(Actions);

        if (
            actions.Any()
            &amp;&amp; (
                string.IsNullOrEmpty(currentAction)
                || !actions.Contains(currentAction, StringComparer.OrdinalIgnoreCase)
            )
        )
        {
            return;
        }

        var existingClasses = output
            .Attributes.FirstOrDefault(a =&gt; a.Name == "class")
            ?.Value.ToString();
        var newClasses = string.IsNullOrEmpty(existingClasses)
            ? CssClass
            : $"{existingClasses} {CssClass}";
        output.Attributes.SetAttribute("class", newClasses);
    }
}
</code></pre>
<p>And then usage is like this:</p>
<pre><code>&lt;li is-selected-controller="Users" is-selected-area="Settings"&gt;
  &lt;a asp-area="Settings" asp-controller="Users" asp-action="Index"&gt;Users&lt;/a&gt;
&lt;/li&gt;
</code></pre>
<p>What's great, though, is that Rider can now intelligently complete the areas, controllers or actions, because it knows the purpose for this property... instead of just telling me it is a <code>string</code>, it knows it is a <code>string</code> that represents an <code>MVC Area</code> or similar.</p>
<p><img src="https://gist.github.com/user-attachments/assets/96da257a-f4fb-433d-87a9-42fb9ea654b8" alt="Rider auto-completing the controller" /></p>
<p>You can see a full list of the attributes up at <a href="https://www.jetbrains.com/help/resharper/Reference__Code_Annotation_Attributes.html">https://www.jetbrains.com/help/resharper/Reference__Code_Annotation_Attributes.html</a>.</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[ClearBot (aka "Burn Bot")]]></title>
            <link>https://mohundro.com/blog/2025-05-18-clearbot-aka-burnbot/</link>
            <guid isPermaLink="false">https://mohundro.com/blog/2025-05-18-clearbot-aka-burnbot/</guid>
            <pubDate>Sun, 18 May 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Almost 9 years ago, an open source project was begun. It had one singular purpose... to make office life a bit more fun. Here's the initial commit for...]]></description>
            <content:encoded><![CDATA[<p>Almost 9 years ago, an open source project was begun. It had one singular purpose... to make office life a bit more fun. Here's the <a href="https://github.com/clearfunction/clearbot/commit/fb5ac74ae82fd707f5df34f32e87d1590b9e47cf">initial commit</a> for "clearbot" (which lovingly was renamed to Burn Bot)!</p>
<h2>What is "Burn Bot"?</h2>
<p>So, Burn Bot is a <em>very</em> simple Slack bot that listens to a handful of events and mostly uses them to queue it to play sounds on the Clear Function office Sonos speaker.</p>
<h2>But... why?</h2>
<p>I know, I know, that sounds... weird. But recall, the singular purpose for Burn Bot was to introduce a bit more fun in our office environment. Keep in mind, this was pre-COVID... so, remote work (or even hybrid work for that matter) was <em>not</em> the default like it is today. There was plenty of remote work back then, but nothing like now.</p>
<p>So, Clear Function was office first. And we had a lot of fun with our small team. But MORE FUN is even better.</p>
<p>The initial premise was that we could post something like <code>burn</code> in any channel and then the office speaker would play a funny sound letting everyone know that someone made a really funny joke to tease someone.</p>
<h2>But... how?</h2>
<p>The high level architecture for the system has remained largely the same from even almost 10 years ago.</p>
<p>Here's a simple diagram:</p>
<p>&lt;pre class="mermaid not-prose"&gt;
sequenceDiagram
Slack--&gt;&gt;ClearBot: POST /slack/events { some message }
ClearBot--&gt;&gt;Sonos Proxy: websocket play_url { some message }
Sonos Proxy--&gt;&gt;node-sonos-http-api: GET http://localhost:5001/Office/clip/burn.mp3
&lt;/pre&gt;</p>
<p>So, here's the general flow in words:</p>
<ol>
<li>Someone posts a message to a channel that Clear Bot is configured to respond to, like <code>burn</code> or <code>say hello world</code>.</li>
<li>This results in Slack forwarding that event to Clear Bot</li>
<li>Clear Bot has a websocket connection to Sonos Proxy (I'll explain this more shortly)</li>
<li>Finally, Sonos Proxy then talks to <code>node-sonos-http-api</code> (I'll explain this one, too)</li>
</ol>
<p>So... there is a bit of duct tape to connect everything here. But for a hobbyist project for fun, it isn't too bad.</p>
<h2>ClearBot (the Slack Bot)</h2>
<p>The actual clearbot (Burn Bot's actual source) is all open source and hosted at <a href="https://github.com/clearfunction/clearbot">https://github.com/clearfunction/clearbot</a>. The <a href="https://github.com/clearfunction/clearbot/commit/fb5ac74ae82fd707f5df34f32e87d1590b9e47cf">initial commit referenced above</a> shows that the first version was based on <a href="https://hubot.github.com/">Hubot</a>. Hubot is still an active bot-based library, but Slack ended up deprecating "custom integrations" so I ported the entire library over to Slack's Bolt API (see the PR up at <a href="https://github.com/clearfunction/clearbot/pull/13">https://github.com/clearfunction/clearbot/pull/13</a>).</p>
<p>The code is <em>super simple</em>.</p>
<p>In <a href="https://github.com/clearfunction/clearbot/blob/main/src/app.ts"><code>src/app.ts</code></a>, there is really one function that kicks things off... <code>attachResponses</code>. It looks like this:</p>
<pre><code>export function attachResponses(app: App, sonos: Sonos): void {
  burnResponses.forEach((resp) =&gt; {
    app.message(resp.listen, async ({ say }) =&gt; {
      if (resp.message) {
        say(resp.message)
      }

      if (typeof resp.play === 'function') {
        sonos.playOnSonos(resp.play(), say)
      } else if (typeof resp.play === 'string') {
        sonos.playOnSonos(resp.play, say)
      }
    })
  })
}
</code></pre>
<p>The <code>burnResponses</code> is an array of type <code>Response</code> that match this interface:</p>
<pre><code>export interface Response {
  keyword: string
  description: string
  listen: string | RegExp
  play: string | RandomPlay
  message?: string
}
</code></pre>
<p>So, this means that we have an array of configurations (all hard-coded actually) that look like this:</p>
<pre><code>  {
    keyword: 'sickburn | fire',
    description: 'A random sick burn!',
    listen: /!sickburn|:fire:/i,
    play: () =&gt; randomResponse(burns),
  },
  {
    keyword: 'yakety',
    description: 'Yakety sax!',
    listen: /yakety/i,
    play: 'yakkety.mp3',
  },
</code></pre>
<p>The <code>listen</code> prop defines what we're looking for in a message... if someone sends <code>sickburn</code> it will call <code>randomResponse</code>... if someone instead sends <code>yakety</code> it will play the Yakety Sax clip. Pretty simple.</p>
<p>There is also a <code>say</code> command hooked up that will just ask Sonos to do a voice to text for whatever comes in.</p>
<p>All of the "sound playing" goes to a Sonos implementation that calls emits a websocket message, meaning we have to have a Sonos Proxy connected to our clearbot. That's the next piece.</p>
<h2>Sonos Proxy</h2>
<p>The Sonos Proxy <em>is also open source</em> and lives at <a href="https://github.com/clearfunction/sonos-proxy-nodejs">https://github.com/clearfunction/sonos-proxy-nodejs</a>. It is even simpler than Burn Bot's source... in fact, this is the main bit of logic (with some logging and comments removed):</p>
<pre><code>  socket.on('play_url', data =&gt; {
    enumeratePlayers(roomName =&gt; {
      playClip(roomName, {
        file: url,
        volume: 20,
      });
    });
  });

  socket.on('play_text', data =&gt; {
    enumeratePlayers(roomName =&gt; {
      sayClip(roomName, data);
    });
  });

// ...

function playClip(roomName: string, data: PlayClip): void {
  const file = encodeURIComponent(data.file);
  const volume = encodeURIComponent(data.volume);
  fetch(`${process.env.SONOS_BRIDGE_URL}/${encodeURIComponent(roomName)}/clip/${file}/${volume}`);
}
</code></pre>
<p>It just waits for websocket messages and forwards them on to this <code>SONOS_BRIDGE_URL</code>. Which leads to...</p>
<h2>Sonos HTTP API</h2>
<p>That last bit of functionality is all defined at <a href="https://github.com/jishi/node-sonos-http-api">https://github.com/jishi/node-sonos-http-api</a>. It is where the magic actually happens. Sonos speakers (at the time of this writing and as far as I know) don't have any defined public API, but this open source project built one.</p>
<p>Meaning, via the magic of open source, we can programmatically play funny sound clips on Sonos speakers on the same network that our Sonos Proxy lives on.</p>
<h2>Wrapping Up</h2>
<p>And that's it! Over the last few years, we've added a multitude of other funny sound clips to it... then we'll go months without playing a single thing.</p>
<p>One actual useful thing from it is that we have a <code>slackbot</code> reminder in a channel that says <code>Reminder: @MrBurns say standup in 1 minute.</code> every morning at 8:29am... which means we get an audible cue to join our standups. That is super useful and, without fail, we'll miss it if we have a power outage or similar that takes the machine that our proxy runs on down.</p>
<p><img src="https://gist.github.com/user-attachments/assets/5fc0ff78-0f93-4a80-a038-d92f9c17b4fa" alt="say standup in 1 minute" /></p>
<p>It can be a lot of fun just being silly with technology - try it out sometime!</p>
<p>&lt;script type="module"&gt;
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
const themeName = document.querySelector('html').classList.contains('dark') ? 'dark' : 'default'
mermaid.initialize({ startOnLoad: true, theme: themeName });
&lt;/script&gt;</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[StirlingPDF PDF Editor]]></title>
            <link>https://mohundro.com/til/2025-04-30-stirlingpdf-editor/</link>
            <guid isPermaLink="false">https://mohundro.com/til/2025-04-30-stirlingpdf-editor/</guid>
            <pubDate>Wed, 30 Apr 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>See https://github.com/Stirling-Tools/Stirling-PDF. Saw it in relation to vert.sh File Converter.</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[vert.sh File Converter]]></title>
            <link>https://mohundro.com/til/2025-04-30-vertsh-file-converter/</link>
            <guid isPermaLink="false">https://mohundro.com/til/2025-04-30-vertsh-file-converter/</guid>
            <pubDate>Wed, 30 Apr 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>See https://vert.sh/</p>
<p>Can be self-hosted, but the hosted version is all in browser.</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[mani Git Tool to Manage Repos]]></title>
            <link>https://mohundro.com/til/2025-03-28-mani-git-tool/</link>
            <guid isPermaLink="false">https://mohundro.com/til/2025-03-28-mani-git-tool/</guid>
            <pubDate>Fri, 28 Mar 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>See https://manicli.com/.</p>
<p>I thought that <code>mani sync</code> would also synchronize all remotes, but it only clones them... if you want to fetch or similar, then you have to set up a task.</p>
<p>Example:</p>
<pre><code>tasks:
  git-status:
    desc: Show working tree status
    cmd: git status
</code></pre>
<p>Then to execute:</p>
<pre><code>mani run git-status --all
</code></pre>
<p>(you have to specify tags or <code>--all</code> or similar... it won't just run automatically against everything)</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[mise Version Manager]]></title>
            <link>https://mohundro.com/til/2025-03-28-mise-version-manager/</link>
            <guid isPermaLink="false">https://mohundro.com/til/2025-03-28-mise-version-manager/</guid>
            <pubDate>Fri, 28 Mar 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>See https://mise.jdx.dev/</p>
<p>It can be a replacement for asdf... it has far less config. Note that I don't know how to arrange the PATH with it as it can do that for you right now.</p>
<p>I'm not using it to its fullest yet, though.</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[sslip.io]]></title>
            <link>https://mohundro.com/til/2025-03-18-sslip-io/</link>
            <guid isPermaLink="false">https://mohundro.com/til/2025-03-18-sslip-io/</guid>
            <pubDate>Tue, 18 Mar 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>For anybody that has used xip.io or nip.io when you need a DNS service that lets you point at a specific IP address such as <code>192-168-0-120.nip.io</code> or, more often <code>127-0-0-1.nip.io</code> when you need to test that a <em>real</em> DNS lookup works against localhost (such as when testing DNS retry/failure scenarios): The owner of nip.io has passed away and the servers/domains will soon be expiring.</p>
<p><a href="https://sslip.io/">sslip.io</a> can be used as the next alternative.</p>
<p>The nip.io site said this:</p>
<blockquote>
<p>Stop editing your <code>etc/hosts</code> file with custom hostname and IP address mappings.</p>
</blockquote>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[xh Tips]]></title>
            <link>https://mohundro.com/til/2025-03-18-xh-tips/</link>
            <guid isPermaLink="false">https://mohundro.com/til/2025-03-18-xh-tips/</guid>
            <pubDate>Tue, 18 Mar 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>This shows how to use form posts... and it is a good example of how to call Azure AD (or a similar OAuth endpoint) to get a Bearer Token (given a Client ID and Client Secret).</p>
<pre><code>#!/usr/bin/env bash

identity_url="https://your-identity-endpoint.example.com"
scope="https://your-scope/.default"
client_id="YOUR_CLIENT_ID"
client_secret="YOUR_CLIENT_SECRET"

xh "${identity_url}/oauth2/v2.0/token" -f scope=$scope -f client_id=$client_id -f client_secret=$client_secret |
    jq -r .access_token
</code></pre>
<p>Then this is what it looks like to use that bearer token (see the <code>-A bearer</code> and <code>-a $bearer</code> parameters).</p>
<pre><code>#!/usr/bin/env bash

api_url="http://localhost:61020"
tenant_id="YOUR_TENANT_ID"
environment="TEST01"
setting_name="TestCustomScopes"

bearer="TOKEN_FROM_ABOVE"

xh $api_url/$tenant_id/v1/settings/$environment/$setting_name -A bearer -a $bearer
</code></pre>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[Adding the Current Weather to your Obsidian Templates]]></title>
            <link>https://mohundro.com/blog/2025-02-24-adding-weather-to-obsidian-template/</link>
            <guid isPermaLink="false">https://mohundro.com/blog/2025-02-24-adding-weather-to-obsidian-template/</guid>
            <pubDate>Mon, 24 Feb 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Last year, I started a renewed effort to use Obsidian and I've stuck with it - in fact, I haven't missed a day to get at least a short bullet journal post up....]]></description>
            <content:encoded><![CDATA[<p>Last year, I started a renewed effort to use Obsidian and I've stuck with it - in fact, I haven't missed a day to get at least a short bullet journal post up. I've slowly been tweaking my templates, though. Recently, I added the option to include the weather forecast to my files.</p>
<p>The key here is the excellent wttr.in project - see <a href="https://github.com/chubin/wttr.in">https://github.com/chubin/wttr.in</a>. A simple <code>curl wttr.in</code> is all it takes to use it. I've found that many widget based tools use it to grab the weather and include it... for example, you could use it to put it in your Terminal title or even your shell prompt.</p>
<p>For me, though, I really just wanted a short snippet in my bullet journals. When we're done, this is what it will output:</p>
<pre><code># ☀️ Weather

Clear 31(27) °F ☀️
</code></pre>
<p>I'll drop the snippet and then explain it below:</p>
<pre><code>const forecast = await fetch('https://wttr.in/Memphis?format=j1').then((res) =&gt; res.json())

const temp = forecast.current_condition[0].temp_F
const feelsLike = forecast.current_condition[0].FeelsLikeF
const description = forecast.current_condition[0].weatherDesc[0].value
const weatherCode = forecast.current_condition[0].weatherCode

// via https://github.com/chubin/wttr.in/blob/4d384f9efe727b28a595d4f502bcb9593fa19c99/lib/constants.py
const WWO_CODE = {
  113: 'Sunny',
  116: 'PartlyCloudy',
  119: 'Cloudy',
  122: 'VeryCloudy',
  143: 'Fog',
  176: 'LightShowers',
  179: 'LightSleetShowers',
  182: 'LightSleet',
  185: 'LightSleet',
  200: 'ThunderyShowers',
  227: 'LightSnow',
  230: 'HeavySnow',
  248: 'Fog',
  260: 'Fog',
  263: 'LightShowers',
  266: 'LightRain',
  281: 'LightSleet',
  284: 'LightSleet',
  293: 'LightRain',
  296: 'LightRain',
  299: 'HeavyShowers',
  302: 'HeavyRain',
  305: 'HeavyShowers',
  308: 'HeavyRain',
  311: 'LightSleet',
  314: 'LightSleet',
  317: 'LightSleet',
  320: 'LightSnow',
  323: 'LightSnowShowers',
  326: 'LightSnowShowers',
  329: 'HeavySnow',
  332: 'HeavySnow',
  335: 'HeavySnowShowers',
  338: 'HeavySnow',
  350: 'LightSleet',
  353: 'LightShowers',
  356: 'HeavyShowers',
  359: 'HeavyRain',
  362: 'LightSleetShowers',
  365: 'LightSleetShowers',
  368: 'LightSnowShowers',
  371: 'HeavySnowShowers',
  374: 'LightSleetShowers',
  377: 'LightSleet',
  386: 'ThunderyShowers',
  389: 'ThunderyHeavyRain',
  392: 'ThunderySnowShowers',
  395: 'HeavySnowShowers',
}

const WEATHER_SYMBOL = {
  Unknown: '✨',
  Cloudy: '☁️',
  Fog: '🌫',
  HeavyRain: '🌧',
  HeavyShowers: '🌧',
  HeavySnow: '❄️',
  HeavySnowShowers: '❄️',
  LightRain: '🌦',
  LightShowers: '🌦',
  LightSleet: '🌧',
  LightSleetShowers: '🌧',
  LightSnow: '🌨',
  LightSnowShowers: '🌨',
  PartlyCloudy: '⛅️',
  Sunny: '☀️',
  ThunderyHeavyRain: '🌩',
  ThunderyShowers: '⛈',
  ThunderySnowShowers: '⛈',
  VeryCloudy: '☁️',
}

const weatherLookupName = WWO_CODE[weatherCode] || 'Unknown'

const weatherIcon = WEATHER_SYMBOL[weatherLookupName] || WEATHER_SYMBOL.Unknown

const output = `${description} ${temp}(${feelsLike}) °F ${weatherIcon}`

tR += `# ${weatherIcon} Weather\n\n`
tR += output
</code></pre>
<p>First off, the following just grabs the weather info. Something that is super nice is that you can use modern JavaScript, though, so I'm using <code>await</code> here without any problem. I'm also explicitly specifying my location instead of relying on the IP. The <code>format=j1</code> returns the results in JSON format.</p>
<pre><code>const forecast = await fetch('https://wttr.in/Memphis?format=j1').then((res) =&gt; res.json())
</code></pre>
<p>Next, these lines pull out the relevant information that I'm looking for... pretty straighforward.</p>
<pre><code>const temp = forecast.current_condition[0].temp_F
const feelsLike = forecast.current_condition[0].FeelsLikeF
const description = forecast.current_condition[0].weatherDesc[0].value
const weatherCode = forecast.current_condition[0].weatherCode
</code></pre>
<p>The next lines are snipped a bit, but they're entirely for getting the emoji weather descriptor. The JSON returns the <code>WWO_CODE</code>, but I wanted to convert that to the emoji value. I couldn't find that in the JSON results, so I just went to the source and then dropped the JS version of it in here. So... I look up the <code>WWO_CODE</code>, then use it to index into the <code>WEATHER_SYMBOL</code> values.</p>
<pre><code>// via https://github.com/chubin/wttr.in/blob/4d384f9efe727b28a595d4f502bcb9593fa19c99/lib/constants.py
const WWO_CODE = {
  113: 'Sunny',
  // snipped...
  395: 'HeavySnowShowers',
}

const WEATHER_SYMBOL = {
  Unknown: '✨',
  // snipped...
  VeryCloudy: '☁️',
}

const weatherLookupName = WWO_CODE[weatherCode] || 'Unknown'

const weatherIcon = WEATHER_SYMBOL[weatherLookupName] || WEATHER_SYMBOL.Unknown
</code></pre>
<p>Finally, we just output the header and description. Again, I like that I can use string interpolation here. Modern JS!</p>
<pre><code>const output = `${description} ${temp}(${feelsLike}) °F ${weatherIcon}`

tR += `# ${weatherIcon} Weather\n\n`
tR += output
</code></pre>
<p>One note - this will be the weather <em>for the time the template is evaluated</em>. So, if you're going back and catching up on old posts, it won't do you any good as it is just for the day.</p>
<p>I've also included this in a Gist at <a href="https://gist.github.com/drmohundro/44a4e3c865a7250b0b128a55a6fc1415">https://gist.github.com/drmohundro/44a4e3c865a7250b0b128a55a6fc1415</a> if that's easier to remember.</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
        <item>
            <title><![CDATA[Git Repository for a Subdirectory]]></title>
            <link>https://mohundro.com/til/2025-01-14-git-repository-for-subdirectory/</link>
            <guid isPermaLink="false">https://mohundro.com/til/2025-01-14-git-repository-for-subdirectory/</guid>
            <pubDate>Tue, 14 Jan 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<pre><code>gh repo clone owner/repo
cd repo/
git sparse-checkout init --cone
git sparse-checkout set infrastructure
git co main
git remote remove origin
uv tool install git-filter-repo
git filter-repo --path infrastructure --force
</code></pre>
<p>See <a href="https://chatgpt.com/share/678694ef-0158-800f-b63c-1e0a141c410b">ChatGPT chat</a> for more context.</p>
]]></content:encoded>
            <author>david@mohundro.com (David Mohundro)</author>
        </item>
    </channel>
</rss>