<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.2.2">Jekyll</generator><link href="https://lahtela.me/feed.xml" rel="self" type="application/atom+xml" /><link href="https://lahtela.me/" rel="alternate" type="text/html" /><updated>2024-06-07T17:38:00+03:00</updated><id>https://lahtela.me/feed.xml</id><title type="html">Tatu Lahtela’s blog</title><subtitle>Tatu Lahtela&apos;s personal blog</subtitle><author><name>Tatu Lahtela</name></author><entry><title type="html">Dealing with PostgreSQL Times and Local Times</title><link href="https://lahtela.me/2024/06/07/trying-to-understand-postgres-timestamps.html" rel="alternate" type="text/html" title="Dealing with PostgreSQL Times and Local Times" /><published>2024-06-07T08:24:48+03:00</published><updated>2024-06-07T08:24:48+03:00</updated><id>https://lahtela.me/2024/06/07/trying-to-understand-postgres-timestamps</id><content type="html" xml:base="https://lahtela.me/2024/06/07/trying-to-understand-postgres-timestamps.html"><![CDATA[<p>My current work involves a lot of local time dependent operations. This means answering a lot annoying questions like
“Find the maximum value during workdays between 7-21.”, “Highest value during daytime in February (in Country X)”,
“Find the average value average value for today” or “Highest value during December”.</p>

<p>To get started let’s create table:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">Measurement</span><span class="p">(</span>
    <span class="n">id</span> <span class="nb">SERIAL</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
    <span class="n">value</span> <span class="nb">DOUBLE</span> <span class="nb">PRECISION</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
    <span class="nb">time</span> <span class="nb">TIMESTAMP</span> <span class="k">WITH</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="k">NOT</span> <span class="k">NULL</span>
<span class="p">);</span>
</code></pre></div></div>

<p>I will not get into details about the “TIMESTAMP WITH TIME ZONE” (spoiler: I will) type and it’s hairy spots, you can go
to <a href="https://www.yugabyte.com/blog/postgresql-timestamps-timezones/">this yagabyte article</a> or
<a href="https://www.enterprisedb.com/postgres-tutorials/postgres-time-zone-explained">EnterpriseDB</a> for that.</p>

<p>Lets generate a six of months of data as in increasing series, one measurement per hour, on the hour. Let’s ask ChatGPT
to create the generate_series query, lazy as we are. ChatGPT (and Gemini) suggests to calculate the amount hour ours
between the timestamps like this: <code class="language-plaintext highlighter-rouge">SELECT EXTRACT(EPOCH FROM AGE('2024-07-01 00:00:00+00', '2024-01-01 00:00:00+00')) /
3600 AS hours_difference;</code> This returns returns 4320, which is not correct. The <code class="language-plaintext highlighter-rouge">AGE</code>function documentation says:</p>

<blockquote>
  <p>“Subtract arguments, producing a “symbolic” result that uses years and months, rather than just days”.</p>
</blockquote>

<p>The
<a href="https://www.postgresql.org/docs/current/functions-datetime.html">doc</a> doesn’t get into any more details, but I suppose
it simply calculates the span from epoc with 30 day months. Oops. ChatGPT even suggested to use <code class="language-plaintext highlighter-rouge">JUSTIFY_HOURS</code>, which is
even more incorrect. I can’t even imagine what it tried to do there.</p>

<p>Fine, let’s do it hourself, <em>sigh</em>:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">Measurement</span> <span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="nb">time</span><span class="p">)</span>
<span class="k">SELECT</span>
  <span class="n">generate_series</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="p">(</span><span class="k">SELECT</span> <span class="k">EXTRACT</span><span class="p">(</span><span class="n">EPOCH</span> <span class="k">FROM</span> 
                     <span class="s1">'2024-07-01 00:00:00+00'</span><span class="p">::</span><span class="nb">timestamp</span> <span class="o">-</span> 
                     <span class="s1">'2024-01-01 00:00:00+00'</span><span class="p">::</span><span class="nb">timestamp</span><span class="p">)</span> <span class="o">/</span> <span class="mi">3600</span> <span class="k">AS</span> <span class="n">hours_difference</span><span class="p">),</span> <span class="mi">1</span><span class="p">)</span> 
                     <span class="k">as</span> <span class="n">value</span><span class="p">,</span>
  <span class="n">generate_series</span><span class="p">(</span>
    <span class="s1">'2024-01-01 00:00:00+00'</span><span class="p">::</span><span class="n">timestamptz</span><span class="p">,</span>
    <span class="s1">'2024-07-01 00:00:00+00'</span><span class="p">::</span><span class="n">timestamptz</span><span class="p">,</span>
    <span class="s1">'1 hour'</span><span class="p">::</span><span class="n">interval</span>
  <span class="p">)</span> <span class="k">AS</span> <span class="nb">time</span><span class="p">;</span>
</code></pre></div></div>

<p>BOOM! Six months of data. The annoying part starts immediately as we actually can’t answer a question like “highest
value in January in Finland”, since we are missing data from the first two hours of Finnish time. So let’s use February.
Nothing special, datewise speaking, happened in February 2024, right?</p>

<h1 id="querying-time-in-local-time-zones---careful-with-casts">Querying time in local time zones - careful with casts</h1>

<p>First thing to remember about PostgreSQL is that the database stores timestamps, assuming the
field has time zone information, is always internally stored as UTC, and it will
also always output UTC unless you set it do otherwise with <code class="language-plaintext highlighter-rouge">SET TIMEZONE</code> command.</p>

<p>Let’s take one of the tasks: “Find the highest value and time during work daytime (after 7 and before 21 i.e.
7:00-20:59.59) in February in Finland”. Since our data is an increasing series, and looking at the calendar, the highest
value should be the last Thursday 29th, at 20:00, so in utc 18:00. We write (ChatGPT wrote!) something like this:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">value</span><span class="p">,</span>
       <span class="nb">time</span>
