I'm a programmer currently working from home. I use this site as a personal pastebin of sorts. Additional miscellaneous pastes can be found on my gist page.
Aside from programming, I enjoy music, video games, and playing guitar.
From 744e6008c7ac1a5ec51b2f675503c6dd12ba25ca Mon Sep 17 00:00:00 2001 From: kevinfiol <> Date: Sat, 23 Mar 2024 01:54:07 +0000 Subject: [PATCH] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@=20kevinfio?= =?UTF-8?q?l/kevinfiol.github.io@edd10a046f29a52cd77a9998d601bc8058f7d96a?= =?UTF-8?q?=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 404.html | 1 + CNAME | 1 + about/index.html | 1 + archive/index.html | 1 + atom.xml | 1237 +++++++++++++++++ blog/a-minimal-rss-feed-reader/index.html | 1 + .../index.html | 26 + blog/books/index.html | 1 + blog/brew-candy-review/index.html | 1 + .../index.html | 52 + .../index.html | 3 + blog/git-cheatsheet/index.html | 22 + blog/hard-symlinks-on-windows/index.html | 2 + .../index.html | 76 + blog/im-on-netlify/index.html | 1 + blog/index.html | 1 + .../index.html | 4 + blog/mithriljs-esbuild-jsx/index.html | 18 + blog/modern-bundling-with-esbuild/index.html | 111 ++ .../index.html | 83 ++ .../validating-checksum-on-windows/index.html | 2 + css/main.css | 289 ++++ css/resume.css | 54 + img/blog/brew_candy/1.jpg | Bin 0 -> 81629 bytes img/blog/brew_candy/2.jpg | Bin 0 -> 57909 bytes img/blog/brew_candy/3.jpg | Bin 0 -> 61420 bytes img/blog/brew_candy/4.jpg | Bin 0 -> 35892 bytes img/blog/brew_candy/5.jpg | Bin 0 -> 35159 bytes img/blog/brew_candy/6.jpg | Bin 0 -> 40677 bytes img/blog/credential_helper/helper.jpg | Bin 0 -> 12742 bytes img/blog/haxeflixel-bullet-timers/1.gif | Bin 0 -> 52114 bytes img/blog/haxeflixel-bullet-timers/2.gif | Bin 0 -> 42399 bytes img/favicon.png | Bin 0 -> 1804 bytes img/me.jpg | Bin 0 -> 32594 bytes img/spes.png | Bin 0 -> 5627 bytes index.html | 1 + js/dark-mode-switch.min.js | 1 + resume/index.html | 1 + robots.txt | 4 + sitemap.xml | 78 ++ 40 files changed, 2073 insertions(+) create mode 100644 404.html create mode 100644 CNAME create mode 100644 about/index.html create mode 100644 archive/index.html create mode 100644 atom.xml create mode 100644 blog/a-minimal-rss-feed-reader/index.html create mode 100644 blog/binding-mouse-button-plus-scroll-wheel-to-system-volume-control-on-windows-or-ubuntu-linux/index.html create mode 100644 blog/books/index.html create mode 100644 blog/brew-candy-review/index.html create mode 100644 blog/bullet-pools-with-timers-in-haxeflixel/index.html create mode 100644 blog/getting-rid-of-the-credential-helper-selector-on-git-for-windows/index.html create mode 100644 blog/git-cheatsheet/index.html create mode 100644 blog/hard-symlinks-on-windows/index.html create mode 100644 blog/hosting-your-invidious-instance-on-a-vps/index.html create mode 100644 blog/im-on-netlify/index.html create mode 100644 blog/index.html create mode 100644 blog/making-your-numeric-keypad-work-on-a-xubuntu-2004-vm-virtualbox/index.html create mode 100644 blog/mithriljs-esbuild-jsx/index.html create mode 100644 blog/modern-bundling-with-esbuild/index.html create mode 100644 blog/simple-state-management-in-mithriljs/index.html create mode 100644 blog/validating-checksum-on-windows/index.html create mode 100644 css/main.css create mode 100644 css/resume.css create mode 100644 img/blog/brew_candy/1.jpg create mode 100644 img/blog/brew_candy/2.jpg create mode 100644 img/blog/brew_candy/3.jpg create mode 100644 img/blog/brew_candy/4.jpg create mode 100644 img/blog/brew_candy/5.jpg create mode 100644 img/blog/brew_candy/6.jpg create mode 100644 img/blog/credential_helper/helper.jpg create mode 100644 img/blog/haxeflixel-bullet-timers/1.gif create mode 100644 img/blog/haxeflixel-bullet-timers/2.gif create mode 100644 img/favicon.png create mode 100644 img/me.jpg create mode 100644 img/spes.png create mode 100644 index.html create mode 100644 js/dark-mode-switch.min.js create mode 100644 resume/index.html create mode 100644 robots.txt create mode 100644 sitemap.xml diff --git a/404.html b/404.html new file mode 100644 index 0000000..8d02ed1 --- /dev/null +++ b/404.html @@ -0,0 +1 @@ +
TL;DR: Visit kevinfiol.com/reader for my new daily RSS reader, updated hourly.
Since mid-2020, I had been using Inoreader as my main RSS/Atom feed reader. Inoreader offers a generous free tier, giving you a maximum of 150 feeds per account, as well as a bunch of QoL features like search, bookmarking, and a dashboard with reasonable amounts of customization. The free tier is ad-supported which is understandable. For the most part, the ads are unintrusive. Using an adblocker such as uBlock Origin ocassionally results in a popup reminder by Inoreader that their free tier is ad-supported, kindly asking you to disable your adblock.
Last year, I had also come across George Mandis' Bubo Reader, an "irrationally minimalist, static RSS feed reader you can instantly deploy on Netlify, Glitch or your own server." George has a great introductory blogpost on his site over here which I recommend reading if the description I gave seemed interesting at all. TL;DR: Bubo Reader is nothing but a static .html page that gets rebuilt and redeployed via a Node.js script. The script will be run at a set interval, fetch the latest posts from feeds (defined in a .json file), and boom you got a new index.html
file with a couple <ul>
lists of the latest articles.
When I first checked out George's demo for Bubo Reader, I simultaneously thought "wow this is cool!" and also "wow this is crappy!". My latter thought stemmed from the fact that the demo is really irrationally minimal.
The genius of Bubo Reader is summarized in a few points that I realized slowly over the past year:
details
and summary
elements to be exactly what I needed.Last month, I forked Bubo Reader and made several personalized changes and improvements. In addition to a dark mode, I took inspiration from the John Doe webpage that was a bit popular on Hacker News some time ago, using the CSS :target
selector to create a "sidebar" for my reader. In addition, I adjusted the build script to sort feeds by the most latest updated.
The end result can be found at kevinfiol.com/reader.
About six years ago, I purchased a ROCCAT Savu gaming mouse to serve as my daily driver. The mouse itself was mediocre, but the bundled Windows-only software included a feature that I enabled on a whim that allowed the user to hold a button on the side of the mouse and use the scroll wheel to control the system volume. I didn't think much of the feature. A couple years later when my Savu finally died, I replaced it with a Logitech G203, only to find that controlling the system volume with the scroll wheel had become second nature.
Fortunately, Logitech's software included ways to bind keys to mouse buttons, and using an Autohotkey script to fake an F13
key press, I was able to bind my mouse button to a non-existent key. Afterwards, I simply launch another Autohotkey script on system startup to have system-wide volume control bound to my mouse.
The state of consumer-grade peripheral software on Linux is getting better by the day, but still severely lacking compared to Windows. For the most part, you should be fine with Piper for your gaming mouse needs on Linux. However, Piper itself is insufficient for our goals.
In the end, I was able to achieve similar functionality to the Autohotkey script above using xbindkeys
and xdotool
, both of which you can install using your package manager:
# Ubuntu
+sudo apt update
+sudo apt install xbindkeys xdotool
+
Create ~/.xbindkeysrc
in your home directory and paste:
"xdotool keydown ctrl"
+ b:8
+
+"xdotool keyup ctrl"
+ release + control + b:8
+
+"pactl -- set-sink-volume 0 -5%"
+ control + b:5
+
+"pactl -- set-sink-volume 0 +5%"
+ control + b:4
+
Run killall xbindkeys && xbindkeys
to restart xbindkeys.
A few things to note:
You may find xbindkeys keycodes using xbindkeys --key
or mouse button codes using xev
.
I am binding the side button on my mouse b:8
to the Control key on my keyboard using xdotool
. This may or may not work for you if you already bind your Control key to something else. What this means in practice is that holding your mouse button is essentially like holding the Control key.
Release, Control, Shift, Mod1 (Alt), Mod2 (NumLock), Mod3 (CapsLock), Mod4 (Super), Mod5 (Scroll)
as available modifiers.The commands pactl -- set-sink-volume 0 -5%
and pactl -- set-sink-volume 0 +5%
assume you are using the PulseAudio sound driver. These commands may not work for you if you're using the ALSA sound driver, in which case, you may use commands like amixer -q sset Master 5%-
and amixer -q sset Master 5%+
respectively. If neither of these work, try amixer -q -D pulse sset Master 5%+
and amixer -q -D pulse sset Master 5%-
, or research how to control your master volume by command line on your system.
A little over a year later, I discovered ksuperkey, and further xfce-superkey (I am an XFCE user). Last year, I avoided binding to the Super key because more often than not, this will interfere with existing keybinds in DEs where the Super key opens the Applications menu. This is the case in XFCE where the Super key is used to open the Whisker Menu. While you can simply rebind your Applications shortcut to something other than Super
, I personally find it hard to deprogram this instinct. The above utilities allow you to keep Super
as your sole shortcut to Applications
but also allow you to use it in other keyboard combinations. Note: if you use XFCE, you still have to unbind Whisker Menu from Super
in your Keyboard settings -- don't worry though, you'll still be able to use Super
after running xfce-superkey
. After installing the above utility, I changed my .xbindkeysrc
to:
"xdotool keydown Super_R keydown ctrl"
+ b:8
+
+"xdotool keyup Super_R keyup ctrl"
+ release + Mod4 + control + b:8
+
+"pactl -- set-sink-volume 0 -5%"
+ Mod4 + control + b:4
+
+"pactl -- set-sink-volume 0 +5%"
+ Mod4 + control + b:5
+
A collection of (free) web books, guides, and references by category that I've found over the years. Suggestions welcome.
All About Electronic Circuits for Kids
Electronic Texts of H.P. Lovecraft
For my birthday, a very dear friend of mine sent me these Brew Candies. She had requested that I let her know how they are, so I resolved to put together a brief review and summary of my experience eating these beer-inspired candies.
I love beer. And maybe because of this, I was very skeptical of these candies. Can you truly replicate the nuances of flavor found in beers such as IPAs and stouts? Perhaps if you jampack your candy with an overbearing bitterness reminiscent of hops.
The package includes three flavors:
Being a stout-lover, I chose to leave what I presumed to be the best for last.
Curious (or not) enough, these candies contain no alcohol! Which makes it safe for you to distribute to your underage cousins without legal repercussions. Anyway here are my thoughts on these beer treats.
What's instantly recognizable from these candies is that they seem to resemble, albeit crudely, the shape of a hop. Or at least... I think it's supposed to be a hop. Either way, it's an admirable feature, and the texture at least adds to the novelty of the packaging.
Onto the taste test!
The Hoppy IPA variant is mildly sweet, but with a sharp hoppy flavor that lingers on the tongue. As you'd imagine, it has a strong bittery aftertaste. Does not taste like a beer per say, but it is recognizable instantly. A taste not too different from other hard candies, surprisingly. Can easily disguise this among other kid-oriented candies. An interesting flavor, but not particularly great. Overall a 6/10.
Has the bitterness of a hoppy beer, but distinctively honey flavor that is not overshadowed by the hoppiness. You may be able to hide this among other honey flavored candies and get away with it. Might be a great gateway drug to trick people into liking IPAs even! A subtle sweetness, and none of the biting aftertaste found in the Hoppy IPA flavor. May be one of my more favorite honey-flavored candies I have tried. I give it a 8/10!
I love stouts, porters, and any kind of coffee-flavored dark beers, so this flavor was right up my alley. This near-perfectly replicates the flavor of a coffee stout in candy form. Easily my favorite of the three flavors, so much that I've probably already eaten most of these from the bag. If you're trying to convince someone who already likes coffee caramels (or similar candies) to try some dark beers, this might be a good stepping stone. I wouldn't mind if Brew Candy made bags full of this stuff. More please. 9/10.
Overall, this was a fun taste test. I've never written reviews for food items (or much anything really), so excuse me if you think this review sucks. I want to give a big thanks to my very thoughtful friend who gave me the opportunity to try these, and I look forward to finishing the rest of the bag. If you'd like to leave a comment, just scroll down and leave one in the non-existent comment box below! Or, you know, just e-mail me at me@kevinfiol.com.
One of the more useful features of the flixel library is the inclusion of the FlxTypedGroup class, which makes it easier to organize, update, and render multiple instances of an FlxBasic object. A few getter methods provide useful information such as the length of the group, or an array of every member in an instantiated group.
A practical application of FlxTypedGroup can be found in the 'Asteroids' demo available via the HaxeFlixel repository. FlxTypedGroup's recycle
method allows us to resuse bullet objects without having to destroy, recreate, and reallocate memory each time. Instead, bullets can be respawned from the queue after the pool has been "expended."
Note: In the demo example available in the HaxeFlixel repository, the properties of each bullet are initialized on the fly within
PlayState.hx
. For my example, I have created a separateBullet
class for the sake of convenience.
In the current PlayState, we can create and initialize a pool called bullets
which we will populate with bullet objects from which we can spawn bullets as we please. In this case, I will create a pool with a maximum size of 3.
class PlayState extends FlxState
+{
+ public static var bullets:FlxTypedGroup<Bullet>;
+ override public function create():Void
+ {
+ var poolSize:Int = 3;
+ var bullet:Bullet;
+ bullets = new FlxTypedGroup<Bullet>(poolSize);
+ }
+}
+
From here, we can write a simple loop to create new bullet objects and simply add them to the existing group.
...
+override public function create():Void
+{
+ var poolSize:Int = 3;
+ var bullet:Bullet;
+ bullets = new FlxTypedGroup<Bullet>(poolSize);
+ for (i in 0...poolSize) {
+ bullet = new Bullet();
+ bullets.add(bullet);
+ }
+}
+...
+
Within our Player class, we can then just reference the main PlayState's bullets
pool to recycle bullet
objects.
if (FlxG.keys.justPressed.Z) {
+ var bullet:Bullet = PlayState.bullets.recycle();
+ // YOUR BULLET VELOCITY CODE GOES HERE
+}
+
After this, we just add our standard logic that handles bullet velocity, acceleration, or how, when, and where your Sprite class may spawn bullet objects. As seen in the example below, only 3 bullets may be on the screen at one time, with the earliest spawned bullet being replaced.
Building on the example of the original Asteroids arcade game, we can give each bullet a limited lifetime, meaning if the bullet does not collide with another asteroid or enemy sprite, it should cease to exist after a certain period of time. Otherwise, it would continue travelling endlessly.
I was able to do this using HaxeFlixel's FlxTimer class, however, you may also use the standard Timer class included in the Haxe Toolkit.
Within my Bullet.hx
class, I declare timer
and initialize it as an FlxTimer object in the class constructor.
class Bullet extends FlxSprite
+{
+ public var timer:FlxTimer;
+ public function new(X:Float = 0, Y:Float = 0)
+ {
+ super(X, Y);
+ timer = new FlxTimer();
+ //YOUR OBJECT PROPERTIES GO HERE
+ }
+ override public function update(elapsed:Float):Void
+ {
+ ...
+ }
+}
+
Instead of creating a new FlxTimer object each time a bullet is recycled, the existing one is simply reset when needed.
Now back in the Player.hx
class, we simply set and start the FlxTimer object for each bullet as they fire. The start
method of an FlxTimer object takes three arguments:
start(Time:Float = 1, ?OnComplete:FlxTimer‑>Void, Loops:Int = 1):FlxTimer
+
Time:Float How many seconds it takes for the timer to go off. If 0 then timer will fire OnComplete callback only once at the first call of update method (which means that Loops argument will be ignored).
OnComplete:FlxTimer->Void Optional, triggered whenever the time runs out, once for each loop. Callback should be formed "onTimer(Timer:FlxTimer);"
Loops:Int How many times the timer should go off. 0 means "looping forever".
In the example below, I pass 2.0
for Time
, an anonymous function for OnComplete
that switches the bullets exists
flag to false
, and 1
for Loops
so that the function only triggers once.
if (FlxG.keys.justPressed.Z) {
+ var bullet:Bullet = PlayState.bullets.recycle();
+ bullet.timer.start(
+ 2.0,
+ function (Timer:FlxTimer) {
+ bullet.exists = false;
+ },
+ 1
+ );
+}
+
So now, not only do you limit the amount of bullets that can be on the screen at once, but you can limit the duration for said bullets! It's a very neat and useful mechanic for balancing your game that can be applied to any pool of FlxBasic objects you may need, whether it be enemies, ammunition, or environmental objects.
If you've ever been tormented by the following pop-up when trying to push or pull from an HTTPS Git origin:
There may be a way out of your misery. Initially I thought that if I added my Git credentials for the respective remote origin directly into my .gitconfig
file, Git for Windows would stop prompting me every time I wanted to push or pull. Nope. And even more painful is that for some unknown reason the pop-up would appear twice no matter what -- even if I selected Always use this from now on
.
First off, you can simply choose to not install the Helper Selector by unchecking a box during the installation process of Git for Windows. This is assuming you are installing Git in this manner, and also that you are willing to reinstall Git for Windows entirely or re-run the installer. But what if this doesn't apply to you?
I installed Git using scoop.sh, so my installation process took place entirely via CLI. However, I guess this option also assumes you want the Credential Helper since I don't remember ever specifying I wanted it. So what now?
A simple way to disable any default system-wide helper is to run the following command
git config --system --unset credential.helper
+
Now that you've disabled the system-wide helper, you can manage your credentials on a repository-basis. Git provides two built-in solutions. I chose to use the store
helper on my repos.
Navigate to your repository and run:
git config credential.helper store
+
This will then prompt you for your remote credentials. Now try running a git pull
and marvel at not having to be bothered by a pop-up!
I never claimed to make wise decisions. But yes, this issue is nonexistent on my Ubuntu-based laptop, and probably on your Macbook Pro too.
This is a collection of git
commands that I use frequently. May or may not be useful to you, but it's useful to me, dammit.
git add -i
+
$ git clone -b <branch name> <host>
+
git push origin --delete <branch_name>
+git branch -d <branch_name>
+
git checkout master
+git pull origin master
+git merge --no-ff test
+git push origin master
+
git checkout production
+git merge development
+git push origin production
+
git checkout -b myFeature dev
+git push origin myFeature
+
git stash
+
And to get it back:
git stash pop
+
git remote add upstream git@github.com:company/projectyourforkedfrom.git
+
To update:
git fetch upstream
+git rebase upstream/master
+
If you have commit rights to the upstream repo, you can create a local upstream
branch and do work that will go to upstream there.
git checkout -b upstream upstream/master
+
.gitignore
Be careful with this, because if you ignore a file, and then do a git pull
after the file was changed upstream, you'll get a conflict. And you'll be confused because Git will tell you to stash your changes, all the while git status
is showing no changes.
Ignore it:
git update-index --assume-unchanged <file>
+
Unignore it:
git update-index --no-assume-unchanged <file>
+
Update 2022: This post is likely outdated. I recommend checking the official installation instructions found on the Github repo.
Invidious is an alternative front-end to YouTube with an emphasis on privacy and low system resource usage. It is wonderful free software that not only does away with the annoyances of Google's bloated, ad-ridden video site, but also adds plenty of QoL features.
This weekend I spent some time setting up my own instance of Invidious to share with friends. Here's a quick rundown on what I did:
I went with the $5/mo Nanode over at Linode. It's packing a grand total 1GB of RAM, a single CPU, and 25GB of storage. More than enough to run our instance, but not enough to actually compile the damn codebase (we'll get to this in a bit).
I wanted a simple and short domain name for myself and my buddies to be able to use instead of typing an IP address in the address bar every time. Getting a domain isn't terribly important especially if you're hosting this just for your own use.
For the record, I used Namecheap to purchase my domain. I found this guide particularly helpful in setting up my domain with my Linode VPS.
Again, not terribly important what you pick. If you plan to use Invidious Updater, know that it only supports Debian, Ubuntu, CentOS, Fedora, and Arch. Originally I attempted to use it on an Alpine Linux installation, but the script promptly failed.
Also, the Invidious docs only provide installation instructions for Arch Linux and Debian/Ubuntu. I went with Arch Linux.
The README provides 3 options:
All options are simple and easy to do. I went with 1.
Install dependencies:
sudo pacman -Syu base-devel shards crystal librsvg postgresql nano
+
Create Invidious user:
useradd -m invidious
+sudo -i -u invidious
+git clone https://github.com/iv-org/invidious
+exit
+
Setup Postgres:
sudo systemctl enable postgresql
+sudo systemctl start postgresql
+sudo -i -u postgres
+psql -c "CREATE USER kemal WITH PASSWORD 'kemal';" # Change 'kemal' here to a stronger password, and update `password` in config/config.yml
+createdb -O kemal invidious
+psql invidious kemal < /home
+exit
+
I was only able to run the following commands as root. Make sure the postgresql service is running:
psql invidious kemal < /home/invidious/invidious/config/sql/channels.sql
+psql invidious kemal < /home/invidious/invidious/config/sql/videos.sql
+psql invidious kemal < /home/invidious/invidious/config/sql/channel_videos.sql
+psql invidious kemal < /home/invidious/invidious/config/sql/users.sql
+psql invidious kemal < /home/invidious/invidious/config/sql/session_ids.sql
+psql invidious kemal < /home/invidious/invidious/config/sql/nonces.sql
+psql invidious kemal < /home/invidious/invidious/config/sql/annotations.sql
+psql invidious kemal < /home/invidious/invidious/config/sql/playlists.sql
+psql invidious kemal < /home/invidious/invidious/config/sql/playlist_videos.sql
+
The next steps to install Invidious involve compiling the project code. On my measly 1GB of RAM, the compilation choked. In order to prepare for this, I used systemd-swap
to create a swapfile. There are other ways to create a swapfile or a swap partition on Linux. See: Swap on Arch Linux Wiki. I found using systemd-swap
the simplest. This guide by Ricosta Cruz was very helpful.
I did as follows:
sudo pacman -Syu systemd-swap
+
Then I edited the config file to enable zram_enabled=1
and swapfc_enabled=1
:
sudo nano /etc/systemd/swap.conf
+
Then enable systemd-swap.service
:
sudo systemctl enable --now systemd-swap
+
Finally add the swapfile to /etc/fstab
so it'll be used on every boot. Add this: /swapfile none swap defaults 0 0
to the end of the file:
sudo nano /etc/fstab
+
Finally, you won't run out of RAM to setup Invidious. Let's go back and finally install it:
sudo -i -u invidious
+cd invidious
+shards update && shards install
+crystal build src/invidious.cr --release
+./invidious # test compiled binary, stop with ctrl c
+exit
+
It's installed! Now some administrative stuff. Let's setup the systemd service so that Invidious runs in the background:
sudo cp /home/invidious/invidious/invidious.service /etc/systemd/system/invidious.service
+sudo systemctl enable invidious.service
+sudo systemctl start invidious.service
+
And lets rotate the logs so that they don't balloon in size:
sudo echo "/home/invidious/invidious/invidious.log {
+rotate 4
+weekly
+notifempty
+missingok
+compress
+minsize 1048576
+}" | tee /etc/logrotate.d/invidious.logrotate
+
+sudo chmod 0644 /etc/logrotate.d/invidious.logrotate
+
You now got a running Invidious instance! Navigate to your VPS's IP address on port 3000 to see it up and running. Type http://<VPS_IP_ADDRESS>:3000
into your browser's address bar, and get to watching some videos!
When I bought my domain, I intended to use it for more than just Invidious. In my case, I wanted a subdomain to serve as my Invidious URL, while I can use the main domain to host a "hub" of sorts to other services.
So for example:
http://tube.mydomain.com -> Invidious Service
+http://mydomain.com -> Homepage
+
I was able to do this easily using nginx reverse proxies. Here are the steps I took.
Install nginx:
pacman -Syu nginx-mainline
+
Enable and start nginx service:
sudo systemctl enable nginx.service
+sudo systemctl start nginx.service
+
Configure /etc/nginx/nginx.conf
. Add this additional server
entry somewhere under http
:
http {
+ ...
+
+ server {
+ server_name tube.mydomain.com;
+ location / {
+ proxy_pass http://127.0.0.1:3000;
+ }
+ }
+
+ ...
+}
+
Next addition is completely optional if you want your root domain to point to a kind of homepage. This is what I wanted, so I'm putting it here partly to document it for myself. I changed the existing, default http.server
entry to point to custom html
I had written:
http {
+ server {
+ server_name mydomain.com
+
+ location / {
+ root /sites/mydomain.com
+ index index.html index.htm
+ }
+
+ ... # the rest remained unchanged from the default
+ }
+
+ ...
+}
+
I used EFF's Certbot for this. It is painfully easy to use. Note that if you choose to, it will make modifications to your nginx.conf
for you.
You must enable an option in Invidious's config if you want assets to only be transferred via HTTPS. More information can be found here.
That's all. Hopefully you found this helpful. If you got any questions, shoot me an email. And always, if you liked this post, hit Like and Subscribe! (haha).
First of all, happy birthday to my big bro! He's 29 today. Wishing him good fortune, good health, good food, etc.
On topic: I had been using Github Pages as my homepage solution for years now. Six years to be exact, which means since I was a senior finishing my undergrad. One of the reasons I began using it was that it was one of the only (if not, most popular) freely available static-site hosting solutions, and incredibly developer friendly. The same cannot be said in the year 2020.
Update: As of May 2021, I am back to Github Pages, being deployed via Github Actions. My reasoning for switching back was some inherent latency I discovered while using pages deployed on Netlify. Github Pages is snappier, in my experience.
Click here to be redirected. \ No newline at end of file diff --git a/blog/making-your-numeric-keypad-work-on-a-xubuntu-2004-vm-virtualbox/index.html b/blog/making-your-numeric-keypad-work-on-a-xubuntu-2004-vm-virtualbox/index.html new file mode 100644 index 0000000..21d1f6e --- /dev/null +++ b/blog/making-your-numeric-keypad-work-on-a-xubuntu-2004-vm-virtualbox/index.html @@ -0,0 +1,4 @@ +
I fought with this for a good 2 hours before I finally got it working. First step is to install numlockx
, if it's not already installed:
sudo apt update
+sudo apt install numlockx
+
Enable your Num Lock in your VM:
numlockx on
+
Now in Xubuntu, go to your Whisker Menu (or whatever launcher/menu you're using) and navigate to Settings -> Accessibility -> Mouse
and disable Use mouse emulation
.
If you're on vanilla Ubuntu (GNOME), or a DE other than XFCE, you may be looking for a menu called Universal Access
, wherein you'll want to disable a feature called Control the pointer using the keypad
.
You may need to log out and back in to confirm this works.
EDIT 7/18/2022: esbuild has since added support for string literals as JSX fragments, meaning much of what I wrote below is now unnecessary. To use JSX with Mithril and esbuild, all you need to do is set jsxFragment: '"["'
in your esbuild config. See here for an example.
I recently bootstrapped a Mithril.js project using esbuild for my bundling purposes. If you don't already know, esbuild is a next-gen bundler written in Go by Evan Wallace that is magnitudes faster than rollup, webpack, or parcel (all of which are written in JS/TS).
esbuild comes with JSX support out of the box. While I don't normally use JSX, I decided to create a demo for mithril.netlify.app showing off that JSX works just fine in a Mithril application. However, I ran into one major issue -- esbuild wasn't compiling JSX fragments correctly when providing m.fragment
as the JSX fragment pragma.
After some digging around, I found this comment by Claudia Meadows.
In short, there's no support for Mithril fragments as Components currently. esbuild (as well as transpilers like Sucrase) compiles JSX using the provided jsx pragma + jsx fragment method in the form of:
React.createElement(React.Fragment, null, "Stuff");
+
See here for more details.
Which in Mithril's case, can't work with m.fragment
. Following Claudia's advice, defining a simple Fragment component as so worked for me. I just put this in my index.js
file near the top:
m.Fragment = { view: vnode => vnode.children };
+
Then I just set jsxFragment
to m.Fragment
instead of m.fragment
in my esbuild config and voilà ! Fragments work as you'd expect.
var JSX = {
+ view: function() {
+ return (
+ <>
+ <h2>JSX</h2>
+ <p>You could use JSX with Mithril.js as well.</p>
+ <p>Be aware that this requires a build-step.</p>
+ <p style={{ color: 'red' }}>
+ Attributes work as expected.
+ </p>
+ <p>This application uses <a href="https://esbuild.github.io/">esbuild</a> to convert JSX.</p>
+ </>
+ );
+ }
+};
+
For reference, you can see my full esbuild config and my bundle scripts here. Direct any questions to me@kevinfiol.com and I'd be happy to answer them.
These days, you can get a pretty robust build setup for a modern browser app using just esbuild. The benefits of using esbuild over rollup, webpack, or parcel are numerous, but the few that stand out to me are:
That's a lot of bang for your buck for a single dev dependency.
First off, install esbuild in your project if you haven't already.
npm install --save-dev esbuild
+
You can use esbuild via CLI or its Node API. For tiny apps where your build config is practically non-existent, using the CLI is fine. You can simply define a script in your package.json
file and be ready to go.
"scripts": {
+ "build": "esbuild index.js --bundle --minify --outfile=./dist/app.js"
+}
+
Define a script like the one above, run npm run build
, and your lightning-fast build is there, minified in all its glory. This isn't terribly different than what you can already do with webpack, but when was the last time you saw webpack's dependency graph? And that doesn't even include webpack-cli. Not to rag on webpack; it is an immensely powerful tool that many great projects rely on, but unless you're already tangled in that web (heh), I'd suggest steering clear.
esbuild also includes a built-in watch mode. No extra plugins needed! Let's go ahead and define another script:
"scripts": {
+ "build": "esbuild index.js --outfile=dist/app.js --bundle --minify",
+ "dev": "esbuild index.js --outfile=dist/app.js --bundle --sourcemap --watch"
+}
+
Make changes to index.js
and you'll see that dist/app.js
is re-bundled automatically. It's even got source map support! Alternatively, we could take things a step further and utilize esbuild's built-in server. Let's change the dev
script a bit:
"scripts": {
+ "build": "esbuild index.js --outfile=dist/app.js --bundle --minify",
+ "dev": "esbuild src/index.jsx --outfile=dist/app.js --servedir=dist --bundle"
+}
+
After running dev, you'll see in your terminal that a server has been started. Navigate to localhost:8000
to see the contents of your dist
folder hosted locally.
$ npm run dev
+
+ > Local: http://127.0.0.1:8000/
+ > Network: http://192.168.1.12:8000/
+ > Network: http://172.11.100.1:8000/
+ > Network: http://192.168.1.3:8000/
+
If you're wondering where your generated output files are, no worries: esbuild's serve mode serves the bundled files directly from memory. They are never written to your disk unless you intentionally omit the servedir
variable.
Configuring our buildstep via CLI flags can get unwieldy over time. With other bundlers, you get the benefit of config files, e.g., rollup.config.js
or webpack.config.js
. With esbuild, we can just use plain old Node scripts plus the existing Node APIs to configure our builds. Start by creating a new .js
file under a directory for scripts, scripts/build.js
. Then, change our existing build
script in our package.json
:
"scripts": {
+ "build": "node ./scripts/build.js"
+}
+
This doesn't do anything yet, because scripts/build.js
is empty. Let's fix that by translating our previous build CLI call to a Node script. That will look something like this:
// scripts/build.js
+import esbuild from 'esbuild';
+import { resolve } from 'path';
+
+esbuild.bundle({
+ format: 'iife',
+ entryPoints: [resolve('index.js')],
+ bundle: true,
+ outfile: resolve('dist/app.js')
+}).catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
+
Running npm run build
should function the same as before, but now we have more control over our bundles! But what about our dev script? And what if we want to reduce code duplication? After all, build
and dev
are very similar with just a couple different options.
Let's create a new file in our scripts folder, called scripts/bundle.js
that will contain the config that both build.js
and dev.js
will use.
// scripts/bundle.js
+import esbuild from 'esbuild';
+import { resolve } from 'path';
+
+export function bundle(config = {}) {
+ return esbuild.build({
+ format: 'iife',
+ entryPoints: [resolve('index.js')],
+ bundle: true,
+ outfile: resolve('dist/app.js')
+ ...config
+ });
+}
+
As you see, bundle.js
will contain all of our default configs. Let's refactor scripts/build.js
to specifically create one minified build:
// scripts/build.js
+import { bundle } from './bundle.js';
+
+bundle({ minify: true })
+ .then(() => {
+ console.log('Bundled!');
+ })
+ .catch((error) => {
+ console.error(error);
+ process.exit(1);
+ });
+
Our build script should now function like before. Let's create scripts/dev.js
now:
// scripts/dev.js
+import { bundle } from './bundle.js';
+
+bundle({
+ minify: false,
+ sourcemap: true,
+ watch: {
+ onRebuild(error) {
+ if (error) console.error(error);
+ else console.log('Bundled!');
+ }
+ }
+}).catch((error) => {
+ console.error(error)
+ process.exit(1);
+});
+
Add the dev
script to your package.json
:
"scripts": {
+ "build": "node ./scripts/build.js",
+ "dev": "node ./scripts/dev.js"
+}
+
And now npm run dev
will watch and rebundle your app on every file change.
You very well could use esbuild's "serve" mode in your scripts/dev.js
script if you'd like to. You would have to adjust the scripts we've created so that "dev" mode uses esbuild.serve
instead of esbuild.build
. While I like that esbuild has a built-in server, it does not support live-reload, which is a nice feature to have. You could implement your own live reload using esbuild.serve
, but a simpler solution would be to include some kind of server in our project as a dev dependency. I've found that nativew/serve was a fine candidate for this. At 18.7kb with 0 dependencies, it was a guilt-free inclusion. Install with npm as normal.
Update (12/18/2021): Since writing this article, I've published a fork of lukejacksonn's servor project titled servbot, which is smaller in scope and intended to be used with existing JS build tools. My move away from nativew/serve was motivated by its lack of SPA support. The instructions below have been updated for servbot:
npm install --save-dev servbot
+
Now let's modify our scripts/dev.js
:
// scripts/dev.js
+import servbot from 'servbot';
+import { bundle } from './bundle.js';
+
+// create server
+const server = servbot({
+ root: 'dist',
+ reload: true,
+ fallback: 'index.html' // fallback to index.html for SPA routes
+});
+
+// start our server at localhost:8000
+server.listen(8000);
+
+bundle({
+ minify: false,
+ sourcemap: true,
+ watch: {
+ onRebuild(error) {
+ if (error) console.error(error);
+ else console.log('Bundled!');
+ server.reload(); // <-- This will live reload on every rebuild
+ }
+ }
+}).catch((error) => {
+ console.error(error)
+ process.exit(1);
+});
+
Re-run npm run dev
and we'll have our live-reloading dev server up:
$ npm run dev
+
+[servbot] Server started: http://localhost:8000
+
If for whatever reason you've made it this far and are not convinced and maybe want a more batteries-included solution, I highly recommend Vite from Vue.js creator, Evan You. Vite actually uses esbuild for its own development mode to bundle vendor packages. Otherwise, I hope this has encouraged you to dig deeper into esbuild and to not be afraid to get your hands dirty in writing custom Node scripts to do your bundling. Sometimes what webpack and rollup do under the hood can seem like black magic since it's all abstracted away from you, basking in the comfort of a small config json file at the root of your project. You can cut away at a lot of that cruft by using simpler tools like esbuild, and get perf benefits and a better understanding of your tooling to boot!
Mithril.js is a lightweight JavaScript framework that has become a staple in my development stack after I discovered it two years ago. At the time, I was looking for a simpler, zero-dependency alternative to React.js that could help me learn modern JavaScript UI development without needing to simultaneously learn and understand various build tools and framework plugins.
I've since learned React and have come to appreciate it for its influence on modern web development. However, I find that Mithril, a framework that sits at half the size of React whilst containing more features, has remained my go-to.
When it comes to state management, Mithril is as unopinionated as they come. You can use Redux, Mobx, Cerebral, some implementation of the SAM pattern, or best of all -- just a plain ol' JavaScript object! Mithril comes with a global, auto-redraw system. The virtual DOM created by Mithril will diff against and synchronize the DOM whenever changes are made to your data layer. Most commonly, the redraws are triggered after an event handler defined in your Mithril application is called. But you can also manually trigger a DOM update with m.redraw
.
What this means in practice is that you are free to structure your data however you'd like, and Mithril takes care of the rest. Below is an example of a simple Counter application written with Mithril:
let count = 0;
+
+const Counter = {
+ view: () =>
+ m('div',
+ m('h1', 'Counter'),
+ m('p', count),
+ m('button', { onclick: () => count += 1 }, '+'),
+ m('button', { onclick: () => count -= 1 }, '-')
+ )
+};
+
+m.mount(document.body, Counter);
+
Our state is just a single primitive variable! For small applications, simple widgets or one-off UI components, the above solution is largely sufficient. What's important about implementing your state management solution is to understand that there is no silver bullet. You will be able to predict your needs more accurately as you work across multiple projects and grow organically. Redux is a brilliant solution for modern UI state management, but the 9/10 times I have attempted to use it out of a desire to do things "the right way", it was absolute overkill. I advise reading this blog post by Dan Abramov, the creator of Redux.
While the above solution is simple and likely sufficient for small use-cases, it introduces one problem - we are modifying the state directly from within the view. It won't take long before this approach proves unwieldy, and you're scanning your templates trying to find where you wrote the logic that is altering your state in (potentially) unpredictable ways.
We can introduce indirection and a more versatile state container using plain JavaScript objects. Our Counter
component becomes more terse, yet more expressive:
const state = { count: 0 };
+
+const actions = {
+ increment: () => state.count += 1,
+ decrement: () => state.count -= 1
+};
+
+const Counter = {
+ view: () =>
+ m('div',
+ m('h1', 'Counter'),
+ m('p', state.count),
+ m('button', { onclick: actions.increment }, '+'),
+ m('button', { onclick: actions.decrement }, '-')
+ )
+};
+
+m.mount(document.body, Counter);
+
As your application grows in size, it might be preferable that your state and actions are easily testable and replicable from the beginning. Further, instead of relying on lexical scoping for your actions to have access to your state, we can use a combination of dependency injection and closures so that an instance of your actions will always directly reference a specific state object. We can easily achieve this with factory functions that provide your initial state and actions that directly reference a single state object.
const State = () => ({ count: 0 });
+
+const Actions = state => ({
+ increment: () => state.count += 1,
+ decrement: () => state.count -= 1
+});
+
From there, it is dead simple to reproduce your state and actions objects respectively:
const state = State();
+const actions = Actions(state);
+
Passing these to a Mithril component is trivial using the attrs
property (near-equivalent to props
in React) and object destructuring. Notice that our Counter component remains virtually unchanged:
const Counter = {
+ view: ({ attrs: { state, actions } }) =>
+ m('div',
+ m('h1', 'Counter'),
+ m('p', state.count),
+ m('button', { onclick: actions.increment }, '+'),
+ m('button', { onclick: actions.decrement }, '-')
+ )
+};
+
+m.mount(document.body, {
+ view: () => m(Counter, { state, actions })
+});
+
(P.S. Credit goes to porsager who shared this brilliant solution in the Mithril.js Gitter, nicknamed "Mitosis", named after the equally awesome Meiosis Pattern by foxdonut). This is my preferred approach to state management in Mithril. Passing your state and actions to child components would work as you'd expect. Simply pass your state and actions objects further down as attrs
, or more wisely, be selective of what you choose to expose to child components.
You could also take an approach where your application is composed of solely stateless components. That is, every component is a pure, deterministic function. Hyperapp is a JavaScript framework that does not allow for local state in components. Instead, every component returns a portion of your UI that reflects the global state. While I highly recommend checking out Hyperapp (it's only 1kb gzipped!), this post is about Mithril, and you can use a similar approach with Mithril.
const State = () => ({ count: 0 });
+
+const Actions = state => ({
+ increment: () => state.count += 1,
+ decrement: () => state.count -= 1
+});
+
+const Counter = (state, actions) =>
+ m('div',
+ m('h1', 'Counter'),
+ m('p', state.count),
+ m('button', { onclick: actions.increment }, '+'),
+ m('button', { onclick: actions.decrement }, '-'),
+ Child(state, actions)
+ );
+
+const Child = (state, actions) =>
+ m('div',
+ m('h2', 'Child'),
+ m('p', state.count * 2),
+ m('button', { onclick: actions.increment }, '+'),
+ m('button', { onclick: actions.decrement }, '-'),
+ );
+
+m.mount(document.body, () => {
+ const state = State();
+ const actions = Actions(state);
+
+ return { view: () => Counter(state, actions) };
+});
+
In the end, always do what feels right to you and makes more sense given your team and/or project. If this has been helpful or if you have any questions, drop me an email!
izZ>{tY^)okT(08Op7$%B;^W+z_N}3{Ri-;N%Iei$Q1@|_
z{Epc1O5X#{g;xTn62L6C<5HVik%zF{3jY9V*{>x01|Tjg7-`dJwV>*oMeL_m--+6*
z_HBW+{Q<(a{{T(lRqs%kMYVRf2A_qTyXjYNz~JQ@KEwd;QOo&iw5bWna8A4u$3~q9
z(5@2;-dsU@+#^pd(06~Dj}n@NzEHVvy%&WZsqGrVd+UR)C0l>dM4WV5t0M;_q8}2N
z-DM4%t7)d}^xffCxaUpAkUWmmBTe28gSY#vwFWh%&OHKcO|hk|{2fqbKM2q#(5&X<
zYuKx532E@WXnxBj22yu9S5~P#{S$BqSUBjSerwGF;6midQ&FHhuqa50=$u1w-FwL=
zviDl|Q+`!3D`ulj(Zr=E&1#>tZJE472X8gSdwBzkal0#DWNhrF*}CIX;wviv`*4eC
zo?ky?Q((ErL@HEsIEPpF@>d@Zn&qg{Jdx~GPv)7#r>sPyXo1`8u4xkr3erLr;04%u
zD_r9#yR8cCpa~ dfGDpKokI17>({2ESs>Ps#(vwu-3nKb3SLxOtD!0qCv>MF~*eP%a;e2E36
zco3p%+#-J)Hk-qLQR;o5&3)r6tN9g7W!|j9gUeK|%HTj!Luy_#FFKfD9s`+(3+5kr
zbmllD>KS-%cNHBw&ZbrqEdYH>_rM1P0c2k>Nv0egV}7wjD_~lAn<*;%kM$Mc^%miY
z5RKu4
zJE|^;er@ve-{i6vk}bRsBnw@Q#+}3uBybb~Z<@ABm_di8>%PK^FKBNMDvd)_3jxPf
zNtCKV&Csb+V~!gRTo`oSDeQG8lI>%%rUP9J5L4Rbo*SSwI-mx@FsT
pr4P|DLbJHqY!VA>UVqf1
zw2F35GlH%W9hIu;WuEZjCxvUaY)fG`I0j~66_45KZ{=I!`l-x3HJX;e@4}`FtbzKE
zK$P-8Xz^PiRb8QBv9RR_5%M3h3o7}Jo1i&kBU0r7bCf_Lw0K39mrCw%GTY)LkWfnb
zK*1>~7i$yTg+OFgD^>IxV(UQJDDGdLXVk1R1w6!38)geY`?smyR~MMWDtRFm4~Pgl
zTm>zN&h6lfZuW+N*D|0P96-P<>I|Ty^)=W4a~!1vc}j>IQZ6udhq%cBxxKuyT*jDx
z2gDVb-cf$g4A5|{L#0NM+SogbyhcT(t}J13vuc%Fyl9npBQH=ubg~fdRmi02Sebn}
zosbRl313$TqTEgldA7_RJUuh&asqYcGMT%3my)|oofa~ZrB@!H%DIka5|kBl1CZRH
zS}~Y@;amEY^&P%YS&pc@ZaHoQJ3knegv>Z)A+&eP8uuubc}G*V5Z%BFiFPost-xs_
zf+&SbrTw5?gv#Py*f(e~Lk|;0?xh*M^DAy#K|F9*G4EM}tD9Otp!a%Zz;Fpd2Sgze
zTw_KdE5+JHj*u7Xme@P9Y6s<~Kg@Ou1Bof8ucX-SolK0%PaY-VEqy@+^TbOwSLRwy
z9S&+`jiTf9QDa0=p7ni99TsG20#3=4zAD67G4~c4o^U0{4U({{s)Y8F}?gd2#)>kUULpzaTyMj&Ti<}3^AbA7fhvD6=p@~<;H!NAm4
z*don%ng-=}mQ0u!l^*&?@;}T!6%406EuYM$KG1+b+d##1Y5JPueS**?pf4LoOjg656=2{6mre+OhH^2nWdz
zR*#C<)f=UG{-$C9`z3xW{60%x)N?yph!I6z*^pk~I*@OfN3~`sPQ=ySC72xqLI>~w
zh0woH6+f~ltLlCkl8vIm;X+&?dFm-D2I>;)bE(Ao+}G64ljd1*996`oWg&wJf@_5|
z2T_9j5hI}ZZ2hv&LsdnKo&H>it$V&ElV%84KQ7en*>BrlJQdf
zolrS^9t5e@)rO;0OT%Q|gfI6hpf8L}Unzq!Uvux=LI>dOK9~v^(`jd_C?Vb~Me0#+
zPT-*MN+{ciikU^=lu}k98*B0w5ZRgu)5Nz@yOtr*9$IJ
eId7Q2id~T6=KTi`^%+oN7L_V%wnX?N07+U3
zo<;{S7yDx5AC(Q*<%ZLccM&|T-9Z#ty)FvJySQQp(5o_!SYV3i&WTZVJCWS5REzsc
z2Lp3IAMRmq?i$P{@>uvL1TS!IVT^L4+$m3=Z1cynEGwPm+9_>iy>?}oF&3tMqEQzf
zLW_sj`6?<3y_P=a#=NB$>RK#Dk+>iYp~NcG^PJRQvYphhBPfMv3o_iW{f*Bji6G4A
z%@w(>8TB97Q5w>hf(`PgqFLjB)NHGlq98?-8n^JSVhah#oA64^9!MokrcnMVlQY*b
z?T7`8RgqZvnj>g5ds|(>fpI7+P-D^X&xcEyNB$8MDrp&(jJGb}HcjSn{{Xlu;_UoJ
zELTMx)C`K{I)>22#kAa{m66G}G3~2Q@`{zk*p~$9m8I}Nd(q}Iz3r88