<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dimitris Zorbas</title>
    <description>The latest articles on DEV Community by Dimitris Zorbas (@zorbash).</description>
    <link>https://dev.to/zorbash</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F138102%2F3c95bcea-0214-4956-9540-2db738d2d59f.jpeg</url>
      <title>DEV Community: Dimitris Zorbas</title>
      <link>https://dev.to/zorbash</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zorbash"/>
    <language>en</language>
    <item>
      <title>A Livebook Smart-Cell to Render Diagrams</title>
      <dc:creator>Dimitris Zorbas</dc:creator>
      <pubDate>Sun, 20 Nov 2022 00:00:00 +0000</pubDate>
      <link>https://dev.to/zorbash/a-livebook-smart-cell-to-render-diagrams-fpm</link>
      <guid>https://dev.to/zorbash/a-livebook-smart-cell-to-render-diagrams-fpm</guid>
      <description>&lt;p&gt;I wrote my first Livebook smart-cell which renders diagrams from a textual description.&lt;/p&gt;

&lt;p&gt;I recently discovered &lt;a href="https://kroki.io" rel="noopener noreferrer"&gt;Kroki&lt;/a&gt;, an opensource tool which generates&lt;br&gt;
images from the text notation of popular diagramming languages. I then&lt;br&gt;
thought, &lt;a href="https://livebook.dev" rel="noopener noreferrer"&gt;Livebook&lt;/a&gt; makes such an excellent application companion for&lt;br&gt;
runnable documentation. What if we could easily embed architectural (or any kind) of diagrams&lt;br&gt;
without having to go through some complicated pipeline to convert the&lt;br&gt;
diagram definition into an image first?&lt;/p&gt;
&lt;h2&gt;
  
  
  Yes, we can!
&lt;/h2&gt;

&lt;p&gt;An old Chinese proverb goes "Up-to-date runnable documentation with diagrams is worth a thousand Confluence pages".&lt;br&gt;
Elixir's standard documentation generator, &lt;a href="https://github.com/elixir-lang/ex_doc/" rel="noopener noreferrer"&gt;ex_doc&lt;/a&gt; makes it&lt;br&gt;
trivial to include Livebook notebooks in your documentation and run them.&lt;br&gt;
It even supports &lt;a href="https://mermaid-js.github.io/" rel="noopener noreferrer"&gt;Mermaid&lt;/a&gt; out of the box, you just need to wrap the source&lt;br&gt;
in mermaid backticks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;```mermaid
graph TD;
  A--&amp;gt;B;
  A--&amp;gt;C;
  B--&amp;gt;D;
  C--&amp;gt;D;
```
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What if your diagrams are not part-fish? No problem, there's &lt;a href="https://github.com/zorbash/kino_kroki" rel="noopener noreferrer"&gt;kino_kroki&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You quickly can give it a go by following the link below:  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://livebook.dev/run?url=https%3A%2F%2Fhexdocs.pm%2Fkino_kroki%2Fexamples.livemd" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu1api4a2jhg9zzm7x9d8.png" alt="Livebook badge"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: Please mind that you need to be running Livebook &amp;gt; 0.6&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To try it from an existing notebook, throw &lt;code&gt;kino_kroki&lt;/code&gt; to the mix with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="no"&gt;Mix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="ss"&gt;:kino_kroki&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you'll notice a new type of smart-cell titled &lt;code&gt;diagram&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe92hxmwfy0ikmdkxb40t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe92hxmwfy0ikmdkxb40t.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An editor will appear where you can paste and edit the diagram source&lt;br&gt;
and when you evaluate the cell, the diagram will be rendered.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9qfthlaha6wh51k2vtdu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9qfthlaha6wh51k2vtdu.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Prior Art
&lt;/h2&gt;

&lt;p&gt;Smart-cells are a powerful way to extend the functionality of Livebook.&lt;br&gt;
There's a fantastic guide bundled with Livebook, to help you write your&lt;br&gt;
own smart-cell, which you can read &lt;a href="https://github.com/livebook-dev/livebook/blob/main/lib/livebook/notebook/learn/kino/smart_cells.livemd" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I took inspiration from &lt;a href="https://github.com/sdball/github_graphql_smartcell" rel="noopener noreferrer"&gt;github_graphql_smartcell&lt;/a&gt; which is somewhat similar to &lt;code&gt;Kino.Kroki&lt;/code&gt; in the sense that they both include a source editor.&lt;/p&gt;
&lt;h2&gt;
  
  
  How it Works
&lt;/h2&gt;

&lt;p&gt;As mentioned in the intro, this smart-cell is powered by &lt;a href="https://kroki.io" rel="noopener noreferrer"&gt;Kroki&lt;/a&gt;.&lt;br&gt;
It's API takes the diagram source as a URL encoded and compressed string&lt;br&gt;
and responds with an image.&lt;/p&gt;

&lt;p&gt;The heart of the smart-cell is this tiny module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Kroki&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@moduledoc&lt;/span&gt; &lt;span class="sd"&gt;"""
  A simple encoder for the online diagram renderer https://kroki.io/
  """&lt;/span&gt;

  &lt;span class="nv"&gt;@doc&lt;/span&gt; &lt;span class="sd"&gt;"""
  Returns a `Kino.Markdown` image to render the diagram.

  ### Examples

      iex&amp;gt; Kino.Kroki.new(Kino.Kroki.Sample.get(:graphviz), :graphviz)
      %Kino.Markdown{
        content: "![svg](https://kroki.io/graphviz/svg/eJx9kM"
      }
  """&lt;/span&gt;
  &lt;span class="nv"&gt;@spec&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="p"&gt;::&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;::&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Kroki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Samples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;::&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Markdown&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:zlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url_encode64&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="s2"&gt;"https://kroki.io/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/svg/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nv"&gt;&amp;amp;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="s2"&gt;"![svg](&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nv"&gt;&amp;amp;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Markdown&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compressing and encoding in Elixir and OTP as you can see, is particularly simple, compared to&lt;br&gt;
some other platforms (see &lt;a href="https://docs.kroki.io/kroki/setup/encode-diagram/" rel="noopener noreferrer"&gt;examples&lt;/a&gt;). With the time left I contributed the &lt;a href="https://github.com/yuzutech/kroki/pull/1352" rel="noopener noreferrer"&gt;Elixir example&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Making it Smart
&lt;/h3&gt;

&lt;p&gt;So far, there's nothing particularly dynamic about &lt;code&gt;Kino.Kroki&lt;/code&gt;, it's a&lt;br&gt;
regular Elixir module which can return a Markdown image.&lt;/p&gt;

&lt;p&gt;To better understand the concept of a smart-cell, consider it a code&lt;br&gt;
template parameterized through UI interactions. At any point you can&lt;br&gt;
convert a smart-cell into a code cell for further modifications.&lt;/p&gt;

&lt;p&gt;We'll dissect each part of the module below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;KrokiSmartcell&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;JS&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;JS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Live&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartCell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="s2"&gt;"Diagram"&lt;/span&gt;

  &lt;span class="c1"&gt;# omitted&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're defining a module for the smart-cell, which adds &lt;code&gt;Kino.JS&lt;/code&gt; making&lt;br&gt;
the module asset-aware allowing us to inject JavaScript for our cell&lt;br&gt;
through the &lt;code&gt;asset "main.js"&lt;/code&gt; macro as well as CSS through &lt;code&gt;asset "main.css"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We also use &lt;a href="https://hexdocs.pm/kino/Kino.JS.Live.html" rel="noopener noreferrer"&gt;&lt;code&gt;Kino.JS.Live&lt;/code&gt;&lt;/a&gt; since we want the cell to be dynamic,&lt;br&gt;
listening to the &lt;code&gt;update_type&lt;/code&gt; event then setting a diagram sample&lt;br&gt;
according to the selected type.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With the &lt;code&gt;init/2&lt;/code&gt; callback, that receives the argument and the "server"
context, we initialise the type, setting it to "graphviz" and we also
fetch and assign a sample textual definition of a &lt;a href="https://graphviz.org/" rel="noopener noreferrer"&gt;GraphViz&lt;/a&gt; diagram. More
details about the module returning samples will be available further below.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="nv"&gt;@default_type&lt;/span&gt; &lt;span class="s2"&gt;"graphviz"&lt;/span&gt;

&lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;type:&lt;/span&gt; &lt;span class="nv"&gt;@default_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;diagram:&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Kroki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Samples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;@default_type&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With the &lt;code&gt;handle_connect/1&lt;/code&gt;, which is invoked whenever a new client connects,&lt;br&gt;
we set up the initial state of the new client.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;type:&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;diagram:&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;diagram&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;handle_event/3&lt;/code&gt; callback is responsible for handling any messages
sent by the client. We fetch a sample diagram source for the selected
type, update the context and broadcast an event which will be handled
by JavaScript side of this cell and update the editor element.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"update_type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type:&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;diagram&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Kroki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Samples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:diagram&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;diagram&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;broadcast_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"update_type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;type:&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;diagram:&lt;/span&gt; &lt;span class="n"&gt;diagram&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the &lt;code&gt;to_attrs/1&lt;/code&gt; callback we compute the arguments for &lt;code&gt;to_source/1&lt;/code&gt;. We set the &lt;code&gt;type&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;diagram&lt;/code&gt; source.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;to_attrs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"diagram"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;diagram&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We implement &lt;code&gt;to_source/1&lt;/code&gt; which returns the Elixir code for our cell.&lt;br&gt;
This is what will replace our smart-cell if we convert it to a "dumb" cell.&lt;br&gt;
We return the AST of the Elixir source which by calling &lt;code&gt;Kino.Kroki.new/2&lt;/code&gt; renders a markdown image&lt;br&gt;
generated by the &lt;a href="https://kroki.io" rel="noopener noreferrer"&gt;Kroki&lt;/a&gt; server.&lt;br&gt;
&lt;code&gt;Kino.SmartCell.quoted_to_string/1&lt;/code&gt; converts the AST to a readable, formatted code string.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;to_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;quote&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Kroki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kn"&gt;unquote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"diagram"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="kn"&gt;unquote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartCell&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quoted_to_string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JavaScript source of Kino.Kroki is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;  &lt;span class="n"&gt;asset&lt;/span&gt; &lt;span class="s2"&gt;"main.js"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="sd"&gt;"""
    export function init(ctx, payload) {
      ctx.importCSS("main.css");
      ctx.importCSS("https://fonts.googleapis.com/css2?family=Inter:wght@400;500&amp;amp;display=swap");

      root.innerHTML = `
        &amp;lt;div class="app"&amp;gt;
          &amp;lt;form&amp;gt;
            &amp;lt;div class="container"&amp;gt;
              &amp;lt;div class="row header"&amp;gt;
                &amp;lt;div class="inline-field"&amp;gt;
                  &amp;lt;label class="inline-input-label label"&amp;gt;Diagram type&amp;lt;/label&amp;gt;
                  &amp;lt;select class="input" name="type"/&amp;gt;
                    &amp;lt;option value="blockdiag"&amp;gt;BlockDiag&amp;lt;/option&amp;gt;
                    &amp;lt;!-- rest of the options omitted --&amp;gt;
                  &amp;lt;/select&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;div class="logo"&amp;gt;
                  &amp;lt;span&amp;gt;Powered by: &amp;lt;/span&amp;gt;
                  &amp;lt;a href="https://kroki.io"&amp;gt;
                    &amp;lt;img alt="kroki" src="https://kroki.io/assets/logo.svg"/&amp;gt;
                  &amp;lt;/a&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/div&amp;gt;

              &amp;lt;div class="row"&amp;gt;
                &amp;lt;div class="field grow"&amp;gt;
                  &amp;lt;label class="input-label"&amp;gt;Diagram Source&amp;lt;/label&amp;gt;
                  &amp;lt;textarea
                    id="diagram-source"
                    name="diagram"
                    class="input textarea code"
                    placeholder=""
                    rows="25"&amp;gt;#{Kino.Kroki.Samples.get(@default_type)}&amp;lt;/textarea&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/div&amp;gt;
            &amp;lt;/container&amp;gt;
          &amp;lt;/form&amp;gt;
        &amp;lt;/div&amp;gt;
      `;

      const typeEl = ctx.root.querySelector(`[name="type"]`);
      const diagramEl = ctx.root.querySelector(`#diagram-source`);

      typeEl.addEventListener("change", (event) =&amp;gt; {
        ctx.pushEvent("update_type", event.target.value);
      });

      ctx.handleEvent("update_type", (event) =&amp;gt; {
        typeEl.value = event.type;
        diagramEl.value = event.diagram;
      });

      ctx.handleSync(() =&amp;gt; {
        // Synchronously invokes change listeners
        document.activeElement &amp;amp;&amp;amp;
          document.activeElement.dispatchEvent(new Event("change"));
      });
    }
    """&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use the &lt;code&gt;asset/2&lt;/code&gt; macro to define the necessary JS code inline, but&lt;br&gt;
there's also the option to set a path to load assets with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;JS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;assets_path:&lt;/span&gt; &lt;span class="s2"&gt;"lib/assets/kino_kroki"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to export an &lt;code&gt;init&lt;/code&gt; function for our JavaScript module. The&lt;br&gt;
first parameter &lt;code&gt;ctx&lt;/code&gt; is the client-side context which provides a&lt;br&gt;
variety of useful functions such as &lt;code&gt;importCSS&lt;/code&gt; which we use to load CSS&lt;br&gt;
from a URL.&lt;/p&gt;

&lt;p&gt;We initialise the root element of the smart-cell with &lt;code&gt;root.innerHTML&lt;/code&gt;&lt;br&gt;
and set up event handlers. With &lt;code&gt;ctx.pushEvent&lt;/code&gt; we push a client-side&lt;br&gt;
change to the cell server and with &lt;code&gt;ctx.handleEvent&lt;/code&gt; we apply changes&lt;br&gt;
from the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;typeEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;update_type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;update_type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;typeEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;diagramEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;diagram&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleSync&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Synchronously invokes change listeners&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeElement&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complete source of &lt;code&gt;Kino.Kroki&lt;/code&gt; can be found &lt;a href="https://github.com/zorbash/kino_kroki/blob/master/lib/kino_kroki_smartcell.ex" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finishing Touches
&lt;/h2&gt;

&lt;p&gt;We need to make sure Livebook knows about this new smart-cell to list it&lt;br&gt;
as one of the available options.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;mix.exs&lt;/code&gt; of &lt;code&gt;Kino.Kroki&lt;/code&gt; there is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;application&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="ss"&gt;mod:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Kroki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and in the application module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Kroki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Application&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@moduledoc&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;

  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Application&lt;/span&gt;

  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# 👇 This tells Kino we implemented a smart-cell&lt;/span&gt;
    &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartCell&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;KrokiSmartcell&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;Supervisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="ss"&gt;strategy:&lt;/span&gt; &lt;span class="ss"&gt;:one_for_one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="no"&gt;KinoDB&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Supervisor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Diagram Samples
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Kino.Kroki&lt;/code&gt; is the last module of the smart-cell. It reads and parses a text file&lt;br&gt;
containing samples for the diagram types Kroki can generate images from.&lt;br&gt;
Let's take it apart to see if there are any useful patterns in there (hint: there are).&lt;/p&gt;

&lt;p&gt;The text file containing the samples has the following format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;---&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;blockdiag&lt;/span&gt; &lt;span class="o"&gt;---&amp;gt;&lt;/span&gt;
&lt;span class="nx"&gt;blockdiag&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Kroki&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;generates&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Block diagrams&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;Kroki&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;very easy!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;Kroki&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;greenyellow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Block diagrams&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pink&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;very easy!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;---&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;wavedrom&lt;/span&gt; &lt;span class="o"&gt;---&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="na"&gt;wave&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p.....|...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="na"&gt;wave&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x.345x|=.x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;head&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="na"&gt;wave&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0.1..0|1.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acknowledge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;wave&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.....|01.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We start by assigning the location of the file to a module attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Kroki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Samples&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@samples_file&lt;/span&gt; &lt;span class="ss"&gt;:code&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priv_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:kino_kroki&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"samples.txt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="nv"&gt;@external_resource&lt;/span&gt; &lt;span class="nv"&gt;@samples_file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We instruct the compiler to recompile &lt;code&gt;Kiko.Kroki.Samples&lt;/code&gt; each time there's a change to the samples file.&lt;/p&gt;

&lt;p&gt;We then parse the file into a map where the keys are diagram types and the values are samples.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="nv"&gt;@samples_separator&lt;/span&gt; &lt;span class="sr"&gt;~r/&amp;lt;--- sample:(?&amp;lt;type&amp;gt;.+) ---&amp;gt;\n/m&lt;/span&gt;
&lt;span class="nv"&gt;@samples&lt;/span&gt; &lt;span class="nv"&gt;@samples_file&lt;/span&gt;
         &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read!&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
         &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;@samples_separator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="ss"&gt;include_captures:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="ss"&gt;trim:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
         &lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;
           &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;diagram&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
             &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_atom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Regex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;named_captures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;@samples_separator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
              &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;diagram&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
         &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also define a &lt;code&gt;type&lt;/code&gt; type, which is a union of all the supported diagram types.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="nv"&gt;@type&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;::&lt;/span&gt; &lt;span class="kn"&gt;unquote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;@samples&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:|&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;&amp;amp;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;&amp;amp;2&lt;/span&gt;&lt;span class="p"&gt;]}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is rendered nicely in the &lt;a href="https://hexdocs.pm/kino_kroki/Kino.Kroki.Samples.html#t:type/0" rel="noopener noreferrer"&gt;docs&lt;/a&gt; and helps the user of this library&lt;br&gt;
provide a valid argument when used programmatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outro
&lt;/h2&gt;

&lt;p&gt;There's a lot of ground to cover with the recent extensibility&lt;br&gt;
improvements the amazing developers, community and sponsors a bringing&lt;br&gt;
to Livebook. I recommend checking out the docs for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hexdocs.pm/kino/Kino.JS.html" rel="noopener noreferrer"&gt;Kino.JS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hexdocs.pm/kino/Kino.JS.Live.html" rel="noopener noreferrer"&gt;Kino.JS.Live&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hexdocs.pm/kino/Kino.SmartCell.html" rel="noopener noreferrer"&gt;Kino.SmartCell&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are seeking inspiration for your next notebook or smart-cell,&lt;br&gt;
check out &lt;a href="https://notes.club/" rel="noopener noreferrer"&gt;notes.club&lt;/a&gt; which is an Elixir app to&lt;br&gt;
help you discover notebooks contributed by the community.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Coming soom to a notebook near you "an &lt;a href="https://github.com/open-api-spex/open_api_spex" rel="noopener noreferrer"&gt;&lt;code&gt;open_api_spex&lt;/code&gt;&lt;/a&gt; smart-cell", stay tuned!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Spotted a Mistake?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Please contact me on &lt;a href="https://twitter.com/_zorbash" rel="noopener noreferrer"&gt;twitter&lt;/a&gt;, or in the comments, or &lt;a href="https://github.com/zorbash/zorbash.github.com" rel="noopener noreferrer"&gt;submit a PR&lt;/a&gt; for corrections.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>livebook</category>
    </item>
    <item>
      <title>A Guide to Secure Elixir Package Updates</title>
      <dc:creator>Dimitris Zorbas</dc:creator>
      <pubDate>Tue, 29 Mar 2022 11:46:06 +0000</pubDate>
      <link>https://dev.to/appsignal/a-guide-to-secure-elixir-package-updates-39kf</link>
      <guid>https://dev.to/appsignal/a-guide-to-secure-elixir-package-updates-39kf</guid>
      <description>&lt;p&gt;Keeping your dependencies up-to-date is essential to ensure that your applications stay healthy, secure, and performant. Thankfully, the BEAM ecosystem has its own package manager, &lt;a href="https://hex.pm/"&gt;Hex&lt;/a&gt;, which is fast, mature, and simple to use.&lt;/p&gt;

&lt;p&gt;This article explores the available tools and commands to manage Hex dependencies and some tips to make the process more enjoyable.&lt;/p&gt;

&lt;p&gt;Let's dive in!&lt;/p&gt;

&lt;h2&gt;
  
  
  List Updatable Dependencies in Your Elixir App
&lt;/h2&gt;

&lt;p&gt;You can use the commands below to understand the relationships between dependencies before you attempt to update any of them.&lt;/p&gt;

&lt;p&gt;List all your application's dependencies with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix deps &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;*&lt;/span&gt; bunt 0.2.0 &lt;span class="o"&gt;(&lt;/span&gt;Hex package&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;mix&lt;span class="o"&gt;)&lt;/span&gt;
  locked at 0.2.0 &lt;span class="o"&gt;(&lt;/span&gt;bunt&lt;span class="o"&gt;)&lt;/span&gt; 7af5c7e0
  ok
&lt;span class="k"&gt;*&lt;/span&gt; castore 0.1.15 &lt;span class="o"&gt;(&lt;/span&gt;Hex package&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;mix&lt;span class="o"&gt;)&lt;/span&gt;
  locked at 0.1.15 &lt;span class="o"&gt;(&lt;/span&gt;castore&lt;span class="o"&gt;)&lt;/span&gt; c69379b9
  ok
&lt;span class="k"&gt;*&lt;/span&gt; connection 1.1.0 &lt;span class="o"&gt;(&lt;/span&gt;Hex package&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;mix&lt;span class="o"&gt;)&lt;/span&gt;
  locked at 1.1.0 &lt;span class="o"&gt;(&lt;/span&gt;connection&lt;span class="o"&gt;)&lt;/span&gt; 722c1eb0
  ok
&lt;span class="k"&gt;*&lt;/span&gt; cowboy 2.9.0 &lt;span class="o"&gt;(&lt;/span&gt;Hex package&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rebar3&lt;span class="o"&gt;)&lt;/span&gt;
  locked at 2.9.0 &lt;span class="o"&gt;(&lt;/span&gt;cowboy&lt;span class="o"&gt;)&lt;/span&gt; 2c729f93
  ok
&lt;span class="k"&gt;*&lt;/span&gt; cowboy_telemetry 0.4.0 &lt;span class="o"&gt;(&lt;/span&gt;Hex package&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rebar3&lt;span class="o"&gt;)&lt;/span&gt;
  locked at 0.4.0 &lt;span class="o"&gt;(&lt;/span&gt;cowboy_telemetry&lt;span class="o"&gt;)&lt;/span&gt; 7d98bac1
  ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or you can choose to print them in a tree format with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix deps.tree
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces output like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;short
├── credo ~&amp;gt; 1.6 &lt;span class="o"&gt;(&lt;/span&gt;Hex package&lt;span class="o"&gt;)&lt;/span&gt;
│   ├── bunt ~&amp;gt; 0.2.0 &lt;span class="o"&gt;(&lt;/span&gt;Hex package&lt;span class="o"&gt;)&lt;/span&gt;
│   ├── file_system ~&amp;gt; 0.2.8 &lt;span class="o"&gt;(&lt;/span&gt;Hex package&lt;span class="o"&gt;)&lt;/span&gt;
│   └── jason ~&amp;gt; 1.0 &lt;span class="o"&gt;(&lt;/span&gt;Hex package&lt;span class="o"&gt;)&lt;/span&gt;
├── ecto_psql_extras ~&amp;gt; 0.6 &lt;span class="o"&gt;(&lt;/span&gt;Hex package&lt;span class="o"&gt;)&lt;/span&gt;
│   ├── ecto_sql ~&amp;gt; 3.4 &lt;span class="o"&gt;(&lt;/span&gt;Hex package&lt;span class="o"&gt;)&lt;/span&gt;
│   ├── postgrex &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; 0.15.7 &lt;span class="o"&gt;(&lt;/span&gt;Hex package&lt;span class="o"&gt;)&lt;/span&gt;
│   └── table_rex ~&amp;gt; 3.1.1 &lt;span class="o"&gt;(&lt;/span&gt;Hex package&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To produce an image output, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix deps.tree &lt;span class="nt"&gt;--format&lt;/span&gt; dot &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; dot &lt;span class="nt"&gt;-Tpng&lt;/span&gt; deps_tree.dot &lt;span class="nt"&gt;-o&lt;/span&gt; deps_tree.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This option requires &lt;code&gt;Graphviz&lt;/code&gt;. &lt;a href="https://graphviz.org/download/"&gt;Read these Graphviz instructions&lt;/a&gt; to install it on your system.&lt;/p&gt;

&lt;p&gt;Then open the created &lt;code&gt;deps_tree.png&lt;/code&gt; file with a viewer of your choice. Being able to quickly visualize dependencies can help you decide whether a package is worth keeping in your mix.lock. A package could be pulling too many sub-dependencies or might not even be used in your app. Remember, the fewer dependencies, the easier it is to keep things up-to-date.&lt;/p&gt;

&lt;p&gt;When you remove a dependency from mix.exs, it will remain in mix.lock. To remove unused dependencies, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix deps.clean &lt;span class="nt"&gt;--unlock&lt;/span&gt; &lt;span class="nt"&gt;--unused&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check for Outdated Dependencies with Hex
&lt;/h3&gt;

&lt;p&gt;After making sense of your dependencies tree and performing any necessary cleanups, check for outdated packages with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix hex.outdated &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output will resemble:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Dependency              Current  Latest  Status
bunt                    0.2.0    0.2.0   Up-to-date
cowlib                  2.11.0   2.11.0  Up-to-date
credo                   1.6.1    1.6.3   Update possible
db_connection           2.4.1    2.4.1   Up-to-date
decimal                 2.0.0    2.0.0   Up-to-date
earmark_parser          1.4.19   1.4.20  Update possible
postgrex                0.15.13  0.16.2  Update not possible

To view the diffs &lt;span class="k"&gt;in &lt;/span&gt;each available update, visit:
https://hex.pm/l/AsY7q
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;code&gt;--all&lt;/code&gt; flag shows all outdated packages, including the children of packages defined in mix.exs.&lt;/p&gt;

&lt;p&gt;Notice the link in the output above. Hex prepares a nice page for us to inspect the diffs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QkIC73bM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.appsignal.com/images/blog/2022-03/hexdiff.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QkIC73bM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.appsignal.com/images/blog/2022-03/hexdiff.png" alt="hexdiff-screenshot" width="880" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro-Tip&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use the &lt;code&gt;--within-requirements&lt;/code&gt; flag in your CI to notify you of available updates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix hex.outdated &lt;span class="nt"&gt;--within-requirements&lt;/span&gt; 1&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'Updates available!'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output of Elixir's package management tasks tends to be concise, well-documented and precisely guides you towards actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspecting Changes with Hex
&lt;/h2&gt;

&lt;p&gt;To see changes between two package versions in the terminal, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# This will display the diff for the package opus&lt;/span&gt;
mix hex.package diff opus 0.7.0 0.8.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/var/folders/nc/t881hgn/T/opus-0.7.0-9F0FC44B/mix.exs b/var/folders/nc/t881/T/opus-0.8.1-2BEF6AED/mix.exs
index 0aba420..0c1ad3e 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/var/folders/nc/t881/T/opus-0.7.0-9F0FC44B/mix.exs
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/var/folders/nc/t881/T/opus-0.8.1-2BEF6AED/mix.exs
&lt;/span&gt;&lt;span class="p"&gt;@@ -4,7 +4,7 @@&lt;/span&gt; defmodule Opus.Mixfile do
   def project do
     [
       app: :opus,
&lt;span class="gd"&gt;-      version: "0.7.0",
&lt;/span&gt;&lt;span class="gi"&gt;+      version: "0.8.1",
&lt;/span&gt;       elixir: "~&amp;gt; 1.6",
       elixirc_paths: elixirc_paths(Mix.env()),
       build_embedded: Mix.env() == :prod,
&lt;span class="p"&gt;@@ -39,7 +39,7 @@&lt;/span&gt; defmodule Opus.Mixfile do
   defp deps do
     [
       {:retry, "~&amp;gt; 0.8"},
&lt;span class="gd"&gt;-      {:telemetry, "~&amp;gt; 0.4", optional: true},
&lt;/span&gt;&lt;span class="gi"&gt;+      {:telemetry, "~&amp;gt; 0.4 or ~&amp;gt; 1.0", optional: true},
&lt;/span&gt;       {:credo, "~&amp;gt; 0.8.10", only: [:dev, :test], runtime: false},
       {:ex_doc, "~&amp;gt; 0.24.2", only: :dev, runtime: false},
       {:dialyxir, "~&amp;gt; 1.0.0-rc.3", only: [:dev, :test], runtime: false},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can view the diff in the browser by navigating to:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://diff.hex.pm/diff/&amp;lt;package_name&amp;gt;/&amp;lt;version1&amp;gt;..&amp;lt;version2&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For example: &lt;a href="https://diff.hex.pm/diff/opus/0.7.0..0.8.1"&gt;https://diff.hex.pm/diff/opus/0.7.0..0.8.1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hex Diff generates a highlighted git diff which you can view in the browser. You can share the link or even highlight a specific row.&lt;/p&gt;

&lt;p&gt;Third-party dependencies are essentially somebody's code downloaded from the internet, which ends up in your application. There is no shortage of examples where packages have been hijacked and malicious versions uploaded.&lt;/p&gt;

&lt;p&gt;Ideally, you should inspect the diff of every update. Hex seems to be the only package manager with this built-in feature at the moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browsing Changelogs
&lt;/h2&gt;

&lt;p&gt;Ultimately, an update might be available, but is it safe to apply it? Are there any code or configuration changes required for the update to work without issues? The diff between two package versions may contain thousands of lines of templates, tests, and docs that might not seem relevant to you.&lt;/p&gt;

&lt;p&gt;Furthermore, a package might not even follow &lt;a href="https://semver.org/"&gt;Semver&lt;/a&gt; (semver indicates whether the update is safe in compatibility terms).&lt;/p&gt;

&lt;p&gt;Commonly, package maintainers keep a changelog to communicate notable changes and upgrade paths concisely. &lt;a href="https://keepachangelog.com/en/1.0.0/"&gt;Read more about the benefits of keeping a changelog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now the bad news: not all packages have a changelog. So let's go changelog hunting!&lt;/p&gt;

&lt;p&gt;The following task will fetch information for the &lt;code&gt;credo&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix hex.info credo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;A static code analysis tool with a focus on code consistency and teaching.

Config: &lt;span class="o"&gt;{&lt;/span&gt;:credo, &lt;span class="s2"&gt;"~&amp;gt; 1.6"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
Locked version: 1.6.3
Releases: 1.6.3, 1.6.2, 1.6.1, 1.6.0, 1.6.0-rc.1, 1.6.0-rc.0, 1.5.6, 1.5.5, ...

Licenses: MIT
Links:
  Changelog: https://github.com/rrrene/credo/blob/master/CHANGELOG.md &amp;lt;&lt;span class="nt"&gt;---&lt;/span&gt; Here
  GitHub: https://github.com/rrrene/credo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the maintainer has added a link to the changelog, so that's nice of them.&lt;/p&gt;

&lt;p&gt;There is even a link to the changelog in the HexDocs, which some developers may find really handy:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8Vpw9UvB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.appsignal.com/images/blog/2022-03/hexdocs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8Vpw9UvB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.appsignal.com/images/blog/2022-03/hexdocs.png" alt="hexdocs" width="295" height="197"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tips:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensure there's a link to your changelog in &lt;code&gt;mix.exs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Include the changelog in the hexdocs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Automated Changelog Fetching
&lt;/h3&gt;

&lt;p&gt;Hunting for changelogs can get tedious after a while, especially if you want to update many packages. Thankfully, there is now an &lt;em&gt;experimental&lt;/em&gt; package for that.&lt;/p&gt;

&lt;p&gt;You can add it in your dependencies with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:changelog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:dev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:test&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;runtime:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ensure it is fetched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix deps.get
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Invoke it for all updatable packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix changelog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or for a number of packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix changelog tailwind jason

Package: tailwind
Current version: 0.1.4
Latest version:  0.1.5
Hexdiff: https://diff.hex.pm/diff/tailwind/0.1.4..0.1.5

&lt;span class="c"&gt;## v0.1.5 (2022-01-18)&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt; Prune app.js css import to remove required manual step on first &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This will only print the version to update to, a link to the diff, and the changelog when there is a more recent version.&lt;/p&gt;

&lt;p&gt;I wrote this &lt;a href="https://github.com/zorbash/changelog"&gt;open-source task&lt;/a&gt;. It uses some heuristics to locate the changelog by retrieving the Hex package metadata from the API, falling back to common locations in the repo.&lt;/p&gt;

&lt;p&gt;My wish is that Hex will standardize including a link to a changelog in mix.exs and &lt;code&gt;mix hex.info&lt;/code&gt;, and that Hex Diff will be enhanced to include changelog information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating Dependencies in Elixir
&lt;/h2&gt;

&lt;p&gt;To update all dependencies, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix deps.update &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the output, you will see version updates in the following format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Upgraded:
  credo 1.6.1 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 1.6.3
  earmark_parser 1.4.19 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 1.4.20
  ecto_sql 3.7.1 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 3.7.2
  ex_doc 0.27.3 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 0.28.2 &lt;span class="o"&gt;(&lt;/span&gt;minor&lt;span class="o"&gt;)&lt;/span&gt;
  makeup 1.0.5 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 1.1.0
  mint 1.4.0 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 1.4.1
  nimble_parsec 1.2.0 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 1.2.2
  nimble_pool 0.2.5 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 0.2.6
  phoenix_live_dashboard 0.6.2 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 0.6.5
  phoenix_live_view 0.17.5 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 0.17.7
  phoenix_view 1.1.0 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 1.1.2
  plug 1.12.1 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 1.13.3
  postgrex 0.15.13 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 0.16.2 &lt;span class="o"&gt;(&lt;/span&gt;minor&lt;span class="o"&gt;)&lt;/span&gt;
  tailwind 0.1.4 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 0.1.5
New:
  hpax 0.1.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same as running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix deps.unlock &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mix deps.get
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep in mind that this task will try to upgrade to versions that match the specifications in your mix.exs.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:some_package&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 0.9"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if there is a more recent 1.0 version for &lt;code&gt;some_package&lt;/code&gt;, it does not match the specification above, and it won't be upgraded. You will have to change your mix.exs and try again.&lt;/p&gt;

&lt;p&gt;As we saw in the first section of this article, there is a task you can run to check if an update is possible for a package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix hex.outdated some_package
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Dependency              Current  Latest  Status
some_package            0.9.0    1.0.0   Update not possible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There might be a conflict in some cases where the package resolution cannot find a version to satisfy the dependencies in mix.exs.&lt;/p&gt;

&lt;p&gt;A workaround (that you should use with caution) is &lt;code&gt;override&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:some_package&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 1.0"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:other_package&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;override:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, the dependency will override any other definitions by other dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading: Keep Your Package Updates Safe in Elixir
&lt;/h2&gt;

&lt;p&gt;Elixir comes with a whole arsenal of tools to manage dependencies. Try to master them and stir your mix.exs often. Read changelogs and diffs to ensure your updates are safe.&lt;/p&gt;

&lt;p&gt;Here are some resources that can help you dive deeper into this topic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hexdocs.pm/mix/Mix.Tasks.Deps.html"&gt;HexDocs:mix deps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hex.pm/docs/usage"&gt;HexDocs:usage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hexdocs.pm/hex/Mix.Tasks.Hex.Outdated.html"&gt;HexDocs:mix hex.outdated&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hex.pm/about"&gt;Hex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://graphviz.org/download/"&gt;Graphviz-install&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://keepachangelog.com/en/1.0.0/"&gt;Keepachangelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://semver.org/"&gt;Semver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/zorbash/changelog"&gt;Changelog-repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, &lt;a href="https://dev.to/elixir-alchemy"&gt;subscribe to our Elixir Alchemy newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>elixir</category>
    </item>
    <item>
      <title>A personal Brain with Nerves and LiveBook (podcast)</title>
      <dc:creator>Dimitris Zorbas</dc:creator>
      <pubDate>Mon, 27 Dec 2021 10:11:23 +0000</pubDate>
      <link>https://dev.to/zorbash/a-personal-brain-with-nerves-and-livebook-podcast-5bie</link>
      <guid>https://dev.to/zorbash/a-personal-brain-with-nerves-and-livebook-podcast-5bie</guid>
      <description>&lt;p&gt;In the most recent episode of the Elixir Mix podcast, I talked about how I built &lt;a href="https://zorbash.com/post/elixir-nerves-pomodoro-timer/"&gt;Brain&lt;/a&gt; a device to display my Kindle highlights and notes on an e-ink screen using Nerves and Livebook.&lt;/p&gt;

&lt;p&gt;Enjoy!&lt;br&gt;
&lt;a href="https://elixirmix.com/personal-brain-with-nerves-and-livebook-dimitri-zorbas"&gt;https://elixirmix.com/personal-brain-with-nerves-and-livebook-dimitri-zorbas&lt;/a&gt;&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>nerves</category>
      <category>livebook</category>
    </item>
    <item>
      <title>Livebook Animations</title>
      <dc:creator>Dimitris Zorbas</dc:creator>
      <pubDate>Sun, 26 Dec 2021 17:43:38 +0000</pubDate>
      <link>https://dev.to/zorbash/livebook-animations-1a4j</link>
      <guid>https://dev.to/zorbash/livebook-animations-1a4j</guid>
      <description>&lt;p&gt;An exciting new feature landed in &lt;a href="https://github.com/livebook-dev/livebook"&gt;Livebook&lt;/a&gt; (through &lt;a href="https://hexdocs.pm/kino"&gt;Kino&lt;/a&gt;) which gives&lt;br&gt;
the ability to animate any output.&lt;/p&gt;

&lt;p&gt;In the process of experimenting with &lt;a href="https://zorbash.com/post/elixir-nerves-pomodoro-timer/"&gt;Brain&lt;/a&gt; and its camera, I needed to&lt;br&gt;
quickly sketch out some code and output video in a Livebook notebook.&lt;/p&gt;

&lt;p&gt;I thought the following would do the trick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Picam&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next_frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:jpeg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but it creates a new output cell every time &lt;code&gt;Kino.render/1&lt;/code&gt; is called.&lt;/p&gt;

&lt;p&gt;So I posted this issue (&lt;a href="https://github.com/livebook-dev/kino/issues/48"&gt;kino#48&lt;/a&gt;) and implemented a new&lt;br&gt;
widget &lt;code&gt;Kino.ImageDynamic&lt;/code&gt; which can be updated with&lt;br&gt;
&lt;code&gt;Kino.ImageDynamic.push/2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then I also implemented a &lt;code&gt;Kino.clear/0&lt;/code&gt;&lt;br&gt;
function to dynamically clear any output cell, so that its contents can&lt;br&gt;
be replaced by calling render again.&lt;/p&gt;

&lt;p&gt;Thankfully the fruits of this conversation on the issue gave us a more&lt;br&gt;
robust API for animation.&lt;/p&gt;
&lt;h3&gt;
  
  
  Kino.animate/3
&lt;/h3&gt;

&lt;p&gt;This PR &lt;a href="https://github.com/livebook-dev/kino/pull/49"&gt;kino#49&lt;/a&gt; and version &lt;a href="https://github.com/livebook-dev/kino/blob/main/CHANGELOG.md#v031-2021-11-10"&gt;0.3.1&lt;/a&gt; of Kino&lt;br&gt;
bring &lt;code&gt;Kino.Frame&lt;/code&gt; and the &lt;code&gt;Kino.animate/3&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Watch a showcase of the feature below:&lt;/p&gt;


  

&lt;h3&gt;
  
  
  &lt;code&gt;Kino.Frame&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;Kino.Frame.new/0&lt;/code&gt; you can start a new&lt;br&gt;
widget which can be updated with &lt;code&gt;Kino.Frame.render/2&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;widget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WQXNBJT4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9lrkk002xyio3f4d595w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WQXNBJT4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9lrkk002xyio3f4d595w.png" alt="animate cells" width="880" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You'll notice that with the code above we only get a single output&lt;br&gt;
cell which gets updated four times.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;Kino.animate/3&lt;/code&gt; the above can be expressed more concisely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;
  &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:cont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:halt&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Life
&lt;/h3&gt;

&lt;p&gt;Let's put this new API to the test by implementing &lt;a href="https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life"&gt;Life&lt;/a&gt;.&lt;br&gt;
To try this on your Livebook instance by importing &lt;a href="https://gist.github.com/zorbash/9ad9ad70335427a655a170c718427370"&gt;this notebook&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The implemenation is based on this &lt;a href="https://gist.github.com/sasa1977/6877c52c3c35c2c03c82"&gt;gist&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Life&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Grid&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;defstruct&lt;/span&gt; &lt;span class="ss"&gt;data:&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;when&lt;/span&gt; &lt;span class="n"&gt;is_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Life&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Grid&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;data:&lt;/span&gt; &lt;span class="n"&gt;list_to_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Life&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Grid&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;data:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tuple_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;cell_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Life&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Grid&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;data:&lt;/span&gt; &lt;span class="n"&gt;new_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;next_cell_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;&amp;amp;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;&amp;amp;2&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;new_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fun&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;fun&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;list_to_data&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;list_to_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="no"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_tuple&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_tuple&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;next_cell_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cell_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;alive_neighbours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;alive_neighbours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cell_x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cell_y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cell_x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cell_x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cell_y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cell_y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;
          &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;cell_x&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;cell_y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;
          &lt;span class="n"&gt;cell_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we need a function which returns an SVG string to visualise the&lt;br&gt;
grid.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Life&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Svg&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@cell_size&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Life&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Grid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;cells&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;into:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Life&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Grid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cell_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;fill&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"#EEE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"purple"&lt;/span&gt;

        &lt;span class="s2"&gt;"&amp;lt;rect x=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;@cell_size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; y=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;@cell_size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; width=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; height=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; fill=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;fill&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; /&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="sd"&gt;"""
    &amp;lt;svg viewBox="0 0 #{@cell_size * size} #{@cell_size * size}" xmlns="http://www.w3.org/2000/svg"&amp;gt;
      #{cells}
    &amp;lt;/svg&amp;gt;
    """&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we'll add a function to generate random starting configurations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;randomize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
  &lt;span class="n"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we'll add a button to generate a few configurations and preview them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Control&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"randomize"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Control&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:randomize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;button&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a button is pressed it sends events as messages. We handle them by rendering an SVG.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;widget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;receive&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:randomize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="c1"&gt;# Preview the configuration when the button is pressed&lt;/span&gt;
      &lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Life&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Svg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Life&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Grid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;randomize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
      &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Buttons are a new addition to Kino and Livebook released in version 0.4.0.&lt;br&gt;
You can find their docs &lt;a href="https://hexdocs.pm/kino/Kino.Control.html#content"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Having made sure that we can correctly render a grid, we can finally animate it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Life&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Grid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;randomize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:cont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Life&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Svg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;Life&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Grid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




  


&lt;p&gt;Thanks for reading this post, hope you'll find it useful and make&lt;br&gt;
your notebooks pop with captivating animations.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>livebook</category>
    </item>
    <item>
      <title>An Unusual Pomodoro Timer on Elixir and Nerves</title>
      <dc:creator>Dimitris Zorbas</dc:creator>
      <pubDate>Tue, 12 Oct 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/zorbash/an-unusual-pomodoro-timer-on-elixir-and-nerves-2hee</link>
      <guid>https://dev.to/zorbash/an-unusual-pomodoro-timer-on-elixir-and-nerves-2hee</guid>
      <description>&lt;p&gt;In my previous post about &lt;a href="https://zorbash.com/post/highlights-notes/"&gt;"Organising Book Highlights and Notes"&lt;/a&gt;,&lt;br&gt;
I wrote:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Some day I may build a gadget for my desk to display a daily quote".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A few days later, it's on my desk and it couldn't have been a better&lt;br&gt;
pretext to give &lt;a href="https://livebook.dev"&gt;Livebook&lt;/a&gt; on &lt;a href="https://www.nerves-project.org/"&gt;Nerves&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2&gt;
  
  
  Finished Product
&lt;/h2&gt;




  &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fkJ-hFB3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qe2pft9ih33s15kfzr0g.jpg" alt="actual image of Brain"&gt;
  It's alive! 🧟
  

  


&lt;p&gt;The case &lt;strong&gt;is not&lt;/strong&gt; made of flesh, but working with clay is not my forte. I should buy a 3D printer at some point..&lt;/p&gt;
&lt;h2&gt;
  
  
  Goal
&lt;/h2&gt;

&lt;p&gt;The scope of this weekend-project was to build some sort of smart desktop&lt;br&gt;
ornament. It should periodically display a random quote from my Kindle&lt;br&gt;
highlights and &lt;a href="https://github.com/zorbash/notes"&gt;notes&lt;/a&gt; on an E Ink screen.&lt;br&gt;
The quote is fetched via &lt;a href="https://github.com/zorbash/bookworm"&gt;bookworm 🪱📚&lt;/a&gt;.&lt;br&gt;
The display should refresh every 25 minutes and flash, marking the end of a time&lt;br&gt;
block.&lt;/p&gt;

&lt;p&gt;As the title hints, this is an &lt;em&gt;unusual&lt;/em&gt; pomodoro timer. It deviates&lt;br&gt;
from the standard &lt;a href="https://en.wikipedia.org/wiki/Pomodoro_Technique"&gt;pomodoro technique&lt;/a&gt;, but it suits me.&lt;/p&gt;
&lt;h3&gt;
  
  
  Name
&lt;/h3&gt;

&lt;p&gt;A project needs a name. The obvious choice for something built on &lt;a href="https://www.nerves-project.org/"&gt;Nerves&lt;/a&gt; is&lt;br&gt;
no other than &lt;strong&gt;Brain&lt;/strong&gt; 🧠!&lt;/p&gt;

&lt;p&gt;And here's a sample quote Brain would display:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The function of the &lt;strong&gt;brain&lt;/strong&gt; and nervous system is to protect us from&lt;br&gt;
being overwhelmed and confused by this mass of largely useless and irrelevant&lt;br&gt;
knowledge, by shutting out most of what we should otherwise perceive or&lt;br&gt;
remember at any moment, and leaving only that very small and special selection&lt;br&gt;
which is likely to be practically useful.&lt;/p&gt;

&lt;p&gt;Aldous Huxley, &lt;em&gt;The Doors of Perception&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The combination this name and Elixir's fault tolerance reminds me &lt;a href="https://en.wikipedia.org/wiki/The_Brain_That_Wouldn%27t_Die"&gt;this&lt;/a&gt; excellent cult horror film:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6YHqdUNN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/frue423k31pugqgr21on.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6YHqdUNN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/frue423k31pugqgr21on.jpg" alt="brain poster"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Optional Features
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Display the week number&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I tend to make daily plans and thinking in terms of numbered weeks, it&lt;br&gt;
drives me to make them memorable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Display the temperature outside&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I tend to hit the gym after work, so this is mostly to inform me whether to wear shorts.&lt;br&gt;
&lt;em&gt;(plot twist: I'll wear shorts anyway)&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why E Ink?
&lt;/h2&gt;

&lt;p&gt;The slow refresh rate of E Ink devices is ideal for something I'll keep&lt;br&gt;
right in from of me. It can be useful even when powered off.&lt;br&gt;
It's not even backlit and it doesn't distract me at all.&lt;br&gt;
I was sceptical of having yet another "screen" to look at. I've&lt;br&gt;
even ditched my multi-monitor setup for a single monitor with the&lt;br&gt;
laptop's monitor solely for messaging apps like Slack.&lt;br&gt;
Oh and the energy ⚡️ consumption is minimal, &lt;a href="https://www.amazon.co.uk/Pisugar2-Portable-Lithium-Raspberry-Accessories/dp/B08D678XPR"&gt;Pisugar&lt;/a&gt; could power it for a couple of days (&lt;em&gt;untested&lt;br&gt;
assumption&lt;/em&gt;).&lt;/p&gt;
&lt;h2&gt;
  
  
  What you'll need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Raspberry Pi 2 model B (that's what I had lying around)&lt;/li&gt;
&lt;li&gt;SD card&lt;/li&gt;
&lt;li&gt;WiFi USB dongle (Raspberry Pi 2 doesn't have on-board WiFi)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://shop.pimoroni.com/products/inky-what?variant=13590497624147"&gt;Pimoroni Inky wHat&lt;/a&gt; E Ink Display&lt;/li&gt;
&lt;li&gt;A JSON file with notes to display (see &lt;a href="https://github.com/zorbash/bookworm"&gt;bookwork&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You might notice there's a camera on my device, it's not required for&lt;br&gt;
the features discussed in this post. A future next one will be about it&lt;br&gt;
though, stay tuned!&lt;/p&gt;
&lt;h2&gt;
  
  
  Instructions
&lt;/h2&gt;

&lt;p&gt;Follow the instructions below to build one yourself.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Burn the Nerves Livebook Firmware
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Install prerequisites for packaging firmware images
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;MacOS&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew update
brew &lt;span class="nb"&gt;install &lt;/span&gt;fwup squashfs coreutils xz pkg-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Linux (Debian)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;build-essential automake autoconf git squashfs-tools ssh-askpass pkg-config curl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then install &lt;a href="https://github.com/nerves-project/nerves_bootstrap"&gt;nerves_bootstrap&lt;/a&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix archive.install hex nerves_bootstrap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;You can find more detailed information about getting started with Nerves&lt;br&gt;
&lt;a href="https://hexdocs.pm/nerves/installation.html"&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Burn the Image
&lt;/h4&gt;

&lt;p&gt;Follow the instructions on the &lt;a href="https://github.com/livebook-dev/nerves_livebook"&gt;nerves_livebook&lt;/a&gt; repo&lt;br&gt;
to burn the firmware using &lt;code&gt;fwup&lt;/code&gt; or run the code below in your shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://github.com/livebook-dev/nerves_livebook/releases/download/v0.2.26/nerves_livebook_rpi2.fw

&lt;span class="c"&gt;# Mind to replace with your SSID and passphrase&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;&lt;span class="nv"&gt;NERVES_WIFI_SSID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'access_point'&lt;/span&gt; &lt;span class="nv"&gt;NERVES_WIFI_PASSPHRASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'passphrase'&lt;/span&gt; fwup nerves_livebook_rpi2.fw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Mount the E Ink Screen Hat on the Pi
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UvcfgO6P--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ohxg0x7p7ndj2tlw0ah7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UvcfgO6P--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ohxg0x7p7ndj2tlw0ah7.jpg" alt="mounted screen"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point you can already power on your Pi.&lt;/p&gt;

&lt;p&gt;Point your browser to &lt;a href="http://nerves.local"&gt;http://nerves.local&lt;/a&gt; ..and &lt;em&gt;voila&lt;/em&gt;!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--y7lDZy6z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rsxxgj13d3ysyqx88q44.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--y7lDZy6z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rsxxgj13d3ysyqx88q44.jpg" alt="livebook auth"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
  The password is "nerves".&lt;br&gt;
  
  &lt;/p&gt;

&lt;p&gt;🙌 You have an easily accessible instance of Livebook running on a tiny computer.&lt;br&gt;
It runs in embedded mode by default which means that code evaluated in&lt;br&gt;
your notebooks runs in the context of the Livebook node.&lt;/p&gt;

&lt;p&gt;This is ideal, as it allows us to redefine &lt;a href="https://hexdocs.pm/scenic/scene_lifecycle.html#the-root-scene"&gt;Scene&lt;/a&gt; modules from within a&lt;br&gt;
notebook and trigger a screen refresh without uploading firmware changes and rebooting the Pi.&lt;br&gt;
More about that further below.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Clone the Brain Repo
&lt;/h3&gt;

&lt;p&gt;Hang tight, &lt;em&gt;we're halfway there&lt;/em&gt;, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone git@github.com:zorbash/brain.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Add Some Notes
&lt;/h3&gt;

&lt;p&gt;You can use &lt;a href="https://github.com/zorbash/bookworm"&gt;bookwork&lt;/a&gt; to download your Kindle highlights in a&lt;br&gt;
format Brain understands or simply copy mine from &lt;a href="https://github.com/zorbash/notes/blob/main/books.json"&gt;https://github.com/zorbash/notes/blob/main/books.json&lt;/a&gt;&lt;br&gt;
and place them in &lt;code&gt;priv/notes/&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Deploy! 🚀
&lt;/h3&gt;

&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MIX_TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rpi2

mix deps.get
mix firmware
mix firmare.gen.script
./upload.sh livebook@nerves.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When it asks for a password, type in &lt;code&gt;nerves&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's it, a few moments later the screen should flash and your Brain&lt;br&gt;
will have come to life.&lt;/p&gt;
&lt;h2&gt;
  
  
  Making Changes
&lt;/h2&gt;

&lt;p&gt;Brain is open-source and it's easy to tweak it according to your needs.&lt;br&gt;
The UI is built using &lt;a href="https://github.com/boydm/scenic"&gt;Scenic&lt;/a&gt; which makes it trivial to&lt;br&gt;
preview changes locally without re-uploading firmware.&lt;/p&gt;

&lt;p&gt;Start Scenic to preview your changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MIX_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev &lt;span class="nv"&gt;MIX_TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host iex &lt;span class="nt"&gt;-S&lt;/span&gt; mix scenic.run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a window like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tk2s9lxw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2j8ygfrjt361itxvbv10.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tk2s9lxw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2j8ygfrjt361itxvbv10.jpg" alt="scenic preview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Scene to modify is &lt;code&gt;NervesLivebook.Scenes.Main&lt;/code&gt; and you can trigger&lt;br&gt;
an update with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;send&lt;/span&gt; &lt;span class="ss"&gt;:main_scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:update&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To trigger a screen refresh remotely run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;ssh&lt;/span&gt; &lt;span class="n"&gt;livebook&lt;/span&gt;&lt;span class="nv"&gt;@nerves&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="s1"&gt;'send :main_scene, :update'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💡 Since Brain runs Livebook (&lt;a href="http://nerves.local"&gt;http://nerves.local&lt;/a&gt;), you can trigger an update by evaluating an&lt;br&gt;
Elixir cell with &lt;code&gt;send :main_scene, :update&lt;/code&gt; in a notebook.&lt;/p&gt;
&lt;h2&gt;
  
  
  Debugging
&lt;/h2&gt;

&lt;p&gt;You can connect to your Brain with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh livebook@nerves.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you can inspect the logs with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="no"&gt;RingLogger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For further debugging, check out &lt;a href="https://github.com/elixir-toolshed/toolshed"&gt;toolshed&lt;/a&gt; which bundles a&lt;br&gt;
variety of helpful utility helpers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Toolshed&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="no"&gt;IEx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Helpers&lt;/span&gt;

&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Toolshed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;The Pimoroni E Ink screen afaik does not support partial refresh, it'd&lt;br&gt;
be cool to partially refresh the header (week, temperature, time) every&lt;br&gt;
minute.&lt;/p&gt;

&lt;p&gt;Scenic's text wrapping could support &lt;code&gt;break-word&lt;/code&gt;, see &lt;a href="https://github.com/boydm/scenic/issues/118"&gt;scenic#118&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Future Enhancements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Get it to show notes per bookshelf, for example it could be configured
to show only notes from the &lt;code&gt;computer science bookshelf&lt;/code&gt; between 10
and 11 AM and from &lt;code&gt;philosophy&lt;/code&gt; between 6 and 7 PM.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Livebook
&lt;/h2&gt;

&lt;p&gt;Livebook is an incredible piece of technology, improving at a mind-blowing&lt;br&gt;
rate.&lt;/p&gt;

&lt;p&gt;A couple of months ago I started work on an enhancement to be&lt;br&gt;
able to show the documentation for any module / function in a notebook.&lt;br&gt;
My intention was to expose a &lt;code&gt;Livedoc.render(:plug)&lt;/code&gt; to fetch and&lt;br&gt;
render the documentation of the given hex package.&lt;/p&gt;

&lt;p&gt;Thankfully, I was surprised to find out that this has already been&lt;br&gt;
implemented in &lt;a href="https://github.com/livebook-dev/livebook/pull/453"&gt;livebook#453&lt;/a&gt; 🙌.&lt;/p&gt;

&lt;p&gt;If you haven't tried Livebook yet, please check out &lt;a href="https://livebook.dev/"&gt;livebook.dev&lt;/a&gt;.&lt;br&gt;
You can even &lt;a href="https://livebook.dev/settings"&gt;configure&lt;/a&gt; the default Livebook location where notebooks&lt;br&gt;
will open. I've set mine to &lt;code&gt;http://nerves.local&lt;/code&gt; where Brain lives.&lt;/p&gt;

&lt;p&gt;With your notes loaded in Brain, you can quickly browse and search them with the following notebook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Notes&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;!&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="ss"&gt;livebook:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"livebook_object"&lt;/span&gt;&lt;span class="ss"&gt;:"cell_input"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="ss"&gt;:"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"reactive"&lt;/span&gt;&lt;span class="ss"&gt;:true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="ss"&gt;:"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="ss"&gt;:""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"search: "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;NervesLivebook&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Notes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contains?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;&amp;amp;1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="no"&gt;Kino&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;DataTable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/livebook-dev/livebook/pull/389"&gt;Reactive inputs&lt;/a&gt; make this possible.&lt;/p&gt;

&lt;p&gt;Watch a video if it in action &lt;a href="https://zorbash.com//images/posts/elixir_nerves_pomodoro/brain.webm"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgements
&lt;/h2&gt;

&lt;p&gt;This project wouldn't be as enjoyable and painless without &lt;a href="https://www.nerves-project.org/"&gt;nerves&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://github.com/livebook-dev/nerves_livebook"&gt;nerves_livebook&lt;/a&gt;, &lt;a href="https://github.com/boydm/scenic"&gt;scenic&lt;/a&gt;, &lt;a href="https://github.com/pappersverk/inky"&gt;inky&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thank you &lt;a href="https://github.com/mobileoverlord"&gt;@mobileoverlord&lt;/a&gt;, &lt;a href="https://github.com/fhunleth"&gt;@fhunleth&lt;/a&gt;, &lt;a href="https://github.com/boydm"&gt;@boydm&lt;/a&gt;,&lt;br&gt;
and &lt;a href="https://github.com/lawik"&gt;@lawik&lt;/a&gt; and all of the contributors to this amazing part of the Elixir ecosystem.&lt;/p&gt;




</description>
      <category>elixir</category>
      <category>nerves</category>
      <category>raspberrypi</category>
    </item>
    <item>
      <title>Organising Book Highlights and Notes</title>
      <dc:creator>Dimitris Zorbas</dc:creator>
      <pubDate>Sat, 02 Oct 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/zorbash/organising-book-highlights-and-notes-1pjl</link>
      <guid>https://dev.to/zorbash/organising-book-highlights-and-notes-1pjl</guid>
      <description>&lt;p&gt;I've used a variety of tools to organise my reading and notes. Given I spend a significant amount of my time studying,&lt;br&gt;
depending on 3rd parties gives me anxiety. Any of the tools I use, even the open-source offline-first ones,&lt;br&gt;
can become unmaintained, ridden with security vulnerabilities, slow or they may change in way which makes&lt;br&gt;
me reluctant to use them.&lt;/p&gt;

&lt;p&gt;To some extent, this post is a sequel to &lt;a href="https://zorbash.com/post/knowledge-mapping/"&gt;"knowledge mapping"&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why care about my notes and Highlights?
&lt;/h2&gt;

&lt;p&gt;A friend asked this question. She said, are you going to use this&lt;br&gt;
mid-conversation to correct someone? Do you want to be the "Well, Ackchyually.." guy?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Kd94sfDm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/Bzw1FFO.png%3F1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Kd94sfDm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/Bzw1FFO.png%3F1" alt="actually"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most definitely &lt;strong&gt;not&lt;/strong&gt;. I've noticed the rate of information&lt;br&gt;
I consume keeps increasing disproportionately to the rate I digest it&lt;br&gt;
into knowledge. I repeat it's about building knowledge not&lt;br&gt;
memorising.&lt;/p&gt;

&lt;p&gt;This project in particular, is about using tech for&lt;br&gt;
"good", &lt;a href="https://www.youtube.com/watch?v=p7Bq_MvkUtU"&gt;doing your thing&lt;/a&gt;, or &lt;em&gt;escaping from servitude to&lt;br&gt;
the capitalistic delusion of perpetual exponential growth (too far eh?  😅)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Initially I became reasonably frustrated of not being able to revisit my Kindle highlights.&lt;br&gt;
Half of the books I read, I do so on my Kindle device, I highlight sentences, then said&lt;br&gt;
highlights appear on &lt;a href="https://goodreads.com"&gt;goodreads.com&lt;/a&gt; and &lt;a href="https://read.amazon.co.uk/"&gt;read.amazon.co.uk&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I felt locked-in using Goodreads (owned by Amazon) and &lt;a href="https://read.amazon.co.uk/"&gt;read.amazon&lt;/a&gt;.&lt;br&gt;
What if my highlights become unavailable due to some licensing issue?&lt;/p&gt;

&lt;p&gt;Having all my highlights in one place, open up some interesting possibilities. Similar&lt;br&gt;
to how I fancy creating &lt;a href="https://open.spotify.com/playlist/3S11TYg16tPejXRn8bxGwg"&gt;playlists&lt;/a&gt;, I might for example, some day curate my favourite&lt;br&gt;
quotes by an author. Some day I may build a gadget for my desk to display a daily quote &lt;br&gt;
(raspberrypi + e-ink).&lt;/p&gt;
&lt;h2&gt;
  
  
  So.. I built a thing
&lt;/h2&gt;

&lt;p&gt;I started off with a simple script which downloads all my Kindle highlights.&lt;br&gt;
It formats and builds a readable, pretty-printed JSON file which I sync&lt;br&gt;
in Git.&lt;/p&gt;

&lt;p&gt;Then I enhanced it so that I can write my own notes and highlights from&lt;br&gt;
non-Kindle physical books.&lt;/p&gt;

&lt;p&gt;Then I made it create and incrementally import highlights&lt;br&gt;
into a Notion database. Why? I was too lazy to write a frontend and I'm&lt;br&gt;
starting to like Notion.&lt;br&gt;
What's great about this database is that it can be embedded as a view in&lt;br&gt;
any page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ExecTvAS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/ZUp3rEK.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ExecTvAS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/ZUp3rEK.png" alt="notion database"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Notion database can then be shared. He's &lt;a href="https://zorbash.notion.site/6bfc231028bd4b71b3a8f4854e31c083?v=5a57cd6e61fd4e7fb2dcaebf335e1da6"&gt;mine&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo Time
&lt;/h2&gt;

&lt;p&gt;The script can be found &lt;a href="https://github.com/zorbash/bookworm"&gt;here&lt;/a&gt; and accepts the following commands:&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;sync_local&lt;/code&gt;
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./notes sync_local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It downloads Kindle highlights and imports local notes into a &lt;code&gt;books.json&lt;/code&gt; file.&lt;/p&gt;


  


&lt;p&gt;The location of the flat-file "database" can be configured through a&lt;br&gt;
&lt;code&gt;DB_FILEPATH&lt;/code&gt; env var, which can also be set in a &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;The "database" file is intended to be version-controlled with Git.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local Notes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Highlights and notes can also be imported into the "database" &lt;code&gt;books.json&lt;/code&gt; file.&lt;br&gt;
Such notes are written as YAML, yes YAML. I contemplated&lt;br&gt;
creating my own tiny markup language for this (ideally using&lt;br&gt;
&lt;a href="https://github.com/dashbitco/nimble_parsec"&gt;nimble_parsec&lt;/a&gt;), this non-feature ended up in the "backlog".&lt;/p&gt;

&lt;p&gt;The location of the notes file can be configure through the &lt;code&gt;NOTES_FILEPATH&lt;/code&gt; env var, which can also be set in a &lt;code&gt;.env&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt;
  &lt;span class="c1"&gt;# The asin key may also hold an ISBN&lt;/span&gt;
  &lt;span class="na"&gt;asin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0-679-76288-4&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;High Output Management&lt;/span&gt;
  &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Andy Grove&lt;/span&gt;
  &lt;span class="na"&gt;highlights&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt;
      &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;17&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="s"&gt;A genuinely effective indicator will cover the output of the work unit and not simply&lt;/span&gt;
        &lt;span class="s"&gt;the activity involved.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  search
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./notes search &amp;lt;keyword&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prints any highlights which match the given keyword.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./notes search work

Found 179 results &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s2"&gt;"work"&lt;/span&gt;

╔════════════════════════════════════════════════════════════════════╗
║  Book:   High Output Management                                    ║
║  Author: Andy Grove                                                ║
╚════════════════════════════════════════════════════════════════════╝

 A genuinely effective indicator will cover the output of the work unit
and not simply the activity involved.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  random
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./notes random
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns a random highlight.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;╔════════════════════════════════════════════════════════════════════╗
║  Book:   The Genealogy of Morals                                   ║
║  Author: Friedrich Nietzsche                                       ║
╚════════════════════════════════════════════════════════════════════╝

 All sick and diseased people strive instinctively after a herd-organisation,
out of a desire to shake off their sense of oppressive discomfort and weakness&lt;span class="p"&gt;;&lt;/span&gt;
the ascetic priest divines this instinct and promotes it&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  update_notion
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./notes update_notion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Syncs the local "database" file into a Notion database.&lt;/p&gt;


  


&lt;p&gt;It supports the &lt;code&gt;--since &amp;lt;date&amp;gt;&lt;/code&gt; flag to only sync the database entries&lt;br&gt;
which have been updated since the given date (ISO-8601 formatted). This&lt;br&gt;
option is particularly useful since the Notion API is rate-limited and&lt;br&gt;
for more than 1000 highlights syncing can take significant time (more&lt;br&gt;
than 10 minutes).&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Offline-first - The "database" is a single JSON file&lt;/li&gt;
&lt;li&gt;Version control - The JSON database file is prettified making it easy to review and commit changes to git&lt;/li&gt;
&lt;li&gt;Both notes and the JSON database are readable and searchable using a text editor
or tools like &lt;code&gt;jq&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fast search - Both the CLI search and Notion's search are fast&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Non-Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Removing highlights from the local database - I never delete highlights&lt;/li&gt;
&lt;li&gt;Full-text search - Simple regex case-insensitive does the trick for now&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Contributing
&lt;/h2&gt;

&lt;p&gt;Feel free to fork either the &lt;a href="https://github.com/zorbash/bookworm"&gt;bookworm script&lt;/a&gt; or my &lt;a href="https://github.com/zorbash/notes"&gt;notes&lt;/a&gt;.&lt;br&gt;
As stated at the top of this post, this is a fun project for me, so&lt;br&gt;
expect no support. However, I'd be glad to discuss ideas on this domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;p&gt;Nah, stop reading. Start organising!&lt;/p&gt;




&lt;p&gt;Cover image credits: &lt;a href="https://unsplash.com/@giamboscaro"&gt;@giamboscaro&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>reading</category>
      <category>learning</category>
    </item>
    <item>
      <title>Phoenix Telemetry</title>
      <dc:creator>Dimitris Zorbas</dc:creator>
      <pubDate>Wed, 07 Apr 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/zorbash/phoenix-telemetry-1pok</link>
      <guid>https://dev.to/zorbash/phoenix-telemetry-1pok</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/beam-telemetry/telemetry"&gt;Telemetry&lt;/a&gt; is becoming the defacto library to instrument and publish&lt;br&gt;
metrics in Elixir apps. This post is a step-by-step guide to integrate&lt;br&gt;
Telemetry in a Phoenix app which leverages &lt;a href="https://github.com/zorbash/opus"&gt;Opus&lt;/a&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  What is Telemetry
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Telemetry is a dynamic dispatching library for metrics and instrumentations.&lt;br&gt;
It is lightweight, small and can be used in any Erlang or Elixir project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Project page: &lt;a href="https://github.com/beam-telemetry/telemetry"&gt;https://github.com/beam-telemetry/telemetry&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead of every library / framework having its own way of publishing&lt;br&gt;
metrics, the ecosystem is gradually converging on using Telemetry.&lt;/p&gt;
&lt;h2&gt;
  
  
  How it Works
&lt;/h2&gt;

&lt;p&gt;You may attach event handlers which are stored in an ETS table. When an&lt;br&gt;
event is emitted, all matching event handlers are called. Due to the&lt;br&gt;
fact that handlers are called synchronously, you should make sure they&lt;br&gt;
are fast, so not to cause bottlenecks.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Set up a handler which logs to the console&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Start an IEx session with &lt;code&gt;iex&lt;/code&gt;, then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Install telemetry (Mix.install requires Elixir &amp;gt; v1.12)&lt;/span&gt;
&lt;span class="no"&gt;Mix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="ss"&gt;:telemetry_metrics&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Attach a uniquely named handler which prints measurements&lt;/span&gt;
&lt;span class="ss"&gt;:telemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"example-handler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:example&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:metric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:done&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;_metric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_config&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inspect&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
                  &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                  &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Emit an event
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="ss"&gt;:telemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:example&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:metric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:done&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;latency:&lt;/span&gt; &lt;span class="mi"&gt;1337&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;%{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;See the output&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You should see &lt;code&gt;%{latency: 1337}&lt;/code&gt; in the output.&lt;/p&gt;

&lt;h4&gt;
  
  
  How it Works
&lt;/h4&gt;

&lt;p&gt;:telemetry.attach/4 inserts an object in an ETS table named &lt;code&gt;telemetry_handler_table&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can inspect the table to verify this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="ss"&gt;:ets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tab2list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:telemetry_handler_table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;#=&amp;gt; [&lt;/span&gt;
&lt;span class="c1"&gt;#=&amp;gt;   {:handler, "example-handler", [:example, :metric, :done],&lt;/span&gt;
&lt;span class="c1"&gt;#=&amp;gt;   #Function&amp;lt;5.126501267/4 in :erl_eval.expr/5&amp;gt;, nil}&lt;/span&gt;
&lt;span class="c1"&gt;#=&amp;gt; ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an event is emitted, the process emitting the event will call all&lt;br&gt;
the event handlers matching the given event name. To list the matching&lt;br&gt;
handlers, you can call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="ss"&gt;:ets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lookup&lt;/span&gt; &lt;span class="ss"&gt;:telemetry_handler_table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:example&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:metric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:done&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;#=&amp;gt; [&lt;/span&gt;
&lt;span class="c1"&gt;#=&amp;gt;   {:handler, "example-handler", [:example, :metric, :done],&lt;/span&gt;
&lt;span class="c1"&gt;#=&amp;gt;   #Function&amp;lt;5.126501267/4 in :erl_eval.expr/5&amp;gt;, nil}&lt;/span&gt;
&lt;span class="c1"&gt;#=&amp;gt; ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Phoenix Telemetry
&lt;/h2&gt;

&lt;p&gt;If you're building a Phoenix application, you're in luck. Since &lt;code&gt;v1.5&lt;/code&gt;, new Phoenix applications are generated&lt;br&gt;
with a Telemetry supervisor. Many popular &lt;a href="https://hexdocs.pm/phoenix/telemetry.html#libraries-using-telemetry"&gt;libraries&lt;/a&gt; emit telemetry&lt;br&gt;
events and you can easily visualize events in LiveDashboard.&lt;/p&gt;
&lt;h2&gt;
  
  
  Opus Telemetry
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/zorbash/opus"&gt;Opus&lt;/a&gt; is a library to build "service modules".&lt;/p&gt;

&lt;p&gt;Consider the following pipeline module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;GenerateSession&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Opus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Pipeline&lt;/span&gt;

  &lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="ss"&gt;:valid?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;match?&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;user_id:&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;when&lt;/span&gt; &lt;span class="n"&gt;is_integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;&amp;amp;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ss"&gt;:generate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="s2"&gt;"session-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nv"&gt;&amp;amp;1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can call this module with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="no"&gt;Example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;GenerateSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;user_id:&lt;/span&gt; &lt;span class="mi"&gt;1337&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# =&amp;gt; {:ok, "session-123"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wouldn't it be ideal to publish metrics for the duration of each step as&lt;br&gt;
well as the total duration?&lt;/p&gt;

&lt;p&gt;Thankfully, Opus supports defining instrumentation modules, but you can&lt;br&gt;
also define instrumentation functions inline.&lt;/p&gt;

&lt;p&gt;To emit telemetry events, add the following module in your codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;OpusTelemetry&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;require&lt;/span&gt; &lt;span class="no"&gt;Logger&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;instrument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:pipeline_started&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;pipeline:&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;input:&lt;/span&gt; &lt;span class="n"&gt;_input&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="ss"&gt;:telemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:opus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:start&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;time:&lt;/span&gt; &lt;span class="no"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_time&lt;/span&gt;&lt;span class="p"&gt;()},&lt;/span&gt;
      &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;pipeline:&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;instrument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:stage_completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;stage:&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;pipeline:&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;time:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="ss"&gt;:telemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:opus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:stage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:stop&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;duration:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;pipeline:&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;stage:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;instrument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:pipeline_completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;pipeline:&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;result:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;time:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;emit_stop&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;pipeline:&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;success?:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;duration:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;instrument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:pipeline_completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;pipeline:&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;result:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;time:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;emit_stop&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;pipeline:&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;success?:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;duration:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;instrument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="ss"&gt;:ok&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;emit_stop&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;pipeline:&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;success?:&lt;/span&gt; &lt;span class="n"&gt;success?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;duration:&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="ss"&gt;:telemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:opus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:stop&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;duration:&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;success:&lt;/span&gt; &lt;span class="n"&gt;success?&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;pipeline:&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📣 The next version of Opus, will provide an &lt;code&gt;Opus.Telemetry&lt;/code&gt; module, and&lt;br&gt;
you won't need to copy-paste code into your app.&lt;/p&gt;

&lt;p&gt;Next, update your &lt;code&gt;config/config.exs&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="ss"&gt;:opus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:instrumentation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;OpusTelemetry&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can first set up a very simple handler to verify that instrumentation&lt;br&gt;
is configured correctly. In an IEx session run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="kn"&gt;require&lt;/span&gt; &lt;span class="no"&gt;Logger&lt;/span&gt;

&lt;span class="ss"&gt;:telemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attach&lt;/span&gt; &lt;span class="s2"&gt;"opus-test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:opus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:stop&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                  &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then call the sample pipeline a few times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;GenerateSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;user_id:&lt;/span&gt; &lt;span class="mi"&gt;1337&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# 20:40:43.164 [info] %{duration: 15000, success: true} 👈 Printed by the handler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"session-1337"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;GenerateSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;user_id:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# 20:40:43.164 [info] %{duration: 17000, success: true}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"session-42"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;GenerateSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;user_id:&lt;/span&gt; &lt;span class="s2"&gt;"invalid"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# 20:40:43.164 [info] %{duration: 19000, success: false}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Opus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PipelineError&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="ss"&gt;error:&lt;/span&gt; &lt;span class="ss"&gt;:failed_check_valid?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="ss"&gt;input:&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;user_id:&lt;/span&gt; &lt;span class="s2"&gt;"invalid"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="ss"&gt;pipeline:&lt;/span&gt; &lt;span class="no"&gt;Example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;GenerateSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="ss"&gt;stacktrace:&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="ss"&gt;stage:&lt;/span&gt; &lt;span class="ss"&gt;:valid?&lt;/span&gt;
 &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works! 🙌&lt;/p&gt;

&lt;p&gt;Moving on to configure our metrics in our Phoenix app so that we can graph then in LiveDashboard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# TODO: Add the code in ExampleWeb.Telemetry module&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any recently generated Phoenix application (after v1.5) will already&lt;br&gt;
have a Telemetry supervisor, which looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;ExammpleWeb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Telemetry&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Supervisor&lt;/span&gt;
  &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="no"&gt;Telemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Metrics&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Supervisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="no"&gt;Telemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Metrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;ConsoleReporter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;metrics:&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;()}]&lt;/span&gt;

    &lt;span class="no"&gt;Supervisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;strategy:&lt;/span&gt; &lt;span class="ss"&gt;:one_for_one&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="c1"&gt;# Phoenix Metrics&lt;/span&gt;
      &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"phoenix.endpoint.stop.duration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;unit:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:native&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:millisecond&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"phoenix.router_dispatch.stop.duration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;tags:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:route&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;unit:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:native&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:millisecond&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;periodic_measurements&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you need to do is to change the &lt;code&gt;metrics&lt;/code&gt; method above to include&lt;br&gt;
the Opus metrics you wish reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# Phoenix Metrics&lt;/span&gt;
    &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"phoenix.endpoint.stop.duration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;unit:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:native&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:millisecond&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"phoenix.router_dispatch.stop.duration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;tags:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:route&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;unit:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:native&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:millisecond&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="c1"&gt;# 👇 Add these two&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"opus.pipeline.start.time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;description:&lt;/span&gt; &lt;span class="s2"&gt;"Pipeline Started"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;tags:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:pipeline&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"opus.pipeline.stop.duration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;description:&lt;/span&gt; &lt;span class="s2"&gt;"Pipeline Duration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;tags:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:pipeline&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;unit:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:native&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:millisecond&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing to notice is how the first argument to metric functions is constructed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"opus.pipeline.stop.duration"

[:opus, :pipeline, :stop]      :duration
&amp;lt;----- event name ------&amp;gt;  &amp;lt;-- measurement --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Live Dashboard
&lt;/h2&gt;

&lt;p&gt;Time to fire up the Phoenix server and point the browser to the dashboard:&lt;/p&gt;

&lt;p&gt;&lt;a href="http://localhost:4000/dashboard/metrics?nav=opus"&gt;http://localhost:4000/dashboard/metrics?nav=opus&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---lOUPz2a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bw69qphx78rhd979u0c5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---lOUPz2a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bw69qphx78rhd979u0c5.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Upcoming Blog post
&lt;/h2&gt;

&lt;p&gt;There's so much to cover in a single post.&lt;br&gt;
In the next post, I'll to write about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating a custom LiveDashboard page to visualize pipelines (think &lt;a href="https://github.com/zorbash/opus_graph"&gt;opus_graph&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Reporting Telemetry events to DataDog&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/beam-telemetry/telemetry"&gt;Telemetry Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hexdocs.pm/telemetry_metrics/Telemetry.Metrics.html"&gt;Telemetry.Metrics Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/zorbash/opus"&gt;Opus Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://claudio-ortolina.org/posts/tips-for-finch-and-telemetry/"&gt;Tips for Finch and Telemetry - Claudio Ortolina&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;banner image by Johannes Groll - &lt;a href="https://unsplash.com/photos/mrIaqKh9050"&gt;unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>phoenix</category>
    </item>
    <item>
      <title>sidekiq-dry</title>
      <dc:creator>Dimitris Zorbas</dc:creator>
      <pubDate>Sun, 17 Jan 2021 20:15:53 +0000</pubDate>
      <link>https://dev.to/zorbash/sidekiq-dry-17ol</link>
      <guid>https://dev.to/zorbash/sidekiq-dry-17ol</guid>
      <description>&lt;p&gt;I published a new gem, &lt;code&gt;sidekiq-dry&lt;/code&gt; aiming to tackle a variety of&lt;br&gt;
common frustrations when it comes to &lt;a href="https://hexdocs.pm/ecto/1.1.0/Ecto.Model.Callbacks.html"&gt;Sidekiq&lt;/a&gt; jobs and their arguments.&lt;/p&gt;


&lt;h2&gt;
  
  
  Rationale
&lt;/h2&gt;

&lt;p&gt;Sidekiq is among the most popular background job solutions. It's my&lt;br&gt;
first choice for Ruby apps. The &lt;a href="https://dry-rb.org/"&gt;dry-rb&lt;/a&gt; family of gems is also&lt;br&gt;
indispensable in non-trivial applications. What if we combined the two..&lt;/p&gt;

&lt;p&gt;&lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;sidekiq-dry&lt;/code&gt; you may pass instances of &lt;code&gt;Dry::Struct&lt;/code&gt; as arguments&lt;br&gt;
to your Sidekiq jobs. But why?&lt;/p&gt;
&lt;h3&gt;
  
  
  Prevent Type Ambiguity
&lt;/h3&gt;

&lt;p&gt;Numerous times I've had to debug jobs which where failing due to being&lt;br&gt;
enqueued with invalid arguments.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendInvitationEmailJob&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Worker&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;invitee_email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# code&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;SendInvitationEmailJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:invitee_email&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem with the above code is that if the &lt;code&gt;user_id&lt;/code&gt; is not an&lt;br&gt;
Integer id or the &lt;code&gt;invitee_email&lt;/code&gt; is not a valid email String then&lt;br&gt;
there's absolutely no chance that the enqueued job will complete&lt;br&gt;
successfully. Of course &lt;code&gt;Dry::Struct&lt;/code&gt; is not to be used for validations,&lt;br&gt;
there's &lt;code&gt;dry-validate&lt;/code&gt; for that, or &lt;code&gt;ActiveModel&lt;/code&gt; / &lt;code&gt;ActiveRecord&lt;/code&gt;&lt;br&gt;
validations if you prefer. Giving more structure to your&lt;br&gt;
background job arguments improves the system's robustness. Your objects&lt;br&gt;
in transport through &lt;code&gt;Redis&lt;/code&gt;, as long as the job is enqueued, they are&lt;br&gt;
guaranteed to have the expected structure when the job is performed.&lt;/p&gt;

&lt;p&gt;The above example would be refactored to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendInvitationEmailJob&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Worker&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# code&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendInvitationEmailJob::Params&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Dry&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Struct&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Strict&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Integer&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:invitee_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Strict&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;format: &lt;/span&gt;&lt;span class="sr"&gt;/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;job_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SendInvitationEmailJob&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user_id: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;invitee_email: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:invitee_email&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="no"&gt;SendInvitationEmailJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point you might ask, &lt;em&gt;what if we passed in a Hash, instead of a &lt;code&gt;Dry::Struct&lt;/code&gt;?&lt;/em&gt;&lt;br&gt;
Well, Hash arguments are deserialised with String keys which can lead to surprises.&lt;/p&gt;
&lt;h3&gt;
  
  
  Eliminate Positional Arguments
&lt;/h3&gt;

&lt;p&gt;When your background job takes two or more positional arguments, it's&lt;br&gt;
better to refactor it to take a single struct object with a&lt;br&gt;
comprehensible name.&lt;/p&gt;

&lt;p&gt;In the Rails world it's common to enqueue jobs with a record's &lt;code&gt;id&lt;/code&gt;.&lt;br&gt;
There's nothing wrong with this pattern. However, in some cases, developers may define a&lt;br&gt;
model blindly following the convention.&lt;/p&gt;
&lt;h3&gt;
  
  
  Documentation
&lt;/h3&gt;

&lt;p&gt;By using &lt;code&gt;Dry::Struct&lt;/code&gt; arguments you'll be able to express constraints&lt;br&gt;
straight in your code. Instead of documenting the types of each job argument,&lt;br&gt;
which can easily become outdated, you can refer to the types of the attributes of the struct.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Dry&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Struct&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Strict&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Coercible&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'published'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'archived'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;min_size: &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;max_size: &lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arguably, in the example above, both types and constraints improve readability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Versioning
&lt;/h3&gt;

&lt;p&gt;Adding this gem does not break any existing jobs in your app.&lt;br&gt;
It only works on jobs enqueued with &lt;code&gt;Dry::Struct&lt;/code&gt; objects.&lt;/p&gt;

&lt;p&gt;Adding a new attribute to a parameter struct won't break already enqueued jobs.&lt;/p&gt;

&lt;p&gt;It's trivial to version your structs using either a &lt;code&gt;version&lt;/code&gt; attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Coupons::ApplyCouponJob::Params&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Dry&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Struct&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Strict&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Integer&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:coupon_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Strict&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Strict&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or versioned classes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Coupons::ApplyCouponJob::Params::V1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Dry&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Struct&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Strict&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Integer&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:coupon_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Strict&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;Job processing libraries compatible with Sidekiq, for example&lt;br&gt;
&lt;a href="https://github.com/akira/exq"&gt;exq&lt;/a&gt;, won't deserialise your Dry::Struct arguments. This is most likely an acceptable tradeoff.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Gem
&lt;/h2&gt;

&lt;p&gt;The gem is hosted on rubygems (&lt;a href="https://rubygems.org/gems/sidekiq-dry"&gt;link&lt;/a&gt;). It provides two Sidekiq&lt;br&gt;
middlewares which serialise and deserialise instances of &lt;code&gt;Dry::Struct&lt;/code&gt;&lt;br&gt;
arguments in your jobs.&lt;/p&gt;
&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;p&gt;Add the gem in your Gemfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'sidekiq-dry'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure &lt;code&gt;Sidekiq&lt;/code&gt; to use the middlewares of the gem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# File: config/initializers/sidekiq.rb&lt;/span&gt;

&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure_client&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client_middleware&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Dry&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SerializationMiddleware&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure_server&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;server_middleware&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Dry&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Server&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DeserializationMiddleware&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dry-rb.org/"&gt;dry-rb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dry-rb.org/gems/dry-types/1.2/"&gt;dry-types&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For other handy libraries and posts, subscribe to my Tefter &lt;a href="https://www.tefter.io/zorbash/lists/ruby-rails"&gt;Ruby &amp;amp; Rails&lt;/a&gt; list.&lt;/p&gt;

</description>
      <category>dryrb</category>
      <category>rails</category>
      <category>ruby</category>
      <category>sidekiq</category>
    </item>
    <item>
      <title>Knowledge Mapping</title>
      <dc:creator>Dimitris Zorbas</dc:creator>
      <pubDate>Sun, 23 Aug 2020 00:00:00 +0000</pubDate>
      <link>https://dev.to/zorbash/knowledge-mapping-1gcb</link>
      <guid>https://dev.to/zorbash/knowledge-mapping-1gcb</guid>
      <description>&lt;p&gt;&lt;em&gt;What do &lt;strong&gt;I&lt;/strong&gt; know? What do &lt;strong&gt;we&lt;/strong&gt; know?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How do we know what we know and what is there that we should know?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Video games like Age of Empires, Civilisation, both favourites of mine and others have the concept of technology trees.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eP96H0__--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/fxwb1lfx8h4jhisbtrup.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eP96H0__--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/fxwb1lfx8h4jhisbtrup.jpg" alt="Alt Text" width="880" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I study a lot, where should I spend my time? What should I read next? Is my knowledge broad enough?&lt;br&gt;
I’ve used goodreads quite extensively, it’s great to be able to discover new books to read, mark your progress, make friends and such.&lt;/p&gt;

&lt;p&gt;Nowadays there’s so much information readily available but not that many tools to organise and allocate your “research points”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learning Together
&lt;/h2&gt;

&lt;p&gt;The recent COVID-19 pandemic surfaced the shortcomings of humanity to work together to solve fundamental healthcare issues. Some “experts” rushed to call it a black swan despite it clearly not being one (&lt;a href="https://www.newyorker.com/news/daily-comment/the-pandemic-isnt-a-black-swan-but-a-portent-of-a-more-fragile-global-system"&gt;read more&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Thousands of Americans are victims of the opioid crisis and countless ones&lt;br&gt;
are losing their money in the wild-west of unregulated fintech companies with mottos like “anyone can be a trader” or in cryptocurrencies 🙃.&lt;/p&gt;

&lt;p&gt;The media promote optimising for the short-term making us think we’re the last generation to walk the planet. Our countries are involved in wars, we fund wars with our taxes, yet the disasters that war brings seem so remote.&lt;/p&gt;

&lt;p&gt;How do we respond? We &lt;strong&gt;educate&lt;/strong&gt; ourselves, we &lt;strong&gt;talk&lt;/strong&gt;, we &lt;strong&gt;act&lt;/strong&gt;. A university degree or a code bootcamp might help you land a job, but securing your freedom of thought it’s a never-ending struggle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learning to Learn
&lt;/h2&gt;

&lt;p&gt;Be more systematic, build your knowledge map and start exploring branch&lt;br&gt;
by branch.&lt;br&gt;
You don’t need a degree as a reward, nor any imaginary internet points.&lt;br&gt;
The real reward is that you’ll be able to better understand the natural world and society.&lt;br&gt;
Think of &lt;a href="https://twitter.com/hashtag/BlackLivesMatter"&gt;#BlackLivesMatter&lt;/a&gt; how much do you know about it? Do you want to be the person who forms&lt;br&gt;
an opinion based on a couple of tweets or headlines?&lt;br&gt;
Create a list of books, films, articles to go through and keep track of your progress.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Reading, has this incredible effect that it minimises your chances of becoming a racist,&lt;br&gt;
an anti-vaxxer, a climate-change denier or a flat earther. Quite a reward ain’t it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Universities
&lt;/h2&gt;

&lt;p&gt;Universities play the role of gatekeepers of knowledge and the perpetuation of class segregation.&lt;br&gt;
Your studies have a certain duration and you get a degree. While such a concept is valuable to maintain&lt;br&gt;
the status quo and keep the economy going it doesn't really seem to be about knowledge.&lt;br&gt;
Learning is a continuous process and universities should not extinguish your passion for knowledge in exchange for a degree.&lt;br&gt;
For me, receiving my degree wasn't something to celebrate, more like a warrant to stay away from the&lt;br&gt;
supposed mecca of learning.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--J_6ihWLn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/qkittv6jqttg4orb14a3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--J_6ihWLn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/qkittv6jqttg4orb14a3.jpg" alt="Alt Text" width="783" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fortunately in the digital age there are plenty of choices when it comes to expanding your knowledge.&lt;br&gt;
&lt;a href="https://www.khanacademy.org/"&gt;Khanacademy&lt;/a&gt;, &lt;a href="https://www.coursera.org/"&gt;Coursera&lt;/a&gt;, &lt;a href="https://www.udacity.com/"&gt;Udacity&lt;/a&gt; to name a few.&lt;br&gt;
Most of them seem heavily leaning towards STEM studies though. Where do we learn about the world?&lt;/p&gt;

&lt;p&gt;First things first, by “the world” I mean outside the tech bubble.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Yes, I can fix your computer, but how do I fix racism, how do I get to know my body, improve my health,&lt;br&gt;
manage my personal finances and understand democracy and political science, so to meaningfully&lt;br&gt;
participate in the commons?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  There's Hope
&lt;/h2&gt;

&lt;p&gt;The open-source community is a fantastic example of people working asynchronously together and&lt;br&gt;
having colossal impact in our lives.&lt;/p&gt;

&lt;p&gt;A laughably simple algo to keep learning could be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Map your knowledge in a graph format&lt;/li&gt;
&lt;li&gt;Keep notes / annotate your readings publicly&lt;/li&gt;
&lt;li&gt;Pick another branch from the tree&lt;/li&gt;
&lt;li&gt;Go to step 2&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Remember, there's nothing embarrassing in admitting you don't know&lt;br&gt;
something, it's empowering and the first step to master any subject.&lt;/p&gt;

&lt;p&gt;What about the cover image of this post?&lt;br&gt;
Well, I couldn't help it and wrote some code to support it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Visualising Knowledge Maps
&lt;/h1&gt;

&lt;p&gt;Khanacademy used to have a &lt;a href="https://khanacademy.fandom.com/wiki/Knowledge_Map"&gt;knowledge map&lt;/a&gt; feature which I found&lt;br&gt;
inspiring. Browsing a universe of infinite topics, continuing education ftw.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TgpRSq6a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/zc8ib108qwwbtvl3u25w.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TgpRSq6a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/zc8ib108qwwbtvl3u25w.jpg" alt="Alt Text" width="880" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Building my Knowledge Map
&lt;/h3&gt;

&lt;p&gt;So, I decided to build my own map and needed to bootstrap it somehow. I&lt;br&gt;
quickly extracted, cleaned and analysed the tags of my Tefter bookmarks.&lt;br&gt;
For the uninitiated, &lt;a href="https://tefter.io"&gt;Tefter&lt;/a&gt; is a social bookmarking app I use heavily and I also develop 🤠.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--K5uy6mAa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/1i721mf8x9h581yrossu.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--K5uy6mAa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/1i721mf8x9h581yrossu.jpg" alt="knowledge graph" width="880" height="1001"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the graph, tags are nodes and edges connect topics when they appear as tags on the same bookmark.&lt;/p&gt;

&lt;p&gt;Is this a real knowledge map? No, it's the minimum viable hack, a&lt;br&gt;
compass to aid me steer towards building the real one.&lt;/p&gt;

&lt;p&gt;Feel free to have a look and remix the code of the visualisation on &lt;a href="https://glitch.com/~helpful-kind-beechnut"&gt;Glitch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To make progress I'll attempt to bring order to that&lt;br&gt;
chaotic graph by grouping some topics together. I'm also starting this&lt;br&gt;
book &lt;a href="https://en.wikipedia.org/wiki/Consilience_(book)"&gt;"Consilence: The Unity of Knowledge"&lt;/a&gt; which might help me with that.&lt;/p&gt;

&lt;p&gt;I'll also try to evaluate some apps and methodologies below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://beepb00p.xyz/promnesia.html"&gt;https://beepb00p.xyz/promnesia.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://foambubble.github.io/foam/"&gt;https://foambubble.github.io/foam/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://roamresearch.com/"&gt;https://roamresearch.com/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.buildingasecondbrain.com/"&gt;https://www.buildingasecondbrain.com/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zettelkasten.de/posts/overview/"&gt;https://zettelkasten.de/posts/overview/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You've reached the end of this post. Please post your thoughts,&lt;br&gt;
feedback and ideas in the comments.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>visualization</category>
      <category>javascript</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Omni - A tabs, history and bookmarks search companion</title>
      <dc:creator>Dimitris Zorbas</dc:creator>
      <pubDate>Mon, 25 May 2020 15:11:50 +0000</pubDate>
      <link>https://dev.to/zorbash/omni-a-tabs-history-and-bookmarks-search-companion-1n4f</link>
      <guid>https://dev.to/zorbash/omni-a-tabs-history-and-bookmarks-search-companion-1n4f</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UCO2RGKL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/J5cpuQA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UCO2RGKL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/J5cpuQA.png" alt="demo1"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We've launched &lt;a href="https://www.producthunt.com/posts/omni-2"&gt;Omni&lt;/a&gt;, the new &lt;a href="https://tefter.io"&gt;Tefter&lt;/a&gt; browser extension that lets you search and navigate your open tabs, history and Tefter bookmarks. It also automatically groups together similar links.&lt;/p&gt;

&lt;p&gt;We usually have many tabs open, it hurts productivity and increases stress. After a certain number of tabs, their titles are not even visible. Most of us switch between tabs, helplessly guessing by the favicons, hundreds of times a day. When it comes to history, trying to find a page from let's say last week, the browsers make it almost impossible. Think about it, Google with their Chrome browser has interest in you blindly "googling" stuff instead of really helping you. Firefox would want you to bookmark everything on Pocket, which shows ads and you may even end up buying a subscription there.&lt;/p&gt;

&lt;p&gt;Omni is the fastest tab and history search tool out there. It's free, never shows ads and won't sell your data. All your data stays local and is optimally indexed. It's tailored for users who prefer keyboard-driven interactions. It analyzes your history and tab switching behavior and the more you use it the better the search results you see.&lt;/p&gt;

&lt;p&gt;We truly believe it can change the way we surf the web and this is only the beginning.&lt;/p&gt;

&lt;p&gt;To install:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://chrome.google.com/webstore/detail/tefter/eldofalegbgagpenjjcapjaogpioldoh"&gt;Chrome&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://addons.mozilla.org/firefox/addon/tefter/"&gt;Firefox&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'd love to get some feedback from you. Please comment and upvote!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.producthunt.com/posts/omni-tabs-and-history-search-by-tefter"&gt;https://www.producthunt.com/posts/omni-tabs-and-history-search-by-tefter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>chrome</category>
      <category>productivity</category>
      <category>bookmarking</category>
      <category>extensions</category>
    </item>
    <item>
      <title>Writing a Command-Line Application in Elixir</title>
      <dc:creator>Dimitris Zorbas</dc:creator>
      <pubDate>Sun, 01 Mar 2020 00:00:00 +0000</pubDate>
      <link>https://dev.to/zorbash/writing-a-command-line-application-in-elixir-c9n</link>
      <guid>https://dev.to/zorbash/writing-a-command-line-application-in-elixir-c9n</guid>
      <description>&lt;p&gt;I've always been fascinated by well-made applications for the terminal. Who doesn't install &lt;code&gt;htop&lt;/code&gt; on a new machine, am I right?&lt;/p&gt;

&lt;p&gt;My plan was to build something that I'd use daily and other people would potentially&lt;br&gt;
find useful. Therefore I decided to build a cli app for &lt;a href="https://tefter.io"&gt;Tefter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's built on &lt;a href="https://elixir-lang.org/"&gt;Elixir&lt;/a&gt; and &lt;a href="https://github.com/ndreynolds/ratatouille"&gt;Ratatouille&lt;/a&gt; and it's open-source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nBITLzDn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/IWBlbfE.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nBITLzDn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/IWBlbfE.png" alt="cli_demo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check out &lt;a href="https://github.com/tefter/cli"&gt;the source&lt;/a&gt; or &lt;a href="https://github.com/tefter/cli/releases"&gt;download&lt;/a&gt; and try it or install via brew.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap tefter/homebrew-cli
brew &lt;span class="nb"&gt;install &lt;/span&gt;tefter
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h1&gt;
  
  
  Why Elixir
&lt;/h1&gt;

&lt;p&gt;Elixir is getting popular for web and distributed applications, but these&lt;br&gt;
days devs tend to write cli apps in Rust / Go / C. We use Elixir at&lt;br&gt;
&lt;a href="https://tefter.io"&gt;Tefter&lt;/a&gt;, so there was a case for code&lt;br&gt;
reuse and at some point I stumbled upon &lt;a href="https://github.com/ndreynolds/ratatouille"&gt;Ratatouille&lt;/a&gt;.&lt;br&gt;
It's an &lt;a href="https://elm-lang.org/"&gt;Elm&lt;/a&gt; inspired framework, which leverages &lt;a href="https://github.com/nsf/termbox"&gt;termbox&lt;/a&gt;,&lt;br&gt;
a C library for text-based user interfaces. Being charmed by the beautiful&lt;br&gt;
API of &lt;a href="https://github.com/ndreynolds/ratatouille"&gt;Ratatouille&lt;/a&gt; and eager to overcome potential hurdles, once again I chose Elixir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High-level language&lt;/li&gt;
&lt;li&gt;Fault-tolerance&lt;/li&gt;
&lt;li&gt;Optimal offline storage (&lt;a href="https://erlang.org/doc/man/ets.html"&gt;ETS&lt;/a&gt; / &lt;a href="https://erlang.org/doc/man/dets.html"&gt;DETS&lt;/a&gt; / &lt;a href="https://erlang.org/doc/man/mnesia.html"&gt;Mnesia&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Some portability with &lt;a href="https://hexdocs.pm/mix/Mix.Tasks.Release.html"&gt;Releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A decent framework (&lt;a href="https://github.com/ndreynolds/ratatouille"&gt;Ratatouille&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Fantastic Concurrency (see: &lt;a href="https://hexdocs.pm/ratatouille/Ratatouille.Runtime.Command.html"&gt;commands&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Code-reloading for quick debugging&lt;/li&gt;
&lt;li&gt;It's so much fun :-)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Releases bundle the VM and could be smaller&lt;/li&gt;
&lt;li&gt;Releases are not truly portable (a release built on a Linux machine won't work on a Mac, etc)&lt;/li&gt;
&lt;li&gt;No trivial way to fork-exec&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;
  
  
  Demo Time!
&lt;/h1&gt;

&lt;p&gt;Take a glimpse of how the app behaves:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EV947mLC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://i.imgur.com/9VgXbtn.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EV947mLC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://i.imgur.com/9VgXbtn.gif" alt="blog_demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  About Tefter
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--OZ7U8W36--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/Ah8wReJ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OZ7U8W36--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/Ah8wReJ.png" alt="tefter_logo.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before going into more detail about the specifics of the &lt;a href="https://github.com/tefter/cli"&gt;cli&lt;/a&gt;, let's talk about Tefter first.&lt;br&gt;
It's a tool aiming to optimise your web surfing routine, a combination of personal search-engine,&lt;br&gt;
a social bookmarking tool and a place to archive stuff to read later and write notes. One would interact&lt;br&gt;
with Tefter through the &lt;a href="https://tefter.io"&gt;Web app&lt;/a&gt;, the &lt;a href="https://chrome.google.com/webstore/detail/tefter/eldofalegbgagpenjjcapjaogpioldoh"&gt;browser extension&lt;/a&gt;, the &lt;a href="https://twitter.com/Tefter_io/status/1106145149019742210"&gt;mobile&lt;/a&gt; and&lt;br&gt;
the &lt;a href="https://github.com/tefter/desktop"&gt;desktop apps&lt;/a&gt; or &lt;a href="https://slack.com/apps/AFBC4A147-tefter"&gt;Slack&lt;/a&gt;!&lt;/p&gt;
&lt;h1&gt;
  
  
  The App
&lt;/h1&gt;

&lt;p&gt;At the moment of writing, it features three main tabs for &lt;code&gt;Search&lt;/code&gt;, &lt;code&gt;Aliases&lt;/code&gt; and &lt;code&gt;Bookmarks&lt;/code&gt;.&lt;br&gt;
Some of the advantages of the cli app to the rest of the available are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You don't have to leave your terminal and keyboard&lt;/li&gt;
&lt;li&gt;vim-style keybindings with mouse support 😎&lt;/li&gt;
&lt;li&gt;Works offline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Shortcuts&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+s&lt;/td&gt;
&lt;td&gt;Jump to Search tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+a&lt;/td&gt;
&lt;td&gt;Jump to Aliases tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+b&lt;/td&gt;
&lt;td&gt;Jump to Bookmarks tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+h&lt;/td&gt;
&lt;td&gt;Jump to Help tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tab&lt;/td&gt;
&lt;td&gt;Jump to the next tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;Jump to the first tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;↑&lt;/td&gt;
&lt;td&gt;Move up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+k&lt;/td&gt;
&lt;td&gt;Move up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;↓&lt;/td&gt;
&lt;td&gt;Move down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+j&lt;/td&gt;
&lt;td&gt;Move down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+d&lt;/td&gt;
&lt;td&gt;Scroll down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+u&lt;/td&gt;
&lt;td&gt;Scroll up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enter&lt;/td&gt;
&lt;td&gt;Open browser window with item under cursor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Esc&lt;/td&gt;
&lt;td&gt;Cancel command / Quit modal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F5&lt;/td&gt;
&lt;td&gt;Force refresh resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+q&lt;/td&gt;
&lt;td&gt;Quit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;Enter filtering mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:&lt;/td&gt;
&lt;td&gt;Enter command mode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Authentication
&lt;/h2&gt;

&lt;p&gt;The authentication is implemented to be as seamless as possible. It&lt;br&gt;
won't ask the user to type their username and password.&lt;/p&gt;

&lt;p&gt;The first time the application is started, it looks for an authentication&lt;br&gt;
token in a &lt;code&gt;~/.tefter&lt;/code&gt; file. This file holds a plain JSON config. If not&lt;br&gt;
found it'll start a tiny web server listening on a random port. It'll&lt;br&gt;
then open a browser window to a special Tefter endpoint which redirects&lt;br&gt;
to the address of the local web server with the authentication token&lt;br&gt;
encoded in the query params. The app then proceeds to create the&lt;br&gt;
&lt;code&gt;~/.tefter&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;See it in action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zML8xpDn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://i.imgur.com/AIWPTCp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zML8xpDn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://i.imgur.com/AIWPTCp.gif" alt="auth"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Search
&lt;/h2&gt;

&lt;p&gt;This work in a similar manner to the auto-complete of the Web app. The&lt;br&gt;
user types and results appear in the panel below. Results can be&lt;br&gt;
bookmarks, lists, domains, tags or aliases.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QPLcw_lh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/Zx7tt5U.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QPLcw_lh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/Zx7tt5U.png" alt="search"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  App Architecture
&lt;/h2&gt;

&lt;p&gt;Let's have a look at how this works. At the moment, the app is structured as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lib/tefter_cli
├── app
│   └── state.ex
├── app.ex
├── application.ex
├── auth_server.ex
├── authentication.ex
├── bookmarks.ex
├── cache.ex
├── command.ex
├── config.ex
├── system.ex
└── views
    ├── aliases
    │   ├── actions.ex
    │   └── state.ex
    ├── aliases.ex
    ├── authentication.ex
    ├── bookmarks
    │   ├── actions.ex
    │   └── state.ex
    ├── bookmarks.ex
    ├── components
    │   ├── bottom_bar.ex
    │   ├── cursor.ex
    │   ├── info_panel.ex
    │   ├── pagination.ex
    │   └── top_bar.ex
    ├── help.ex
    ├── helpers
    │   └── text.ex
    ├── lists.ex
    ├── search
    │   └── state.ex
    └── search.ex
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;views/&lt;/code&gt; directory there is a module per tab, so we have&lt;br&gt;
&lt;code&gt;search.ex&lt;/code&gt;, &lt;code&gt;aliases.ex&lt;/code&gt;, &lt;code&gt;bookmarks.ex&lt;/code&gt; and &lt;code&gt;help.ex&lt;/code&gt;. Each view has a&lt;br&gt;
state management module, eg &lt;code&gt;views/bookmarks/state.ex&lt;/code&gt; and where&lt;br&gt;
applicable a module for actions. The actions handle side-effects such as&lt;br&gt;
the interaction with the server and the cache.&lt;/p&gt;

&lt;p&gt;The main entrypoint for the application is &lt;code&gt;app.ex&lt;/code&gt;. It's rather brief&lt;br&gt;
so it fits in the snippet below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;TefterCli&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;App&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@behaviour&lt;/span&gt; &lt;span class="no"&gt;Ratatouille&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;App&lt;/span&gt;

  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;Ratatouille&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;Subscription&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;TefterCli&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;State&lt;/span&gt;
  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;TefterCli&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Views&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;Search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Bookmarks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Lists&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Authentication&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Help&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nv"&gt;@tabs&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:bookmarks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:help&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;State&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;State&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;token:&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Authentication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;tab:&lt;/span&gt; &lt;span class="ss"&gt;:search&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Search&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;tab:&lt;/span&gt; &lt;span class="ss"&gt;:bookmarks&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Bookmarks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;tab:&lt;/span&gt; &lt;span class="ss"&gt;:aliases&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Aliases&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;tab:&lt;/span&gt; &lt;span class="ss"&gt;:lists&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Lists&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;tab:&lt;/span&gt; &lt;span class="ss"&gt;:help&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Help&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nv"&gt;@doc&lt;/span&gt; &lt;span class="s2"&gt;"Returns the available application tabs"&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;@tabs&lt;/span&gt;

  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;token:&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Subscription&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:check_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Subscription&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:check_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;It's placed under the supervision tree with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="no"&gt;Ratatouille&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Supervisor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;runtime:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;app:&lt;/span&gt; &lt;span class="no"&gt;TefterCli&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;quit_events:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;:key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Ratatouille&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Constants&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ctrl_q&lt;/span&gt;&lt;span class="p"&gt;)}]]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;where we declare the "main" module of the app and that &lt;code&gt;ctrl + q&lt;/code&gt; quits.&lt;/p&gt;

&lt;p&gt;The most important functions of &lt;code&gt;TefterCli.App&lt;/code&gt; are &lt;code&gt;update/2&lt;/code&gt; and &lt;code&gt;render/1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;update/2&lt;/code&gt; receives the &lt;code&gt;model&lt;/code&gt; - the current state of the app, as&lt;br&gt;
the first argument and a message as the seconds one. The message is&lt;br&gt;
usually a tuple &lt;code&gt;{:event, event}&lt;/code&gt; where event is a &lt;a href="https://github.com/nsf/termbox"&gt;termbox&lt;/a&gt; mouse or keyboard event like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;ExTermbox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;ch:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;h:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;key:&lt;/span&gt; &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;mod:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;type:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;w:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;x:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;y:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;update/2&lt;/code&gt; should return the updated state, but in some cases you&lt;br&gt;
may have it return &lt;code&gt;{model(), Command.t()}&lt;/code&gt;.&lt;br&gt;
 More about commands later.&lt;br&gt;
The &lt;code&gt;render/1&lt;/code&gt; receives the &lt;code&gt;model&lt;/code&gt; and must return a &lt;code&gt;%Ratatouille.Element{}&lt;/code&gt;.&lt;br&gt;
Thankfully you don't have to assemble the element structs manually and&lt;br&gt;
there are macros for that. Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;content:&lt;/span&gt; &lt;span class="s2"&gt;"Hello, &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;TefterCli&lt;/code&gt; view modules like &lt;code&gt;TefterCli.Views.Bookmarks&lt;/code&gt; define the &lt;code&gt;render/1&lt;/code&gt; function and&lt;br&gt;
delegate the &lt;code&gt;update/2&lt;/code&gt; to their state management modules like &lt;code&gt;TefterCli.Views.Bookmarks.State&lt;/code&gt;.&lt;br&gt;
At the moment, the model in &lt;code&gt;TefterCli&lt;/code&gt; is a plain map, but will be refactored to be a struct in the future.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anatomy of the view&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TOJrpo6l--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/bdX7l1l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TOJrpo6l--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/bdX7l1l.png" alt="aliases_anatomy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most views share the &lt;a href="https://github.com/tefter/cli/blob/master/lib/tefter_cli/views/components/top_bar.ex"&gt;&lt;code&gt;TopBar&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/tefter/cli/blob/master/lib/tefter_cli/views/components/bottom_bar.ex"&gt;&lt;code&gt;BottomBar&lt;/code&gt;&lt;/a&gt;. Views with paginated resources have the &lt;a href="https://github.com/tefter/cli/blob/master/lib/tefter_cli/views/components/info_panel.ex"&gt;&lt;code&gt;InfoPanel&lt;/code&gt;&lt;/a&gt; which displays pagination info the permits typing commands.&lt;/p&gt;
&lt;h3&gt;
  
  
  Aliases
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What is an alias?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Think of an alias as a dynamic shortened link. You can create a &lt;code&gt;maps&lt;/code&gt;&lt;br&gt;
alias pointing to &lt;code&gt;https://www.google.com/maps/search/{{*}}?hl=en&amp;amp;source=opensearch&lt;/code&gt; and then with the browser extension installed,&lt;br&gt;
type &lt;code&gt;go/maps/london&lt;/code&gt; in the address bar to be redirected to&lt;br&gt;
&lt;code&gt;https://www.google.com/maps/search/london?hl=en&amp;amp;source=opensearch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So with &lt;code&gt;{{*}}&lt;/code&gt; you can set dynamic segments in your shortened links.&lt;br&gt;
Dynamic segments are optional though.&lt;/p&gt;

&lt;p&gt;In the command-line app you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;View all the aliases you've created&lt;/li&gt;
&lt;li&gt;Create an alias with the &lt;code&gt;:c &amp;lt;alias&amp;gt; &amp;lt;url&amp;gt;&lt;/code&gt; command&lt;/li&gt;
&lt;li&gt;Delete an alias with the &lt;code&gt;:d&lt;/code&gt; command&lt;/li&gt;
&lt;li&gt;Search for an alias by typing &lt;code&gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Open a browser window with the link of an alias by pressing &lt;code&gt;enter&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Idn9BJtV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://i.imgur.com/yW7ThPX.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Idn9BJtV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://i.imgur.com/yW7ThPX.gif" alt="aliases"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Bookmarks
&lt;/h3&gt;

&lt;p&gt;The bookmarks tab is very similar to aliases. A user is likely to have way more bookmarks than aliases,&lt;br&gt;
since on average a user has ~1000 bookmarks but fewer than 10 aliases.&lt;br&gt;
This calls for a different pagination strategy. In aliases, there's a&lt;br&gt;
sliding &lt;code&gt;viewport&lt;/code&gt; with an offset controlled by the cursor. This doesn't&lt;br&gt;
work well with bookmarks. I tested it initially with my bookmarks (I&lt;br&gt;
have more that 9K bookmarks) and it was sluggish. The reason is, that on&lt;br&gt;
every keyboard / mouse event, &lt;a href="https://github.com/ndreynolds/ratatouille"&gt;Ratatouille&lt;/a&gt; tries to re-render everything.&lt;br&gt;
In the case of thousands of bookmarks within a viewport, it renders each&lt;br&gt;
and every bookmark despite most of them being off-screen. The solution&lt;br&gt;
is to only feed a slice of the bookmarks list to the viewport.&lt;/p&gt;

&lt;p&gt;On a similar note, &lt;a href="https://github.com/ndreynolds/ratatouille"&gt;Ratatouille&lt;/a&gt; will re-render everything every&lt;br&gt;
500ms, see &lt;a href="https://github.com/ndreynolds/ratatouille/blob/master/lib/ratatouille/runtime.ex#L53"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding Bookmarks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One can add a bookmark by typing &lt;code&gt;:c &amp;lt;url&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iL2auJyH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/kHi1JR5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iL2auJyH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/kHi1JR5.png" alt="add_bookmark"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deleting Bookmarks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To delete a bookmark, type &lt;code&gt;:d&lt;/code&gt; and the currently selected bookmark will&lt;br&gt;
be deleted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filtering&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To filter, type &lt;code&gt;/&lt;/code&gt;. To highlight a result, &lt;a href="https://github.com/tefter/cli/blob/master/lib/tefter_cli/views/helpers/text.ex#L12"&gt;&lt;code&gt;TefterCli.Views.Helpers.Text.highlight/2&lt;/code&gt;&lt;/a&gt; is used.&lt;br&gt;
Unfortunately, &lt;a href="https://github.com/ndreynolds/ex_termbox"&gt;ExTermbox&lt;/a&gt; seems to drop diacritical marks from strings which would let me&lt;br&gt;
render highlighted text like this "z̲o̲r̲b̲a̲s̲h̲" (see: &lt;a href="https://dev.tounderline"&gt;TefterCli.Views.Helpers.Text.underline/1&lt;/a&gt;)&lt;br&gt;
and I resorted in surrounding matching text with &lt;code&gt;[&lt;/code&gt; and &lt;code&gt;]&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;
  
  
  Development
&lt;/h1&gt;

&lt;p&gt;Clone the &lt;a href="https://github.com/tefter/cli"&gt;repo&lt;/a&gt;, run it with &lt;code&gt;iex -S mix&lt;/code&gt;, make changes and&lt;br&gt;
you're welcome to submit a pull-request!&lt;/p&gt;

&lt;p&gt;Since the app takes over your IEx session, to simplify your debugging,&lt;br&gt;
most events are logged to a file in &lt;code&gt;log/dev.log&lt;/code&gt; in development.&lt;/p&gt;

&lt;p&gt;To drop to the IEx console, you can go to the &lt;code&gt;search&lt;/code&gt;&lt;br&gt;
tab and hit ctrl + y.&lt;/p&gt;

&lt;p&gt;To reload the source without restarting the app hit f5.&lt;/p&gt;
&lt;h1&gt;
  
  
  Packaging
&lt;/h1&gt;

&lt;p&gt;There are two simple bash scripts to prepare releases for Linux and MacOS&lt;br&gt;
in &lt;code&gt;./bin/release_linux&lt;/code&gt; and &lt;code&gt;./bin/release_macos&lt;/code&gt; respectively.&lt;br&gt;
They both use &lt;code&gt;mix release&lt;/code&gt; to prepare a tarball which bundles the Erlang VM.&lt;br&gt;
The Linux script leverages Docker to ensure that a release can be built&lt;br&gt;
even on a non-Linux machine.&lt;/p&gt;
&lt;h1&gt;
  
  
  The framework - Ratatouille
&lt;/h1&gt;

&lt;p&gt;Ratatouille is impressive. It makes you want to write something in it&lt;br&gt;
and it's well documented and it's also rather simple. One can read its&lt;br&gt;
source in one go.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;There's the view, which is better explained quoting the documentation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In Ratatouille, a view is simply a tree of elements. Each element in the tree&lt;br&gt;
holds an attributes map and a list of zero or more child nodes. Visually, it&lt;br&gt;
looks like something this:&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;tag:&lt;/span&gt; &lt;span class="ss"&gt;:view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;attributes:&lt;/span&gt; &lt;span class="p"&gt;%{},&lt;/span&gt;
  &lt;span class="ss"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;tag:&lt;/span&gt; &lt;span class="ss"&gt;:row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;attributes:&lt;/span&gt; &lt;span class="p"&gt;%{},&lt;/span&gt;
      &lt;span class="ss"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;tag:&lt;/span&gt; &lt;span class="ss"&gt;:column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;attributes:&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;size:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[]},&lt;/span&gt;
        &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;tag:&lt;/span&gt; &lt;span class="ss"&gt;:column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;attributes:&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;size:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[]},&lt;/span&gt;
        &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;tag:&lt;/span&gt; &lt;span class="ss"&gt;:column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;attributes:&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;size:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[]}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Then there's the runtime, which is basically this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="k"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="ss"&gt;:ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Window&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="k"&gt;receive&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;type:&lt;/span&gt; &lt;span class="nv"&gt;@resize_event&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;state&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;process_update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;:resize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;quit_event?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quit_events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;
        &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;process_update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;:event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:command_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;state&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;process_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;after&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;state&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;process_subscriptions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;I'd change &lt;a href="https://github.com/ndreynolds/ratatouille/blob/master/lib/ratatouille/runtime.ex"&gt;Ratatouille.Runtime&lt;/a&gt; to be a GenServer for&lt;br&gt;
plenty of reasons, introspection with &lt;code&gt;:sys&lt;/code&gt; being one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;Ratatouille is fantastic, but there are a few things that could be improved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performs unnecessary re-renderings&lt;/li&gt;
&lt;li&gt;Lack of form controls&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  What's Next
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Debian and Homebrew packages&lt;/li&gt;
&lt;li&gt;Windows support (mention the desktop app)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://guides.tefter.io/features/lists/"&gt;Lists&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://guides.tefter.io/features/team_collaboration/"&gt;Organisations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Create and delete aliases&lt;/li&gt;
&lt;li&gt;Display notes in an overlay&lt;/li&gt;
&lt;li&gt;Edit a bookmark
Trigger the edit mode with the &lt;code&gt;:e&lt;/code&gt; command.
Then open &lt;code&gt;$EDITOR&lt;/code&gt; with a tempfile containing the JSON representation
of the bookmark. When the editor is closed update the bookmark&lt;/li&gt;
&lt;li&gt;Import chrome / firefox bookmarks&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Thank you
&lt;/h1&gt;

&lt;p&gt;I want to thank &lt;a href="https://github.com/ndreynolds"&gt;ndreynolds&lt;/a&gt; for creating Ratatouille and I hope people will gain something&lt;br&gt;
by reading this post and the source of the app and provide feedback!&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>commandline</category>
      <category>erlang</category>
      <category>terminal</category>
    </item>
    <item>
      <title>A Slack bookmarking application in Elixir with Opus</title>
      <dc:creator>Dimitris Zorbas</dc:creator>
      <pubDate>Wed, 23 Oct 2019 00:00:00 +0000</pubDate>
      <link>https://dev.to/zorbash/a-slack-bookmarking-application-in-elixir-with-opus-3b53</link>
      <guid>https://dev.to/zorbash/a-slack-bookmarking-application-in-elixir-with-opus-3b53</guid>
      <description>&lt;p&gt;This post describes how we used Elixir and Opus in one of our services at&lt;br&gt;
&lt;a href="https://tefter.io"&gt;Tefter&lt;/a&gt;, which implements bookmarking collaboration in Slack.&lt;/p&gt;
&lt;h2&gt;
  
  
  My relationship with Slack
&lt;/h2&gt;

&lt;p&gt;I remember, when Slack started getting viral and it was set as the main&lt;br&gt;
chat app  at work, I was very reluctant to use it. I was quite happy with IRC and always in favour of open protocols. Since it supported an IRC / XMPP gateway, tweaking my &lt;a href="https://packages.debian.org/stable/irssi"&gt;irssi&lt;/a&gt; config and later &lt;a href="https://packages.debian.org/stable/finch"&gt;finch&lt;/a&gt; was trivial and my overall experience was good. Later I developed my first Slack apps to experiment, accomplish trivial tasks and participate in company hackathons.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tefter
&lt;/h2&gt;

&lt;p&gt;Recently at &lt;a href="https://tefter.io"&gt;Tefter&lt;/a&gt;, we released a new &lt;a href="https://tefter.io/faq#organizations"&gt;organisations&lt;/a&gt; feature. This feature, gives users the ability to collaborate within a Slack workspace.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RZ0S7eZR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/DWfE87A.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RZ0S7eZR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/DWfE87A.png" alt="tefter create organization" width="880" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Essential commands of the Slack app:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create a bookmark&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/tefter &amp;lt;url&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create an alias&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/tefter &lt;span class="nb"&gt;alias&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;alias&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &amp;lt;url&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Search&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/tefter search &amp;lt;query&amp;gt;

&lt;span class="c"&gt;# Alternatively&lt;/span&gt;
/tefter s &amp;lt;query&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what a search looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--54F7K8FQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/aczWLnX.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--54F7K8FQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/aczWLnX.png" alt="tefter search slack with organizations" width="880" height="575"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrieve a link by alias&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/tefter &amp;lt;&lt;span class="nb"&gt;alias&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aliases are especially useful for recurring questions concerning a link. For example&lt;br&gt;
"What is the API documentation page for service x?". There you can&lt;br&gt;
create a "docs" alias and point people to the link by calling &lt;code&gt;/tefter docs&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Microlith
&lt;/h2&gt;

&lt;p&gt;The microservice dealing with that side of our system is named Microlith. That is to contradict&lt;br&gt;
its tendency to become a monolith 🙈. It is written in Elixir and it&lt;br&gt;
leverages a library for railway-oriented programming called &lt;a href="https://github.com/zorbash/opus"&gt;Opus&lt;/a&gt;.&lt;br&gt;
Surprisingly I've never blogged about this tiny library of mine before, but a &lt;a href="https://medium.com/quiqup-engineering/how-to-create-beautiful-pipelines-on-elixir-with-opus-f0b688de8994"&gt;few&lt;/a&gt; &lt;a href="https://www.pagerduty.com/eng/elixir-webhook-service/"&gt;others&lt;/a&gt; have. It incorporates some software design principles I keep close to my heart.&lt;/p&gt;

&lt;p&gt;The main principles of Opus are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each Opus pipeline module has a single entry point and returns tagged tuples &lt;code&gt;{:ok, value}&lt;/code&gt; | &lt;code&gt;{:error, error}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A pipeline is a composition of stateless stages&lt;/li&gt;
&lt;li&gt;A stage returning &lt;code&gt;{:error, _}&lt;/code&gt; halts the pipeline&lt;/li&gt;
&lt;li&gt;A stage may be skipped based on a condition function (&lt;code&gt;:if&lt;/code&gt; and &lt;code&gt;:unless&lt;/code&gt; options)&lt;/li&gt;
&lt;li&gt;Exceptions are converted to &lt;code&gt;{:error, error}&lt;/code&gt; tuples by default&lt;/li&gt;
&lt;li&gt;An exception may be left to raise using the &lt;code&gt;:raise&lt;/code&gt; option&lt;/li&gt;
&lt;li&gt;Each stage of the pipeline is instrumented. Metrics are captured automatically (but can be disabled).&lt;/li&gt;
&lt;li&gt;Errors are meaningful and predictable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, I'll show you some code examples from Microlith where Opus is used.&lt;/p&gt;

&lt;p&gt;The great thing about Opus is that a use-case can be described as a&lt;br&gt;
series of stages. Similar to your grandma's beef stew recipe.&lt;/p&gt;
&lt;h3&gt;
  
  
  Creating a bookmark with Opus
&lt;/h3&gt;

&lt;p&gt;So the "recipe" to create a bookmark from Slack is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;check that the payload has the correct format&lt;/li&gt;
&lt;li&gt;check that the payload contains a URL to bookmark&lt;/li&gt;
&lt;li&gt;normalise the URL&lt;/li&gt;
&lt;li&gt;retrieve the Tefter account by its Slack identifier&lt;/li&gt;
&lt;li&gt;check that the account can create a bookmark&lt;/li&gt;
&lt;li&gt;create the bookmark&lt;/li&gt;
&lt;li&gt;respond to the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our next move will be to translate this to pseudo-code in Opus terms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="ss"&gt;:valid_payload?&lt;/span&gt;
&lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="ss"&gt;:payload_contains_url?&lt;/span&gt;
&lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ss"&gt;:normalize_url&lt;/span&gt;
&lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ss"&gt;:fetch_user&lt;/span&gt;
&lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="ss"&gt;:can_create_bookmark?&lt;/span&gt;
&lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ss"&gt;:create_bookmark&lt;/span&gt;
&lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ss"&gt;:respond&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick rundown of the available stages of Opus.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;step&lt;/code&gt;: This stage processes the input value and with a success value the next stage is called with that value.&lt;br&gt;
With an error value the pipeline is halted and an &lt;code&gt;{:error, any}&lt;/code&gt; is returned.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;check&lt;/code&gt;: This stage is intended for validations. It calls the stage function and unless it returns true, it halts the pipeline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;tee&lt;/code&gt;: This stage is intended for side effects, such as a notification or a call to an external system where the return value is not meaningful. It never halts the pipeline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;link&lt;/code&gt;: This stage is to link with another Opus.Pipeline module. It calls &lt;code&gt;call/1&lt;/code&gt; for the provided module. If the module is not an &lt;code&gt;Opus.Pipeline&lt;/code&gt; it is ignored.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;skip&lt;/code&gt;: The skip macro can be used for linked pipelines. A linked pipeline may act as a true bypass, based on a condition,&lt;br&gt;
expressed as either &lt;code&gt;:if&lt;/code&gt; or &lt;code&gt;:unless&lt;/code&gt;. When skipped, none of the stages are executed and it returns the input,&lt;br&gt;
to be used by any next stages of the caller pipeline.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now we can define our Opus.Pipeline module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Microlith&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Commands&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;CreateBookmark&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@moduledoc&lt;/span&gt; &lt;span class="s2"&gt;"Pipeline which handles the bookmark command to create a bookmark"&lt;/span&gt;

  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Opus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Pipeline&lt;/span&gt;

  &lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;Microlith&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Pipelines&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;FetchUser&lt;/span&gt;

  &lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="ss"&gt;:valid_payload?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;error_message:&lt;/span&gt; &lt;span class="ss"&gt;:invalid_payload&lt;/span&gt;
  &lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="ss"&gt;:contains_url?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;error_message:&lt;/span&gt; &lt;span class="s2"&gt;"Command called without a URL"&lt;/span&gt;
  &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ss"&gt;:trim_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="nv"&gt;&amp;amp;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;url:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;&amp;amp;1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:url&lt;/span&gt;&lt;span class="p"&gt;])}&lt;/span&gt;
  &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ss"&gt;:fetch_user&lt;/span&gt;
  &lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="ss"&gt;:can_create_bookmark?&lt;/span&gt;
  &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ss"&gt;:create_bookmark&lt;/span&gt;
  &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ss"&gt;:respond&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The module we just defined, can be used as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;
  &lt;span class="ss"&gt;input:&lt;/span&gt; &lt;span class="s2"&gt;"https://zorbash.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;slack_user_id:&lt;/span&gt; &lt;span class="s2"&gt;"a Slack user identifier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;team_id:&lt;/span&gt; &lt;span class="s2"&gt;"a Slack team identifier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;team_domain:&lt;/span&gt; &lt;span class="s2"&gt;"whitehouse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;response_url:&lt;/span&gt; &lt;span class="s2"&gt;"https://hooks.api.slack.com/deadbeef"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="no"&gt;Microlith&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Commands&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;CreateBookmark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="no"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;send_resp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"The request could not be accepted"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Concurrency
&lt;/h2&gt;

&lt;p&gt;Like any decent cooking recipe, the implementation is left to the chef.&lt;br&gt;
You may want to crack 6 eggs with one hand while stirring some sauce&lt;br&gt;
with the other, but the end-result should be the same. &lt;/p&gt;

&lt;p&gt;Thankfully the Elixir toolset is very well equipped with facilities to make operations in a pipeline concurrent. In most cases all we have to do is to start &lt;a href="https://elixir-lang.org/getting-started/mix-otp/distributed-tasks.html#asyncawait"&gt;Task&lt;/a&gt;s and pass them down to next stages. When a stage requires the result of a Task, &lt;code&gt;Task.await/1&lt;/code&gt; can be used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visualising Pipelines
&lt;/h2&gt;

&lt;p&gt;I kept my favourite part for last.&lt;/p&gt;

&lt;p&gt;With Opus you can visualise your pipelines using &lt;code&gt;Opus.Graph&lt;/code&gt; using &lt;a href="https://hexdocs.pm/opus_graph/Opus.Graph.html"&gt;&lt;code&gt;Opus.Graph.generate/1&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So at some point in the development of Microlith, it looked like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fjYoma-Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/bW0oubj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fjYoma-Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/bW0oubj.png" alt="opus visualisation of tefter" width="880" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Protip: You should prefer the SVG output, where you can hover on stages and pipelines to read their documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;I hope that this post gives an idea of the features of Opus and I promise to cover the next of them in a following post.&lt;br&gt;
If you're using Opus, I'd be glad to hear your feedback.&lt;/p&gt;

&lt;p&gt;Do you represent an open-source community and you're interested to try&lt;br&gt;
out Tefter Organizations? Let me know and we'll add you to an unlimited&lt;br&gt;
plan without any cost.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>slack</category>
      <category>bookmarking</category>
    </item>
  </channel>
</rss>