<span class="k">FROM</span> <span class="n">Measurement</span>
<span class="k">WHERE</span> <span class="k">EXTRACT</span><span class="p">(</span><span class="n">DOW</span> <span class="k">FROM</span> <span class="p">(</span><span class="nb">time</span><span class="p">::</span><span class="nb">timestamp</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'Europe/Helsinki'</span><span class="p">))</span> <span class="k">BETWEEN</span> <span class="mi">1</span> <span class="k">AND</span> <span class="mi">5</span>
  <span class="k">AND</span> <span class="k">EXTRACT</span><span class="p">(</span><span class="n">HOUR</span> <span class="k">FROM</span> <span class="p">(</span><span class="nb">time</span><span class="p">::</span><span class="nb">timestamp</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'Europe/Helsinki'</span><span class="p">))</span> <span class="k">BETWEEN</span> <span class="mi">7</span> <span class="k">AND</span> <span class="mi">20</span>
  <span class="k">AND</span> <span class="n">date_trunc</span><span class="p">(</span><span class="s1">'month'</span><span class="p">,</span> <span class="nb">time</span><span class="p">,</span> <span class="s1">'Europe/Helsinki'</span><span class="p">)</span> <span class="o">=</span>
      <span class="n">date_trunc</span><span class="p">(</span><span class="s1">'month'</span><span class="p">,</span> <span class="s1">'2024-02-01'</span><span class="p">::</span><span class="nb">timestamp</span><span class="p">,</span> <span class="s1">'Europe/Helsinki'</span><span class="p">)</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">value</span> <span class="k">DESC</span>
<span class="k">LIMIT</span> <span class="mi">1</span>
</code></pre></div></div>

<p>This gives gives us <code class="language-plaintext highlighter-rouge">1437</code> at <code class="language-plaintext highlighter-rouge">2024-02-29 21:00:00.000000 +00:00</code>, which is wrong. Three hours after the correct value?
Can you spot the error?</p>

<p>There’s a spurious cast <code class="language-plaintext highlighter-rouge">::timestamp</code> which casts <code class="language-plaintext highlighter-rouge">time</code> to type <code class="language-plaintext highlighter-rouge">TIMESTAMP WITHOUT TIMEZONE</code> i.e. it strips timezone
information out from the timestamp, then “converts it from Finnish time” i.e. shifts it two ours backward to get back to
UTC. And voíla - we have a stamp in the range, albeit all wrong. Here, the correct syntax is simply no casts, but we
could also cast with <code class="language-plaintext highlighter-rouge">::timestamptz</code>, which would solve our problem as well.</p>

<p>Sometimes PostgreSQL fails to implicit cast timestamps, so to be sure we just always use tz-version, right? Let’s say we
need the first value of the month. We write something like this:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">value</span><span class="p">,</span> <span class="nb">time</span> <span class="k">FROM</span> <span class="n">measurement</span> 
<span class="k">WHERE</span> <span class="nb">time</span> <span class="o">&gt;=</span> <span class="s1">'2024-02-01'</span><span class="p">::</span><span class="n">timestamptz</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'Europe/Helsinki'</span> 
<span class="k">ORDER</span> <span class="k">BY</span> <span class="nb">time</span> <span class="k">limit</span> <span class="mi">1</span><span class="p">;</span>
</code></pre></div></div>
<p>-&gt; <code class="language-plaintext highlighter-rouge">746,2024-02-01 02:00:00.000000 +00:00</code></p>

<p>Not quite. Now the logic works against our intention: 2024-02-01 is casted as UTC value (2024-02-01T00:00:00Z), then
converted to Helsinki time i.e. two hours forward. <code class="language-plaintext highlighter-rouge">::timestamp</code> would have done what wanted here - takes the naive date
“at time zone ‘Europe/Helsinki’”, and the end result is UTC timestamped (all PostgresSQL timestamps are UTC internally)
correctly. Weird but OK, I guess it makes sense.</p>

<p>You can also use the <code class="language-plaintext highlighter-rouge">date_trunc</code> function, which takes a third parameter of the timezone. This might make the intention
(first of month) more clear:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">value</span><span class="p">,</span>
       <span class="nb">time</span>
<span class="k">FROM</span> <span class="n">Measurement</span>
<span class="k">WHERE</span> <span class="n">date_trunc</span><span class="p">(</span><span class="s1">'month'</span><span class="p">,</span> <span class="nb">time</span><span class="p">,</span> <span class="s1">'Europe/Helsinki'</span><span class="p">)</span> <span class="o">=</span> <span class="n">date_trunc</span><span class="p">(</span><span class="s1">'month'</span><span class="p">,</span> <span class="s1">'2024-02-01'</span><span class="p">,</span> <span class="s1">'Europe/Helsinki'</span><span class="p">)</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">value</span> <span class="k">ASC</span>
<span class="k">LIMIT</span> <span class="mi">1</span>
</code></pre></div></div>

<p>Interestingly here, it doesn’t matter if you cast ‘2024-02-01’ in the date_trunc method - the result is the same. These
all produce the same value:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">value</span><span class="p">,</span>
       <span class="n">date_trunc</span><span class="p">(</span><span class="s1">'month'</span><span class="p">,</span> <span class="s1">'2024-02-01'</span><span class="p">::</span><span class="n">timestamptz</span><span class="p">,</span> <span class="s1">'Europe/Helsinki'</span><span class="p">)</span> <span class="k">as</span> <span class="n">tz</span><span class="p">,</span>
       <span class="n">date_trunc</span><span class="p">(</span><span class="s1">'month'</span><span class="p">,</span> <span class="s1">'2024-02-01'</span><span class="p">::</span><span class="nb">timestamp</span><span class="p">,</span> <span class="s1">'Europe/Helsinki'</span><span class="p">)</span> <span class="k">as</span> <span class="n">notz</span><span class="p">,</span>
       <span class="n">date_trunc</span><span class="p">(</span><span class="s1">'month'</span><span class="p">,</span> <span class="s1">'2024-02-01'</span><span class="p">,</span> <span class="s1">'Europe/Helsinki'</span><span class="p">)</span> <span class="k">as</span> <span class="n">plain</span><span class="p">,</span>
       <span class="nb">time</span>
