<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>Dhruva's Blog</title><link>https://dhruvasambrani.github.io/blog</link><description>Dhruva's Blog, a collectionf of thoughts, guides and random shit</description><ttl>60</ttl><atom:link href="https://dhruvasambrani.github.io/blog/feed.xml" rel="self" type="application/rss+xml"/><item><pubDate>Fri, 20 Aug 2021 00:00:00 +0530</pubDate><title>My Calendar Setup</title><link>https://dhruvasambrani.github.io/blog/./2021-08-20_010631</link><description>I love TUIs and wanted to use calcurse everywhere</description><content:encoded><![CDATA[
<h1 id=my-calendar-setup>My calendar setup</h1><blockquote><p>Edit: No longer true, I now use <code>vdirsyncer</code>, <code>khal</code> and <code>khard</code>. Just read <a href=https://hunden.linuxkompis.se/2020/08/12/how-to-sync-and-manage-your-caldav-and-carddav-via-the-terminal.html>hund’s blog</a>. The calcurse syncing was such a big pain, and I started to selfhost radicale and so everything was pretty easy.</blockquote><h2 id=the-problem>The Problem</h2><p>I love TUIs. Being able to use just keys to do a lot of things without touching your mouse is pretty awesome. But I can’t constantly be on my laptop. I mean sure, with the pandemic, that’s all I’ve done, but <del>if</del> when it gets over, I’ll be on the move. I can’t turn on my laptop for every damn thing. I <em>need</em> to have the things I am working with on my phone too, especially things like the calendar. Now you get to read about the things I’ve tried to set up a TUI calendar which also syncs to my phone.<h2 id=on-my-laptop>On my laptop</h2><p>Conclusion: Calcurse. Beautiful.<p>I first tried <a href=https://linux.die.net/man/1/remind><code>remind</code></a> with <a href=https://github.com/haguenau/wyrd><code>wyrd</code></a>, but it just seemed too… much. It’s pretty cool, but it seems to have a very condensed UI, and I prefer more open UIs<p>Then I tried <a href=https://www.calcurse.org/><code>calcurse</code></a>. With a pretty light UI and sensible (read vim-like) key bindings, it’s super easy to get a hang of. Creating, editing and other stuff is pretty easy. Also, the notes feature is pretty awesome as it allows me to add pesky zoom links which have a tendency to get mixed up. Zoom should just give a random name to their meetings, like yellow-monkey-poop or something. It might even bring some joy into a dreary life of pointless Zoom meetings. Anywho, I pretty much stuck to calcurse.<p>But then I read about khal in <a href=https://hunden.linuxkompis.se/2020/08/12/how-to-sync-and-manage-your-caldav-and-carddav-via-the-terminal.html>hund’s blog</a> and decided to try it. The interface is a bit different, with way too many months shown at once, and the key bindings are weird too. It seemed like there were 3 layers to the UI- the month layer, the day layer and the event layer. Calcurse on the other hand uses a single layer with 3 parallel sections, the to-do, appointments and the month. Calcurse just looked better, but khal isn’t far behind.<h2 id=syncing-with-other-laptops>Syncing with other laptops</h2><p>Now comes the real issue. Syncing. Sure I could set up a CalDav server, but I don’t have a static IP, and if I go back to my institute, I will not be able to access my laptop. Also, my laptop has to always be up, which is not the case. Plus, implementing my own set up would be a pretty painful task.<p>I could use some online server, but I couldn’t find any services which were not self-hosting. Also, I wouldn’t be able to store the calcurse format, instead store an ical which I convert on opening calcurse, which seems dumb.<p>My solution was git. Git is device and OS-agnostic, and I already know how to use it. Plus I get a change history for free, though I assume that at some point the history will get too large, and I’ll have to squash previous commits, but that’s like a once in a year thing. Putting it on a remote server also ensures that it is backed up, so even if my beauty of a laptop dies, I’d still have my calendar.<p>You can make the calcurse directory a git repo and add <a href=https://calcurse.org/files/manual.html#_hooks>hooks</a> to commit and push in post-save and pull in pre-load. I used GitHub as the remote repo, but you could use anything of your choice. Now in another device, all I had to do was clone the calcurse directory, and I’d be done. Since literally everything from settings to hooks was in the same place, it would work exactly the same in all devices.<h2 id=calcurse-on-android>Calcurse on Android</h2><p>The final issue. Phone syncing.<p>My first attempt was to simply try to extend the same set up to android via <a href=https://termux.com/>termux</a>. While that worked, calcurse was pretty hopeless on such a tiny screen with a soft keyboard. TUIs are not made for touch screens. I tried wyrd and khal too, but they weren’t easy to use either.<p>I knew I could get the calcurse data onto the phone, it was the UI that was a problem. I briefly thought about making an app which could parse and display calcurse data, but then remembered the painful thing that is Android development. Plus I would basically have to rewrite the calcurse parser which itself is in constant development, which would mean my thing would have to be in constant development. No, thanks.<p>I then dabbled with exporting an ical file from calcurse on exit, which worked fine, and I was able to import it into my calendar(<a href=https://www.simplemobiletools.com/calendar/>Simple Calendar</a>) too. At first, it would import the data multiple times, but then I found out that adding <code>--export-uid</code> while exporting from calcurse prevented that from happening, so that was fine. But the major issue was that I would have to run the git pull myself and then open the ics file with the calendar. Then too, events which were deleted from calcurse wouldn’t be deleted from the calendar, as events would be imported and merged to the default calendar.<p>I needed a calendar app which would basically only display the ics file, that’s all. Not knowing any apps, I just typed <code>ics</code> into Aurora Droid and I found <a href=https://icsx5.bitfire.at/>ICSx⁵</a>. This was perfect. I could sync a CalDav server, and it would work perfectly.<p>But this cycled back to the first issue. I don’t want to set up CalDav. Then I thought of just trying to “sync” a URL which was just the link to an ics file. I used the <code>raw</code> link of the exported ics in my calcurse repo, and it worked!<p>Now, any time I edit the calendar on my laptop, it exports the calendar to an ics, commits it the repo, pushes to GitHub. From there, ICSx⁵ syncs the changes to my calendar when I refresh in the app.<p>Success!<h2 id=still-not-the-best>Still not the best</h2><p>This set up is still not perfect. I cannot make changes on my calendar app which propagate to the laptop. I have to fall back to the calcurse CLI or TUI to add an event via termux. It’s not a dealbreaker, especially since I don’t have too many updates anyway, but a person can dream.
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2021-08-20_010631</guid></item><item><pubDate>Fri, 20 Aug 2021 00:00:00 +0530</pubDate><title>Closer to Truth</title><link>https://dhruvasambrani.github.io/blog/./2021-08-20_010715</link><description>A fiction piece</description><content:encoded><![CDATA[
<h1 id=closer-to-truth>Closer to Truth</h1><p>I was going to the mall when I saw three people sitting on the bench. As I approached them they started running away. I was surprised by this. Pleasantly surprised. It was rare that people ran from me, they generally fainted. I don’t really blame them. How often do you see Death anyway?<p>Non-believers were generally the ones that reacted worse than the believers. I suppose that the believers already plan for this moment. I’ve really wondered though, is it that they are closer to the truth because they believe in Me or are they further away because they do so without any real evidence. I never leave evidence.<hr><p><strong>Newspaper article</strong><blockquote><p>Three people die as they ran into a speeding truck. The bus driver held for homicide. Hearing on…</blockquote><p>Life was convenient
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2021-08-20_010715</guid></item><item><pubDate>Sun, 29 Aug 2021 00:00:00 +0530</pubDate><title>My thoughts on Gemini</title><link>https://dhruvasambrani.github.io/blog/./2021-08-29_185046</link><description>Opinions about the 'new' web</description><content:encoded><![CDATA[
<h1 id=my-thoughts-on-gemini>My thoughts on Gemini</h1><p>As I scrolled through my posts on Mastodon, I came across <a href=https://tilde.team/~scientiac/>scientiac’s</a> <a href=gemini://gemini.ctrl-c.club/~scientiac/>Gemini site</a> and was intrigued by the concept of Gemini.<h2 id=a-bloated-web>A Bloated Web</h2><p>Today’s day websites are filled with lovely designs and transitions and animations and… ads. While there is a certain beauty in these websites, they tend to distract users from real content. See any news site for example, it is nearly impossible to visit these sites without having an AdBlock installed.<p>Some of these UI “enhancements” may be well-intentioned and may even improve User Experience, but they often come at a cost to accessibility. As web layouts get more complex, and with barely any standardization, those who use assistive devices such as screen readers find it harder to navigate websites.<p>Technically too, this has certain costs. Websites are styled with (S)CSS and JavaScript, which add a ton of functionality to the websites. So much so that a web browser can even be entire OS, like the chrome OS. But this extra content, which is “hidden” from a user needs to be downloaded from some server, which slows down load times and pushes up data usage.<p>From a security standpoint too, arbitrary JS, especially “closed source” JS can pose threats. In order to mitigate these threats, additional features like sandboxing need to be implemented which further increases the weight of the browser.<p>Finally, an increasingly bloated web experience leads to people having to buy newer hardware as the software gets more and more demanding. This leads to increased consumerism and e-Waste, which is a whole another problem that we have to solve. It would make sense if truly newer features which add to the user experience are introduced, but it seems like efforts are put more towards increasing the data collected from users or adding unnecessary UI transitions which reduce accessibility.<h2 id=the-solution-is-gemini>The Solution is Gemini</h2><p>Most of these issues can be solved by “simply” enforcing standardized accessibility features, and better optimizers which strip out unnecessary CSS, and educating web designers that fancy design is not necessarily good design. But enforcing is damn near impossible(which is a good thing, as any mechanism that can enforce design can also enforce content), finding unused CSS classes is a difficult process(someone should check if it is NP-Hard), and even if web designers make simple designs, a focus on fast deployment forces web-builders to use bloated frameworks.<p>Gemini has a completely different take on this issue. In order to solve the issue for a smaller subset of websites, especially those of the blog/microblog variety, news sites and text heavy sites, Gemini completely restricts the kind of content that can be displayed.<p>The only things that can be displayed are 3 levels of headings, unformatted text, pre-formatted text, and out of text links. Each site must load within one request of the server. No images or other multimedia can be displayed inline, but browsers can pass over the link to other media players for a perfectly normal multimedia viewing. This allows clients to implement just one thing, and do it properly (yay, the Unix way!). The syntax too is stupidly simple, with no tags, no complex parsing, nothing. Just pure unadulterated content.<p>This simple website can then be rendered with any font, any colors, any OS. Hell, you can just <code>curl</code>, <code>less</code> and <code>grep</code> your way through it. A lack of layouting implies text readers can easily do what they are designed to do — read text.<p>This simplicity warms my Vim-using, markdown loving heart.<h2 id=but-is-it>…but, is it?</h2><p>The Gemini protocol can be extensively minimalist, with people debating on whether favicons in the form of emojis should be implemented or not. While every site looking the same could be extremely useful, as my friend put it, is r/aBoringUtopia. It prevents web developers from expressing themselves via design. Sure, Unicode art can provide some relief, but it is still lacking. I would have preferred at least “pure” markdown level formatting. But a valid response to this can be found on the <a href=https://gemini.circumlunar.space/>Project Gemini website</a> -<blockquote><p>Gemini will not replace the web</blockquote><p>I don’t want an image sharing website on Gemini, though it could very well be implemented by calling external applications. I don’t want a banking solution on Gemini, because I would prefer a more secure implementation(but open, one can dream) on the web. But I sure as hell don’t want to see 5 transitions, autoscrolling, 3 sign up boxes, 4 newsletter boxes, 15 cookie policies, 10 TnCs and a million ads just so that I can be depressed by a news article highlighting the effects of global warming. Give me just content.<p>Content is king.
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2021-08-29_185046</guid></item><item><pubDate>Mon, 06 Sep 2021 00:00:00 +0530</pubDate><title>My blog on Gemini</title><link>https://dhruvasambrani.github.io/blog/./2021-09-06_010053</link><description>My weird setup to get my blog on Gemini</description><content:encoded><![CDATA[
<h1 id=my-blog-on-gemini>My blog on Gemini</h1><blockquote><p>Edit: See <a href=2022-05-12_181238>Another blog</a> for current setup</blockquote><p>My <a href=./2021-08-29_185046>previous post</a> was a brief intro and what I feel about the Gemini protocol. All said and done, I thought that it wouldn’t hurt to “expand” my blog to the Gemini space too. This blog is about what I had to do to get my blog to the Gemini space. The blog is at <a href=gemini://gemini.ctrl-c.club/~dhruva>gemini://gemini.ctrl-c.club/~dhruva</a><p>Tl;dr — LOL no, read the whole thing.<h2 id=server-and-hosting>Server and Hosting</h2><p>The first thing I needed was a server to host my Gemini capsule. For my HTTP site, I use GitHub pages, which is super easy to set up because I don’t need to manage my own server. But is there something similar for Gemini?<p>Turns out, it’s even better. Tildeverse is a universe of Tilde server instances. What’s a tilde server? It’s a public server which you can request for an account on, and you can just… use it. So I signed up for an account on the <a href=https://ctrl-c.club/>ctrl-c club</a> tilde instance. In about a day I received my ssh details, and I was up and running in about 15 mins.<h2 id=using-franklin>Using Franklin</h2><p>My first approach was the most straightforward one. Git clone my blog, install Julia, run the Franklin build step, then do something to the HTML so that it would output the gmi file. The problem with this however is that Julia and the .julia directories are HUGE. My disk usage quickly hit the soft limit of 1Gb, and I really did not want to exploit the courtesy of the ctrl-c club.<p>I could try and attempt to only parse the markdown posts to gmi, but then all the Franklin specific content would have to be reparsed. I definitely don’t want to reimplement Franklin.jl. What then?<h2 id=scraping-the-blog>Scraping the blog</h2><p>So the constraint is that I cannot build the HTML files on the tilde server, but I need to build it before I can convert it to Gemini files. This catch-22 situation can only be solved by putting the Franklin build step on some other device, and only do the HTML → Gemini part of it on the tilde server.<p>Why not just get the rendered HTML directly from the website and convert it? But for this I would need to find the correct locations. Well, thank god for <code>sitemap.xml</code>.<p>So I curl the <code>sitemap.xml</code>, grep for “posts/”, sed out the useless parts, and for each link, download and convert. Voilà, one script to sync the Gemini server with the HTML blog!<h2 id=some-html-cleanup>Some HTML cleanup</h2><p>While the previous solution works from a technical point of view, the end result is… illegible. The problem is that the HTML page has a lot of links and layout which any HTML → gmi converter attempts to convert, leading to a lot of noise. I needed to only convert the content part of the page, not the entire thing.<p>However, the content <code>div</code> only has a class which differentiates it from the rest of the <code>div</code>s. While regexing the start of the content is easy, finding the closing <code>div</code> tag is pretty difficult with regex(technically impossible, as XML is a CFG, and HTML isn’t even that, but it can be done with a little extended regex and prayers to god.). I would need an entire HTML parser from some language, and write a proper program which would parse the HTML to get me the right <code>div</code>. This was again going into the territory of too much work.<p>All I needed was a way to demarcate the content. I could do that with comments, but those would be removed on build. So the trick I came up with was to add an extra <code>contentthing</code> tag where the content would be in the build templates. Since <code>contentthing</code> is not defined by anyone at all, no browser should render the page any differently than before. Writing a regex to find the content inside was now pretty easy. The irony however is that Gemini was made to get rid of non-standard HTML.<p>Another tiny issue was that Franklin added an extra <code>p</code> block inside a <code>blockquote</code>. This I fixed with a sed replace.<h2 id=the-htmlgmi-script>The HTML→gmi script</h2><p>This was pretty straightforward. <a href=https://github.com/LukeEmmet/html2gmi>html2gmi</a> is a go script which does the job very well.<h2 id=index.gmi>index.gmi</h2><p>While the posts were nicely converted, there was no way that the blog index page could be translated directly. I anyway needed an index page which was unique to the Gemini capsule. This I did by writing a template index.gmi which a python script fills with the posts. Now my capsule is complete.<h2 id=conclusion>Conclusion</h2><p>In the end, the final script is this -<div class=sourceCode id=cb1><pre class="sourceCode sh"><code class="sourceCode bash"><span id=cb1-1><a href=#cb1-1 aria-hidden=true tabindex=-1></a><span class=co>#! /bin/bash</span></span>
<span id=cb1-2><a href=#cb1-2 aria-hidden=true tabindex=-1></a><span class=fu>myf()</span> <span class=kw>{</span></span>
<span id=cb1-3><a href=#cb1-3 aria-hidden=true tabindex=-1></a>    <span class=va>out</span><span class=op>=</span><span class=va>$(</span><span class=fu>basename</span> <span class=va>$(</span><span class=fu>dirname</span> <span class=va>$1))</span>.gmi</span>
<span id=cb1-4><a href=#cb1-4 aria-hidden=true tabindex=-1></a>    <span class=bu>echo</span> <span class=va>$1</span></span>
<span id=cb1-5><a href=#cb1-5 aria-hidden=true tabindex=-1></a>    <span class=ex>curl</span> <span class=at>-sL</span> <span class=va>$1</span> <span class=kw>|</span></span>
<span id=cb1-6><a href=#cb1-6 aria-hidden=true tabindex=-1></a>        <span class=fu>sed</span> <span class=at>-E</span> <span class=st>&quot;s/(.*&lt;contentthing&gt;|&lt;\/contentthing&gt;.*)//g&quot;</span> <span class=kw>|</span></span>
<span id=cb1-7><a href=#cb1-7 aria-hidden=true tabindex=-1></a>        <span class=fu>sed</span> <span class=at>-E</span> <span class=st>&quot;s/&lt;blockquote&gt; &lt;p&gt;/&lt;blockquote&gt;/g&quot;</span> <span class=kw>|</span></span>
<span id=cb1-8><a href=#cb1-8 aria-hidden=true tabindex=-1></a>        <span class=fu>sed</span> <span class=at>-E</span> <span class=st>&quot;s/&lt;\/p&gt; &lt;\/blockquote&gt;/&lt;\/blockquote&gt;/g&quot;</span> <span class=kw>|</span></span>
<span id=cb1-9><a href=#cb1-9 aria-hidden=true tabindex=-1></a>        <span class=va>$HOME</span><span class=ex>/go/bin/html2gmi</span> <span class=at>-met</span> <span class=at>-l</span> 1 <span class=at>-o</span> <span class=va>$HOME</span>/public_gemini/<span class=va>$out</span></span>
<span id=cb1-10><a href=#cb1-10 aria-hidden=true tabindex=-1></a><span class=kw>}</span></span>
<span id=cb1-11><a href=#cb1-11 aria-hidden=true tabindex=-1></a><span class=bu>export</span> <span class=at>-f</span> <span class=va>myf</span></span>
<span id=cb1-12><a href=#cb1-12 aria-hidden=true tabindex=-1></a><span class=fu>rm</span> <span class=at>-rf</span> <span class=va>$HOME</span>/public_gemini/<span class=pp>*</span></span>
<span id=cb1-13><a href=#cb1-13 aria-hidden=true tabindex=-1></a><span class=ex>curl</span> <span class=at>-s</span> https://dhruvasambrani.github.io/blog/sitemap.xml <span class=at>--</span> <span class=kw>|</span> <span class=fu>grep</span> .<span class=pp>*</span>/posts/2.<span class=pp>*</span> <span class=kw>|</span> <span class=fu>sed</span> <span class=at>-E</span> <span class=st>&quot;s/([ ]*&lt;\/*loc&gt;)//g&quot;</span> <span class=kw>|</span> <span class=fu>xargs</span> <span class=at>-I{}</span> <span class=at>--</span> bash <span class=at>-c</span> <span class=st>&#39;myf &quot;{}&quot;&#39;</span></span>
<span id=cb1-14><a href=#cb1-14 aria-hidden=true tabindex=-1></a><span class=ex>python3</span> makeindex.py</span></code></pre></div><p>where <code>makeindex.py</code> is<div class=sourceCode id=cb2><pre class="sourceCode python"><code class="sourceCode python"><span id=cb2-1><a href=#cb2-1 aria-hidden=true tabindex=-1></a><span class=im>import</span> os</span>
<span id=cb2-2><a href=#cb2-2 aria-hidden=true tabindex=-1></a><span class=kw>def</span> listposts():</span>
<span id=cb2-3><a href=#cb2-3 aria-hidden=true tabindex=-1></a>    files <span class=op>=</span> os.listdir(<span class=st>&quot;../public_gemini/&quot;</span>)</span>
<span id=cb2-4><a href=#cb2-4 aria-hidden=true tabindex=-1></a>    files.sort()</span>
<span id=cb2-5><a href=#cb2-5 aria-hidden=true tabindex=-1></a>    <span class=cf>return</span>(<span class=st>&quot;</span><span class=ch>\n</span><span class=st>&quot;</span>.join([<span class=st>&quot;=&gt; &quot;</span><span class=op>+</span>_file <span class=cf>for</span> _file <span class=kw>in</span> files]))</span>
<span id=cb2-6><a href=#cb2-6 aria-hidden=true tabindex=-1></a><span class=cf>with</span> <span class=bu>open</span>(<span class=st>&quot;template.gmi&quot;</span>) <span class=im>as</span> f:</span>
<span id=cb2-7><a href=#cb2-7 aria-hidden=true tabindex=-1></a>    s <span class=op>=</span> f.read()</span>
<span id=cb2-8><a href=#cb2-8 aria-hidden=true tabindex=-1></a>    final <span class=op>=</span> s.replace(<span class=st>&quot;</span><span class=sc>{{</span><span class=st> listposts </span><span class=sc>}}</span><span class=st>&quot;</span>, listposts())</span>
<span id=cb2-9><a href=#cb2-9 aria-hidden=true tabindex=-1></a>    <span class=cf>with</span> <span class=bu>open</span>(<span class=st>&quot;../public_gemini/index.gmi&quot;</span>, <span class=st>&quot;w&quot;</span>) <span class=im>as</span> out:</span>
<span id=cb2-10><a href=#cb2-10 aria-hidden=true tabindex=-1></a>        out.write(final)</span></code></pre></div><p>By simply running <code>. makegemini.sh</code>, I can recreate my blog entirely in Gemini.<h2 id=things-i-can-improve>Things I can improve</h2><p>Definitely, there are still some things I can improve<ol type=1><li>The posts listed in the Gemini only have the filename and not the post title. This makes it hard for viewers to find a particular post<li>Post pages do not link back to the index page<li>Internal links may not work<li>HTML → Gemini isn’t the best, because scripts attempt to retain content while ignoring formatting, where in some places, it may be better to just leave the content on the whole.</ol><p>But on the whole, I’m pleased with the present output.
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2021-09-06_010053</guid></item><item><pubDate>Fri, 29 Apr 2022 00:00:00 +0530</pubDate><title>Science articles everyday</title><link>https://dhruvasambrani.github.io/blog/./2022-04-29_150432</link><description>How I consume Science on a daily basis, why you should too, and fangirling about RSS</description><content:encoded><![CDATA[
<h1 id=science-articles-everyday>Science Articles Everyday</h1><p>I like science. I am a science student writing my masters thesis, and I need to read new articles related to my subject (Quantum Computing) everyday.<p>Even otherwise, I want to be somewhat up to date about things happening. Why? Because a couple of years ago, I got drawn so much into my other hobby of coding and tech that I started to lose touch with my love for Science. Much like how I lost in touch with my preference for music when I was younger. You need a constantexposure to what you like, and only then will you keep liking it.<h2 id=what-to-read>What to read</h2><p>There is a whole ocean of sites to read from. I prefer to stay away form the pop-sci kind of articles and youtube channels because it gives you a false sense of the topic, and as someone who knows a substantial amount of math and science, I prefer something with more rigor.<p>Cue Arxiv papers and Nature articles. There’s also Science, AIP and a tonne of other journals, but Nature has kept me happy. Viewing arxiv papers for my topics keeps me updated about the newest things and also sometimes pushes me into a new direction with my research. Nature and Science journals on the other hand give a more overview of things happening in the science world, and also introduce larger ideas and discussions related to academia in general.<h2 id=how-to-read>How to read</h2><p>Once I selected the sites I wanted to follow, I tried following them via their websites, but this is painfully hard. I then learnt about <a href=https://en.wikipedia.org/wiki/RSS>RSS</a> and was hooked. You simply have to copy the link of the feed you want to follow into your reader application and you get new articles everytime they update. No accounts, no changing apps, and no shady web ads. Just plain content, which is the best thing anyone can ask for.<p>So I added <a href=http://feeds.nature.com/nature/rss/current>Nature’s RSS feed link</a> which I got by just searching <code>Nature rss feeds</code>. Arxiv’s rss feeds are much more advanced and nuanced. Basically, it is of the form <code>https://export.arxiv.org/rss/topic.subtopic</code>, where the topic and subtopic list can be found <a href=https://arxiv.org/>here</a>.<p>I added these two to my feed readers, which are <a href=https://newsboat.org/>newsboat</a> for desktop and <a href=https://hyliu.me/fluent-reader/>Fluent Reader</a> for Android. IIRC, Fluent Reader is also available on iOS. That said there’s a plethora of other readers you can choose from.<p>And voila! Science in my feed.<h1 id=caveats>Caveats</h1><p>None, apart from when the pressure to read the articles. Most of the articles, I just read the title and decide to read it completely if I like it.
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2022-04-29_150432</guid></item><item><pubDate>Sun, 01 May 2022 00:00:00 +0530</pubDate><title>My Todo Manager</title><link>https://dhruvasambrani.github.io/blog/./2022-05-01_164311</link><description>Making a Suckless Todo manager which also syncs with calcurse</description><content:encoded><![CDATA[
<h1 id=my-todo-manager>My Todo Manager</h1><h1 id=calcurse>Calcurse</h1><p>In my previous post about <a href=./2021-08-20_010631>my Calender Setup</a>, I mentioned about how I was using Calcurse as my calender. Now calcurse also has a Todo section which works great, except that I have to open calcurse to use it. I just want to quickly see what I need to do, press enter to say I am done with it and it should be gone. Adding an entry should be just as easy<h1 id=enter-dmenu>Enter dmenu</h1><p><code>dmenu</code> is a suckless menu selector which has fuzzy search (patch), default text (patch) and autocomplete. This is everything I need. So I made a script to use dmenu as my So I simply read the calcurse todo file and display a menu with each entry in the todo as a dmenu entry.<p>When user inputs, I check if it matches any previous entry. If it does, then I remove the entry from the todo file.<p>When the user input does not match any entry, it is assumed to be a new entry, and added to the file.<h1 id=rounding-off-edges>Rounding off edges</h1><p>This solution has 2 problems,<ol type=1><li>Calcurse uses a numbered ranking to signify the urgency of the entry. But adding a new entry at the default urgency is hard to type: <code>[0] Something I have to do</code>. So if the new entry input doesn’t start with <code>\[.\]</code>, the script just adds <code>[0]</code><li>There is no way to edit an entry. So I changed the script to put back the last deleted entry into the prompt. So an <code>edit</code> is actually a delete followed by a re-entry. If you really wanted to delete an entry, then just hit <code>ctrl-u</code> to clear the line.</ol><h1 id=tying-it-all-up-together>Tying it all up together</h1><p>I added a keybind to dwm to run my script, and everything works so beautifully. Now I can go and remove the todo entry to write this post.<h1 id=final-script>Final script</h1><p>Here is the final script-<div class=sourceCode id=cb1><pre class="sourceCode bash"><code class="sourceCode bash"><span id=cb1-1><a href=#cb1-1 aria-hidden=true tabindex=-1></a><span class=co>#!/bin/zsh</span></span>
<span id=cb1-2><a href=#cb1-2 aria-hidden=true tabindex=-1></a><span class=co>#</span></span>
<span id=cb1-3><a href=#cb1-3 aria-hidden=true tabindex=-1></a><span class=co># Write/remove a task to do later.</span></span>
<span id=cb1-4><a href=#cb1-4 aria-hidden=true tabindex=-1></a><span class=co>#</span></span>
<span id=cb1-5><a href=#cb1-5 aria-hidden=true tabindex=-1></a><span class=co># Select an existing entry to remove it from the file, or type a new entry to</span></span>
<span id=cb1-6><a href=#cb1-6 aria-hidden=true tabindex=-1></a><span class=co># add it.</span></span>
<span id=cb1-7><a href=#cb1-7 aria-hidden=true tabindex=-1></a><span class=co>#</span></span>
<span id=cb1-8><a href=#cb1-8 aria-hidden=true tabindex=-1></a></span>
<span id=cb1-9><a href=#cb1-9 aria-hidden=true tabindex=-1></a><span class=va>file</span><span class=op>=</span><span class=st>&quot;</span><span class=va>$HOME</span><span class=st>/.calcurse/todo&quot;</span></span>
<span id=cb1-10><a href=#cb1-10 aria-hidden=true tabindex=-1></a><span class=fu>touch</span> <span class=st>&quot;</span><span class=va>$file</span><span class=st>&quot;</span></span>
<span id=cb1-11><a href=#cb1-11 aria-hidden=true tabindex=-1></a><span class=va>height</span><span class=op>=</span><span class=va>$(</span><span class=fu>wc</span> <span class=at>-l</span> <span class=st>&quot;</span><span class=va>$file</span><span class=st>&quot;</span> <span class=kw>|</span> <span class=fu>awk</span> <span class=st>&#39;{print $1}&#39;</span><span class=va>)</span></span>
<span id=cb1-12><a href=#cb1-12 aria-hidden=true tabindex=-1></a><span class=va>prompt</span><span class=op>=</span><span class=st>&quot;Add/delete a task: &quot;</span></span>
<span id=cb1-13><a href=#cb1-13 aria-hidden=true tabindex=-1></a></span>
<span id=cb1-14><a href=#cb1-14 aria-hidden=true tabindex=-1></a><span class=va>cmd</span><span class=op>=</span><span class=va>$(</span><span class=fu>sort</span> <span class=at>-r</span> <span class=st>&quot;</span><span class=va>$file</span><span class=st>&quot;</span> <span class=kw>|</span> <span class=ex>dmenu</span> <span class=at>-l</span> <span class=st>&quot;</span><span class=va>$height</span><span class=st>&quot;</span> <span class=at>-p</span> <span class=st>&quot;</span><span class=va>$prompt</span><span class=st>&quot;</span> <span class=at>-i</span> <span class=va>)</span></span>
<span id=cb1-15><a href=#cb1-15 aria-hidden=true tabindex=-1></a><span class=cf>while</span> <span class=bu>[</span> <span class=ot>-n</span> <span class=st>&quot;</span><span class=va>$cmd</span><span class=st>&quot;</span> <span class=bu>]</span><span class=kw>;</span> <span class=cf>do</span></span>
<span id=cb1-16><a href=#cb1-16 aria-hidden=true tabindex=-1></a>  <span class=cf>if</span> <span class=fu>grep</span> <span class=at>-qF</span> <span class=st>&quot;</span><span class=va>$cmd</span><span class=st>&quot;</span> <span class=st>&quot;</span><span class=va>$file</span><span class=st>&quot;</span><span class=kw>;</span> <span class=cf>then</span></span>
<span id=cb1-17><a href=#cb1-17 aria-hidden=true tabindex=-1></a>        <span class=fu>grep</span> <span class=at>-vF</span> <span class=st>&quot;</span><span class=va>$cmd</span><span class=st>&quot;</span> <span class=st>&quot;</span><span class=va>$file</span><span class=st>&quot;</span> <span class=op>&gt;</span> <span class=st>&quot;</span><span class=va>$file</span><span class=st>.new&quot;</span> <span class=co># This selects all the lines that DONT match</span></span>
<span id=cb1-18><a href=#cb1-18 aria-hidden=true tabindex=-1></a>        <span class=fu>mv</span> <span class=st>&quot;</span><span class=va>$file</span><span class=st>.new&quot;</span> <span class=st>&quot;</span><span class=va>$file</span><span class=st>&quot;</span></span>
<span id=cb1-19><a href=#cb1-19 aria-hidden=true tabindex=-1></a>    <span class=kw>((</span> <span class=va>height</span> <span class=op>=</span> <span class=va>height</span> <span class=op>-</span> <span class=dv>1</span> <span class=kw>))</span></span>
<span id=cb1-20><a href=#cb1-20 aria-hidden=true tabindex=-1></a>    <span class=cf>else</span></span>
<span id=cb1-21><a href=#cb1-21 aria-hidden=true tabindex=-1></a>    <span class=cf>if</span> <span class=kw>[[</span> <span class=st>&quot;</span><span class=va>$(</span><span class=bu>echo</span> <span class=va>$cmd</span> <span class=kw>|</span> <span class=fu>tr</span> <span class=at>-d</span> <span class=dt>\\</span>n<span class=va>)</span><span class=st>&quot;</span> <span class=ot>=~</span> <span class=st>&quot;^\[[[:digit:]]\]&quot;</span> <span class=kw>]];</span> <span class=cf>then</span></span>
<span id=cb1-22><a href=#cb1-22 aria-hidden=true tabindex=-1></a>      <span class=bu>echo</span> <span class=st>&quot;</span><span class=va>$cmd</span><span class=st>&quot;</span> <span class=op>&gt;&gt;</span> <span class=st>&quot;</span><span class=va>$file</span><span class=st>&quot;</span></span>
<span id=cb1-23><a href=#cb1-23 aria-hidden=true tabindex=-1></a>    <span class=cf>else</span></span>
<span id=cb1-24><a href=#cb1-24 aria-hidden=true tabindex=-1></a>      <span class=bu>echo</span> <span class=st>&quot;[0] </span><span class=va>$cmd</span><span class=st>&quot;</span> <span class=op>&gt;&gt;</span> <span class=st>&quot;</span><span class=va>$file</span><span class=st>&quot;</span></span>
<span id=cb1-25><a href=#cb1-25 aria-hidden=true tabindex=-1></a>    <span class=cf>fi</span></span>
<span id=cb1-26><a href=#cb1-26 aria-hidden=true tabindex=-1></a>    <span class=va>cmd</span><span class=op>=</span><span class=st>&quot;&quot;</span></span>
<span id=cb1-27><a href=#cb1-27 aria-hidden=true tabindex=-1></a>    <span class=kw>((</span> <span class=va>height</span> <span class=op>=</span> <span class=va>height</span> <span class=op>+</span> <span class=dv>1</span> <span class=kw>))</span></span>
<span id=cb1-28><a href=#cb1-28 aria-hidden=true tabindex=-1></a>    <span class=cf>fi</span></span>
<span id=cb1-29><a href=#cb1-29 aria-hidden=true tabindex=-1></a></span>
<span id=cb1-30><a href=#cb1-30 aria-hidden=true tabindex=-1></a>  <span class=va>cmd</span><span class=op>=</span><span class=va>$(</span><span class=fu>sort</span> <span class=at>-r</span> <span class=st>&quot;</span><span class=va>$file</span><span class=st>&quot;</span> <span class=kw>|</span> <span class=ex>dmenu</span> <span class=at>-l</span> <span class=st>&quot;</span><span class=va>$height</span><span class=st>&quot;</span> <span class=at>-p</span> <span class=st>&quot;</span><span class=va>$prompt</span><span class=st>&quot;</span> <span class=at>-it</span> <span class=st>&quot;</span><span class=va>$cmd</span><span class=st>&quot;</span> <span class=st>&quot;</span><span class=va>$@</span><span class=st>&quot;</span> <span class=va>)</span></span>
<span id=cb1-31><a href=#cb1-31 aria-hidden=true tabindex=-1></a><span class=cf>done</span></span>
<span id=cb1-32><a href=#cb1-32 aria-hidden=true tabindex=-1></a></span>
<span id=cb1-33><a href=#cb1-33 aria-hidden=true tabindex=-1></a><span class=bu>exit</span> 0</span></code></pre></div>
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2022-05-01_164311</guid></item><item><pubDate>Thu, 12 May 2022 00:00:00 +0530</pubDate><title>Another blog</title><link>https://dhruvasambrani.github.io/blog/./2022-05-12_181238</link><description>I revamped my blog again instead of actually writing posts. Oh well.</description><content:encoded><![CDATA[
<h1 id=another-blog>Another blog</h1><p>I revamped my blog again instead of actually writing posts. Oh well.<h2 id=why>Why</h2><p>I wanted a minimalistically cool blog like <a href=https://erzadel.net/><span class=citation data-cites=edel>@edel</span>’s</a> and <a href=https://joelchrono12.xyz/><span class=citation data-cites=joel>@joel</span>’s</a> websites.<p>My previous blog setup was made using <a href=https://franklinjl.org/>Franklin.jl</a>, and building it implied setting up an entire julia installation. And even after all that, the blog looked pretty boring. I still struggled to set up tags and I had to constantly fight the default css or write everything myself. Also, when I built that blog, I was learning JS, and I used a lot of unnecessary js and styling. Finally, Franklin’s complex build meant that <a href=./2021-09-06_010053>my gemini build</a> was so much more harder to make.<h2 id=enter-pandoc>Enter pandoc</h2><p>I still wanted to write in markdown and make it to html. The best converter I know is pandoc. It has a very good template support, and having used it before, I was comfortable with it. I had already made a <a href=https://foambubble.github.io/foam/>foam</a> <a href=https://github.com/DhruvaSambrani/notes-template>template</a> for jekyll for my <a href=https://dhruvasambrani.github.io/notes>notes this semester</a>, and modifying it to match pandoc’s template was easy.<h2 id=gemini-build>Gemini Build</h2><p>The best part of pandoc is that it is so easily extendable. There is a gemtext filter for pandoc which meant that with just one more command I could also build my gemini capsule. I had to tweak the filter a bit to add <code>.gmi</code> to links where necessary, which was technically a simple change, but I don’t know lua, so I flailed around a bit before I got it. Templating for gemini was even easier since there isn’t much you can do in the first place.<h2 id=table-of-contents>Table of Contents</h2><p>I initially thought I would make the blog like a digital garden, but then decided against since I knew I wouldn’t write too much to actually populate the garden enough. And setting up such a thing I felt was way too complicated for a low traffic blog like mine.<p>So I ended making a Table of Contents. Previously, I just had the entry as the filename, but that was very difficult to follow. With a little of <code>sed</code>-fu, I extracted the title from the front matter. With a tiny change, I made it work for gemini too.<p>At this point, I was going to release the new blog, but then I had a new itch<h2 id=rss>RSS</h2><p>I wanted to also have an RSS feed. I already use RSS extensively for comics, <a href=./2022-04-29_150432>science</a> and other blogs, and wanted people to see mine too. I searched online a bit for an RSS maker script, but I only found subscription stuff. I was almost going to write my own script, when I came across a post on fosstodon which talked about an RSS-only blog. They used txt files to manage their content, but converting markdown to html was already possible with pandoc, so with some heavy modifications, I built the RSS feed with a script<h2 id=zk>ZK</h2><p>At this point, I was ALMOST going to publish it. I had made everything I wanted to. Just a few more CSS fixes, and we were good to go. But as I was setting up markdown support for lunarvim, I came across <a href=https://github.com/mickael-menu/zk>zk</a>. And this blew my mind. Without me doing anything, it gave me everything I needed. Contents were just one command away, and for almost free, I got tag support.<p>Within a day, I set up zk, added tags to my previous articles, added a new template for the tags page, and fixed some other templates. I also added a cute neon effect to the tag list at the end of every page. I found a small mistake in the gemini script, which I fixed.<p>Now, my blog is perfect.<p>Except…<h2 id=fediring-and-512kb-club>Fediring and 512KB club</h2><p>I really want to add my blog to both of these. For 512KB, I don’t know whether it is per page or per site. Right now, my site sits at 131KB for the whole thing, but that is with 8 posts. Every post I add, I add about 1KB. I don’t know how people are getting their sites to less than 100KB. That seems impossible.<p>As for fediring, it is just a matter of doing it, which I haven’t done.
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2022-05-12_181238</guid></item><item><pubDate>Fri, 09 Sep 2022 00:00:00 +0530</pubDate><title>About self hosting</title><link>https://dhruvasambrani.github.io/blog/./2022-09-09_131905</link><description>My journey in self hosting</description><content:encoded><![CDATA[
<h1 id=self-hosting>Self hosting</h1><blockquote><p>Edit: This “opinion” was a little premature, after playing around with docker a bit more and moving my “server” to my home network, I really see the point of self hosting. For me, its mainly its getting services for free, and having full control over where the data is. Its also pretty cool now that my girlfriend is also on the tailscale network, and she gives me a lot of validation when she gets involved in what I’m self hosting. See <a href=2025-04-21_200237>Self Hosting without a public IP address or domain</a> and <a href=2025-04-22_155445>Currently selfhosted services</a>.</blockquote><p>I was interested in self hosting from forever, but having to pay for a static IP and not having an extra computer to keep always on were hurdles that prevented me from trying anything. But that all changed when my university decided to install LAN ports in each room, allowing me to connect more than one device to the network. I brought with me an old laptop (Acer V or something), which no one was using.<h2 id=gitea-and-tailscale>Gitea and Tailscale</h2><p>After playing around a bit with installing arch and trying to use it in a dual screen setup with <a href=https://github.com/debauchee/barrier>barrier</a>, I finally got down to self hosting things. I first started with gitea, which seemed simple enough to install. I followed the arch wiki instructions, and was running gitea in about an hour on localhost. I already new how to port forward on the router so I set that up and I could connect as long as I was on the institute network.<p>There was still the issue of a lack of a public static IP. I turned to the internet for ideas, and put a post on r/selfhosted about the issue. I was directed towards <a href=https://tailscale.com/>Tailscale</a>, which took barely half an hour to install. Over the next few days I played around with tailscale and found the MagicDNS and AutoHTTPS settings. But now I had an issue caused by my institute proxy server. With the new domain names, the DNS resolver at the proxy server has to add the tailscale dns resolvers, but I don’t have access to it. Which meant that while my phone happily connected to the server over Cellular, my laptop couldn’t connect despite being on the same LAN without falling back to the local IP address. Oh well, at least a stopgap solution existed.<h2 id=nextcloud>Nextcloud</h2><p>Nextcloud was the next thing I wanted to selfhost. This was a big pain to get running. I was under the impression that all apps had their inbuilt web servers, and I just needed to set the correct ports to get them working. But nextcloud being a php application, needed a php webserver to be running first. This took me a while to understand. After this, I had issues with running the same postgres server for gitea and nextcloud. Not being too well versed with DBMS concepts, there were some weird issues with conflicting user permissions and stuff.<h2 id=docker>Docker</h2><p>At this point I gave up trying to run nextcloud on bare metal. Nearly all docs relate to running nextcloud in a docker container, but I had no idea what docker was. It took me a couple of weeks trying to get nextcloud to run from docker and understand what needed to be changed so it would run properly. Eventually I got it running on some port.<h2 id=caddy-and-reverse-proxy>Caddy and reverse proxy</h2><p>Once again, this was easy for gitea, because gitea has a clear example of gitea behind a caddy reverse proxy, and when I hadn’t set the right hostname in gitea’s config, it gave me a nice banner saying I had to change so and so variable at so and so location. Nice and clean. Now came nextcloud. I couldn’t for the life of me figure out where the config file was, because the docker container’s volume was in the default location in <code>/var/lib/docker/volumes</code>, and I had no idea it was there. Finally after some poking around, I changed the config, but to no avail. I kept having issue after issue with nextcloud. So I clean restarted the whole process, uninstalled postgres, gitea, docker, caddy, cleaned files, and put everything except caddy on docker. Once again I faced issues with the webroot on nextcloud and I kind of gave up for a while.<h2 id=another-attempt>Another attempt</h2><p>After a few weeks’ hiatus caused by a new semester, I tried setting it up again. I don’t know if it was just plain luck or the break had given some clarity of what had to be done, but this time I was able to set up everything pretty easily. Everything was working barring tailscale on LAN, which now seemed like the proxy server blocking the connection, along with the DNS issue.<h2 id=nextcloud-office-with-collabora>Nextcloud Office with Collabora</h2><p>Now I wanted to add Nextcloud office, but I was unsuccessful in getting the nextcloud instance to see the collabora server running in the same docker-compose file regardless of what I tried. Eventually I gave up.<h2 id=other-services>Other services</h2><p>I also have a docker running freshrss, but I haven’t set it up, but it seems pretty easy to set up. I’ll also probably try to set up hedgedocs as an alternative to nextcloud office, and then see if I can connect the nextcloud storage to hedgedocs, but I haven’t tried it yet.<h2 id=opinions-about-self-hosting>Opinions about self-hosting</h2><p>Self hosting isn’t easy, and I don’t see the point of it. It makes sense for a group or something like a mastodon or tilde instance to self host certain services for a more decentralized internet, but the effort of setting it up for personal access is not worth it.<p>For all editors or viewers, there are tonnes of local applications, and it makes no sense to use a server set up for it. The only point I see is as a backup solution or as a file sharing mechanism. For backup and sync, I feel syncthing is a lot easier to setup and get working. Sure, the connection between devices is a bit finicky sometimes, but its frankly alright. Syncthing + Tailscale satisfies whatever classic Nextcloud can do.<p>For those apps which need syncing of data between devices, it is a lot easier to look for public instances of them. The hostux network [hostux.network] provides a lot of services for free, and trusting them is about the same as trusting a droplet server.<p>As for Nextcloud, it now offers to set up a server for you on one of the VPS providers for free, which I think is a pretty great deal. For my music and photos, I sync using syncthing, and for documents, contacts, calendar, tasks, I use the nextcloud instance.<p>So with that, I don’t know how much longer I’ll try to get the self hosting thing working, but there you have it.
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2022-09-09_131905</guid></item><item><pubDate>Mon, 24 Apr 2023 00:00:00 +0530</pubDate><title>Why so much Alcohol</title><link>https://dhruvasambrani.github.io/blog/./2023-04-24_050254</link><description>A short piece questioning why we drink alcohol</description><content:encoded><![CDATA[
<h1 id=why-so-much-alcohol>Why so much Alcohol</h1><p>It was my girlfriend’s birthday recently, and I was tasked with organising the event. This generally means that I navigate the complex structures of the birthday girl’s social life and invite the right set of people so that no one is offended, but also no one feels uncomfortable in the party. That, and order the food and alcohol. Now naturally, being students at university, the alcohol part requires some tact. The common method has been to order it via a delivery app/service thing. The delivery boy reaches the store, you pick your poison and pay digitally, and he arrives within 15 minutes to the campus. Now the city I live in is weird, and shops tend to close by 11. Still, late enough that one can order it before the party starts.<p>Most students however, prefer to get their alcohol well in advance, showing a knack for forethought rarely seen when it comes to matters of academic interest. By 8 PM, it has reached some acquaintance’s room, and eventually it comes with the said acquaintance to the party. Often in friend groups, this job is given to the person most experienced with this protocol, and the initial experience is sought by befriending said experienced folks.<p>In our party however, my go to experienced friend couldn’t make it. No issues, I have another. However, the complex social hierarchies dictated that the other experienced friend shouldn’t be asked for this help. Thus it fell to me.<p>Now I have rarely broken rules. In the past I may have been a Hermione, and been a stickler for them. Through years of a mixture of peer pressure, curiosity, and exposure to a more liberal view to such things, I have begun to partake in some rule breaking, but still refrain from doing so too often, and from facilitating the said rule breaking.<p>That meant I was unsure of the exact protocol, and the many, many dependencies necessary to complete it. If the party starts at 11, I reasoned, then starting the delivery by 10 should cut it. It would still cause a slight inconvenience with my schedule for bath, but that should be manageable. What I didn’t factor, dear reader, is that this city goes to sleep so early, and given it was a Sunday, it did so even earlier. Not one delivery boy could be found, and that meant I couldn’t order the alcohol.<p>But my friends’ expectations for a fun night rested on me. I had to secure some from anywhere. Now I did attempt a few ways, but none worked out. Thus, we were going to have a party without alcohol. But, I thought, most people in my friend circle are not heavy drinkers. Also some have exams tomorrow, and need to study. Some have to wake up early. One doesn’t drink at all. Plus its not like we’ve not had parties before. In our more timid early semesters, things were pretty virgin. We’ll still have fun.<p>Sour grapes.<p>We did have fun. People stayed for about 3 hours, which is a decent time, and no one seemed exceptionally bored. We played a few card games, which I thought were fun, and most people enjoyed. Me and my girlfriend still had fun after. Playing some more card games I mean. And yet, at some point, someone remarked that since most of us were free that day, any of us could have gone out and gotten alcohol. True, I can hardly argue. I was lazing around until dinner time anyway. And we did have fun during the party without alcohol too. But still, people wanted it.<p>Now, I get the point of alcohol, it lets you let yourself lose (loose?). It lubricates the slightly slower moments of the night. But you hardly require it for fun. Sure, its funny to see the lightweights let loose much faster, and the more serious ones lighten up for once. But you don’t need alcohol to feel comfortable, and to relax. Definitely, it tastes kinda bad, but a light buzz feels great. But surely, that buzz can be replaced with great moments in your full consciousness.<p>And yet, sometimes I wonder, maybe we really do need alcohol to have fun.
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2023-04-24_050254</guid></item><item><pubDate>Sun, 12 Nov 2023 00:00:00 +0530</pubDate><title>My Default apps in 2023</title><link>https://dhruvasambrani.github.io/blog/./2023-11-12_003358</link><description>desc:</description><content:encoded><![CDATA[
<h1 id=my-default-apps-in-2023>My Default Apps in 2023</h1><p>So I saw <a href=https://kevquirk.com/my-default-apps-at-the-end-of-2023>Kev’s post</a> on his list of apps, and I thought I might as well put my thing up too, because why not.<ul><li>📨 Mail Service: Gmail/institute mail<li>📮 Mail Client: <a href=https://aerc-mail.org/>aerc</a> (Linux), <a href=https://k9mail.app/>K9 Mail</a> (Android)<li>📝 Notes: <a href=https://joplinapp.org/>Joplin</a><li>✅ To-Do: Joplin with <a href=https://github.com/joplin/plugin-kanban>Kanban plugin</a><li>📆 Calendar: Selfhosted <a href=https://radicale.org/v3.html>Radicale</a>. Clients: <a href=https://github.com/pimutils/khal>khal</a> for PC, <a href=https://www.davx5.com/>DAVx</a> for Android.<li>🙍🏻♂️ Contacts: Selfhosted Radical. Clients: <a href=https://github.com/lucc/khard>khard</a> for PC, <a href=https://www.davx5.com/>DAVx</a> for Android.<li>📖 RSS Service: Selfhosted <a href=https://github.com/FreshRSS/FreshRSS>FreshRSS</a><li>🗞️ RSS Client: <a href=https://hyliu.me/fluent-reader-lite/>FluentReader</a> (android), <a href=https://newsboat.org/>newsboat</a><li>⌨️ Launcher: <a href=https://kisslauncher.com/>KISS</a> (Android), <a href=https://tools.suckless.org/dmenu/scripts/>dmenu</a> (X11, planning to switch to bemenu or equiv)<li>☁️ Cloud storage: <a href=https://syncthing.net/>Syncthing</a><li>🌅 Photo library: Local, Synced with syncthing<li>🌐 Web Browser: <a href=https://f-droid.org/packages/org.mozilla.fennec_fdroid/>Fennec</a> (android), Firefox<li>💬 Chat: Signal & WhatsApp<li>🔖 Bookmarks: Ew. Never cleared history ftw. But i should probably set up something.<li>📚 Reading: Kindle 8th Gen something, idek<li>📜 Word Processing: Libreoffice<li>📈 Spreadsheets: Libreoffice, but would like to switch<li>📊 Presentations: <a href=https://quarto.org/>Quarto</a><li>🛒 Shopping Lists: Joplin<li>💰 Personal Finance: Libreoffice, but i tried to make a finance app. pretty pointless<li>🎵 Music: Spotify<li>🎤 Podcasts: None<li>🔐 Password Management: <a href=https://www.passwordstore.org/>pass(1)</a>, <a href=https://github.com/android-password-store/Android-Password-Store#readme>Password Store</a> (android)<li>🤦♂️ Social Media: Instagram<li>🌤️ Weather: <a href=https://openweathermap.org/>Open Weather API</a> with <a href=https://gitlab.com/BeoCode/weather>Weather (BeoCode)</a><li>🔎 Search: DuckDuckGo<li>🧮 Code Editor: VSCodium</ul><p>Happy Deepawali!!
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2023-11-12_003358</guid></item><item><pubDate>Sat, 01 Jun 2024 00:00:00 +0530</pubDate><title>On Loneliness</title><link>https://dhruvasambrani.github.io/blog/./2024-06-01_011941</link><description>My thoughts on loneliness</description><content:encoded><![CDATA[
<h1 id=on-loneliness>On Loneliness</h1><p>I have recently started a PhD in a new country, and none of my friends are in the same city. And so naturally, I felt lonely for a while until I made some friends, and then for a while even after.<p>Recently though, a friend who may also move to a different city/country for his studies expressed his concern that he’ll become lonely. The following is a slightly abridged version of what I said in our conversation.<blockquote><p>Friend: I’m worried about living alone</blockquote><blockquote><p>Friend 2: It gets lonely then too You need to go out</blockquote><blockquote><p>Me: Definitely</blockquote><blockquote><p>Friend 2: IDK</blockquote><blockquote><p>Me: But I know you Friend, you’ll do very well You can easily make friends Try a bit, you’ll find a gang</blockquote><blockquote><p>Friend: I make friends yes…. Friend: But I also get lonely pretty easily</blockquote><blockquote><p>Me: That is ok, I’ve realized Me: Occasional loneliness is not very bad, its only if it extends for too long Me: I mean, of course it’s better to never be lonely, but it’s something that will happen whatever you do. And so, don’t worry about feeling lonely.</blockquote><blockquote><p>Friend: It’s like my first instinct when I’m lonely is to isolate myself more</blockquote><blockquote><p>Me: Oh that’s me too</blockquote><blockquote><p>Friend 2: I don’t think not worrying helps. Push yourself to do things.</blockquote><blockquote><p>Me: Definitely. I feel lonely, I feel sad, I miss ‘my old institute’, I feel sadder, then no motivation, so I sit it out, then miss out, then feel worse Bad cycle</blockquote><blockquote><p>Friend: 🙁</blockquote><blockquote><p>Me: But I’ve learned, it’s not very hard to break it You just go. You must. And then, you will enjoy. Because I am one of those people who love hanging out and talking, and it brings me joy. You are like that too, I think So I just have to get there Be reserved for a few minutes to adjust Then open up fully</blockquote><blockquote><p>Friend: I’m not sure… I usually feel comfortable only in 1 on 1 settings But you could be right It’s so hard to say when I am stuck at home working remotely like this</blockquote><blockquote><p>Friend 2: Go out with people near your house</blockquote><blockquote><p>Friend: I moved houses… I know only one guy here I guess I just crave a lot of company at this time of the night And usually everyone is asleep</blockquote><blockquote><p>Me: correct But when you are studying or working in like a group setting, you will find people to talk to. Not everyone will be a very good friend, but eventually you will find people you like talking to. Friends of friends type. You just need to go out, and stick at it When I first moved to Paderborn, full josh (excitement) was there, for 2 months. Then December I went to Vienna and met (some other friends), which took me till mid-Feb. But then a combination of bad weather, not really vibing with my group entirely, and the International Group not actually connecting, I felt very, very lonely. Plus I was going to travel back in March, and I get real bad travel anxiety, so that made it even worse. But in all this time, two new people joined the Intl group, a Polish girl and a Chilean guy. After coming back from India, somehow we vibed really well. Plus another Indian guy joined, on the day I went back home who turned out to live right opposite my house. We don’t vibe as much, but Indian Indian Now I am in a good space. I’ve made my peace with the fact that my lab group won’t be my best friends. I also know that there are some people I vibe with who don’t have a similar upbringing to me, so there must be others too. Weather has also become pretty much awesome, and I’ve settled in. But there is SO MUCH TO LEARN from people. Remember how we talked about how many different viewpoints we get in IISER? Now you get so many more! Different food, language, customs, geography so much fun But finally, you still will feel lonely. This is ok. You will find people who you will vibe with. Eventually you won’t vibe with them. And this is scary, but this is ok. Finally, you will live a life that will be defined only by you. Your associations shape your path, but they do not make you, You. I imagine this like a long path. You set off from a unique location, and you’ll end up in a unique location. You are the only person who will do this path, and in that sense you are alone. But most segments of this path you will share with others. But they are going different ways, just for a while, they are with you. You make the most of it. Sometimes you have to exit a highway to a different one, and this connection road is naturally very sparse, and you may not meet people. But in the next highway, you will again find people who will drive a long way with you. But finally, you are the only one in your car.</blockquote><p>Maybe I was very sleepy and tired after a long day, who knows? But this still makes sense to me.
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2024-06-01_011941</guid></item><item><pubDate>Sun, 07 Jul 2024 00:00:00 +0530</pubDate><title>Now on 512kb</title><link>https://dhruvasambrani.github.io/blog/./2024-07-07_184009</link><description>We are now on 512kb!</description><content:encoded><![CDATA[
<h1 id=now-on-512kb>Now on 512kb</h1><p>We are now on 512kb!<p>In a <a href=./2022-05-12_181238>previous post</a> I said that I wanted to add my blog to the 512kb club, but I did not know how to check if I qualified. Turns out you should read the FAQs. Following the FAQs, now we are on the 512kb club! On some later date I will also add a webring maybe.
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2024-07-07_184009</guid></item><item><pubDate>Mon, 21 Apr 2025 00:00:00 +0530</pubDate><title>Self Hosting without a public IP address or domain</title><link>https://dhruvasambrani.github.io/blog/./2025-04-21_200237</link><description>or How I Self Host for free</description><content:encoded><![CDATA[
<h1 id=self-hosting-without-a-public-ip-address-or-domain>Self Hosting without a public IP address or domain</h1><p>Probably the hardest part about self hosting is reaching your server. If you use a paid hosting/VPS like Dropocean or the myriad others, this is pretty easy, but if you want to go for a 0 cost hosting like me, then this guide is for you.<h1 id=hardware-and-apps>Hardware and Apps</h1><p>You will need a device with an internet connection. I used a netbook from 2010 with great success until my mom upgraded her laptop, allowing me to upgrade my server to her old laptop. The old device ran Arch on 4GB single channel ram, so you really don’t need any fancy hardware.<p>The first thing you want to set up is an ssh server on your server device. See <a href=https://wiki.archlinux.org/title/OpenSSH#Server_usage>the archwiki page</a> for a quickstart.<p>You will probably install the selfhosted software itself via docker, or on baremetal. I would suggest docker for more complicated software, and baremetal if its a single run binary.<p>As for the main reverse proxy, I would suggest Caddy as it is the lightest and also automatically configures the TLS for the tailscale domains, but for some software, you might not get a config file for caddy in the docs. But playing around a little, you can easily adapt the NGINX or Apache configs pretty easily. See <a href=https://caddyserver.com/docs/getting-started>here</a> for a simple run through. I would suggest to run caddy on baremetal.<p>Having these, a TL;DR of the post is to install Tailscale on all devices, install pihole and unbound (optional), set pihole as the DNS resolver with Tailscale’s MagicDNS, then add a <code>deny</code> rule in pihole to respond to <code>yourname.com</code> with <code>tailscale-ip-of-server</code>.<h1 id=connecting-to-your-device>Connecting to your device</h1><p>Now to the main part of this blog. How to connect to this device from outside a LAN network. There are two options:<ol type=1><li>Pay your ISP for a public/static IP, and also pay for a domain for the TLS part. This is the usual way of doing things. The exact cost of this will depend on the domain and your ISP.<li>Use a service that provides a VPN, like Tailscale. Note however that this only allows access from devices on your tailnet.</ol><p>We will explore the set up with Tailscale. First set up your tailscale by following the 6 steps on <a href=https://tailscale.com/kb/1017/install>Tailscale quickstart</a>. Then, <a href=https://tailscale.com/kb/1081/magicdns>activate MagicDNS</a> if not already active. From now on, we will assume that <code>server</code> is the tailnet name of your server, and <code>client</code> is some client. To verify a complete setup, try to ssh into <code>server</code> from <code>client</code> with both of them on different networks. Troubleshoot connectivity with <code>tailscale ping server</code> from <code>client</code>, and also check if firewall rules on <code>server</code> allow access. Note that for testing you can turn off the firewall since only devices on your tailnet can access your device anyway.<h1 id=set-up-caddy>Set up caddy</h1><p>Once you have verified that the server is accessible, try to <a href=https://caddyserver.com/docs/quick-starts/static-files>run a file server with caddy</a>, <a href=https://caddyserver.com/docs/caddyfile/options#auto-https>turn off auto_https</a> and access the fileserver at <code>http://server.your-tailnet-name.ts.net</code> from a browser on <code>client</code>. Once again, troubleshoot by checking that server is accessible to ping, firewall rules are correct. Once this works, <a href=https://tailscale.com/kb/1190/caddy-certificates#provide-non-root-users-with-access-to-fetch-certificate>set up tailscale</a> to allow caddy to fetch the certs, <a href=https://caddyserver.com/docs/caddyfile/options#auto-https>turn on auto_https</a>, and try to access the file server at <code>https://server.your-tailnet-name.ts.net</code>.<p>At this point, you can use some self hosted software hosted at different sub directories, for e.g. <a href=#>https://server.your-tailnet-name.ts.net/service</a>. Almost all the apps will require some setup to allow access from a non-root base directory, and not all the apps support this. For example, I self host an instance of Gitea on <code>/git</code>. This requires the following set up:<ol type=1><li>Tell <code>gitea</code> that it is <a href=https://docs.gitea.com/next/administration/reverse-proxies#use-a-sub-path>hosted on a sub-path</a><li>Reverse proxy all queries on <code>/git/*</code> to the correct port<li>redirect <code>/git</code> to <code>/git/</code></ol><p>2 and 3 are configd in caddy’s config as<pre class=caddyfile><code>server.your-tailnet-name.ts.net {
    handle_path /git/* {
        reverse_proxy localhost:3000
    }
    redir /git /git/
}</code></pre><p>Note that some apps require that you use <code>handle</code> instead of <code>handle_path</code>.<p>This kind of set up is a big headache when you want to add services, since you have to find a service that supports this, or use hacky ways to fix it if there is no support. I wrote about it in [[2022-09-09_131905]]. Normally you can check for the key words <code>basepath</code>, <code>subpath</code>, <code>subdir</code>, <code>rootdir</code> in the docs or the Issues tab in the project’s repo if there’s some unofficial/undocumented way to do it.<h1 id=custom-dns-resolution>Custom DNS Resolution</h1><p><strong>or, getting a custom domain, for <em>free</em></strong><p>Now it would be great if MagicDNS allowed us to forward <code>*.server.ts.net</code> to <code>server</code> but it doesn’t do so automatically. Now there are 3 ways to fix this:<ol type=1><li>Run a tailscale instance in each docker container, and access the different services through <code>service.your-tailnet-name.ts.net</code>. This is called a tailscale sidecar. Requires you to edit each docker container, which I felt was annoying.<li>Run <a href=https://almeidapaulopt.github.io/tsdproxy/docs/>TSDProxy</a> which gives the same result with a single proxy.<li>Run your own DNS resolver like pihole and add a rule to resolve <code>yourname.com</code> to <code>your-tailscale-ip</code></ol><p>I did 3 because 1 is annoying and I got to know about 2 only as i was writing this post. Also, pihole allows ad blocking at the DNS level. But anyway, here goes:<ol type=1><li>Set up <a href=https://docs.pi-hole.net/docker/>pihole on docker</a>. You may have to turn off systemd-resolved.<li>Set up a reverse proxy to the pihole interface in caddy<li>In the <a href=https://login.tailscale.com/admin/dns>dns section of the tailscale console</a>, add a custom Global Nameserver as the tailscale ip address to <code>server</code>.<li>Use <code>dig</code> to check if you are able to resolve the ip address for a public domain like <code>google.com</code>. Again, check if firewall rules are correct. pihole typically runs on <code>53</code><li>In the pihole admin interface, go to <code>Domains</code>, add a <code>RegEx filter</code>: <code>(\.|^)yourname\.com$;reply=server-tailscale-ip</code>, and <code>Add to denied domains</code>. Now check if you are able to resolve <code>yourname.com</code> to the correct ip. Also check <code>random.yourname.com</code></ol><p>At this point, any device connected to the tailscale should use the pihole as the DNS resolver, which resolves <code>*.yourname.com</code> to your server. But you still have to config caddy to reverse proxy depending on the domain requested. This looks like<pre class=caddyfile><code>servicename.yourname.com {
    reverse_proxy localhost:port
}</code></pre><p>Note you do not have to <code>handle_path</code> anymore. You may also have to change the service to serve on the rootdir <code>/</code>.<h2 id=done-almost>Done, Almost</h2><p>At this point you can access all selfhosted services at <code>service.yourname.com</code> as far as you are connected to Tailscale on your <code>client</code>. However, it may happen, as it did in my case, that <code>server</code> is physically located in a different country than <code>client</code>. Since the final dns resolution to public domains happens through a request by <code>server</code>, the resulting ip address is optimal for the country of <code>server</code> rather than <code>client</code>. In terms of speed, this is barely any different, but if <code>server</code>’s country blocks certain domains by <a href=https://torrentfreak.com/google-cloudflare-cisco-will-poison-dns-to-stop-piracy-block-circumvention-240613/>dns poisoning</a>, then <code>client</code> will also not be able to access these websites. You can get a better ip by using an <a href=https://medium.com/nextdns/how-we-made-dns-both-fast-and-private-with-ecs-4970d70401e5>ECS</a> enabled upstream (see <code>Settings > DNS</code> in pihole interface), but I couldn’t really circumvent the dns poisoning. So I also set up an <code>unbound rescursive resolver</code>. Note that since pihole is in a docker container (which has its own subnet) but unbound is on baremetal, you need to run unbound on the docker interface. You can edit <code>/etc/unbound/unbound.conf</code> as I have<pre><code>server:
    # If no logfile is specified, syslog is used
    logfile: &quot;/var/log/unbound/unbound.log&quot;
    verbosity: 0

    interface: 172.17.0.1
    port: 5335
    do-ip4: yes
    do-udp: yes
    do-tcp: yes
    
    access-control: 172.17.0.0/12 allow
    # May be set to no if you don&#39;t have IPv6 connectivity
    do-ip6: yes

    # You want to leave this to no unless you have *native* IPv6. With 6to4 and
    # Terredo tunnels your web browser should favor IPv4 for the same reasons
    prefer-ip6: no

    # Use this only when you downloaded the list of primary root servers!
    # If you use the default dns-root-data package, unbound will find it automatically
    #root-hints: &quot;/var/lib/unbound/root.hints&quot;

    # Trust glue only if it is within the server&#39;s authority
    harden-glue: yes

    # Require DNSSEC data for trust-anchored zones, if such data is absent, the zone becomes BOGUS
    harden-dnssec-stripped: yes

    # Don&#39;t use Capitalization randomization as it known to cause DNSSEC issues sometimes
    # see https://discourse.pi-hole.net/t/unbound-stubby-or-dnscrypt-proxy/9378 for further details
    use-caps-for-id: no

    # Reduce EDNS reassembly buffer size.
    # IP fragmentation is unreliable on the Internet today, and can cause
    # transmission failures when large DNS messages are sent via UDP. Even
    # when fragmentation does work, it may not be secure; it is theoretically
    # possible to spoof parts of a fragmented DNS message, without easy
    # detection at the receiving end. Recently, there was an excellent study
    # &gt;&gt;&gt; Defragmenting DNS - Determining the optimal maximum UDP response size for DNS &lt;&lt;&lt;
    # by Axel Koolhaas, and Tjeerd Slokker (https://indico.dns-oarc.net/event/36/contributions/776/)
    # in collaboration with NLnet Labs explored DNS using real world data from the
    # the RIPE Atlas probes and the researchers suggested different values for
    # IPv4 and IPv6 and in different scenarios. They advise that servers should
    # be configured to limit DNS messages sent over UDP to a size that will not
    # trigger fragmentation on typical network links. DNS servers can switch
    # from UDP to TCP when a DNS response is too big to fit in this limited
    # buffer size. This value has also been suggested in DNS Flag Day 2020.
    edns-buffer-size: 1232

    # Perform prefetching of close to expired message cache entries
    # This only applies to domains that have been frequently queried
    prefetch: yes

    # One thread should be sufficient, can be increased on beefy machines. In reality for most users running on small networks or on a single machine, it should be unnecessary to seek performance enhancement by increasing num-threads above 1.
    num-threads: 1

    # Ensure kernel buffer is large enough to not lose messages in traffic spikes
    so-rcvbuf: 1m

    # Ensure privacy of local IP ranges
    private-address: 192.168.0.0/16
    private-address: 169.254.0.0/16
    private-address: 172.16.0.0/12
    private-address: 10.0.0.0/8
    private-address: fd00::/8
    private-address: fe80::/10

    # Ensure no reverse queries to non-public IP ranges (RFC6303 4.2)
    private-address: 192.0.2.0/24
    private-address: 198.51.100.0/24
    private-address: 203.0.113.0/24
    private-address: 255.255.255.255/32
    private-address: 2001:db8::/32</code></pre><p>Note that you may need to change <code>interface</code> and <code>access-control</code> to the docker ip address, which you can find by <code>ip addr | grep docker</code>. You can then set <code>Settings > DNS > Upstream DNS Server > Custom DNS Servers</code> to <code>172.17.0.1#5353</code>, or the appropriate ip address. This finally allows us to resolve DNS normally without DNS poisoning. Note that the ISP can still directly block the IP address.<h2 id=overview-of-dns-resolution>Overview of DNS resolution</h2><p>At this point a typical DNS resolution request will first hit MagicDNS, which responds with the appropriate ip for <code>*.ts.net</code> or forward to the custom upstream DNS which is pihole running on <code>server</code>. Pihole then replies with <code>NXDOMAIN</code> for blocked domains, <code>server-tailscale-ip</code> for <code>*.yourname.com</code> or forwards to unbound also running on <code>server</code>. Unbound then <a href=https://www.geeksforgeeks.org/what-is-recursive-dns/>recursively resolves</a> the public domains with ECS.
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2025-04-21_200237</guid></item><item><pubDate>Tue, 22 Apr 2025 00:00:00 +0530</pubDate><title>Currently selfhosted services</title><link>https://dhruvasambrani.github.io/blog/./2025-04-22_155445</link><description>Showing off, basically</description><content:encoded><![CDATA[
<h1 id=currently-selfhosted-services>Currently selfhosted services</h1><blockquote><p>Edited last on : 05/01/2026</blockquote><ul><li><del><strong>Actual</strong>: <a href=https://www.actualbudget.com/>Actual</a> - Budgeting, potentially replacing <a href=https://www.ynab.com/>YNAB (You Need a Budget)</a> or <a href=https://www.personalcapital.com/>Personal Capital</a> - 15$</del> Edit 2026-01: I don’t really use it anymore<li><strong>Affine</strong>: <a href=https://affine.pro/>Affine</a> - Notes, potentially replacing <a href=https://evernote.com/>Evernote</a> or <a href=https://www.onenote.com/>OneNote</a> or <a href=https://www.notion.so/>Notion</a> or <a href=https://trello.com/>Trello</a> - 7.5$<li><strong>FreshRSS</strong>: <a href=https://freshrss.org/>FreshRSS</a> - RSS feed reader, potentially replacing <a href=https://feedly.com/>Feedly</a> or <a href=https://newsblur.com/>NewsBlur</a> - 7$<li><strong>Gitea</strong>: <a href=https://gitea.io/>Gitea</a> - Git, potentially replacing <a href=https://github.com/>GitHub</a> or <a href=https://www.gitlab.com/>GitLab</a><li><strong>Linkding</strong>: <a href=https://linkding.org/>Linkding</a> - Link Archive, potentially replacing <a href=https://getpocket.com/>Pocket</a> or <a href=https://raindrop.io/>Raindrop.io</a> - 3.75$<li><strong>Lychee</strong>: <a href=https://lychee.evansh.com/>Lychee</a> - Picture Library, potentially replacing <a href=https://photos.google.com/>Google Photos</a> or <a href=https://www.flickr.com/>Flickr</a> - 2$<li><strong>Nextcloud</strong>: <a href=https://nextcloud.com/>Nextcloud</a> - Cloud storage, potentially replacing <a href=https://drive.google.com/>Google Drive</a> or <a href=https://www.microsoft.com/en-us/microsoft-365/onedrive>Microsoft OneDrive</a> - 2$<li><del><strong>PiHole</strong>: <a href=https://pi-hole.net/>PiHole</a> - DNS Resolver, potentially replacing <a href=https://adguard.com/>AdGuard</a> or <a href=https://www.opendns.com/>OpenDNS</a></del> Edit 2026-01: Not useful after changing DNS to mullvad DNS in tailscale and TSDProxy to give a domain for each service.<li><del><strong>Unbound</strong>: <a href=https://unbounddns.com/>Unbound</a> - Upstream DNS Resolver, potentially replacing <a href=https://www.cloudflare.com/dns/>Cloudflare DNS</a> or <a href=https://developers.google.com/speed/public-dns>Google Public DNS</a></del><li><strong>Vaultwarden</strong>: <a href=https://vaultwarden.com/>Vaultwarden</a> - Password Store, potentially replacing <a href=https://www.lastpass.com/>LastPass</a> or <a href=https://1password.com/>1Password</a> - 4$<li><strong>Zotero WebDAV</strong>: <a href=https://www.zotero.org/>Zotero</a> - Storage for Zotero, potentially replacing <a href=https://www.zotero.org/storage>Zotero Storage</a> - 5$<li><del><strong>Notability WebDAV</strong>: <a href=https://www.notability.com/>Notability</a> - Backup for Notability, potentially replacing <a href=https://drive.google.com/>Google Drive</a> or <a href=https://www.dropbox.com/>Dropbox</a> - 2$</del> Edit 2026-01: Preview App and SyncTrain on IPad.<li><strong>Syncthing</strong>: <a href=https://syncthing.net/>Syncthing</a> - Syncing software<li><strong>SyncTrain</strong>: <strong>Syncthing</strong> for IPad<li><strong>Radicale</strong>: <a href=https://radicale.org/>Radicale</a> - CalDav and CardDav, potentially replacing <a href=https://calendar.google.com/>Google Calendar</a><li><del><strong>Homer</strong>: <a href=https://homer.sh/>Homer</a> - personal dashboard</del><li><strong>Dashy</strong>: <a href=https://dashy.to/>Dashy</a> - personal dashboard<li><strong>.Calibre Web Advanced</strong>: <a href=https://github.com/crocodilestick/Calibre-Web-Automated>Calibre Web Automated</a> - self hosted library <strong>Book-Downloader</strong> - <a href=https://github.com/calibrain/calibre-web-automated-book-downloader>calibre-web-automated-book-downloader</a> - Books downloaded from sources for CWA to ingest.<li>CopyParty - <a href=https://github.com/9001/copyparty>CopyParty</a> - File uploader<li>YT-DLP Web UI - <a href=https://github.com/marcopiovanello/yt-dlp-web-ui>YT-DLP Webui</a> - Download Youtube music.<li>Jellyfin - <a href=https://jellyfin.org/>Jellyfin</a> - Media server</ul><p>Potentially saving like 47$/month more or less. Almost worth it already even without considering data safety.
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2025-04-22_155445</guid></item><item><pubDate>Sat, 27 Sep 2025 00:00:00 +0530</pubDate><title>Jumping into the `river` and AI</title><link>https://dhruvasambrani.github.io/blog/./2025-09-27_185759</link><description>How I used the help of AI to move from dwl to river</description><content:encoded><![CDATA[
<h1 id=jumping-into-the-river-and-ai>Jumping into the <code>river</code> and AI</h1><p>I <a href=2023-11-12_003358>used</a> <code>dwl</code> until <a href=https://codeberg.org/dwl/dwl/issues/1166>dwl was stated as stagnant</a>. I tried hyprland, but it was not clear to me how to add custom layouts, and I like my grid based layout from dwl. Also i would have to set up a new bar, and would have to work with their way of showing things, which i felt was way too much effort. I wanted the flexibility of dwl. There is also a maximalist fork of dwl called <a href=https://github.com/DreamMaoMao/mangowc>mangowc</a>, but I would have to either accept the maximalist view of it or maintain a stripped down fork of that, which seemed convoluted. niri was not tiled, and I don’t think I would like the scrolling layout.<h2 id=migrating-to-river>Migrating to river</h2><h3 id=layouts>Layouts</h3><p><a href=https://codeberg.org/river/river>riverwc</a> was what I finally settled on. The main advantage that I saw was that its structured the same way as dwl, with rule based layouting. The custom layout protocol was great, especially <a href=https://github.com/MaxVerevkin/river-luatile>river-luatile</a> which allows you to write the layouts in lua. After some off by one errors I had implemented a gaplessgrid and monocle layout, and used the default <code>master</code> layout from river-luatile. I setup keybinds to change between them: I really love that you can call any lua command from the keybinds, which made the layout changing so easy.<div class=sourceCode id=cb1><pre class="sourceCode lua"><code class="sourceCode lua"><span id=cb1-1><a href=#cb1-1 aria-hidden=true tabindex=-1></a><span class=co>#! /usr/bin/lua</span></span>
<span id=cb1-2><a href=#cb1-2 aria-hidden=true tabindex=-1></a></span>
<span id=cb1-3><a href=#cb1-3 aria-hidden=true tabindex=-1></a><span class=co>-- You can define your global state here</span></span>
<span id=cb1-4><a href=#cb1-4 aria-hidden=true tabindex=-1></a><span class=kw>local</span> <span class=va>agaps</span> <span class=op>=</span> <span class=dv>4</span></span>
<span id=cb1-5><a href=#cb1-5 aria-hidden=true tabindex=-1></a><span class=kw>local</span> <span class=va>master_ratio</span> <span class=op>=</span> <span class=dv>0.55</span></span>
<span id=cb1-6><a href=#cb1-6 aria-hidden=true tabindex=-1></a></span>
<span id=cb1-7><a href=#cb1-7 aria-hidden=true tabindex=-1></a><span class=kw>function</span> iswidescreen<span class=op>(</span><span class=va>args</span><span class=op>)</span></span>
<span id=cb1-8><a href=#cb1-8 aria-hidden=true tabindex=-1></a>    <span class=cf>return</span> <span class=va>args</span><span class=op>.</span><span class=va>width</span> <span class=op>&gt;</span> <span class=dv>2</span> <span class=op>*</span> <span class=va>args</span><span class=op>.</span><span class=va>height</span></span>
<span id=cb1-9><a href=#cb1-9 aria-hidden=true tabindex=-1></a><span class=kw>end</span></span>
<span id=cb1-10><a href=#cb1-10 aria-hidden=true tabindex=-1></a></span>
<span id=cb1-11><a href=#cb1-11 aria-hidden=true tabindex=-1></a><span class=kw>function</span> master<span class=op>(</span><span class=va>args</span><span class=op>)</span></span>
<span id=cb1-12><a href=#cb1-12 aria-hidden=true tabindex=-1></a>    <span class=kw>local</span> <span class=va>retval</span> <span class=op>=</span> <span class=op>{}</span></span>
<span id=cb1-13><a href=#cb1-13 aria-hidden=true tabindex=-1></a>    <span class=kw>local</span> <span class=va>gaps</span> <span class=op>=</span> <span class=va>agaps</span></span>
<span id=cb1-14><a href=#cb1-14 aria-hidden=true tabindex=-1></a>    <span class=cf>if</span> <span class=va>args</span><span class=op>.</span><span class=va>count</span> <span class=op>==</span> <span class=dv>1</span> <span class=cf>then</span></span>
<span id=cb1-15><a href=#cb1-15 aria-hidden=true tabindex=-1></a>        <span class=fu>table.insert</span><span class=op>(</span><span class=va>retval</span><span class=op>,</span> <span class=op>{</span> <span class=dv>0</span><span class=op>,</span> <span class=dv>0</span><span class=op>,</span> <span class=va>args</span><span class=op>.</span><span class=va>width</span><span class=op>,</span> <span class=va>args</span><span class=op>.</span><span class=va>height</span> <span class=op>})</span></span>
<span id=cb1-16><a href=#cb1-16 aria-hidden=true tabindex=-1></a>    <span class=cf>elseif</span> <span class=va>args</span><span class=op>.</span><span class=va>count</span> <span class=op>&gt;</span> <span class=dv>1</span> <span class=cf>then</span></span>
<span id=cb1-17><a href=#cb1-17 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>main_w</span> <span class=op>=</span> <span class=op>(</span><span class=va>args</span><span class=op>.</span><span class=va>width</span> <span class=op>-</span> <span class=va>gaps</span> <span class=op>*</span> <span class=dv>3</span><span class=op>)</span> <span class=op>*</span> <span class=va>master_ratio</span></span>
<span id=cb1-18><a href=#cb1-18 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>side_w</span> <span class=op>=</span> <span class=op>(</span><span class=va>args</span><span class=op>.</span><span class=va>width</span> <span class=op>-</span> <span class=va>gaps</span> <span class=op>*</span> <span class=dv>3</span><span class=op>)</span> <span class=op>-</span> <span class=va>main_w</span></span>
<span id=cb1-19><a href=#cb1-19 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>main_h</span> <span class=op>=</span> <span class=va>args</span><span class=op>.</span><span class=va>height</span> <span class=op>-</span> <span class=va>gaps</span> <span class=op>*</span> <span class=dv>2</span></span>
<span id=cb1-20><a href=#cb1-20 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>side_h</span> <span class=op>=</span> <span class=op>(</span><span class=va>args</span><span class=op>.</span><span class=va>height</span> <span class=op>-</span> <span class=va>gaps</span><span class=op>)</span> <span class=op>/</span> <span class=op>(</span><span class=va>args</span><span class=op>.</span><span class=va>count</span> <span class=op>-</span> <span class=dv>1</span><span class=op>)</span> <span class=op>-</span> <span class=va>gaps</span></span>
<span id=cb1-21><a href=#cb1-21 aria-hidden=true tabindex=-1></a>        <span class=fu>table.insert</span><span class=op>(</span><span class=va>retval</span><span class=op>,</span> <span class=op>{</span></span>
<span id=cb1-22><a href=#cb1-22 aria-hidden=true tabindex=-1></a>            <span class=va>gaps</span><span class=op>,</span></span>
<span id=cb1-23><a href=#cb1-23 aria-hidden=true tabindex=-1></a>            <span class=va>gaps</span><span class=op>,</span></span>
<span id=cb1-24><a href=#cb1-24 aria-hidden=true tabindex=-1></a>            <span class=va>main_w</span><span class=op>,</span></span>
<span id=cb1-25><a href=#cb1-25 aria-hidden=true tabindex=-1></a>            <span class=va>main_h</span><span class=op>,</span></span>
<span id=cb1-26><a href=#cb1-26 aria-hidden=true tabindex=-1></a>        <span class=op>})</span></span>
<span id=cb1-27><a href=#cb1-27 aria-hidden=true tabindex=-1></a>        <span class=cf>for</span> <span class=va>i</span> <span class=op>=</span> <span class=dv>0</span><span class=op>,</span> <span class=op>(</span><span class=va>args</span><span class=op>.</span><span class=va>count</span> <span class=op>-</span> <span class=dv>2</span><span class=op>)</span> <span class=cf>do</span></span>
<span id=cb1-28><a href=#cb1-28 aria-hidden=true tabindex=-1></a>            <span class=fu>table.insert</span><span class=op>(</span><span class=va>retval</span><span class=op>,</span> <span class=op>{</span></span>
<span id=cb1-29><a href=#cb1-29 aria-hidden=true tabindex=-1></a>                <span class=va>main_w</span> <span class=op>+</span> <span class=va>gaps</span> <span class=op>*</span> <span class=dv>2</span><span class=op>,</span></span>
<span id=cb1-30><a href=#cb1-30 aria-hidden=true tabindex=-1></a>                <span class=va>gaps</span> <span class=op>+</span> <span class=va>i</span> <span class=op>*</span> <span class=op>(</span><span class=va>side_h</span> <span class=op>+</span> <span class=va>gaps</span><span class=op>),</span></span>
<span id=cb1-31><a href=#cb1-31 aria-hidden=true tabindex=-1></a>                <span class=va>side_w</span><span class=op>,</span></span>
<span id=cb1-32><a href=#cb1-32 aria-hidden=true tabindex=-1></a>                <span class=va>side_h</span><span class=op>,</span></span>
<span id=cb1-33><a href=#cb1-33 aria-hidden=true tabindex=-1></a>            <span class=op>})</span></span>
<span id=cb1-34><a href=#cb1-34 aria-hidden=true tabindex=-1></a>        <span class=cf>end</span></span>
<span id=cb1-35><a href=#cb1-35 aria-hidden=true tabindex=-1></a>    <span class=cf>end</span></span>
<span id=cb1-36><a href=#cb1-36 aria-hidden=true tabindex=-1></a>    <span class=cf>return</span> <span class=va>retval</span></span>
<span id=cb1-37><a href=#cb1-37 aria-hidden=true tabindex=-1></a><span class=kw>end</span></span>
<span id=cb1-38><a href=#cb1-38 aria-hidden=true tabindex=-1></a></span>
<span id=cb1-39><a href=#cb1-39 aria-hidden=true tabindex=-1></a><span class=kw>function</span> grid<span class=op>(</span><span class=va>args</span><span class=op>)</span></span>
<span id=cb1-40><a href=#cb1-40 aria-hidden=true tabindex=-1></a>    <span class=kw>local</span> <span class=va>gaps</span> <span class=op>=</span> <span class=va>agaps</span></span>
<span id=cb1-41><a href=#cb1-41 aria-hidden=true tabindex=-1></a>    <span class=cf>if</span> <span class=va>args</span><span class=op>.</span><span class=va>count</span> <span class=op>==</span> <span class=dv>1</span> <span class=cf>then</span></span>
<span id=cb1-42><a href=#cb1-42 aria-hidden=true tabindex=-1></a>        <span class=va>gaps</span> <span class=op>=</span> <span class=dv>0</span></span>
<span id=cb1-43><a href=#cb1-43 aria-hidden=true tabindex=-1></a>    <span class=cf>end</span></span>
<span id=cb1-44><a href=#cb1-44 aria-hidden=true tabindex=-1></a>    <span class=kw>local</span> <span class=va>retval</span> <span class=op>=</span> <span class=op>{}</span></span>
<span id=cb1-45><a href=#cb1-45 aria-hidden=true tabindex=-1></a>    <span class=kw>local</span> <span class=va>ncols</span> <span class=op>=</span> <span class=dv>1</span></span>
<span id=cb1-46><a href=#cb1-46 aria-hidden=true tabindex=-1></a>    <span class=cf>while</span> <span class=va>ncols</span> <span class=op>*</span> <span class=va>ncols</span> <span class=op>&lt;</span> <span class=va>args</span><span class=op>.</span><span class=va>count</span> <span class=cf>do</span></span>
<span id=cb1-47><a href=#cb1-47 aria-hidden=true tabindex=-1></a>        <span class=va>ncols</span> <span class=op>=</span> <span class=va>ncols</span> <span class=op>+</span> <span class=dv>1</span></span>
<span id=cb1-48><a href=#cb1-48 aria-hidden=true tabindex=-1></a>    <span class=cf>end</span></span>
<span id=cb1-49><a href=#cb1-49 aria-hidden=true tabindex=-1></a>    <span class=cf>if</span> iswidescreen<span class=op>(</span><span class=va>args</span><span class=op>)</span> <span class=kw>and</span> <span class=va>args</span><span class=op>.</span><span class=va>count</span> <span class=op>==</span> <span class=dv>3</span> <span class=cf>then</span></span>
<span id=cb1-50><a href=#cb1-50 aria-hidden=true tabindex=-1></a>        <span class=va>ncols</span> <span class=op>=</span> <span class=dv>3</span> <span class=co>-- 1:1:1</span></span>
<span id=cb1-51><a href=#cb1-51 aria-hidden=true tabindex=-1></a>    <span class=cf>elseif</span> <span class=va>args</span><span class=op>.</span><span class=va>count</span> <span class=op>==</span> <span class=dv>5</span> <span class=cf>then</span></span>
<span id=cb1-52><a href=#cb1-52 aria-hidden=true tabindex=-1></a>        <span class=va>ncols</span> <span class=op>=</span> <span class=dv>2</span></span>
<span id=cb1-53><a href=#cb1-53 aria-hidden=true tabindex=-1></a>    <span class=cf>end</span></span>
<span id=cb1-54><a href=#cb1-54 aria-hidden=true tabindex=-1></a>    <span class=kw>local</span> <span class=va>nrows</span> <span class=op>=</span> <span class=fu>math.ceil</span><span class=op>(</span><span class=va>args</span><span class=op>.</span><span class=va>count</span> <span class=op>/</span> <span class=va>ncols</span><span class=op>)</span></span>
<span id=cb1-55><a href=#cb1-55 aria-hidden=true tabindex=-1></a>    <span class=kw>local</span> <span class=va>cw</span> <span class=op>=</span> <span class=va>args</span><span class=op>.</span><span class=va>width</span> <span class=op>/</span> <span class=va>ncols</span></span>
<span id=cb1-56><a href=#cb1-56 aria-hidden=true tabindex=-1></a>    <span class=kw>local</span> <span class=va>ch</span> <span class=op>=</span> <span class=va>args</span><span class=op>.</span><span class=va>height</span> <span class=op>/</span> <span class=va>nrows</span></span>
<span id=cb1-57><a href=#cb1-57 aria-hidden=true tabindex=-1></a>    <span class=kw>local</span> <span class=va>fcnrows</span> <span class=op>=</span> <span class=va>args</span><span class=op>.</span><span class=va>count</span> <span class=op>-</span> <span class=op>(</span><span class=va>nrows</span> <span class=op>*</span> <span class=op>(</span><span class=va>ncols</span> <span class=op>-</span> <span class=dv>1</span><span class=op>))</span></span>
<span id=cb1-58><a href=#cb1-58 aria-hidden=true tabindex=-1></a>    <span class=cf>for</span> <span class=va>row</span> <span class=op>=</span> <span class=dv>0</span><span class=op>,</span> <span class=va>fcnrows</span> <span class=op>-</span> <span class=dv>1</span> <span class=cf>do</span></span>
<span id=cb1-59><a href=#cb1-59 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>fcch</span> <span class=op>=</span> <span class=va>args</span><span class=op>.</span><span class=va>height</span> <span class=op>/</span> <span class=va>fcnrows</span></span>
<span id=cb1-60><a href=#cb1-60 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>x</span> <span class=op>=</span> <span class=dv>2</span> <span class=op>*</span> <span class=va>gaps</span></span>
<span id=cb1-61><a href=#cb1-61 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>y</span> <span class=op>=</span> <span class=va>row</span> <span class=op>*</span> <span class=va>fcch</span> <span class=op>+</span> <span class=op>((</span><span class=va>row</span> <span class=op>==</span> <span class=dv>0</span><span class=op>)</span> <span class=kw>and</span> <span class=dv>2</span> <span class=op>*</span> <span class=va>gaps</span> <span class=kw>or</span> <span class=va>gaps</span><span class=op>)</span></span>
<span id=cb1-62><a href=#cb1-62 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>w</span> <span class=op>=</span> <span class=va>cw</span> <span class=op>-</span> <span class=dv>3</span> <span class=op>*</span> <span class=va>gaps</span></span>
<span id=cb1-63><a href=#cb1-63 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>h</span> <span class=op>=</span> <span class=va>fcch</span> <span class=op>-</span> <span class=op>(</span><span class=va>row</span> <span class=op>==</span> <span class=dv>0</span> <span class=kw>and</span> <span class=dv>2</span> <span class=op>*</span> <span class=va>gaps</span> <span class=kw>or</span> <span class=va>gaps</span><span class=op>)</span> <span class=op>-</span> <span class=op>((</span><span class=va>row</span> <span class=op>==</span> <span class=va>fcnrows</span> <span class=op>-</span> <span class=dv>1</span><span class=op>)</span> <span class=kw>and</span> <span class=dv>2</span> <span class=op>*</span> <span class=va>gaps</span> <span class=kw>or</span> <span class=va>gaps</span><span class=op>)</span></span>
<span id=cb1-64><a href=#cb1-64 aria-hidden=true tabindex=-1></a>        <span class=fu>table.insert</span><span class=op>(</span><span class=va>retval</span><span class=op>,</span> <span class=op>{</span> <span class=va>x</span><span class=op>,</span> <span class=va>y</span><span class=op>,</span> <span class=va>w</span><span class=op>,</span> <span class=va>h</span> <span class=op>})</span></span>
<span id=cb1-65><a href=#cb1-65 aria-hidden=true tabindex=-1></a>    <span class=cf>end</span></span>
<span id=cb1-66><a href=#cb1-66 aria-hidden=true tabindex=-1></a>    <span class=cf>for</span> <span class=va>i</span> <span class=op>=</span> <span class=va>fcnrows</span><span class=op>,</span> <span class=va>args</span><span class=op>.</span><span class=va>count</span> <span class=op>-</span> <span class=dv>1</span> <span class=cf>do</span></span>
<span id=cb1-67><a href=#cb1-67 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>cellid</span> <span class=op>=</span> <span class=op>(</span><span class=va>i</span> <span class=op>-</span> <span class=va>fcnrows</span><span class=op>)</span> <span class=op>+</span> <span class=va>nrows</span></span>
<span id=cb1-68><a href=#cb1-68 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>row</span> <span class=op>=</span> <span class=op>(</span><span class=va>cellid</span> <span class=op>+</span> <span class=va>fcnrows</span><span class=op>)</span> <span class=op>%</span> <span class=va>nrows</span></span>
<span id=cb1-69><a href=#cb1-69 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>col</span> <span class=op>=</span> <span class=fu>math.floor</span><span class=op>(</span><span class=va>cellid</span> <span class=op>/</span> <span class=va>nrows</span><span class=op>)</span></span>
<span id=cb1-70><a href=#cb1-70 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>x</span> <span class=op>=</span> <span class=va>col</span> <span class=op>*</span> <span class=va>cw</span> <span class=op>+</span> <span class=va>gaps</span></span>
<span id=cb1-71><a href=#cb1-71 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>y</span> <span class=op>=</span> <span class=va>row</span> <span class=op>*</span> <span class=va>ch</span> <span class=op>+</span> <span class=op>((</span><span class=va>row</span> <span class=op>==</span> <span class=dv>0</span><span class=op>)</span> <span class=kw>and</span> <span class=dv>2</span> <span class=op>*</span> <span class=va>gaps</span> <span class=kw>or</span> <span class=va>gaps</span><span class=op>)</span></span>
<span id=cb1-72><a href=#cb1-72 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>w</span> <span class=op>=</span> <span class=va>cw</span> <span class=op>-</span> <span class=op>((</span><span class=va>col</span> <span class=op>==</span> <span class=va>ncols</span><span class=op>)</span> <span class=kw>and</span> <span class=dv>3</span> <span class=op>*</span> <span class=va>gaps</span> <span class=kw>or</span> <span class=dv>2</span> <span class=op>*</span> <span class=va>gaps</span><span class=op>)</span></span>
<span id=cb1-73><a href=#cb1-73 aria-hidden=true tabindex=-1></a>        <span class=kw>local</span> <span class=va>h</span> <span class=op>=</span> <span class=va>ch</span> <span class=op>-</span> <span class=op>(</span><span class=va>row</span> <span class=op>==</span> <span class=dv>0</span> <span class=kw>and</span> <span class=dv>2</span> <span class=op>*</span> <span class=va>gaps</span> <span class=kw>or</span> <span class=va>gaps</span><span class=op>)</span> <span class=op>-</span> <span class=op>((</span><span class=va>row</span> <span class=op>==</span> <span class=va>fcnrows</span> <span class=op>-</span> <span class=dv>1</span><span class=op>)</span> <span class=kw>and</span> <span class=dv>2</span> <span class=op>*</span> <span class=va>gaps</span> <span class=kw>or</span> <span class=va>gaps</span><span class=op>)</span></span>
<span id=cb1-74><a href=#cb1-74 aria-hidden=true tabindex=-1></a>        <span class=fu>table.insert</span><span class=op>(</span><span class=va>retval</span><span class=op>,</span> <span class=op>{</span> <span class=va>x</span><span class=op>,</span> <span class=va>y</span><span class=op>,</span> <span class=va>w</span><span class=op>,</span> <span class=va>h</span> <span class=op>})</span></span>
<span id=cb1-75><a href=#cb1-75 aria-hidden=true tabindex=-1></a>    <span class=cf>end</span></span>
<span id=cb1-76><a href=#cb1-76 aria-hidden=true tabindex=-1></a>    <span class=cf>return</span> <span class=va>retval</span></span>
<span id=cb1-77><a href=#cb1-77 aria-hidden=true tabindex=-1></a><span class=kw>end</span></span>
<span id=cb1-78><a href=#cb1-78 aria-hidden=true tabindex=-1></a></span>
<span id=cb1-79><a href=#cb1-79 aria-hidden=true tabindex=-1></a><span class=kw>function</span> monocle<span class=op>(</span><span class=va>args</span><span class=op>)</span></span>
<span id=cb1-80><a href=#cb1-80 aria-hidden=true tabindex=-1></a>    <span class=kw>local</span> <span class=va>retval</span> <span class=op>=</span> <span class=op>{}</span></span>
<span id=cb1-81><a href=#cb1-81 aria-hidden=true tabindex=-1></a>    <span class=cf>for</span> <span class=cn>_</span> <span class=op>=</span> <span class=dv>0</span><span class=op>,</span> <span class=va>args</span><span class=op>.</span><span class=va>count</span> <span class=op>-</span> <span class=dv>1</span> <span class=cf>do</span></span>
<span id=cb1-82><a href=#cb1-82 aria-hidden=true tabindex=-1></a>        <span class=fu>table.insert</span><span class=op>(</span><span class=va>retval</span><span class=op>,</span> <span class=op>{</span> <span class=dv>0</span><span class=op>,</span> <span class=dv>0</span><span class=op>,</span> <span class=va>args</span><span class=op>.</span><span class=va>width</span><span class=op>,</span> <span class=va>args</span><span class=op>.</span><span class=va>height</span> <span class=op>})</span></span>
<span id=cb1-83><a href=#cb1-83 aria-hidden=true tabindex=-1></a>    <span class=cf>end</span></span>
<span id=cb1-84><a href=#cb1-84 aria-hidden=true tabindex=-1></a>    <span class=cf>return</span> <span class=va>retval</span></span>
<span id=cb1-85><a href=#cb1-85 aria-hidden=true tabindex=-1></a><span class=kw>end</span></span>
<span id=cb1-86><a href=#cb1-86 aria-hidden=true tabindex=-1></a></span>
<span id=cb1-87><a href=#cb1-87 aria-hidden=true tabindex=-1></a><span class=co>-- The most important function - the actual layout generator</span></span>
<span id=cb1-88><a href=#cb1-88 aria-hidden=true tabindex=-1></a><span class=co>--</span></span>
<span id=cb1-89><a href=#cb1-89 aria-hidden=true tabindex=-1></a><span class=co>-- The argument is a table with:</span></span>
<span id=cb1-90><a href=#cb1-90 aria-hidden=true tabindex=-1></a><span class=co>--  * Focused tags (`args.tags`)</span></span>
<span id=cb1-91><a href=#cb1-91 aria-hidden=true tabindex=-1></a><span class=co>--  * Window count (`args.count`)</span></span>
<span id=cb1-92><a href=#cb1-92 aria-hidden=true tabindex=-1></a><span class=co>--  * Output width (`args.width`)</span></span>
<span id=cb1-93><a href=#cb1-93 aria-hidden=true tabindex=-1></a><span class=co>--  * Output height (`args.height`)</span></span>
<span id=cb1-94><a href=#cb1-94 aria-hidden=true tabindex=-1></a><span class=co>--  * Output name (`args.output`)</span></span>
<span id=cb1-95><a href=#cb1-95 aria-hidden=true tabindex=-1></a><span class=co>--</span></span>
<span id=cb1-96><a href=#cb1-96 aria-hidden=true tabindex=-1></a><span class=co>-- The return value must be a table with exactly `count` entries. Each entry is a table with four</span></span>
<span id=cb1-97><a href=#cb1-97 aria-hidden=true tabindex=-1></a><span class=co>-- numbers:</span></span>
<span id=cb1-98><a href=#cb1-98 aria-hidden=true tabindex=-1></a><span class=co>--  * X coordinate</span></span>
<span id=cb1-99><a href=#cb1-99 aria-hidden=true tabindex=-1></a><span class=co>--  * Y coordinate</span></span>
<span id=cb1-100><a href=#cb1-100 aria-hidden=true tabindex=-1></a><span class=co>--  * Window width</span></span>
<span id=cb1-101><a href=#cb1-101 aria-hidden=true tabindex=-1></a><span class=co>--  * Window height</span></span>
<span id=cb1-102><a href=#cb1-102 aria-hidden=true tabindex=-1></a></span>
<span id=cb1-103><a href=#cb1-103 aria-hidden=true tabindex=-1></a><span class=kw>local</span> <span class=va>active_layout</span> <span class=op>=</span> <span class=va>grid</span></span>
<span id=cb1-104><a href=#cb1-104 aria-hidden=true tabindex=-1></a><span class=kw>function</span> handle_layout<span class=op>(</span><span class=va>args</span><span class=op>)</span></span>
<span id=cb1-105><a href=#cb1-105 aria-hidden=true tabindex=-1></a>    <span class=cf>return</span> active_layout<span class=op>(</span><span class=va>args</span><span class=op>)</span></span>
<span id=cb1-106><a href=#cb1-106 aria-hidden=true tabindex=-1></a><span class=kw>end</span></span>
<span id=cb1-107><a href=#cb1-107 aria-hidden=true tabindex=-1></a></span>
<span id=cb1-108><a href=#cb1-108 aria-hidden=true tabindex=-1></a><span class=co>-- This optional function returns the metadata for the current layout.</span></span>
<span id=cb1-109><a href=#cb1-109 aria-hidden=true tabindex=-1></a><span class=co>-- Currently only `name` is supported, the name of the layout. It get&#39;s passed</span></span>
<span id=cb1-110><a href=#cb1-110 aria-hidden=true tabindex=-1></a><span class=co>-- the same `args` as handle_layout()</span></span>
<span id=cb1-111><a href=#cb1-111 aria-hidden=true tabindex=-1></a><span class=kw>function</span> handle_metadata<span class=op>(</span><span class=va>args</span><span class=op>)</span></span>
<span id=cb1-112><a href=#cb1-112 aria-hidden=true tabindex=-1></a>    <span class=cf>if</span> <span class=va>active_layout</span> <span class=op>==</span> <span class=va>grid</span> <span class=cf>then</span></span>
<span id=cb1-113><a href=#cb1-113 aria-hidden=true tabindex=-1></a>        <span class=cf>return</span> <span class=op>{</span> <span class=va>name</span> <span class=op>=</span> <span class=st>&quot;#&quot;</span> <span class=op>}</span></span>
<span id=cb1-114><a href=#cb1-114 aria-hidden=true tabindex=-1></a>    <span class=cf>elseif</span> <span class=va>active_layout</span> <span class=op>==</span> <span class=va>master</span> <span class=cf>then</span></span>
<span id=cb1-115><a href=#cb1-115 aria-hidden=true tabindex=-1></a>        <span class=cf>return</span> <span class=op>{</span> <span class=va>name</span> <span class=op>=</span> <span class=st>&quot;T&quot;</span> <span class=op>}</span></span>
<span id=cb1-116><a href=#cb1-116 aria-hidden=true tabindex=-1></a>    <span class=cf>elseif</span> <span class=va>active_layout</span> <span class=op>==</span> <span class=va>monocle</span> <span class=cf>then</span></span>
<span id=cb1-117><a href=#cb1-117 aria-hidden=true tabindex=-1></a>        <span class=cf>return</span> <span class=op>{</span> <span class=va>name</span> <span class=op>=</span> <span class=st>&quot;M&quot;</span> <span class=op>}</span></span>
<span id=cb1-118><a href=#cb1-118 aria-hidden=true tabindex=-1></a>    <span class=cf>end</span></span>
<span id=cb1-119><a href=#cb1-119 aria-hidden=true tabindex=-1></a><span class=kw>end</span></span>
<span id=cb1-120><a href=#cb1-120 aria-hidden=true tabindex=-1></a></span>
<span id=cb1-121><a href=#cb1-121 aria-hidden=true tabindex=-1></a><span class=kw>function</span> set_layout<span class=op>(</span><span class=va>arg</span><span class=op>)</span></span>
<span id=cb1-122><a href=#cb1-122 aria-hidden=true tabindex=-1></a>    <span class=va>active_layout</span> <span class=op>=</span> <span class=va>arg</span></span>
<span id=cb1-123><a href=#cb1-123 aria-hidden=true tabindex=-1></a><span class=kw>end</span></span>
<span id=cb1-124><a href=#cb1-124 aria-hidden=true tabindex=-1></a></span>
<span id=cb1-125><a href=#cb1-125 aria-hidden=true tabindex=-1></a><span class=kw>function</span> incmr<span class=op>()</span></span>
<span id=cb1-126><a href=#cb1-126 aria-hidden=true tabindex=-1></a>    <span class=cf>if</span> <span class=va>master_ratio</span> <span class=op>&lt;</span> <span class=dv>0.9</span> <span class=cf>then</span></span>
<span id=cb1-127><a href=#cb1-127 aria-hidden=true tabindex=-1></a>        <span class=va>master_ratio</span> <span class=op>=</span> <span class=va>master_ratio</span> <span class=op>+</span> <span class=dv>0.05</span></span>
<span id=cb1-128><a href=#cb1-128 aria-hidden=true tabindex=-1></a>    <span class=cf>end</span></span>
<span id=cb1-129><a href=#cb1-129 aria-hidden=true tabindex=-1></a><span class=kw>end</span></span>
<span id=cb1-130><a href=#cb1-130 aria-hidden=true tabindex=-1></a><span class=kw>function</span> decmr<span class=op>()</span></span>
<span id=cb1-131><a href=#cb1-131 aria-hidden=true tabindex=-1></a>    <span class=cf>if</span> <span class=va>master_ratio</span> <span class=op>&gt;</span> <span class=dv>0.1</span> <span class=cf>then</span></span>
<span id=cb1-132><a href=#cb1-132 aria-hidden=true tabindex=-1></a>        <span class=va>master_ratio</span> <span class=op>=</span> <span class=va>master_ratio</span> <span class=op>-</span> <span class=dv>0.05</span></span>
<span id=cb1-133><a href=#cb1-133 aria-hidden=true tabindex=-1></a>    <span class=cf>end</span></span>
<span id=cb1-134><a href=#cb1-134 aria-hidden=true tabindex=-1></a><span class=kw>end</span></span></code></pre></div><h3 id=migrating-config.h>Migrating config.h</h3><p>Migrating from dwl’s config.h to river’s init file was pretty easy, especially with the help of some regex replacements. Since river’s init can be written in anything, I just used bash scripts. I just had to remember to quote <code>*</code> and stuff, and it was seriously simpler than the <code>c</code> macros I was using in dwl.<h3 id=migrating-from-dwlb-to-sandbar>Migrating from dwlb to sandbar</h3><p>I was using the <a href=https://github.com/vetu104/dwlb>systray fork</a> of <a href=https://github.com/kolunmi/dwlb>dwlb</a> as my bar in dwl, which depended on the ipc patch. I was dreading having to set up a new bar system which also had a systray. It looked like waybar was the only one, but I would have to learn how to configure it. Then I saw <a href=https://github.com/kolunmi/sandbar>sandbar</a> which looked exactly like dwlb! I was pleasantly surprised by the similarities until I saw that they were both written by the same person, kolunmi! I realised that the systray and “bar without tags” patches were pretty easily transferable to sandbar, with only one major issue: dwlb used a socket based communication, whereas sandbar uses a pipe based communication. Since the systray requires the height of the rendered bar, it technically had to be a child process of sandbar. But since the width of systray would change, the communication had to happen from the child to parent. This meant piping the stdout of systray back into stdin of sandbar, reminiscent of the human caterpillar. Since this would prevent my status program from communicating sandbar, I used aistudio to tell me how to create another <code>fd</code> in sandbar and pipe it to that instead. Learnt a good deal about pipes I guess.<p>But river still did not have two features that I used a lot in dwl: <code>swallow</code> and <code>dim-unfocused</code>.<h2 id=patching-river-and-llms>Patching river, and LLMs</h2><p>I tried to use river without these features for a week or so. <code>dim-unfocused</code> was something I missed but could live without. The lack of <code>swallow</code> on the other hand was annoying. I searched if someone had already implemented it or if there was interest in this. There certainly was! But as people in <a href=https://codeberg.org/river/river/issues/73>this issue</a> rightly pointed out, any implementation would be quite hacky. This was fine in the “patch and maintain it yourself” ideology of dwl, but not for the centrally maintained “install and run” ideology of river. But nothing stopped me from patching it for my own personal use. Except that river is a pretty complex application, and written in zig, which I don’t know. As is the trend these days I used aistudio to help me.<p>Just asking it to implement swallow led to abysmal results, obviously. What I’ve learnt is to instead tell it to not make any assumptions, and instead ask me for information. It took a little coaxing, but with an initial tree of files, finally aistudio was able to ask accurately for the contents of the files it asked me to edit, and gave much better results.<p>It did get stuck on how it assumed a particular <code>c</code> call (<code>wl_client_get_credentials</code>) would be ported to zig, and kept suggesting different variations to try, until I looked for the source code of the library and asked it which file contents it would like to look at. Once I gave it the contents though, it was able to accurately implement the call. There was a similar issue with a file read (I imagine it has not seen a lot of zig code, since I assumed this would be elementary), but again manually providing it with the docs helped it correctly implement it.<p>And with this, <code>swallow</code> was implemented with just one bug. When I tried to <code>unswallow</code>, it crashed. When I informed aistudio of the error, it guessed an invalid null pointer exception. I again cautioned it against guessing and provided a backtrace. With this it accurately caught that the unswallow code was being run too late, after the child client was already in a half-<code>destroy</code>ed state. Fixing this and some documentation and code completion changes, we were done.<p>With the harder swallow patch done, I started a new chat with a prompt that looked like<pre><code>&gt;tree of river source

This is the structure of river window manager. In the following requests, do not make any guesses, instead ask for information. Provide concise answers. Do not add text about feeling sorry from my frustration if something doesn&#39;t work. If we hit a snag, we will look for information, not make guesses.

I want to implement a dimming method which dims unfocussed windows. I already have an implementation for dwl. This is only a starting point, if you have a better idea, we will use that.

&gt;my dim-unfocused patch from dwl</code></pre><p>This resulted in far better results and the changes were very smooth, and stepwise.<h2 id=conclusion>Conclusion</h2><p>I think this LLM business is pretty useful if you yourself know how to do it. Its a very good tool, but its just that, a tool, still. So I guess the conclusion of this post is our jobs are still somewhat kinda safe? In any case, I now have a system that will hopefully last for a year, before river 0.3.x gets dropped in favor of <code>rwm</code>, and I’ll have to migrate all over again.
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2025-09-27_185759</guid></item><item><pubDate>Tue, 16 Dec 2025 00:00:00 +0530</pubDate><title>Controlling devices by Voltage injection</title><link>https://dhruvasambrani.github.io/blog/./2025-12-16_163200</link><description>Smart-ifying things more than just light bulbs</description><content:encoded><![CDATA[
<h1 id=controlling-devices-by-voltage-injection>Controlling devices by Voltage injection</h1><p>I have a friend in Ghent, <a href=https://20akshay00.github.io/>akshay</a>. He got me back onto electronics, which I got him onto in the first place. Strange times. So I bought an ESP32 (C6 variant, this is necessary).<p>I have a projector at home. Its a shitty chinese projector that can be controlled either by the buttons on it or the remote. How to control the projector with my phone? I can communicate with an esp either via Bluetooth or wifi, but then how to use the ESP32 to control the projector? We have two ways: either emulate the IR Remote or the buttons.<h1 id=emulating-the-ir-remote>Emulating the IR Remote</h1><p>This is actually pretty easy. Connect an IR Sensor and an IR Emitter to your ESP. Use <a href=https://github.com/Arduino-IRremote/Arduino-IRremote>IRRemote.h</a> and the arduino programming language (? Environment? I don’t exactly know what it really is actually), to write to serial what was last recorded by the IR Sensor. Most cheap remotes use the NEC protocol, and so did my remote. The buttons are mapped to address <code>0x0001</code> and command <code>0x00 - 0x0C</code> for the 14 buttons on the remote.<p>This actually works great with the Arduino Uno, but not so much with the esp, for unknown reasons. I also controlled cheap led strips with the same code, which at least worked, but had really poor range. I even tried to drive the led with a 5V source and a transistor, and while it makes it much better, the range is still bad for the strips. The projector however refused to react at all. Why? I don’t know.<h1 id=emulating-button-presses>Emulating button presses</h1><p>I could try troubleshooting this, but I felt it would be more fun to emulate the button presses instead. Obviously mechanically doing this is trivial if you had so many motors, but that’s not what I had in mind. There had to be a way to directly signal the projector MC to control it. So I carefully opened up the projector housing, which was unsurprisingly easy.<h2 id=first-observations>First Observations</h2><p>I had predicted that the wiring would be very simple. There would be a wire for each button, and the button would either pull down to ground (typically) or to Vcc (rarely). My ESP would just have to match it, which would be easy if it were &lt; 3.3V as I could directly drive it from the GPIOs, and if not would require a transistor to isolate the voltages.<p>This is what I saw when I opened it up.<figure><img src=./assets/images/2025-12-16_163200-1.jpg alt="Button daughter board connected to MC with a 5 wired cable"><figcaption aria-hidden=true>Button daughter board connected to MC with a 5 wired cable</figcaption></figure><p>First of all I was smart to not just yank the top shell, since there was a wire connecting the MC to the daughter board with the buttons. Carefully unplugging and unscrewing the board, we see:<h2 id=button-board>Button board</h2><figure><img src=./assets/images/2025-12-16_163200-2.jpg alt="Front View"><figcaption aria-hidden=true>Front View</figcaption></figure><figure><img src=./assets/images/2025-12-16_163200-3.jpg alt="Back View"><figcaption aria-hidden=true>Back View</figcaption></figure><p>As you can see, there are not enough wires (5) to match the buttons (9) for my assumption. Instead, we see pins marked <code>K0</code>, <code>K1</code>, <code>GND</code>, <code>R</code>, <code>G</code>. Now the <code>R</code> and <code>G</code> are probably the 2 colored LED, which you can confirm by tracing the traces too. Clearly K0 and K1 must carry the data of the buttons. Is it like the membrane buttons which are indexed by the X and Y to ID them? But even those need 8 pins for 16 buttons. Looking at the traces more carefully we see that K0 is connected to 8 of the buttons, with small resistors on the traces. Clearly, pressing the button will complete the circuit, engaging exactly one of these resistors (trace it and you’ll see). Presumably then the MC sensor reads the resistance between K0 and ground to figure out which button is pressed. How does it do that? It sets some <code>Vcc</code> and has a large internal resistor <code>R0</code> before pin <code>K0</code>. When the circuit completes, the voltage at <code>K0</code> gets pulled down by the button, but not to 0, rather to a voltage given by:<p><span class="math display">\[V_i = V_{cc} \frac{R_i}{R_0 + R_i} \approx V_{cc} \frac{R_i}{R_0}\]</span><p>These values could be found by just turning on the projector and reading the voltages while pressing buttons. <code>Vcc</code> was around 1.85V, so I could safely continue with using the ESP.<p>All we need to do is emulate the same action.<p>The obvious way is to place a transistor in switch mode in parallel to each button, thus providing a secondary path for the current to flow, allowing either switch (original or transistor) to work as expected. But this will need finicky soldering to each button, and there would be no space for the button caps to sit. The alternative is to set the voltage by an analog write from the ESP to <code>K0</code>.<h2 id=pwm>PWM</h2><p>But the ESP32 C6 variant does not have an onboard DAC (I am reasonably sure about this, but I did see some sources claiming that it does have one), and can only PWM. I hoped that the PWM was by itself fast enough for the microcontroller’s inherent capacitance, but clearly that was not enough. I then tried to make an RC filter, but this failed too since I only had a 25uF electrolytic cap or a 100pF ceramic cap, and nothing in between. The 25uF was way too large and had a very long response time, and instead of instantly setting the voltage to 0, it would slowly go down to it, triggering all the other buttons in between. This was obviously not going to work. The 100pF cap was too small and I don’t think it actually did anything. I don’t have an oscilloscope, but I made a make-shift one with an arduino reading A0 and writing to a serial plotter. Obviously this did not have the time resolution necessary for the task, so it wasn’t clear why it didn’t work in spite of picking large R values 🤷‍♂️.<h2 id=dac>DAC</h2><p>Searching online a little, the MCP4725 was a chip that supported I2C communications and did DAC. I ordered one off aliexpress and waited for a week. It came, I tested it. Since I had not soldered the pins yet, there were loose connections that initially made me think the chip didn’t work, but it actually worked very well. 200ms is a good delay for it to register as a single click.<p>And so there we had it! I could now control the projector with my phone by sending the esp commands. Except…<h1 id=audio>Audio</h1><p>Turns out there are no volume buttons on the projector. I don’t know why. Anyway, the music is output via an aux cable to some old Logitech speakers which have a potentiometer controlling the audio. Presumably I can emulate the volume level by setting the voltage on the output of the potentiometer? So I opened it up to see what voltage the potentiometer was being run on. And I got 0V. Strange. What was stranger was as I was testing for continuity, I touched some pin by mistage and the speakers gave a sound. There should be no reason for a volume control to give a sound right? Thats when I thought a bit more and realised the aux cable was directly coming into the control. Asking AI what this could mean, I realised that the audio signal itself was manipulated by the resistor. By acting as a divider, the amplitude of the audio signal was damped, which obviously could not be controlled by setting voltage. So turns out I have to actually emulate a variable resistance, not a variable voltage. This can be done in one of the following ways:<ol type=1><li>A DigiPot chip (MCP4131): This is the resistor brother of a MCP4725. In fact one could build a poor DAC from a DigiPot, but not the other way around.<li>A transistor in its active phase: This is super finicky, and the transistor has to be very carefully tuned to prevent it from going out of the linear response region, and is hard to control.<li>A Resistive optoisolator: Also called a Vactrol, this uses an Light dependant resistor (Light sensor) controlled by an LED. By setting the LED brightness, one can set the LDR resistance, thus allowing for signal damping. This I could beild with components I have, but is finicky.</ol><p>The final issue with audio control is that I cannot easily run both controls in parallel. They would both perform a dampings, causing 2 controls rather than one. The only way is to desolder the pot, and connect it to the ESP, which would have to do some logic to keep both controls in sync. Maybe I could replace it with a rotary encoder, and thus only have a “volume up/down” control rather than setting a specific volume. Of course, running a motor to physically rotate the pot is always an option 🙃.<p>Considering that I will anyway set the volume from the source device, I felt I would not gain much from this. But then again, I don’t see what I’m gaining from using my phone rather than the remote either 😅.<p>That’s it for today.
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2025-12-16_163200</guid></item><item><pubDate>Sun, 04 Jan 2026 00:00:00 +0530</pubDate><title>Tailscale on Kindle</title><link>https://dhruvasambrani.github.io/blog/./2026-01-04_233221</link><description>How to get tailscale on a jailbroken Kindle Gen 8</description><content:encoded><![CDATA[
<h1 id=tailscale-on-kindle>Tailscale on Kindle</h1><p>Tailscale just posted a <a href=https://tailscale.com/blog/tailscale-jailbroken-kindle>blogpost</a> on getting tailscale to work on kindle. I had tried this before, but I had to make some major changes before it worked on my old kindle. Since jailbreaks are most useful on old tech anyway, I thought it was prudent to write a post highlighting what went wrong and what I had to fix.<h1 id=jailbreaking-the-kindle>Jailbreaking the Kindle</h1><p>I have a Gen 8 Kindle Basic from… 2015?<p>I have tried to get self-hosted reading forever, and the biggest issue was getting epubs on kindle. Calibre is so good, its basically jellyfin for books, but it needs a cable to connect to any device. You could use the content server, but I don’t think you can start it headless on a server. Also I don’t think you can do progress syncing, but I’m probably wrong. Even if that wasn’t an issue, the default kindle firmware doesn’t allow you to read epub files, which are probably the most ubiquitous. There is no open source application that you can use on an Android phone either. I kind of left this for almost 5-6 years, but still kept bringing it with me on every move.<p>Regardless, with the <a href=https://kindlemodding.org/jailbreaking/WinterBreak/>WinterBreak</a> jailbreak released, it becomes a lot easier. After applying the jailbreak, I also installed the KUAL framework, and then the <a href=https://koreader.rocks/>KOReader</a> application. This also has an android application, and lots and lots of plugins. It also has a progress sync, but it syncs to a public server.<p>As I did this, I started thinking about my <a href=2025-04-22_155445>Currently selfhosted services</a>. It should be easy enough to self-host <a href=https://github.com/crocodilestick/Calibre-Web-Automated/>Calibre-Web-Advanced</a>, which also has a sync plugin (it’s basically a slightly modified version of the ProgressSync plugin that comes default with KOReader). It also provides an OPDS endpoint so I can get files onto the kindle.<p>But all this depends on the kindle being able to connect to the server.<h1 id=connect-to-tailscale>Connect to Tailscale</h1><p>I already use Tailscale for <a href=2025-04-21_200237>Self Hosting without a public IP address or domain</a>, so I just had to get the kindle on the tailnet. I tried to use subnet-routing, but that only works for <em>incoming</em> connections, not outgoing.<p>That’s when I came across <a href=https://github.com/mitanshu7/tailscale_kual>Tailscale for KUAL</a>. I tried to install and run it, and it worked! … Almost. See <a href=https://github.com/mitanshu7/tailscale_kual/issues/2>mitanshu7/tailscale_kual#2</a>. However, because my kindle kernel does not have the tun.ko kernel module compiled in, I could not use the fix in that issue. The socks5 proxy fix and some AI asking, it told me to instead use the http proxy setting. Note that this is a good time to start using the ssh plugin on the kindle to ssh in!<p>While the following changes also work for the KUAL plugin above, I realised that the <a href=https://github.com/victoria-riley-barnett/koreader-tailscale>koreader plugin for tailscale</a> would probably be easier to work with. It does basically the same thing.<p>So I changed the lines<div class=sourceCode id=cb1><pre class="sourceCode sh"><code class="sourceCode bash"><span id=cb1-1><a href=#cb1-1 aria-hidden=true tabindex=-1></a><span class=co># Start daemon</span></span>
<span id=cb1-2><a href=#cb1-2 aria-hidden=true tabindex=-1></a><span class=fu>nohup</span> ./tailscaled <span class=at>--statedir</span><span class=op>=</span>/mnt/us/tailscale/bin/ <span class=op>&gt;</span> tailscaled.log <span class=dv>2</span><span class=op>&gt;&amp;</span><span class=dv>1</span> <span class=kw>&amp;</span></span></code></pre></div><p>in <code>./bin/start_tailscale.sh</code> with<div class=sourceCode id=cb2><pre class="sourceCode sh"><code class="sourceCode bash"><span id=cb2-1><a href=#cb2-1 aria-hidden=true tabindex=-1></a><span class=co># Start daemon</span></span>
<span id=cb2-2><a href=#cb2-2 aria-hidden=true tabindex=-1></a><span class=fu>nohup</span> ./tailscaled <span class=at>--statedir</span><span class=op>=</span>/mnt/us/tailscale/bin/ <span class=at>--tun</span><span class=op>=</span>userspace-networking <span class=at>--outbound-http-proxy-listen</span><span class=op>=</span>localhost:8080 <span class=at>--socks5-server</span><span class=op>=</span>localhost:1055 <span class=op>&gt;</span> tailscaled.log <span class=dv>2</span><span class=op>&gt;&amp;</span><span class=dv>1</span> <span class=kw>&amp;</span></span></code></pre></div><p>Now setting the <code>HTTP proxy</code> variable in <code>Settings > Network > HTTP Proxy</code> to <code>http://localhost:8080</code> should have just worked. But when has life ever been so kind?<h1 id=lua-shenanigans-and-vibe-coding>Lua shenanigans and vibe coding</h1><p>The <code>CWASync</code> plugin was not able to connect to the server in spite of the http proxy. The proxy itself worked, I checked by <code>curl</code>ing from the <code>ash</code> shell plugin with the <code>http_proxy</code> envvar set. The OPDS plugin and<p>There were no logs to <code>crash.log</code> either. With a lot of help from AIStudio I added logging to check why the connection was failing to my tailscale server. It initially looked like <code>SPORE</code> was the issue, but the error log string was not found in the Spore library. So I just <code>grep</code>ed the string in the <code>common</code> folder in koreader. This way, I finally realised that this was because the <code>luasec</code> library <a href=https://github.com/lunarmodules/luasec/blob/989ee5f993c4d2486c25763c079b9f3163336dc9/src/https.lua#L124>does not support</a> https over a proxy. There was an <a href=https://github.com/lunarmodules/luasec/pull/102>attempt</a> to add this feature, but it seems the original author never finished it.<p>So I asked AIStudio to vibe code the fix. It did surprisingly well, except it seemed that the initial connection to the http proxy was also going through ssl, which was not necessary. The easy way was to simply bypass when a local connection was required. Ideally it should implement <code>noproxy</code> support, but 🤷.<p>I added this fix to <a href=https://github.com/DhruvaSambrani/luasec/tree/add-proxy-redirect-custom>my fork of luasec</a>.<p><strong>To use it in my KOReader build</strong>, I just had to replace <code>koreader/common/ssl/https.lua</code> with the new file. Now everything works!<h1 id=upstreaming>Upstreaming</h1><p>I don’t exactly know how to upstream this change. Should it go to <code>luasec</code>? Or should KOReader replace <code>luasec</code> with <code>lua-http</code>? Let your opinion be known on <a href=https://github.com/koreader/koreader/issues/14693>this issue</a>.<p><strong>EDIT:</strong> While the issue should probably be upstreamed somewhere, until then, a user-patch seems like a good replacement. See <a href=https://gist.github.com/MDCore/5c6821b4f71289e3afa7f24e32ee854d class=uri>https://gist.github.com/MDCore/5c6821b4f71289e3afa7f24e32ee854d</a> for details.
        ]]></content:encoded><author>dhruvasambrani19@gmail.com (Dhruva Sambrani)</author><guid>https://dhruvasambrani.github.io/blog/./2026-01-04_233221</guid></item></channel></rss>