Compare commits

..

44 commits

Author SHA1 Message Date
6c225f26ee Update flake 2024-12-09 10:38:01 -06:00
27ca1c9f9e At atproto-did verification 2024-12-09 10:32:32 -06:00
d3540a57d3 Remove build badges 2024-10-25 10:51:00 -05:00
44d813c902 Add logo 2024-10-25 10:50:53 -05:00
b01a79b9fa Merge remote-tracking branch 'origin/master' 2024-10-15 16:39:06 -05:00
571894a6a4 Better proxying 2024-10-15 16:38:41 -05:00
bb20d8d222 Fix extra table cells 2024-09-24 10:01:56 -05:00
aca436a4e6 Merge remote-tracking branch 'origin/master' 2024-09-24 10:00:53 -05:00
bfb3b55fda Salarrrryyyyy 2024-09-24 10:00:40 -05:00
ffff60e036 Fix images breaking body bounds and causing horizontal scrolling 2024-07-09 12:04:40 -05:00
848f2d79ae Add learn flakes the fun way blog post 2024-07-09 11:59:15 -05:00
bbf18323ea Ignore nix build results 2024-07-08 16:27:23 -05:00
4adc38883c Fix some styling 2024-06-14 10:36:05 -05:00
19b67010cc More stuff 2024-06-12 17:08:53 -05:00
4b67ef3b9d Merge remote-tracking branch 'origin/master' 2024-06-06 17:05:27 -05:00
d9deb85b45 Versus 2024-06-06 17:04:11 -05:00
ab5649a531 Remove logs 2024-05-26 10:47:10 -05:00
9daa276c2e Better align icons 2024-05-26 10:46:28 -05:00
f921721f32 Fix double alignment icons, unify left-border stuff 2024-05-26 10:22:10 -05:00
f2d27568a1 Parity 2024-05-26 10:05:00 -05:00
9194c58444 Cleanup 2024-05-07 09:08:39 -05:00
8a847c9a3f Use deno endpoint 2024-05-07 00:57:09 -05:00
0991cfdcdf Some updates to uses, fix analytics script 2024-03-29 16:40:03 -05:00
b4d021b22a Combine tips with blog
Still should 301 the old URLs or something
2024-03-26 07:59:50 -05:00
1a90ac8620
Add a 'your' 2024-02-29 14:54:47 -06:00
186ba0261a
Go proxy tip 2024-02-29 14:53:46 -06:00
2d38e11cc7
phrasing 2023-11-07 14:43:28 -06:00
9904d770af
Fix player list ordering 2023-11-07 11:21:26 -06:00
ac1f03bec0
Add player list 2023-11-06 20:04:11 -06:00
6dd8d36f57
Update minecraft server status page 2023-11-06 17:19:44 -06:00
ef6e306cb3
Move minecraft server status page to a more generic URL and change contents to be less-specific to a single server
This is in preparation for showing the status page for multiple servers
2023-11-03 20:19:28 -05:00
2e332e20ea
Fix nix flake, add uber 2023-11-02 21:03:27 -05:00
4b37f740b3
Flake 2023-10-06 17:04:53 -05:00
43a41fdcac
Fix SVG 2023-09-20 10:34:44 -05:00
84d7289507
Add salary page with table 2023-09-20 10:31:25 -05:00
6becc36f49
Merge remote-tracking branch 'origin/master' 2023-09-20 09:38:33 -05:00
e08d3bbb48
Hide ToC on ourcraft status page 2023-09-20 09:37:57 -05:00
b8a829e0a1
Update 2023-07-17 23:53:09 -05:00
aa27b1a257
Fix typos and such 2023-07-17 22:16:14 -05:00
83f31c5da4
Fix makefile 2023-07-16 20:15:50 -05:00
7b844b6153
Man, I need to switch to tailwind 2023-07-16 20:13:55 -05:00
7b91f1f2a7
Add link 2023-07-10 12:23:53 -05:00
df28a035dd
WIP restic article 2023-07-10 12:07:48 -05:00
ef7be7d6cd
Nah 2023-07-10 11:22:17 -05:00
40 changed files with 1984 additions and 242 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

4
.gitignore vendored
View file

@ -1,6 +1,10 @@
/.direnv
/result
/resources
/public
/node_modules
/.netlify
*.lock
!flake.lock

View file

@ -19,7 +19,7 @@ markup:
# post: /blog/:title
params:
Description: "Hi! I'm Daniel. I live in Kansas City where I help run a small Christian church, raise two kids with my awesome wife, and write software for Divvy."
Description: "Hi! I'm Daniel. I live in Kansas City where I help run a small Christian church, raise three boys with my awesome wife, and write software."
outputs:
home: [html]
@ -35,10 +35,10 @@ menu:
name: blog
url: /blog/
weight: 20
- identifier: tips
name: tips
url: /tips/
weight: 30
# - identifier: tips
# name: tips
# url: /tips/
# weight: 30
- identifier: contact
name: contact
url: /contact/

View file

@ -7,19 +7,22 @@
<!-- updates here must be mirrored in site config for meta description-->
I live in Kansas City where I help run <a target="_blank"
href="https://kcrising.church">a small Christian church</a>, raise two kids
href="https://kcrising.church">a small Christian church</a>, raise three boys
with my <a target="_blank"
href="https://www.instagram.com/valerielauren93">awesome wife</a>, and write
software for <a target="_blank" href="https://getdivvy.com">Divvy</a>.
software for <a target="_blank" href="https://getdivvy.com">Divvy (acquired by
Bill.com)</a>.
I run a ton of self-hosted software here at home on some machines that sit on
I run a lot of self-hosted software here at home on some machines that sit on
[an unnecessarily large server rack in my basement][rack]. I love building
[keyboards][kb], too. [I heavily customize my workflow][wf] and you can
sift through my [dotfiles][df] if you like.
see how I set everything up with [Nix][nix] if you like (or even my old
[dotfiles][df]).
Occasionally, I post technical articles here.
Occasionally, I post articles here.
[rack]: //files.lyte.dev/images/server-rack.jpg
[rack]: //files.lyte.dev/images/server-rack-angle-2023-07.jpg
[kb]: //files.lyte.dev/keyboards
[wf]: //files.lyte.dev/images/desktop-screenshot.png
[wf]: //files.lyte.dev/images/desktop-screenshot-busy-2023-07.png
[nix]: //git.lyte.dev/lytedev/nix
[df]: //git.lyte.dev/lytedev/dotfiles

View file

@ -0,0 +1,9 @@
---
title: "Contributor License Agreements"
date: "2024-06-12"
draft: true
---
# Other interesting points on CLAs:
- Some people cannot contribute when CLAs are in the way because they cannot give out "perpetual" or "unrestricted" licenses to their work due to agreements with their own employer or place of education: https://utcc.utoronto.ca/~cks/space/blog/tech/CLAsImpedeContributions

View file

@ -35,7 +35,7 @@ can either use [`Ecto.Changeset.change/2`][Ecto.Changeset.change/2] to handle
the results of an admin user submitting those forms or implement some sort of
protocol that lets you specify an admin-specific changeset.
## Getting That Sweet, Sweet Metadata
# Getting That Sweet, Sweet Metadata
My first issue was figuring out how to get the metadata that I knew Ecto already
had about my schemas. Y'know, which fields are which types, so that I could use
@ -301,7 +301,7 @@ single schema you have, too! Ahh, simplicity.
Here's all the code jumbled together (and perhaps slightly different):
## All Together Now!
# All Together Now!
```elixir
# router.ex

View file

@ -22,7 +22,7 @@ Check it out in context on
<a href="https://ellie-app.com/53ypTnnFykXa1" target="_blank">Ellie</a>,
the Elm playground!
## How Did I Get Here?
# How Did I Get Here?
To preface this, I'm going to be writing a *lot* of forms with a *lot* of
fields, all of which will pretty much work exactly the same, so creating a layer
@ -42,7 +42,7 @@ of copying code from it, I'm just going to roll my own, which is generally
a really good way to learn something anyways. So, while frustrated, I was also
eager to tackle the problem.
## Code Already!
# Code Already!
Since the Elm compiler is so awesome, I've taken to what I'm calling
"compiler-driven development", which is where I write out what I expect to work
@ -248,7 +248,7 @@ do (computers, amirite?) before I made it to this point.
Now, I'm confident Elm is a tool that can do everything I need it to do. I look
forward to using it more in the future!
## Full Source
# Full Source
Check it out in context on
<a href="https://ellie-app.com/53ypTnnFykXa1" target="_blank">Ellie</a>,

View file

@ -0,0 +1,63 @@
---
title: "Fetching Go Modules via `goproxy` Inside VPN"
date: "2024-02-29"
toc: false
---
I think I finally setup the holy grail of universally being able to
fetch-by-proxy go modules through a firewall using
https://github.com/goproxy/goproxy
<!--more-->
On your internal host (such as your work machine), run the following:
```shell_session
GOPRIVATE=git.company.com GOMODCACHE=~/go goproxy server --address localhost:9981
```
On your external host (such as a network isolated Linux VM):
```shell_session
ssh -L 9981:localhost:9981 $INTERNALHOST &
GOPROXY=http://localhost:9981,direct go mod tidy
```
Of course, the tunneling is optional and you can use a non-`localhost`
`--address` when running `goproxy server`, but then of course you are dealing
with this proxy being open on the LAN, which may upset security in some cases.
And bam! Now you can fetch go modules as if you're on the VPN even if you're not
on the VPN.
You can use something like `go env -w GOPROXY=http://localhost:9981,direct` to
avoid prefixing all your `go` commands with the environment variable. Obviously,
this can cause things to break weirdly if/when the `goproxy server` dies or the
tunnel is disconnected. Tread lightly!
One last possible step is that when the proxy machine clones the repo it may try
to do so over HTTPS when you almost certainly want it to use SSH. To avoid this,
you can do something like this in `~/.gitconfig` or `~/.config/git/config` to
force git to use SSH instead of HTTPS:
```ini
[url "git@git.example.com:"]
insteadOf = "https://git.example.com"
```
My full invocation looks something like this:
```shell_session
go install github.com/goproxy/goproxy/cmd/goproxy@latest
# put this cute background job somewhere or `disown`
GOPRIVATE=git.example.com GOMODCACHE=~/go goproxy server --address localhost:58320 &
```
And then on the client:
```shell_session
# put this cute background job somewhere or `disown`
ssh -L 58320:localhost:58320 $PROXYHOST &
go env -w GOPROXY=http://localhost:58320,direct
go mod tidy
```

View file