<span class="k">FROM</span> <span class="n">Measurement</span>
<span class="k">WHERE</span> <span class="n">date_trunc</span><span class="p">(</span><span class="s1">'month'</span><span class="p">,</span> <span class="nb">time</span><span class="p">,</span> <span class="s1">'Europe/Helsinki'</span><span class="p">)</span> <span class="o">=</span> <span class="n">date_trunc</span><span class="p">(</span><span class="s1">'month'</span><span class="p">,</span> <span class="s1">'2024-02-01'</span><span class="p">::</span><span class="n">timestamptz</span><span class="p">,</span> <span class="s1">'Europe/Helsinki'</span><span class="p">)</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">value</span> <span class="k">ASC</span>
<span class="k">LIMIT</span> <span class="mi">1</span>
</code></pre></div></div>

<h3 id="querying-many-key-values">Querying many key values</h3>

<p>From our original questions, let’s try to answer many key values at once:</p>
<ul>
  <li>Average and peak workday value (and time)</li>
  <li>Average and peak “other time” values</li>
</ul>

<p>We might end up with something like this:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">WITH</span> <span class="n">workday</span> <span class="k">AS</span> <span class="p">(</span><span class="k">SELECT</span> <span class="n">value</span><span class="p">,</span>
                        <span class="nb">time</span>
                 <span class="k">FROM</span> <span class="n">Measurement</span>
                 <span class="k">WHERE</span> <span class="k">EXTRACT</span><span class="p">(</span><span class="n">DOW</span> <span class="k">FROM</span> <span class="p">(</span><span class="nb">time</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'Europe/Helsinki'</span><span class="p">))</span> <span class="k">BETWEEN</span> <span class="mi">1</span> <span class="k">AND</span> <span class="mi">5</span>
                   <span class="k">AND</span> <span class="k">EXTRACT</span><span class="p">(</span><span class="n">HOUR</span> <span class="k">FROM</span> <span class="p">(</span><span class="nb">time</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'Europe/Helsinki'</span><span class="p">))</span> <span class="k">BETWEEN</span> <span class="mi">7</span> <span class="k">AND</span> <span class="mi">20</span>
                   <span class="k">AND</span> <span class="n">date_trunc</span><span class="p">(</span><span class="s1">'month'</span><span class="p">,</span> <span class="nb">time</span><span class="p">,</span> <span class="s1">'Europe/Helsinki'</span><span class="p">)</span> <span class="o">=</span>
                       <span class="n">date_trunc</span><span class="p">(</span><span class="s1">'month'</span><span class="p">,</span> <span class="s1">'2024-02-01'</span><span class="p">,</span> <span class="s1">'Europe/Helsinki'</span><span class="p">)),</span>
     <span class="n">ohter_time</span> <span class="k">as</span> <span class="p">(</span><span class="k">SELECT</span> <span class="n">value</span><span class="p">,</span>
                           <span class="nb">time</span>
                    <span class="k">FROM</span> <span class="n">Measurement</span>
                    <span class="k">WHERE</span> <span class="k">NOT</span> <span class="p">(</span><span class="k">EXTRACT</span><span class="p">(</span><span class="n">DOW</span> <span class="k">FROM</span> <span class="p">(</span><span class="nb">time</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'Europe/Helsinki'</span><span class="p">))</span> <span class="k">BETWEEN</span> <span class="mi">1</span> <span class="k">AND</span> <span class="mi">5</span>
                        <span class="k">AND</span> <span class="k">EXTRACT</span><span class="p">(</span><span class="n">HOUR</span> <span class="k">FROM</span> <span class="p">(</span><span class="nb">time</span> <span class="k">AT</span> <span class="nb">TIME</span> <span class="k">ZONE</span> <span class="s1">'Europe/Helsinki'</span><span class="p">))</span> <span class="k">BETWEEN</span> <span class="mi">7</span> <span class="k">AND</span> <span class="mi">20</span><span class="p">)</span>
                      <span class="k">AND</span> <span class="n">date_trunc</span><span class="p">(</span><span class="s1">'month'</span><span class="p">,</span> <span class="nb">time</span><span class="p">,</span> <span class="s1">'Europe/Helsinki'</span><span class="p">)</span> <span class="o">=</span>
                          <span class="n">date_trunc</span><span class="p">(</span><span class="s1">'month'</span><span class="p">,</span> <span class="s1">'2024-02-01'</span><span class="p">,</span> <span class="s1">'Europe/Helsinki'</span><span class="p">)),</span>
     <span class="n">max_other</span> <span class="k">as</span> <span class="p">(</span><span class="k">SELECT</span> <span class="n">value</span> <span class="k">AS</span> <span class="n">work_max</span><span class="p">,</span>
                         <span class="nb">time</span>  <span class="k">AS</span> <span class="n">work_max_time</span>
                  <span class="k">FROM</span> <span class="n">workday</span>
                  <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">value</span> <span class="k">DESC</span>
                  <span class="k">LIMIT</span> <span class="mi">1</span><span class="p">),</span>
     <span class="n">avg_work</span> <span class="k">AS</span> <span class="p">(</span><span class="k">SELECT</span> <span class="k">AVG</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="k">as</span> <span class="n">average</span> <span class="k">FROM</span> <span class="n">workday</span><span class="p">),</span>
     <span class="n">other_max</span> <span class="k">as</span> <span class="p">(</span><span class="k">SELECT</span> <span class="n">value</span> <span class="k">AS</span> <span class="n">other_max_value</span><span class="p">,</span>
                          <span class="nb">time</span>  <span class="k">AS</span> <span class="n">other_max_value_time</span>
                   <span class="k">FROM</span> <span class="n">ohter_time</span>
                   <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">value</span> <span class="k">DESC</span>
                   <span class="k">LIMIT</span> <span class="mi">1</span><span class="p">),</span>
     <span class="n">avg_other</span> <span class="k">AS</span> <span class="p">(</span><span class="k">SELECT</span> <span class="k">AVG</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="k">as</span> <span class="n">average</span> <span class="k">FROM</span> <span class="n">workday</span><span class="p">)</span>
