feat: add blu-site static site generator and fix language issues
Build a complete static site generator in Lux that faithfully clones
blu.cx (elmstatic). Generates 14 post pages, section indexes, tag pages,
and a home page with snippets grid from markdown content.
Language fixes discovered during development:
- Add \{ and \} escape sequences in string literals (lexer)
- Register String.indexOf and String.lastIndexOf in type checker
- Fix formatter to preserve brace escapes in string literals
- Improve LSP hover to show documentation for let bindings and functions
ISSUES.md documents 15 Lux language limitations found during the project.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: The First Monotheist
|
||||
description: Zoroaster founded the world's first monotheistic religion, thereby opening a new chapter in world history.
|
||||
date: 2023-09-21
|
||||
tags: religion
|
||||
---
|
||||
|
||||
I'm continuing to read [_Decline and Fall of the Roman Empire_](https://www.amazon.com/Decline-Empire-Volumes-Everymans-Library/dp/0307700763/ref=sr_1_5?crid=1G5JJM2BIH17N&keywords=decline+and+fall+of+the+roman+empire&qid=1695350465&sprefix=decline+and+%2Caps%2C85&sr=8-5&ufe=app_do%3Aamzn1.fos.f5122f16-c3e8-4386-bf32-63e904010ad0) by Edward Gibbon, and I learned another fun series of facts I thought worth writing about. I'd heard of [Zoroastrianism](https://en.wikipedia.org/wiki/Zoroastrianism), and knew that it had something to do with Persia, but I'd failed to remember from World History class (shocker!) that it was _the first_ monotheistic religion. Given that most people today, if they believe (or more likely, _claim to believe_, in God) they tend to believe there's only _one_. This is an extremely important development in the history of human thought, and may be owed to Zoroaster.
|
||||
|
||||
He's an extremely ancient figure, so much so that no one really knows when he lived, typically placing him somewhere between 1500 to 1000 BC. Zoroastrianism was the religion of the ancient Persians, and dominated much of the Middle East for a millennium, until being overtaken by Islam.
|
||||
|
||||
Aside from many of its very interesting ideas about free will, heaven and hell, creation and anti-creation, etc., one idea I found especially refreshing to find in minds that _long_ predate Jesus, Socrates, Confucius, and even the [first philosopher](https://en.wikipedia.org/wiki/Thales_of_Miletus) -- was the rejection of asceticism; the abstinence of sensual pleasures which often also leads to the withdrawal of societal participation and sense of social responsibility. Zoroastrianism apparently differs directly with the founders of many of our modern religions in this regard. To quote Gibbon:
|
||||
|
||||
> But there are some remarkable instances, in which Zoroaster lays aside the prophet, assumes the legislator, and discovers a liberal concern for private and public happiness, seldom to be found among the grovelling or visionary schemes of superstition. Fasting and celibacy, the common means of purchasing the Divine favour, he condemns with abhorrence, as a criminal rejection of the best gifts of Providence. The saint, in the Magian religion, is obliged to beget children, to plant useful trees, to destroy noxious animals, to convey water to the dry lands of Persia, and to work out his salvation by pursuing all the labours of agriculture.
|
||||
|
||||
One may wonder how much pointless human suffering was caused by the elevation of self-inflicted pain to an act smiled upon by God. This isn't to diminish the merits of well-directed sacrifice -- a sacrifice to some social purpose -- just sacrifice _for sacrifice's sake_.
|
||||
|
||||
Continuing with Gibbon:
|
||||
|
||||
> We may quote from the Zendavesta a wise and benevolent maxim, which compensates for many an absurdity. 'He who sows the ground with care and diligence, acquires a greater stock of religious merit, than he could gain by the repetition of ten thousand prayers.'
|
||||
|
||||
While the rejection of asceticism doesn't exist without the opposing danger of hedonism, and while I'm sure a closer look at Zoroastrianism would reveal many strange features, it is interesting to see a religion that _explicitly_ celebrates the application of our efforts toward positive social utility. If you're going to be judged by God, it's nice to be _judged by whether you do good in the world_, instead of being judged simply by the degree to which you abstain from participating in its evils.
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Reading, Writing, and Civilization
|
||||
description: Gibbon on the importance of reading and writing to civilization, and what distinguished the Romans from the 'barbarians', i.e. the Germans
|
||||
date: 2023-09-26
|
||||
tags: reading
|
||||
---
|
||||
|
||||
The importance of reading seems to need more and more explaining these days as people convince themselves they have no need for it (or because they simply have a hard time sitting still). Every once in awhile I'll stumble across yet another explanation for why reading, and by reading I mean reading _great books_, is so important.
|
||||
|
||||
Here's Gibbon on the importance of reading and writing to civilization, and what distinguished the Romans from the "barbarians", i.e. the Germans:
|
||||
|
||||
> The Germans, in the age of Tacitus, were unacquainted with the use of letters; and the use of letters is the principal circumstance that distinguishes a civilized people from a herd of savages incapable of knowledge or reflection. Without that artificial help, the human memory soon dissipates or corrupts the ideas intrusted to her charge; and the nobler faculties of the mind, no longer supplied with models or with materials, gradually forget their powers; the judgement becomes feeble and lethargic, the imagination languid or irregular. Fully to apprehend this important truth, let us attempt, in an improved society, to calculate the immense distance between the man of learning and the _illiterate_ peasant. The former, by reading and reflection, multiplies his own experience, and lives in distant ages and remote countries; whilst the latter, rooted to a single spot, and confined to a few years of existence, surpasses, but very little, his fellow-labourer the ox in the exercise of his mental faculties. The same, and even a greater, difference will be found between nations than between individuals; and we may safely pronounce that, without some species of writing, no people has ever preserved the faithful annals of their history, ever made any considerable progress in the abstract sciences, or ever possessed, in any tolerable degree of perfection, the useful and agreeable arts of life.
|
||||
|
||||
-- _Decline and Fall of the Roman Empire, Ch. IX, p. 242_
|
||||
89
projects/blu-site/content/blog/2025-01-29-blog-revamped.md
Normal file
89
projects/blu-site/content/blog/2025-01-29-blog-revamped.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: Toward A Better Site
|
||||
description: A more thoughtful approach to personal site development
|
||||
date: 2025-01-29
|
||||
tags: web blog
|
||||
---
|
||||
|
||||
> We shall not cease from exploration
|
||||
> And the end of all our exploring
|
||||
> Will be to arrive where we started
|
||||
> And know the place for the first time.
|
||||
|
||||
- [T.S. Eliot, from “Little Gidding,” Four Quartets. Originally published 1943.](https://www.columbia.edu/itc/history/winter/w3206/edit/tseliotlittlegidding.html)
|
||||
|
||||
This past year, I've experimented with several different methods of creating and managing a personal website. I didn't merely want the most convenient solution, or the solution most likely to buy popularity more easily via its platform or network effects. I wanted a solution that was in line with:
|
||||
|
||||
- 1. A growing set of principles I want to practice see more of in software I use and create.
|
||||
- 2. A set of very unique and specific features I desired.
|
||||
|
||||
The pursuit of principles and features, of course, give way to the brutal constraints of reality to some degree. I don't have infinite time. And so the desire to obtain the features I want while maintaining those principles must be balanced with the constraints of time and effort. After experimenting with several iterations of website-building, I have in many ways "arrived where I started", but now feel more confident, more sure-footed, more "aligned", and more excited about what even a simple little personal website could be.
|
||||
|
||||
### First Site — A DIY Compromise
|
||||
|
||||
I started out as most Gen-Z developers start, I think. You want to be a developer, you need a portfolio. You want to be a _web developer_? Your portfolio is your website. And so my first website was just that. I built it during many skipped classes in college, while I was still painstakingly and inefficiently learning HTML, CSS, and Javascript. It was designed to be artsy and beautiful and to get me a job as a web developer. And so I didn't post or write about anything, just listed what I felt would be most impressive to recruiters. But, after proving to yourself that you can indeed get a job and you can indeed make enough to pay your rent and not be a broke college kid forever, you start to realize there's more important things in life than just trying to impress recruiters. You realize that a personal website can be so much more. It can be a _reflection of yourself_.
|
||||
|
||||
And so, with that in mind, I built my first "real" attempt at a personal site. I built it in [Sveltekit](https://svelte.dev/) (which was in unstable version 3 at the time), used the [MDSveX](https://github.com/pngwn/MDsveX) library so I could write my articles in [Markdown](https://en.wikipedia.org/wiki/Markdown). This was a proper blogging site that also listed projects and "About Me" and "Contact" and whatnot, however there were several problems:
|
||||
|
||||
- **Hosting**: It was hosted on Netlify, which meant that there were often cold starts when visiting it. This also meant that I wasn't hosting the site myself, which wasn't a big deal to me at the time, but developed into one over the course of the year.
|
||||
- **Features**: The site was low on features and bells and whistles, but this was mainly because I just didn't take the time to implement them. The fact that everything was written in Sveltekit meant that I could have, but I wasn't taking the time to really think about what I wanted implemented and then come up with a good plan to do it.
|
||||
- **Preprocessing the Markdown**: I didn't quite understand how to set up the preprocessor in MDSveX properly, which does the job of converting the Markdown to HTML when building the site. What I _think_ was happening was this resulted in pages having to convert the Markdown to HTML on the fly during runtime, and so the bigger the article, the slower the page load, but I never truly investigating this since I decided to redo the website from the ground up anyway. To the end user, this just felt like the page was frozen for awhile, which was really annoying UX.
|
||||
- **Content and Organization**: I wasn't sure of how I wanted things organized, and I only had a few articles, blogs, and quotes on there. The way I would write my content was by writing the article in Markdown directly on the site and then just committing and pushing the code to Github, and from there it would build and deploy to Netlify automagically. That was nice, but I would prefer to be able to write the article in another program (so that I could have "draft" versions of it and whatnot) and then just commit + push when ready in a single command/click sort of way.
|
||||
|
||||
All these problems contributed to a year of experimentation that left my actual ability to write things a bit in limbo.
|
||||
|
||||
### Substack
|
||||
|
||||
I started out by just wanting something that worked, and worked well, for _writing_, without having to worry about anything else. Substack is the obvious choice for this. It's famously anti-censorship, has an intuitive UI, a large audience, most of my own subscriptions worth reading are from Substack, easy subscription tiers and connection to Stripe for payments, easy email subscriptions. What's not to like?
|
||||
|
||||
There was one thing that just ended up being a deal breaker for me. Probably a bizarre requirement to most people, but I just couldn't in good conscience do it. At the time, I was considering requiring subscriptions and payments for some of the articles I wrote, and on Substack, there's _no option for bitcoin payments_. The saddest thing of all is, it appears that there _was_ a way to pay with bitcoin while [Substack was using OpenNode's API](https://opennode.com/blog/bitcoin-payments-with-substack/), but don't any longer. I suppose that's a price of vendor lock-in. It is now Substack's [official policy](https://support.substack.com/hc/en-us/articles/360037862551-I-can-t-use-Stripe-Do-you-accept-PayPal-crypto-clamshells-etc) to _not_ support any bitcoin/crypto payments. I work in the Lightning Network _because_ the payments use case of bitcoin is one of if not it's most exciting aspect. It's probably one of the strongest ways bitcoin can positively influence how we use the internet. To only allow credit card subscriptions, and not bitcoin lightning micropayments, for paywalled articles if I did any would be sad and hypocritical of me. There are other reasons of course, like the fact that Substack isn't open source and that I wouldn't actually own my own data. The idea that my blog dies when the company dies was another major negative.
|
||||
|
||||
So what other options were there that fit my increasingly stringent criteria?
|
||||
|
||||
### Ghost
|
||||
|
||||
Enter [Ghost](https://ghost.org), a website which markets itself as essentially the more [principled and feature-rich version of Substack](https://ghost.org/vs/substack/) (which I believe they are). They are fully open source and you can run the blog software locally yourself, deploy it on a VPS of your choice, setup your own email provider, etc. If you choose to pay for them to host it for you, it's only $9 a month and that includes the ability to bring your own domain, email subscriptions, etc. A huge improvement to me, and a project and team that I was (and still am) very excited about.
|
||||
|
||||
They even have a marketplace of templates and integrations, and given their open-source spirit I was hopeful they'd have some sort of bitcoin integration on there — and there is one...that you have to pay for. It's called [Scribsat](https://scribsat.com/); here's the Ghost Forum [post](https://forum.ghost.org/t/a-new-bitcoin-payments-integration-scrib/36125/4) about it. They charge a monthly fee steeper than Ghost's hosting fees itself to allow you to accept bitcoin lightning payments on Ghost. That didn't really fit the bill for me, either, and I don't want to be forced to depend on another service to accept the payments for me. For what it's worth, this does seem to be a [hot topic](https://forum.ghost.org/t/bitcoin-payments/10514/37) on the Ghost Forum, and I hope that someone will take up the call of developing something people can use for this. Ghost is a great tool and company, [used by Bitcoiners](https://blog.lopp.net/), and would be powerful to many if properly combined with bitcoin payments.
|
||||
|
||||
I considered working on this myself, but there is significant overhead that I just didn't and don't have the time (for now) to take up.
|
||||
|
||||
For awhile, the lack of bitcoin-based payment options was worth it, and I paid for a full year's subscription even though I wasn't sure if I'd use it for long (I didn't, but I consider it a tithe to open-source software). I loved the writing and publishing experience, which was extremely simple, and there were a good number of default themes to choose from. However, none that I _loved_, and I found myself longing for more customizability. For most bloggers who can pay a few bucks a month or have the wherewithal to self-host, I would definitely recommend Ghost.
|
||||
|
||||
### Inspiration Draws From Disparate Sources
|
||||
|
||||
> Shall I tell you the secret of the true scholar? It is this: Every man I meet is my master in some point, and in that I learn of him.
|
||||
|
||||
- [Ralph Waldo Emerson, _Greatness_](https://wist.info/emerson-ralph-waldo/65482/)
|
||||
|
||||
My website was hosted on Ghost for a couple months, and it actually allowed me to write an article or two and post a quote here and there, and I was starting to give up on monetizing it (was I ever really doing it for that anyway?).
|
||||
|
||||
But around this time I was coming into greater contact with online presences that helped me to think differently, and though they all differ radically in occupation, style, and personal beliefs, they share an energy for creating good things meant to _last_; a passion for thinking long-term and for following their hearts over easy money and fame. A desire to achieve greatness in some form or other, and to have the revenues of their considerable efforts paid in something deeper than cash.
|
||||
|
||||
One of those I stumbled across was [Luke Smith](https://lukesmith.xyz/), a thoroughly original person, whose unique admixture of Twitter-like meme humor, dead-serious power usage of FOSS-only tech, deep historical, philosophical, and linguistic knowledge, and opinionated take on Christianity fascinated me, even if I didn't agree with many of his opinions and means of presenting that information. It's rare to find a person occupying all those particular spheres simultaneously, particularly in technology. From him I learned to ask for more from my software, to seek and value the right to and practice of _ownership_, whether that's in owning your own land, owning your own money (bitcoin, being debt-free), or owning your own technology to the highest degree possible. He even [removed all credit card-based ways to donate to him](https://lukesmith.xyz/updates/retiring-fiat-donation-portal/) on his website, as they weren't in keeping with his principles. I used his tutorial website on becoming an [Internet Landlord](https://landchad.net/) by self-hosting a website on your own DNS, for example, to build and host the version of this site at the time of writing. Another thing that is important to him is to have technology be a _tool you use_, rather than you be _its' tool_. To that end, he helped encourage me to try to build and use software that will last, and focus less on the ephemeral. Not an easy pursuit in the software world, open source or otherwise!
|
||||
|
||||
Another figure who influenced this blog is [Henrik Karlsson](https://www.henrikkarlsson.xyz/), whose article [_A blog post is a very long and complex search query to find fascinating people and make them route interesting stuff to your inbox_](https://www.henrikkarlsson.xyz/p/search-query) had a deep impact on me and the direction of my thinking about how a blog should be made. It is excellent, both the idea and the read.
|
||||
|
||||
[Zig](https://ziglang.org/) creator Andrew Kelley, also comes to mind, whose article [_Why We Can't Have Nice Things_](https://andrewkelley.me/post/why-we-cant-have-nice-software.html) helped me realize the incredible waste that often goes into making products worse when money alone is the goal.
|
||||
|
||||
[Visakan Veerasamy](https://visakanv.com/)'s article [_We Were Voyagers_](https://visakanv.substack.com/p/we-were-voyagers) is also an amazing and deeply encouraging read, and while it didn't directly influence how I built this website, it further influenced the "do something useful to others with your life in a spirit of gratitude" approach I'm trying to take to life in general, and will have an effect on the prose and content of the site if nothing else.
|
||||
|
||||
But the final major influence was [gwern.net](https://gwern.net/), whose beautiful and carefully crafted website design is matched by its rich content and elegant prose. It's filled with a hundred little features, optimizations, and design decisions that make it a joy to use. For example, it has custom [page previews](https://www.mediawiki.org/wiki/Page_Previews) which allow you to see a preview of where a link will take you when hovering over it. It has tables of contents and footnotes for each article, and the footnotes become sidenotes on wide screens for convenience. He takes the time to design artsy [dropcaps](https://en.wikipedia.org/wiki/Initial) for each article. And something I found very interesting that I've never seen before, is his extensive use of local archives to fight against [linkrot](https://en.wikipedia.org/wiki/Link_rot), which is an important problem if we wish to preserve something of ourselves for the future. Read his [about page](https://gwern.net/about) for the site to learn more.
|
||||
|
||||
All told, Gwern's website appears to me an exercise in how to create an optimal personal website for the long-run. He summarizes the fundamental question of building a site from the link above:
|
||||
|
||||
> What does it take to present, for the long-term, complex, highly-referenced, link-intensive, long-form text online as effectively as possible, while conserving the reader’s time & attention?
|
||||
|
||||
This version of my site, though still nascent, is based on a fuzzy mesh of these influences, and I will make the attempt to improve it and make it more resilient and higher quality over time.
|
||||
|
||||
### Knowing the Place for the First Time
|
||||
|
||||
Who was I kidding? I'm a _web developer_. I should've known from the beginning it would feel wrong to me to outsource that task, despite the work it may have taken. So after a year of exploration, I'm back at doing the same thing I did before — building the website in Sveltekit and MDsveX. But hopefully this time better, and having learned a lot about how to practically achieve that in addition to learning my own reasons for trying to do so in the first place.
|
||||
|
||||
Before, building my own website was a cool thing to put on my resume. But this time I have my _reasons_.
|
||||
|
||||
### What Now?
|
||||
|
||||
My primary reason for building this site is to document everything I do that I think might be of use to someone else (or my future self). I want something that I built myself and which I have full control over, whose format I have the freedom to modify or restructure at any time (which I couldn't do in my previous methods). I want something that allows me to publish articles, blogs, daily journals, quotes I find interesting, projects I'm working on, what have you, in as frictionless a manner as possible while still retaining maximum expressive possibilities. Which, in essence, means I have to build things myself, and that there will be a high up-front cost to doing so.
|
||||
|
||||
So be it!
|
||||
43
projects/blu-site/content/blog/2025-02-12-asahi-boot-fix.md
Normal file
43
projects/blu-site/content/blog/2025-02-12-asahi-boot-fix.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: Hard Reset Asahi Linux After Boot Error and Data Backup
|
||||
description: dnf update may cause a boot into a black screen. Here's how you can reset your computer.
|
||||
date: 2025-02-12
|
||||
tags: linux software
|
||||
---
|
||||
|
||||
[Asahi Linux](https://asahilinux.org/) is a port of Linux on Apple Silicon (the M-series chips).
|
||||
|
||||
I've been using it fairly happily for awhile, but it booted into a black screen recently. I had a flash drive plugged in and I had also installed new programs using `dnf` (The Fedora package manager, which is the distro Asahi uses), and after some reddit searching it appears that the problem was caused by something having to do with `dnf update`. After restarting the computer, it showed the Apple logo, Asahi logo, and Fedora logo in sequence as usual, and then never booted past a black screen. Unfortunately I wasn't really able to "fix" it, but I was at least able to get into a terminal screen, mount a flash drive, backup my data, and then do a hard reset.
|
||||
|
||||
On boot, press and hold FN + CTRL + OPTION + F2. This should take you into a terminal-based login screen. Login with your normal user credentials. Then plugin a flash drive. Identify the device name by typing `lsblk`. It should appear as `/dev/sda` or something of the like. You should be able to identify it by its' file size (e.g. 32GB, etc.). Then, mount the device. First, you should create a mount point using `mkdir`, such as in `/mnt`:
|
||||
|
||||
Create a mount point:
|
||||
|
||||
```
|
||||
sudo mkdir `/mnt/mydrive`
|
||||
```
|
||||
|
||||
Mount the device:
|
||||
|
||||
```
|
||||
sudo mount /dev/sda /mnt/mydrive
|
||||
```
|
||||
|
||||
Copy files from your `home` directory (or wherever you have data stored you want to keep) to the flashdrive:
|
||||
|
||||
```
|
||||
cp -r ~/* /mnt/mydrive
|
||||
```
|
||||
|
||||
Unmount and eject the drive when done:
|
||||
|
||||
```
|
||||
sudo umount /mnt/mydrive
|
||||
sudo eject /dev/sda
|
||||
```
|
||||
|
||||
### Repartition the Hard Drive
|
||||
|
||||
Now, you'll need to do a complete uninstall/reinstall of Asahi Linux by repartitioning the hard drive on your Mac.
|
||||
|
||||
I followed [this tutorial video](https://www.youtube.com/watch?v=nMnWTq2H-N0&t=7s) to help me repartition the Mac properly, and it worked like a charm. Then, I was able to go through the same process of reinstalling Asahi again and using my Apple Silicon machine with Linux.
|
||||
53
projects/blu-site/content/blog/2025-02-12-syncthing-nb.md
Normal file
53
projects/blu-site/content/blog/2025-02-12-syncthing-nb.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: File Synchronization For All Your Devices with Syncthing
|
||||
description: How to synchronize files across many devices automagically
|
||||
date: 2025-02-12
|
||||
tags: open-source software
|
||||
---
|
||||
|
||||
I use the [TUI](https://en.wikipedia.org/wiki/Text-based_user_interface)-based tool [nb](https://github.com/xwmx/nb) for writing things. But I have a few problems:
|
||||
|
||||
1. I use it on a lot of different computers and often want to add to notes that only exist on computer A while I'm on computer B.
|
||||
2. Since everything I write on my website is in Markdown, I want to have the ability to have what I place in that notebook automatically update the website (or at least update it after running some command)
|
||||
|
||||
Solving Problem 1 will be the focus in this article. In a subsequent post, I plan to detail solving problem 2.
|
||||
|
||||
## Requirements
|
||||
|
||||
I want a system that _automatically_ syncs files across devices that have it installed on and connected to at an interval I define when the machine comes online.
|
||||
|
||||
## Syncthing
|
||||
|
||||
After prompting [Perplexity.ai](https://www.perplexity.ai/) about the issue, two methods which appealed to me are [Syncthing](https://syncthing.net/) and Git. Using Git would've been more manual and time-consuming, and since Syncthing is a very popular tool and fully open source, I went with that option.
|
||||
|
||||
## Installing
|
||||
|
||||
Syncthing has a great ["Getting Started"](https://docs.syncthing.net/intro/getting-started.html) page that gave me everything I needed to know to use it. Just download the app on two devices. Select the architecture you're using from their releases page [here](https://github.com/syncthing/syncthing/releases). Follow their Getting Started page and you should have all the basics to get up and running.
|
||||
|
||||
## Helpful Notes
|
||||
|
||||
### Use Cases
|
||||
|
||||
Syncthing is not really meant to be a backup software like [rsync](https://linux.die.net/man/1/rsync), because changes made on one machine will synchronize to the other machines, and therefore if one machine deletes a file it will also be deleted on the other machine. It's meant to be a _sharing_ software keep documents on different computers synchronized, as the name suggests. Which means that it should really be used for things like media libraries or as an alternative to cloud storage solutions like Box or Google Drive, or when needing to edit documents on multiple machines, as in my case with `nb`.
|
||||
|
||||
### Mobile Devices
|
||||
|
||||
I wanted one of my devices to be my phone, so just note that for mobile (i.e. by mobile I mean _Android_ or a security-hardened derivative like [GrapheneOS](https://grapheneos.org/) — sorry iPhone fans, but their anti open source ethos precludes you from using this one) you have to use a [fork](https://github.com/Catfriend1/syncthing-android) of their [now deprecated Android app](https://github.com/syncthing/syncthing-android). You can download the apk file [here](https://github.com/Catfriend1/syncthing-android/releases).
|
||||
|
||||
### Folder Types
|
||||
|
||||
There are [three types](https://docs.syncthing.net/users/foldertypes.html) of folders:
|
||||
|
||||
- Send Only
|
||||
- Receive Only
|
||||
- Send and Receive
|
||||
|
||||
This is useful for sharing files while still setting some basic permissions for them. Send Only is great for sharing with people/devices that you don't want to be able to modify your local copy via their modifications; Receive Only is the opposite — it can only _be shared with_, and yet changes from the sending folder will not modify changes in the receiving folder.
|
||||
|
||||
### The FAQ is Very Helpful
|
||||
|
||||
Initially when using Syncthing I was confused about a few things (Should I use this for backups? Why does syncing take so long? How does it work? Will it ever connect to anything other than my own machines and can they see my data?). The [FAQ](https://docs.syncthing.net/users/faq.html#what-is-syncthing) answered all my questions very nicely and in detail, so be sure to give that a read.
|
||||
|
||||
### Note For Obsidian Users
|
||||
|
||||
[Obsidian](https://obsidian.md/) is an (unfortunately closed-source) note-taking app that pairs nicely with `nb`. I use `nb` on all my desk/laptop machines and use Obsidian on mobile since trying to use a terminal on mobile would be a terrible experience. After installing Syncthing-Fork on your mobile device and syncing the relevant folder, you can open that folder as your Vault in Obsidian. Before making any changes in Obsidian, however, you should disable a setting in Syncthing. Click on the synced folder, scroll down to "Watch for changes", and disable it. This will make it so changes to that synced directory will only attempt to sync changes to the others on an hourly basis. Obsidian saves files to disk extremely frequently, basically on every keystroke, and this caused Syncthing to behave strangely or simply not sync any changes made from Obsidian to the other devices. This is a bit unfortunate because there's a possibility that you could update the same file on your mobile device and another machine within that hour, causing a conflict. But fortunately Syncthing will save both versions of the file in a "conflict" file and you will have both versions so that there's no content loss.
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
title: Automating Site Deployment for an `nb` -> Github -> VPS Setup
|
||||
description: ""
|
||||
date: 2025-02-13
|
||||
tags: open-source software self-hosting
|
||||
---
|
||||
|
||||
As mentioned in the blog post on [syncing my `nb` notebook across devices using Syncthing](../blog/syncthing-nb.md), I wanted a way to auto-publish to my site after finishing a piece of writing. What I ended up coming up with was having two notebooks: _draft_ and _final_, which were structurally the same as the markdown folder in the actual website code. When done with a piece in _draft_, I `cp` it to the appropriate place in `final`, then run a bash syncing script which copies all files from _final_ to the corresponding markdown folder in my Sveltekit website repo, `git commit`'s the results, and pushes it to the remote repository. Here is the bash script I'm using, in case anyone else finds it useful. Note the environment variables which must be defined before running. It's really just a directory copying + `git commit` script:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Script to copy files from nb drafts to site code, then commit and push
|
||||
|
||||
# --- Check environment variables ---
|
||||
if [ -z "$SITE_GIT_REPO_DIR" ]; then
|
||||
echo "ERROR: SITE_GIT_REPO_DIR is not set."
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$NB_FINAL_DRAFT_DIR" ]; then
|
||||
echo "ERROR: NB_FINAL_DRAFT_DIR is not set."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$NB_FINAL_DRAFT_IMAGES_DIR" ]; then
|
||||
echo "ERROR: NB_FINAL_DRAFT_IMAGES_DIR is not set."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$SITE_CODE_MD_DIR" ]; then
|
||||
echo "ERROR: SITE_CODE_MD_DIR is not set."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$SITE_CODE_MD_IMAGES_DIR" ]; then
|
||||
echo "ERROR: SITE_CODE_MD_IMAGES_DIR is not set."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Copy files ---
|
||||
|
||||
# Copy markdown files
|
||||
echo "Copying files from $NB_FINAL_DRAFT_DIR to $SITE_CODE_MD_DIR..."
|
||||
cp -r "$NB_FINAL_DRAFT_DIR"/* "$SITE_CODE_MD_DIR"/
|
||||
|
||||
# Check if the copy command was successful.
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: Failed to copy files from $NB_FINAL_DRAFT_DIR to $SITE_CODE_MD_DIR."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy image files
|
||||
echo "Copying files from $NB_FINAL_DRAFT_IMAGES_DIR to $SITE_CODE_MD_IMAGES_DIR..."
|
||||
cp -r "$NB_FINAL_DRAFT_IMAGES_DIR"/* "$SITE_CODE_MD_IMAGES_DIR"/
|
||||
|
||||
# Check if the copy command was successful.
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: Failed to copy files from $NB_FINAL_DRAFT_IMAGES_DIR to $SITE_CODE_MD_IMAGES_DIR."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Git Commit and Push ---
|
||||
|
||||
# Navigate to the Git repository directory
|
||||
cd "$SITE_GIT_REPO_DIR"
|
||||
|
||||
# Check if it's a git repository
|
||||
if [ ! -d ".git" ]; then
|
||||
echo "ERROR: $SITE_GIT_REPO_DIR is not a git repository."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Add the changed files (only the md and images directories)
|
||||
echo "Adding files to git..."
|
||||
git add "$SITE_CODE_MD_DIR" "$SITE_CODE_MD_IMAGES_DIR"
|
||||
|
||||
# Check if there are any changes to commit
|
||||
if ! git diff --cached --quiet --exit-code; then
|
||||
echo "Committing changes to git..."
|
||||
git commit -m "Update content"
|
||||
|
||||
# Push the changes to the remote repository
|
||||
echo "Pushing changes to remote origin..."
|
||||
git push origin master
|
||||
|
||||
# Check if the push command was successful.
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: Failed to push changes to remote origin."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Successfully updated remote repository."
|
||||
else
|
||||
echo "No changes to commit."
|
||||
fi
|
||||
|
||||
echo "Script completed."
|
||||
exit 0
|
||||
|
||||
```
|
||||
|
||||
## Automating Deployments to VPS
|
||||
|
||||
I also wanted to automate deployments to the VPS I was using on every push. So essentially, my website would update every time the script ran. So I put this `deploy.yml` in `.github/workflows`:
|
||||
|
||||
```yml
|
||||
name: Deploy to VPS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build SvelteKit app
|
||||
run: npm run build
|
||||
|
||||
- name: Deploy to VPS
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USER }}
|
||||
password: ${{ secrets.VPS_PASSWORD }}
|
||||
source: "build/*"
|
||||
target: "/var/www/blu"
|
||||
rm: true
|
||||
strip_components: 1
|
||||
```
|
||||
|
||||
Note that the `VPS_HOST`, `VPS_USER`, and `VPS_PASSWORD` must be defined repository/environment secrets in Github. Also note the action we're using at the end to do the actual deploy, `appleboy/scp-action`. Two parameters were important for what I was trying to do. First, `rm: true`, which deletes the target folder (`/var/www/blu` in this case) before uploading the data. In the scenario where I delete or move a file in the new build, the old file would've remained present without that parameter set, which I didn't want.
|
||||
|
||||
Second, the `strip_components: 1` is also important here. Without it, the `build` directory was getting placed inside `/var/www/blu`, instead of the _contents_ of the build directory moving into `blu`. So `strip_components` specifies that while I want to use `build/*` as the _source_ of my files, I only want _what's in it_, i.e. `build/*`, so don't put anything after the first slash (e.g. `build`) into `/var/www/blu`.
|
||||
|
||||
That's it! Now every time I update the content of the site (such as with this post), I just copy it over to _final_, then run the sync script, and voila!
|
||||
64
projects/blu-site/content/blog/2025-02-21-bitcoin-qr.md
Normal file
64
projects/blu-site/content/blog/2025-02-21-bitcoin-qr.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: <bitcoin-qr/> - A Zero-Dependency Web Component for Stylish BIP-21 Payments
|
||||
description: A QR code web component for Bitcoin on-chain, Lightning, and unified BIP-21 payments
|
||||
date: 2025-02-21
|
||||
tags: bitcoin
|
||||
---
|
||||
|
||||
<span class="flex justify-center w-[100px]">
|
||||
<img src="/images/blog/bitcoin-qr/qr.webp" alt="Bitcoin QR Example" width="594" height="596" loading="lazy">
|
||||
</span>
|
||||
|
||||
When developing a Bitcoin payment flow, there are multiple ways a user can expect to be able to pay. They might want to pay an on-chain address or Lightning invoice, they may be scanning a QR Code from their phone, copy/pasting from a wallet, or using a [WebLN](https://www.webln.guide/) browser extension. Creating an intuitive interface that also captures all the possible ways a user can pay is one of the fundamental UX challenges of developing an application that can receive payments in Bitcoin.
|
||||
|
||||
This flexibility opens up many [exciting use cases](https://blu.cx/articles/bitcoin/micropayments), but often comes at the cost of being able to easily develop a smooth experience for the user. The greater the developer's cognitive load, the more difficult it is to create intuitive UX.
|
||||
|
||||
This project aims to provide everything needed to allow standard Bitcoin & Lightning Network payments out of the box. It handles creating the proper [URIs](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki) from just an address or invoice, and favors creating unified URIs whenever possible. Styles are highly customizable and images can be embedded. It also includes polling functionality -- a callback can be passed as a property of the element to periodically check for payment.
|
||||
|
||||
Check it out on [Github](https://github.com/thebrandonlucas/bitcoin-qr) and feel free to play around with the options and create your own [here](https://bitcoin-qr.blu.cx).
|
||||
|
||||
In addition, because it's a [web component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components), you should just be able to drop it into any framework and have it work, whether it's pure HTML, Sveltekit, React, making it very versatile. Examples are included in the Github repo.
|
||||
|
||||
Here's a quick demo of how to use it in Sveltekit:
|
||||
|
||||
Install:
|
||||
|
||||
```sh
|
||||
yarn add bitcoin-qr
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { defineCustomElements } from 'bitcoin-qr/loader';
|
||||
|
||||
defineCustomElements();
|
||||
</script>
|
||||
|
||||
<bitcoin-qr
|
||||
id="qr"
|
||||
width="300"
|
||||
height="300"
|
||||
bitcoin="bc1p4mqqpxdyg3l0tvpv9xaesw5rsqv7fqpcn9ydawx3e8yr4enmc3qq7m59vd"
|
||||
lightning="lno1zrxq8pjw7qjlm68mtp7e3yvxee4y5xrgjhhyf2fxhlphpckrvevh50u0q2a8y2qt9jeu3pj7p3u58rxs98yu93dxjlmy3eqp24qze09anfr6cqszm40686ujqq7t9jsjjf42tl2pryv3ds9f4hm6hysk7lvswdm70ntsqvlgc5nm4px6k3q2wmj4xun784sss2jn00q0w4dhydmln92wvash6jckffu0wmetwptj38f5g09k8pznuk4wq2df046yv0c5m3dakwgfz5ths2ymm3z6vnehvxj6urcarufa7uecwqqst9x4063hdpxq4frmupnsam9ahg"
|
||||
parameters="amount=0.00001&label=bitcoin-qr.blu.cx&message=bitcoin-qr.blu.cx"
|
||||
image="./assets/bitcoin.svg"
|
||||
is-polling="false"
|
||||
poll-interval="1000"
|
||||
type="svg"
|
||||
corners-square-color="#000000"
|
||||
corners-dot-color="#000000"
|
||||
corners-square-type="extra-rounded"
|
||||
dots-type="classy-rounded"
|
||||
dots-color="#000000"
|
||||
debug="true"
|
||||
poll-callback={callbackExample}
|
||||
/>
|
||||
```
|
||||
|
||||
I built this about a year ago but didn't post about it because there was more I wanted to do and test before putting it out there. But there's always something else to do, isn't there?
|
||||
|
||||
If you find any issues, feature requests, or suggestions for improvement, feel free to [create an issue](https://github.com/thebrandonlucas/bitcoin-qr/issues/new) in Github.
|
||||
|
||||
Hope this saves you some time!
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
title: Stack Programs Like Legos with Nix!
|
||||
description: Learn how to stitch programs together reprodicibly with Nix
|
||||
date: 2025-11-21
|
||||
tags: nix software open-source
|
||||
---
|
||||
|
||||
Nix was made to solve the _software deployment problem_, concisely defined by creator Eelco Dolstra thus:
|
||||
|
||||
> [The software deployment problem] is about getting computer programs from one machine to another—and having
|
||||
> them still work when they get there.
|
||||
>
|
||||
> The Purely Functional Software Deployment Model, Eelco Dolstra
|
||||
|
||||
Nix allows you to setup software on your computer in such a way that your setup is _reproducible_, meaning your setup on machine A can be _exactly_ the same as your setup on machine B -- as long as you have Nix.
|
||||
|
||||
To most people, learning Nix is a pain, due to the new concepts, sparse and outdated documentation, and community infighting.
|
||||
|
||||
But I think Nix can make using computers _fun_ and _powerful_ and _less painful_, once you learn how to handle its' edges.
|
||||
|
||||
One fun way we can use Nix is to stitch together programs like Legos and have them interact with each other in a reproducible way. Let's create an example!
|
||||
|
||||
First [install Nix](https://nixos.org/download/) if you haven't.
|
||||
|
||||
Say we have a fun little `bash` script like this:
|
||||
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# filename: pokefortune.sh
|
||||
|
||||
message="${1:-}"
|
||||
pokemon="${2:-slowking}"
|
||||
|
||||
# Silence Perl locale warnings.
|
||||
export LC_ALL=C
|
||||
|
||||
# Generate a fortune if user did not pass a message.
|
||||
if [[ -z "$message" ]]; then
|
||||
message=$(fortune)
|
||||
fi
|
||||
|
||||
echo "$message" | pokemonsay -p "$pokemon" -n
|
||||
```
|
||||
|
||||
It optionally takes in a message and a [Pokédex](https://pokemondb.net/pokedex/all) number, and prints out that Pokémon and message using the `pokemonsay` program. If either the message or the Pokemon aren't specified, it uses a default Pokemon and the program `fortune` to generate the message.
|
||||
|
||||
Save this in a file `pokefortune.sh`, make it executable, then run it:
|
||||
|
||||
```sh
|
||||
# Make the bash script executable
|
||||
chmod +x pokefortune.sh
|
||||
|
||||
./pokefortune.sh
|
||||
```
|
||||
|
||||
If you're like most people, you probably don't have `pokemonsay` or `fortune` installed on your system, so you'll likely see something like this:
|
||||
|
||||
```sh
|
||||
./pokefortune.sh: line 6: fortune: command not found
|
||||
./pokefortune.sh: line 7: pokemonsay: command not found
|
||||
```
|
||||
|
||||
Therefore this script, which works on my system because I have these programs installed, isn't _reproducible_ on your system. Let's create a Nix _derivation_ to make it so. Create a file called `pokefortune.nix` and copy the following:
|
||||
|
||||
```nix
|
||||
# filename: pokefortune.nix
|
||||
|
||||
{
|
||||
pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-24.05.tar.gz") { },
|
||||
}:
|
||||
pkgs.writeShellScriptBin "pokefortune" ''
|
||||
|
||||
message="''${1:-}"
|
||||
pokemon="''${2:-slowking}"
|
||||
|
||||
# Silence Perl locale warnings.
|
||||
export LC_ALL=C
|
||||
|
||||
# Generate a fortune if user did not pass a message.
|
||||
if [[ -z "$message" ]]; then
|
||||
message=''$(${pkgs.fortune}/bin/fortune)
|
||||
fi
|
||||
|
||||
echo $message | ${pkgs.pokemonsay}/bin/pokemonsay -p "$pokemon" -n
|
||||
''
|
||||
```
|
||||
|
||||
Now run:
|
||||
|
||||
```sh
|
||||
nix-build pokefortune.nix
|
||||
```
|
||||
|
||||
This may take awhile, especially if this is your first time running Nix. Let's look at the output:
|
||||
|
||||
```sh
|
||||
nix-build src/scripts/nix/pokefortune.nix
|
||||
unpacking 'https://github.com/NixOS/nixpkgs/archive/nixos-24.05.tar.gz' into the Git cache...
|
||||
this derivation will be built:
|
||||
/nix/store/vg4zmghqcnhjbs8kqhx04xixvm36d3ik-pokefortune.drv
|
||||
these 9 paths will be fetched (2.65 MiB download, 26.91 MiB unpacked):
|
||||
/nix/store/0vdf5mpd762bw53rgl5nkmhvzq8n4m0d-file-5.45
|
||||
/nix/store/77cdqhqprqbciyhzsnzmsk7azbk2xv6r-fortune-mod-3.20.0
|
||||
/nix/store/d0i8idmbb4jji9ml01xsqgykrbvm7dss-gnu-config-2024-01-01
|
||||
/nix/store/0554jm1l1qw1pcfqsliw91hnifn11w8m-gnumake-4.4.1
|
||||
/nix/store/2wp235bg03gykpixd9v2nyxp08w8xq8a-patchelf-0.15.0
|
||||
/nix/store/c5x32idp600dklz9n25q38lk78j5vwxb-pokemonsay-1.0.0
|
||||
/nix/store/pba53n11na87fs4c20mp8yg4j7qx1by2-recode-3.7.14
|
||||
/nix/store/zix67r268ihi4c362zw7c0989z12jmy7-stdenv-linux
|
||||
/nix/store/2329271b42wh6b6yhl7jmjyi0cs4428b-update-autotools-gnu-config-scripts-hook
|
||||
copying path '/nix/store/d0i8idmbb4jji9ml01xsqgykrbvm7dss-gnu-config-2024-01-01' from 'https://cache.nixos.org'...
|
||||
copying path '/nix/store/c5x32idp600dklz9n25q38lk78j5vwxb-pokemonsay-1.0.0' from 'https://cache.nixos.org'...
|
||||
copying path '/nix/store/0vdf5mpd762bw53rgl5nkmhvzq8n4m0d-file-5.45' from 'https://cache.nixos.org'...
|
||||
copying path '/nix/store/0554jm1l1qw1pcfqsliw91hnifn11w8m-gnumake-4.4.1' from 'https://cache.nixos.org'...
|
||||
copying path '/nix/store/2wp235bg03gykpixd9v2nyxp08w8xq8a-patchelf-0.15.0' from 'https://cache.nixos.org'...
|
||||
copying path '/nix/store/pba53n11na87fs4c20mp8yg4j7qx1by2-recode-3.7.14' from 'https://cache.nixos.org'...
|
||||
copying path '/nix/store/2329271b42wh6b6yhl7jmjyi0cs4428b-update-autotools-gnu-config-scripts-hook' from 'https://cache.nixos.org'...
|
||||
copying path '/nix/store/zix67r268ihi4c362zw7c0989z12jmy7-stdenv-linux' from 'https://cache.nixos.org'...
|
||||
copying path '/nix/store/77cdqhqprqbciyhzsnzmsk7azbk2xv6r-fortune-mod-3.20.0' from 'https://cache.nixos.org'...
|
||||
building '/nix/store/vg4zmghqcnhjbs8kqhx04xixvm36d3ik-pokefortune.drv'...
|
||||
/nix/store/vsv2spw517cwq791fl3f8iymm6hshhyq-pokefortune
|
||||
```
|
||||
|
||||
Nix fetched the packages we specified in our `.nix` file: `pokemonsay`, `fortune`, and some we didn't specify: such as `file`, `patchelf`, and `recode`, which one or both of the other two packages depends on. Then it copied them locally, built a binary, and placed it at `/nix/store/vsv2spw517cwq791fl3f8iymm6hshhyq-pokefortune/bin`. We can confirm this by running it:
|
||||
|
||||
```sh
|
||||
/nix/store/vsv2spw517cwq791fl3f8iymm6hshhyq-pokefortune/bin
|
||||
```
|
||||
|
||||
You should see something like this:
|
||||
|
||||
<img src="/images/blog/nix-programs-as-legos/img_2_pokefortune.webp" alt="pokefortune result" width="800" height="1240" loading="lazy">
|
||||
|
||||
To be reproducible, Nix ensures that it knows about every single dependency needed to create a package at all times. Because of that, you can do fun things like this:
|
||||
|
||||
```sh
|
||||
# Quickly enter a shell with the dependencies to create and display images.
|
||||
nix-shell -p graphviz chafa
|
||||
|
||||
# Query the derivation's dependency graph, create a .png from it and display it:
|
||||
nix-store --query --graph $(nix-build pokefortune.nix) | \
|
||||
dot -Tpng -o pokefortune-dependency-graph.png && chafa pokefortune-dependency-graph.png
|
||||
```
|
||||
|
||||
Resulting in a view of every single dependency (the "closure") that our `pokefortune` program requires:
|
||||
|
||||
<!-- <div style="text-align: center;"> -->
|
||||
|
||||
<img src="/images/blog/nix-programs-as-legos/img_3_pokefortune_dependency_graph.webp" alt="pokefortune dependency graph" width="800" height="634" loading="lazy">
|
||||
|
||||
<!-- <img src="assets/introduction/intro_img_3_pokefortune_dependency_graph.png" alt="pokefortune result" height="600"> -->
|
||||
<!-- </div> -->
|
||||
|
||||
How cool is that! We can see the whole dependency tree for anything packaged with Nix!
|
||||
|
||||
You have just done something very powerful with Nix: You've created a reproducible derivation that anyone who has the package manager installed on their system can use, which they couldn't before.
|
||||
|
||||
This is just a taste of what Nix can do, but I hope that the potential is clear. As Farid Zakaria says in his blog post "Learn Nix the Fun Way":
|
||||
|
||||
> Hopefully, seeing the fun things you can do with Nix might inspire you to push through the hard parts.
|
||||
>
|
||||
> There is a golden pot 💰 at the end of this rainbow 🌈 awaiting you.
|
||||
>
|
||||
> - <https://fzakaria.com/2024/07/05/learn-nix-the-fun-way>
|
||||
|
||||
---
|
||||
|
||||
_This article is directly inspired by Farid Zakaria's blog post [**Learn Nix the Fun Way**](https://fzakaria.com/2024/07/05/learn-nix-the-fun-way). Check out his excellent blog [here](https://fzakaria.com/)._
|
||||
|
||||
_Special thanks to [Russell Weas](https://github.com/russweas) and [veracius](https://veracius.dev) for their input to this article and code_
|
||||
|
||||
---
|
||||
|
||||
References:
|
||||
|
||||
- <https://edolstra.github.io/pubs/phd-thesis.pdf>
|
||||
- <https://web.archive.org/web/20171017151526/http://aptitude.alioth.debian.org/doc/en/pr01s02.html>
|
||||
- <https://fzakaria.com/2024/07/05/learn-nix-the-fun-way>
|
||||
- <https://zero-to-nix.com/start/nix-run/>
|
||||
Reference in New Issue
Block a user