<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link href="https://www.romanzolotarev.com/rss.xml" rel="self" type="application/rss+xml" />
<title>Mastering the Web - Roman Zolotarev</title>
<description>Subscribe via RSS</description>
<link>https://www.romanzolotarev.com/</link>
<lastBuildDate>Mon, 11 Feb 2019 00:00:00 +0000</lastBuildDate>

<item>
<guid>https://www.romanzolotarev.com/openbsd/oams.html</guid>
<link>https://www.romanzolotarev.com/openbsd/oams.html</link>
<pubDate>Sun, 01 Jul 2018 00:00:00 +0000</pubDate>
<title>Deploy VM on OpenBSD Amsterdam</title>
<description><![CDATA[

<p><strong>DISCLAIMER</strong><br>
I&#39;m just a happy customer of <em>OpenBSD Amsterdam</em>.
<a href="https://twitter.com/mischapeters">Mischa Peters</a> runs the place.
He donates &euro;10 for every VM per year to
<a href="https://www.openbsdfoundation.org">OpenBSD Foundation</a>.</p>

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and 6.4</em></p>

<h1 id="Deploy%20VM%20on%20OpenBSD%20Amsterdam">Deploy VM on OpenBSD Amsterdam</h1>

<p><a href="https://openbsd.amsterdam?rz">OpenBSD in Amsterdam</a> is running dedicated
<a href="https://man.openbsd.org/vmd.8">vmd(8)</a> servers to host opinionated
VMs.</p>

<p>Send your name, email address, hostname, username, and <a href="https://www.romanzolotarev.com/ssh.html">public SSH
key</a> to OpenBSDAms via <a href="https://openbsd.amsterdam/contact.html?rz">contact
form</a>,
<a href="https://twitter.com/OpenBSDAms">Twitter</a>, or
<a href="https://bsd.network/@OpenBSDAms">Mastodon</a>, before you pay.</p>

<p>For example:</p>

<pre><code>Roman Zolotarev
hi@romanzolotarev.com
www.romanzolotarev.com
romanzolotarev
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIqh7BmO...
</code></pre>

<p>Please allow few hours for your VM to be started. You&#39;ll get a IPv4
and IPv6 address as soon as your VM is deployed. Login to the
VM (assuming your private SSH key is in its default location):</p>

<pre>
$ <b>ssh <em>username@XXX.XXX.XXX.XXX</em></b>
OpenBSD 6.4-current (GENERIC) #358: Sat Oct 20 01:44:18 MDT 2018

Welcome to OpenBSD: The proactively secure Unix-like operating system.

Please use the sendbug(1) utility to report bugs in the system.
Before reporting a bug, please try to reproduce it with the latest
version of the code.  With bug reports, please try to ensure that
enough information to reproduce the problem is enclosed, and if a
known fix for it exists, include that as well.

$
</pre>

<p>Get the password from <code>~/.ssh/authorized_keys</code> and switch to <code>root</code>.</p>

<pre>
$ <b>awk '{print$NF}' .ssh/authorized_keys</b>
<em>XXXXXXXXXXXXXXXXXXXXXXXXXX</em>
$ <b>su -</b>
password:
#
</pre>

<p>Add your <em>username</em> to <code>/etc/doas.conf</code>:</p>

<pre>
# <b>echo 'permit <em>username</em>' > /etc/doas.conf</b>
#
</pre>

<p>Edit <code>/etc/ssh/sshd_config</code>:</p>

<pre>
PermitRootLogin no
PasswordAuthentication no
</pre>

<p>Verify the new config and restart <code>sshd</code>:</p>

<pre>
# <b>sshd -t</b>
# <b>rcctl restart sshd</b>
sshd(ok)
sshd(ok)
#
</pre>

<p>It has been reported by some users that IPv6 needs <code>-soii</code> in order
to work properly.  In that case you can edit <code>/etc/hostname.vio0</code>:</p>

<pre><code>dhcp
inet6 2a03:6000:9xxx::xxx 64 -soii
</code></pre>

<p>When you don&#39;t want the IPv4 address to be provided by dhcpd you
can change <code>/etc/hostname.vio0</code> to:</p>

<pre><code>inet 46.23.xx.xx 255.255.255.0
inet6 2a03:6000:9xxx::xxx 64 -soii
</code></pre>

<p>When you do, make sure to edit <code>/etc/mygate</code>:</p>

<pre><code>46.23.xx.1
2a03:6000:9xxx::1
</code></pre>

<p>Reinitialize the network:</p>

<pre>
# <b>sh /etc/netstart vio0</b>
#
</pre>

<p>Add to your crontab:</p>

<pre>
# <b>crontab -e</b>
</pre>

<p>These are workarounds for <a
href="https://openbsd.amsterdam/known.html">known issues</a>.
Replace <code>46.23.88.1</code> with your default gateway from <code>/etc/mygate</code>.</p>

<pre>
*/15 * * * * /usr/sbin/rdate pool.ntp.org > /dev/null
*/5 * * * * /sbin/ping -c3 <em>46.23.88.1</em> > /dev/null
</pre>

<p>Update <code>/etc/pf.conf</code>, test, and load it:</p>

<pre>
# <b>echo 'pass in quick proto { icmp, icmp6 } all' >> /etc/pf.conf</b>
# <b>pfctl -nf /etc/pf.conf</b>
# <b>pfctl -f /etc/pf.conf</b>
# <b>pfctl -sr</b>
block return all
pass all flags S/SA
block return in on ! lo0 proto tcp from any to any port 6000:6010
block return out log proto tcp all user = 55
block return out log proto udp all user = 55
pass in quick proto icmp all
pass in quick proto ipv6-icmp all
#
</pre>

<p>Check <a href="https://www.openbsd.org/errata64.html">6.4 errata</a> and apply
available patches.</p>

<pre>
# <b>syspatch</b>
...
Relinking to create unique kernel... done.
# <b>reboot</b>
Connection to XXX.XXX.XXX.XXX closed.
</pre>

<p>Now you may want <a href="https://www.romanzolotarev.com/openbsd/httpd.html">to setup a web server</a>.</p>

<hr/>

<p><strong>Thanks</strong> to
<a href="https://twitter.com/mischapeters">Mischa Peters</a> for reading drafts of this,
to <a href="https://twitter.com/mlarkin2012">Mike Larkin</a>,
<a href="https://twitter.com/canadianbryan">Bryan Steele</a>,
<a href="https://twitter.com/h3artbl33d">h3artbl33d</a>, and
<a href="https://twitter.com/v6shell">Jeff Neitzel</a> for tips and hints.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/vultr.html</guid>
<link>https://www.romanzolotarev.com/openbsd/vultr.html</link>
<pubDate>Wed, 11 Apr 2018 00:00:00 +0000</pubDate>
<title>Install OpenBSD on Vultr</title>
<description><![CDATA[

<p><strong>DISCLAIMER</strong><br> I&#39;m a customer of Vultr and the blue button is
a referral link.  When you <a href="https://www.romanzolotarev.com/vultr.html">sign
up</a>, Vultr adds few
weeks of hosting for this site. Thank you.</p>

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and 6.4</em></p>

<h1 id="Install%20OpenBSD%20on%20Vultr">Install OpenBSD on Vultr</h1>

<p>If you need a new OpenBSD server, make sure you have your <a href="https://www.romanzolotarev.com/ssh.html">public
SSH key</a> handy, then...</p>

<p><a class="b link ph2 pv1 mb2 dib ba bg-vltr white b--black
hover-white hover-bg-black" href="https://www.romanzolotarev.com/vultr.html">register at Vultr</a></p>

<p>Deploy an instance, for example:</p>

<ol>
<li>Server Location: <em>pick your prefered location</em></li>
<li>Server Type: 64 bit OS, <strong>OpenBSD 6.3 x64</strong></li>
<li>Server Size:

<ul>
<li><em>For IPv6 only</em>: <strong>$2.50/mo</strong> 20 GB SSD 512MB Memory</li>
<li><em>IPv4 and IPv6</em>: <strong>$3.50/mo</strong> 20 GB SSD 512MB Memory</li>
</ul></li>
<li>Additional Features: <strong>None</strong></li>
<li>Start Script: <strong>None</strong></li>
<li>SSH Keys: <strong>Add new key</strong></li>
<li>Firewall Group: <strong>No firewall</strong></li>
<li>Server Hostname &amp; Label: <strong>www</strong></li>
</ol>

<p>In a minute your sever will be deployed. Login using the new IP address
and your private SSH key (assuming it&#39;s in the default location:
<code>~/.ssh/id_ed25519</code>):</p>

<pre><code>$ ssh root@XXX.XXX.XXX.XXX
OpenBSD 6.3 (GENERIC) #349: Thu Oct 11 13:25:13 MDT 2018

Welcome to OpenBSD: The proactively secure Unix-like operating system.

Please use the sendbug(1) utility to report bugs in the system.
Before reporting a bug, please try to reproduce it with the latest
version of the code.  With bug reports, please try to ensure that
enough information to reproduce the problem is enclosed, and if a
known fix for it exists, include that as well.

www#
</code></pre>

<p>Read <a href="https://man.openbsd.org/afterboot.8">afterboot(8)</a>.</p>

<p><a href="https://www.romanzolotarev.com/openbsd/webserver.html">Setup a web server</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/install.html</guid>
<link>https://www.romanzolotarev.com/openbsd/install.html</link>
<pubDate>Wed, 20 Sep 2017 00:00:00 +0000</pubDate>
<title>Install OpenBSD on a desktop</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and 6.4</em></p>

<h1 id="Install%20OpenBSD%20on%20a%20desktop">Install OpenBSD on a desktop</h1>

<p>Prepare an USB drive with OpenBSD installer.<br>
For example, <a href="https://www.romanzolotarev.com/macos/openbsd-installer.html">on macOS</a>.</p>

<p>Backup everything.</p>

<p>Check BIOS: <em>Secure Boot</em> disabled, <em>UEFI Boot</em> enabled.<br>
For example, <a href="https://www.romanzolotarev.com/openbsd/lenovo-thinkpad-x1c5.html">on ThinkPad X1C5</a>.</p>

<p>Boot the installer.</p>

<p>Select <strong>(S)hell</strong> to create encrypt the drive.</p>

<pre>
# <b>sysctl hw.disknames</b>
hw.disknames=sd0:xxxxxxxxxxx,rd0:xxxxxxxxxxx,sd1:xxxxxxxxxxx
</pre>

<p>In this case <em>sd0</em> is the target drive.<br>
<em>rd0</em> is ramdisk for installer kernel.<br>
<em>sd1</em> is USB drive with OpenBSD installer.</p>

<p><strong>Erase all data on <em>sd0</em>.</strong> Create GUID Partition Table (GPT) and
a partition layout.</p>

<pre>
# <b>dd if=/dev/urandom of=/dev/rsd0c bs=1m</b>
# <b>fdisk -iy -g -b 960 sd0</b>
# <b>disklabel -E sd0</b>
Label editor (enter '?' for help at any prompt)
<i><b>a a</b></i>
offset: [1024]
size: [500117105]
FS type: [4.2BSD] <b>RAID</b>
<i><b>w</b></i>
<i><b>q</b></i>
No label changes.
#
</pre>

<p>Generate a strong passphrase.
Use <a href="https://www.romanzolotarev.com/diceware.html">diceware</a>, for example.</p>

<pre>
# <b>bioctl -c C -l sd0a softraid0</b>
New passphrase:
Re-type passphrase:
<strong>sd2 at scsibus2 targ 1 lun 0: &lt;OPENBSD, SR CRYPTO, 006&gt; SCSI2 0/direct fixed
sd2: 244190MB, 512 bytes/sector, 500102858 sectors</strong>
softraid0: CRYPTO volume attached as sd2
# <b>cd /dev && sh MAKEDEV sd2</b>
# <b>dd if=/dev/zero of=/dev/rsd2c bs=1m count=1</b>
1+0 records in
1+0 records out
1048576 bytes transferred in 0.003 secs (265557618 bytes/sec)
# <b>exit</b>
</pre>

<p>Select <code>(I)nstall</code> and answer questions.</p>

<pre>
System hostname? = <b>foo</b>
Which network interface do you wish to configure? = <b>em0</b>
DNS domain name? = <b>romanzolotarev.com</b>
Password for root account? = <b>**************************</b>
Do you want the X Window System to be started by xenodm(1)? = <b>yes</b>
Setup a user? = <b>romanzolotarev</b>
Full name for user romanzolotarev? = <b>Roman Zolotarev</b>
Password for user romanzolotarev? = <b>*******************</b>
What timezone are you in? = <b>UTC</b>
Which disk is the root disk? = <b>sd2</b>
Use (W)hole disk MBR, whole disk (G)PT or (E)dit? = <b>gpt</b>
Location of sets? = <b>disk</b>
Is the disk partition already mounted? = <b>no</b>
Which disk contains the install media? = <b>sd1</b>
Directory does not contain SHA256.sig. Continue without verification? = <b>yes</b>
</pre>

<p>Unplug USB drive with the installer and boot OpenBSD from the target
drive. Login as a regular user and run this command in <a href="https://man.openbsd.org/xterm.1">xterm(1)</a>
to switch to <em>root</em>.</p>

<pre>
$ <b>su -</b>
Password:
#
</pre>

<p>Set install URL and run <a href="https://man.openbsd.org/syspatch.8">syspatch(8)</a>:</p>

<pre>
# <b>echo 'https://fastly.cdn.openbsd.org/pub/OpenBSD'>/etc/installurl</b>
# <b>syspatch</b>
...
Relinking to create unique kernel... done.
#
</pre>

<p>Update <a href="https://man.openbsd.org/fstab.5">fstab(5)</a> to add <em>noatime</em>:</p>

<pre>
# <b>cp /etc/fstab /etc/fstab.bak</b>
# <b>sed -i 's/rw/rw,noatime/' /etc/fstab</b>
#
</pre>

<p>Update <a href="https://man.openbsd.org/login.conf.5">login.conf(5)</a> to
increase memory limits:</p>

<pre>
# <b>cp /etc/login.conf /etc/login.conf.bak</b>
# sed -i 's/datasize-cur=768M/datasize-cur=4096M/' /etc/login.conf</b>
# sed -i 's/datasize-max=768M/datasize-max=4096M/' /etc/login.conf</b>
#
</pre>

<p>Enable <a href="https://man.openbsd.org/apmd.8">apmd(8)</a>:</p>

<pre>
# <b>rcctl enable apmd</b>
# <b>rcctl set apmd flags -A -z 7</b>
# <b>rcctl start apmd</b>
ampd (ok)
#
</pre>

<p>Add your <em>username</em> <code>/etc/doas.conf</code>:</p>

<pre>
# <b>echo 'permit <em>username</em>' > /etc/doas.conf</b>
#
</pre>

<p>Reboot and login as a regular user.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/upgrade.html</guid>
<link>https://www.romanzolotarev.com/openbsd/upgrade.html</link>
<pubDate>Thu, 18 Oct 2018 00:00:00 +0000</pubDate>
<title>Upgrade OpenBSD on bare metal</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and 6.4</em></p>

<h1 id="Upgrade%20OpenBSD%20on%20bare%20metal">Upgrade OpenBSD on bare metal</h1>

<p>If you have <a href="https://www.romanzolotarev.com/install.html">OpenBSD installed</a> and want to upgrate
it, then backup your data first.</p>

<h2 id="Prepare">Prepare</h2>

<p>Download and verify <code>install64.fs</code>:</p>

<pre>
# <b>ftp -V https://cdn.openbsd.org/pub/OpenBSD/6.4/amd64/install64.fs</b>
install64.fs 100% |*******************************|  360 MB     00:34
# <b>ftp -V https://cdn.openbsd.org/pub/OpenBSD/6.4/amd64/SHA256.sig</b>
SHA256.sig   100% |*******************************|  2141       00:00
# <b>signify -C -p /etc/signify/openbsd-64-base.pub -x SHA256.sig install64.fs</b>
Signature Verified
install64.fs: OK
#
</pre>

<p><a href="https://www.openbsd.org/faq/upgrade64.html">Read the official FAQ</a>.</p>

<p>Update your configuration files, if needed for 6.4.</p>

<h2 id="Create%20bootable%20USB%20drive">Create bootable USB drive</h2>

<p>Plug in a USB drive:</p>

<pre>
# <b>dmesg | grep removable | tail -n1</b>
sd3 at scsibus5 targ 1 lun 0: ... removable serial.12345...
#
</pre>

<p>In this case it appears as <em>sd3</em>.
Copy the installer image to the USB flash drive
(<strong>be extremely cautious</strong>):</p>

<pre>
# <b>dd if=install64.fs of=/dev/rsd3c bs=1m</b>
...

</pre>

<h2 id="Upgrade">Upgrade</h2>

<p>Boot from that USB drive, then choose the <code>(S)hell</code> option to mount
the encrypted drive.</p>

<pre>
# <b>bioctl -c C -l /dev/sd0a softraid0</b>
passphrase:
<strong>scsibus1 at softraid0: 1 targets
sd3 at scsibus2 targ 0 lun 0: &lt;OPENBSD, SR RAID 1, 005&gt; SCSI2 0/direct fixed</strong>
sd3: 10244MB, 512 bytes/sec, 20980362 sec total
# <b>exit</b>
</pre>

<p>Choose <code>(U)pgrade</code> and answer the questions.</p>

<pre>
Which disk is the root disk = <b>sd3</b>
Location of sets = <b>disk</b>
</pre>

<h2 id="Update%20packages">Update packages</h2>

<p>Login and update the installed packages:</p>

<pre>
# <b>pkg_add -uv</b>
...
#
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/random.html</guid>
<link>https://www.romanzolotarev.com/random.html</link>
<pubDate>Thu, 27 Sep 2018 00:00:00 +0000</pubDate>
<title>Generate random string with random(4)</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3</em></p>

<h1 id="Generate%20random%20string%20with%20random(4)">Generate random string with random(4)</h1>

<p>The <strong>urandom</strong> device produces high quality pseudo-random output
data.</p>

<blockquote>
<p>&quot;Never use <code>/dev/random</code>. On OpenBSD, it does the same as
<code>/dev/urandom</code>, but on many other systems, it misbehaves.  For
example, it may block, directly return entropy instead of using a
stream cipher, or only return data from hardware random
generators.&quot;<br>&mdash;
<a href="https://man.openbsd.org/random.4">random(4)</a></p>
</blockquote>

<h2 id="Limit%20character%20set">Limit character set</h2>

<p>Keep characters you need and exclude everything else
<a href="https://man.openbsd.org/tr.1">tr(1)</a>. For example, keep characters
from <code>1</code> to <code>6</code>.</p>

<pre>
$ <b>tr -cd '1-6' < /dev/urandom</b>
4135234354265156412324163535634456635452512413235
163421554662651365144426161433312335
<b>^C</b>
$
</pre>

<h2 id="Trim">Trim</h2>

<p><a href="https://man.openbsd.org/fold.1">fold(1)</a> into twenty-character
wide lines, then <a href="https://man.openbsd.org/head.1">head(1)</a> the first
line:</p>

<pre>
$ <b>tr -cd '1-6' < /dev/urandom |</b>
<i><b>fold -w 20 |</b></i>
<i><b>head -n 1</b></i>
15521625233645245322
$
</pre>

<p>Another way to take first 20 characters, use
<a href="https://man.openbsd.org/dd.1">dd(1)</a>:</p>

<pre>
$ <b>tr -cd '1-6' < /dev/urandom |</b>
<i><b>echo $(dd count=20 bs=1 status=none)</b></i>
35611246252555226656
$
</pre>

<h2 id="Adjust%20character%20set">Adjust character set</h2>

<p>Use any range of characters. For, example from the first printable
char, <em>space</em>, to <em>tilde</em>.</p>

<pre>
$ <b>tr -cd ' -~' < /dev/urandom |</b>
<i><b>fold -w 20 | head -n 1</b></i>
a(k#$(K ?I?d!^NM^(5x
$
</pre>

<p>Or all <em>alphanumeric</em> characters, <em>comma</em>, and <em>dot</em>.</p>

<pre>
$ <b>tr -cd '[:alnum:],.' < /dev/urandom |</b>
<i><b>fold -w 20 | head -n 1</b></i>
3zgoNRosNuznXUxzENI.
$
</pre>

<h2 id="Or%20just%20use%20jot(1)">Or just use jot(1)</h2>

<p>Run <a href="https://man.openbsd.org/jot.1">jot(1)</a> with the option <code>-r</code> to print random numbers.</p>

<pre>
$ <b>jot -r 3</b>
95
23
58
$
</pre>

<p>Set the range from 32 to 126 (<a href="https://man.openbsd.org/ascii.7">ASCII</a>
codes of <em>space</em> and <em>tilde</em>), print a character represented by
this number (<code>-c</code>), and separate characters with an empty string
(<code>-s &#39;&#39;</code>).</p>

<pre>
$ <b>jot -rcs '' 20 32 126</b>
L(k&C%M{E}7FFT9*H5tt
$
</pre>

<h2 id="Or%20use%20openssl(1)">Or use openssl(1)</h2>

<p><a href="https://man.openbsd.org/openssl.1">openssl(1)</a> with <code>rand</code> command
outputs pseudo-random bytes and with the <code>-base64</code> option it encodes
the output to its printable form.</p>

<pre>
$ <b>openssl rand -base64 20</b>
zM+i3ms6UGh8TkS4azknU+ncMIY=
$
</pre>

<blockquote>
<p>&quot;I&#39;d be wary of using openssl(1)&rarr;<a href="https://en.wikipedia.org/wiki/Base64#Output_padding">Base64</a> unless you know that &quot;=&quot; can only come at the end because it&#39;s used as padding and so it&#39;s not adding anything extra to the password&#39;s entropy.&quot;<br>&mdash;
<a href="https://twitter.com/gumnos/status/1045268053997617153" title="27 Sep 2018">Tim Chase</a>
(@gumnos)</p>
</blockquote>

<h2 id="See%20also">See also</h2>

<p><a href="https://www.romanzolotarev.com/diceware.html">diceware</a>, <a href="https://www.romanzolotarev.com/pass.html">pass</a></p>

<hr/>

<p><strong>Thanks</strong> to
<a href="https://twitter.com/DahlbergCgn/status/1044909647310794752">David Dahlberg</a>,
<a href="https://mobile.twitter.com/gumnos/status/1044907834432016384">Tim Chase</a>,
<a href="https://mobile.twitter.com/bnastic/status/1044891171615625217">Bojan Nastic</a>,
<a href="https://bsd.network/@horia/100791722609427845">horia</a>,
<a href="https://twitter.com/ben_bai/status/1044986145900253185">Ben Bai</a>
for the hints, and to
<a href="http://www.openbsd.org/papers/hackfest2014-arc4random/index.html">Theo de Raadt</a>
for arc4random.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/disk.html</guid>
<link>https://www.romanzolotarev.com/openbsd/disk.html</link>
<pubDate>Wed, 19 Sep 2018 00:00:00 +0000</pubDate>
<title>Find disk name and partition with sysctl(1) and dmesg(1)</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and 6.4 (amd64)</em></p>

<h1 id="Find%20disk%20name%20and%20partition%20with%20sysctl(1)%20and%20dmesg(1)">Find disk name and partition with sysctl(1) and dmesg(1)</h1>

<p>Make sure you are using the device name and partition you need,
before your <del>destroy all your data</del> run any destructive commands like dd(1).</p>

<h2 id="Disk%20names">Disk names</h2>

<p>You can address disk/partitions...</p>

<table>
<thead>
<tr>
<th style="text-align: left"><strong>by disklabel UID</strong></th>
<th style="text-align: left"> </th>
</tr>
</thead>

<tbody>
<tr>
<td style="text-align: left"><code>d2L6wcn2dlggwqel</code></td>
<td style="text-align: left">raw device</td>
</tr>
<tr>
<td style="text-align: left"><code>d2L6wcn2dlggwqel.c</code></td>
<td style="text-align: left">entire disk</td>
</tr>
<tr>
<td style="text-align: left"><code>d2L6wcn2dlggwqel.a</code></td>
<td style="text-align: left">partition <code>a</code></td>
</tr>
<tr>
<td style="text-align: left"> </td>
<td style="text-align: left"> </td>
</tr>
<tr>
<td style="text-align: left"><strong>or by full path</strong></td>
<td style="text-align: left"> </td>
</tr>
<tr>
<td style="text-align: left"><code>/dev/rsd1c</code></td>
<td style="text-align: left">entire disk raw device <code>sd1</code></td>
</tr>
<tr>
<td style="text-align: left"><code>/dev/rsd1a</code></td>
<td style="text-align: left"><code>a</code> partition</td>
</tr>
<tr>
<td style="text-align: left"><code>/dev/sd1c</code></td>
<td style="text-align: left">entire disk same, but as a <em>block device</em></td>
</tr>
<tr>
<td style="text-align: left"><code>/dev/sd1a</code></td>
<td style="text-align: left"><code>a</code> partition</td>
</tr>
</tbody>
</table>

<blockquote>
<p>a <em>raw device</em> (or <em>character device</em>) is accessed directly,
bypassing the operating system&#39;s caches and buffers.</p>
</blockquote>

<p>When to use block devices then?</p>

<blockquote>
<p>&quot;Use block dev only for mounting, use raw for anything else&quot;<br>&mdash;
<a href="https://twitter.com/ottom6k/status/1042437641860460544" title="19 Sep 2018">Otto Moerbeek</a>
(@ottom6k)</p>
</blockquote>

<p>There are programs like
<a href="https://man.openbsd.org/disklabel.8">disklabel(8)</a>, they accept
all kinds of names (<code>/dev/sd0c</code>, or <code>/dev/sd0</code>, or even <code>sd0</code>) and
parse them into full path <code>/dev/rsd0c</code>.</p>

<pre>
# <b>disklabel sd1</b>
# <em>/dev/rsd1c</em>:
...
#
</pre>

<p>Some programs, like <a href="https://man.openbsd.org/fdisk.8">fdisk(8)</a>,
accept some abbreviations, like <code>sd1</code> or <code>sd1c</code>, but refuse to work
with block devices.</p>

<pre>
# <b>fdisk /dev/sd1c</b>
fdisk: <em>/dev/sd1c</em> is not a character device
#
</pre>

<p>Other programs, for example, <a href="https://man.openbsd.org/dd.1">dd(1)</a>,
just do what you told them to do.</p>

<p>Don&#39;t repeat at home:</p>

<pre>
# <b>dd if=/dev/zero of=/dev/sd3 bs=1m</b>

/: write failed, file system is full
dd: /dev/sd3: No space left on device
919+0 records in
918+0 records out
962592768 bytes transferred in 2.466 secs (390316750 bytes/sec)
#
</pre>

<p>This <code>dd</code> creates the file <code>/dev/sd3</code> and fills up your root
partition.</p>

<h2 id="Find%20disks">Find disks</h2>

<pre>
$ <b>sysctl hw.disknames</b>
hw.disknames=sd0:ew9w8ueO9m0t1wda,sd1:66160c68a67e00e6
$
</pre>

<p><code>sd0</code> is the device connected first, its DUID is <code>ew9w8ueO9m0t1wda</code><br>
<code>sd1</code> is the second and DUID is <code>66160c68a67e00e6</code>.</p>

<p>These numbers (<code>sd0</code> and <code>sd1</code>) may change after reboot or
pluging/unpluging USB devices, but DUIDs are persitent until you
rewrite it with disklabel.</p>

<p>If you are looking for a just connected USB drive, then grep(1) word
<code>removable</code> in the output of dmesg(1).</p>

<pre>
$ <b>dmesg | grep removable | tail -1</b>
<em>sd1</em> at scsibus5 ...  SCSI3 0/direct removable ...
$
</pre>

<p><code>sd1</code> is what you are looking for.</p>

<h2 id="Find%20partitions">Find partitions</h2>

<p>Run disklabel(1) with a disk name or DUID.</p>

<pre>
# <b>disklabel sd1</b>
# /dev/rsd1c:
type: SCSI
disk: SCSI disk
label: XXXXXXXXXX
duid: 66160c68a67e00e6
flags:
bytes/sector: 512
sectors/track: 63
tracks/cylinder: 255
sectors/cylinder: 16065
cylinders: 1945
total sectors: 31260672
boundstart: 0
boundend: 31260672
drivedata: 0

16 partitions:
#                size      offset  fstype [fsize bsize   cpg]
<em>  a:</em>         31260672           0  4.2BSD   2048 16384     1
  c:         31260672           0  unused
#
</pre>

<p>If you are looking for OpenBSD partition (<code>4.2BSD</code>) it&#39;s the partition <code>a</code>.</p>

<p>The entire disk is represented as the partition <code>c</code> (marked as
<code>unused</code>, because you can&#39;t create a file system on this partition).</p>

<hr/>

<p><strong>Thanks</strong> to
<a href="https://twitter.com/gumnos">Tim Chase</a>,
<a href="https://twitter.com/freebsdfrau">Devin Teske</a>,
<a href="https://twitter.com/mischapeters">Mischa Peters</a>,
<a href="https://twitter.com/canadianbryan">Bryan Steele</a>
for reading drafts of
this.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/geteltorito.html</guid>
<link>https://www.romanzolotarev.com/openbsd/geteltorito.html</link>
<pubDate>Wed, 12 Sep 2018 00:00:00 +0000</pubDate>
<title>Make bootable image with geteltorito(1) and dd(1)</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3</em></p>

<h1 id="Make%20bootable%20image%20with%20geteltorito(1)%20and%20dd(1)">Make bootable image with geteltorito(1) and dd(1)</h1>

<p>For example, make a bootable USB disk to update BIOS on ThinkPad
X1 Carbon 6th gen with <a href="https://pcsupport.lenovo.com/de/en/products/LAPTOPS-AND-NETBOOKS/THINKPAD-X-SERIES-LAPTOPS/THINKPAD-X1-CARBON-6TH-GEN-TYPE-20KH-20KG/downloads/DS502282">ISO provided by
Lenovo</a>.</p>

<p>Download <em>BIOS Update 1.30 06 Sep 2018</em> and verify the checksum:</p>

<pre>
$ <b>ftp -V https://download.lenovo.com/pccbbs/mobiles/n23ur11w.iso</b>
n23ur11w.iso 100% |**************************| 21868 KB    00:16
$ <b>sha256 n23ur11w.iso</b>
SHA256 (n23ur11w.iso) = e308a5...
</pre>

<p>Install
<a href="http://freshmeat.sourceforge.net/projects/geteltorito">geteltorito(1)</a>
and convert the ISO to El Torito boot image.</p>

<pre>
$ <b>doas pkg_add geteltorito</b>
$ <b>geteltorito -o bios.img n23ur11w.iso</b>
Booting catalog starts at sector: 20
Manufacturer of CD: NERO BURNING ROM VER 12
Image architecture: x86
Boot media type is: harddisk
El Torito image starts at sector 27 and has 43008 sector(s) of 512 Bytes
$ <b>sha256 bios.img</b>
SHA256 (bios.img) = c6a11b...
$
</pre>

<p>Plug in a USB drive:</p>

<pre>
$ <b>dmesg | grep removable | tail -n1</b>
sd3 at scsibus5 targ 1 lun 0: &lt;Vendor, Model, 1.11&gt;
SCSI3 0/direct removable serial.12345678901234567890987654
</pre>

<p>In this case it appears as <em>sd3</em>.</p>

<p>Copy the image (replace <code>/dev/rsdXc</code> with your drive).  For example,
for <code>sd3</code> that would be <code>/dev/r<b>sd3</b>c</code>, where <code>r</code>
means <em>raw</em> and <code>c</code> is a whole device.  <strong>All data on <code>sdX</code> will
be erased!</strong></p>

<pre>
# <b>dd if=bios.img of=/dev/rsdXc bs=1m</b>
21+0 records in
21+0 records out
22020096 bytes transferred in 2.201 secs (10002851 bytes/sec)
#
</pre>

<p>Check the content of the drive:</p>

<pre>
# <b>disklabel sdX</b>
# /dev/rsd3c:
type: SCSI
disk: SCSI disk
label: XXXXXXXXXXXXXXX
duid: 0000000000000000
flags:
bytes/sector: 512
sectors/track: 63
tracks/cylinder: 255
sectors/cylinder: 16065
cylinders: 1945
total sectors: 31260672
boundstart: 0
boundend: 31260672
drivedata: 0

16 partitions:
#                size           offset  fstype [fsize bsize   cpg]
  c:         31260672                0  unused
  i:            42976               32   MSDOS
# <b>mkdir ./sdXi</b>
# <b>mount /dev/sdXi ./sdXi</b>
# <b>cd /mnt/sdXi</b>
# <b>find .</b>
.
./System Volume Information
./System Volume Information/WPSettings.dat
./System Volume Information/IndexerVolumeGuid
./EFI
./EFI/Boot
./EFI/Boot/BootX64.efi
./FLASH
./FLASH/406E8.PAT
./FLASH/806E9.PAT
./FLASH/806EA.PAT
./FLASH/BCP.EVS
./FLASH/NoDCCheck_BootX64.efi
./FLASH/README.TXT
./FLASH/SHELLFLASH.EFI
./FLASH/N23ET55W
./FLASH/N23ET55W/$0AN2300.FL1
./FLASH/N23ET55W/$0AN2300.FL2
#
</pre>

<p>Reboot and flash the BIOS.</p>

<hr/>

<p><strong>Thanks</strong> to Mikko Nyman for testing and Peter Hessler for <a href="https://twitter.com/phessler/status/950647948043485185">the
pointer</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/dock.html</guid>
<link>https://www.romanzolotarev.com/openbsd/dock.html</link>
<pubDate>Wed, 12 Sep 2018 00:00:00 +0000</pubDate>
<title>Dock laptop with xrandr(1), xinput(1), xrdb(1), and sysctl(8)</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and <a href="https://www.romanzolotarev.com/openbsd/hardware.html">ThinkPad X1C5</a></em></p>

<h1 id="Dock%20laptop%20with%20xrandr(1),%20xinput(1),%20xrdb(1),%20and%20sysctl(8)">Dock laptop with xrandr(1), xinput(1), xrdb(1), and sysctl(8)</h1>

<p>TL;DR: Check out my <a href="https://www.romanzolotarev.com/bin/dock">dock</a> and <a href="https://www.romanzolotarev.com/bin/undock">undock</a>.</p>

<h2 id="Screens">Screens</h2>

<p>Toggle displays with <a href="https://man.openbsd.org/xrandr.1">xrandr(1)</a>.
For example, only laptop&#39;s display (<code>eDP-1</code>) is on, then an external
one (<code>HDMI-1</code>), then both side by side:</p>

<pre>
$ <b>xrandr --output eDP-1 --auto --output HDMI-1 --off</b>
$ <b>xrandr --output HDMI-1 --auto --output eDP-1 --off</b>
$ <b>xrandr --output HDMI-1 --auto --output eDP-1 --auto --right-of HDMI-1</b>
$
</pre>

<h2 id="Mouse%20and%20touchpad">Mouse and touchpad</h2>

<p>List all connected devices with
<a href="https://man.openbsd.org/xinput.1">xinput(1)</a>:</p>

<pre>
$ <b>xinput</b>
&#9121; Virtual core pointer                          id=2    [master pointer  (3)]
&#9116;   &#8627; Virtual core XTEST pointer                id=4    [slave  pointer  (2)]
&#9116;   &#8627; /dev/wsmouse0                             id=7    [slave  pointer  (2)]
&#9116;   &#8627; /dev/wsmouse                              id=8    [slave  pointer  (2)]
&#9123; Virtual core keyboard                         id=3    [master keyboard (2)]
    &#8627; Virtual core XTEST keyboard               id=5    [slave  keyboard (3)]
    &#8627; /dev/wskbd                                id=6    [slave  keyboard (3)]
</pre>

<p>Then adjust button mapping and pointer acceleration. For example,
reverse touchpad scrolling (<code>id=7</code>) and slow down trackball (<code>id=8</code>).</p>

<pre>
$ <b>xinput set-button-map 7 1 2 3 5 4 7 6</b>
$ <b>xinput set-prop 8 'Device Accel Constant Deceleration' 5</b>
$
</pre>

<p>See also my <a href="https://www.romanzolotarev.com/xsession">.xsession</a>.</p>

<h2 id="Fonts">Fonts</h2>

<p>Toggle DPI for all X programs (including Firefox) and fonts for
<a href="https://man.openbsd.org/xterm.1">xterm(1)</a>.  On high DPI screens,
use large TrueType fonts:</p>

<pre><code>Xft.dpi: 133
XTerm*font:
XTerm*faceName: DejaVu Sans Mono:size=12
</code></pre>

<p>On low DPI screens, use bitmap ones:</p>

<pre><code>Xft.dpi: 92
XTerm*font: -misc-fixed-medium-r-normal--15-140-75-75-c-90-iso10646-1
XTerm*faceName:
</code></pre>

<p>To apply these settings use <a href="https://man.openbsd.org/xrdb.1">xrdb(1)</a>
(don&#39;t forget to restart X programs after that):</p>

<pre>
$ <b>xrdb .Xdefaults</b>
$ <b>echo 'Xft.dpi: 92' | xrdb -merge</b>
$
</pre>

<p>Here is my <a href="https://www.romanzolotarev.com/Xdefaults">.Xdefaults</a>.</p>

<h2 id="Lid">Lid</h2>

<p>Define an action on laptop&#39;s lid closing. Do nothing (<code>0</code>), suspend
(<code>1</code>), or hibernate (<code>2</code>):</p>

<pre>
# <b>sysctl machdep.lidaction=0</b>
machdep.lidaction: 2 -> 0
# <b>sysctl machdep.lidaction=1</b>
machdep.lidaction: 0 -> 1
# <b>sysctl machdep.lidaction=2</b>
machdep.lidaction: 1 -> 2
#
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/lenovo-thinkpad-x1c5.html</guid>
<link>https://www.romanzolotarev.com/openbsd/lenovo-thinkpad-x1c5.html</link>
<pubDate>Mon, 13 Aug 2018 00:00:00 +0000</pubDate>
<title>Prepare ThinkPad X1 Carbon Gen 5 for OpenBSD</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3</em></p>

<h1 id="Prepare%20ThinkPad%20X1&amp;nbsp;Carbon%20Gen&amp;nbsp;5%20for%20OpenBSD">Prepare ThinkPad X1 Carbon Gen 5 for OpenBSD</h1>

<p>Turn ThinkPad on and press <strong>F1</strong> to enter <em>Setup</em>.</p>

<p>Select <em>Security &gt; I/O Ports</em>, set <strong>Bluetooth</strong> and <strong>Fingerprint
Reader</strong> to <strong>Disabled</strong>. These devices are not supported by OpenBSD.
Disable other devices you don&#39;t use.</p>

<p>Select <em>Security &gt; Secure Boot</em>, set <strong>Secure Boot</strong> to <strong>Disabled</strong>.</p>

<p>Select <em>Startup &gt; Boot</em>, set <strong>Boot Priority Order</strong> to:<br> <strong>USB
HDD</strong>, then <strong>NVMe0 ...</strong>.</p>

<p>Select <em>Startup</em>, set <em>UEFI/Legacy Boot</em> to <strong>UEFI Only</strong>.</p>

<p>Press <strong>F10</strong> to save changes and reboot.</p>

<p><a href="https://www.romanzolotarev.com/install.html">Install OpenBSD</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/bioctl-crypto.html</guid>
<link>https://www.romanzolotarev.com/openbsd/bioctl-crypto.html</link>
<pubDate>Sun, 12 Aug 2018 00:00:00 +0000</pubDate>
<title>Encrypt disk with bioctl(8) and CRYPTO</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3</em></p>

<h1 id="Encrypt%20disk%20with%20bioctl(8)%20and%20CRYPTO">Encrypt disk with bioctl(8) and CRYPTO</h1>

<p><a href="https://man.openbsd.org/bioctl.8">bioctl(8)</a> is a RAID management interface with CRYPTO discipline
for disk encryption.</p>

<h2 id="Create%20an%20encrypted%20volume">Create an encrypted volume</h2>

<p>Plug the drive in. Assuming it&#39;s <strong>sd3</strong>.</p>

<p><strong>DANGER!</strong> All data on <strong>sd3</strong> will be erased.</p>

<pre>
# <b>dd if=/dev/urandom of=/dev/rsd3c bs=1m</b>
# <b>fdisk -iy -g -b 960 sd3</b>
# <b>printf 'a a


RAID
w
q
'|disklabel -E sd3</b>
# <b>bioctl -c C -l sd3a softraid0</b>
New passphrase:
Re-type passphrase:
<strong>softraid0: CRYPTO volume attached as sd4</strong>
# <b>dd if=/dev/zero of=/dev/rsd4c bs=1m count=1</b>
# <b>fdisk -iy sd4</b>
# <b>printf 'a i



w
q
'|disklabel -E sd4</b>
# <b>newfs sd4a</b>
# <b>mkdir /mnt/sd4a</b>
# <b>mount /dev/sd4a /mnt/sd4a</b>
# ...
# <b>umount /dev/sd4a</b>
# <b>bioctl -d sd4</b>
#
</pre>

<p>It&#39;s safe to unplug <strong>sd3</strong> drive now.</p>

<h2 id="Mount%20and%20umount">Mount and umount</h2>

<p>Plug the drive in.</p>

<pre>
# <b>bioctl -c C -l sd3a softraid0</b>
Passphrase:
softraid0: CRYPTO volume attached as sd4
# <b>mkdir /mnt/sd4a</b>
# <b>mount /dev/sd4a /mnt/sd4a</b>
...
# <b>umount /dev/sd4a</b>
# <b>bioctl -d sd4</b>
#
</pre>

<p>Check out my helpers
<a href="https://www.romanzolotarev.com/bin/mnt_crypto">mnt_crypto</a> and
<a href="https://www.romanzolotarev.com/bin/umnt_crypto">umnt_crypto</a> and how to use them:</p>

<pre>
# <b>bin/mnt_crypto  'XXXXXXXXXXXXXXXX.x' 'YYYYYYYYYYYYYYYY.y'</b>
# <b>bin/umnt_crypto 'XXXXXXXXXXXXXXXX.x'</b>
</pre>

<p>Where <code>XXXXXXXXXXXXXXXX.x</code> is DUID and partition of a CRYPTO
volume and <code>YYYYYYYYYYYYYYYY.y</code>&mdash;of a physical device.</p>

<p>You can find DUIDs by running this:</p>

<pre>
# <b>disklabel /dev/sd3a | grep -E 'duid|RAID'</b>
duid: XXXXXXXXXXXXXXXX
  a:          7716864                 0    RAID
# <b>disklabel /dev/sd4a | grep -E 'duid|BSD'</b>
duid: YYYYYYYYYYYYYYYY
  i:          7716864                64    4.2BSD   4096 32768 26062
#
</pre>

<h2 id="Check%20file%20system%20consistency">Check file system consistency</h2>

<p>A drive was accidentally disconnected (before you could unmount it properly).
That happens. Run <a href="https://man.openbsd.org/fsck.8">fsck(8)</a>:</p>

<pre>
# <b>bioctl -c C -l sd3a softraid0</b>
softraid0: sd4 was not shutdown properly
Passphrase:
softraid0: sd4 was not shutdown properly
softraid0: CRYPTO volume attached as sd4
# <b>fsck /dev/sd4a</b>
** /dev/rsd4a
** Last Mounted on /mnt/sd4a
** Phase 1 - Check Blocks and Sizes
** Phase 2 - Check Pathnames
** Phase 3 - Check Connectivity
** Phase 4 - Check Reference Counts
** Phase 5 - Check Cyl groups
38996 files, 58177423 used, 62950830 free
(10766 frags, 7867508 blocks, 0.0% fragmentation)

MARK FILE SYSTEM CLEAN? [Fyn?] <b>y</b>

***** FILE SYSTEM WAS MODIFIED *****
#
</pre>

<h2 id="Change%20the%20passphrase">Change the passphrase</h2>

<pre>
# <b>bioctl -P sd4</b>
Old passphrase:
New passphrase:
Re-type passphrase:
#
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/newsboat.html</guid>
<link>https://www.romanzolotarev.com/newsboat.html</link>
<pubDate>Sun, 06 May 2018 00:00:00 +0000</pubDate>
<title>Configure newsboat(1) to read RSS feeds in terminal</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD 6.3</a> with newsboat-2.10.2</em></p>

<h1 id="Configure%20newsboat(1)%20to%20read%20RSS%20feeds%20in&amp;nbsp;terminal">Configure newsboat(1) to read RSS feeds in terminal</h1>

<p>Install <a href="https://www.newsboat.org/">newsboat(1)</a>. For example, on OpenBSD:</p>

<pre>
# <b>pkg_add newsboat</b>
...
newsboat-2.10.2: ok
#
</pre>

<p>Add the first feed to your <code>.newsboat/urls</code>, add <code>.newsboat/config</code>, run newsboat(1):</p>

<pre>
$ <b>mkdir -p "$HOME/.newsboat"</b>
$ <b>echo 'https://www.romanzolotarev.com/rss.xml' \</b>
<i><b>"$HOME/.newsboat/urls"</b></i>
$
$ <b>cat &gt; "$HOME/.newsboat/config" &lt;&lt; EOF</b>
<i>browser         "firefox"</i>
<i>player          "mpv"</i>
<i>download-path   "~/downloads/%n"</i>
<i>save-path       "~/downloads"</i>
<i>reload-threads  20</i>
<i>cleanup-on-quit yes</i>
<i>text-width      74</i>
<i></i>
<i>bind-key - quit</i>
<i>bind-key G end</i>
<i>bind-key g home</i>
<i>bind-key j down</i>
<i>bind-key k up</i>
<i><b>EOF</b></i>
$
$ <b>newsboat</b>
</pre>

<p>From a list of feeds select the first one and get the list of items:</p>

<pre>
newsboat 2.10.2 - Articles in feed 'Roman Zolotarev' (1 unread
   1 N  May 06   2.2K  Configure newsboat(1) to read RSS feeds
   2    May 01   1.4K  Configure minimalist login on OpenBSD
   3    May 01   1.8K  Set default applications on X Window Sy
   4    Apr 24   5.3K  My dear sponsor,
   5    Apr 13   5.6K  Enable HTTPS on OpenBSD with Let's Encr
   6    Apr 12   2.4K  Configure OpenBSD httpd(8) on your web
   7    Apr 11   2.4K  Deploy a VPS on Vultr
   8    Apr 07   9.6K  Static site generator with rsync and lo
   9    Apr 03   1.4K  Upgrade OpenBSD
  10    Mar 30   1.6K  Strong password generator
  11    Mar 16   710   Change time zone in OpenBSD
  12    Mar 02   1.6K  Backup with borg
  13    Mar 01   1.8K  Mount drives on OpenBSD
  14    Feb 27   713   Printing from the command line on macOS
  15    Nov 17  12.6K  OpenBSD on my fanless desktop computer
  16    Nov 15   3.8K  Why OpenBSD?
  17    Nov 02   2.1K  Enable full disk encryption on OpenBSD
  18    Oct 10   7.3K  Password manager powered by LibreSSL
  19    Sep 20   2.3K  Install OpenBSD on your desktop
  20    Sep 19   1.0K  Prepare a bootable OpenBSD drive on mac
  21    Sep 01   4.2K  Configure YubiKey for login and SSH on
-:Quit ENTER:Open s:Save r:Reload n:Next Unread A:Mark All Rea
</pre>

<p>Navigate with  <code>j</code>, <code>k</code>, <code>G</code>, <code>g</code>, <code>-</code>.</p>

<p>Default key bindings:</p>

<p><code>Enter</code> - open the feed or the article<br>
<code>n</code> - jump to the next unread article<br>
<code>p</code> - jump to the previous unread article<br>
<code>o</code> - open an article in your browser<br>
<code>e</code> - enqueue podcast for <code>podboat</code><br>
<code>E</code> - edit the list of your feeds in <code>~/.newsboat/urls</code><br>
<code>r</code> - reload the feed<br>
<code>q</code> - go up or quit</p>

<p>Check out my <a href="https://www.romanzolotarev.com/blogroll.txt">.newsboat/urls</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/xdg-mime.html</guid>
<link>https://www.romanzolotarev.com/xdg-mime.html</link>
<pubDate>Tue, 01 May 2018 00:00:00 +0000</pubDate>
<title>Set default programs with xdg-mime(1)</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD 6.3</a> with mupdf-1.11p2, firefox-59.0.2,
and libreoffice-6.0.2.1v0</em></p>

<h1 id="Set%20default%20programs%20with%20xdg-mime(1)">Set default programs with xdg-mime(1)</h1>

<p>When you click on a downloaded PDF file in Firefox or when you run
<a href="https://man.openbsd.org/xdg-open.1">xdg-open(1)</a>, the file opens
with ths default program for its mimetype.</p>

<p>Check the mimetype with
<a href="https://man.openbsd.org/xgd-mime.html">xdg-mime(1)</a>:</p>

<pre>
$ <b>xdg-mime query filetype example.pdf</b>
application/pdf
$
</pre>

<p>Let&#39;s check the default application for <code>application/pdf</code>?</p>

<pre>
$ <b>xdg-mime query default application/pdf</b>
gimp.desktop
$
</pre>

<p>What? Why would you want to open your PDF files in Gimp? Weird.
Let&#39;s fix this.</p>

<p>First off, install MuPDF, if you didn&#39;t yet.</p>

<pre>
# <b>pkg_add mupdf</b>
...
mupdf-1.11p2:glfw-3.2.1p0: ok
mupdf-1.11p2: ok
#
</pre>

<p>Then create <code>mupdf.desktop</code> in <code>~/.local/share/applications</code> directory
with just two lines.</p>

<pre><code>[Desktop Entry]
Exec=/usr/local/bin/mupdf %u
</code></pre>

<p>Or use the full version:</p>

<pre><code>[Desktop Entry]
Encoding=UTF-8
Version=1.0
Type=Application
NoDisplay=true
Exec=/usr/local/bin/mupdf %u
Name=MuPDF
Comment=A lightweight PDF viewer
</code></pre>

<p>Set the new default application:</p>

<pre>
$ <b>xdg-mime default mupdf.desktop application/pdf</b>
$
</pre>

<p>Let&#39;s verify:</p>

<pre>
$ <b>xdg-mime query default application/pdf</b>
mupdf.desktop
$
</pre>

<p>P.S. Types for Word and Excel documents are not exactly what would
you expect:</p>

<pre>
$ <b>xdg-mime query filetype example.doc</b>
application/octet-stream
$ <b>xdg-mime query filetype example.xls</b>
application/octet-stream
$ <b>xdg-mime query filetype example.docx</b>
application/zip
$ <b>xdg-mime query filetype example.xlsx</b>
application/zip
$
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/timezone.html</guid>
<link>https://www.romanzolotarev.com/openbsd/timezone.html</link>
<pubDate>Fri, 16 Mar 2018 00:00:00 +0000</pubDate>
<title>Set time zone on OpenBSD</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3</em></p>

<h1 id="Set%20time%20zone%20on%20OpenBSD">Set time zone on OpenBSD</h1>

<p>Check your current time with <a href="https://man.openbsd.org/date.1">date(1)</a>:</p>

<pre>
# <b>date</b>
Thu Apr  5 12:26:43 UTC 2018
#
</pre>

<p>The local timezone is <em>UTC</em>.</p>

<p>Check <code>/etc/localtime</code> with <a href="https://man.openbsd.org/readlink.1">readlink(1)</a>:</p>

<pre>
# <b>readlink /etc/localtime</b>
/usr/share/zoneinfo/UTC
#
</pre>

<p>It&#39;s a symbolic link to <em>UTC</em> time zone file.</p>

<p>Find a file for the time zone you want to set with <a href="https://man.openbsd.org/find.1">find(1)</a>:</p>

<pre>
# <b>find /usr/share/zoneinfo -name 'Mos*'</b>
/usr/share/zoneinfo/Europe/Moscow
...
#
</pre>

<p>Set timezone with <a href="https://man.openbsd.org/zic.8">zic(8)</a>.</p>

<pre>
# <b>zic -l Europe/Moscow</b>
#
</pre>

<p>Check the time zone:</p>

<pre>
# <b>readlink /etc/localtime</b>
/usr/share/zoneinfo/Europe/Moscow
# <b>date</b>
Thu Apr  5 15:26:51 MSK 2018
#
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/diceware.html</guid>
<link>https://www.romanzolotarev.com/diceware.html</link>
<pubDate>Fri, 30 Mar 2018 00:00:00 +0000</pubDate>
<title>Generate passphrases with random(4)</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and <a href="https://www.romanzolotarev.com/macos/">macOS</a> 10.13</em></p>

<h1 id="Generate%20passphrases%20with%20random(4)">Generate passphrases with random(4)</h1>

<p><a href="https://www.romanzolotarev.com/bin/diceware">diceware</a> is a random passphrase generator. It uses
<a href="https://man.openbsd.org/random.4">random(4)</a> as a source of entropy. Make sure your operating
system provides good enough randomness.</p>

<h2 id="Install">Install</h2>

<pre>
$ <b>cd bin</b>
$ <b>ftp -V https://www.romanzolotarev.com/bin/diceware</b>
diceware     100% |********************| 18711       00:00
$ <b>chmod +x diceware</b>
</pre>

<h2 id="Roll%20dice">Roll dice</h2>

<p>On every run it generates a random passphrase, 8-word long by
default.</p>

<pre>
$ <b>diceware</b>
guerrilla agnostic backdoor glove jealous mummy myth sloth
$ <b>diceware 20</b>
khaki hemoglobin artichoke cyclist coverless dictionary
vegetable sardine datebook ruined purse cytoplasm
absorbing narrator snapshot smitten cuticle journal
fiscally neither
$
</pre>

<p>Pipe it to <a href="https://www.romanzolotarev.com/pass.html">your favorite password manager</a>:</p>

<pre>
$ <b>diceware | pass import twitter</b>
Enter pass phrase for /home/romanzolotarev/.pass/.key:
$
</pre>

<h2 id="See%20also">See also</h2>

<p><a href="https://www.romanzolotarev.com/random.html">random</a>, <a href="https://www.romanzolotarev.com/pass.html">pass</a></p>

<hr/>

<p><strong>Thanks</strong> to
<a href="https://m.xkcd.com/936/">Randall Munroe</a> and
<a href="https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases">Joseph Bonneau</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/pass.html</guid>
<link>https://www.romanzolotarev.com/pass.html</link>
<pubDate>Tue, 10 Oct 2017 00:00:00 +0000</pubDate>
<title>Manage passwords with openssl(1) and oathtool(1)</title>
<description><![CDATA[

<p><strong>WARNING</strong><br>
This shell script uses parts of OpenSSL/LibreSSL, which are intended for
testing purposes only. <strong>You may loose your passwords.</strong> Use it at your
own risk.</p>

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and 6.4</em></p>

<h1 id="Manage%20passwords%20with%20openssl(1)%20and%20oathtool(1)">Manage passwords with openssl(1) and oathtool(1)</h1>

<p><a href="https://www.romanzolotarev.com/bin/pass">pass</a> is a password manager written in shell and powered
by <a href="https://man.openbsd.org/openssl.1">openssl(1)</a> and
<a href="http://www.nongnu.org/oath-toolkit/oathtool.1.html">oathtool(1)</a>.</p>

<h2 id="Install">Install</h2>

<pre>
# <b>pkg_add oath-toolkit</b>
...
oath-toolkit-2.6.2p0: ok
#
</pre>

<p>Download and run <code>pass</code>. Assuming <code>./bin</code> is in <code>PATH</code>.</p>

<pre>
$ <b>cd bin</b>
$ <b>ftp https://www.romanzolotarev.com/bin/pass</b>
$ <b>chmod +x pass</b>
$ <b>pass</b>
usage: export BASE_DIR=~/.pass
       export PRIVATE_KEY=~/.pass/.key
       export PUBLIC_KEY=~/.pass/.key.pub

       pass init
          | passphrase
          | add        id
          | import     id &lt;pass&gt;
          | show       id
          | export     id &lt;pass&gt;
          | ls        &lt;id&gt;
</pre>

<h2 id="Initialization">Initialization</h2>

<p>Create a directory for your passwords and generate your key pair.
Generate a strong pass phrase. For example, with <a href="https://www.romanzolotarev.com/diceware.html">diceware</a>.</p>

<pre>
$ <b>pass init</b>
Generating public/private key pair.
New pass phrase:
Confirm:
Generating RSA private key, 2048 bit long modulus
..........................+++
..................+++
e is 65537 (0x10001)
writing RSA key
$ <b>ls -1 ~/.pass</b>
.key
.key.pub
$
</pre>

<p>As result you get two files in <code>~/.pass</code> directory. These files are
your keys and they are protected with your master pass phrase.</p>

<p><strong>Important!</strong> Backup those files, you won&#39;t be able to recover any
of your passwords without the private key. Also make sure you
remember your pass phrase, there is no way to recover it either.</p>

<h2 id="Change%20pass%20phrase">Change pass phrase</h2>

<p>Change the master pass phrase for your private key.</p>

<pre>
$ <b>pass passphrase</b>
Changing /home/username/.pass/.key pass phrase.
Current pass phrase:
New pass phrase:
Confirm:
Pass phrase changed.
$
</pre>

<h2 id="Add%20a%20password">Add a password</h2>

<p>Add the first password.</p>

<p>Run the following command and enter your master pass phrase, then
type-in the password and hit Enter. In the second line type username
and in the third line type url. Press Enter and <strong>CTRL-D</strong> to save
the password.</p>

<pre>
$ <b>pass add github</b>
Pass phrase
Press Enter and CTRL-D to complete.
<b>always mule boots jaguar agnostic singles dalmatian vixen
username: username
url: https://github.com</b>
$
</pre>

<h2 id="Import%20a%20password">Import a password</h2>

<p>Instead of typing your passwords manually you can pipe <a href="https://www.romanzolotarev.com/diceware.html">your favorite password
generator</a> right into <code>pass</code>.</p>

<pre>
$ <b>diceware | pass import twitter</b>
Enter pass phrase for /home/username/.pass/.key:
$
</pre>

<h2 id="Edit%20the%20password">Edit the password</h2>

<p>If you want to update your password run:</p>

<pre>
$ <b>pass edit github</b>
Enter pass phrase for /home/username/.pass/.key:
</pre>

<p>As soon as you enter the pass phrase <code>pass</code> opens <code>vi</code> with the
content of your password file. Let&#39;s enable <a href="https://help.github.com/articles/providing-your-2fa-authentication-code/">2FA at
GitHub</a>
paste the TOTP seed from GitHub into the password file. For example:</p>

<pre><code>totp: fx33dwhsbw7esrda
</code></pre>

<p>When you&#39;re done press <code>ZZ</code> to save and exit <code>vi</code>.</p>

<h2 id="Show%20the%20password">Show the password</h2>

<p>To show a password you can run:</p>

<pre>
$ <b>pass show twitter</b>
pelican mule satchel headband yo-yo lemon luscious older
$
</pre>

<p>But if a password file has a line staring with <code>totp:</code>, then <code>pass</code> shows
one time password in the second line.</p>

<pre>
$ <b>pass show github</b>
Enter pass phrase for /home/username/.pass/.key:
always mule boots jaguar agnostic singles dalmatian vixen
122635
$
</pre>

<h2 id="Export%20the%20password">Export the password</h2>

<p>If you want to see all lines of your password file, you can use <code>export</code></p>

<pre>
$ <b>pass export github</b>
Enter pass phrase for /home/username/.pass/.key:
always mule boots jaguar agnostic singles dalmatian vixen
username: username
url: https://github.com
$
</pre>

<h2 id="List%20all%20passwords">List all passwords</h2>

<p>To list all your passwords run:</p>

<pre>
$ <b>pass ls</b>
github
twitter
$
</pre>

<h2 id="Files">Files</h2>

<pre><code>.pass
|-- .key           - RSA private key protected by pass phrase
|-- .key.pub       - RSA public key
|-- github         - tar archive of two files:
|   |-- github.key - AES key encrypted with RSA public key
|   `-- github.enc - text file encrypted with AES key
|-- github.sig     - signature of tar archive created with
|                    RSA private key
...
</code></pre>

<p>Every time you change your password file <code>pass</code> generates tar archive with
a new AES key and a new signature. <code>pass</code> verifies the signature every
time you show or export the password.</p>

<h2 id="Environment%20variables">Environment variables</h2>

<p>To change path to the working directory or your keys, define
environment variables <code>BASE_DIR</code>, <code>PRIVATE_KEY</code>, <code>PUBLIC_KEY</code>. For example:</p>

<pre>
$ <b>BASE_DIR=~/.pass \
PRIVATE_KEY=~/.pass/.key \
PUBLIC_KEY=~/.pass/.key.pub pass init</b>
...
</pre>

<h2 id="Completions%20in%20Korn%20shell">Completions in Korn shell</h2>

<p>If you run <code>pass</code> on OpenBSD you may want to add completions in
<a href="https://man.openbsd.org/ksh.1">ksh(1)</a>&mdash;its default shell.
Add these functions to your <code>~/.profile</code>:</p>

<pre><code>update_complete_pass() {
  pass_list=$(pass ls)
  set -A complete_pass_edit -- $pass_list
  set -A complete_pass_export -- $pass_list
  set -A complete_pass_show -- $pass_list
}
update_complete_pass
pass_edit() { pass edit &quot;$1&quot;; }
pass_export() { pass export &quot;$1&quot; &amp;&amp; update_complete_pass; }
pass_show() { pass show &quot;$1&quot; &amp;&amp; update_complete_pass; }
</code></pre>

<p>Now open a terminal or source <code>~/.profile</code> and try <code>pass</code>:</p>

<pre>
$ <b>pass&lt;Tab&gt;</b>
init  passphrase  add  import  show  export  ls
$ <b>pass</b>
</pre>

<p>Or most importantly try <code>pass_show</code>:</p>

<pre>
$ <b>pass_show twit&lt;Tab&gt;</b>
twitch  twitter
$ <b>pass_show twit</b>
</pre>

<h2 id="See%20also">See also</h2>

<p><a href="https://www.romanzolotarev.com/diceware.html">diceware</a>, <a href="https://www.romanzolotarev.com/random.html">random</a></p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/ssh.html</guid>
<link>https://www.romanzolotarev.com/ssh.html</link>
<pubDate>Mon, 01 May 2017 00:00:00 +0000</pubDate>
<title>Generate SSH keys</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and <a href="https://www.romanzolotarev.com/macos/">macOS</a> 10.13</em></p>

<h1 id="Generate%20SSH%20keys">Generate SSH keys</h1>

<p>Generate a strong passphrase to protect your private key. For
example, with <a href="https://www.romanzolotarev.com/diceware.html">diceware</a>.</p>

<p>Run <a href="https://man.openbsd.org/ssh-keygen.1">ssh-keygen(1)</a> to create
a SSH key pair and enter that passphrase:</p>

<pre>
$ <b>ssh-keygen -t ed25519 -a 100</b>
Enter file in which to save the key
(/home/<em>username</em>/.ssh/id_ed25519):
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ~/.ssh/id_ed25519.
Your public key has been saved in ~/.ssh/id_ed25519.pub.
The key fingerprint is:
SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx comment
The key's randomart image is:
+--[ED25519 256]--+
|       .o=@*=    |
|        oX = .=  |
|        * o +    |
|       = o =     |
|        S o +    |
|       * + o     |
|      = X.o.=    |
|       O =+o     |
|      . E++++    |
+----[SHA256]-----+
$
</pre>

<p>Option <code>-t ed25519</code> specifies the type of the key.<br>Option <code>-a 100</code>
specifies the number of key derivation function rounds used (higher
the number&mdash;better protection against brute-force cracking).</p>

<h2 id="RSA%20fallback">RSA fallback</h2>

<p>If Ed25519 isn&#39;t yet supported by your operating systems, use long
RSA keys as a fallback.</p>

<pre>
$ <b>ssh-keygen -t rsa -b 4096 -o -a 100</b>
Enter file in which to save the key
(/home/<em>username</em>/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ~/.ssh/id_rsa.
Your public key has been saved in ~/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx comment
The key's randomart image is:
+---[RSA 2048]----+
|  .ooo ...ooo    |
|  ..o.+  .o+  E  |
|.o.o.o = . o     |
|.Boo= + +        |
|+ Bo . =S        |
| o . ...+. o     |
|    . o  ++      |
|     o o .o      |
|      + ..*      |
+----[SHA256]-----+
$
</pre>

<p>Option <code>-o</code> enables the new OpenSSH format to increase resistance to
brute-force cracking.</p>

<h2 id="Do%20not%20share%20private%20keys">Do not share private keys</h2>

<p>Don&#39;t copy or share your private key. Generate a new key pair for
every user and every device. Use the same key pair for multiple
destinations.</p>

<h2 id="Use%20SSH%20configuration">Use SSH configuration</h2>

<p>Add all your frequently used hosts to <code>~/.ssh/config</code>, like this:</p>

<pre>
Host <em>remote_host</em>
  User <em>username_on_remote_host</em>
  Hostname <em>www.example.com</em>
  IdentityFile /home/<em>username</em>/.ssh/id_ed25519
</pre>

<p>After adding this to your SSH configuration you can run:</p>

<pre>
# <b>ssh www</b>
</pre>

<p>instead of:</p>

<pre>
$ <b>ssh -i ~/.ssh/id_ed25519 username_on_remote_host@www.example.com</b>
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/xenodm.html</guid>
<link>https://www.romanzolotarev.com/openbsd/xenodm.html</link>
<pubDate>Tue, 01 May 2018 00:00:00 +0000</pubDate>
<title>Customize xenodm(1) login screen</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and 6.4</em></p>

<h1 id="Customize%20xenodm(1)%20login%20screen">Customize xenodm(1) login screen</h1>

<p><img src="https://www.romanzolotarev.com/xenodm.png" alt="login screen" /></p>

<p>Enable <a href="https://man.openbsd.org/xenodm.1">xenodm(1)</a>:</p>

<pre>
# <b>rcctl enable xenodm</b>
#
</pre>

<p>Edit <code>/etc/X11/xenodm/Xresources</code>:</p>

<pre><code>xlogin.Login.echoPasswd:       true
xlogin.Login.fail:             fail
xlogin.Login.greeting:
xlogin.Login.namePrompt:        login 
xlogin.Login.passwdPrompt:     passwd 

xlogin.Login.height:           180
xlogin.Login.width:            280
xlogin.Login.y:                320
xlogin.Login.frameWidth:       0
xlogin.Login.innerFramesWidth: 0

xlogin.Login.background:       black
xlogin.Login.foreground:       #eeeeee
xlogin.Login.failColor:        white
xlogin.Login.inpColor:         black
xlogin.Login.promptColor:      #eeeeec

xlogin.Login.face:             fixed-13
xlogin.Login.failFace:         fixed-13
xlogin.Login.promptFace:       fixed-13
```
</code></pre>

<p>Edit <code>/etc/X11/xenodm/Xsetup_0</code>:</p>

<pre><code>#!/bin/sh
xsetroot -solid black
</code></pre>

<p>Logout to check the login screen.</p>

<p><a href="https://www.romanzolotarev.com/openbsd/yubikey.html">Use YubiKey for login and SSH</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/cwm.html</guid>
<link>https://www.romanzolotarev.com/openbsd/cwm.html</link>
<pubDate>Mon, 04 Feb 2019 00:00:00 +0000</pubDate>
<title>Configure cwm(1)</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and 6.4</em></p>

<h1 id="Configure%20cwm(1)">Configure cwm(1)</h1>

<p><a href="http://man.openbsd.org/cwm.1">cwm(1)</a> is my favorite window manager
for X11. It has a tiling mode, so I don&#39;t have to rearrange windows
manually.</p>

<p>cwm(1) is in OpenBSD base.</p>

<pre>
$ echo 'exec cwm' >> ~/.xsession
</pre>

<p>Here is my <a href="https://www.romanzolotarev.com/openbsd/cwmrc">.cwmrc</a>. Quite often I keep just two
windows open. On the left side: <a href="https://www.romanzolotarev.com/tmux.html">tmux</a> in
<a href="http://man.openbsd.org/xterm.1">xterm(1)</a>. On the right side:
Firefox.</p>

<p><a href="https://www.romanzolotarev.com/desktop.png"><img src="https://www.romanzolotarev.com/desktop.jpeg" alt="desktop" /></a></p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/mount.html</guid>
<link>https://www.romanzolotarev.com/openbsd/mount.html</link>
<pubDate>Thu, 01 Mar 2018 00:00:00 +0000</pubDate>
<title>Mount disk with... mount(1)</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3.</em></p>

<h1 id="Mount%20disk%20with...%20mount(1)">Mount disk with... mount(1)</h1>

<p>Only <em>root</em> can <a href="http://man.openbsd.com/mount.8">mount(8)</a> file
systems on OpenBSD, a regular user should run mount(8) via
<a href="http://man.openbsd.com/doas.1">doas(1)</a>.</p>

<p>Plug in a USB drive and check system messages:</p>

<pre>
# <b>dmesg</b>
sd1 at scsibus2 targ 1 lun 0: &lt;Vendor, Model, 1.26&gt;
SCSI3 0/direct removable serial.12345678901234568789
sd1: 7633MB, 512 bytes/sector, 15633408 sectors
#
</pre>

<p>Check partitions:</p>

<pre>
# <b>disklabel sd1</b>
...
      size     offset  fstype [fsize bsize   cpg]
a:    736256       1024  4.2BSD   2048 16384 16142
c:  15633408          0  unused
i:       960         64   MSDOS
#
</pre>

<p>To mount a partition, for example, <em>a:</em>), use <em>/dev/sd1a</em> device.</p>

<p>Create a mount point directory, for example, <em>/mnt/usb-drive</em>, and
mount the drive:</p>

<pre>
# <b>mkdir -p /mnt/usb-drive</b>
# <b>mount /dev/sd1a /mnt/usb-drive</b>
# <b>ls /mnt/usb-drive</b>
...
#
</pre>

<p>It&#39;s mounted.</p>

<p>Before disconnecting the drive from the USB port, unmount it. Leave
mount point directory and then use it as an argument for
<a href="https://man.openbsd.org/umount.8">unmount(8)</a>.</p>

<pre>
# <b>cd</b>
# <b>umount /mnt/usb-drive</b>
#
</pre>

<p>Or you can address your device directly:</p>

<pre>
# <b>umount /dev/sd1a</b>
#
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/exfat.html</guid>
<link>https://www.romanzolotarev.com/openbsd/exfat.html</link>
<pubDate>Fri, 16 Nov 2018 00:00:00 +0000</pubDate>
<title>Mount exFAT file system on OpenBSD</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.4 with exfat-fuse 1.2.8</em></p>

<h1 id="Mount%20exFAT%20file%20system%20on%20OpenBSD">Mount exFAT file system on OpenBSD</h1>

<p>First, find your <code>uid</code>:</p>

<pre>
$ <b>id -u</b>
1000
$
</pre>

<p>Then as <em>root</em> install <em>exfat-fuse</em> and mount exFAT file system with your <code>uid</code>:</p>

<pre>
# <b>pkg_add exfat-fuse</b>
quirks-3.16 signed on 2018-10-12T15:26:25Z
exfat-fuse-1.2.8: ok
# <b>mkdir -p <em>/mnt/sd1i</em></b>
# <b>mount.exfat -o <em>uid=1000</em> <em>/dev/sd1i /mnt/sd1i</em></b>
FUSE exfat 1.2.8
#
</pre>

<p>Use the file system as a regular user:</p>

<pre>
$ <b>df /mnt/sd1i</b>
Filesystem  512-blocks      Used     Avail Capacity  Mounted on
fusefs         7716800      1344   7710336     0%    /mnt/sd1i
$ <b>touch /mnt/sd1i/test</b>
$ <b>ls -l /mnt/sd1i/test</b>
-rwxrwxrwx  1 romanzolotarev  wheel  0 Nov  5 16:11 /mnt/sd1i/test
$
</pre>

<p>To unmount run as <em>root</em>:</p>

<pre>
# <b>umount /mnt/sd1i</b>
#
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/mtp.html</guid>
<link>https://www.romanzolotarev.com/openbsd/mtp.html</link>
<pubDate>Tue, 06 Nov 2018 00:00:00 +0000</pubDate>
<title>Mount file system via Media Transfer Protocol on OpenBSD</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.4 with simple-mtpfs 0.3.0</em></p>

<h1 id="Mount%20file%20system%20via%20Media%20Transfer%20Protocol%20on%20OpenBSD">Mount file system via Media Transfer Protocol on OpenBSD</h1>

<p>First, find your <code>uid</code> and <code>gid</code>:</p>

<pre>
$ <b>id -u</b>
1000
$ <b>id -g</b>
1000
$
</pre>

<p>Connect your phone, camera, or any other MTP compatible device to your computer.
Set the device to MTP mode.</p>

<p>For example, on Android 8.0 it&#39;s called <strong>Transfer files</strong>.</p>

<p>Then as <em>root</em> install <em>simple-mtpfs</em> and mount a file system via MTP with your <code>uid</code>:</p>

<pre>
# <b>pkg_add simple-mtpfs</b>
quirks-3.16 signed on 2018-10-12T15:26:25Z
simple-mtpfs-0.3.0p0:libusb1-1.0.21p1: ok
simple-mtpfs-0.3.0p0:libmtp-1.1.15: ok
simple-mtpfs-0.3.0p0: ok
# <b>mkdir -p <em>/mnt/mtp</em></b>
# <b>simple-mtpfs --device 1 /mnt/mtp -o uid=1000 -o gid=1000 -o allow_other</b>
#
</pre>

<p>Use the file system as a regular user:</p>

<pre>
#
$ <b>df /mnt/mtp</b>
Filesystem  512-blocks      Used     Avail Capacity  Mounted on
fusefs       291042488  42956896 248085592    15%    /mnt/mtp
$ <b>touch /mnt/mtp/test</b>
$ <b>ls -l /mnt/mtp/test</b>
-rw-r--r--  1 romanzolotarev  romanzolotarev  0 Nov  5 16:29 /mnt/mnt/test
$
</pre>

<p>To unmount run as <em>root</em>:</p>

<pre>
# <b>umount /mnt/mtp</b>
#
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/yubikey.html</guid>
<link>https://www.romanzolotarev.com/openbsd/yubikey.html</link>
<pubDate>Fri, 01 Sep 2017 00:00:00 +0000</pubDate>
<title>Configure login(1) and sshd(8) for YubiKey on OpenBSD</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3</em></p>

<h1 id="Configure%20login(1)%20and%20sshd(8)%20for%20YubiKey%20on%20OpenBSD">Configure login(1) and sshd(8) for YubiKey on OpenBSD</h1>

<p>The <a href="http://man.openbsd.com/login_yubikey.8">login_yubikey(8)</a>
utility is called by <a href="https://man.openbsd.org/login.1">login(1)</a>
and others to authenticate the user with
<a href="https://www.yubico.com/store/">YubiKey</a> authentication.</p>

<h2 id="Prepare%20YubiKey">Prepare YubiKey</h2>

<p>Install and start <a href="https://github.com/Yubico/yubikey-personalization-gui">YubiKey Personalization
GUI</a>:</p>

<pre>
# <b>pkg_add yubikey-personalization-gui</b>
...
yubikey-personalization-gui-3.1.25: ok
# <b>yubikey-personalization-gui</b>
</pre>

<p>Insert your YubiKey into USB port, select <em>Yubico OTP &gt; Quick</em>,
select <strong>Configuration Slot 1</strong> or <strong>2</strong>, click <strong>Write
Configuration</strong>, save the log into <code>/tmp/yubikey.csv</code>, click
<strong>Exit</strong>.</p>

<p>Extract <em>uid</em> and <em>key</em> from the log, verify <code>/var/db/yubikey/*</code>
files, and remove <code>yubikey.csv</code> file.</p>

<pre>
# <b>cd /var/db/yubikey</b>
# <b>touch romanzolotarev.{uid,key}</b>
# <b>chown root:auth *</b>
# <b>chmod 440 *</b>
# <b>grep Yubico /tmp/yubikey.csv | cut -f5 -d, > romanzolotarev.uid</b>
# <b>grep Yubico /tmp/yubikey.csv | cut -f6 -d, > romanzolotarev.key</b>
# <b>cat *</b>
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxx
# <b>rm /tmp/yubikey.csv</b>
# <b>ls -l</b>
-r--r-----  1 root  auth  33 May  1 15:22 romanzolotarev.key
-r--r-----  1 root  auth  13 May  1 15:22 romanzolotarev.uid
#
</pre>

<p>You can uninstall <strong>yubikey-personalization-gui</strong></p>

<pre>
# <b>pkg_delete yubikey-personalization-gui</b>
yubikey-personalization-gui-3.1.25: ok
Read shared items: ok
# <b>pkg_delete -a</b>
...
Read shared items: ok
#
</pre>

<h2 id="Configure%20login(1)%20and%20sshd(8)">Configure login(1) and sshd(8)</h2>

<p>Back up <a href="https://man.openbsd.org/login.conf.5">login.conf(5)</a> and
<a href="https://man.openbsd.org/sshd_config.5">sshd_config(5)</a> to be able to
revert changes.</p>

<pre>
# <b>cp /etc/login.conf /etc/login.conf.bak</b>
# <b>cp /etc/ssh/sshd_config /etc/ssh/ssh_config.bak</b>
#
</pre>

<p>Change <code>auth-defaults</code> in <code>/etc/login.conf</code>:</p>

<pre><code>auth-defaults:auth=yubikey:
</code></pre>

<p>Add this line to <code>etc/ssh/sshd_config</code>:</p>

<pre><code>AuthenticationMethods publickey,password
</code></pre>

<p>Restart <code>sshd</code> and verify: when ssh asks for a password&mdash;instead
of entering your regular password&mdash;touch YubiKey, if you
have used slot 1 (or touch and hold it for 2-3 seconds for
slot 2)...</p>

<pre>
# <b>rcctl restart sshd</b>
# <b>ssh root@localhost</b>
root@localhost's password:
Last login: Wed May  2 17:11:06 2018 OpenBSD 6.3
(GENERIC.MP) #1: Sat Apr 21 14:26:25 CEST 2018

Welcome to OpenBSD: The proactively secure Unix-like
operating system.

Please use the sendbug(1) utility to report bugs in the system.
Before reporting a bug, please try to reproduce it with the
latest version of the code. With bug reports, please try to
ensure that enough information to reproduce the problem is
enclosed, and if a known fix for it exists, include that as well.
# exit
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/git.html</guid>
<link>https://www.romanzolotarev.com/git.html</link>
<pubDate>Thu, 07 Jun 2018 00:00:00 +0000</pubDate>
<title>Host Git repositories on OpenBSD</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 with git-2.16.2</em></p>

<h1 id="Host%20Git%20repositories%20on%20OpenBSD">Host Git repositories on OpenBSD</h1>

<p><a href="https://www.romanzolotarev.com/openbsd/">Deploy a server</a> and login into it.</p>

<p>On the remote host install
<a href="https://mirrors.edge.kernel.org/pub/software/scm/git/docs/">git(1)</a>,
add <code>git</code> user, add your public <a href="https://www.romanzolotarev.com/ssh.html">SSH key</a>, change owner
and group, then exit.</p>

<pre>
# <b>pkg_add git</b>
...
git-2.16.2: ok
The following new rcscripts were installed: /etc/rc.d/gitdaemon
See rcctl(8) for details.
Look in /usr/local/share/doc/pkg-readmes for extra documentation.
#
# <b>mkdir /home/git</b>
# <b>user add git</b>
# <b>mkdir -m 700 /home/git/.ssh</b>
# <b>cp /root/.ssh/authorized_keys /home/git/.ssh/</b>
# <b>chown -R git:git /home/git</b>
#
</pre>

<p>From your local host initialize a bare repository on the remote,
add the remote and push a local copy to it.</p>

<pre>
$ <b>ssh git@<em>REMOTE</em> git init --bare <em>REPOSITORY.git</em></b>
Initialized emtpy Git repository in /home/git/REPOSITORY.git/
$ <b>cd <em>REPOSITORY</em></b>
$ <b>git remote add <em>REMOTE</em> <em>git@REMOTE_SERVER:REPOSITORY.git</em></b>
$ <b>git push <em>REMOTE</em> master</b>
Counting objects: 1049, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (1041/1041), done.
Writing objects: 100% (1049/1049), 3.80 MiB | 257.00 KiB/s, done.
Total 1049 (delta 676), reused 0 (delta 0)
remote: Resolving deltas: 100% (676/676), done.
To <em>REMOTE_SERVER:REPOSITORY.git</em>
 * [new branch]      master -> master
$
</pre>

<p><a href="https://www.romanzolotarev.com/stagit.html">Publish Git repositories</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/stagit.html</guid>
<link>https://www.romanzolotarev.com/stagit.html</link>
<pubDate>Thu, 07 Jun 2018 00:00:00 +0000</pubDate>
<title>Publish Git repositories with stagit(1) on OpenBSD</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.4 with stagit-0.8</em></p>

<h1 id="Publish%20Git%20repositories%20with%20stagit(1)%20on%20OpenBSD">Publish Git repositories with stagit(1) on OpenBSD</h1>

<p><a href="https://git.codemadness.org/stagit/">stagit(1)</a> generates HTML files
from your git repository. The source of this website, for example:
<a href="https://www.romanzolotarev.com/src/">/src/</a>.</p>

<h2 id="Install">Install</h2>

<p>Set up <a href="https://www.romanzolotarev.com/git.html">git</a> and <a href="https://www.romanzolotarev.com/openbsd/httpd.html">httpd</a>, then install stagit.</p>

<pre>
# <b>pkg_add stagit</b>
quirks-3.16 signed on 2018-10-12T15:26:25Z
stagit-0.8:libssh2-1.8.0p0: ok
stagit-0.8:libgit2-0.27.2: ok
stagit-0.8: ok
#
</pre>

<h2 id="Update%20Git%20repository">Update Git repository</h2>

<p>Add <code>owner</code> and <code>description</code> to the Git repository:</p>

<pre>
$ <b>cd <em>REPOSITORY.git</em></b>
$ <b>echo <em>'OWNER_NAME'</em> > owner</b>
$ <b>echo <em>'DESCRIPTION'</em> > description</b>
$
</pre>

<p>Add <code>post-receive</code> hook to <code>REPOSITORY.git/hooks/</code>:</p>

<pre><code>#!/bin/sh
set -e
dst=&quot;/var/www/htdocs/$(basename &quot;$(pwd)&quot; &#39;.git&#39;)&quot;

mkdir -p &quot;$dst/src&quot;
(cd &quot;$dst/src&quot; &amp;&amp; stagit &quot;$src&quot;)
cp -f &quot;$dst/src/log.html&quot; &quot;$dst/src/index.html&quot;
</code></pre>

<p>Check out my files: <a href="https://www.romanzolotarev.com/post-receive">post-receive</a> hook and
<a href="https://www.romanzolotarev.com/stagit/style.css">style.css</a>.</p>

<h2 id="Test">Test</h2>

<p>To test <code>post-receive</code> hook push from your local host to the server:</p>

<pre>
$ <b>git push <em>REMOTE</em> master</b>
...
$
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/httpd.html</guid>
<link>https://www.romanzolotarev.com/openbsd/httpd.html</link>
<pubDate>Thu, 12 Apr 2018 00:00:00 +0000</pubDate>
<title>Configure httpd(8) on OpenBSD</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and 6.4</em></p>

<h1 id="Configure%20httpd(8)%20on%20OpenBSD">Configure httpd(8) on OpenBSD</h1>

<p><a href="https://www.romanzolotarev.com/install.html">Install OpenBSD</a>.</p>

<p>Edit <code>/etc/httpd.conf</code>. Add two <code>server</code> sections&mdash;one for
<code>www</code> and another for naked domain (all requests are redirected to
<code>www</code>).</p>

<pre><code>server &quot;www.example.com&quot; {
  listen on * port 80
  root &quot;/htdocs/www.example.com&quot;
}

server &quot;example.com&quot; {
  listen on * port 80
  block return 301 &quot;http://www.example.com$REQUEST_URI&quot;
}
</code></pre>

<p><a href="https://man.openbsd.org/httpd.8">httpd(8)</a> is chrooted to <code>/var/www</code>
by default, so let&#39;s make a document root directory:</p>

<pre>
# <b>mkdir -p /var/www/htdocs/www.example.com</b>
#
</pre>

<p>Save and check the configuration. Enable httpd(8) and start it.</p>

<pre>
# <b>httpd -n</b>
configuration ok
# <b>rcctl enable httpd</b>
# <b>rcctl start httpd</b>
#
</pre>

<h2 id="Publish%20your%20website">Publish your website</h2>

<p>Copy your website content into <code>/var/www/htdocs/www.example.com</code> and then
test it your web browser.</p>

<pre>
http://XXX.XXX.XXX.XXX/
</pre>

<p>Your web server should be up and running.</p>

<h2 id="Update%20DNS%20records">Update DNS records</h2>

<p>If there is another HTTPS server using this domain, configure that server
to redirect all HTTPS requests to HTTP.</p>

<p>Now as your new server is ready you can update DNS records accordingly.</p>

<pre><code>    example.com. 300 IN     A XXX.XXX.XXX.XXX
www.example.com. 300 IN     A XXX.XXX.XXX.XXX
</code></pre>

<p>Examine your DNS is propagated.</p>

<pre>
$ <b>dig example.com www.example.com</b>
...
;; ANSWER SECTION:
example.com.         299     IN      A       XXX.XXX.XXX.XXX
...
;; ANSWER SECTION:
www.example.com.     299     IN      A       XXX.XXX.XXX.XXX
...
$
</pre>

<p>Check IP addresses in answer sections.</p>

<p>Open your website in a browser.</p>

<pre><code>http://www.example.com/
</code></pre>

<p><a href="https://www.romanzolotarev.com/openbsd/acme-client.html">Enable HTTPS on your server</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/acme-client.html</guid>
<link>https://www.romanzolotarev.com/openbsd/acme-client.html</link>
<pubDate>Fri, 13 Apr 2018 00:00:00 +0000</pubDate>
<title>Enable HTTPS with acme-client(1) and Let&#39;s Encrypt on OpenBSD</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.4</em></p>

<h1 id="Enable%20HTTPS%20with%20acme-client(1)%20and%20Let&amp;#39;s%20Encrypt%20on%20OpenBSD">Enable HTTPS with acme-client(1) and Let&#39;s Encrypt on OpenBSD</h1>

<p><a href="https://www.romanzolotarev.com/openbsd/httpd.html">Configure httpd(8)</a>.</p>

<p>To use <a href="https://letsencrypt.org">Let&#39;s Encrypt</a> as a certificate
authority for TLS encryption add or update your CAA records for
your domain.</p>

<pre><code>    example.com. 300 IN   CAA   0 issue &quot;letsencrypt.org&quot;
www.example.com. 300 IN   CAA   0 issue &quot;letsencrypt.org&quot;
</code></pre>

<p>To configure <a href="https://man.openbsd.org/acme-client.1">acme-client(1)</a>,
add these sections to <code>/etc/acme-client.conf</code>:</p>

<pre><code>authority letsencrypt {
  api url &quot;https://acme-v01.api.letsencrypt.org/directory&quot;
  account key &quot;/etc/acme/letsencrypt-privkey.pem&quot;
}
authority letsencrypt-staging {
  api url &quot;https://acme-staging.api.letsencrypt.org/directory&quot;
  account key &quot;/etc/acme/letsencrypt-staging-privkey.pem&quot;
}
domain www.example.com {
  alternative names { example.com }
  domain key &quot;/etc/ssl/private/www.example.com.key&quot;
  domain certificate &quot;/etc/ssl/www.example.com.crt&quot;
  domain full chain certificate &quot;/etc/ssl/www.example.com.pem&quot;
  sign with letsencrypt
}
</code></pre>

<p>Create directories:</p>

<pre>
# <b>mkdir -p -m 700 /etc/acme</b>
# <b>mkdir -p -m 700 /etc/ssl/acme/private</b>
# <b>mkdir -p -m 755 /var/www/acme</b>
#
</pre>

<p>Update <code>/etc/httpd.conf</code> to handle verification requests from Let&#39;s
Encrypt.  It should look like this:</p>

<pre><code>server &quot;www.example.com&quot; {
  listen on * port 80
  root &quot;/htdocs/www.example.com&quot;
  location &quot;/.well-known/acme-challenge/*&quot; {
    root &quot;/acme&quot;
    request strip 2
  }
}

server &quot;example.com&quot; {
  listen on * port 80
  block return 301 &quot;http://www.example.com$REQUEST_URI&quot;
}
</code></pre>

<p>Check this configuration and restart <code>httpd</code>:</p>

<pre>
# <b>httpd -n</b>
configuration ok
# <b>rcctl restart httpd</b>
httpd(ok)
httpd(ok)
#
</pre>

<p>Let&#39;s run <code>acme-client</code> to create new account and domain keys.</p>

<pre>
# <b>acme-client -vAD www.example.com</b>
...
acme-client: /etc/ssl/www.example.com.crt: created
acme-client: /etc/ssl/www.example.com.pem: created
#
</pre>

<p>To renew certificates automatically edit the current crontab:</p>

<pre>
# <b>crontab -e</b>
</pre>

<p>Append this line:</p>

<pre><code>0 0 * * * acme-client www.example.com &amp;&amp; rcctl reload httpd
</code></pre>

<p>Save and exit:</p>

<pre>
crontab: installing new crontab
#
</pre>

<h2 id="Enable%20HTTPS%20and%20restart%20the%20daemon">Enable HTTPS and restart the daemon</h2>

<p>Now we have the new certificate and domain key, so we can re-configure
<code>httpd</code> to handle HTTPS requests. Add two server sections to
<code>/etc/httpd.conf</code> for TLS. The result should look like this:</p>

<pre><code>server &quot;www.example.com&quot; {
  listen on * tls port 443
  root &quot;/htdocs/www.example.com&quot;
  tls {
    certificate &quot;/etc/ssl/www.example.com.pem&quot;
    key &quot;/etc/ssl/private/www.example.com.key&quot;
  }
  location &quot;/.well-known/acme-challenge/*&quot; {
    root &quot;/acme&quot;
    request strip 2
  }
}

server &quot;example.com&quot; {
  listen on * tls port 443
  tls {
    certificate &quot;/etc/ssl/www.example.com.pem&quot;
    key &quot;/etc/ssl/private/www.example.com.key&quot;
  }
  block return 301 &quot;https://www.example.com$REQUEST_URI&quot;
}

server &quot;www.example.com&quot; {
  listen on * port 80
  root &quot;/htdocs/www.example.com&quot;
  location &quot;/.well-known/acme-challenge/*&quot; {
    root &quot;/acme&quot;
    request strip 2
  }
}

server &quot;example.com&quot; {
  listen on * port 80
  block return 301 &quot;http://www.example.com$REQUEST_URI&quot;
}
</code></pre>

<p>Test this configuration and restart <code>httpd</code>:</p>

<pre>
# <b>httpd -n</b>
configuration ok
# <b>rcctl restart httpd</b>
httpd (ok)
httpd (ok)
#
</pre>

<p>To verify your setup <a href="https://www.ssllabs.com/ssltest/analyze.html">run SSL server test</a>.</p>

<p>Congratulation! Your website and its visitors are now secured.</p>

<h2 id="Add%20domains">Add domains</h2>

<p>Backup and remove the certificate</p>

<pre>
# <b>mv /etc/ssl/www.example.com.crt /etc/ssl/www.example.com.crt.bak</b>
#
</pre>

<p>Add a new alternative name to <code>/etc/acme-client.conf</code>:</p>

<pre><code>...
alternative names { example.com new.example.com }
...
</code></pre>

<p>Add a new server section to  <code>/etc/httpd.conf</code>. Use the same certificate and key.</p>

<pre><code>...
server &quot;new.example.com&quot; {
  listen on * tls port 443
  root &quot;/htdocs/new.example.com&quot;
  tls {
    certificate &quot;/etc/ssl/www.example.com.pem&quot;
    key &quot;/etc/ssl/private/www.example.com.key&quot;
  }
  location &quot;/.well-known/acme-challenge/*&quot; {
    root &quot;/acme&quot;
    request strip 2
  }
}
...
</code></pre>

<p>Request a new certificate with the new alternative new in it. Verify
<code>httpd.conf</code> and restart <code>httpd(8)</code>:</p>

<pre>
# <b>acme-client -vFAD www.example.com</b>
...
acme-client: /etc/ssl/www.example.com.crt: created
acme-client: /etc/ssl/www.example.com.pem: created
# <b>httpd -n</b>
configuration ok
# <b>rcctl restart httpd</b>
httpd(ok)
httpd(ok)
#
</pre>

<hr/>

<p><strong>Thanks</strong> to
<a href="https://twitter.com/gumnos">Tim Chase</a>,
<a href="https://twitter.com/mischapeters">Mischa Peters</a>,
and <a href="https://twitter.com/vetelko">Ve Telko</a>
for reading drafts of this,
to <a href="https://reykfloeter.com/">Reyk Floeter</a>
for <a href="https://bsd.plumbing">httpd(8)</a>
and to <a href="https://www.divelog.blue/">Kristaps Dzonsons</a>
for <a href="https://kristaps.bsd.lv/acme-client/">acme-client(1)</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/smtpd-forward.html</guid>
<link>https://www.romanzolotarev.com/openbsd/smtpd-forward.html</link>
<pubDate>Fri, 23 Nov 2018 00:00:00 +0000</pubDate>
<title>Forward outgoing mail to a remote SMTP server</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.4</em></p>

<h1 id="Forward%20outgoing%20mail%20to%20a%20remote%20SMTP%20server">Forward outgoing mail to a remote SMTP server</h1>

<p>Replace <a href="https://man.openbsd.org/smtpd.conf.5">smtpd.conf(5)</a>, add <code>secrets</code>, set permissions, test the
configuration, and restart <a href="https://man.openbsd.org/smtpd.8">smtpd(8)</a>:</p>

<pre>
# <b>cat &gt; /etc/mail/smtpd.conf &lt;&lt; EOF</b>
<i>table aliases file:/etc/mail/aliases</i>
<i>table secrets file:/etc/mail/secrets</i>
<i>listen on lo0</i>
<i>action "local" mbox alias &lt;aliases&gt;</i>
<i>action "relay" relay host smtp+tls://<em>foo@server:port</em> auth &lt;secrets&gt;</i>
<i>match for local action "local"</i>
<i>match for any action "relay"</i>
<i><b>EOF</b></i>
#
# <b>touch /etc/mail/secrets</b>
# <b>chmod 640 /etc/mail/secrets</b>
# <b>chown root:_smtpd /etc/mail/secrets</b>
# <b>echo "<em>foo username:password</em>" &gt; /etc/mail/secrets</b>
#
# <b>smtpd -n</b>
configuration OK
# <b>rcctl restart smtpd</b>
smtpd (ok)
smtpd (ok)
#
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/openbsd/nsd.html</guid>
<link>https://www.romanzolotarev.com/openbsd/nsd.html</link>
<pubDate>Fri, 14 Dec 2018 00:00:00 +0000</pubDate>
<title>Configure nsd(8) on OpenBSD</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.4</em></p>

<h1 id="Configure%20nsd(8)%20on%20OpenBSD">Configure nsd(8) on OpenBSD</h1>

<p>Install two VMs in two different networks.<br>
For example, <a href="https://www.romanzolotarev.com/oams.html">OpenBSD.Amsterdam</a> and <a href="https://www.romanzolotarev.com/vultr.html">Vultr</a>.</p>

<p>Let&#39;s pick arbitrary names for them:</p>

<pre><code>ns1.example.com
ns2.example.com
</code></pre>

<p>Edit <a href="https://man.openbsd.org/nsd.conf.5">nsd.conf(5)</a> on <code>ns1</code>,<br>
create a zone file for <code>example.com</code>,<br>
copy <code>nsd.conf</code> and <code>example.com.zone</code> to <code>ns2</code>,<br>
enable and start <a href="https://man.openbsd.org/nsd.8">nsd(8)</a> on both servers.</p>

<pre>
# <b>cat &gt; /var/nsd/etc/nsd.conf &lt;&lt; EOF</b>
<i>server:</i>
<i>  database: ""</i>
<i></i>
<i>remote-control:</i>
<i>  control-enable: yes</i>
<i>  control-interface: /var/run/nsd.sock</i>
<i></i>
<i>zone:</i>
<i>  name: <em>example.com</em></i>
<i>  zonefile: master/%s.zone</i>
<i>EOF</i>
#
# <b>cat &gt; /var/nsd/zones/master/<em>example.com</em>.zone &lt;&lt; EOF</b>
<i>$ORIGIN             <em>example.com.</em></i>
<i>$TTL    300</i>
<i>@       3600  SOA   <em>ns1.example.com</em>. hostmaster.<em>example.com</em>. (</i>
<i>        2018121401  ; serial YYYYMMDDnn</i>
<i>        1440        ; refresh</i>
<i>        3600        ; retry</i>
<i>        604800      ; expire</i>
<i>        300 )       ; minimum TTL</i>
<i>@             NS    ns1.<em>example.com</em>.</i>
<i>@             NS    ns2.<em>example.com</em>.</i>
<i>ns1           A     <em>46.23.88.178</em></i>
<i>ns2           A     <em>140.82.28.210</em></i>
<i>@             MX    10 smtp.<em>example.com</em>.</i>
<i>@             MX    20 smtp.<em>example.com</em>.</i>
<i>@             A     <em>46.23.88.178</em></i>
<i>www           A     <em>46.23.88.178</em></i>
<i>EOF</i>
#
# <b>rcctl enable nsd</b>
# <b>rcctl start nsd</b>
nsd (ok)
# <b>dig +short <em>example.com</em> NS @127.0.0.1</b>
ns1.example.com.
ns2.example.com.
#
</pre>

<p>Update nameservers <code>ns1.example.com</code> and their IP addreses (for
<em>glue records</em>) at your domain registrar.  Your mail server should
accept mail for <em>hostmaster@example.com</em>.</p>

<p>Verify your setup with <a href="https://zonemaster.net">zonemaster.net</a>.</p>

<h2 id="Update%20zone">Update zone</h2>

<p>Edit the zone file and <strong>increment the serial</strong> on <code>ns1</code>,
copy the zone file to <code>ns2</code>, reload nsd(8) on <code>ns1</code> and <code>ns2</code>.</p>

<pre>
# <b>rcctl reload nsd</b>
nsd(ok)
#
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/ws.html</guid>
<link>https://www.romanzolotarev.com/ws.html</link>
<pubDate>Sun, 23 Sep 2018 00:00:00 +0000</pubDate>
<title>Find and remove whitespaces with grep(1) and sed(1)</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3.</em></p>

<h1 id="Find%20and%20remove%20whitespaces%20with%20grep(1)%20and%20sed(1)">Find and remove whitespaces with grep(1) and sed(1)</h1>

<p>TL;DR: Checkout my <a href="https://www.romanzolotarev.com/bin/fws">fws</a>, <a href="https://www.romanzolotarev.com/bin/tws">tws</a>, and
<a href="https://www.romanzolotarev.com/openbsd/gitconfig">.gitconfig</a>.</p>

<hr/>

<p>Find lines with trailing spaces in all non-binary files (recursively
starting from the current directory).</p>

<pre>
$ <b>grep -rIl '[[:space:]]$' .</b>
file-with-trailing-spaces.txt
$
</pre>

<p>Find and remove those spaces.</p>

<pre>
$ <b>grep -rIl '[[:space:]]$' . | xargs sed -i 's/[[:space:]]*$//'</b>
$
</pre>

<h2 id="A%20slightly%20faster%20version">A slightly faster version</h2>

<p>Exclude <code>*.git</code> directories and use all CPU cores.</p>

<pre>
$ <b>find . \
    \( -type d -name '*.git' -prune \) -o \
    \( -type f -print0 \) |
xargs -0 \
    -P "$(sysctl -n hw.ncpu)" \
    -r 2>/dev/null grep -Il '[[:space:]]$'</b>
file-with-trailing-spaces.txt
$
</pre>

<h2 id="Configure%20vi">Configure vi</h2>

<p>Add to <code>.exrc</code>:</p>

<pre><code>map gt mm:%s/[[:space:]]*$//^M`m
</code></pre>

<p>Where <code>^M</code> is the actual CR character: press <code>^V</code>, then <code>&lt;Enter&gt;</code>.</p>

<h2 id="Configure%20git">Configure git</h2>

<p>Git can <a href="https://www.git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#_code_core_whitespace_code">detect whitespaces</a>.</p>

<pre>
$ <b>git config --global core.whitespace \
    trailing-space,-space-before-tab,indent-with-non-tab,cr-at-eol</b>
$
</pre>

<p>Add <code>pre-commit</code> hook to <code>.git/hooks</code>:</p>

<pre><code>#!/bin/sh
exec git diff-index --check --cached HEAD --
</code></pre>

<p>If there are whitespace errors, it prints the file names and fails.</p>

<hr/>

<p><strong>Thanks</strong> to <a href="https://twitter.com/gumnos">Tim Chase</a> for performance
hints.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/markdown.html</guid>
<link>https://www.romanzolotarev.com/markdown.html</link>
<pubDate>Tue, 30 Aug 2016 00:00:00 +0000</pubDate>
<title>Markdown in twenty four lines</title>
<description><![CDATA[

<h1 id="Markdown%20in%20twenty%20four%20lines">Markdown in twenty four lines</h1>

<pre><code># Start a heading with one hash

## Start a subheading with two hashes

Paragraphs are separated by empty lines.
**Bold text** is wrapped in double asterisks,
_italic_ in underscores,
`monospace` in backticks.

[A link](https://www.romanzolotarev.com/).
![An image](/avatar.jpeg)

    # Indented code block
    hello() { echo &#39;Hello&#39;; }

A list of items:

- Each item is on its line
- Lines start with `-`

An ordered list:

1. The first item
1. The second item can start with `1.` too
1. Markdown handles the numbering for you.
</code></pre>

<p>Convert Markdown to HTML with
<a href="https://www.romanzolotarev.com/jekyll.html">Jekyll</a>,
<a href="https://kristaps.bsd.lv/lowdown/">lowdown</a>,
<a href="http://pandoc.org/">pandoc</a>,
or with the original
<a href="https://daringfireball.net/projects/markdown/">Markdown.pl</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/tmux.html</guid>
<link>https://www.romanzolotarev.com/tmux.html</link>
<pubDate>Fri, 18 May 2018 00:00:00 +0000</pubDate>
<title>Manage terminals with tmux(1)</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3</em></p>

<h1 id="Manage%20terminals%20with%20tmux(1)">Manage terminals with tmux(1)</h1>

<p><a href="https://man.openbsd.org/tmux.1">tmux(1)</a> is a terminal multiplexor.</p>

<h2 id="Session">Session</h2>

<p>To create and attach to a new session run:</p>

<pre>
$ <b>tmux</b>
</pre>

<p>You&#39;ve got a green status bar at the bottom of the window. That
means now you&#39;re attached to a tmux session with name <code>0</code> (in the
left bottom corner <code>[0]</code>) in the first and only window open number
<code>0</code> and name <code>ksh</code> (<code>0:ksh*</code>).</p>

<p>To detach from tmux session press <code>C-b d</code> (<code>Ctrl-b</code> and then <code>d</code>)
or you can just kill your terminal window. <code>C-b</code> is the default
<code>PREFIX</code> key in tmux(1). The session stays intact.</p>

<p>To attach to existing session instead of <code>tmux</code> run:</p>

<pre>
$ <b>tmux new -A -s 0</b>
</pre>

<p>tmux(1) tries to attach (<code>-A</code>) to the existing session (<code>-s 0</code>) and if
fails then it creates a new session.</p>

<h2 id="Windows">Windows</h2>

<p>To create a new window press <code>PREFIX c</code>. You&#39;ve got
another window created and selected (<code>1:ksh*</code>).</p>

<p>To select window <code>0</code> press <code>PREFIX 0</code>, to select <code>1</code> press <code>PREFIX 1</code>.</p>

<p>To close a window close program its running (to exit shell press <code>C-d</code>)
or you can kill the window with <code>PREFIX d</code>. By default name of a window
changes to its active program (for example, <code>ksh</code>).</p>

<p>To rename a window press <code>PREFIX ,</code> then edit its name and hit <code>Enter</code>.
Once a window is renamed its name will persist for the session.</p>

<h2 id="Panes">Panes</h2>

<p>With tmux(1) you can split a window into panes. <code>PREFIX &quot;</code> to split
horizontally and `PREFIX %&#39; to split the window vertically.</p>

<p>To select another pane use <code>PREFIX arrow</code>, where <code>arrow</code> is <code>up</code>,
<code>down</code>, <code>left</code>, or <code>right</code>.</p>

<p>To resize panes use <code>PREFIX c-arrow</code>.</p>

<p>Press <code>PREFIX</code> just once, then press <code>arrow</code> as many times as you need
to select (or <code>c-arrow</code> to resize).  But key press intervals should be
under 500 ms (see <code>repeat-time</code> option), otherwise you need to press
<code>PREFIX</code> again.</p>

<table>
<thead>
<tr>
<th style="text-align: left">Action</th>
<th style="text-align: left">Keys</th>
</tr>
</thead>

<tbody>
<tr>
<td style="text-align: left">Open a window</td>
<td style="text-align: left"><code>PREFIX c</code></td>
</tr>
<tr>
<td style="text-align: left">Select windows 0 to 9</td>
<td style="text-align: left"><code>PREFIX 0</code> ... <code>PREFIX 9</code></td>
</tr>
<tr>
<td style="text-align: left">Rename the window</td>
<td style="text-align: left"><code>PREFIX ,</code></td>
</tr>
<tr>
<td style="text-align: left">Kill the window</td>
<td style="text-align: left"><code>PREFIX x</code></td>
</tr>
<tr>
<td style="text-align: left">Veritical split</td>
<td style="text-align: left"><code>PREFIX %</code></td>
</tr>
<tr>
<td style="text-align: left">Horizontal</td>
<td style="text-align: left"><code>PREFIX &quot;</code></td>
</tr>
<tr>
<td style="text-align: left">Select a pane</td>
<td style="text-align: left"><code>PREFIX up</code></td>
</tr>
<tr>
<td style="text-align: left"></td>
<td style="text-align: left"><code>PREFIX down</code></td>
</tr>
<tr>
<td style="text-align: left"></td>
<td style="text-align: left"><code>PREFIX left</code></td>
</tr>
<tr>
<td style="text-align: left"></td>
<td style="text-align: left"><code>PREFIX right</code></td>
</tr>
<tr>
<td style="text-align: left">Resize the pane</td>
<td style="text-align: left"><code>PREFIX c-up</code></td>
</tr>
<tr>
<td style="text-align: left"></td>
<td style="text-align: left"><code>PREFIX c-down</code></td>
</tr>
<tr>
<td style="text-align: left"></td>
<td style="text-align: left"><code>PREFIX c-left</code></td>
</tr>
<tr>
<td style="text-align: left"></td>
<td style="text-align: left"><code>PREFIX c-right</code></td>
</tr>
</tbody>
</table>

<h2 id="Workflow">Workflow</h2>

<p>Keep one session at a time and use one window per task. Each window
may have multiple panes.</p>

<p>To start tmux(1) use this shell script, <code>~/bin/stmux</code>:</p>

<pre><code>#!/bin/sh
usage() { &gt;&amp;2 echo &quot;usage: ${0##*/} window path command&quot;; exit 1; }
[ -z &quot;$1&quot; ] &amp;&amp; usage
[ -z &quot;$2&quot; ] &amp;&amp; usage
[ -z &quot;$3&quot; ] &amp;&amp; usage

tmux select-window -t &quot;$1&quot; 2&gt;/dev/null ||
tmux new-window -n &quot;$1&quot; -c &quot;$HOME/$2&quot; &quot;$3&quot;
</code></pre>

<p><code>stmux</code> requires three arguments: a window name, a path to the
working directory, and an initial command.</p>

<p>Here is a shortcut:</p>

<pre><code>m() { &quot;$HOME/bin/stmux&quot; m pub/music cmus; }
</code></pre>

<p>Run <code>m</code> from anywhere:</p>

<pre>
$ <b>m</b>
</pre>

<p>It tries to select the window named <code>m</code> and if it fails, it
creates that window and runs <code>cmus</code> from <code>pub/music</code> directory.</p>

<h2 id="See%20also">See also</h2>

<p><a href="https://www.romanzolotarev.com/openbsd/tmux.conf">.tmux.conf</a>,
<a href="https://www.romanzolotarev.com/openbsd/profile">.profile</a>,
<a href="https://www.romanzolotarev.com/bin/status">status</a>,
<a href="https://github.com/tmux/tmux/wiki">tmux wiki</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/vi.html</guid>
<link>https://www.romanzolotarev.com/vi.html</link>
<pubDate>Sat, 12 May 2018 00:00:00 +0000</pubDate>
<title>Edit text with vi(1)</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and 6.4</em></p>

<h1 id="Edit%20text%20with%20vi(1)">Edit text with vi(1)</h1>

<p>On OpenBSD <a href="https://man.openbsd.org/vi.1">vi(1)</a> is based on <a href="https://sites.google.com/a/bostic.com/keithbostic/vi">nvi
1.79</a>, written
by Keith Bostic.</p>

<p>You can&#39;t <em>record</em> macros in nvi, but you still can use them. Add
a sequence of commands into a buffer (for example, <code>&quot;q...</code>), then
apply the macro as usual (with <code>@q</code>).</p>

<p>To edit multiple files: <code>vi file1 file2</code>, then <code>:n[ext]</code>, <code>:prev</code> to
switch, and <code>:ar[gs]</code> to list them all.</p>

<p>To open one more file <code>:e[dit] file</code>, then <code>^6</code> to alternate between
two, or use <code>:e#</code> command.</p>

<p>To open in a split <code>:E[dit] file</code>, then <code>^W</code> to switch between windows,
and to set the window height to 20 lines <code>:res[ize] 20</code>.</p>

<p>To scroll current line to the top <code>z&lt;Enter&gt;</code>, to the center <code>z.</code>,
and to the bottom of screen <code>z-</code>. Scroll lines with <code>^Y</code> and
<code>^E</code> as usual.</p>

<p>To search for the &quot;expression&quot; and place the next occurence of it
to the center of screen: <code>/expression/z.</code>.</p>

<p>Undo and redo: Press <code>u</code> to undo previous edit, then press <code>.</code> (dot)
to undo, to redo press <code>u</code> again.</p>

<p>Increment a number: place cursor at the first digit and press <code>#</code>.</p>

<p>To redraw the screen press <code>^L</code>.</p>

<p>If you miss <em>Visual</em> mode, try marks. For example, mark the line
by pressing <code>mm</code>, move to another line, then delete from the current
to the marked line with <code>d&#39;m</code>. Or copy to the buffer with <code>y&#39;m</code>.</p>

<p>Break lines at column 72 in <em>Insert</em> and <em>Append</em> modes.</p>

<pre><code>:set wraplen=72
</code></pre>

<p>Format a paragraph with goal line length 72, allow indented paragraphs.</p>

<pre><code>:?^$?,//!fmt -pw 72
</code></pre>

<p>Sort lines in a paragraph (in <em>Command</em> mode):</p>

<pre><code>!}sort
</code></pre>

<p>To remove trailing spaces:</p>

<pre><code>:%s/[[:space:]]\{1,\}/
</code></pre>

<p>To edit command-history:</p>

<pre><code>:set cedit=\&lt;TAB&gt;
</code></pre>

<p>Where <code>&lt;TAB&gt;</code> is the actual tab character: press <code>^V</code>, then <code>&lt;TAB&gt;</code>.</p>

<p>To read help:</p>

<pre><code>:help
</code></pre>

<p>If you need Unicode, use <a href="https://github.com/lichray/nvi2">nvi2</a>:</p>

<pre>
# <b>pkg_add nvi</b>
...
nvi-2.1.3p1-iconv: ok
#
</pre>

<h2 id="See%20also">See also</h2>

<p><a href="https://www.romanzolotarev.com/openbsd/exrc">.exrc</a>,
<a href="https://www.romanzolotarev.com/openbsd/profile"><code>.profile</code></a>,
<a href="https://www.romanzolotarev.com/openbsd/tmux.conf">.tmux.conf</a>,
<a href="http://www.jeffw.com/vi/vi_help.txt">vi command help guide</a> by Jeff W.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/ssg.html</guid>
<link>https://www.romanzolotarev.com/ssg.html</link>
<pubDate>Sat, 07 Apr 2018 00:00:00 +0000</pubDate>
<title>Make a static site with find(1), grep(1), and lowdown(1)</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and 6.4</em></p>

<h1 id="Make%20a%20static%20site%20with%20find(1),%20grep(1),%20and%20lowdown(1)">Make a static site with find(1), grep(1), and lowdown(1)</h1>

<p><a href="https://www.romanzolotarev.com/bin/ssg4">ssg</a> is a static site generator written in shell. Optionally it
converts Markdown files to HTML with
<a href="https://kristaps.bsd.lv/lowdown/">lowdown(1)</a>.</p>

<p>Unless a page has <code>&lt;HTML&gt;</code> tag <em>ssg4</em> extracts its title from <code>&lt;H1&gt;</code>
tag, wraps the page with <code>_header.html</code>, <code>_footer.html</code>.</p>

<p>Then copies everything (excluding <code>.*</code>, <code>CVS</code>, and <code>_*</code>) from <code>src</code>
to <code>dst</code> directory.</p>

<p><a href="https://www.romanzolotarev.com/ssg4.png"><img src="https://www.romanzolotarev.com/ssg4.png" alt="ssg4" /></a>
<em>180 LoC. <a href="https://www.romanzolotarev.com/ssg4.png">Enlarge, enhance, zoom!</a></em></p>

<h2 id="Install">Install</h2>

<p>Download and chmod it:</p>

<pre>
$ <b>mkdir -p bin</b>
$ <b>ftp -Vo bin/ssg4 https://www.romanzolotarev.com/bin/ssg4</b>
ssg4       100% |*********************|    4916      00:00
$ <b>chmod +x bin/ssg4</b>
$ <b>doas pkg_add lowdown</b>
quirks-2.414 signed on 2018-03-28T14:24:37Z
lowdown-0.3.1: ok
$
</pre>

<p>lowdown(1) is optional. It&#39;s required only if there are
any <code>*.md</code> files.</p>

<h2 id="Usage">Usage</h2>

<pre>
$ <b>mkdir src dst</b>
$ <b>echo '# Hello, World!' > src/index.md</b>
$ <b>ftp -Vo src/_header.html https://www.romanzolotarev.com/raw/_header.html</b>
_header.html 100% |**************************|  3362       00:00
$ <b>ftp -Vo src/_footer.html https://www.romanzolotarev.com/raw/_footer.html</b>
_header.html 100% |**************************|   727       00:00
$ <b>ftp -Vo src/favicon.png https://www.romanzolotarev.com/raw/favicon.png</b>
favicon.png  100% |**************************|   408       00:00
$ <b>bin/ssg4 src dst 'Test' 'http://www'</b>
./index.md
./favicon.png
[ssg] 2 files, 1 url
$ <b>find dst</b>
dst
dst/.files
dst/index.html
dst/favicon.png
dst/sitemap.xml
$ <b>open dst/index.html</b>
</pre>

<h2 id="Markdown%20and%20HTML%20files">Markdown and HTML files</h2>

<p>HTML files from <code>src</code> have greater priority than Markdown ones.
<em>ssg4</em> converts Markdown files from <code>src</code> to HTML in <code>dst</code> and then
copies HTML files from <code>src</code> to <code>dst</code>. In the following example
<code>src/a.html</code> wins:</p>

<pre><code>src/a.md   -&gt; dst/a.html
src/a.html -&gt; dst/a.html
</code></pre>

<h2 id="Favicon">Favicon</h2>

<p>Make sure you have <code>/favicon.png</code> in place.</p>

<p>Some browsers fetch <code>/favicon.ico</code> despite what you specified in
the <code>&lt;LINK&gt;</code> tag, so you can use <a href="https://www.romanzolotarev.com/favicon.ico">an empty one</a> (180
bytes) as a placeholder.</p>

<h2 id="Sitemap">Sitemap</h2>

<p><em>ssg4</em> generates <code>sitemap.xml</code> with the list of all page.  Don&#39;t
forget to add absolute URL of the sitemap to your <code>robot.txt</code>.<br>For
example:</p>

<pre><code>user-agent: *
sitemap: https://www.romanzolotarev.com/sitemap.xml
</code></pre>

<h2 id="RSS">RSS</h2>

<p>To generate RSS feeds use <a href="https://www.romanzolotarev.com/rssg.html">rssg</a>, then add their URLs
to <code>_header.html</code>.<br>For example:</p>

<pre><code>&lt;link rel=&quot;alternate&quot; type=&quot;application/atom+xml&quot; href=&quot;/rss.xml&quot;&gt;
</code></pre>

<h2 id="Incremental%20updates">Incremental updates</h2>

<p>On every run <em>ssg4</em> saves a list of files in <code>dst/.files</code> and updates
only newer files. If no files were modified after that, <em>ssg4</em> does
nothing.</p>

<pre>
$ <b>bin/ssg4 src dst 'Test' 'https://www'</b>
[ssg] no files, 1 url
$
</pre>

<p>To force the update delete <code>dst/.files</code> and re-run <em>ssg4</em>.</p>

<pre>
$ <b>rm dst/.files</b>
$ <b>bin/ssg4 src dst 'Test' 'https://www'</b>
index.md
[ssg] 1 file, 1 url
$
</pre>

<h2 id="Watch">Watch</h2>

<p>Save this helper to <code>~/bin/sssg</code>. It re-runs <em>ssg4</em> with
<a href="http://entrproject.org">entr(1)</a> on every file change.</p>

<pre>
$ <b>cat $HOME/bin/sssg</b>
#!/bin/sh
while :
do
    find . -type f ! -path '*/.*' |
    entr -d "$HOME/bin/ssg4" . "$1" "$(date)" '//www'
done
$
</pre>

<p>Install entr(1):</p>

<pre>
$ <b>doas pkg_add entr</b>
quirks-2.414 signed on 2018-03-28T14:24:37Z
entr-4.0: ok
$
</pre>

<p>Start the helper and keep it running:</p>

<pre>
$ <b>~/bin/s /var/www/htdocs/www</b>
[ssg] 1 file, 1 url
</pre>

<h2 id="Upgrade">Upgrade</h2>

<p><em><a href="https://www.romanzolotarev.com/ssg3.html">Previous version of ssg</a> has been retired.</em></p>

<p>Add <code>&lt;!DOCTYPE html&gt;</code>, <code>&lt;STYLE&gt;...&lt;/STYLE&gt;</code> with your styles and
an empty <code>&lt;TITLE&gt;&lt;/TITLE&gt;</code> tags to <code>_header.html</code>.</p>

<p><em>ssg4</em> captures page&#39;s title from the first <code>&lt;H1&gt;</code> tag of the page
and inject it into <code>&lt;TITLE&gt;</code>, if it&#39;s present and empty.</p>

<p>Move <code>_rss.html</code> to <code>_header.html</code>, <code>_styles.css</code> to <code>&lt;STYLE&gt;</code> tag
in <code>_header.html</code>, and <code>_scripts.js</code> to <code>&lt;SCRIPT&gt;</code> tag.</p>

<table>
<thead>
<tr>
<th style="text-align: left"><em>ssg3</em></th>
<th style="text-align: left"><em>ssg4</em></th>
</tr>
</thead>

<tbody>
<tr>
<td style="text-align: left">Builds 1,730 files in <strong>8.54s</strong></td>
<td style="text-align: left">in <strong>5.43s</strong></td>
</tr>
<tr>
<td style="text-align: left"> </td>
<td style="text-align: left"></td>
</tr>
<tr>
<td style="text-align: left">Contains basic HTML tags.</td>
<td style="text-align: left">Contains no HTML tags.</td>
</tr>
<tr>
<td style="text-align: left">wc(1) is required.</td>
<td style="text-align: left">Doesn&#39;t use wc(1).</td>
</tr>
<tr>
<td style="text-align: left"> </td>
<td style="text-align: left"></td>
</tr>
<tr>
<td style="text-align: left">List of feeds read from <code>_rss.html</code>,</td>
<td style="text-align: left"><code>_rss.html</code>,</td>
</tr>
<tr>
<td style="text-align: left">styles from <code>_styles.css</code>, and</td>
<td style="text-align: left"><code>_styles.css</code>, and</td>
</tr>
<tr>
<td style="text-align: left">scripts from <code>_scripts.js</code>.</td>
<td style="text-align: left"><code>_scripts.js</code> have been removed.</td>
</tr>
</tbody>
</table>

<h2 id="Dependencies">Dependencies</h2>

<p><em>ssg4</em> depends on few programs from OpenBSD base:</p>

<pre>
$ <b>for f in $(which cat cpio date sh awk find grep printf readlink sort tee)</b>
<i><b>do ldd "$f"</b></i>
<i><b>done | awk '/\//{print$7}' | grep '.' | sort -u</b></i>
/bin/cat
/bin/cpio
/bin/date
/bin/sh
/usr/bin/awk
/usr/bin/find
/usr/bin/grep
/usr/bin/printf
/usr/bin/readlink
/usr/bin/sort
/usr/bin/tee
/usr/lib/libc.so.92.5
/usr/lib/libm.so.10.1
/usr/lib/libutil.so.13.0
/usr/lib/libz.so.5.0
/usr/libexec/ld.so
</pre>

<hr/>

<h2 id="Users">Users</h2>

<p><a href="https://blog.solobsd.org/">blog.solobsd.org</a><br>
<a href="https://www.bloguslibrus.fr">bloguslibrus.fr</a><br>
<a href="https://www.bsdjobs.com/">bsdjobs.com</a><br>
<a href="https://cryogenix.net">cryogenix.net</a><br>
<a href="https://www.dethronedemperor.com">dethronedemperor.com</a><br>
<a href="https://grosu.nl/">grosu.nl</a><br>
<a href="https://h3artbl33d.nl/">h3artbl33d.nl</a><br>
<a href="https://high5.nl/">high5.nl</a><br>
<a href="https://matthewgraybosch.com/">matthewgraybosch.com</a><br>
<a href="https://mvidal.net/">mvidal.net</a><br>
<a href="https://openbsd.amsterdam/?rz">openbsd.amsterdam</a><br>
<a href="https://openbsd.space/">openbsd.space</a><br>
<a href="https://www.romanzolotarev.com/">romanzolotarev.com</a> &mdash; obviously ;)<br>
<a href="https://runbsd.info/">runbsd.info</a><br>
<a href="https://www.stockersolutions.com/">stockersolutions.com</a><br></p>

<hr/>

<p><strong>Thanks</strong> to
<a href="https://twitter.com/freebsdfrau/status/1075797843460288512">Devin Teske</a>
for helping with awk(1),
<a href="https://www.divelog.blue/">Kristaps Dzonsons</a> for
<a href="https://kristaps.bsd.lv/lowdown/">lowdown(1)</a>, and
<a href="http://eradman.com">Eric Radman</a> for
<a href="http://entrproject.org">entr(1)</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/rssg.html</guid>
<link>https://www.romanzolotarev.com/rssg.html</link>
<pubDate>Fri, 21 Sep 2018 00:00:00 +0000</pubDate>
<title>Generate RSS feeds with grep(1), sed(1), and awk(1)</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and 6.4</em></p>

<h1 id="Generate%20RSS%20feeds%20with%20grep(1),%20sed(1),%20and%20awk(1)">Generate RSS feeds with grep(1), sed(1), and awk(1)</h1>

<p><a href="https://www.romanzolotarev.com/bin/rssg">rssg</a> is an RSS feed generator written in shell. It&#39;s
a good companion for <a href="https://www.romanzolotarev.com/ssg.html">ssg</a>.</p>

<p>It gets feed&#39;s description, URL, and the list of items
from an index file.</p>

<p>Then for every item it extracts its title and treats the rest of
the file as its description, replacing all relative URLs with
absolute ones.</p>

<p>An index file can be in HTML or in Markdown format.</p>

<p>Finally, <em>rssg</em> outputs the feed in XML format.</p>

<p><a href="https://www.romanzolotarev.com/rssg.png"><img src="https://www.romanzolotarev.com/rssg.jpeg" alt="rssg" /></a>
<em>148 LoC. <a href="https://www.romanzolotarev.com/rssg.png">grep and sed everything</a></em></p>

<h2 id="Install">Install</h2>

<p>Download and chmod it:</p>

<pre>
$ <b>ftp -Vo bin/rssg https://www.romanzolotarev.com/bin/rssg</b>
rssg       100% |*********************|    4137      00:00
$ <b>chmod +x bin/rssg</b>
$ <b>doas pkg_add lowdown</b>
quirks-2.414 signed on 2018-03-28T14:24:37Z
lowdown-0.3.1: ok
$
</pre>

<h2 id="Usage">Usage</h2>

<pre>
$ <b>rssg index.html 'title' &gt; rss.xml</b>
$
</pre>

<p>Here is an example of a minimal <code>index.html</code> file:</p>

<pre>
&lt;h1&gt;<b>People who run BSD</b>&lt;/h1&gt;

&lt;p&gt;<em>
Stories written by users of BSD operating systems.
Hosted by Roman Zolotarev.
</em>&lt;/p&gt;

&lt;p&gt;
Subscribe via &lt;a href="<b>https://bsdjobs.com/people/rss.xml</b>"&gt;RSS&lt;/a&gt;.
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="<b>mischapeters.html</b>" title="<b>2018-08-07</b>"&gt;<b>Mischa Peters</b>&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="<b>h3artbl33d.html</b>" title="<b>2018-08-06</b>"&gt;<b>h3artbl33d</b>&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</pre>

<p>HTML parsing rules:</p>

<ul>
<li><code>&lt;H1&gt;</code> and <code>&lt;A&gt;</code> tags should not contain line breaks.</li>
<li><strong>title</strong> is the first <code>&lt;H1&gt;</code> tag.</li>
<li><strong>description</strong> is the first <code>&lt;P&gt;</code> tag.<br>
All <code>&lt;H1&gt;</code> tags are excluded from description.</li>
<li><strong>url</strong> is <code>HREF</code> attribute of the first <code>&lt;A&gt;</code> tag
with <code>RSS</code> content.<br></li>
<li><strong>items</strong> are lines with <code>&lt;A&gt;</code> tags and <code>TITLE</code> attribute.</li>
<li><strong>item file</strong> is <code>HREF</code> attribute of that line, <strong>item date</strong> is
<code>TITLE</code> attribute, and <strong>item title</strong> is the content of <code>&lt;A&gt;</code> tag.</li>
</ul>

<hr/>

<p><strong>Thanks</strong> to <a href="https://twitter.com/devinteske">Devin Teske</a> for
<a href="https://twitter.com/freebsdfrau/status/1042076552400265219">her awk(1)
wizardry</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/borg.html</guid>
<link>https://www.romanzolotarev.com/borg.html</link>
<pubDate>Fri, 02 Mar 2018 00:00:00 +0000</pubDate>
<title>Archive with borg(1)</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 with borgbackup-1.1.4p0</em></p>

<h1 id="Archive%20with%20borg(1)">Archive with borg(1)</h1>

<p><a href="https://www.borgbackup.org/">Borg</a> is a deduplicating archiver
with compression and encryption.</p>

<h2 id="Install">Install</h2>

<pre>
# <b>pkg_add borgbackup</b>
...
borgbackup-1.1.4p0: ok
#
</pre>

<h2 id="Initialize">Initialize</h2>

<pre>
$ <b>borg init -e repokey -v ~/archives</b>
Initializing repository at "archives"
Enter new passphrase:
Enter same passphrase again:
Do you want your passphrase to be displayed for verification? [yN]:
Remember your passphrase. Your data will be inaccessible without it.
Key in "&lt;Repository /home/romanzolotarev/archives&gt;" created.
Keep this key safe. Your data will be inaccessible without it.
Synchronizing chunks cache...
Archives: 0, w/ cached Idx: 0, w/ outdated Idx: 0, w/o cached Idx: 0.
Done.
$
</pre>

<h2 id="Back%20up">Back up</h2>

<pre>
$ <b>borg create ~/archives::src-20180626-1304 src</b>
Enter passphrase for key /home/romanzolotarev/archives:
$
</pre>

<h2 id="Restore">Restore</h2>

<pre>
$ <b>cd /tmp</b>
$ <b>borg extract -v --list ~/archives::src-20180626-1304 src/www/borg.md</b>
Enter passphrase for key /home/romanzolotarev/archives:
src/www/borg.md
$ <b>sha256 /tmp/src/www/borg.md ~/src/www/borg.md</b>
SHA256 (/tmp/src/www/borg.md) = 66d23...
SHA256 (/home/romanzolotarev/src/www/borg.md) = 66d23...
$
</pre>

<h2 id="Set%20up%20a%20remote%20storage">Set up a remote storage</h2>

<p>To keep archives off-site <a href="https://www.romanzolotarev.com/openbsd/">set up your own server</a> or
<a href="https://www.rsync.net/products/attic.html">sign up for Rsync.net</a>
($10/year for 40 GB or $0.24/year/GB).</p>

<h2 id="Automate">Automate</h2>

<p>Create <code>~/bin/borgbackup</code> script like this:</p>

<pre>
#!/bin/sh
export BORG_REPO='<b>USERNAME@SERVER.rsync.net:REPO</b>'
export BORG_RSH="ssh -i <b>$HOME/.ssh/PUBLIC_KEY</b>"
export BORG_REMOTE_PATH='/usr/local/bin/borg1/borg1'
export BORG_PASSPHRASE='<b>PASSPHRASE</b>'

archive=$(date +%Y%m%d-%H%M)
borg create -C lzma,9 -p \
    --exclude <b>'*/.cache/*'</b> \
    --exclude <b>'*/Downloads/*'</b> \
    "::$archive" <b>"$HOME"</b>

borg prune -v --list --stats \
    --keep-hourly 48 \
    --keep-daily 60 \
    --keep-monthly 12 \
    --keep-yearly 10 \
    '::'
</pre>

<pre>
$ <b>chmod 0700 ~/bin/borgbackup</b>
$ <b>~/bin/borgbackup</b>
...
------------------------------------------------------------------------------
Keeping archive: 20180726-1352                        Thu, 2018-07-26 13:05:15
...
Keeping archive: 20180331-1505                        Sat, 2018-03-31 12:05:37
------------------------------------------------------------------------------
                       Original size      Compressed size    Deduplicated size
Deleted data:                    0 B                  0 B                  0 B
All archives:               66.93 GB             66.65 GB              7.38 GB

                       Unique chunks         Total chunks
Chunk index:                   34152               185692
------------------------------------------------------------------------------
Remote: Starting repository check
Remote: Starting repository index check
Remote: Completed repository check, no problems found.
Starting archive consistency check...
Analyzing archive 20180331-1505 (1/10)
...
Analyzing archive 20180726-1305 (10/10)
Archive consistency check complete, no problems found.
$
</pre>

<p>Use <a href="https://man.openbsd.org/crontab.1">crontab(1)</a> to run
<code>~/bin/borgbackup</code> by schedule.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/macos/cups.html</guid>
<link>https://www.romanzolotarev.com/macos/cups.html</link>
<pubDate>Tue, 27 Feb 2018 00:00:00 +0000</pubDate>
<title>Print with cups(1) on macOS</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/macos/">macOS</a> 10.13.</em></p>

<h1 id="Print%20with%20cups(1)%20on%20macOS">Print with cups(1) on macOS</h1>

<p>Add a printer:</p>

<pre>
# <b>lpadmin -E \
-p Printer \
-v ipp://192.168.1.10/ipp/print \
-P Vendor-Model.ppd</b>
#
</pre>

<p><em>-E</em> enables accepting job<br>
<em>-p</em> an arbitrary printer name<br>
<em>-v</em> the device URI<br>
<em>-P</em> a path to the PPD file<br></p>

<p>Check available printers:</p>

<pre>
$ <b>lpstat -a</b>
Printer accepting requests since Sat Mar 31 23:59:32 2018
$
</pre>

<p>Print a document:</p>

<pre>
$ <b>lp -d Printer document.pdf</b>
$
</pre>

<p>Delete the printer:</p>

<pre>
# <b>lpadmin -x Printer</b>
#
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/macos/openbsd-installer.html</guid>
<link>https://www.romanzolotarev.com/macos/openbsd-installer.html</link>
<pubDate>Tue, 19 Sep 2017 00:00:00 +0000</pubDate>
<title>Prepare bootable USB drive with OpenBSD installer on macOS</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/macos/">macOS</a> 10.13 with <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3</em></p>

<h1 id="Prepare%20bootable%20USB%20drive%20with%20OpenBSD%20installer%20on%20macOS">Prepare bootable USB drive with OpenBSD installer on macOS</h1>

<p>Download the installer and verify its checksum:</p>

<pre>
$ <b>cd /tmp</b>
$ <b>export URL=https://cloudflare.cdn.openbsd.org/pub/OpenBSD</b>
$ <b>curl -Os $URL/6.3/amd64/SHA256</b>
$ <b>curl -O  $URL/6.3/amd64/install63.fs</b>
...
$ <b>grep install63.fs SHA256|cut -f4 -d' '</b>
df19266be16079ccd6114447f7bb13bdedb9c5cb66ecc1ea98544290fa4dc138
$ <b>shasum -a 256 install63.fs|cut -f1 -d' '</b>
df19266be16079ccd6114447f7bb13bdedb9c5cb66ecc1ea98544290fa4dc138
$
</pre>

<p>Plug in the USB drive. Its size should be at least 400 MB. Run
<code>diskutil list</code> to find an identifier of the flash drive. Usually
it&#39;s <code>/dev/disk2</code>.</p>

<p>Replace <code>/dev/diskX</code> with the identifier of the flash drive.<br>
<strong>All data on <code>/dev/diskX</code> will be erased!</strong></p>

<pre>
$ <b>sudo diskutil unmount /dev/diskX</b>
$ <b>sudo dd if=install63.fs of=/dev/diskX bs=1m</b>
...
$
</pre>

<p>Wait few minutes.</p>

<p><a href="https://www.romanzolotarev.com/openbsd/install.html">Install OpenBSD</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/vim.html</guid>
<link>https://www.romanzolotarev.com/vim.html</link>
<pubDate>Sat, 26 Aug 2017 00:00:00 +0000</pubDate>
<title>Edit text with Vim</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/macos/">macOS</a> 10.13</em></p>

<h1 id="Edit%20text%20with%20Vim">Edit text with Vim</h1>

<p>Vim has everything you may need for text editing out-of-the-box:
syntax highlighting, autocompletion, split screen, diff, spell
checking, text formatting, text objects, persistent undo, automatic
commands, macros, scripting, and many more.</p>

<p>Most of Vim features are kind of hidden and it makes sense, nothing
stands on your way when you are working. Although, such minimalism
makes learning curve a bit steeper.</p>

<p>On the other hand, everything in Vim is thoroughly documented and
the documentation is always available via <code>:help</code> command.</p>

<h2 id="Vim%20is%20keyboard-based">Vim is keyboard-based</h2>

<p>Usual Vim command is a sequence of keystrokes. You rarely use key
chords in Vim and you don&#39;t need a mouse.</p>

<p>You can effortlessly make large edits <a href="https://www.vimgolf.com">with few
keystrokes</a>, because Vim has commands,
movements, and can operate text objects: paragraphs, sentences,
words, etc.  Everything is mnemonic. For example, you can type <code>dap</code>
in normal mode to <em>&quot;delete a paragraph&quot;</em>, and <code>ciw</code> to <em>&quot;change in
word&quot;</em>, and so on.</p>

<h2 id="Vim%20is%20extensible">Vim is extensible</h2>

<p>Vim can do a lot without any customization or plugins, but you can
tailor Vim for your needs. There are tons of plugins for Vim (you
may need just few), you can install and update them with ease.</p>

<p>Vim is well integrated with command line tools (<code>grep</code>, <code>git</code>,
<code>head</code>, <code>tail</code>, <code>tee</code>, <code>find</code>, <code>cp</code>, <code>mv</code>, <code>rm</code>, <code>cut</code>, <code>uniq</code>,
<code>sort</code>, etc). With pipes and redirects you can compose programs to
do any text manipulations you may need.</p>

<h2 id="Vim%20is%20fast">Vim is fast</h2>

<p>Vim doesn&#39;t need a lot of resources <a href="https://github.com/jhallen/joes-sandbox/tree/master/editor-perf">to perform
well</a>.
Its startup time is <strong>under six milliseconds</strong>:</p>

<pre>
$ <b>vim -u NONE --startuptime vim.log +q</b>
$ <b>tail -n 1 vim.log | cut -f1 -d' '</b>
005.287
$
</pre>

<p>To be fair, to start Vim with a dozen of plugins and to open a file
takes about 60 ms. Still impressive.</p>

<p>Vim almost never lags and typing latency is very low.
<a href="https://github.com/pavelfatin/typometer">Typometer</a> shows 4.8 ms
for Vim in Terminal.app on macOS.</p>

<p>Even when you are editing files on a remote machine over SSH and
the internet connection is slow, you still are in control, because
in Vim you can do a lot with a single command: quick jumps inside
a large file, switching among multiple files, searching and replacing
across all files in a project, etc.</p>

<p>Yes, Vim works perfectly in a terminal over SSH. You can edit files
remotely and be as productive as on your computer.</p>

<h2 id="Vim%20is%20future-proof">Vim is future-proof</h2>

<p>Vim (or its predecessor vi) is pre-installed on all Unix-like
machines (macOS, BSD, Linux). Vim has been ported to almost all
operating systems, including Windows.</p>

<p>Vim is a free and open-source software. It&#39;s a time-tested software:
vi released in 1976, Vim &mdash; in 1991. Vim will stay around for
a long time.  Once you learned Vim you may use it for decades. You
workflow may change, programming languages come and go, but Vim
always does its job well.</p>

<p>Still undecided? Try Vim for a few days.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/macos/security.html</guid>
<link>https://www.romanzolotarev.com/macos/security.html</link>
<pubDate>Tue, 16 May 2017 00:00:00 +0000</pubDate>
<title>Manage passwords with security(1) on macOS</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/macos/">macOS</a> 10.13</em></p>

<h1 id="Manage%20passwords%20with%20security(1)%20on%20macOS">Manage passwords with security(1) on macOS</h1>

<p>Add a password to the default keychain:</p>

<pre>
$ <b>security add-generic-password -a ${USER} -s NAME -w</b>
</pre>

<p>Retrieve the password:</p>

<pre>
$ <b>security find-generic-password -a ${USER} -s NAME -w</b>
</pre>

<p>Delete the password:</p>

<pre>
$ <b>security delete-generic-password -a ${USER} -s NAME</b>
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/github.html</guid>
<link>https://www.romanzolotarev.com/github.html</link>
<pubDate>Sun, 16 Apr 2017 00:00:00 +0000</pubDate>
<title>Host repositories on GitHub</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/openbsd/">OpenBSD</a> 6.3 and <a href="https://www.romanzolotarev.com/macos/">macOS</a> 10.13</em></p>

<h1 id="Host%20repositories%20on%20GitHub">Host repositories on GitHub</h1>

<p><a href="https://www.romanzolotarev.com/ssh.html">Generate a new SSH key pair</a> and copy public key to
<a href="https://www.romanzolotarev.com/xclip.html">the clipboard</a>.</p>

<pre>
$ <b>xclip -selection clipboad ~/.ssh/id_ed25519.pub</b>
$
</pre>

<p>Open your <a href="https://github.com/settings/keys">GitHub profile</a>.</p>

<p>On <em>Personal Settings &gt; SSH and GPG keys</em> page click <strong>New SSH key</strong>.</p>

<p>Type-in a <strong>Title</strong>, for example, a hostname of your computer.</p>

<p>Paste your public key into <strong>Key</strong> textarea.</p>

<p>Click <strong>Add SSH key</strong>.</p>

<p>Expect email <em>A new public key was added to your account</em> from GitHub.</p>

<pre>
$ <b>ssh -T git@github.com</b>
Hi romanzolotarev! You've successfully authenticated, but GitHub
does not provide shell access.
$
</pre>

<p>GitHub makes your public key available via HTTPS.</p>

<pre>
$ <b>curl https://github.com/romanzolotarev.keys</b>
ssh-ed25519 AAAAC3NzaC...
$
</pre>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/jekyll.html</guid>
<link>https://www.romanzolotarev.com/jekyll.html</link>
<pubDate>Tue, 22 Nov 2016 00:00:00 +0000</pubDate>
<title>Make a static website with Jekyll</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/macos/">macOS</a> 10.13 with ruby and jekyll</em></p>

<h1 id="Make%20a%20static%20website%20with%20Jekyll">Make a static website with Jekyll</h1>

<p><a href="https://jekyllrb.com">Jekyll</a> is a static website generator. It converts your
<a href="https://www.romanzolotarev.com/markdown.html">markdown</a> files to web pages which you can publish
with a single command or few clicks.</p>

<p>If you are on a Mac, both Ruby and Jekyll are pre-installed.</p>

<p>Create a new directory for your project and run Jekyll.</p>

<pre>
$ <b>mkdir hello-world</b>
$ <b>cd hello-world</b>
$ <b>jekyll s</b>
$
</pre>

<p>Create an <code>index.md</code> file:</p>

<pre><code>---
---
# Hello, World!
</code></pre>

<p>Those dashes separate the Jekyll <em>front matter</em> from your Markdown
content. Jekyll needs front matter even if it&#39;s empty.</p>

<p>Open <a href="http://127.0.0.1:4000">http://127.0.0.1:4000</a> in a web browser.</p>

<p>Edit <code>index.md</code>. Jekyll detects changes and regenerates all files. To see
changes, reload the web page.</p>

<p>Add more files if needed.</p>

<h2 id="Publish">Publish</h2>

<p>One of the simplest ways to publish is to push your project to GitHub. It
will take care of running Jekyll to regenerate all files on push.</p>

<p>You can also publish your site on any web server. By default, Jekyll
renders all files to the <code>_site</code> directory, so just copy those files and
you&#39;re good to go. No dependencies, just static HTML files.</p>

<h2 id="Layout%20(optional)">Layout (optional)</h2>

<p>When you have multiple files and want to style your web pages, it makes
sense to create a layout.</p>

<p>To add a new layout:</p>

<ul>
<li>Choose any layout name, say, <code>default</code>.</li>
<li>Create a file <code>default.html</code> in the <code>_layouts</code> directory.</li>
<li>Add any HTML you want and include the <code>{% raw %}{{content}}{% endraw %}</code>
tag.</li>
</ul>

<h2 id="Live%20reload%20(advanced)">Live reload (advanced)</h2>

<p>When I&#39;m editing wordy pages or tweaking layouts I use the
<code>jekyll-livereload</code> plugin. It triggers page reload every time I save a
file.</p>

<p><code>jekyll-reload</code> injects JavaScript code into <code>&lt;head&gt;</code>. So <strong>make sure you
have a <code>&lt;head&gt;</code> tag in your layout.</strong></p>

<p>Here&#39;s how to enable live reload:</p>

<p>Install Bundler:</p>

<pre>
$ <b>gem install bundler</b>
</pre>

<p>Add <code>Gemfile</code>:</p>

<pre><code>source &#39;https://rubygems.org&#39;
gem &#39;jekyll&#39;, group: :jekyll_plugins
group &#39;jekyll_plugins&#39; do
  gem &#39;jekyll-livereload&#39;
end
</code></pre>

<p>Install and run:</p>

<pre>
$ <b>bundle install</b>
$ <b>bundle exec jekyll serve -L</b>
</pre>

<h2 id="See%20also">See also</h2>

<p><a href="https://romanzolotarev.github.io/jekyll-minimalist/">Jekyll Minimalist</a>,
<a href="https://pages.github.com">GitHub Pages</a></p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/typing.html</guid>
<link>https://www.romanzolotarev.com/typing.html</link>
<pubDate>Sat, 19 Nov 2016 00:00:00 +0000</pubDate>
<title>Touch typing</title>
<description><![CDATA[

<h1 id="Touch%20typing">Touch typing</h1>

<p>Hi, my name is Roman, and I am addicted to tools.</p>

<p>Do you type for hours every day like I do? Then let me share a year-long
journey to <del>the holy grail</del> ergonomic typing. Touch typing can change
your life (in a good way). If you&#39;re looking for a new keyboard or
considering learning an alternative layout, you should definitely keep
reading.</p>

<h2 id="Long%20story%20short">Long story short</h2>

<p>I &quot;touch typed&quot; with seven fingers for decades and managed to type at
average speed with pretty poor accuracy. I tried so many times to learn
proper technique, but I always gave up in a week. A year ago I decided
it was now or never. I had only one goal: to type faster, like 100 WPM.
<em>Spoiler: I&#39;m not there yet</em>. Back then I was using QWERTY on an Apple
Wireless keyboard.</p>

<p>This was my method: &quot;Okay, roll up your sleeves, and just memorize every
key for all ten fingers.&quot; It took two days. I memorized all the keys
during that weekend. Typing speed plummeted to 10 WPM on the first day;
I was &quot;thinking&quot; before pressing almost every key. I could type with
100% accuracy without looking, but it was very very slow.</p>

<p>Next phase: typing lessons every day for an hour. From time to time I
typed with my seven (favorite) fingers when I needed to get work done,
but in a few days I switched to the ten-finger method cold turkey. I
continued practicing every day at <a href="https://www.keybr.com/">Keybr</a> and <a href="https://www.keyhero.com/free-typing-test/">Keyhero</a>
and finally reached 40 WPM in two weeks.</p>

<p>Phew...</p>

<p>As soon as I returned to my original speed and accuracy, I decided to
upgrade my keyboard to an ergonomic one. Two months later <a href="https://www.romanzolotarev.com/ergodox.html">ErgoDox</a>
arrived. And of course, I wanted to try an alternative keyboard layout
on it. After a quick analysis, I picked the Norman layout: it&#39;s easy to
learn, and it significantly reduces distance traveled from the home row.</p>

<p>Relearning. My speed dropped to 10 WPM, and recovered to 40 WPM in three
weeks. I switched to the ErgoDox full time and played with ErgoDox
layers, ending up with a single-layer solution. I reached 50 WPM in 40
days and stopped all typing lessons.</p>

<p>Today I am a happy ErgoDox user. My average speed on Norman is 60 WPM,
and it&#39;s increasing every month, slowly but steadily.</p>

<h2 id="Lessons%20learned">Lessons learned</h2>

<p>Fast and accurate typing is a must-have skill for a programmer. I wish
I&#39;d switched to the right path earlier.</p>

<p>I am not the fastest typist, just a bit quicker than average. My
achievements in terms of words per minute are very humble. Still, I can
say the return on investment is overwhelmingly high.</p>

<h2 id="Proper%20typing%20style">Proper typing style</h2>

<p>Better typing speed saves me a few hours every week on code
documentation, notes, and emails. Fast typing enables blogging: you can
find an hour for a draft, but it is harder to find two. So a difference
of 20 WPM can affect your productivity quite significantly.</p>

<p>If you are an average typist, you should invest few minutes a day in
typing lessons. Focus on accuracy and practice every day. Totally worth
it.</p>

<h2 id="Ergonomic%20keyboard">Ergonomic keyboard</h2>

<p>If you have a Mac, there&#39;s a good chance that your keyboard is
excellent.  Apple keyboards are robust, compact, and quiet.</p>

<p>ErgoDox is louder and bigger. The primary benefit of ErgoDox is
ergonomics. (Surprise!) As with any split keyboard, you can sit (or
stand) straight, so your posture is healthier, and it&#39;s simply more
relaxing.</p>

<p>My initial goal was just to improve my typing speed. Of course,
high speed is necessary, but when I started using ErgoDox, I realized
that comfort is the primary reward: my hands, shoulders, and back
are much happier now.  Yes, ErgoDox costs three hundred dollars,
but it&#39;s the best keyboard available today for that price, and the
keyboard is the most important part of my workplace. If you&#39;re
typing all day long, you can connect your awesome keyboard to any
cheap computer and be productive in no time.</p>

<h2 id="Alternative%20keyboard%20layout">Alternative keyboard layout</h2>

<p>I use Norman and like it better than QWERTY. I haven&#39;t tried any other
layouts, and honestly, I&#39;m not sure that learning alternative layouts is
worth it. What I am 100% sure of is that I can learn any keyboard layout
and be productive in two weeks. Layouts and keyboards do not limit my
speed; my fingers can move faster than I can compose words in English.</p>

<p>Why not stick with QWERTY on all my keyboards? Switching to a different
layout and keyboard helped me break my bad typing habits. It&#39;s easier to
learn correct technique from scratch on an entirely new instrument than
it is to to fix those bad habits deeply wired into your brain.</p>

<p><strong>How did I choose Norman?</strong> Two sources. First, people who already who
use multiple layouts for years: <a href="https://twitter.com/search?q=from%3Agarybernhardt%20norman">Gary Bernhardt</a>, <a href="https://twitter.com/search?q=from%3Atenderlove%20norman">Aaron
Patterson</a>, a good review by <a href="http://teds.space/posts/ive-been-typing-for-little-while">Ted</a>. Second, I compared
Dvorak, Colemak, Workman, Norman, and even my custom layout with <a href="http://patorjk.com/keyboard-layout-analyzer/">a
keyboard layout analyzer</a> (made by Patrick Gillespie). Norman
performed slightly better than the others on my custom corpus of text.</p>

<p>One more reason I picked Norman: It&#39;s easy to switch to QWERTY and back
to Norman in few minutes. (I use QWERTY when I travel.) Norman is just a
fifteen-key difference from QWERTY.</p>

<p><img src="https://www.romanzolotarev.com/typing-norman.png" alt="Norman layout on ErgoDox" /></p>

<p>I use Vim with both QWERTY and Norman, and I do not remap anything in
Vim.  In the beginning, I had one annoying issue: <code>HJKL</code> on Norman are
in weird locations. I was hitting <code>U</code> instead of <code>J</code>. That was such a
painful week.  Now everything is just fine.</p>

<p>Looking back, I am not entirely sure if all these layouts made any
difference in my case. I can learn any crazy layout and reach an average
speed in few weeks, but it won&#39;t increase my speed beyond that level.
Maybe I should try QWERTY on ErgoDox someday.</p>

<h2 id="Level%20up">Level up</h2>

<p>If you need to type at high speed for hours at a time, then you should
probably follow Mirabai Knight and learn <a href="http://www.openstenoproject.org">stenography</a>. Beware:
the learning curve for steno is steep and you need a steno machine (or
for the first time you can use NKRO keyboard, e.g., ErgoDox).</p>

<h2 id="Conclusion">Conclusion</h2>

<p>Typing can be fun if you fix your bad typing habits. The earlier you
start learning, the more rewarding it can be. Teach kids to touch type
as soon as they start playing with the computer. This skill will stay
relevant for at least one more generation.</p>

<p>If you want to take away only one thing from my story, it&#39;s this: Learn
proper touch typing technique today.</p>

<h2 id="Recipe">Recipe</h2>

<p>Take a typing test on <a href="https://www.keyhero.com/free-typing-test/">Keyhero</a>. Slower than 40 WPM? Practice
15 minutes every day for a month. If your accuracy is lower than 95%,
slow down and try to type as accurately as possible.</p>

<p>If you type more than four hours every day, you should use an ergonomic
keyboard.</p>

<h2 id="See%20also">See also</h2>

<p><a href="https://pavelfatin.com/typing-with-pleasure/#input-latency">Typing with pleasure by Pavel Fating</a>,
<a href="http://steve-yegge.blogspot.sg/2008/09/programmings-dirtiest-little-secret.html">Programming&#39;s dirtiest little secret by Steve Yegge</a>,
<a href="https://normanlayout.info/">Norman layout by David Norman</a>,
<a href="https://github.com/qmk/qmk_firmware/tree/master/layouts/community/ergodox/romanzolotarev-norman-osx">My keyboard layout on GitHub</a></p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/screencasts.html</guid>
<link>https://www.romanzolotarev.com/screencasts.html</link>
<pubDate>Tue, 25 Oct 2016 00:00:00 +0000</pubDate>
<title>Screencasts for programmers</title>
<description><![CDATA[

<h1 id="Screencasts%20for%20programmers">Screencasts for programmers</h1>

<p>Looking for top-notch screencasts? You are on the right page.</p>

<p>I like watching screencasts better than reading books or documentation.
Don&#39;t get me wrong, I love books and manual pages.</p>

<p>When it comes to learning a new programming language, to grasping
complex abstractions, I prefer to start with screencasts. It&#39;s easier for
me to learn from concrete examples narrated by an experienced programmer.
Learning should not be mind-bending all the time.</p>

<p>Another thing I appreciate about screencasts is that they give me a chance
to see industry leaders working with their tools. By looking at someone
else&#39;s screen you can make accidental discoveries and learn about tools
and techniques you never knew existed, one little thing at a time.</p>

<p>All the screencasts listed on this page are well made and cover everything
you need to know as a professional programmer: from command line tools and
text editors to advanced programming practices and lambda calculus.</p>

<p><a href="https://www.destroyallsoftware.com/screencasts"><strong>Destroy All Software</strong></a>
by Gary Bernhardt
<br>computation, OOP, Unix, Rails, Python, TDD, Vim</p>

<p><a href="https://egghead.io/instructors/brian-lonsdorf"><strong>Professor Frisby Introduces Composable Functional JavaScript</strong></a>
by Brian Lonsdorf
<br>JavaScript, FP</p>

<p><a href="https://pragmaticstudio.com/elm"><strong>Building Web Apps with Elm</strong></a>
by Mike and Nicole Clark
<br>Elm</p>

<p><a href="https://pragmaticstudio.com/elixir"><strong>Developing With Elixir/OTP</strong></a>
by Mike and Nicole Clark
<br>Elixir</p>

<p><a href="http://derekwyatt.org/vim/tutorials/"><strong>Vim Videos</strong></a>
by Derek Wyatt
<br>Vim</p>

<p><a href="http://vimcasts.org/episodes/archive/"><strong>Vim Casts</strong></a>
by Drew Neil
<br>Vim, VimScript, Unix</p>

<p><a href="https://www.youtube.com/watch?v=h_tkIpwbsxY&amp;list=PLK_hdtAJ4KqX0JOs_KMAmUNTNMRYhWEaC"><strong>Classroom Coding with Prof. Frisby</strong></a>
by Brian Lonsdorf
<br>JavaScript, FP</p>

<p><a href="https://www.pluralsight.com/courses/hardcore-functional-programming-javascript"><strong>Hardcore Functional Programming in JavaScript</strong></a>
by Brian Lonsdorf
<br>JavaScript, FP</p>

<p><a href="http://elmseeds.thaterikperson.com/"><strong>Elmseeds</strong></a>
by Erik Person
<br>Elm</p>

<p><a href="https://www.dailydrip.com/"><strong>DailyDrips</strong></a>
by Josh Adams
<br>Elixir, Elm, HTML, CSS, Ember, React Native, R</p>

<p><a href="https://www.youtube.com/user/gruen0"><strong>ElmLive</strong></a>
by Aaron VonderHaar
<br>Elm</p>

<p><a href="https://knowthen.com/"><strong>KnowThen</strong></a>
by James Moore
<br>Elm, React, Go, RethinkDB</p>

<p><a href="https://www.learnelixir.tv/"><strong>Learn Elixir</strong></a>
by Daniel Berkompas
<br>Elixir</p>

<p><a href="https://www.learnphoenix.tv/"><strong>Learn Phoenix</strong></a>
by Daniel Berkompas
<br>Elixir, Phoenix</p>

<p><a href="https://bigmachine.io/products/take-off-with-elixir/"><strong>Take off with Elixir</strong></a>
by Rob Conery
<br>Elixir, Phoenix</p>

<p><a href="https://lambdaisland.com/"><strong>Lambda Island</strong></a>
by Arne Brasseur
<br>Clojure, Emacs</p>

<p><a href="https://killring.org/hack-emacs/"><strong>Hack Emacs</strong></a>
by Rick Dillon
<br>Emacs</p>

<p><a href="https://reactforbeginners.com/"><strong>React for Beginners</strong></a>
by Wes Bos
<br>React, Firebase</p>

<p><a href="https://egghead.io/instructors/dan-abramov"><strong>Redux</strong></a>
by Dan Abramov
<br>React, Redux</p>

<p><a href="http://peertopeer.io/"><strong>Peer to Peer</strong></a>
by Drew Neil
<br>Ruby, JavaScript, Haskell</p>

<p><a href="https://www.eventedmind.com/"><strong>Evented Mind</strong></a>
by Chris Mather
<br>JavaScript, React, Meteor, Node.js, git</p>

<p><a href="https://mijingo.com/"><strong>Mijingo</strong></a>
by Ryan Irelan
<br>CMS, Git, HTML, CSS, Python, Jekyll</p>

<p><a href="https://learn.cloudcannon.com/"><strong>Jekyll Tips</strong></a>
by CloudCannon
<br>Jekyll, MacOS</p>

<p><a href="https://sysadmincasts.com/"><strong>Sysadmin Casts</strong></a>
by Justin Weissig
<br>AWS, Unix, Vagrant, Ansible, Puppet</p>

<p><a href="https://css-tricks.com/video-screencasts/"><strong>CSS Tricks</strong></a>
by Chris Coyier
<br>CSS</p>

<p><a href="http://www.letscodejavascript.com/"><strong>Test-Driven JavaScript</strong></a>
by James Shore
<br>JavaScript</p>

<p><a href="https://www.youtube.com/playlist?list=PLxj9UAX4Em-IiOfvF2Qs742LxEK4owSkr"><strong>.Emacs Tutorials</strong></a>
by Chris Forno
<br>Emacs</p>

<p><a href="http://emacsrocks.com/"><strong>Emacs Rocks</strong></a>
by Magnar Sveen
<br>Emacs</p>

<p><a href="http://www.parens-of-the-dead.com/"><strong>Parens of the dead</strong></a>
by Magnar Sveen
<br>Clojure</p>

<p><a href="https://gorails.com/"><strong>GoRails</strong></a>
by Chris Oliver
<br>Rails</p>

<p><a href="https://www.rubytapas.com/"><strong>RubyTapas</strong></a>
by Avdi Grimm
<br>Ruby</p>

<p><a href="https://training.talkpython.fm/courses/bundle/2017-annual-bundle"><strong>Talk Python Training</strong></a>
by Michael Kennedy
<br>Python, MongoDB, Pyramid, REST, SQL</p>

<p><a href="http://nsscreencast.com/episodes"><strong>NSScreencast</strong></a>
by Ben Scheirman
<br>iOS, Swift, Objective-C, Xcode</p>

<p><a href="https://www.leveluptutorials.com/"><strong>Level Up Tutorials</strong></a>
by Scott Tolinski
<br>Meteor, React, Angular, Drupal, WordPress, Magento, Sketch, Sass, Stylus, PostCSS, Java</p>

<p><a href="http://www.neckbeardrepublic.com/"><strong>Neckbeard Republic</strong></a>
by Mahdi Yusuf
<br>Python</p>

<p><a href="https://www.emberschool.com/"><strong>EmberSchool</strong></a>
by Jeffrey Biles
<br>Ember</p>

<p><a href="https://www.emberscreencasts.com/"><strong>EmberScreencasts</strong></a>
by Jeffrey Biles
<br>Ember, JavaScript</p>

<p><a href="https://handmadehero.org/"><strong>Handmade Hero</strong></a>
by Casey Muratori
<br>C++, Win32</p>

<p><a href="https://www.youtube.com/c/brackeys"><strong>Brackeys</strong></a>
by Asbj&oslash;rn Thirslund
<br>C#, JavaScript, Unity</p>

<p><a href="https://www.youtube.com/user/Cercopithecan"><strong>Game Development Tutorials</strong></a>
by Sebastian Lague
<br>Unity, C#</p>

<p><a href="https://www.driftingruby.com/"><strong>Drifting Ruby</strong></a>
by Dave Kimura
<br>Rails, JavaScript</p>

<h2 id="Schools%20and%20Stores">Schools and Stores</h2>

<p><a href="https://egghead.io/"><strong>Egghead</strong></a>
<br>Angular, React, Elm, JavaScript, RxJs, Node.js, D3</p>

<p><a href="https://thoughtbot.com/upcase"><strong>Upcase</strong></a>
<br>Rails, Vim, JavaScript, Unix, Ruby, OOP/FP, Git</p>

<p><a href="https://frontendmasters.com/"><strong>Frontend Masters</strong></a></p>

<p><a href="https://www.pluralsight.com/"><strong>Pluralsights</strong></a></p>

<p><a href="https://www.raywenderlich.com/"><strong>Ray Wenderlich</strong></a>
<br>Swift, iOS, Android, macOS, Apple Game Frameworks, Unity</p>

<p><a href="https://linuxacademy.com/"><strong>Linux Academy</strong></a>
<br>Linux, AWS, Azure, OpenStack, DevOps, BigData</p>

<p><a href="https://caster.io/"><strong>Caster IO</strong></a>
<br>Android, RxJava, Material Design, Espresso</p>

<p><a href="https://laracasts.com/"><strong>Laracasts</strong></a>
<br>PHP, Laravel, testing, JavaScript, tooling, HTML</p>

<p><a href="https://www.packtpub.com/"><strong>Packt</strong></a>
<br>Swift, Python, JavaScript, Java, and many more</p>

<p>See also Udemy, Udacity, Treehouse, Coursera, Envato Tuts+, and Codeacademy.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/js-ramda-elm.html</guid>
<link>https://www.romanzolotarev.com/js-ramda-elm.html</link>
<pubDate>Wed, 26 Oct 2016 00:00:00 +0000</pubDate>
<title>Compare JavaScript, Ramda, and Elm</title>
<description><![CDATA[

<h1 id="Compare%20JavaScript,%20Ramda,%20and%20Elm">Compare JavaScript, Ramda, and Elm</h1>

<p>I recently worked on pagination for a web app. This simple problem is a
good case study for comparing JavaScript and Elm.</p>

<p>Here&#39;s the use case:</p>

<p>We have a current page address, for example <strong>index</strong>, and a list of all
the page addresses: <strong>index</strong>, <strong>vanilla</strong>, <strong>ramda</strong>, <strong>elm</strong>.</p>

<p>There are two links, <strong>Previous</strong> and <strong>Next</strong>. By clicking on those links
you go to the previous or the next page accordingly.</p>

<p>When you&#39;re on the first page, the <strong>Previous</strong> link is disabled. When
you&#39;re on the last page, <strong>Next</strong> is disabled.</p>

<pre><code>              current page
                  |
                  |
[ Previous ]    index    vanilla    ramda    elm    [ Next ]
     |
     |
disabled link
</code></pre>

<p>Let&#39;s write two functions&mdash;one to prepare a data structure and
another to generate HTML code based on it.</p>

<h2 id="The%20%3Ccode%3Epaginate()%3C/code%3E%20function">The <code>paginate()</code> function</h2>

<p>This function takes a <strong>current</strong> page and a list of <strong>pages</strong>. It returns
a tuple of <strong>previous</strong>, <strong>next</strong>. Both elements of the tuple can be
empty.</p>

<h2 id="The%20%3Ccode%3Ehtml()%3C/code%3E%20function">The <code>html()</code> function</h2>

<p>This function takes the result of <code>paginate()</code> and returns an HTML string
containing links to the next and previous pages. It handles cases when a
current page is not found, or either of the <strong>previous</strong> or <strong>next</strong> links
is missing.</p>

<pre><code>// List of pages
pages = [&#39;index&#39;, &#39;vanilla&#39;, &#39;ramda&#39;, &#39;elm&#39;]

// There should be no Previous page
paginate(&#39;index&#39;, pages);
// =&gt; [undefined, &#39;vanilla&#39;]

html(paginate(&#39;index&#39;, pages));
// =&gt; &#39;No previous&lt;a href=&quot;vanilla&quot;&gt;Next&lt;/a&gt;&#39;

// Both links are available
html(paginate(&#39;vanilla&#39;, pages));
// =&gt; &#39;&lt;a href=&quot;index&quot;&gt;Previous&lt;/a&gt;&lt;a href=&quot;ramda&quot;&gt;Next&lt;/a&gt;&#39;

// Non-existent page
html(paginate(&#39;pageX&#39;, pages));
// =&gt; &#39;Current not found&#39;
</code></pre>

<p>To start with, let&#39;s implement this in vanilla JavaScript.</p>

<h2 id="Vanilla%20JavaScript">Vanilla JavaScript</h2>

<p>We&#39;ll start with a plain JavaScript implementation; to be specific,
ECMAScript 5. Then we&#39;ll see what we can improve.</p>

<p>How can we implement <code>paginate()</code> in JavaScript? First, get the index of
the <code>current</code> page in the array of <code>pages</code>. If <code>current</code> is found, then
get its neighbors and return an array of <code>[previous, next]</code>; otherwise,
return nothing.</p>

<p>Implementing <code>html()</code> is pretty straightforward: get that array and render
it to a string.</p>

<h2 id="Na&amp;#x00EF;ve%20implementation%20in%20ECMAScript%205.">Na&#x00EF;ve implementation in ECMAScript 5.</h2>

<pre><code>var pages = [&#39;index&#39;, &#39;vanilla&#39;, &#39;ramda&#39;, &#39;elm&#39;];
var current = &#39;vanilla&#39;;

var paginate = function(current, pages) {

  // Get index of current page
  // in array of pages
  var i = pages.indexOf(current);

  // If current page is found
  // (index is not -1)
  return i !== -1
    ? ([ pages[i - 1], // Previous page
        pages[i + 1]  // Next page
      ])
    // else return undefined
    : undefined;
}

var html = function(pagination) {
  if (pagination === undefined) return &#39;Current not found&#39;;

  var previous =
    pagination[0] === undefined
      ? &#39;No previous&#39;
      : (&#39;&lt;a href=&quot;&#39; + pagination[0] + &#39;&quot;&gt;Previous&lt;/a&gt;&#39;);

  var next =
    pagination[1] === undefined
      ? &#39;No next&#39;
      : (&#39;&lt;a href=&quot;&#39; + pagination[1] + &#39;&quot;&gt;Next&lt;/a&gt;&#39;);

  return (previous + next);
}
</code></pre>

<p>Now we can test our functions.</p>

<pre><code>pages = [&#39;index&#39;, &#39;vanilla&#39;, &#39;ramda&#39;, &#39;elm&#39;]
// =&gt; &#39;[&quot;index&quot;, &quot;vanilla&quot;, &quot;ramda&quot;, &quot;elm&quot;]&#39;

current = &#39;vanilla&#39;
// =&gt; &quot;vanilla&quot;

paginate(current, pages)
// =&gt; [&quot;index&quot;, &quot;ramda&quot;]

html(paginate(current, pages))
// =&gt; &quot;&lt;a href=\&quot;index\&quot;&gt;Previous&lt;/a&gt;&lt;a href=\&quot;ramda\&quot;&gt;Next&lt;/a&gt;&quot;
</code></pre>

<h2 id="How%20can%20we%20improve%20it?">How can we improve it?</h2>

<p>First, those <code>undefined</code> values look suspicious. Just take a look at the
edge case, when <strong>previous</strong> is <code>undefined</code>:</p>

<pre><code>paginate(&#39;index&#39;, pages)
// =&gt; [undefined, &quot;vanilla&quot;]
</code></pre>

<p>We need some safer data types.</p>

<p>Second, those <code>if</code> and <code>else</code> statements are a problem. It is possible to
forget something and accidentally miss a case.</p>

<p>We need a better way to branch our code.</p>

<h2 id="Built%20with%20Ramda">Built with Ramda</h2>

<p>Before we dive in into our implementation, let&#39;s get familiar with Ramda,
a functional library for JavaScript.</p>

<p>If you haven&#39;t tried functional programming before, it may look unusual,
but it&#39;s totally worth learning. It is consistently simple, backed by
math, and fun to learn.</p>

<h2 id="Ramda">Ramda</h2>

<p>Ramda helps us to write code in a purer functional style. It&#39;s a small
library, has no dependencies, and works in all browsers as well as Node.</p>

<pre><code>var numbers = [10, 20, 30, 40];
var inc = function (x) { return (x + 1); }
</code></pre>

<p>Compare the native <code>map</code>...</p>

<pre><code>numbers.map(inc)
// =&gt; [11, 21, 31, 41]
</code></pre>

<p>... with Ramda&#39;s:</p>

<pre><code>R.map(inc, numbers)
// =&gt; [11, 21, 31, 41]
</code></pre>

<p>It doesn&#39;t look that different at the beginning... but it&#39;s only the
beginning.</p>

<p>How does <code>map()</code> work? It takes a function and a functor (e.g., array,
string, or object), applies the function to each of the functor&#39;s values,
and returns a functor of the same shape.</p>

<pre><code>R.map(inc, {a: 100, b: 200})
// =&gt; {&quot;a&quot;: 101, &quot;b&quot;: 201}

R.map(inc, {a: &#39;a&#39;, b: &#39;b&#39;})
// =&gt; {&quot;a&quot;: &quot;a1&quot;, &quot;b&quot;: &quot;b1&quot;}

R.map(inc, &#39;abc&#39;)
// =&gt; [&quot;a1&quot;, &quot;b1&quot;, &quot;c1&quot;]
</code></pre>

<p>What happens when you pass fewer parameters than the function expects?
Usually JavaScript functions are not happy:</p>

<pre><code>numbers.map()
// =&gt; TypeError: Array.prototype.map callback must be a function
</code></pre>

<p>Now look at the Ramda function:</p>

<pre><code>R.map()
// =&gt; function n(r,e){switch(arguments.length){case 0:return n;case 1:return b(r)?n:L(function(n){return t(r,n)});default:return b(r)&amp;&amp;b(e)?n:b(r)?L(function(n){return t(n,e)}):b(e)?L(function(n){return t(r,n)}):t(r,e)}}
</code></pre>

<p>That&#39;s right --- it returns a function. If there were no parameters
passed, it returns itself. Just double checking:</p>

<pre><code>R.map
// =&gt; function n(r,e){switch(arguments.length){case 0:return n;case 1:return b(r)?n:L(function(n){return t(r,n)});default:return b(r)&amp;&amp;b(e)?n:b(r)?L(function(n){return t(n,e)}):b(e)?L(function(n){return t(r,n)}):t(r,e)}}
</code></pre>

<p>What if you pass one parameter instead of two?</p>

<pre><code>incAll = R.map(inc)
// =&gt; function n(r){return 0===arguments.length||b(r)?n:t.apply(this,arguments)}
</code></pre>

<p>It returns a partially applied function.</p>

<p>All Ramda functions are automatically curried and work this way. A
<em>curried</em> function can take only a subset of its parameters and return a
new function that takes the remaining parameters. If you call a curried
function with all of its parameters, it just calls the function and
returns a value.</p>

<p>So all of the following expressions return the same result:</p>

<pre><code>incAll(numbers)
// =&gt; [11, 21, 31, 41]

R.map(inc, numbers)
// =&gt; [11, 21, 31, 41]

R.map(inc)(numbers)
// =&gt; [11, 21, 31, 41]

R.map()(inc)(numbers)
// =&gt; [11, 21, 31, 41]
</code></pre>

<p>Note that in most Ramda functions the data is supplied as the last
parameter, to make it convenient for currying.</p>

<p>By the way, Ramda has <code>R.inc()</code> and <code>R.dec()</code> already.</p>

<pre><code>R.map(R.dec)(numbers)
// =&gt; [9, 19, 29, 39]
</code></pre>

<p>Probably the simplest Ramda function is <code>R.identity()</code>. It does nothing
but return the parameter supplied to it, so it&#39;s good as a placeholder
function. We will use it in the example.</p>

<pre><code>R.map(R.identity)(numbers)
// =&gt; [10, 20, 30, 40]
</code></pre>

<p>The next two function names speak for themselves: <code>R.head()</code> returns the
first element of the array.</p>

<pre><code>R.head(numbers)
// =&gt; 10
</code></pre>

<p><code>R.last()</code> returns the last element of the array.</p>

<pre><code>R.last(numbers)
// =&gt; 40
</code></pre>

<p>The last function you need to learn for now is <code>R.concat()</code>.</p>

<pre><code>R.concat(numbers, 50)
// =&gt; [10, 20, 30, 40, 50]
</code></pre>

<p>I am glad you got this far! Finally, we are at the point where we can get
rid of <code>undefined</code>. There is a library for this. :)</p>

<h2 id="Sanctuary">Sanctuary</h2>

<p>Sanctuary is a functional programming library. It depends on and works
nicely with Ramda.</p>

<p>Let&#39;s compare what two <code>indexOf</code> functions return when the element is not
found in the array:</p>

<pre><code>R.indexOf(42, numbers)
// =&gt; -1
</code></pre>

<p><code>-1</code>? Okay. Kind of weird, but familiar. Now look at Sanctuary&#39;s version:</p>

<pre><code>S.indexOf(42, numbers)
// =&gt; Nothing()
</code></pre>

<p>It returns a value of <code>Maybe</code> type. <code>Maybe</code> is useful for composing
functions that might not return a value.</p>

<p>Here&#39;s one more example of a function which returns <code>Maybe</code>.</p>

<p><code>S.at()</code> takes an index and a list and returns <code>Just</code> the element of the
list at the index, if the index is within the list&#39;s bounds...</p>

<pre><code>S.at(1, numbers)
// =&gt; Just(20)
</code></pre>

<p>... and returns <code>Nothing</code> otherwise.</p>

<pre><code>S.at(100, numbers)
// =&gt; Nothing()
</code></pre>

<p>Compare it with the native JavaScript alternative:</p>

<pre><code>numbers[100]
// =&gt; undefined
</code></pre>

<p>Okay, sounds like we can get rid of <code>undefined</code>. But how should we handle
<code>Maybe</code> values? To apply functions to <code>Maybe</code> values you can use old good
<code>map</code>:</p>

<pre><code>R.map(R.inc, S.Just(3))
// =&gt; Just(4)

R.map(R.inc, S.Nothing())
// =&gt; Nothing()
</code></pre>

<p>But what about branching code without using conditionals? In our case, we
have two branches, left and right.</p>

<pre><code>pagination[2] === undefined
  ? &#39;No next&#39;                                     // Left
  : (&#39;&lt;a href=&quot;&#39; + pagination[2] + &#39;&quot;&gt;Next&lt;/a&gt;&#39;); // Right
</code></pre>

<p>To handle it, we can convert <code>Maybe</code> to <code>Either</code>. What is <code>Either</code>? The
<code>Either</code> type represents values with two possibilities, <code>Left</code> and
<code>Right</code>.</p>

<pre><code>S.maybeToEither(&#39;No next&#39;, S.Just(3))
// =&gt; Right(3)

S.maybeToEither(&#39;No next&#39;, S.Nothing())
// =&gt; Left(&quot;No next&quot;)
</code></pre>

<p>Now we can apply two different functions to <code>Left</code> and <code>Right</code>.</p>

<p>When the third parameter is <code>Left</code>, then <code>R.identity()</code> is applied to <code>&#39;No
next&#39;</code>. Let&#39;s hard-code the third parameter to test <code>S.either</code>.</p>

<pre><code>S.either(R.identity, R.toString, S.Left(&#39;No next&#39;))
// =&gt; &quot;No next&quot;
</code></pre>

<p>When the third parameter is <code>Right</code>, then <code>R.toString()</code> is applied to
<code>3</code>. The third parameter is hard-coded again.</p>

<pre><code>S.either(R.identity, R.toString, S.Right(3))
// =&gt; &quot;3&quot;
</code></pre>

<p>Congratulations! Now you are ready to read the refactored solution. ;)</p>

<h2 id="Implementation%20with%20Ramda%20and%20Sanctuary">Implementation with Ramda and Sanctuary</h2>

<pre><code>// var R = require(&#39;ramda&#39;);
// var S = require(&#39;sanctuary&#39;);

var pages = [&#39;index&#39;, &#39;vanilla&#39;, &#39;ramda&#39;, &#39;elm&#39;];
var current = &#39;ramda&#39;;

var paginate = function(current, pages) {

  return R.map(
    function(index) {
      return [
        S.at(R.dec(index), pages),
        S.at(R.inc(index), pages)
      ]
    },
    // Gets index of the current page
    S.indexOf(current, pages)
  )
}

var html = function(pagination) {

  var previous =
    function(url) {
      return (&#39;&lt;a href=&quot;&#39; + url + &#39;&quot;&gt;Previous&lt;/a&gt;&#39;);
    }

  var next =
    function(url) {
      return (&#39;&lt;a href=&quot;&#39; + url + &#39;&quot;&gt;Next&lt;/a&gt;&#39;);
    }

  var buttons = function(x) {
    return R.concat(
      S.either(
        R.identity,
        previous,
        S.maybeToEither(&#39;No previous&#39;, R.head(x))
      ),
      S.either(
        R.identity,
        next,
        S.maybeToEither(&#39;No next&#39;, R.last(x))
      )
    )
  }

  return S.either(
    R.identity,
    buttons,
    S.maybeToEither(&#39;Current not found&#39;, pagination)
  )
}
</code></pre>

<p>Let&#39;s test it.</p>

<pre><code>pages = [&#39;index&#39;, &#39;vanilla&#39;, &#39;ramda&#39;, &#39;elm&#39;]
// =&gt; [&quot;index&quot;, &quot;vanilla&quot;, &quot;ramda&quot;, &quot;elm&quot;]

current = &#39;ramda&#39;
// =&gt; &quot;ramda&quot;

paginate(&#39;ramda&#39;, pages)
// =&gt; Just([Just(&quot;vanilla&quot;), Just(&quot;elm&quot;)])

html(paginate(&#39;ramda&#39;, pages))
// =&gt; &quot;&lt;a href=\&quot;vanilla\&quot;&gt;Previous&lt;/a&gt;&lt;a href=\&quot;elm\&quot;&gt;Next&lt;/a&gt;&quot;
</code></pre>

<h2 id="What%20can%20we%20improve?">What can we improve?</h2>

<p>Syntax. It is getting pretty lengthy and noisy. ECMAScript 2015 solves
this partially.</p>

<pre><code>const paginate = (current, pages) =&gt; R.map(
  (i) =&gt; R.map(
    S.at(R.dec(i), pages),
    S.at(R.inc(i), pages)
  ), S.indexOf(current, pages)
)
</code></pre>

<p>By the way, here&#39;s our vanilla version in ECMAScript 2015.</p>

<pre><code>const paginate = (current, pages) =&gt; {
  const i = pages.indexOf(current);
  return i !== -1
    ? [pages[i - 1], pages[i + 1]]
    : undefined;
}
</code></pre>

<p>Another problem with our solution is that <code>html()</code> returns a string. It
would be good to have DOM elements instead. To manipulate the DOM we can
use one of the Virtual DOM libraries or frameworks, like React, Angular,
Ember, or Vue.</p>

<p>If we&#39;re looking for a lighter and purely functional solution, Elm is
a better option.</p>

<h2 id="Built%20with%20Elm">Built with Elm</h2>

<p>I have been playing with Elm for several months and find it fascinating:
Helpful error messages. If it compiles, it works; no runtime errors; good
performance; clean syntax; simple refactoring, and so on.</p>

<h2 id="Writing%20in%20Elm">Writing in Elm</h2>

<p>Create a file, say <code>pagination.elm</code>. If you are using any modules, you
have to import them explicitly.</p>

<p>For this implementation in Elm I&#39;m using the <code>List</code> type for the list of
pages. To get value by index, we use <code>getAt()</code> and to get index by value,
we use <code>elemIndex()</code>. Both functions are from the <code>List.Extra</code> module;
let&#39;s import it.</p>

<pre><code>List.Extra

List.Extra.getAt 3 pages
-- Just &quot;elm&quot;
</code></pre>

<p>We can use the <code>exposing</code> keyword to import particular functions of the
module, and then call them <em>without a qualifier</em>.</p>

<pre><code>import List.Extra exposing (getAt, elemIndex)

getAt 3 pages
-- Just &quot;elm&quot;

elemIndex &quot;elm&quot; pages
-- Just 3
</code></pre>

<p>As you can see, you don&#39;t need brackets in these function calls, just the
function name and a list of its parameters.</p>

<p>To define a function you just type in a function name, then a list of its
parameters, then the <code>=</code> sign, and finally its definition.</p>

<pre><code>inc x = x + 1

inc 1
-- 2
</code></pre>

<p>You can define local functions inside a function with the <code>let ... in</code>
keywords.</p>

<pre><code>current = &quot;ramda&quot;

isCurrent page =
    let
        current = &quot;elm&quot;
    in
        page == current

isCurrent &quot;elm&quot;
-- True
</code></pre>

<p>To handle cases you can use the <code>case ... of</code> keywords, which is useful
for pattern matching. The important difference with Elm is that its
compiler won&#39;t let you forget to cover cases, and will make sure all
branches return values of the same shape.</p>

<pre><code>get index pages =
    case index of
        Just i -&gt;
            &quot;Index is &quot; ++ i

        Nothing -&gt;
            &quot;No index&quot;
</code></pre>

<p>You&#39;ve probably noticed that for summing numbers in Elm we use <code>+</code>, but to
concatenate strings we need <code>++</code>.</p>

<p>Our function <code>paginate()</code> returns a tuple, which is a data type in Elm.
Here&#39;s the syntax.</p>

<pre><code>pagination = (Just &quot;elm&quot;, Nothing)
</code></pre>

<p>Oh, the <code>Maybe</code> type is in the Elm core library, so you don&#39;t need to
import it.</p>

<p>HTML elements are functions of the <code>Html</code> module. To create a link ---
HTML element <code>&lt;A&gt;</code> --- you need to call the function <code>Html.a</code> with two
parameters: list of attributes and list of children.</p>

<pre><code>Html.a [ Html.Attributes.href &quot;ramda&quot; ] [ Html.text &quot;Previous&quot; ]
</code></pre>

<p>As you can guess, <code>href</code> is a function from the <code>Html.Attributes</code> module,
and <code>text</code> is a function from the <code>Html</code> module.</p>

<p>That&#39;s it. You know enough Elm to read the code:</p>

<pre><code>import Html exposing (div, text, a)
import Html.Attributes exposing (href)
import List.Extra exposing (getAt, elemIndex)


pages =
    [ &quot;index&quot;, &quot;vanilla&quot;, &quot;ramda&quot;, &quot;elm&quot; ]


current =
    &quot;elm&quot;


paginate current pages =
    case (elemIndex current pages) of
        Just i -&gt;
            ( (getAt (i - 1) pages), (getAt (i + 1) pages) )

        _ -&gt;
            ( Nothing, Nothing )


html pagination =
    let
        next page =
            case page of
                Just url -&gt;
                    a [ href url ] [ text &quot;Next&quot; ]

                Nothing -&gt;
                    text &quot;No next&quot;

        previous page =
            case page of
                Just url -&gt;
                    a [ href url ] [ text &quot;Previous&quot; ]

                Nothing -&gt;
                    text &quot;No previous&quot;
    in
        case pagination of
            ( Nothing, Nothing ) -&gt;
                text &quot;Current not found&quot;

            ( p, n ) -&gt;
                div [] [ previous p, next n ]


main =
    html (paginate current pages)
</code></pre>

<h2 id="Epilogue">Epilogue</h2>

<p>You may ask, what&#39;s the point of adding lines of code and introducing new
libraries and weird data types --- even a new language! Doesn&#39;t it
complicate everything?</p>

<p>First, type safety helps you to avoid runtime errors and catch mistakes
earlier. Second, pure functions are much simpler to understand, to
maintain, and to refactor. Third, in most cases composability and
point-free style make your code much cleaner.</p>

<p>If you&#39;re building a small web app and file size is critical, then nothing
can beat vanilla JavaScript, of course. However, when your app grows,
Ramda or Elm can save you from mistakes and make your developer experience
much better.</p>

<p>You&#39;d still better know <a href="https://dorey.github.io/JavaScript-Equality-Table/">JavaScript equality
quirks</a>, how to
manipulate the
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model">DOM</a>
directly, how to manage scope, how to avoid mutable state, how to handle
<code>null</code>, etc.</p>

<p>If JavaScript is so weird, why should we learn it? Because 3 billion
people use web browsers and  JavaScript is the only scripting language
natively supported by most web browsers today. JavaScript is getting
better, but it won&#39;t be changed dramatically anytime soon.</p>

<h2 id="Bonus%20track">Bonus track</h2>

<p>Thank you for reading this far! Here&#39;s another implementation with the Elm
core library only. You can copy-paste it to <a href="http://elm-lang.org/try">Elm
REPL</a> and play with it.</p>

<pre><code>import Html
import List

pages = [ &quot;index&quot;, &quot;vanilla&quot;, &quot;ramda&quot;, &quot;elm&quot; ]
current = &quot;elm&quot;

paginate current pages =
  let
      cs = pages |&gt; List.map Just
      ns = List.drop 1 cs ++ [ Nothing ]
      ps = Nothing :: List.take (List.length cs - 1) cs
      isCurrent (p, c, n) =
          case c == Just current of
              True -&gt; Just (p, n)
              False -&gt; Nothing
  in
    List.map3 (\p c n -&gt; ( p, c, n )) ps cs ns
      |&gt; List.filterMap isCurrent

main =
    paginate current pages
        |&gt; toString
        |&gt; Html.text
        -- Just (Just &quot;ramda&quot;, Just &quot;elm&quot;, Nothing)
</code></pre>

<h2 id="See%20also">See also</h2>

<p><a href="https://guide.elm-lang.org">An Introduction to Elm by Evan Czaplicki</a>,<br>
<a href="https://github.com/ramda/ramda/wiki/Type-Signatures">Type Signatures by Scott Sauyet</a>,<br>
<a href="http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html">Functors, Applicatives, And Monads In Pictures by Aditya Bhargava</a>,<br>
<a href="http://randycoulman.com/blog/categories/thinking-in-ramda/">Thinking in Ramda by Randy Coulman</a>,<br>
<a href="https://github.com/MostlyAdequate/mostly-adequate-guide">Professor Frisby&#39;s Mostly Adequate Guide to FP by Brian Lonsdorf</a>,<br>
<a href="https://github.com/getify/functional-light-js">Functional-Light JavaScript by Kyle Simpson</a>,<br>
<a href="https://github.com/stoeffel/awesome-fp-js">Awesome FP JavaScript</a></p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/macos/textedit.html</guid>
<link>https://www.romanzolotarev.com/macos/textedit.html</link>
<pubDate>Sat, 17 Sep 2016 00:00:00 +0000</pubDate>
<title>Edit text with TextEdit.app</title>
<description><![CDATA[

<p><em>Tested on <a href="https://www.romanzolotarev.com/macos/">macOS</a> 10.13</em></p>

<h1 id="Edit%20text%20with%20TextEdit.app">Edit text with TextEdit.app</h1>

<p>Usually I type my words into <a href="https://www.romanzolotarev.com/vi.html">vi</a>, but today I&#39;m composing
this page in TextEdit.app.</p>

<p>TextEdit.app is pre-installed on every Mac and supports plain text.
To start using it for plain text files, set these defaults.</p>

<ul>
<li>First, open TextEdit.app,</li>
<li>press <strong>&#x2318;,</strong> to open <strong>Preferences</strong>,</li>
<li>switch <strong>Format</strong> to <strong>Plain text</strong>.</li>
</ul>

<p>Note: You can also adjust the default font and size by clicking on
<strong>Change</strong> beside <strong>Plain text font</strong>.</p>

<p><img src="https://www.romanzolotarev.com/textedit-new-document.png" alt="TextEdit.app &gt; Plain text" /></p>

<p>You can always adjust the font size in the current window with
<strong>&#x2318;+</strong> and <strong>&#x2318;-</strong>.</p>

<p>Optionally, go to <strong>Preferences &gt; Open and Save</strong>. Check <strong>When Opening a
File</strong> to <strong>Display HTML files as HTML code instead of formatted text</strong>.</p>

<p><img src="https://www.romanzolotarev.com/textedit-open-and-save.png" alt="TextEdit.app &gt; HTML as code" /></p>

<p>Now you can close the TextEdit window and open a new one in plain text.</p>

<p>You can write some prose in TextEdit, but editing long texts,
especially across multiple files, is inefficient. Frankly, I switched
to <a href="https://www.romanzolotarev.com/vim.html">Vim</a> to edit this page as soon as the first draft
was complete.</p>

<p><a href="https://www.romanzolotarev.com/plaintext.html">Plain text is awesome!</a></p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/standalone.html</guid>
<link>https://www.romanzolotarev.com/standalone.html</link>
<pubDate>Tue, 23 Aug 2016 00:00:00 +0000</pubDate>
<title>A standalone website</title>
<description><![CDATA[

<h1 id="A%20standalone%20website">A standalone website</h1>

<p>As soon as you figure out what and how to write, you need to publish
what you have written. You have many options. Start with a standalone
website.</p>

<p>You can post your writing on social media sites. Those sites have huge
audiences, most of the people you want to reach are already there, you
just sign in and start writing, and it is &quot;free&quot;. But you give up all your
rights at the moment of posting. You give up your content and your
audience from the beginning. You have no control over page URLs, which
makes it hard to transfer your content out of the platform. Your posts may
be censored and your account can be banned. You cannot know which of your
posts will appear in your followers&#39; feeds. You do not decide how your
pages look. You cannot remove ads, or change colors or fonts. When a
platform dies, all your hard work disappears. You do not own your website.</p>

<p>A slightly better option is to use one of the blogging platforms: Medium,
Svbtle, Tumblr, WordPress, etc. You have control over your page content.
You can set up a custom domain and have some control over page URLs. But
you are tied to a platform and its software --- it&#39;s painful to
transfer your content out.</p>

<p>What does it mean to own your website? You need to fully control your
domain, your content (rights, URLs, styles, markup, etc) the communication
channels to your readers (emails and comments).</p>

<p>So if you&#39;re serious about publishing, should you make everything from
scratch? Fortunately, you don&#39;t have to create the universe to make your
own website.</p>

<p>Try <a href="https://www.romanzolotarev.com/github-pages.html">GitHub Pages</a>.</p>

]]></description>
</item>

<item>
<guid>https://www.romanzolotarev.com/website.html</guid>
<link>https://www.romanzolotarev.com/website.html</link>
<pubDate>Mon, 15 Aug 2016 00:00:00 +0000</pubDate>
<title>Do you need a website?</title>
<description><![CDATA[

<h1 id="Do%20you%20need%20a&amp;nbsp;website?">Do you need a website?</h1>

<p>Keeping a website is time-consuming, your site may remain obscure,
your pages will become outdated, and eventually you&#39;ll probably
quit writing anyway. So why bother?</p>

<p>Because regular writing helps you to learn things deeper, helps you
to spread your ideas, and helps your readers learn faster too&mdash;or
at least have some fun.</p>

<p>There are many other means of communication nowadays, but writing
is one of the most powerful, accessible, and scalable. Words are
magical. You can write something today and people might read your
words years later. Almost anyone can download your pages and read
them. Even over a slow internet connection, on a slow computer,
with a small screen: Nothing can stop your words. The number of
your readers is unlimited, and the delivery cost is close to zero
(less than one US dollar for a million page views).</p>

<p>You may say that writing is hard. Yes, it is hard, especially if
you are writing in a foreign language, but with practice your writing
will get better and the act of writing will become much more
enjoyable.</p>

<h2 id="What%20to%20write%20about?">What to write about?</h2>

<p>Probably the first question you&#39;ll ask yourself is, what should I
write about? You might say you definitely have nothing special to
say, all your thoughts are obvious and all your stories are boring.
But something that is obvious to you is not always obvious to other
people, and something that is boring to you might be fascinating
to someone from a different culture or who is new to the topic.</p>

<p>Tell a story about a project you are working on. It can be useful
or entertaining, or both. It can be about a thing you made this
week or a thing you learned today. You can write about how and what
you are doing, and why you are doing it. What problems you are
solving, how you pick and configure your tools, and so on.</p>

<p>In my case, today&#39;s story is about restarting this website and
writing the first post. Way too meta? True.</p>

<p>If you want to make your website useful, make your pages actionable.
Each of your posts can be a step-by-step instruction, a recipe.
Start with small, simple pages and add depth gradually.</p>

<p>After choosing your topic, the next hard thing is to keep writing.
To make your new hobby sustainable it is wise to build a writing
habit&mdash;and most importantly, a publishing habit, or you may
end up with hundreds of unpublished drafts. To fight your perfectionism
set up a schedule: collect notes during the week (or better tweet
as you go) and then on weekend repackage those notes into a structured
page to publish it on Monday.</p>

<p>Just keep going.</p>

]]></description>
</item>
</channel></rss>