<span class="k">SELECT</span> <span class="n">m</span><span class="p">.</span><span class="o">*</span><span class="p">,</span> <span class="n">a</span><span class="p">.</span><span class="o">*</span><span class="p">,</span> <span class="n">om</span><span class="p">.</span><span class="o">*</span><span class="p">,</span> <span class="n">ao</span><span class="p">.</span><span class="o">*</span>
<span class="k">FROM</span> <span class="n">max_work</span> <span class="n">m</span><span class="p">,</span>
     <span class="n">avg_work</span> <span class="n">a</span><span class="p">,</span>
     <span class="n">max_other</span> <span class="n">om</span><span class="p">,</span>
     <span class="n">avg_other</span> <span class="n">ao</span><span class="p">;</span>
</code></pre></div></div>

<p>ChatGPT does a decent job creating at least a scaffolding of queries like this, but it invents syntax and functions so
you still need to know what you are duing. And makes fun to debug casting issues, which was the inspiration for this
post.</p>

<p>Perhaps in the next post I’ll deal with time ranges and “wall clock” issues.</p>]]></content><author><name>Tatu Lahtela</name></author><category term="postgresql" /><summary type="html"><![CDATA[My current work involves a lot of local time dependent operations. This means answering a lot annoying questions like “Find the maximum value during workdays between 7-21.”, “Highest value during daytime in February (in Country X)”, “Find the average value average value for today” or “Highest value during December”.]]></summary></entry><entry><title type="html">Having Multiple Default Browsers</title><link href="https://lahtela.me/2023/10/19/having-multiple-default-browsers.html" rel="alternate" type="text/html" title="Having Multiple Default Browsers" /><published>2023-10-19T08:53:24+03:00</published><updated>2023-10-19T08:53:24+03:00</updated><id>https://lahtela.me/2023/10/19/having-multiple-default-browsers</id><content type="html" xml:base="https://lahtela.me/2023/10/19/having-multiple-default-browsers.html"><![CDATA[<p>I use many browsers and many profiles in one browser. I would like to open Project 1 related in browser A, other work to
B, memes to C etc. But</p>

<p><img src="highlander.webp" alt="There can be only one" /></p>

<p>So what do? Well simple enough, make a script that relays the openings to different browsers.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
# Link-Relay: Open some links in different browsers. Set the Browser Relay as your default browser.
</span><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">re</span>
<span class="kn">import</span> <span class="nn">sys</span>


<span class="k">def</span> <span class="nf">get_browser</span><span class="p">(</span><span class="n">link</span><span class="p">):</span>
    <span class="n">mappings</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">"work"</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">"browser"</span><span class="p">:</span> <span class="s">"google-chrome --profile-directory=Default"</span><span class="p">,</span>
            <span class="s">"urls"</span><span class="p">:</span> <span class="p">[</span><span class="s">"work"</span><span class="p">,</span> <span class="s">"docs\.google"</span><span class="p">,</span> <span class="s">"forms"</span><span class="p">,</span> <span class="s">"drive"</span><span class="p">]</span>
        <span class="p">},</span>
        <span class="s">"customer"</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">"browser"</span><span class="p">:</span> <span class="s">"google-chrome --profile-directory='Profile 1'"</span><span class="p">,</span>
            <span class="s">"urls"</span><span class="p">:</span> <span class="p">[</span><span class="s">"sap"</span><span class="p">,</span> <span class="s">"outlook"</span><span class="p">,</span> <span class="s">"portal"</span><span class="p">,</span> <span class="s">"github"</span><span class="p">,</span> <span class="s">"microsoftonline"</span><span class="p">,</span> <span class="s">"sharepoint"</span><span class="p">,</span> <span class="s">"customer"</span><span class="p">]</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="n">default</span> <span class="o">=</span> <span class="s">"firefox"</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span> <span class="ow">in</span> <span class="n">mappings</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
        <span class="k">for</span> <span class="n">url</span> <span class="ow">in</span> <span class="n">v</span><span class="p">[</span><span class="s">"urls"</span><span class="p">]:</span>
            <span class="k">if</span> <span class="n">re</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">link</span><span class="p">):</span>
                <span class="k">return</span> <span class="n">v</span><span class="p">[</span><span class="s">"browser"</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">default</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">link</span> <span class="o">=</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
    <span class="n">app</span> <span class="o">=</span> <span class="n">get_browser</span><span class="p">(</span><span class="n">link</span><span class="p">)</span>
    <span class="n">os</span><span class="p">.</span><span class="n">system</span><span class="p">(</span><span class="s">"%s '%s'"</span> <span class="o">%</span> <span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="s">" "</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">:])))</span>
</code></pre></div></div>
<p>Save this script to <code class="language-plaintext highlighter-rouge">/usr/local/bin/link-relay.py</code> (or wherever you want to store these) and make it executable. Update the Exec path below if you do.</p>

<p>Tha <code class="language-plaintext highlighter-rouge">mappings</code> variable contains urls that relay to which browser. Here, all google stuff to Chrome Default browser, MS
related to customer profile, and everything else to Firefox.</p>

<p>Then you need a desktop file for the browser to register is at your default:
So create <code class="language-plaintext highlighter-rouge">~/.local/share/applications/browser-relay.desktop</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Desktop Entry]
Name=Browser Relay
Comment=Sends some applications to different browsers
Keywords=browser;
Type=Application
Exec=/usr/local/bin/link-relay.py %u
Terminal=false
Categories=Network;WebBrowser;
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rss+xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/ftp;x-scheme-handler/chrome;video/webm;application/x-xpinstall;
</code></pre></div></div>

<p>Then run <code class="language-plaintext highlighter-rouge">update-desktop-database</code>, set “Browser Relay” as your default browser and your done! Simply updating the mappings table will update the configuration immediately.</p>]]></content><author><name>Tatu Lahtela</name></author><category term="linux" /><summary type="html"><![CDATA[I use many browsers and many profiles in one browser. I would like to open Project 1 related in browser A, other work to B, memes to C etc. But]]></summary></entry><entry><title type="html">Debugging Org Capture on Firefox</title><link href="https://lahtela.me/2022/06/17/debugging-org-protocol-on-firefox.html" rel="alternate" type="text/html" title="Debugging Org Capture on Firefox" /><published>2022-06-17T09:53:14+03:00</published><updated>2022-06-17T09:53:14+03:00</updated><id>https://lahtela.me/2022/06/17/debugging-org-protocol-on-firefox</id><content type="html" xml:base="https://lahtela.me/2022/06/17/debugging-org-protocol-on-firefox.html"><![CDATA[<p><strong>TL;DR</strong>: Don’t use snap if you want to use Org Capture (or xdg-open with any custom schemes).</p>