@ -1,11 +1,14 @@
---
title: "iex and dbg/1 without pry prompts"
date: "2023-06-22"
toc: false
---
I love `iex -S mix ...` but I usually don't like when `dbg` asks me to `pry`.
Just show me my data! Well, today I learned about `iex --no-pry`:
<!--more-->
```console
$ iex --help
Usage: iex [options] [.exs file] [data]
@ -15,4 +18,4 @@ Usage: iex [options] [.exs file] [data]
Now I can `iex --no-pry -S mix ...` and just see output instead of dealing with
the prompts for `pry`ing! Not sure if anybody else felt this pain, but there
ya go.
ya go.

View file

@ -0,0 +1,382 @@
---
date: "2024-07-08T16:34:00-05:00"
title: "Learn Flakes the Fun Way"
draft: false
toc: false
---
This post is a Flake-based rewrite of [Learn Nix the Fun Way on fzakaria.com][0].
I really enjoyed the content of the post and wanted to write it as a Nix
user who is just using and prefers Flakes. It does add a few extra steps and
complexity, but I think it's still valuable and perhaps reveals a bit more about
Nix and why it's pretty fantastic. I also include a few neat things that are
more Flake-specific at the end.
But yes, it's basically plagiarism.
<!--more-->
## what-is-my-ip
Let's walk through a single example of a shell script one may write, `what-is-my-ip`:
```bash
#!/usr/bin/env bash
curl -s http://httpbin.org/get | jq --raw-output .origin
```
Sure, it's _sort of portable_, if you tell the person running it to have _curl_ and _jq_. What if you relied on a specific version of either, though?
Nix **guarantees** portability.
We might leverage _[Nixpkgs' trivial builders](https://ryantm.github.io/nixpkgs/builders/trivial-builders/)_ (specifically, `writeShellScriptBin`) in a basic Nix Flake to turn this into a Nix derivation (i.e. build recipe):
```nix
# flake.nix
{
inputs.nixpkgs.url = "nixpkgs/nixos-24.05";
outputs = {nixpkgs, ...}: let
systems = ["aarch64-linux" "aarch64-darwin" "x86_64-darwin" "x86_64-linux"];
pkgsFor = func: (nixpkgs.lib.genAttrs systems (system: (func (import nixpkgs {inherit system;}))));
in {
packages = pkgsFor (pkgs: {
default = pkgs.callPackage ./what-is-my-ip.nix {};
});
};
}
```
```nix
# what-is-my-ip.nix
{pkgs}:
pkgs.writeShellScriptBin "what-is-my-ip" ''
${pkgs.curl}/bin/curl -s http://httpbin.org/get | ${pkgs.jq}/bin/jq --raw-output .origin
''
```
> 😬 Avoid over-focusing on the fact I just introduced a new language and a good chunk of boilerplate. Just come along for the ride.
Here we are pinning our package to dependencies which come from NixOS/Nixpkgs release branch 24.05.
We can build our package and find out the Nix store path (which contains the
hash) like so:
```console
$ nix build --print-out-paths
/nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip
```
And, of course, we can run our built result:
```console
$ ./result/bin/what-is-my-ip
24.5.113.148
```
Or run it from the Flake directly:
```console
$ nix run
24.5.113.148
```
### Graph Dependencies
Now that this is in our Nix store, we've naturally modeled our dependencies
and can do _fun_ things like generate graph diagrams:
```console
$ nix-store --query --graph $(readlink result) | nix shell nixpkgs#graphviz -c dot -Tpng -o what-is-my-ip-deps.png
```
[![Image of what-is-my-ip dependencies as a graph](/img/what-is-my-ip-deps.png)](/img/what-is-my-ip-deps.png)
### Developer Environments
Let's add a _developer environment_ which contains our new tool.
This is a great way to create developer environments with reproducible tools.
Generally, though, `devShells` are for working _on_ the product of the Flake and
do not _usually_ include the product itself.
```diff
diff --git a/flake.nix b/flake.nix
index 2a99357..ab32421 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1,11 +1,24 @@
{
inputs.nixpkgs.url = "nixpkgs/nixos-24.05";
- outputs = {nixpkgs, ...}: let
+ outputs = {
+ self, # we will need to reference our own outputs to pull in the package we've declared
+ nixpkgs,
+ ...
+ }: let
systems = ["aarch64-linux" "aarch64-darwin" "x86_64-darwin" "x86_64-linux"];
pkgsFor = func: (nixpkgs.lib.genAttrs systems (system: (func (import nixpkgs {inherit system;}))));
in {
packages = pkgsFor (pkgs: {
default = pkgs.callPackage ./what-is-my-ip.nix {};
});
+
+ devShells = pkgsFor (pkgs: {
+ default = pkgs.mkShell {
+ packages = [self.outputs.packages.${pkgs.system}.default];
+ shellHook = ''
+ echo "Hello, Nix!"
+ '';
+ };
+ });
};
}
```
```console
$ nix develop -c $SHELL
Hello, Nix!
$ which what-is-my-ip
/nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip/bin/what-is-my-ip
```
🕵️ Notice that the hash `lr6wlz2652r35rwzc79samg77l6iqmii` is _exactly_ the same which we built earlier.
### Deployment
We can now do binary or source deployments 🚀🛠️📦 since we know the full dependency closure of our tool. We simply copy the necessary `/nix/store` paths to another machine with Nix installed.
```console
$ nix copy --to ssh://beefcake $(nix build --print-out-paths)
$ ssh beefcake /nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip/bin/what-is-my-ip
98.147.178.19
```
#### With Containers
Maybe though you are stuck with Kubernetes or Docker. Let's use Nix to create an OCI-compatible image with our tool:
```diff
diff --git a/flake.nix b/flake.nix
index 99d6d52..81e98c9 100644
--- a/flake.nix
+++ b/flake.nix
@@ -10,6 +10,12 @@
in {
packages = pkgsFor (pkgs: {
default = pkgs.callPackage ./what-is-my-ip.nix {};
+ container = pkgs.dockerTools.buildImage {
+ name = "what-is-my-ip-container";
+ config = {
+ Cmd = ["${self.outputs.packages.${pkgs.system}.default}/bin/what-is-my-ip"];
+ };
+ };
});
devShells = pkgsFor (pkgs: {
```
```console
$ docker load < $(nix build .#container --print-out-paths)
Loaded image: what-is-my-ip-docker:c9g6x30invdq1bjfah3w1aw5w52vkdfn
$ docker run -it what-is-my-ip-container:c9g6x30invdq1bjfah3w1aw5w52vkdfn
24.5.113.148
```
Cool! Nix + Docker integration perfectly. The image produced has only the files exactly necessary to run the tool provided, effectively **distroless**. You may also note that if you are following along, your image digest is exactly the same. **Reproducibility!**
#### In Virtual Machines
Finally, let's take the last step and create a reproducible operating system using NixOS to contain only the programs we want:
```diff
diff --git a/flake.nix b/flake.nix
index 99d6d52..81e98c9 100644
--- a/flake.nix
+++ b/flake.nix
@@ -20,5 +26,27 @@
'';
};
});
+
+ nixosConfigurations = let
+ system = "x86_64-linux";
+ in {
+ default = nixpkgs.lib.nixosSystem {
+ inherit system;
+ modules = [
+ {
+ users.users.alice = {
+ isNormalUser = true;
+ # enable sudo
+ extraGroups = ["wheel"];
+ packages = [
+ self.outputs.packages.${system}.default
+ ];
+ initialPassword = "swordfish";
+ };
+ system.stateVersion = "24.05";
+ }
+ ];
+ };
+ };
};
}
```
Now we can run this NixOS configuration as a reproducible virtual machine:
```console
$ nixos-rebuild build-vm --flake .#default
$ ./result/bin/run-nixos-vm
# I/O snippet from QEMU
nixos login: alice
Password:
$ readlink $(which what-is-my-ip)
/nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip/bin/what-is-my-ip
$ what-is-my-ip
24.5.113.148
```
💥 Hash `lr6wlz2652r35rwzc79samg77l6iqmii` present again!
We took a relatively simple script through a variety of applications in the Nix ecosystem: build recipe, shell, docker image, and finally NixOS VM.
## Flakes as All Kinds of Package Management
One of the super neat part about Flakes is that anywhere you find a Flake, you
can make use of it. Try it out now!
> **NOTE**: The following obviously runs code from the internet. Be wary of doing this in general.
Run a Flake's package:
```console
$ nix run git+https://git.lyte.dev/lytedev/learn-flakes-the-fun-way
24.5.113.148
```
Enter a Flake's development environment:
```console
$ nix develop git+https://git.lyte.dev/lytedev/learn-flakes-the-fun-way -c $SHELL
Hello, Nix!
$ what-is-my-ip
24.5.113.148
# want to hack on the Helix text editor?
$ nix develop github:helix-editor/helix
# yeah, seriously! that's it!
```
Load a Flake's docker image and run it:
```console
$ docker load < $(nix build git+https://git.lyte.dev/lytedev/learn-flakes-the-fun-way#container --print-out-paths)
Loaded image: what-is-my-ip-container:wg0z43v4sc1qhq7rsqg02w80vsfk9dl0
$ docker run -it what-is-my-ip-container:c9g6x30invdq1bjfah3w1aw5w52vkdfn
```
Run a Flake's NixOS configuration as a virtual machine:
```console
$ nixos-rebuild build-vm --flake git+https://git.lyte.dev/lytedev/learn-flakes-the-fun-way#default
Done. The virtual machine can be started by running /nix/store/xswwdly9m5bwhcz9ajd6km5hx9vdmfzw-nixos-vm/bin/run-nixos-vm
$ /nix/store/xswwdly9m5bwhcz9ajd6km5hx9vdmfzw-nixos-vm/bin/run-nixos-vm
```
Run an exact replica my workstation in a virtual machine (as configured via Nix
-- meaning it will not have my passwords on it 😉):
```console
# NOTE: this will probably take a good, long time to build and lots of bandwidth
# NOTE: you probably won't even be able to login
$ nixos-rebuild build-vm --flake git+https://git.lyte.dev/lytedev/nix#dragon
Done. The virtual machine can be started by running /nix/store/abc-nixos-vm/bin/run-dragon-vm
$ /nix/store/abc-nixos-vm/bin/run-dragon-vm
```
## Flakes as Inputs
Finally, Flakes can take any other Flake as input and use them for their own outputs. This means that they compose wonderfully. Any Flake can use anything from any other Flake. They're basically big ol' distributed functions (if you squint just right). `nixpkgs` itself is "simply" a huge Flake.
You can generate your own Flake however you like; Flakes provide templating
facilities. Here, you can use my very own template:
```console
$ mkdir -p ~/my-fun-flake
$ cd ~/my-fun-flake
$ nix flake init --template git+https://git.lyte.dev/lytedev/nix#nix-flake
$ git init
$ git add -A
$ git commit -am 'initial commit'
$ nix develop -c $SHELL
```
And now you have everything you need to work on a Nix flake as if you were me!
To add the package from this tutorial, we can simply do this:
```diff
diff --git a/flake.nix b/flake.nix
index 408d4f2..c5d6883 100644
--- a/flake.nix
+++ b/flake.nix
@@ -2,16 +2,20 @@
inputs.pre-commit-hooks.url = "github:cachix/pre-commit-hooks.nix";
inputs.pre-commit-hooks.inputs.nixpkgs.follows = "nixpkgs";
+ inputs.learn-flakes-the-fun-way.url = "git+https://git.lyte.dev/lytedev/learn-flakes-the-fun-way";
+ inputs.learn-flakes-the-fun-way.inputs.nixpkgs.follows = "nixpkgs";
+
outputs = {
self,
nixpkgs,
pre-commit-hooks,
+ learn-flakes-the-fun-way,
...
}: let
systems = ["aarch64-linux" "aarch64-darwin" "x86_64-darwin" "x86_64-linux"];
forSystems = nixpkgs.lib.genAttrs systems;
in {
formatter = genPkgs (pkgs: pkgs.alejandra);
@@ -26,7 +30,7 @@
devShells = genPkgs (pkgs: {
nix = pkgs.mkShell {
- packages = with pkgs; [nil alejandra];
+ packages = with pkgs; [nil alejandra learn-flakes-the-fun-way.packages.${pkgs.system}.default];
inherit (self.outputs.checks.${pkgs.system}.pre-commit-check) shellHook;
};
```
Afterwards you can re-enter your development shell and run our script:
```console
$ nix develop -c $SHELL
$ what-is-my-ip
24.5.113.148
```
## Other Flakey (but in a good way) Tools
On top of all this, many very awesome tools can exist. You can leverage a `direnv`-style setup such that whenever you enter a directory you enter its development shell and are completely ready to work within that project with all the necessary tools with all the correct versions _reproducibly_.
Flakes have `checks` which are run when you run `nix flake check`. You can use this for CI to ensure that your Flake is always able to build whatever packages it needs or even check that code is formatted correctly.
All of this in one place makes for a pretty incredible developer experience.
## Conclusion
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.
**Learn Flakes the fun way.**
[0]: https://fzakaria.com/2024/07/05/learn-nix-the-fun-way.html

View file

@ -5,7 +5,12 @@ draft: false
toc: false
---
I saw a post recently on ye ole fediverse that linked [this post](https://dev.to/nickytonline/do-you-have-a-uses-page-5b82) and I was inspired to create mine! So I did! <a href="/uses">You can view it here</a>!
I saw a post recently on ye ole fediverse that [asked developers if they have
a "uses" page](https://dev.to/nickytonline/do-you-have-a-uses-page-5b82) and
I was inspired to create mine. I wrote up just about everything I use on a
weekly basis.
<a href="/uses">You can read about all the things I use here</a>!
<!--more-->

View file

@ -0,0 +1,111 @@
---
date: "2023-07-06T14:32:00-05:00"
title: "Backups with Restic"
draft: true
toc: false
---
For the longest time, my backup setup has been [a script I run manually that
was quite dumb][backupify] that had no features other than encryption. After
getting my feet wet with `btrfs` somewhat recently and seeing the magic of
deduplication, compression, and snapshots, I was all-in on these features and
also wanted them for my backups.
<!--more-->
# TL;DR
- Install `restic` on both machines (may only be needed on the backupper?)
- Create a `restic` user on the backuppee, setup a directory for backups with
the right permissions, and add the backupper's public key
- `restic -r sftp:restic@backuppee:/backups init` to setup a repo, put the
password in a secret place accessible only to the backupper user
- `for d in $DIRS; do RESTIC_PASSWORD_COMMAND="load secret restic-key" restic -r sftp:restic@backuppee:/backups "$d"; done`
# Planning
The most important thing to think about when it comes to backups is to think
about what you are protecting. It's easy enough to just backup _everything_ and
I know plenty of folks that do this! However, I'm not that type. I like to keep
things pretty minimal, so I'll evaluate which things truly are worth backing up:
In my case, the only things I really want to back up are anything that might be
considered unique data that cannot be easily reproduced such as the following:
- Family photos and videos
- Secrets, keys, and anything else that provides access to other important
assets
- Communications and their context (emails, texts, etc.)
- Backups of devices for restoration in case of bricking (phones and nintendo
consoles come to mind)
- Source code for projects
My current solutions for these are varied:
- **Family Pictures**: Google Photos
- I would love to possibly look into a self-hosted solution, but Google Photos
is unfortunately too easy and cheap
- **Secrets**: These go into a combination of `password-store` and Vaultwarden.
- The `password-store` database is backed up pretty much automatically via
`git` to a handful of places and the data is encrypted at rest.
- The Vaultwarden database is part of the "mission critical" server backup
that happens manually. These backups are untested, but anything in here
should ultimately be recoverable from redundancies in the `password-store`
database via "forgot my password" mechanisms.
- **Communications**: These are handled by whatever cloud services the
communications happen over. Email is all Gmail at the moment, chats are
varied, etc.
- **Device Backups**: These have been simple enough. Copy the backups to the
various backup locations and forget about them.
- **Code**: I have pretty good `git` habits after almost 15 years of version
control'ing, so I push code all the time to a server which in-turn backs up
the code.
So where am I putting this data? I have a few larger disks here at my house, but
I also host a sprinkling of machines at friends and family's houses out of the
way and we share space as needed, allowing all of us to have redundant remote
backups. That said, my machines there are not the most robust. Here are things I'm concerned about:
- Running out of space
- No deduplication means this _will_ happen eventually.
- Bitrot
- They say it's rare, and perhaps I'm confusing disk damage with bitrot, but
I definitely have been bit by this in some form or another. I want my backup
system to combat this as much as possible (checksums and error correction
via btrfs) but also to somehow regularly and automatically let me know if
and when it occurs
- Not automated
- I would have a lot more peace-of-mind if I knew I could just backup
everything nightly and not worry about it.
Backing up everything nightly was not an option currently, since I have ~1TB of
data backed up and I currently just sync over everything in the local backup
directory via rsync. I know, I've probably got the wrong flags, since rsync
should be just fine for this, but I also wanted deduplication and a system that
would let me pull out individual files if I wanted.
# Enter Restic
Restic pretty much seemed perfect. Seemed simple enough to setup and manage, so I gave it a shot
My current goals are certainly "good enough", but the lack of automation and terribly inefficient use of bandwidth with my remote hosts was _not_ ideal:
# Setup
I aimed to keep things pretty secure, so I setup a user specifically for my
backups on my backuppee devices, the machines with big hard disks (calm down)
that would hold the backed-up data.
My backupper machines would be the ones pushing data, which, for now, was really
just one box. My server which houses the actually important data.
All my other machines really just interface with that server to push and pull
code and data via git. Pretty much anything else should be lose-able in my
situation. I use Google Photos for backing up pictures, so I don't really worry
about those for now. The only other data I want backed up is older backups
(giant tarballs).
[backupify]: https://git.lyte.dev/lytedev/dotfiles/src/commit/afed5fa6bbb6bc2733a3cadcb940d6599823b59d/common/bin/backupify

428
content/blog/rust-vs-go.md Normal file
View file

@ -0,0 +1,428 @@
---
title: "Rust vs. Go"
date: "2024-06-06"
draft: true
---
A lot of folks have been pitting Go and Rust against each other. Normally, I
would abstain from throwing my opinion out on the internet, but it's come up at
work and since I heavily prefer one over the other (Rust subjectively!), I want
to write out why that is. Hopefully it is useful to others out there.
<!--more-->
# Rust versus Golang
I'm aiming to keep this succinct so the people I share this with will actually
read it. This will probably be dense and pointed, as I'm trying to construct an
argument.
This article is opinion and not stating objective fact. These are my
preferences. I have roughly-equivalent Rust experience, exposure, and knowledge
than I do Golang, though I have run, operated, and maintained far more Golang
services at my job than I have Rust.
Also by posting this I'm sure I will get free <s>flame wars</s> consultation on
my Go code and become a better programmer! 😜
## Stop Comparing! They're Different!
Of course they are! This doesn't mean they can't be used to solve the same
problems. In my experience, one is simply better than the other for the same
use-cases.
They're also the same _category_ of thing: general purpose programming
languages. Which means it's good to talk about the various strengths and
weaknesses of the two.
# Why Rust
## Rust has _all_ of Go's superpowers
Ok, except it's uber simplicity. But what you lose in simplicity, you gain in
many other areas. I'll elaborate later.
### Goroutines
Use `tokio::spawn`. Yes, they are different (mainly stackless versus
stackful) but the goal and considerations are effectively the same.
If you don't like Tokio or can't use it, there are different implementations of
this on top of Rust's `async/await` system that have the same benefits.
While it's arguably not as cohesive as Go's implementation, it's there if you
want it.
### Channels
Rust has these. See `std::sync::mpsc`.
### Defer
Largely unnecessary thanks to the compiler, but you can implement your
own via the `Drop` trait or reach for the `scopeguard` crate.
### Tooling
Again, Rust has an incredible tools ecosystem just like Go. Better in many ways
since many tools considered _necessary_ for modern and idiomatic Go are simply
not for Rust, such as code generation tools. Such needs are instead fulfilled by
metaprogramming (macros) instead.
### Explicit error handling
Haha! I guess I'm the four-millionth person to write about how awful `if err !
= nil` is, but seriously, Rust's `?` operator and pattern matching abilities are
so fantastic.
You're gonna _love_ Rust in comparison.
## Rust is expressive
Now we're getting to some of the things Rust is simply better than Go at.
Being expressive has downsides. A common question I'll find myself asking is
"Which of these four ways is the _best_ way to do this?"
But I would rather be able to have the expressions in the code mirror the intent
_more_ than be stuck with Go's simplicity.
## Rust code has fewer bugs
Obviously this isn't entirely a fair thing to say, but it is true that Rust
simply makes it incredibly difficult (effectively impossible?) to have certain
kinds of bugs in your code as opposed to Go.
I simply write my code in Rust and when it compiles it works as I intended.
Rust code and the compiler make it much more difficult to represent invalid
states.
Meanwhile Go code has nil checks, implicit interfaces, and a few other things
you will inevitably run into.
That said, Go's tooling has been improving a _lot_ around this in my opinion;
`golangci-lint` is awesome and shores up much of these issues.
## Inline tests
I strongly dislike being required to put my unit tests in separate files. Call
me crazy, but having the tests in the same file is something I find appealing.
Also
[doctests](https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html).
The best kind of inline test. Gotta have 'em.
## Modules and dependencies
Go's module system got bolted on a little late and suffers from the Google
monorepo assumptions/expectations situation. In some ways, this is actually
very cool. In many ways, though, it's pretty annoying. These days, if you follow
the "modern" path, things can work great.
Rust has its own issues with Cargo and its ties to GitHub, but even those can be
worked around if you like.
Overall, Rust has a more familiar and approachable module system.
## Making Changes
When you change a hunk of Go code, you can't know for sure what else in the code
may need to change as a result. If you add a field to a struct, you may need to
find the places you are building that struct and update the code and hopefully
you don't miss a spot.
Rust makes changing an existing codebase less scary. It will tell you pretty
much everywhere you need to go and change other things as a result of your small
change. It totally changes the way you modify software.
## Documentation
Rust's standard library and popular crates' documentation is _generally_ much
better than Go's. Doctests are wonderful and examples are more common in Rust
crates than in Go modules.
## Macros
Having them is better than not. They are awesome. Yes, they make the compiler
slower. Yes, they can do awful stuff.
So does Go codegen.
But being able to derive implementations and affect them with attributes is
really phenomenal. Especially if you mess with something like `bevy` or `axum`.
## Traits
Rust forces you the implement a trait explicitly while Go does not. It's a small
thing, but this again lets the code communicate the intent of the programmer.
## No surprises
Nothing that executes before `main()` while Go has freakin' `init()` and of
course you can instantiate literally anything globally.
In Rust, you don't have this, which sucks and means you do `OnceLock`/
`OnceCell`/`lazy_static!` instead and it can be painful if you're so dead set on
doing something weird like that.
But I'll take no surprises and explicitness any day.
## Examples
I wanted to include a number of code examples I found particularly
Rust-favoring. A picture is worth a thousand words!
### Obvious: Error Handling
This is a simplified function in production right now:
```go
func (t *Bot) FinishReceivingRequest(req RequestInProgress) (result *Request, err error) {
result.Data = req.Data
assignee, err := t.GetAssigneeSlackUser(req)
if err != nil {
return nil, fmt.Errorf("failed to retrieve assignee's Slack user info: %w", err)
}
result.SlackAssigneeID = assignee.ID
manager, err := t.Slack.GetUserManager(assignee.ID)
if err != nil {
return nil, fmt.Errorf("failed to retrieve the assignee's manager Slack user info: %w", err)
}
result.SlackAssigneeMgrID = manager.ID
issueSkeleton, err := req.Data.BuildJiraIssue(t, req)
if err != nil {
return nil, logrus.Errorf("Failed to create Jira issue object: %+v", err)
}
issue, res, err := t.Jira.CreateIssue(issueSkeleton)
defer res.Body.Close()
if err != nil {
_, _, err := t.Slack.PostMessage(t.Conf.Slack.Channels.RequestThreads,
slack.MsgOptionDisableLinkUnfurl(),
slack.MsgOptionBlocks(
slack.NewSectionBlock(
&slack.TextBlockObject{
Type: "mrkdwn",
Text: "I failed to create a Jira ticket. See logs for details.\n\nError: " + err.Error(),
}, nil, nil,
),
),
)
if err != nil {
logrus.Errorf("failed to send Jira ticket creation failed message to slack: %v", err)
}
}
result.JiraIssueKey = &issue.Key
logrus.Infof("Jira issue created: %s", result.JiraIssueKey)
channelId, ts, err := t.Slack.PostMessage(
t.Conf.Slack.Channels.RequestThreads,
slack.MsgOptionDisableLinkUnfurl(),
req.MainMessageBlocks(),
)
if err != nil {
return result, fmt.Errorf("failed to post Slack message for request -- this means there is data that has been created that nobody knows about: %w", err)
}
result.SlackTimeStamp = ts
result.SlackChannelId = channelId
// fetch permalink to the posted message to update jira ticket with
url, err := t.Slack.GetPermalink(&slack.PermalinkParameters{
Channel: channelId,
Ts: ts,
})
if err != nil {
// TODO: retry?
logrus.Errorf("Failed to retrieve permalink for Slack message for request -- this means we could not link the jira ticket to the slack thread: %s", err)
}
req.SlackThreadURL = url
if issue != nil {
rl := jira.RemoteLink{
Object: &jira.RemoteLinkObject{
URL: url,
Title: "Slack Thread",
},
}
_, res, err := t.Jira.AddIssueRemoteLink(issue.ID, &rl)
defer res.Body.Close()
if err != nil {
// TODO: retry?
logrus.Errorf("Failed to add remote link to Slack message thread (ts: '%s', link: '%s') for jira issue (key: '%s'): %s", ts, url, issue.Key, err)
}
}
logrus.Infof("Request data: %+v", req)
logrus.Tracef("request's db data json: %+v", req.DataJSON)
logrus.Tracef("request's db data: %+v", req.Data)
dbResult := t.DB.Create(req)
logrus.Tracef("request db create result: %+v", dbResult)
// it's not the end of the world if we cannot update the main message, so we try it, log it, and keep going
blocks, err = req.MainMessageBlocks()
if err != nil {
logrus.Errorf("Failed to build slack blocks for original message update: %v", err)
}
_, _, _, err = t.Slack.UpdateMessage(
t.Conf.Slack.Channels.RequestThreads,
req.SlackTimeStamp,
blocks,
)
if err != nil {
logrus.Errorf("Failed to update original message: %v", err)
}
_, _, err = t.Slack.PostMessage(
channelId,
slack.MsgOptionTS(ts),
slack.MsgOptionBlocks(req.FirstReplyMessageBlocks()...),
)
if err != nil {
logrus.Errorf("failed to post follow-up message in thread: %s", err)
}
if req.Data.NeedsApproval() {
req, err = t.ApprovalSetup(req)
if err != nil {
return result, fmt.Errorf("failed to setup approval: %w", err)
}
}
return
}
```
Here's how it looks in my Rust dream rewrite (thanks `color_eyre::Result`):
```rust
// fetch permalink to the posted message to update jira ticket with
url, err := t.Slack.GetPermalink(&slack.PermalinkParameters{
Channel: channelId,
Ts: ts,
})
if err != nil {
// TODO: retry?
logrus.Errorf("Failed to retrieve permalink for Slack message for request -- this means we could not link the jira ticket to the slack thread: %s", err)
}
req.SlackThreadURL = url
if issue != nil {
rl := jira.RemoteLink{
Object: &jira.RemoteLinkObject{
URL: url,
Title: "Slack Thread",
},
}
_, res, err := t.Jira.AddIssueRemoteLink(issue.ID, &rl)
defer res.Body.Close()
if err != nil {
// TODO: retry?
logrus.Errorf("Failed to add remote link to Slack message thread (ts: '%s', link: '%s') for jira issue (key: '%s'): %s", ts, url, issue.Key, err)
}
}
logrus.Infof("Request data: %+v", req)
logrus.Tracef("request's db data json: %+v", req.DataJSON)
logrus.Tracef("request's db data: %+v", req.Data)
dbResult := t.DB.Create(req)
logrus.Tracef("request db create result: %+v", dbResult)
// it's not the end of the world if we cannot update the main message, so we try it, log it, and keep going
blocks, err = req.MainMessageBlocks()
if err != nil {
logrus.Errorf("Failed to build slack blocks for original message update: %v", err)
}
_, _, _, err = t.Slack.UpdateMessage(
t.Conf.Slack.Channels.RequestThreads,
req.SlackTimeStamp,
blocks,
)
if err != nil {
logrus.Errorf("Failed to update original message: %v", err)
}
_, _, err = t.Slack.PostMessage(
channelId,
slack.MsgOptionTS(ts),
slack.MsgOptionBlocks(req.FirstReplyMessageBlocks()...),
)
if err != nil {
logrus.Errorf("failed to post follow-up message in thread: %s", err)
}
if req.Data.NeedsApproval() {
req, err = t.ApprovalSetup(req)
if err != nil {
return result, fmt.Errorf("failed to setup approval: %w", err)
}
}
return
}
```
# Why Not Rust
While I've made it clear that I prefer Rust, I also recognize that there are
things that I consider Go to handle better.
## Standard libraries
Go has a big standard library and just includes a ton of stuff. I wish Rust had
something similar for "blessed" "core" crates. I understand the standard library
for a systems-level language must be small, but that's how I feel.
### Other Builtins
Go can kind of manage its own versions and everything and you pretty much just
do `go whatever` and get on with your life.
With Rust, you need to learn a couple different tools (but mainly just `cargo`)
such as `rustup`.
## Go is Simple
It's easy to get caught up trying to do things in Rust the "right" way and work
with the borrow checker and think about the optimal solution -- to revel in the
computer science puzzle that the compiler presents to you.
In Go, that is simply less true. You will do the obvious thing at perhaps some
slight performance cost and get back to implementing your feature and shipping
code. It's almost always going to be perfectly sufficient and generally easy to
come back to and understand.
## Go Compilation Speed
The Go compiler is wonderfully quick. In my experience, though, there are
usually codegen tools you end up adopting that offset this speed and slow things
down again that require extra vigilance and tending to keep things fast.
With Rust, you have one compiler speed: not so fast. It's still sufficiently
quick for me, especially on a Ryzen 9 5950X or an Apple M1 Max. For most
projects, it does a good job being incremental, too.
But you will build Rust
## Build Tinkering
With Go, you're just gonna build the binary and run it.
With Rust, you _do_ have to think about `--release` performance -- such as when
writing Leetcode solutions against your friends.
I'm getting nitpicky here, but... it's my article.
## Versioning
Rust has issues with MSRV "promises" being largely unenforceable due to Cargo
lockfiles not being `--locked` by default. This is pretty mind-boggling to me.
Could also be a skill issue.

View file

@ -23,13 +23,13 @@ https://git.faceless.lyte.dev/lytedev/weechat-matrix-encryption-guide.git
/tmp/wmeg && $EDITOR /tmp/wmeg/easy-script.bash && /tmp/wmeg/easy-script.bash`
+ [Configure](#configuration) as needed
## Python Versions
# Python Versions
We need to establish which version of Python your WeeChat is using. You can find
this out in WeeChat with `/python version`. In my case, my `python` binary is
3.7.2 (`python -V`) while my WeeChat Python version is 2.7.15.
## Dependencies
# Dependencies
There are a number of dependencies we can go ahead and start grabbing. The main
repository lists a number of them in the `README`, so we will grab those. We
@ -44,7 +44,7 @@ Notice that we left out the [`matrix-nio`][matrix-nio] dependency. It's not in
PyPi, so we can't just `pip2 install matrix-nio` (yet!) and PyPi's `nio` package
is something probably unrelated, so we'll need to install it manually.
## Installing `matrix-nio`
# Installing `matrix-nio`
Let's go ahead and clone down the repository and get ready to do some stuff:
@ -98,7 +98,7 @@ package:
sudo python2 ./setup.py install
```
## Weechat Plugin Installation
# Weechat Plugin Installation
Once we've done that, we should have all the dependencies for `weechat-matrix`,
so let's go ahead and clone that and install it!
@ -111,7 +111,7 @@ make install
Done!
## Configuration
# Configuration
The rest is up to you! You'll need to [configure your Matrix servers within
WeeChat][weechat-matrix-config] and then verify keys. Verifying keys isn't

View file

@ -0,0 +1,90 @@
---
title: Minecraft Server Status Checker
toc: false
aliases:
/ourcraft-status
---
<noscript>
<p>
It looks like you have javascript disabled! This page uses javascript to query a third-party server which fetches data from our minecraft servers.
</p>
</noscript>
<button id="check-minecraft-server-status">Reload</button>
<div id="server-status">
<p id="server-status-loading">
Checking minecraft servers' status...
</p>
<ul id="servers">
</ul>
</div>
<script>
function checkMinecraftServerStatus() {
const loading = document.getElementById("server-status-loading")
const servers = document.getElementById("servers")
loading.style.display = "inherit";
servers.style.display = "none";
try {
fetch("https://deno-deploy-private.deno.dev/minecraft-server-status").then(res => {
res.json().then(statuses => {
console.log(statuses)
loading.style.display = "none";
servers.style.display = "inherit";
const newChildren = [];
for (let k of Object.keys(statuses)) {
let pl = null;
const status = statuses[k]
const el = document.createElement("li")
const s = document.createElement("strong")
s.textContent = k
const c = document.createElement("code")
c.className = "chroma"
const cs = document.createElement("span")
c.appendChild(cs)
cs.className = (status.online ? "na" : "err")
cs.textContent = status.online ? "ONLINE" : "OFFLINE"
const p = document.createElement("span")
if (status.players) {
p.textContent = ` with ${status.players.online}/${status.players.max} players`
if (status.players.online > 0 && status.players.list) {
pl = document.createElement("details")
pl.style.marginTop = "0"
const pld = document.createElement("summary")
pld.textContent = "Players List"
pld.style.color = "var(--link-fg)"
pld.style.textDecoration = "underline"
const plist = document.createElement("ul")
pl.appendChild(pld)
pl.appendChild(plist)
for (let p of status.players.list) {
const pi = document.createElement("li")
pi.textContent = p.name
plist.appendChild(pi)
}
}
} else {
p.textContent = ` with ??/?? players`
}
el.replaceChildren(
s,
document.createTextNode(": "),
c,
p,
)
if (pl != null) el.appendChild(pl)
newChildren.push(el)
}
servers.replaceChildren(...newChildren)
})
}).catch(err => console.error(err))
} catch (err) {
console.err(err)
}
}
document.getElementById("check-minecraft-server-status").addEventListener("click", checkMinecraftServerStatus)
document.addEventListener("DOMContentLoaded", checkMinecraftServerStatus)
</script>

View file

@ -1,96 +0,0 @@
---
title: Ourcraft Server Status Checker
---
<p id="server-status-loading">
Checking <code>ourcraft.lyte.dev</code> server status...
</p>
<div id="server-status-check-failed">
<p>
Failed to retrieve <code>ourcraft.lyte.dev</code> server status. Unfortunately, that usually means we're <code><span style="color: var(--syntax-mag);">OFFLINE</span></code>.
</p>
<p>
Please yell at <code>@lytedev</code> in the Ourcraft Discord to fix it!
</p>
<p><button id="server-status-check-button">Try Again</button></p>
</div>
<div id="server-status-online" style="display: none;">
<p>Server <code>ourcraft.lyte.dev</code> is <code><span style="color: var(--syntax-name);">ONLINE</span></code></p>
<p id="server-status-player-info" style="display: none;">There are currently <span id="server-status-current-num-players">0</span>/<span id="server-status-max-players">20</span> players online.</p>
<h3 id="server-status-player-sampling-header" style="display: none;">Online Players</h3>
<ul id="server-status-player-sampling" style="display: none;">
<li>Player</li>
</ul>
<p id="server-status-player-sampling-insufficient" style="display: none">... and more!</p>
</div>
<p id="server-status-offline" style="display: none;">
Server is <code><span style="color: var(--syntax-mag);">OFFLINE</span></code>
</p>
<script src="https://mcapi.us/scripts/minecraft.min.js"></script>
<script>
function showServerStatus(key) {
for (const name of ['server-status-check-failed', 'server-status-online', 'server-status-offline', 'server-status-loading']) {
if (name == key) {
document.getElementById(name).style.display = null
} else {
document.getElementById(name).style.display = 'none'
}
}
}
function checkServerStatus() {
showServerStatus('server-status-loading')
MinecraftAPI.getServerStatus('ourcraft.lyte.dev', {port: 25565}, function (err, status) {
console.debug("Status Errors:", err || false)
if (err || !('online' in status)) {
showServerStatus('server-status-check-failed')
} else {
handleOnlineStatus(status)
}
});
}
function handleOnlineStatus(status) {
console.debug("Status Data:", status)
if (status.online === true) {
showServerStatus('server-status-online')
const info = document.getElementById('server-status-player-info')
const samplingHeader = document.getElementById('server-status-player-sampling-header')
const samplingList = document.getElementById('server-status-player-sampling')
const insufficient = document.getElementById('server-status-player-sampling-insufficient')
info.style.display = 'none'
samplingHeader.style.display = 'none'
samplingList.style.display = 'none'
insufficient.style.display = 'none'
if ('players' in status) {
if ('max' in status.players && 'now' in status.players) {
info.style.display = null
document.getElementById('server-status-current-num-players').textContent = status.players.now
document.getElementById('server-status-max-players').textContent = status.players.max
if ('sample' in status.players && status.players.now > 0) {
samplingHeader.style.display = null
samplingList.style.display = null
samplingList.textContent = ''
for (const player of status.players.sample) {
const newListItem = document.createElement('li')
newListItem.textContent = player.name
newListItem.title = player.id
samplingList.appendChild(newListItem)
}
if (status.players.sample.length != status.players.now) {
document.getElementById('server-status-player-sampling-insufficient').style.display = null
}
}
}
}
} else {
showServerStatus('server-status-offline')
}
}
document.getElementById("server-status-check-button").addEventListener("click", checkServerStatus)
checkServerStatus()
</script>

164
content/salary.md Normal file
View file

@ -0,0 +1,164 @@
---
# date: 2017-02-22T14:43:02-06:00
title: Salary Transparency
description: "My salary information made public to help combat pay discrimination."
toc: false
---
Below is a list of jobs and salaries I've had. Dollar amounts are annual
estimates in USD. Stock is generally sold as soon as legally possible for
simplicity and personal policy. Dates are rough estimates in many cases.
## Why?
- Combat pay discrimination
- Help fellow employees negotiate with employers
- Reduce stigma around talking about personal finances
## Jobs
<section class="table-container">
<table>
<thead>
<tr>
<th>Company</th>
<th>Title</th>
<th>Start Date</th>
<th>End Date</th>
<th>Salary</th>
<th>Annual Bonus Target</th>
<th>Annual Stock Value</th>
<th>Departure Reason</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bill</td>
<td>Sr. Staff <abbr title="Software Engineer">SWE</abbr></td>
<td>2024-08-30</td>
<td>Still here!</td>
<td>$217,350</td>
<td>$22,000</td>
<td>$130,000</td>
<td>N/A</td>
<tr/>
<tr>
<td>Bill</td>
<td>Sr. Staff <abbr title="Software Engineer">SWE</abbr></td>
<td>2023-08-20</td>
<td>2024-08-30</td>
<td>$210,000</td>
<td>$21,000</td>
<td>$120,000</td>
<td>Raise</td>
<tr/>
<tr>
<td>Bill</td>
<td>Staff <abbr title="Software Engineer">SWE</abbr></td>
<td>2021-06-05</td>
<td>2023-08-20</td>
<td>$192,258</td>
<td>$19,226</td>
<td>$90,000</td>
<td>Promoted</td>
<tr/>
<tr>
<td>Divvy</td>
<td>Staff <abbr title="Software Engineer">SWE</abbr></td>
<td>2021-03-05</td>
<td>2023-06-01</td>
<td>$175,000</td>
<td>$8,000</td>
<td>$70,000</td>
<td>Acquired by Bill, Raises</td>
<tr/>
<tr>
<td>Uber</td>
<td><abbr title="Software Engineer">SWE</abbr> II</td>
<td>2020-12-01</td>
<td>2021-03-05</td>
<td>$165,000</td>
<td>$0</td>
<td>$90,000</td>
<td>Quit</td>
<tr/>
<tr>
<td>Postmates</td>
<td><abbr title="Software Engineer">SWE</abbr> II</td>
<td>2019-06-10</td>
<td>2020-12-01</td>
<td>$165,000</td>
<td>$0</td>
<td>$90,000</td>
<td>Acquired by Uber</td>
<tr/>
<tr>
<td>Mixon Hill</td>
<td>Systems Engineer</td>
<td>2017-12-01</td>
<td>2019-06-10</td>
<td>$60,000</td>
<td>$5,000</td>
<td>N/A</td>
<td>Quit</td>
<tr/>
<tr>
<td>Toggl</td>
<td><abbr title="Software Engineer">SWE</abbr> II</td>
<td>2017-04-01</td>
<td>2019-07-01</td>
<td>$55,000</td>
<td>$0</td>
<td>$0</td>
<td>Fired</td>
<tr/>
<tr>
<td>Emfluence</td>
<td><abbr title="Software Engineer">SWE</abbr> II</td>
<td>2014-04-01</td>
<td>2015-05-01</td>
<td>$60,000</td>
<td>$0</td>
<td>$0</td>
<td>Fired</td>
<tr/>
<tr>
<td>Voxa Ventures</td>
<td><abbr title="Software Engineer">SWE</abbr></td>
<td>2013-05-01</td>
<td>2015-03-01</td>
<td>$55,000</td>
<td>$0</td>
<td>$0</td>
<td>Company Bankrupt</td>
<tr/>
<tr>
<td>Waterway Gas & Wash</td>
<td>Line Associate</td>
<td>2012-05-01</td>
<td>2013-05-01</td>
<td>$30,000</td>
<td>$0</td>
<td>$0</td>
<td>Quit</td>
<tr/>
<tr>
<td>Price Chopper</td>
<td>Cashier</td>
<td>2011-04-01</td>
<td>2012-04-01</td>
<td>$23,000</td>
<td>$0</td>
<td>$0</td>
<td>Quit</td>
<tr/>
</tbody>
</table>
</section>
## Micro-Rant on Titles
Also, if we talked about how meaningless "levels" are in titles, just know
that... they are. They're really just there to reflect that employee's pay.
Don't judge by titles.

View file

@ -1,5 +0,0 @@
---
title: Blog
---
## Latest Tips ([RSS](/tips/index.xml))

View file

@ -11,6 +11,12 @@ description: "About my tools, workflow, and other things I use as a software dev
This page lists the tools (both physical and otherwise) that I use to do my job
as a software developer along with some thoughts on them.
While this page is likely to be out of date when you're reading it, since I am
usually trying a few small changes here and there at any given point to try and
improve things, I try to update it regularly. You can follow those updates by
looking at [the history of the source code for this page
specifically][uses-history].
For other pages like this from other folks, check out this repository:
https://github.com/wesbos/awesome-uses
@ -22,7 +28,8 @@ for simplicity, but everything listed here should be searchable. If not, let
me know! I'll try to link to anything free, though, such as software.
I'll break stuff up by topic as things come up so you can skip things that are
not interesting to you.
not interesting to you. There is also a Table of Contents at the top to help
you navigate.
I also think that in general sharing this much information about yourself isn't
the _best_ idea. However, since I'm confident the bots can't know much more
@ -31,12 +38,16 @@ fellow human beings, I think it's worth sharing. I hope you discover some cool
new stuff! Better yet, I hope you recommend me better stuff! I'm always wanting
to try new tools and discover something new that's good at something.
Regarding the configuration of my machines and the software referenced below,
[please refer to my Nix repo](https://git.lyte.dev/lytedev/nix)! It may also
be useful to look through my old [dotfiles
repo](https://git.lyte.dev/lytedev/dotfiles).
# Good morning!
I wake up when my kids do out of a Purple mattress.
I slip on my PineTime wrist watch, grab my Android smartphone, put on my
prescription glasses, and usually make some tea.
I wake up when my kids do out of a Purple mattress. I slip on my PineTime wrist
watch, grab my flashlight, Android smartphone and backpack, put on my prescription glasses,
and usually make some tea.
## Mattress: Purple King Size
@ -55,15 +66,31 @@ keeps improving! I get about two weeks of battery with light use and bluetooth
off. I get about 5 days if I've got notifications on full blast, but they
recently improved the firmware and claim this may now be more than double!
## Smart Phone: ASUS ROG Phone 5S
## Flashlight: Emisar DW4
I bought the phone that I can get root access to with the biggest battery,
nicest display, and a headphone jack. That's pretty much all I want in a phone.
The speakers are a great bonus. The two USB-C ports is actually a super nice
feature since I can connect peripherals while charging without a dock or crazy
dongle. I've also used the video mirroring on the side port to good effect a few
times in a pinch. I love this device so much I've bought it twice. I'm not sure
I've done that for anything else...
I have a phone with a flashlight. And even my watch can be enough of a
flashlight to navigate in pitch black, but I've taken to carrying an actual
flashlight. Specifically, an Emisar DW4. It has a magnetic tailcap so it can
attach near or directly to many work surfaces. It can get hilariously bright or
dim enough to be suitable for use around sleeping family members in the dark.
And it has fun RGB LEDs that can flash, show you the battery level, and just
look cool. It's not a game-changer, but at times it is incredibly convenient to
have on hand.
## Smart Phone: ASUS Zenfone 10
I've enjoyed ASUS's phones and have previously used the ROG Phone 5S. I bought
this since it maintained most of the important features of the ROG Phone while
being cheaper and my old ROG Phone started having bluetooth and phone call
issues. To be fair, I bought the international Chinese version off ebay to try
and save a buck.
The Zenfone 10 does everything I need. Lots of battery life, nice display for
reading on, good speakers, blah blah blah. Phone's get less interesting all the
time and most of them are good enough these days.
I hope a real Linux phone comes around!
### Android-Specific Software & Applications
@ -72,9 +99,14 @@ with my laptops/desktops) now in no particular order. I have no idea if any of
these have iOS equivalents, but here ya go.
- [Firefox](https://github.com/mozilla-mobile/fenix) as my web browser
- Firefox supports (some) extensions even on Android! I use the following:
- [Dark Reader](https://addons.mozilla.org/en-US/firefox/addon/darkreader/) for keeping things easy on my eyes
- [uBlock Origin](https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/) for blocking ads
- [F-Droid](https://f-droid.org) as an awesome resource for applications
- [Termux](https://github.com/termux/termux-app) for doing Linux-y and
terminal-y things on my phone
- There's also [Nix-on-Droid](https://github.com/nix-community/nix-on-droid)
which I've started using more and more
- [OpenKeychain](https://github.com/open-keychain/open-keychain) for mobile GPG
key management
- [Password Store](https://github.com/android-password-store/Android-Password-Store)
@ -91,10 +123,8 @@ these have iOS equivalents, but here ya go.
- [Obsidian](https://play.google.com/store/apps/details?id=md.obsidian) for
reading and writing my notes (sync'd via `git`)
- [Fedilab](https://codeberg.org/tom79/Fedilab) as my mobile fediverse client
- [Weechat-Android](https://github.com/ubergeek42/weechat-android) as my mobile
IRC relay interface
- [Tailscale](https://github.com/tailscale/tailscale-android) for accessing
my VPN
- [Goguma](https://git.sr.ht/~emersion/goguma) as my mobile IRC client (connected to my IRC bouncer, (Soju)[https://git.sr.ht/~emersion/soju])
- [Tailscale](https://github.com/tailscale/tailscale-android) for my VPN
- Google Wallet for NFC payments (tap-to-pay or contactless) because getting
cards out of a wallet is _so_ pre-COVID
- Google Messages for SMS, MMS, and RCS
@ -102,6 +132,13 @@ these have iOS equivalents, but here ya go.
- Pocket Casts for listening to podcasts
- Spotify for listening to music
## Backpack
The portable office! I keep my laptop, a multi-tool, extension cord, power
strip, laptop charger, a suite of adapters and flash drives, water bottle, and
the odd cable here or there. If I'm feelin' that I might be gamin', I throw in
the Steam Deck. More on all this later.
# Server Room
Once the day has started and I've said my goodbyes to the fam, I head downstairs
@ -133,6 +170,9 @@ problems immensely and makes port forwarding stuff a breeze. I'm roughly
familiar with its workings, which makes troubleshooting network problems that
much easier for me.
This is the last bastion for Arch Linux in my network and I'm excited to move.
Not because I hate Arch, but I'm _really_ loving NixOS.
## WiFi Access Point: Unifi AP-Pro
Fantastic access point that plays nicely with my very DIY home router. Not
@ -151,45 +191,45 @@ _Share the load_.
## Server(s)
I have a lot of servers, but the main server is just an ASUS Chromebox 3 that I
flashed Arch Linux to. It pretty much just runs a big ol' Docker Compose setup
with a sprinkling of other non-Docker'd services. It can do the hardware
transcoding for Jellyfin, my home media server, and just generally does not
break a sweat.
I recently was given a Dell R720xd with 20 hyperthreaded CPU cores (40 threads),
256GB RAM, and 44TB of raw disk space, which I am _very_ excited about, so I'll
probably be moving most stuff to that bad boy, though I expect the power bill to
go up _just a tad_.
I was given a Dell R720xd with dual Xeon E5-2580 CPUs (10c/20t),
256GB RAM, and 12x4TB (48TB) of raw disk space. It runs my own, my servers, and
hosts onsite backups for all my stuff and serves as an offsite encrypted backup
for some friends.
I have a few other cheap machines with larger disks at friends and family's
houses for off-site, encrypted backups of important data. I should
_really_ take the time to validate and automate my backup setup, because right
now, I do a completely garbage job of it.
houses for off-site, encrypted backups of important data. They all run NixOS and
use [its built-in restic backup setup][backups-nix].
Any paid client workloads are served via redundant mechanisms via cloud
services, generally Digital Ocean, and backed up with whatever the relevant
cloud offering is.
I run the following applications for my home:
I run the following main applications:
- Traefik to reverse proxy all the things
- [Caddy](https://traefik.io/traefik/) to reverse proxy, TLS-terminate all
the things, and serve static, public files
- A homemade chat bot for various things
- Various game servers
- A small service the multiplexes audio and video feeds
- Gitea for https://git.lyte.dev
- Nginx to serve static files for https://files.lyte.dev
- Jellyfin
- Plausible for web analytics
- PostgreSQL as the great database for anything that needs one
- MariaDB for anything too lame to use Postgres
- Various game servers (Minecraft, Factorio, Valheim, etc.)
- [Gitea](https://about.gitea.com/) for https://git.lyte.dev 💜💛💙
- [Jellyfin](https://jellyfin.org/) for streaming my video media to approved
users (family, friends, etc.)
- [PostgreSQL](https://www.postgresql.org/) as the great database for anything
that needs one
- [Vaultwarden](https://github.com/dani-garcia/vaultwarden) for sharing and
managing passwords
- [Atuin](https://atuin.sh) for sync'ing shell histories across my machines
- Samba file shares
Other details can be found in [the Nix config for the `beefcake` host][beefnix].
I run a few services from the cloud as well:
- A small DDNS application that machines report to so I have relatively up-to-
date public IP information on most of my devices (this can't run from home for
fairly obvious reasons 😉)
- Various monitoring scripts for specific things
- [A small DDNS application](https://github.com/lytedev/deno-netlify-ddns) that
machines report to so I have relatively up-to-date public IP information on
most of my devices (this can't run from home for fairly obvious reasons 😉)
- Each machine runs [the accompanying client](https://github.com/lytedev/deno-netlify-ddns-client) with unique credentials
- Various monitoring scripts for specific things (also can't run from home - who
would monitor the monitors?)
# Starting Work
@ -215,11 +255,11 @@ settings are reset for the workday from any tinkering I may have done the
previous evening, usually with musical instruments or just general goofing
around with Pipewire.
## Chair: Steelcase Gesture with Headreset
## Chair: Steelcase Gesture with Headrest
Like a mattress, very subjective. Get your chairs secondhand for way cheap and
you can get some heckin' nice chairs. I spend about 8 hours a day in my chair,
so having a good chair is well worth it, even if the price tag is 1,500USD. 😬
so having a good chair is well worth it, even if the price tag is $1,500 USD. 😬
## Desk: Custom
@ -229,7 +269,7 @@ and by the time I can't move it around by myself anymore is probably a good
indicator for retirement.
You can see an old but decent picture of it here:
https://files.lyte.dev/images/battle-station-2020.jpg
https://files.lyte.dev/images/archives/battle-station-2020.jpg
## Mouse: Logitech MX Master 3 and Mionix Avior Pro
@ -264,11 +304,11 @@ as well.
Here's roughly my monitor layout, though I usually have two of the secondary
monitors -- one on either side:
https://files.lyte.dev/images/battle-station-2022-10-13.3.jpg
https://files.lyte.dev/images/archives/battle-station-2022-10-13.3.jpg
## Laptop: Lenovo T480 and MacBooks
A recent acquisition. It was pretty busted up when I got it, but I bought the
The T480 is a recent acquisition. It was pretty busted up when I got it, but I bought the
high capacity external battery and some phat sticks of RAM and replaced the
trackpad. A little bit of superglue and a good cleaning later and I've just
fallen in love with this laptop. Sometimes I use it even though I have a really
@ -339,7 +379,7 @@ you're considering it.
## Headphones: Sony WH-1000XM4
Bought the XM3s on a Black Friday special and fell in love. _Nice_ noise
cancelling head are absolutely wonderful. They made my mom cry.
cancelling headphones are absolutely wonderful. They made my mom cry.
Work got almost all their engineers the XM4s not long after so I kind of have
2 pair. The XM4s are _much_ nicer with multiple device pairing and the firmware
@ -402,6 +442,23 @@ background color for the longest time.
## Web Applications
Firefox is awesome. I'm a big fan. I make heavy use of their "Sync" offering,
which syncs just about everything. It's very convenient and I'm sure it will
somehow bite me later.
I use the following must-have browser extensions:
- [hide-scrollbars](https://addons.mozilla.org/en-US/firefox/addon/hide-scrollbars/) to hide scroll bars
- [FoxyProxy](https://addons.mozilla.org/en-US/firefox/addon/foxyproxy-standard/) for proxying through to various resources
- [PassFF](https://github.com/passff/passff) for interfacing with my `password-store`
- [Firefox Multi-Account Containers](https://addons.mozilla.org/en-US/firefox/addon/multi-account-containers/) for being logged into all my accounts simulataneously
- This is probably good enough reason _alone_ to use Firefox over anything else
- [Tree Style Tab](https://addons.mozilla.org/en-US/firefox/addon/tree-style-tab/) for a much nicer way of managing four billion tabs
- [Dark Reader](https://darkreader.org/) to keep things easy on my eyes
- [uBlock Origin](https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/) for blocking ads
- [Vimium](https://addons.mozilla.org/en-US/firefox/addon/vimium-ff/) for moar keyboard shortcuts
- [open-url-in-container](https://github.com/honsiorovskyi/open-url-in-container) for programmatically opening URLs in certain Multi-Account Containers, for opening certain tabs in certain containers from my terminal
Firefox usually has the following web applications opened:
- [Shortwave](https://shortwave.com) as my email client for my Google Mail
@ -549,3 +606,50 @@ occasionally. It lists the following often-used applications:
Code folks or troubleshooting a VS Code user's setup
- [Thunar](https://docs.xfce.org/xfce/thunar/start) for managing files in a GUI
- [PulseAudio Volume Control (or `pavucontrol`)](https://freedesktop.org/software/pulseaudio/pavucontrol/)
## Music
I dabble in music sometimes, depending on my workload or how much I need an
outlet, mostly as a poser metal drummer, so I'll list that stuff here, too!
- Roland TD-11 electric drum kit
- Electric is convenient for adding music and not bothering people in a
quarter-mile radius
- [Here's a video of me playing it](https://www.youtube.com/watch?v=ucpYCSxxvy0)
- Arturia MicroFreak digital synthesizer
- Very new to me; super fun to play with
- Novation LaunchKey 61 MIDI controller
- Haven't taken the time yet to really setup a DAW to work with this
- Some piece of crap first act electric guitar that I keep trying to fix and make work
- I just need to bite the bullet and save up for a nice Ibanez (used of course)
## Electronics
I also tinker a lot with electronics and "maker"-y things! The only name-able
thing is my 3D printer, which is an Ender CR-10S. Micro Center was having an
incredible sale one upon a day. The thing is really awesome. I still want a real
nice one that I can reliably send prints over the network to.
Other unnamed tools I have and use on roughly a weekly basis:
- Soldering iron
- Solder sucker
- Air purifier (solder _and_ 3D printer fumes ain't all that good for you)
And I'm sure there are other unsung heroes I just don't think about. Like butter
knives.
# Finishing The Day
That about does it! I usually head upstairs when work is done, make food in
unidentifiable cookware, tinker and play with my kids for a bit, head out to
whatever evening activity we've got going on if applicable, come home, and bedtime!
Ah, we have a couple tablets for Khan Academy Kids, white noise (and other
sleep-inducing ambience), and podcasts (like Base Camp Adventures!) and a
Google Home Mini or two, mostly for playing loud and obnoxious music or setting
timers.
[uses-history]: https://git.lyte.dev/lytedev/site.lyte.dev/commits/branch/master/content/uses.md
[backups-nix]: https://git.lyte.dev/lytedev/nix/src/commit/fafd242e461620aaa48a669b3623614cc6829700/nixos/beefcake.nix#L528-L573
[beefnix]: https://git.lyte.dev/lytedev/nix/src/branch/main/nixos/beefcake.nix

61
flake.lock Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1733581040,
"narHash": "sha256-Qn3nPMSopRQJgmvHzVqPcE3I03zJyl8cSbgnnltfFDY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "22c3f2cf41a0e70184334a958e6b124fb0ce3e01",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

30
flake.nix Normal file
View file

@ -0,0 +1,30 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
nixpkgs,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
};
in {
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
hugo
gnumake
netlify-cli
nodejs
];
shellHook = ''
npm install
PATH="$(realpath node_modules/.bin):''${PATH}"
'';
};
});
}

View file

@ -12,7 +12,7 @@
<link rel="shortcut icon" href="/icon.png" />
<link defer rel="stylesheet" href="/styles.css" />
<script defer type="module" src="/global.mjs"></script>
<script async defer data-domain="lyte.dev" src="https://a.lyte.dev/js/plausible.js"></script>
<script async defer data-domain="lyte.dev" src="https://a.lyte.dev/js/script.js"></script>
</head>
<body class="system-theme nojs">
<noscript>
@ -54,6 +54,15 @@
<h2 id="footer-links">External Links</h2>
<ul class="horizontal-blocks">
<li>
<a href="//files.lyte.dev">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" />
</svg>
Files
</a>
</li>
<li>
<a href="https://discord.gg/jUCXCYp">
<svg fill="currentColor" width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"/></svg>
@ -93,6 +102,14 @@
Privacy Policy
</a>
</li>
<li>
<a href="/salary">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="24" height="24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Salary Transparency
</a>
</li>
<li>
<a href="https://git.lyte.dev/lytedev/site.lyte.dev">
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">

View file

@ -1,11 +1,13 @@
{{ define "main" }}
{{ .Content }}
{{ range .Pages }}
{{ .Render "li" }}
{{ else }}
<p>Looks like there's nothing here!... yet!</p>
{{ end }}
<ul style="padding: 0;">
{{ range .Pages }}
{{ .Render "li" }}
{{ else }}
<p>Looks like there's nothing here!... yet!</p>
{{ end }}
</ul>
{{ end }}

View file

@ -10,7 +10,7 @@
</p>
{{ end }}
{{ if not (isset .Params "toc") }}
<details style="margin-bottom: 2.5em;">
<details class="left-border" style="margin-bottom: 2.5em;">
<summary>
<h3>Table of Contents</h3>
</summary>

View file

@ -1,8 +1,12 @@
<h3>
<a style="flex-shrink: 1; margin-right: auto;" href="{{ .Permalink }}">
{{ .Title }}
</a>
<br />
<small>Posted on {{ dateFormat "Jan 2 2006" (cond .Date.IsZero .Lastmod .Date) }}</small>
</h3>
{{ .Summary }}
<div class="left-border">
<h3>
<a style="flex-shrink: 1; margin-right: auto;" href="{{ .Permalink }}">{{ .Title }}</a>
{{ if .Draft }}<span class="badge">DRAFT</span>{{end}}
<br />
<small>Posted on {{ dateFormat "Jan 2 2006" (cond .Date.IsZero .Lastmod .Date) }}</small>
</h3>
<p>
{{ .Summary }}
</p>
</div>

View file

@ -16,18 +16,4 @@
{{ end }}
</details>
<details open>
<summary>
<h2 id="latest-tips">
Latest <a href="/tips">Tips</a> (<a target="_blank" href="/tips/index.xml">RSS</a>)
</h2>
</summary>
{{ range (where .Site.RegularPages "Section" "tips") }}
{{ .Render "li" }}
{{ else }}
<p>Looks like there's nothing here!... yet!</p>
{{ end }}
</details>
{{ end }}

View file

@ -1,6 +1,6 @@
<svg class="hide-in-align-center" width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12" />
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12H12m-8.25 5.25h16.5" />
</svg>
<svg class="hide-in-align-left" width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M7.75 12 h8.5 m-12.25 5.25 h16.5" />
</svg>

Before

Width:  |  Height:  |  Size: 556 B

After

Width:  |  Height:  |  Size: 559 B

View file

@ -5,5 +5,5 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
</svg>
<svg class="only-in-system-theme" height="24" width="24" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
<path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -7,7 +7,7 @@ LOCAL_IP ?= $(shell ip a | grep -oP '192\.168\.\d+\.\d+/' | head -n 1 | tr -d '/
all: build
.PHONY: build
build: static/font.css static/styles.css ; @${HUGO}
build: static/styles.css ; @${HUGO}
.PHONY: public
public: build

165
package-lock.json generated Normal file
View file

@ -0,0 +1,165 @@
{
"name": "site.lyte.dev",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "site.lyte.dev",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"stylus": "^0.60.0"
}
},
"node_modules/@adobe/css-tools": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz",
"integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA=="
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
"engines": {
"node": ">= 8"
}
},
"node_modules/stylus": {
"version": "0.60.0",
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.60.0.tgz",
"integrity": "sha512-j2pBgEwzCu05yCuY4cmyp0FtPQQFBBAGB7TY7QaNl7eztiHwkxzwvIp5vjZJND/a1JNOka+ZW9ewVPFZpI3pcA==",
"dependencies": {
"@adobe/css-tools": "~4.2.0",
"debug": "^4.3.2",
"glob": "^7.1.6",
"sax": "~1.2.4",
"source-map": "^0.7.3"
},
"bin": {
"stylus": "bin/stylus"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://opencollective.com/stylus"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}
}
}

8
package.json Normal file
View file

@ -0,0 +1,8 @@
{
"name": "site.lyte.dev",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"stylus": "^0.60.0"
}
}

View file

@ -1,4 +1,11 @@
# [lyte.dev][/]
<div align="center">
<h1>
<img width="100" src="static/img/logo.svg" /> <br>
<a href="https://lyte.dev">lyte.dev</a>
</h1>
</div>
## Dependencies

View file

@ -14,6 +14,7 @@ a
color var(--link-fg)
&:visited { color: var(--link-visited-fg) }
footer a > svg,
main a > svg
margin-right: 0.5em
@ -34,29 +35,39 @@ button.copy-code-button
pre.chroma:hover > button.copy-code-button
opacity 0.9
img, embed, frame, iframe { max-width: 100vw }
img, embed, frame, iframe { max-width: 100% }
details
position relative
details > summary
cursor pointer
list-style none
text-decoration underline
color var(--link-fg)
details > summary::-webkit-details-marker
display none
details > summary::after
position absolute
padding 0 1em
top 0
right 0
align-self center
right -1em
// background-image url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' style='width: 1em; height: 1em;'><path stroke-linecap='round' stroke-linejoin='round' d='M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z' /></svg>")
content "+"
details[open] > summary::after
content "-"
details
.left-border
position relative
padding-left 0.5em
border-left solid var(--syntax-bpx) var(--syntax-ledg)
transition 0.2s border-color
&:hover
border-color var(--syntax-ledgh)
html,body
min-height 100vh
@ -75,15 +86,21 @@ html,body
padding 0.5em
line-height 1.6em
main:first-child, footer:first-child { margin-top: 0 }
main:first-child, footer:first-child
margin-top 0
&> main, > footer
> main, > footer
.highlight
position relative
background-color var(--syntax-bg)
max-width 100vw
border-left solid var(--syntax-bpx) var(--syntax-ledg)
display flex
pre.chroma
position relative
background-color var(--syntax-bg)
max-width 100vw
border-left solid var(--syntax-bpx) var(--syntax-ledg)
display flex
transition 0.2s border-color
&:hover
border-color var(--syntax-ledgh)
li
margin-top 0.5em
@ -133,8 +150,8 @@ html,body
right 0
top 0
> p code, > h1 code, > h2 code, > h3 code, > h4 code, > h5 code, > h6 code, blockquote code
padding 0.1em 0.25em
> p code, > ol code, > ul code, > h1 code, > h2 code, > h3 code, > h4 code, > h5 code, > h6 code, blockquote code
padding 0.25em
border-radius 0.25em
background-color var(--inline-code-bg)
@ -193,13 +210,20 @@ button, input[type=submit]
.lead { font-size: 1.5rem }
.rounded { border-radius: 0.5em }
table
border-collapse collapse
table tr > td.leading
padding-right 1em
table tr > th
table tr > td
vertical-align top
padding-top 0.25em
padding-bottom 0.25em
vertical-align center
padding 0.25em 0.5em
border solid 1px var(--input-focus-bg)
table tr > th
text-align center
form
&> label, > fieldset
@ -242,12 +266,22 @@ form
&:focus
transform translateX(0%)
.table-container
position relative
max-width 100vw
overflow-x auto
blockquote > p
padding 1em
background-color var(--Mantle)
border-left solid var(--syntax-bpx) var(--Surface1)
@media (min-width 600px)
body.align-left
.hide-in-align-left { display: none }
.with-align { text-align: left }
body.align-center
body:not(.align-left)
.hide-in-align-center { display: none }
.with-align { text-align: center }
&> main, > footer
@ -255,31 +289,37 @@ form
line-height 1.5em
text-align left
h1, h2, h3, h4, h5, h6, form, ul, ol, p, table, .content, details
table
position relative
margin-left auto
margin-right auto
h1, h2, h3, h4, h5, h6, form, ul, ol, p, .content, details
position relative
max-width 600px
margin-left auto
margin-right auto
.highlight
border-left 0
pre.chroma
padding 0
display flex
flex-direction column
width auto
flex 1
justify-content center
align-content center
align-items center
// justify-content center
// align-content center
// align-items center
margin-left auto
margin-right auto
> code
width: 600px;
padding 0.25em 0.5em
border-left solid var(--syntax-bpx) var(--syntax-ledg)
width 600px
padding 0.25em 0.5em
border-left solid var(--syntax-bpx) var(--syntax-ledg)
transition border-color 0.2s
&:hover
border-color var(--syntax-ledgh)
img,
embed,

View file

@ -10,7 +10,7 @@ main .highlight pre.chroma
color var(--fg)
main > .highlight > pre.chroma > code > .line
margin-right 5em
// margin-right 5em
.chroma
.err

View file

@ -19,9 +19,38 @@ mocha = {
/* TODO: everything should be a catppuccin color or based on one */
light-theme = {
--Rosewater: #dc8a78,
--Flamingo: #dd7878,
--Pink: #ea76cb,
--Mauve: #8839ef,
--Red: #d20f39,
--Maroon: #e64553,
--Peach: #fe640b,
--Yellow: #df8e1d,
--Green: #40a02b,
--Teal: #179299,
--Sky: #04a5e5,
--Sapphire: #209fb5,
--Blue: #1e66f5,
--Lavender: #7287fd,
--Text: #4c4f69,
--Subtext1: #5c5f77,
--Subtext0: #6c6f85,
--Overlay2: #7c7f93,
--Overlay1: #8c8fa1,
--Overlay0: #9ca0b0,
--Surface2: #acb0be,
--Surface1: #bcc0cc,
--Surface0: #ccd0da,
--Base: #eff1f5,
--Mantle: #e6e9ef,
--Crust: #dce0e8,
--syntax-brd: rgba(255, 255, 255, 0.2),
--syntax-bg: #e6e9ef,
--syntax-ledg: rgba(0, 0, 0, 0.1),
--syntax-ledg: rgba(0, 0, 0, 0.3),
--syntax-ledgbq: rgba(0, 0, 0, 0.3),
--syntax-sh: #6c6f85,
--syntax-name: latte['green'],
--syntax-mag: latte['red'],
@ -37,7 +66,8 @@ light-theme = {
--header-bg: rgba(255, 255, 255, 0.1),
--header-hover-bg: rgba(255, 255, 255, 0.2),
--fg: #4c4f69,
--inline-code-bg: rgba(0,0,0,0.08),
--inline-code-bg: rgba(255,255,255,0.2),
--inline-code-fg: #000000,
--input-bg: #dce0e8,
--input-hover-bg: rgba(0, 0, 0, 0.16),
--input-focus-bg: var(--input-bg),
@ -47,9 +77,38 @@ light-theme = {
}
dark-theme = {
--Rosewater: #f5e0dc,
--Flamingo: #f2cdcd,
--Pink: #f5c2e7,
--Mauve: #cba6f7,
--Red: #f38ba8,
--Maroon: #eba0ac,
--Peach: #fab387,
--Yellow: #f9e2af,
--Green: #a6e3a1,
--Teal: #94e2d5,
--Sky: #89dceb,
--Sapphire: #74c7ec,
--Blue: #89b4fa,
--Lavender: #b4befe,
--Text: #cdd6f4,
--Subtext1: #bac2de,
--Subtext0: #a6adc8,
--Overlay2: #9399b2,
--Overlay1: #7f849c,
--Overlay0: #6c7086,
--Surface2: #585b70,
--Surface1: #45475a,
--Surface0: #313244,
--Base: #1e1e2e,
--Mantle: #181825,
--Crust: #11111b,
--syntax-brd: rgba(255, 255, 255, 0.2),
--syntax-bg: #181825,
--syntax-ledg: #313244,
--syntax-ledg: alpha(mocha['sapphire'], 0.5),
--syntax-ledgh: mocha['sapphire'],
--syntax-ledgbq: mocha['surface1'],
--syntax-sh: #6c7086,
--icon-shadow: #000,
--syntax-name: mocha['green'],
@ -65,7 +124,8 @@ dark-theme = {
--heading-fg: #cdd6f4,
--link-fg: mocha['sapphire'],
--link-visited-fg: alpha(mocha['mauve'], 1.0),
--inline-code-bg: rgba(255,255,255,0.05),
--inline-code-bg: rgba(0,0,0,0.2),
--inline-code-fg: #ffffff,
--input-bg: #181825,
--input-hover-bg: #313244,
--input-focus-bg: #45475a,

View file

@ -0,0 +1 @@
did:plc:syp5oev6zvxsrlnoxbsucz3t

View file

@ -14,8 +14,8 @@ document.querySelectorAll(".theme-toggler").forEach((a) =>
const initAlign = () => {
const cur = localStorage.getItem("align");
const prev = cur == "center" ? "left" : "center";
document.body.classList.add("align-" + cur);
document.body.classList.remove("align-" + prev);
document.body.classList.add("align-" + cur);
};
if (localStorage.getItem("align") === null) {
@ -82,7 +82,7 @@ function initCodeCopyButtons() {
}
window.addEventListener("load", initAlign);
window.addEventListener("load", initCodeCopyButtons);
// window.addEventListener("load", initCodeCopyButtons);
window.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".nojs").forEach(e => {
e.classList.remove("nojs")

88
static/img/logo.svg Normal file
View file

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
viewBox="0 0 70 60"
width="48"
height="48"
alt="lyte.dev LED icon"
id="svg3"
sodipodi:docname="logo.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs3" />
<sodipodi:namedview
id="namedview3"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="16.25"
inkscape:cx="23.969231"
inkscape:cy="24"
inkscape:window-width="1790"
inkscape:window-height="1178"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg3" />
<linearGradient
id="logo-gradient"
x1="0"
x2="0"
y1="0"
y2="1">
<stop
offset="50%"
style="stop-color:#df3c59"
id="stop1" />
<stop
offset="100%"
style="stop-color:#8e293b"
id="stop2" />
</linearGradient>
<filter
id="logo-shadow"
x="-0.04137931"
y="-0.050420168"
width="1.0827586"
height="1.1323529">
<feGaussianBlur
stdDeviation="1"
id="feGaussianBlur2" />
<feOffset
dx="0"
dy="1.5"
result="offsetblur"
id="feOffset2" />
<feMerge
id="feMerge3">
<feMergeNode
id="feMergeNode2" />
<feMergeNode
in="SourceGraphic"
id="feMergeNode3" />
</feMerge>
</filter>
<path
id="logo-path"
stroke-width="3.5"
fill-opacity="0"
stroke-linecap="round"
stroke-linejoin="round"
d="M 19.5 44.6 h 30 v -12.3 a 15.0 14.057142857142859 0 0 0 -30 0 v 12.3 M 19.5 44.6 m 8 0 v 7 m 14 -7 v 7 M 19.5 44.6 m -9 -13.3 h -5 M 19.5 44.6 m -2.6999999999999997 -28.3 l -3.55 -3.55 M 19.5 44.6 m 15.0 -35.6 v -5 M 19.5 44.6 m 32.7 -28.3 l 3.55 -3.55 M 19.5 44.6 m 39 -13.3 h 5 M 19.5 44.6 m 8.0 -12.3 a 7.5 6.15 0 0 1 7.0 -6.5" />
<use
href="#logo-path"
style="filter: url(#logo-shadow);"
id="icon-shadow" />
<use
href="#logo-path"
stroke="url(#logo-gradient)"
id="use3" />
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View file

@ -54,6 +54,13 @@ export class Theme {
document.body.classList.remove(`dark-theme`)
document.body.classList.remove(`system-theme`)
document.body.classList.add(`${this.#theme}-theme`)
const toggler = document.getElementById("theme-toggler")
toggler.setAttribute("title", this.toggleTitle)
toggler.setAttribute("aria-label", "toggle theme")
}
get toggleTitle() {
return `toggle theme (current theme: ${this.theme})`
}
get theme() {