<p>I use <a href="https://orgmode.org/">org mode</a> for my brain management, it collects and tracks all my TODOs, organizes my
workflows and acts as a scratch pad for my code snippets and much more. To capture text and links from browsers, there’s
org-protocol in Emacs which allows external tools to invoke org mode, This is in turn used by <a href="https://addons.mozilla.org/en-US/firefox/addon/org-capture/">Org
Capture</a> extension to transfer the data into org.</p>

<p>After upgrading Ubuntu to 22.04 Org Capture is once again broken on Firefox. Let’s try to figure out why.</p>

<h2 id="test-1-verify-org-protocol-works">Test 1: Verify org-protocol works</h2>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>emacsclient <span class="nt">-n</span> <span class="s2">"org-protocol:///capture?url=http%3a%2f%2fduckduckgo%2ecom&amp;title=DuckDuckGo"</span>
</code></pre></div></div>

<p>Success! Emacs opens the template windows. I knew this would work, since Org Capture still works on Chrome.</p>

<h2 id="test-2-enable-debug-logging-on-the-add-on">Test 2: Enable debug logging on the add-on</h2>

<p>Manage Extension-&gt;Preferences-&gt;Debug. Save.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; Capturing the following URI with new org-protocol:  org-protocol://capture?template=L&amp;url=https%3A%2F%2Fduckduckgo.com%2F&amp;title=DuckDuckGo%20%E2%80%94%20Privacy%2C%20simplified.&amp;body=
</code></pre></div></div>

<p>So the add-on works.</p>

<h2 id="test-4-mime-is-registered">Test 4: MIME is registered</h2>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>xdg-mime query default x-scheme-handler/org-protocol
emacsclient.desktop
<span class="nv">$ </span>xdg-open <span class="s2">"org-protocol://capture?template=L&amp;url=https%3A%2F%2Fduckduckgo.com%2F&amp;title=DuckDuckGo%20%E2%80%94%20Privacy%2C%20simplified.&amp;body="</span>
</code></pre></div></div>

<p>Success! So that works. Again, this is expected as it works on Chrome.</p>

<h2 id="test-5-protocol-handler-invocation-from-ff">Test 5: Protocol handler invocation from FF</h2>

<p>Now that I have the developer console open I can browse into the code and set a breakpoint to <code class="language-plaintext highlighter-rouge">capture.js:56</code>, the
location where it tries to send Firefox to the org protocol URI with <code class="language-plaintext highlighter-rouge">location.href = uri</code>.</p>

<p>The uri is <code class="language-plaintext highlighter-rouge">"org-protocol://capture?template=L&amp;url=https%3A%2F%2Fduckduckgo.com%2F&amp;title=DuckDuckGo%20%E2%80%94%20Privacy%2C%20simplified.&amp;body="</code>.</p>

<p>I can invoke <code class="language-plaintext highlighter-rouge">location.href = uri</code> and sure enough, nothing happens. So now I know it doesn’t understand the <code class="language-plaintext highlighter-rouge">org-protocol</code> protocol, or does nothing with it.</p>

<p>The instructions on setting this up seem a bit vague, so let’s try to understand the entire thing. So off to <a href="http://kb.mozillazine.org/Register_protocol">Register Protocol Documentation</a>. The instructions say (for non gconf users):</p>

<blockquote>
  <p>Firefox 3.5 and above</p>

  <p>(Works without installed Gnome libraries)</p>

  <ul>
    <li>Type about:config into the Location Bar (address bar) and press Enter.</li>
    <li>Right-click -&gt; New -&gt; Boolean -&gt; Name: network.protocol-handler.expose.foo -&gt; Value -&gt; false (Replace foo with the protocol you’re specifying)</li>
    <li>Next time you click a link of protocol-type foo you will be asked which application to open it with.</li>
  </ul>
</blockquote>

<p>Not encouraging that the instructions talk about version 3.5 and the page was last modified on 2011. My version is
101.0.1. But let’s continue anyway, it did work very recently anyway. Since I have done this already and I’m in
regression mode, It’s double checking mode.</p>

<ul>
  <li><em>network.protocol-handler.expose.org-protocol</em> is <em>false</em></li>
</ul>

<p>Dead end. I tried the gconf commands as well with no avail.</p>

<h2 id="test-6-reinstall-extension">Test 6: Reinstall extension</h2>

<p>No effect.</p>

<h2 id="test-7-revalidate-org-capture-documentation">Test 7: Revalidate org-capture documentation</h2>

<p>Re-reading <a href="http://orgmode.org/worg/org-contrib/org-protocol.html">org-protocol</a> documentation gave no new insight.</p>

<h2 id="test-8-explicit-org-link">Test 8: Explicit org link</h2>

<p>Let’s create a web page to test:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;html&gt;</span>
  <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"org-protocol://capture?template=L&amp;url=https%3A%2F%2Fduckduckgo.com%2F&amp;title=DuckDuckGo%20%E2%80%94%20Privacy%2C%20simplified.&amp;body="</span><span class="nt">&gt;</span>Org link<span class="nt">&lt;/a&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>
<p>Works on Chrome. Not that this is any different from invoking <code class="language-plaintext highlighter-rouge">location.href</code>.</p>

<h2 id="test-8-make-sure-there-are-no-application-associations-to-org-protocol-in-firefox-settings">Test 8: Make sure there are no application associations to org-protocol in Firefox settings</h2>

<p>There was, clearing it I was able to see the “launch external app” dialog to appear, but after that nothing.</p>

<p>So now I know that FF does understand the protocol, but for some reason it fails to invoke the <code class="language-plaintext highlighter-rouge">xdg-open</code> or whatever mechanism it uses to launch the external tool.</p>

<h2 id="what-has-changed-in-2204">What has changed in 22.04?</h2>

<p>Hmm.. snap has changed. Could it be? No.. Could it? Googling.. Sure enough, <a href="https://forum.snapcraft.io/t/xdg-open-not-working-on-firefox/17670/10">it has issues</a>. So next test <a href="https://www.omgubuntu.co.uk/2022/04/how-to-install-firefox-deb-apt-ubuntu-22-04">install Firefox with deb</a>.</p>

<p>Works like charm.</p>

<p>F you snap.</p>

<h1 id="so-what-is-actually-required">So what is actually required?</h1>

<p>I don’t want to start this all over but my guess is:</p>
<ul>
  <li>Install <a href="https://orgmode.org/worg/org-contrib/org-protocol.html">org-protocol</a>.</li>
  <li>Especially make sure <a href="https://orgmode.org/worg/org-contrib/org-protocol.html#orgb37978d">Desktop Entry</a> with the protocol association.</li>
  <li>Install Org Capture extension.</li>
  <li>Don’t use snap.</li>
  <li>Profit.</li>
</ul>

<p>Can it be made to work with snap? By that snap thread it should work(?) but doesn’t. I don’t know or care. There’s a <a href="https://bugs.launchpad.net/snapd/+bug/1776873">bug</a> to track. I’m out.</p>]]></content><author><name>Tatu Lahtela</name></author><category term="emacs" /><category term="org-mode" /><category term="firefox" /><summary type="html"><![CDATA[TL;DR: Don’t use snap if you want to use Org Capture (or xdg-open with any custom schemes).]]></summary></entry><entry><title type="html">RSI</title><link href="https://lahtela.me/2022/06/01/rsi.html" rel="alternate" type="text/html" title="RSI" /><published>2022-06-01T14:40:46+03:00</published><updated>2022-06-01T14:40:46+03:00</updated><id>https://lahtela.me/2022/06/01/rsi</id><content type="html" xml:base="https://lahtela.me/2022/06/01/rsi.html"><![CDATA[<p>Now that I’m well passed the magical 40yo mark I’ve noticed my fingers aren’t as invincible as they used to be. I get
random RSI strain in mu fingers and especially in my thumbs. Likely a lot from phone usage though, but I’ve noticed my
keyboard habits also have a big impact. I’m not convinced that “ergonomic” keyboards that remove weird angles are a
permanent solution - a permanent solution would be to not use keyboards.  While I do have an ergonomic keyboard - many
in fact - and they do help, it’s more helpful to rotate them just to remove the repetition. My current setup is rotation <a href="https://ultimatehackingkeyboard.com/product/uhk60v2">Ultimate Hacking Keyboard v2</a>, <a href="https://www.logitech.com/fi-fi/products/keyboards/k860-split-ergonomic.html">Logitech K860</a> and a <a href="https://www.realforcekeyboards.com/uk/">Topre Realforce (TKL)</a>.</p>

<p>What also helps is - weirdly enough - typing. More specifically typing text that you don’t normally type which in my case is prose. My current
go-to is <a href="https://www.typelit.io/">typelit.io</a> which allows you to type classic literature. Heck I’ve read many books practicing typing with it!</p>]]></content><author><name>Tatu Lahtela</name></author><category term="rsi" /><category term="typing" /><category term="keyboards" /><summary type="html"><![CDATA[Now that I’m well passed the magical 40yo mark I’ve noticed my fingers aren’t as invincible as they used to be. I get random RSI strain in mu fingers and especially in my thumbs. Likely a lot from phone usage though, but I’ve noticed my keyboard habits also have a big impact. I’m not convinced that “ergonomic” keyboards that remove weird angles are a permanent solution - a permanent solution would be to not use keyboards. While I do have an ergonomic keyboard - many in fact - and they do help, it’s more helpful to rotate them just to remove the repetition. My current setup is rotation Ultimate Hacking Keyboard v2, Logitech K860 and a Topre Realforce (TKL).]]></summary></entry><entry><title type="html">Joseph Heller Catch 22 and Kenny G</title><link href="https://lahtela.me/2022/01/30/Joseph-Heller-Catch-22.html" rel="alternate" type="text/html" title="Joseph Heller Catch 22 and Kenny G" /><published>2022-01-30T22:38:04+02:00</published><updated>2022-01-30T22:38:04+02:00</updated><id>https://lahtela.me/2022/01/30/Joseph-Heller-Catch-22</id><content type="html" xml:base="https://lahtela.me/2022/01/30/Joseph-Heller-Catch-22.html"><![CDATA[<p>(No spoiers about Catch 22)</p>

<p>Just as I was finishing Catch 22 - a book that felt more like a chore than enjoyable, I happened to listen the Econtalk
episode on <a href="https://www.econtalk.org/penny-lane-on-loving-and-loathing-kenny-g/">Kenny G with Penny Lane</a>. In it Russ
refers to Hegel’s Aesthetics and how it narrowed the Western definition of art to mean something that is difficult,
though provoking and challenging. Something simply enjoyable cannot be art by almost definition.</p>

<p>The book appears on many lists of literary classics and it no doubt checks many of the boxes Hegel laid out for good
art. Difficult, though provoking and challenging indeed. Confusing with a paper thing plot I’d say. Personally if this
makes books art, a reminder for myself not read books that are considered good art. The books sure has some fancy
writing, but I’m not after long form poetry in Novel form. I want something that keeps me engaged. This one felt like it
would’ve made it’s point if half the pages. War Sucks.</p>]]></content><author><name>Tatu Lahtela</name></author><category term="econtalk" /><category term="book" /><category term="onething" /><summary type="html"><![CDATA[(No spoiers about Catch 22)]]></summary></entry><entry><title type="html">Econtalk - Lorne Buchman on Creativity</title><link href="https://lahtela.me/2022/01/08/econtalk-lorn-buchman-on-creativity.html" rel="alternate" type="text/html" title="Econtalk - Lorne Buchman on Creativity" /><published>2022-01-08T12:37:19+02:00</published><updated>2022-01-08T12:37:19+02:00</updated><id>https://lahtela.me/2022/01/08/econtalk-lorn-buchman-on-creativity</id><content type="html" xml:base="https://lahtela.me/2022/01/08/econtalk-lorn-buchman-on-creativity.html"><![CDATA[<p><a href="https://www.econtalk.org/lorne-buchman-on-creativity-leadership-and-art/">This episode</a> discussed the eternal struggle
on how to be creative. One of the main points is that many successful creators say that they “follow the characters with
they pen” and “just writing” instead of waiting for inspiration. Exploring by creating <em>something</em> and stumbling upon
the ideas. That discovery of novel ideas is very hard or impossible to control. Stephen King comes to mind who talks a
lot about writing his books this way.</p>

<p>On the “just the follow the characters” method my biggest worry is the endings, which they did get into albeit very
briefly. Many authors that write this way write excellent characters with compelling beginnings only to have the story
fall flat in the end. Case in point King or Lost (JJ. Abrams) mentioned in the podcast. For Abrams this is especially
flagrantly evident in his <a href="https://www.ted.com/talks/j_j_abrams_the_mystery_box">Mystery Box TED Talk</a>. Nice idea, just
expect to be disappointed in the end.</p>]]></content><author><name>Tatu Lahtela</name></author><category term="econtalk" /><category term="onething" /><summary type="html"><![CDATA[This episode discussed the eternal struggle on how to be creative. One of the main points is that many successful creators say that they “follow the characters with they pen” and “just writing” instead of waiting for inspiration. Exploring by creating something and stumbling upon the ideas. That discovery of novel ideas is very hard or impossible to control. Stephen King comes to mind who talks a lot about writing his books this way.]]></summary></entry><entry><title type="html">Econtalk - Frank Rose on Internet Narratives</title><link href="https://lahtela.me/2022/01/05/econtalk-frank-rose-on-internet-narratives.html" rel="alternate" type="text/html" title="Econtalk - Frank Rose on Internet Narratives" /><published>2022-01-05T18:08:45+02:00</published><updated>2022-01-05T18:08:45+02:00</updated><id>https://lahtela.me/2022/01/05/econtalk-frank-rose-on-internet-narratives</id><content type="html" xml:base="https://lahtela.me/2022/01/05/econtalk-frank-rose-on-internet-narratives.html"><![CDATA[<p>Just to prove the power of one thing idea - I had no memory of this until I read my notes…</p>

<p>This <a href="https://www.econtalk.org/frank-rose-on-internet-narratives/">Econtalk episode</a> was again rather non economic which is find. Here are my
On telling compelling stories:</p>
<blockquote>
  <p>People don’t care about what you do, they care why you do it. It’s what differentiates you from everybody else.</p>
</blockquote>

<p>This is always something I will try to keep in mind when making things like presentations. Don’t try to squeeze out the
human from your ideas. People can get the facts in seconds if they wish, but to make them stick they need to be compelling.</p>

<p>Second one as a reminder from Kahnemann:</p>
<blockquote>
  <p>From most experiences we remember the peak moment and the end.</p>
</blockquote>

<p>While a bit contested research, I like to think this is also something nice to keep in mind. Actually have a peak moment
and think about a proper ending.</p>]]></content><author><name>Tatu Lahtela</name></author><category term="podcast" /><category term="econtalk" /><category term="onething" /><summary type="html"><![CDATA[Just to prove the power of one thing idea - I had no memory of this until I read my notes…]]></summary></entry><entry><title type="html">Death’s End - Liu Cixin</title><link href="https://lahtela.me/2021/12/28/deaths-end-liu-cixin.html" rel="alternate" type="text/html" title="Death’s End - Liu Cixin" /><published>2021-12-28T09:44:25+02:00</published><updated>2021-12-28T09:44:25+02:00</updated><id>https://lahtela.me/2021/12/28/deaths-end-liu-cixin</id><content type="html" xml:base="https://lahtela.me/2021/12/28/deaths-end-liu-cixin.html"><![CDATA[<p><em>WARNING might contain spoilers</em></p>

<p>The third and final installment in the <em>Remembrance of Earth’s Past</em> trilogy. Where the first one - The Three Body
Problem - was still rather straight forward scifi with an orient perspective, the next two press on the hard scifi
button harder and the deep end gets deeper. This one picked up where the second book left off. Again my inherent
<del>racism</del> unfamiliarity with Chinese names got the best of me - I got lost multiple times on the characters. Xe? Ji?
Li? But the book was certainly long enough to get you back on track. The symbolism used (sword bearers etc.) was still
rather foreign but it made it all the more interesting.</p>

<p>The scifi certainly gets awesomely bat shit crazy in the end, which was to be expected. Dimensions come and go, millions
of years are jumped.</p>

<p>4/5.</p>]]></content><author><name>Tatu Lahtela</name></author><category term="onething" /><category term="books" /><category term="scifi" /><summary type="html"><![CDATA[WARNING might contain spoilers]]></summary></entry><entry><title type="html">One Thing</title><link href="https://lahtela.me/2021/12/28/one-thing.html" rel="alternate" type="text/html" title="One Thing" /><published>2021-12-28T09:30:04+02:00</published><updated>2021-12-28T09:30:04+02:00</updated><id>https://lahtela.me/2021/12/28/one-thing</id><content type="html" xml:base="https://lahtela.me/2021/12/28/one-thing.html"><![CDATA[<p><a href="https://www.econtalk.org/a-j-jacobs-on-thanks-a-thousand/">A.J. Jacobs in Econtalk</a> notes that we remember nothing from
most stuff we read or otherwise consume some time later. I certainly experience this as much as anyone - even books I
enjoyed quite a bit I can’t recall anything. I thought I’d attempt to write down at least one thing from at least the
books I read. Or rather listen to mostly. But anything and everything goes on this series.</p>]]></content><author><name>Tatu Lahtela</name></author><category term="onething" /><summary type="html"><![CDATA[A.J. Jacobs in Econtalk notes that we remember nothing from most stuff we read or otherwise consume some time later. I certainly experience this as much as anyone - even books I enjoyed quite a bit I can’t recall anything. I thought I’d attempt to write down at least one thing from at least the books I read. Or rather listen to mostly. But anything and everything goes on this series.]]></summary></entry><entry><title type="html">Org Capture Templates with Org Files</title><link href="https://lahtela.me/2021/12/22/org-capture-templates.html" rel="alternate" type="text/html" title="Org Capture Templates with Org Files" /><published>2021-12-22T10:26:03+02:00</published><updated>2021-12-22T10:26:03+02:00</updated><id>https://lahtela.me/2021/12/22/org-capture-templates</id><content type="html" xml:base="https://lahtela.me/2021/12/22/org-capture-templates.html"><![CDATA[<p>I fiddle with my org mode captures templates a lot. This usually involves customizing (or setting)
<a href="https://orgmode.org/manual/Capture-templates.html">org-capture-templates</a>. I found it hard to visualize the end result
the traditional customize-view, so I wrote a small package that allows you to create an org file that contains all
the templates. <a href="https://github.com/ration/capture-org-template.el">capture-org-template</a>. Allows you to do the following:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>* Generic TODO
  :PROPERTIES:
  :DESCRIPTION: Generic TODO item in the Inbox
  :KEY:      t
  :TYPE:     entry
  :TARGET:   file+headline "~/Org/todo.org" "Inbox"
  :END:
** TODO %?
* TODO Email
  :PROPERTIES:
  :DESCRIPTION: TODO mu4e emails with a deadline in two days
  :KEY:      P
  :TARGET:   file+olp "~/Org/todo.org" "Inbox"
  :OPTIONS: :empty-lines 1
  :END:
** TODO %:fromname: %a %?
   DEADLINE: %(org-insert-time-stamp (org-read-date nil t "+2d"))
* Add new capture template
  :PROPERTIES:
  :KEY:      M
  :TARGET:   file "~/Org/capture.org"
  :DESCRIPTION: Add new capture template. Prompt for key and description
  :END:
** %^{Capture name}
  %^{KEY}p%^{TARGET|file "~/Org/todo.org"}p%^{DESCRIPTION}p
*** %?
</code></pre></div></div>

<p>This configuration creates 3 templates (main level), “Generic TODO”, “TODO Email” and “Add new capture template”. The
last one can be used to add more capture templates for example.</p>

<p>While this format should be rather self evident for anyone familiar with capture templates, here is an explanation:</p>
<ul>
  <li>Root level determines a new template.</li>
  <li>Any levels under the root is the <em>template</em> parameter <em>moved one level up</em>. Inside the template all <a href="https://orgmode.org/manual/Template-expansion.html">template
extensions</a> are freely available.</li>
  <li><em>KEY</em> Property determines capture key.</li>
  <li><em>TYPE</em> Defaults to entry, capture type parameters.</li>
  <li><em>TARGET</em> The <a href="https://orgmode.org/manual/Template-elements.html">target</a> element.</li>
  <li><em>OPTIONS</em> The properties parameter in org templates.</li>
  <li><em>DESCRIPTION</em> Is just for the org file and discarded.</li>
</ul>]]></content><author><name>Tatu Lahtela</name></author><category term="emacs" /><category term="org-mode" /><summary type="html"><![CDATA[I fiddle with my org mode captures templates a lot. This usually involves customizing (or setting) org-capture-templates. I found it hard to visualize the end result the traditional customize-view, so I wrote a small package that allows you to create an org file that contains all the templates. capture-org-template. Allows you to do the following:]]></summary></entry></feed>