<?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: Luke Barnard</title>
    <description>The latest articles on DEV Community by Luke Barnard (@luke).</description>
    <link>https://dev.to/luke</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%2F46629%2F7dfc7d21-1a29-44e3-b7d8-7990f337ba93.png</url>
      <title>DEV Community: Luke Barnard</title>
      <link>https://dev.to/luke</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/luke"/>
    <language>en</language>
    <item>
      <title>Morgen Assist + OpenAI generate my daily plan + send it to my Slack. Here’s how.</title>
      <dc:creator>Luke Barnard</dc:creator>
      <pubDate>Mon, 30 Oct 2023 10:22:43 +0000</pubDate>
      <link>https://dev.to/luke/morgen-assist-openai-generate-my-daily-plan-send-it-to-my-slack-heres-how-h6k</link>
      <guid>https://dev.to/luke/morgen-assist-openai-generate-my-daily-plan-send-it-to-my-slack-heres-how-h6k</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally written by Luke for &lt;a href="https://morgen.so"&gt;morgen.so&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This year, I had the great pleasure of joining a very exciting startup, which has a mission to revolutionise the way teams automate their time management.&lt;/p&gt;

&lt;p&gt;Morgen recently launched a new product – &lt;a href="https://morgen.so"&gt;Morgen Assist&lt;/a&gt; – which we think is really going to supercharge calendar automation and see 1000s of awesome workflows and apps come to market, with Morgen powering the whole thing.&lt;/p&gt;

&lt;p&gt;But don’t take my word for it, follow along and find out how easy it is to write, test and deploy your own calendar workflows from your very own environment.&lt;/p&gt;

&lt;p&gt;And I’m sorry but putting OpenAI in the title was the easiest way to get you to click but I promise it is necessary for this &amp;lt;100 line app that intelligently summarises calendar events from any-and-all of your calendars*.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google and O365 supported currently, CalDAV coming Soon™️&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  “What am I doing today?”
&lt;/h2&gt;

&lt;p&gt;You might have wondered this question, coffee in hand, staring blankly into your remote working office having rolled out of bed and into the living room this morning. &lt;/p&gt;

&lt;p&gt;With 50+ new slack messages from the PM, design team, marketing and support channel, you might forget to check your calendar, which is currently lost amidst a sea of Firefox tabs, terminals and well… more terminals.&lt;/p&gt;

&lt;p&gt;And that’s where the the Morning Schedule Workflow comes in.&lt;/p&gt;

&lt;p&gt;A slack message is delivered every morning (or evening) straight to your DMs informing you of any upcoming meetings, including retrospectives, 1-1s, other sprint meetings, customer calls, all in the language and tone of your choice.&lt;/p&gt;

&lt;p&gt;As a demonstration I have submitted the following text to OpenAI’s ChatGPT model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are the legendary Apple founder, Steve Jobs delivering a key note

Please summarise all of these upcoming events listed in a way typical with your
classic presentation style.

- 1-1 with David

- Retrospective on the last sprint

- Customer Call /w Apple
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is what I got back:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ladies and gentlemen, &lt;/p&gt;

&lt;p&gt;Today, I'm here to give you a brief update on some upcoming events that are of utmost importance to our company.&lt;/p&gt;

&lt;p&gt;First on the list, we have a 1-1 session scheduled with David. This is a fantastic opportunity for us to connect on a personal level, exchange ideas, and align our vision. Such interactions are …&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And it goes on. The full keynote is truly inspiring but you’ll have to see that for yourself.&lt;/p&gt;

&lt;p&gt;So we have the central piece of the puzzle, but how does this fit within a Morgen Assist workflow?&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started with Morgen Assist
&lt;/h2&gt;

&lt;p&gt;Next up, you’re going to need an account with &lt;a href="https://morgen.so"&gt;Morgen Assist&lt;/a&gt;. You’ll only need an email address and a calendar account which is either provided by Microsoft O365 or Google.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a custom workflow
&lt;/h3&gt;

&lt;p&gt;We will first need a custom workflow on the Morgen Assist platform, which you can find here.&lt;/p&gt;

&lt;p&gt;Don’t forget to &lt;a href="https://platform.morgen.so/integrations/connected"&gt;add a calendar integration&lt;/a&gt; to your account, you’ll need that shortly. You can ignore the slack integration for now - this is for a slightly different use-case.&lt;/p&gt;

&lt;p&gt;Then follow these steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set the trigger type to “timed” at a time of day of your choosing (e.g. 6pm)&lt;/li&gt;
&lt;li&gt;Select a calendar to source the events to summarise&lt;/li&gt;
&lt;li&gt;And finally insert code into the User Script section according to the instructions below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✨ Note that if you want that hacker 10x engineer experience, you can also write and deploy your workflow via the &lt;a href="https://github.com/morgen-so/morgen-cw-sdk"&gt;SDK&lt;/a&gt;, which has a TypeScript API client for making requests to our backend. Currently your code has to fit entirely into the &lt;code&gt;run&lt;/code&gt; function, which I’ll explain below. Some interaction is still necessary in the web app, but we’re looking at improving this in future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the Script
&lt;/h2&gt;

&lt;p&gt;Thankfully, a version of this workflow has already been written and can be accessed in a form that is compatible with both the Morgen Assist SDK and the Morgen Assist Web Interface.&lt;/p&gt;

&lt;p&gt;You can find it here: &lt;a href="https://github.com/morgen-so/morgen-cw-sdk/blob/main/examples/openai-tomorrow-summary.ts"&gt;https://github.com/morgen-so/morgen-cw-sdk/blob/main/examples/openai-tomorrow-summary.ts&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;I will now break this script down.&lt;/p&gt;

&lt;p&gt;First up, we’re going to need to include a couple of utilities to make requests to Morgen and OpenAI. Paste the following into the top of your user script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;fetchMorgen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&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="s1"&gt;application/json&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;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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="nx"&gt;opts&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getOpenAIResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.openai.com/v1/chat/completions&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer [YOUR-TOKEN]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Accepts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="s1"&gt;Content-Type&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="s1"&gt;application/json&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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model&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;gpt-3.5-turbo&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;messages&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&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;user&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;content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;
        &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;temperature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error with OpenAI: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&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;Note that you’ll need an OpenAI token for this to all work as expected. Once you create an OpenAI account, you can create tokens here: &lt;a href="https://platform.openai.com/account/api-keys"&gt;https://platform.openai.com/account/api-keys&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s get the Slack token ready as well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new app here: &lt;a href="https://api.slack.com/apps"&gt;https://api.slack.com/apps&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;(this is how the summary bot will access and appear in Slack)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Set your app's appearance and permissions

&lt;ul&gt;
&lt;li&gt;Go to “OAuth &amp;amp; Permissions”, find the “Bot Token Scopes” section&lt;/li&gt;
&lt;li&gt;Add the “chat:write” permission&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Scroll up to the “Oauth Tokens for Your Workspace” section and click “Install to Workspace”, click “Allow” on the confirmation page&lt;/li&gt;
&lt;li&gt;Find the xorb-... token under “OAuth &amp;amp; Permissions”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Okay, now let’s write a basic run function. This function will be called when the workflow is triggered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slackToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;xorb-...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openAIToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;k-...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Logic goes here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally let’s look at the actual logic of this workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetching tomorrow’s events
&lt;/h2&gt;

&lt;p&gt;To do this, we need to make an API call to the Morgen API with a start/end range that captures tomorrow midnight until midnight the day after.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;
&lt;span class="c1"&gt;// fetchMorgen () { ... }&lt;/span&gt;
&lt;span class="c1"&gt;// getOpenAIResponse () { ... }&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slackToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;xorb-...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openAIToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;k-...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Fetch events from tomorrow&lt;/span&gt;

  &lt;span class="c1"&gt;// Get the first calendar selected in workflow config web page&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;calId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;calendar&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="nx"&gt;calendarId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Set the start of the range to the start of tomorrow&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;luxon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startTs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;day&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;plus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;days&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="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toISO&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;includeOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;suppressMilliseconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Set the end of the range to the start of the day after tomorrow&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endTs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;day&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;plus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;days&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="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toISO&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;includeOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;suppressMilliseconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Make a API request with fetchMorgen utility we added before&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mresp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetchMorgen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://sync.morgen.so/v1/events/list`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s2"&gt;`?calendarIds=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;calId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s2"&gt;`&amp;amp;start=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;startTs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s2"&gt;`&amp;amp;end=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;endTs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;evs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mresp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&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="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;If you run this, provided there are events in your selected calendar for tomorrow, you should see them listed below the editor after you click “Run &amp;amp; Save”.&lt;/p&gt;

&lt;p&gt;Note that it might take a while for anything to happen, we’re working on improving this part of the custom workflows feature.&lt;/p&gt;

&lt;p&gt;You should see a JSON blob logged that corresponds to a list of events in your calendar tomorrow. :party:&lt;/p&gt;

&lt;p&gt;Next we’re going to send the titles of these events to OpenAI to see what it can come up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  ChatGPT, do your thing
&lt;/h2&gt;

&lt;p&gt;Let’s take our events and get ChatGPT to summarise them in a simple format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;run&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;evs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mresp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Get all busy events and format for ChatGPT&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;evs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;freeBusyStatus&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&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="c1"&gt;// ChatGPT gets confused if we don't order things chronologically&lt;/span&gt;
  &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Generate a nice summary&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;getOpenAIResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Summarise the following calendar events in an easily &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; 
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;digestable chronological bullet list: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Example: Events today:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; 
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;9:00 –&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Standup 15 minutes&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;12:00 –&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Lunch&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;15:00 –&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Catchup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Response: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&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;Now we have that summary, you can try running this to see that it correctly logs a nice bullet-pointed summary of events for tomorrow.&lt;/p&gt;

&lt;p&gt;Of course, you can tweak this to be pretty much anything, for example you might want the movie trailer edition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;getOpenAIResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Summarise the following calendar events &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;in the form of a short movie trailer: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; 
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Response: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the response is hilarious as required:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In a world filled with challenges and opportunities, a team comes together to conquer the day. Brace yourself for an epic journey through the world of corporate endeavor.&lt;/p&gt;

&lt;p&gt;In the early morning light, the team gathers for their gripping "Morning Standup". Witness the camaraderie and determination as they share their progress, align their goals, and prepare for the battles ahead. With each member showcasing their skills and expertise, it's a spectacle that will leave you inspired.&lt;/p&gt;

&lt;p&gt;But victory is not without reflection. As the sun sets and shadows deepen, our heroes find themselves in the midst of an awe-inspiring "Retrospective". Together, they venture into the past, analyzing their triumphs and challenges in order to emerge stronger than ever…&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or a poem perhaps?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In morning's golden light, we convene,&lt;br&gt;
A team united in purpose, it seems,&lt;br&gt;
The standup whispers tales of tasks anew,&lt;br&gt;
Sharing progress, challenges, and breakthroughs.&lt;/p&gt;

&lt;p&gt;With hearts filled with hope and eyes wide awake,&lt;br&gt;
We march towards goals, no time to forsake.&lt;br&gt;
A symphony of voices, ideas flow,&lt;br&gt;
As the morning standup sparks our shared glow.&lt;/p&gt;

&lt;p&gt;But when shadows grow long, and day takes its toll,&lt;br&gt;
We gather once more, to reflect as a whole…&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Brilliant. Something for everyone here with these event summaries. And of course it will always reflect the events of the day.&lt;/p&gt;

&lt;p&gt;I will leave it as an exercise to the reader to try these modifications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ignore recurring events&lt;/li&gt;
&lt;li&gt;Write a prompt to handle the case where there are no events scheduled&lt;/li&gt;
&lt;li&gt;Add a description of tasks that are due to be completed today and this week&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  But how do I send this to Slack?
&lt;/h2&gt;

&lt;p&gt;So long as you have set up your Slack app, as described in the previous section, you should be able to start sending messages on behalf of a bot user that can be controlled with the token you copied.&lt;/p&gt;

&lt;p&gt;To do this, use the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="p"&gt;...&lt;/span&gt; 

  &lt;span class="c1"&gt;// Send summary to Slack user&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.slack.com/api/chat.postMessage&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slackToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&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="s1"&gt;application/json&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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@Luke&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;summary&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;Notice that I sent the message to the &lt;code&gt;“@Luke”&lt;/code&gt; channel, which is how to send a direct message using the Slack API. You’ll have to change that to your username in the workspace that your app is installed under.&lt;/p&gt;

&lt;p&gt;And with that, the workflow is complete. You can try running it, and expect it to run at the configured time in the “Trigger” section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Thanks for making it to the end of my blog, I’m really grateful for it and if you’re interested in finding out more about Morgen Assist, here are some links to get you started:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find out more about how to develop your own Custom Workflows from our &lt;a href="https://docs.morgen.so/cw-introduction"&gt;Morgen Developer Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Check out our pre-alpha SDK for deploying Custom Workflows from your own environment on &lt;a href="https://github.com/morgen-so/morgen-cw-sdk"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Come join the discussion on &lt;a href="https://discord.com/invite/KNaeNhZ9yf"&gt;Discord&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Emulating Xmonad in Javascript - Part 5</title>
      <dc:creator>Luke Barnard</dc:creator>
      <pubDate>Tue, 03 Mar 2020 21:47:04 +0000</pubDate>
      <link>https://dev.to/luke/emulating-xmonad-in-javascript-part-5-40oa</link>
      <guid>https://dev.to/luke/emulating-xmonad-in-javascript-part-5-40oa</guid>
      <description>&lt;p&gt;It's a whole year since I first started developing nomad! I recently made some changes that I thought were worth highlighting with a blog, so here it is!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qG9T1UL---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://pbs.twimg.com/media/ESI4XXRXsAIpOWy%3Fformat%3Djpg%26name%3D4096x4096" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qG9T1UL---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://pbs.twimg.com/media/ESI4XXRXsAIpOWy%3Fformat%3Djpg%26name%3D4096x4096" alt="" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  New control sequence parsing
&lt;/h2&gt;

&lt;p&gt;I decided to reimplement the logic that nomad uses to parse control sequences from program output. This meant ditching a few regex sequences that were mostly created through trial and error and replacing them with something that consumes control sequences character-by-character, checking against a list of known sequences.&lt;/p&gt;

&lt;p&gt;Now when I say "known", I mean documented by some folks who very much appreciate the importance of this weird language known as ANSI. I copied a bunch of documentation from a great resource on these sequences that can be found here: &lt;a href="https://invisible-island.net/xterm/ctlseqs/ctlseqs.html"&gt;https://invisible-island.net/xterm/ctlseqs/ctlseqs.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code in question can be found here: &lt;a href="https://github.com/lukebarnard1/nomad-term/blob/0fd7f1c8b60b6d319c0283a9d63fd6ee961495c6/ctlseqs.js"&gt;https://github.com/lukebarnard1/nomad-term/blob/0fd7f1c8b60b6d319c0283a9d63fd6ee961495c6/ctlseqs.js&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Essentially what it does is it takes a bunch of templates that look a bit like the documentation in the above resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CSI Pm m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and creates a function that will match a given input string to this particular sequence if the string is a &lt;em&gt;prefix&lt;/em&gt; of it.&lt;/p&gt;

&lt;p&gt;I opted to not optimise this code too much so that I could focus on being confident that the matches were valid and that the outputs were restricted. After all, sending all of your program output through one program should require a high level of scrutiny.&lt;/p&gt;

&lt;p&gt;There are definitely some more optimisations to be made here - for example, most sequences start with a CSI (Control Sequence Initiator) of &lt;code&gt;ESC[&lt;/code&gt; followed by a sequence of digits and semi-colons. One potential optimisation here could be to consume a series of characters matching &lt;code&gt;[0-9;]&lt;/code&gt; without even testing against any individual sequences. I'll be making these optimisations soon!&lt;/p&gt;

&lt;p&gt;Changing the parsing of control sequences also meant changing the internal representation of them, which required changes to the (sub)terminal implementation itself. The integration tests I had written were fairly useful but it turns out they may have recorded an incorrect snapshot before, so the actual output wasn't totally useful...&lt;/p&gt;

&lt;p&gt;I still haven't gotten around to writing my dream unit tests of "start with an empty terminal, following these control sequences expect state to have updated in this way". I'll get around to do it! I think this would be a great candidate for some test-driven development of more separate updating of different terminal state.&lt;/p&gt;

&lt;p&gt;Let's just say there's a lot of work left over for a rainy day.&lt;/p&gt;

&lt;h2&gt;
  
  
  New help screen
&lt;/h2&gt;

&lt;p&gt;I showed nomad to my boss and he let me know that when he run nomad, it was "blocked". When he showed me what he meant it was clear that nomad was doing a poor job at giving the user an idea of how to control it! So I decided to make the controls a lot more accessible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gn1pAyh2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/rhzjeujdejfktmkkfu8i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gn1pAyh2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/rhzjeujdejfktmkkfu8i.png" alt="Nomad's little help dialog" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1tKzb7_4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/pishv7pwuysnokrjsdf5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1tKzb7_4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/pishv7pwuysnokrjsdf5.png" alt="Nomad's big help dialog" width="800" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So there it is, a preview of the help box that follows you around when using nomad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it!!
&lt;/h2&gt;

&lt;p&gt;You got this far, why not &lt;a href="https://github.com/lukebarnard1/nomad-term"&gt;try nomad out&lt;/a&gt;! Bear in mind that all of everything you type goes through nomad and everything a program sends to stdout also goes through nomad and is stored in memory. This is true of your &lt;em&gt;actual&lt;/em&gt; terminal (e.g. &lt;code&gt;gnome-terminal&lt;/code&gt;, &lt;code&gt;Terminal.app&lt;/code&gt; etc.) but they're used by the whole world.&lt;/p&gt;

&lt;p&gt;The repo is here: &lt;a href="https://github.com/lukebarnard1/nomad-term"&gt;https://github.com/lukebarnard1/nomad-term&lt;/a&gt; :)&lt;/p&gt;

</description>
      <category>dx</category>
      <category>terminal</category>
      <category>ansi</category>
      <category>nomad</category>
    </item>
    <item>
      <title>Functional components vs. Feature components</title>
      <dc:creator>Luke Barnard</dc:creator>
      <pubDate>Sat, 01 Feb 2020 11:29:10 +0000</pubDate>
      <link>https://dev.to/luke/functional-components-vs-feature-components-1cjd</link>
      <guid>https://dev.to/luke/functional-components-vs-feature-components-1cjd</guid>
      <description>&lt;p&gt;I've been working as part of a product team for a while now and I've started to pick up on a pattern. It comes back to whether development is focused on building what I'm calling &lt;em&gt;features&lt;/em&gt; or &lt;em&gt;functions&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;These terms are quite similar and are quite frequently used interchangeably in software engineering - they're very similar in definition.&lt;/p&gt;

&lt;p&gt;Here I'll define them to avoid any doubt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Feature&lt;/strong&gt; - part of the product that the user can use directly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Function&lt;/strong&gt; - a system component that can exist in isolation from any feature that uses it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When building a feature, a trade-off between &lt;em&gt;reusability&lt;/em&gt; and &lt;em&gt;extensibility&lt;/em&gt; exists whereby features that are developed entirely isolated from each other are infinitely extensible whereas features that share as many components as is technically possible are less extensible.&lt;/p&gt;

&lt;p&gt;If a feature exists in total isolation, none of its components are used by other features. For example, this could be part of an app dedicated to listing blog posts that allows filtering by category and full-text search. If built in isolation, none of the other parts of the app would share any of the same components.&lt;/p&gt;

&lt;p&gt;By nature, isolation lends itself to extensibility and so the app page in the example above could theoretically be changed in any way without impacting other parts of the app.&lt;/p&gt;

&lt;p&gt;But this comes at a cost - building everything from scratch takes longer.&lt;/p&gt;

&lt;p&gt;So naturally most systems are not built in this way and there are hundreds of ways to consider how to architect a system that I won't go into here.&lt;/p&gt;

&lt;p&gt;But one thing that I've seen a lot first-hand is how the feature design process influences the boundaries of each component in a system.&lt;/p&gt;

&lt;p&gt;Which brings me to an observation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;feature-specific&lt;/em&gt; components are bound to a feature and therefore cannot be reused by other features&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;function-specific&lt;/em&gt; components are &lt;em&gt;used&lt;/em&gt; by a feature and &lt;em&gt;can&lt;/em&gt; be reused by other features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By designing a system component to be explicitly bound to a feature, reuse is actively discouraged, whether it's a service, an API endpoint, a button in the UI or anything really. What this allows is for individual features to be changed frequently and without much consequence to other parts of the system (although by nature system components are never totally isolated).&lt;/p&gt;

&lt;p&gt;This might be suitable for certain products that need to change frequently, or that have a short shelf life. But in the long-run features will be developed repeatedly that are similar in nature but differ in their components.&lt;/p&gt;

&lt;p&gt;This can become costly - a lot of dev time that could be spent investing in components for the future is instead spent building components for today.&lt;/p&gt;

&lt;p&gt;By designing a system component to be explicitly bound to a &lt;em&gt;function&lt;/em&gt;, reuse is a fundamental consideration because by the definition above it can be &lt;em&gt;used&lt;/em&gt; by new features in the future. The more features that can utilise a component, the more extensible the product is overall.&lt;/p&gt;

&lt;p&gt;The thought process becomes more about separating the functional requirements of a feature from the feature itself - system components &lt;em&gt;become&lt;/em&gt; functional requirements.&lt;/p&gt;

&lt;p&gt;As an example, take a dashboard for monitoring a service that tracks employee workload by monitoring how many email threads they are active in in a single day. Let's suppose the product team wants to add a page to the dashboard that let's the user view a graph that shows the workload over time for a specific group of employees.&lt;/p&gt;

&lt;p&gt;One way of delivering the feature is to create a new service dedicated to this page, have it expose an API that provides the data, which is stored in a new database called &lt;code&gt;group_workload_stats&lt;/code&gt;. The service updates the table over time and the API will return an updated view when that happens.&lt;/p&gt;

&lt;p&gt;It's easy to see that none of the above can be reused as part of another feature.&lt;/p&gt;

&lt;p&gt;To rethink this implementation, we could consider making each component &lt;em&gt;function-specific&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;For example, the new service could be dedicated to the &lt;em&gt;function&lt;/em&gt; of returning workload data over time - something that is predictably reusable, something required by other features that are planned for the future. It's true that this restricts new features by constraining them to the functionality provided, but it &lt;em&gt;is&lt;/em&gt; possible to build such&lt;br&gt;
services with extensibility in mind.&lt;/p&gt;

&lt;p&gt;In summary, I think there are a lot of benefits to thinking of system components in terms of whether they are &lt;em&gt;feature-specific&lt;/em&gt; or &lt;em&gt;function-specific&lt;/em&gt;. This in theory encourages features comprised of functional components, which in turn saves time in the long run.&lt;/p&gt;

&lt;p&gt;In a later post I'd like to discuss how this relates to design patterns such as microservices and how some parts of the system must define the composition of functions in order to deliver features.&lt;/p&gt;

</description>
      <category>product</category>
      <category>systems</category>
      <category>design</category>
      <category>architecture</category>
    </item>
    <item>
      <title>When to criticise constructively when building a product</title>
      <dc:creator>Luke Barnard</dc:creator>
      <pubDate>Mon, 11 Nov 2019 23:33:25 +0000</pubDate>
      <link>https://dev.to/luke/when-to-criticise-constructively-when-building-a-product-1d7b</link>
      <guid>https://dev.to/luke/when-to-criticise-constructively-when-building-a-product-1d7b</guid>
      <description>&lt;p&gt;I used to complain a lot whilst doing my job. Not because things were actually bad or worth complaining over, but because my ruthless perfectionism caused me to notice things that I knew could be improved, and, to their dismay, the whole team had to hear about it.&lt;/p&gt;

&lt;p&gt;This lead to some lengthy discussions surrounding the possibility of changing parts of the system. One good example was the state management in a React app I was working on.  This was my first production app so with fresh curiosity and perfectionism abundant, I drew everyone's attention to what I thought to be obvious: we weren't using Redux.&lt;/p&gt;

&lt;p&gt;Not only that but the app was making use of dozens of what were essentially singletons, each with their own method of managing state. It was similar to MVC, where a set of controllers expose the model to the view, allowing the view to manipulate the underlying model.&lt;/p&gt;

&lt;p&gt;This is quite a well known pattern and remains a decent approach for many projects. But having just learned about Flux and Redux, I had a new found energy for pointing out that things would be easier if we used another abstraction.&lt;/p&gt;

&lt;p&gt;What I failed to realise at the time was two-fold:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I didn't realise that shipping new features at the time was more important than finding the perfect abstraction and&lt;/li&gt;
&lt;li&gt;I didn't realise that introducing a second abstraction would actually make things harder for everyone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'll circle back to the second one later, but let's focus on why shipping features might have been the right call at the time.&lt;/p&gt;

&lt;p&gt;Every team has its trade-offs and I cannot stress how important it is to know which ones are currently being made.  Sometimes you just have to ship functionality - it just has to work. Sometimes users are frustrated with a certain bit of the app and the UX needs to be improved. Other times the technical debt is so high that it's actually slowing down addition of more features and is making it impossible to fix bugs.&lt;/p&gt;

&lt;p&gt;Whatever the current situation, trade-offs need to be made to rebalance the scales and stop the project from toppling over.&lt;/p&gt;

&lt;p&gt;During this project, the main target was growing the user base - a familiar one for any B2C app. Secondary to that was improving the UX of the app because a lot of users found it confusing relative to other similar apps they were using.&lt;/p&gt;

&lt;p&gt;Although I didn't realise it, to come to the table with the problem of the app using the wrong abstraction was probably the least productive thing I could have done at that point.  But I was convinced it would help us debug the app more easily and implement new features more quickly, so I got buy-in from a few colleagues and started merging changes that were written differently to other parts of the app.&lt;/p&gt;

&lt;p&gt;I justified the changes to myself, "it can't be &lt;em&gt;worse&lt;/em&gt; to add an abstraction that &lt;em&gt;adds&lt;/em&gt; structure - other parts of the app have &lt;em&gt;no&lt;/em&gt; structure!"&lt;/p&gt;

&lt;p&gt;This brings me to the second thing I had failed to realise.  Abstractions increase complexity. When something is made generic, a tradeoff is made where uniformity is bought in exchange for rigidity and potentially hidden complexity.&lt;/p&gt;

&lt;p&gt;What I thought I had done was to slowly start moving the app towards a more generic, easier to understand place but what had actually happened was that I had introduced a new concept that was actually incompatible with existing parts of the app. It even required extra complexity to be compatible with the layer in the stack that made API requests.&lt;/p&gt;

&lt;p&gt;Some new features could be implemented with this new abstract and that's what I would try to do. My colleagues agreed with me that the code was easier to read and they bought into the idea as more features were developed this way.&lt;/p&gt;

&lt;p&gt;But then we had to debug them. The added complexity that we acquired in the tradeoff bit us when bugs arose in the parts of the app using the new abstraction. This obviously was not great for morale and it was all too easy to blame the abstraction itself (Flux/Redux) over the actual problem - increased complexity.&lt;/p&gt;

&lt;p&gt;My goal was actually to decrease complexity and the irony wasn't lost on me; I learned a few lessons as a result.&lt;/p&gt;

&lt;p&gt;Untangling such an architecture would take more than a new paradigm to shift it away from unscalable "ball-of-mud" territory. I think the real problem was probably the API and the fact that it required a sizeable, stateful SDK to manage all of the client-side objects. This left the view and the controllers highly coupled to the SDK.&lt;/p&gt;

&lt;p&gt;In hindsight, it's possible that with a lot of pain the app could be rewritten using React hooks. I remember a lot of the controllers in the app were essentially complex hooks (minus the abstraction now built into React itself).&lt;/p&gt;

&lt;p&gt;But of course, this might not actually make things better - certainly cleaner but not necessarily less complex or easier to debug.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Nothing exists in a vacuum. Understanding the current trade-offs is crucial.&lt;/li&gt;
&lt;li&gt;Buy-in is not always worth it. If you have to work hard to convince your coworkers of something, back it up with concrete evidence (e.g. a POC) and make sure you've convinced yourself first that you're really solving a problem.&lt;/li&gt;
&lt;li&gt;No framework/paradigm is a silver bullet. An abstraction will likely increase complexity.&lt;/li&gt;
&lt;li&gt;Multiple competing paradigms might be more confusing than a single one that isn't strict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To that end, remember to know your trade-offs and hopefully you can make sure that your team is at least making them consciously and planning on rebalancing the scales at some point.&lt;/p&gt;

</description>
      <category>development</category>
      <category>team</category>
    </item>
    <item>
      <title>(Not) Building a Decentralised Blog</title>
      <dc:creator>Luke Barnard</dc:creator>
      <pubDate>Sat, 26 Oct 2019 17:04:45 +0000</pubDate>
      <link>https://dev.to/luke/not-building-a-decentralised-blog-53n0</link>
      <guid>https://dev.to/luke/not-building-a-decentralised-blog-53n0</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/lukebarnard1/journal"&gt;journal&lt;/a&gt; is exactly where I left it a few months ago, looking a bit dated and not being easy to use and not having the features I wanted in my blog site.&lt;/p&gt;

&lt;p&gt;Meanwhile, the main purpose of journal remains unfulfilled. The work to expose a &lt;a href="https://matrix.org"&gt;matrix&lt;/a&gt; room as a journal blog was never done as I haven’t had the time or the will to go and make it happen.&lt;/p&gt;

&lt;p&gt;My original decision to replace the client-based version of journal was a little short-sighted. It’s true that it took a long time to load a blog, but it worked and had live updates and allowed posting of blogs and comments. In hindsight I could have enhanced this with a server that exposed a room as a set of markdown files to be consumed by the frontend.&lt;/p&gt;

&lt;p&gt;But when you think about it, trying to optimise the bit that content management systems have already solved would also have been a bit short-sighted.&lt;/p&gt;

&lt;p&gt;The other way of looking at it is that if the client-based journal could have avoided doing the main blog article bit, it might have actually been a useable solution and a widely compatible one. For example, it could have been embedded into a blog that was already deployed via a CMS.&lt;/p&gt;

&lt;p&gt;Content management is a well developed area of open source, with a lot of different options (e.g. Ghost, Hugo, Jekyll), all that produce static content in a customisable way. Any solution for blogs on matrix should clearly make use of the work that has been done here.&lt;/p&gt;

&lt;p&gt;Disqus is an example of a centralised discussion thread embed which I think is an obvious solution for the matrix community to clone. The matrix equivalent could be highly customisable and integrate with popular matrix clients via a “matrix.to” link (e.g. “Open discussion in Riot.im”).&lt;/p&gt;

&lt;p&gt;For other features that I intended to implement with journal, there are existing solutions that are already widely used. For example, twitter, mastodon or email can be used to notify readers of a new blog (or a message in a matrix room, which could even be the start of the embedded discussion thread!).&lt;/p&gt;

&lt;p&gt;Regardless of the direction I took journal in, I clearly didn’t have time to maintain such a project in it’s entirety. My current plan is to stop maintaining it, eventually replace my personal blog with Hugo and who knows, maybe I’ll have a go at making a Disqus clone for Matrix one day.&lt;/p&gt;

&lt;p&gt;Thanks for reading and showing interest in this project.&lt;/p&gt;

&lt;p&gt;Happy decentralising 😄&lt;/p&gt;

</description>
      <category>decentralised</category>
      <category>personal</category>
      <category>blog</category>
    </item>
    <item>
      <title>Writing a Relevant CV</title>
      <dc:creator>Luke Barnard</dc:creator>
      <pubDate>Tue, 17 Sep 2019 22:56:20 +0000</pubDate>
      <link>https://dev.to/luke/frontend-developer-cv-tips-content-3g26</link>
      <guid>https://dev.to/luke/frontend-developer-cv-tips-content-3g26</guid>
      <description>&lt;p&gt;For the last few months I've been helping out with the frontend developer hiring process at &lt;a href="https://www.tessian.com/careers"&gt;Tessian&lt;/a&gt; (that's right, we're hiring - send me a &lt;a href="https://twitter.com/lukebarnard_"&gt;DM&lt;/a&gt;!) and I'd like to share some thoughts on keeping your CV relevant. I must note, these views are entirely my own - not Tessian's.&lt;/p&gt;

&lt;p&gt;This will be the first in a series of posts that I want to share from the perspective of someone who has been looking at A LOT of CVs with the sole aim of finding really incredible engineers to join an already awesome tech team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context &amp;gt; Breadth of Knowledge
&lt;/h2&gt;

&lt;p&gt;The first thing I notice when I look at a CV is the number of technologies and frameworks mentioned. I find that lists of entire A-Z of libraries, frameworks, architectures or paradigms can distract from the information that matters the most. A CV that really stands out is one that brings to light the unique parts of someone's experience. Mentioning relevant technologies with your previous roles and projects will help give a much clearer idea of how much experience you've had with a particular bit of tech. It also gives context - reading "PHP" on it's own doesn't give as much insight as "helped rewrite PHP monolith as a set of micr-frontends".&lt;/p&gt;

&lt;p&gt;I've also seen some CVs that show how comfortable someone is with certain technologies with graphs or ratings out of 5. When it comes to indicating relative proficiency in certain technologies, I think it's best to stick to the facts, which in this case are previous employment and projects. &lt;/p&gt;

&lt;p&gt;In any case, remember that you can learn anything in time - there should be nothing stopping you from becoming more proficient with a given stack. The projects you list on your CV will showcase this. &lt;/p&gt;

&lt;p&gt;Remember that giving context is more important than the number of technologies you can list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Impact &amp;gt; Work
&lt;/h2&gt;

&lt;p&gt;I've noticed a lot of CVs mention very common tasks that you might do as a developer. In particular I notice most people tend to mention one of the below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developed UI components&lt;/li&gt;
&lt;li&gt;Fixed bugs&lt;/li&gt;
&lt;li&gt;Wrote unit tests&lt;/li&gt;
&lt;li&gt;Participated in SCRUM meetings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every time I read this I find myself wishing that I could ask the candidate what they &lt;em&gt;really&lt;/em&gt; achieved on a particular project and how they had an impact.&lt;/p&gt;

&lt;p&gt;My advice is to focus on the unique aspects of a particular project and the impact of the tasks you did, e.g.:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Optimised the performance of the checkout page, which lead to an increase in successful purchases&lt;/li&gt;
&lt;li&gt;Helped rearchitect a Rails monolith to a set of micro frontends to increase maintainability and decouple key products&lt;/li&gt;
&lt;li&gt;Introduced BDD practice to the team to increase stability in new features&lt;/li&gt;
&lt;li&gt;Merged multiple repositories into a monorepo, which decreased lead time&lt;/li&gt;
&lt;li&gt;Maintained development environment for the frontend team to speed up local testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you can't think of many achievements for a particular role, try describing the project and what it achieved or parts of the software that you built and maintained. &lt;/p&gt;

&lt;p&gt;Remember, the impact of your work is more important than the work itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Humility &amp;gt; Exaggeration
&lt;/h2&gt;

&lt;p&gt;When writing your CV it can be tempting to exaggerate or show off about the things you've achieved. This might seem appropriate given that you want to wow a potential employer and appear to be superior to other candidates. For instance, I've seen a few CVs that decorate experience or projects with adjectives and impressive numbers.&lt;/p&gt;

&lt;p&gt;The problem with this is that it has unintended biases in the mind of the reader. Exaggeration distracts from the facts themselves. The experience on your CV should be able to stand alone without any exaggeration. Being transparent and humble will go a long way, if the facts are impressive they should impress without the extra additives.&lt;/p&gt;

&lt;p&gt;As an example, let's say I decided to write on my CV:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Optimised the SEO of the landing page which lead to a dramatic increase of traffic (x10)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Compare this with the following:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Optimised the SEO of the landing page which lead to an increase in the number of users registering&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With a small adjustment, the statement is more humble, and focusses more on the impact of the completed task.&lt;/p&gt;

&lt;p&gt;Remember, your experience got you to where you are today - it doesn't need exaggeration.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;To summarise, here are three tips for writing a CV:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Giving context is important, your work doesn't exist in a vacuum&lt;/li&gt;
&lt;li&gt;The impact that your work has is more important than the work itself&lt;/li&gt;
&lt;li&gt;Exaggeration only serves to distract, humility will go a long way&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading, for more like this follow me here on &lt;a href="https://dev.to/luke"&gt;dev.to&lt;/a&gt; or &lt;a href="https://twitter.com/lukebarnard_"&gt;on twitter&lt;/a&gt;! 🚀&lt;/p&gt;

&lt;p&gt;*NB: I re-wrote this post because I felt it was too negative and that's really not how I want to share my ideas. Any criticism is hugely valuable for me, so please leave any constructive feedback in the comments.&lt;/p&gt;

</description>
      <category>career</category>
    </item>
    <item>
      <title>Micro Dark Theme</title>
      <dc:creator>Luke Barnard</dc:creator>
      <pubDate>Sat, 22 Jun 2019 15:28:07 +0000</pubDate>
      <link>https://dev.to/luke/micro-dark-theme-5el6</link>
      <guid>https://dev.to/luke/micro-dark-theme-5el6</guid>
      <description>&lt;p&gt;I'm rewriting my personal website at the moment to make it easier to update and add things to. In the process I'm trying to keep things as simple as possible.&lt;/p&gt;

&lt;p&gt;When I started designing it, I decided to keep the colour scheme completely black and white, with a few splashes of colour. What I realised was that some might prefer a darker colour scheme as opposed to the brilliant white pages I was developing.&lt;/p&gt;

&lt;p&gt;But in my mission for simplicity I was determined to use the simplest implementation to achieve my dark theme.&lt;/p&gt;

&lt;p&gt;It just so happens that the W3C is drafting up a new standard for a CSS media query that indicates whether the user has requested a light or dark colour theme for the current website. A few browsers support this today including Chrome, Firefox and Safari. See &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme" rel="noopener noreferrer"&gt;MDN&lt;/a&gt; for the details.&lt;/p&gt;

&lt;p&gt;By offloading the responsibility of theme preference to the browser, a dark theme implementation can be as simple as a few lines of CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;      &lt;span class="nf"&gt;#root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;     

      &lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-color-scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;#root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;img&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.no-invert&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;invert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&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;The way this works is fairly straight-forward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the OS or browser "dark mode" is activated, the CSS in the media tag is applied. &lt;/li&gt;
&lt;li&gt;The &lt;code&gt;#root&lt;/code&gt; element is inverted from a white background with black text to black with white text (the background colour is required for this to work).&lt;/li&gt;
&lt;li&gt;Elements &lt;em&gt;within&lt;/em&gt; the root element are inverted &lt;em&gt;again&lt;/em&gt; to appear as they normally do.&lt;/li&gt;
&lt;/ul&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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F58wzavoz8hkpxyuv7n74.gif" 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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F58wzavoz8hkpxyuv7n74.gif"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As expected, elements inverted twice are displayed correctly - see the pills in the top-right:&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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F8o2p9utue50m5uwvy54f.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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F8o2p9utue50m5uwvy54f.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And for some interesting modifications, other filters could be used in conjunction with &lt;code&gt;invert()&lt;/code&gt; such as &lt;code&gt;contrast()&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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fai9ph9tmltwbkrm3gjt6.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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fai9ph9tmltwbkrm3gjt6.png"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fal6imzkkznuhb88l7djp.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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fal6imzkkznuhb88l7djp.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Oh and if you hadn't already guessed, there's also a draft for &lt;code&gt;prefers-contrast&lt;/code&gt; in the works - &lt;a href="https://drafts.csswg.org/mediaqueries-5" rel="noopener noreferrer"&gt;find the draft here&lt;/a&gt; - so that the user can avoid a low contrast theme altogether with a bit of CSS!&lt;/p&gt;

&lt;h2&gt;
  
  
  Fin
&lt;/h2&gt;

&lt;p&gt;Thanks for reading! Follow me for more posts like this 🚀&lt;/p&gt;

</description>
      <category>css</category>
      <category>theming</category>
      <category>responsive</category>
    </item>
    <item>
      <title>Emulating Xmonad in JavaScript - Part 4</title>
      <dc:creator>Luke Barnard</dc:creator>
      <pubDate>Mon, 27 May 2019 01:40:49 +0000</pubDate>
      <link>https://dev.to/luke/emulating-xmonad-in-javascript-part-4-45b6</link>
      <guid>https://dev.to/luke/emulating-xmonad-in-javascript-part-4-45b6</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aaH7ukT---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/r5ezkfecpm8of4lpjbsr.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aaH7ukT---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/r5ezkfecpm8of4lpjbsr.gif" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A lot has changed since &lt;a href="https://dev.to/luke/emulating-xmonad-in-javascript---part-3-219i"&gt;I last posted&lt;/a&gt;. Namely I have finally given this project a &lt;a href="https://github.com/lukebarnard1/nomad-term"&gt;GitHub repo&lt;/a&gt; for anyone who wants to follow my progress or even give it a test drive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Here's a quick list of updates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The project is no longer called "shmonad", but rather &lt;em&gt;nomad&lt;/em&gt;. This is for two reasons: anyone using this is almost certainly a development environment nomad and secondly, it's an anagram of xmonad when you remove the "x". See what I did there?&lt;/li&gt;
&lt;li&gt;As I mentioned, nomad now has a &lt;a href="https://github.com/lukebarnard1/nomad-term"&gt;GitHub page&lt;/a&gt;, although it is not officially released - use at your own risk.&lt;/li&gt;
&lt;li&gt;Nomad now supports background and foreground colours!&lt;/li&gt;
&lt;li&gt;The implementation for the sub terminal buffer and scrolling margins is now much more robust.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  The longer version
&lt;/h1&gt;

&lt;p&gt;This update has been made possible by my new favourite resource for implementing a terminal, &lt;a href="https://dev.topublicised%20by%20Microsoft"&gt;https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences&lt;/a&gt;. I have been studying this page in order to better my understanding of terminals but also to realise an improved underlying model of nomad. &lt;/p&gt;

&lt;p&gt;The two biggest additions are support for formatting sequences and for scrolling margins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Formatting
&lt;/h2&gt;

&lt;p&gt;nomad now supports foreground and background colours. The biggest challenge here was understanding how to model ANSI terminal format state and how to apply it when text is inserted. Essentially the problem can be divided according to this diagram:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        // Diagrams to show the insertion of format and
        // it's effect on existing formats, f and the resulting
        // formats, r.
        //
        // format         |---------|
        // f0:              |--|
        // r0:            |---------|
        //
        // f1: |------------------------------|
        // r1: |----------|---------|---------|
        //
        // f2:                   |--------------|
        // r2:            |---------|-----------|
        //
        // f3: |--------------|
        // r3: |----------|---------|
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The above diagram shows how nomad splits the underlying format state when a new string of text is inserted. Each line contains a string of text equal in length to the width of the viewport. Each line has a sequence of format states which contain a start index, a length and a definition for how to colour the text it contains. &lt;/p&gt;

&lt;p&gt;The method I chose to implement the function responsible for updating the format sequence of a line was TDD. The tests created here have become the first tests for nomad and I hope to add more to cover the vast majority of terminal features. More on this some other time! Suffice to say, a big refactor would really help to make components of nomad more testable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scrolling Margins
&lt;/h2&gt;

&lt;p&gt;Some programs (like vim) require correct implementation of &lt;em&gt;scroll margins&lt;/em&gt; within which lines of the buffer are shifted when inserting lines, deleting lines and of course scrolling up and down. Previously, nomad worked on the basis of a "scroll offset" - a single scalar value that indicated the viewport offset within the buffer. This is not generally how terminals represent state and was mostly my own estimation.&lt;/p&gt;

&lt;p&gt;ANSI terminals in fact use a region of the viewport that represents which lines to scroll under certain conditions and which lines to leave unaffected. This is useful for title bars and footers in GUI-based programs. &lt;/p&gt;

&lt;p&gt;As part of my debugging, I found it very useful to carefully look at the output sequences from vim. It allowed me to discern what vim was attempting to do during certain interactions and what nomad was doing wrong in response to that.&lt;/p&gt;

&lt;p&gt;Ideally this could have been another opportunity for a TDD-based approach, but in this case nomad is not written in a way that easily allows for this. My current thinking is that each part of terminal state can be considered separate and therefore easily testable.&lt;/p&gt;

&lt;h1&gt;
  
  
  Fin
&lt;/h1&gt;

&lt;p&gt;Please feel free to leave a comment and share your thoughts and if you're keen for more posts like this, follow me! :)&lt;/p&gt;

&lt;p&gt;See you next time folks, thanks for reading.&lt;/p&gt;

</description>
      <category>xmonad</category>
      <category>javascript</category>
      <category>developerexperience</category>
    </item>
    <item>
      <title>Emulating Xmonad in JavaScript - Part 3</title>
      <dc:creator>Luke Barnard</dc:creator>
      <pubDate>Sun, 24 Mar 2019 23:03:10 +0000</pubDate>
      <link>https://dev.to/luke/emulating-xmonad-in-javascript---part-3-219i</link>
      <guid>https://dev.to/luke/emulating-xmonad-in-javascript---part-3-219i</guid>
      <description>&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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fq7le3y6cla34c17v2cjk.gif" 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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fq7le3y6cla34c17v2cjk.gif" alt="a GIF showing bash and terminal focussing"&gt;&lt;/a&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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fsnad0iko97ci7okjecdx.gif" 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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fsnad0iko97ci7okjecdx.gif" alt="a GIF showing terminal swapping"&gt;&lt;/a&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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fnh5dbnkujvx0p86u6kzx.gif" 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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fnh5dbnkujvx0p86u6kzx.gif" alt="a GIF showing less working quite well in shmonad"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look! I made more progress!&lt;/p&gt;

&lt;p&gt;This week has mostly been bug-fixing and reverse-engineering the functionality of certain control sequences.&lt;/p&gt;

&lt;p&gt;One method I've found very useful is opening &lt;code&gt;Terminal.app&lt;/code&gt; and running&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;$ &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\e#8'&lt;/span&gt;   &lt;span class="c"&gt;# fill the terminal screen with "E" (no really)&lt;/span&gt;
 &lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\e[XXX'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to see what happens!&lt;/p&gt;

&lt;p&gt;I'm slowly starting to realise (a) how complicated terminals are, (b) how tricky ANSI control sequences are as an API and (c) that I'm trying to write an ASNI-compliant terminal in Node.js...&lt;/p&gt;

&lt;p&gt;This week's post is a short one, and I'll make sure to post another one ASAP.&lt;/p&gt;

&lt;h1&gt;
  
  
  Fin
&lt;/h1&gt;

&lt;p&gt;Please feel free to leave a comment and share your thoughts and if you're keen for more posts like this, follow me! :)&lt;/p&gt;

&lt;p&gt;See you next time folks, thanks for reading.&lt;/p&gt;

</description>
      <category>xmonad</category>
      <category>javascript</category>
      <category>developerexperience</category>
    </item>
    <item>
      <title>Emulating Xmonad in JavaScript - Part 2</title>
      <dc:creator>Luke Barnard</dc:creator>
      <pubDate>Mon, 18 Mar 2019 23:34:50 +0000</pubDate>
      <link>https://dev.to/luke/emulating-xmonad-in-javascript---part-2-585p</link>
      <guid>https://dev.to/luke/emulating-xmonad-in-javascript---part-2-585p</guid>
      <description>&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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fsbew1dbxgxyy7qg98h3o.gif" 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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fsbew1dbxgxyy7qg98h3o.gif" alt="tiny GIF showing my progress"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you squint at the above GIF, you can see that I've made quite a bit of progress since &lt;a href="https://dev.to/luke/emulating-xmonad-in-javascript--2cmf"&gt;my last post&lt;/a&gt; - a great introduction, pls read for context.&lt;/p&gt;

&lt;p&gt;Shmonad.js can now spawn multiple child processes and handle their output, buffering it into an appropriately-sized viewport. For now, formatting control sequences are stripped out - these would normally produce a vast array of pretty colours. There are also &lt;a href="http://ascii-table.com/ansi-escape-sequences-vt-100.php" rel="noopener noreferrer"&gt;many&lt;/a&gt;, &lt;a href="http://ascii-table.com/documents/vt100/chapter3.php" rel="noopener noreferrer"&gt;many&lt;/a&gt; control sequences that are not yet handled, and I'm still figuring out which ones to prioritise.&lt;/p&gt;

&lt;p&gt;My ultimate goal would be to handle enough control sequences to be able to run, say, &lt;code&gt;vim&lt;/code&gt;. At the moment, the best it can do is to handle &lt;code&gt;top&lt;/code&gt;, which is hugely satisfying to watch, but not very useful on it's own.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bash&lt;/code&gt; seems to run pretty well, with auto-complete and &lt;code&gt;reverse-i-search&lt;/code&gt; &lt;em&gt;kind of&lt;/em&gt; working, but they seem to rely on some control sequences that aren't yet supported.&lt;/p&gt;

&lt;p&gt;All in good time!&lt;/p&gt;

&lt;p&gt;So how have I made this progress? Well, I took a another deep dive after my last dig into terminals and control sequences. This time, I learned all about pseudo terminals.&lt;/p&gt;

&lt;p&gt;Let's take a look!&lt;/p&gt;

&lt;h2&gt;
  
  
  Pseudo Terminals
&lt;/h2&gt;

&lt;p&gt;Having achieved most of the functionality of Xmonad itself in my previous efforts, I switched focus to the other important part of this project - spawning processes.&lt;/p&gt;

&lt;p&gt;In itself, this is not a complicated task. There are well-defined APIs provided by the OS for this and Node.js exposes them in a fairly straight-forward way. &lt;/p&gt;

&lt;p&gt;However, programs and their creators have made things complicated.&lt;/p&gt;

&lt;p&gt;I'd like to start this journey by looking at the well-known &lt;code&gt;less&lt;/code&gt; command. Commonly used for manually scanning and jumping through a file or program output, the incredibly useful and powerful &lt;code&gt;less&lt;/code&gt; has been around since 1984 (according to &lt;code&gt;man less&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Piping hot output
&lt;/h3&gt;

&lt;p&gt;When I ran &lt;code&gt;less package.json&lt;/code&gt; in my project directory in an earlier version of shmonad, an unexpected thing happened. &lt;code&gt;less&lt;/code&gt; appeared to output the entire file from &lt;code&gt;stdout&lt;/code&gt;. The whole point of &lt;code&gt;less&lt;/code&gt; is that it only outputs a portion of the file to the terminal! So what gives?&lt;/p&gt;

&lt;p&gt;I began to look for clues that might indicate what was going on here. A useful tool for debugging the output of a program designed to run in a terminal is &lt;code&gt;cat -v&lt;/code&gt;. Piping program output into &lt;code&gt;cat -v&lt;/code&gt; will display all control sequences escaped so that they can be displayed in a terminal without being interpreted.&lt;/p&gt;

&lt;p&gt;For example, running &lt;code&gt;nvim | cat -v&lt;/code&gt; produces the control sequences and text that Neovim uses to render its UI. If you try this yourself, you'll notice the characteristic "[No Name]" somewhere in the mess of "["s. When &lt;code&gt;nvim&lt;/code&gt; is running in a terminal, the text "[No Name]" is inserted at a position determined by the current cursor position of the terminal. This in turn depends on the control sequences (and characters) that were previously sent to the terminal.&lt;/p&gt;

&lt;p&gt;So you might think - as did I - that running &lt;code&gt;less package.json | cat -v&lt;/code&gt; would produce a similar output. However, this is not the case. &lt;/p&gt;

&lt;p&gt;The more seasoned POSIX-based developer would be familiar with the fact that piping output from &lt;code&gt;less&lt;/code&gt; into another command basically does nothing (without specifying any options to &lt;code&gt;less&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The key difference between Neovim and &lt;code&gt;less&lt;/code&gt; in this case is their behaviour when running in a &lt;em&gt;terminal&lt;/em&gt; as opposed to streaming output to something that is &lt;em&gt;not&lt;/em&gt; attached to a &lt;em&gt;TTY&lt;/em&gt; (a legacy acronym that used to mean one of these things):&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/http%3A%2F%2Fwww.classiccmp.org%2Fdunfield%2Ftty%2Fh%2Fadm20.jpg" 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/http%3A%2F%2Fwww.classiccmp.org%2Fdunfield%2Ftty%2Fh%2Fadm20.jpg" alt="a vintage TTY"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;credit: &lt;a href="http://www.classiccmp.org/" rel="noopener noreferrer"&gt;http://www.classiccmp.org/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Anyway. Vintage terminals aside. &lt;/p&gt;

&lt;p&gt;When faced with this difference in behaviour, my first thought was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There &lt;em&gt;must&lt;/em&gt; be a &lt;em&gt;control sequence&lt;/em&gt; that tells &lt;code&gt;less&lt;/code&gt; that it's running in a terminal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And I was wrong, again.&lt;/p&gt;

&lt;p&gt;Going back to Node.js, I started wondering about the API for accessing &lt;code&gt;stdout&lt;/code&gt; and &lt;code&gt;stdin&lt;/code&gt;. Part of this API indicates whether &lt;code&gt;stdout&lt;/code&gt;/&lt;code&gt;stdin&lt;/code&gt; is attached to a terminal, called &lt;code&gt;.isTTY&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;This flag is always true for a process that is started within a terminal - useful for when it makes sense to behave differently in a terminal.&lt;/p&gt;

&lt;p&gt;Having realised that Node.js provides an interface for such a thing, I naturally assumed that there would likewise be an interface provided by Node.js that allows the program to spawn a process in a way that makes it &lt;em&gt;think&lt;/em&gt; it was spawned within a terminal. But these two concepts are very much disassociated: one is an API provided to the &lt;em&gt;current&lt;/em&gt; program, another requires creation of processes attached to &lt;code&gt;stdin&lt;/code&gt;/&lt;code&gt;stdout&lt;/code&gt; streams that are in turn (kind of) attached to a TTY. &lt;/p&gt;

&lt;p&gt;Initially, I confused this for "raw" mode of terminals, which is a whole other thing. If you're interested in the 2.5h I spent digging deep into the depths of all this, see &lt;a href="https://twitter.com/lukebarnard_/status/1105595476164993025" rel="noopener noreferrer"&gt;this twitter thread&lt;/a&gt;. As part of my deep dive, I came across a term "Pseudo Terminals" and decided to take a closer look at these strange creatures. &lt;a href="https://dev.toAccording%20to%20Wikipedia:"&gt;https://en.wikipedia.org/wiki/Pseudoterminal&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In some operating systems, including Unix, a pseudoterminal, pseudotty, or PTY is a pair of pseudo-devices, one of which, the slave, emulates a hardware text terminal device, the other of which, the master, provides the means by which a terminal emulator process controls the slave.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At this point, I was like "why didn't search for that before??"&lt;/p&gt;

&lt;p&gt;A quick search in &lt;a href="https://duckduckgo.com" rel="noopener noreferrer"&gt;my fave search engine&lt;/a&gt; provided exactly what I was looking for: &lt;code&gt;a set of Node.js bindings for "Getting certain programs to think you're a terminal, such as when you need a program to send you control sequences&lt;/code&gt; - &lt;a href="https://github.com/Microsoft/node-pty" rel="noopener noreferrer"&gt;&lt;code&gt;node-pty&lt;/code&gt;&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;node-pty&lt;/code&gt; is an Open Source library for spawning processes in the context of a &lt;em&gt;pseudo terminal&lt;/em&gt;. It also supports a lot of different operating systems, which is pretty cool! It also happens to power VSCode, Hyper and a bunch of other really cool terminal emulators.&lt;/p&gt;

&lt;p&gt;Having struck this gold mine (yay for Open Source &amp;lt;3), I proceeded to add &lt;code&gt;node-pty&lt;/code&gt; to shmonad and with very little modification; it worked like a charm!&lt;/p&gt;

&lt;p&gt;The next steps will be to continue to add support for more and more control sequences. The results should be quite amazing, especially when I get around to implementing formatting (which would warrant it's own blog post!)&lt;/p&gt;

&lt;p&gt;Prior to supporting all the control sequences in the world, shmonad needs a bit of a refactor. &lt;/p&gt;

&lt;p&gt;For now, I'll leave you with more TTYs.&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/http%3A%2F%2Fwww.classiccmp.org%2Fdunfield%2Ftty%2Fh%2Fvc303.jpg" 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/http%3A%2F%2Fwww.classiccmp.org%2Fdunfield%2Ftty%2Fh%2Fvc303.jpg" alt="vc303"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;vc303: credit &lt;a href="http://www.classiccmp.org" rel="noopener noreferrer"&gt;http://www.classiccmp.org&lt;/a&gt;&lt;/em&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/http%3A%2F%2Fwww.classiccmp.org%2Fdunfield%2Ftty%2Fh%2Fvc4404.jpg" 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/http%3A%2F%2Fwww.classiccmp.org%2Fdunfield%2Ftty%2Fh%2Fvc4404.jpg" alt="vc4404"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;vc404: credit &lt;a href="http://www.classiccmp.org" rel="noopener noreferrer"&gt;http://www.classiccmp.org&lt;/a&gt;&lt;/em&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/http%3A%2F%2Fwww.classiccmp.org%2Fdunfield%2Ftty%2Fh%2Fvt100.jpg" 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/http%3A%2F%2Fwww.classiccmp.org%2Fdunfield%2Ftty%2Fh%2Fvt100.jpg" alt="vt100"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;vt100*: credit &lt;a href="http://www.classiccmp.org" rel="noopener noreferrer"&gt;http://www.classiccmp.org&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;*: the TTY to popularise ANSI control sequences!!&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Fin
&lt;/h1&gt;

&lt;p&gt;Please feel free to leave a comment and share your thoughts and if you're keen for more posts like this, follow me! :)&lt;/p&gt;

&lt;p&gt;See you next time folks, thanks for reading.&lt;/p&gt;

</description>
      <category>xmonad</category>
      <category>javascript</category>
      <category>developerexperience</category>
    </item>
    <item>
      <title>Emulating Xmonad in JavaScript</title>
      <dc:creator>Luke Barnard</dc:creator>
      <pubDate>Sat, 02 Mar 2019 23:29:18 +0000</pubDate>
      <link>https://dev.to/luke/emulating-xmonad-in-javascript--2cmf</link>
      <guid>https://dev.to/luke/emulating-xmonad-in-javascript--2cmf</guid>
      <description>&lt;p&gt;One of my biggest obsessions is developing as fast as I can type on a keyboard.  It has gotten so bad at times that previous managers of mine have suggested adding another column to the Kanban board for dev env optimisation just for me!&lt;/p&gt;

&lt;p&gt;You might have seen &lt;a href="https://dev.to/luke/macos-window-management-without-xmonad-2agk"&gt;another post&lt;/a&gt; that I wrote about how I deal with the pain of organising my terminals in macOS. Having used xmonad at work for over two years, I couldn't be happier - it's simple, quick and I feel unrestricted despite only using a single monitor. Xmonad does support multiple monitors but I find switching between monitors quite slow, not least for the increased cursor action.&lt;/p&gt;

&lt;p&gt;Anyway, I want to explain my most recent project to reinvent my dev env at home. This is an attempt to use the &lt;em&gt;same&lt;/em&gt; environment whether I'm at home or work. I'm sure many are faced with a similar dilemma where a Mac is best at home and of course linux is best at work (aside from those working hard on programs that must be developed on macOS or Windows or other proprietary software environents).&lt;/p&gt;

&lt;p&gt;To that end, here is my progress so far. This GIF shows shmonad.js (renaming TBC) with the user giving some very basic interactions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--35AeYz_z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://lukebarnard.co.uk/img/shmonad-1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--35AeYz_z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://lukebarnard.co.uk/img/shmonad-1.gif" alt="" width="690" height="856"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the GIF, you can see a few boxes being drawn into the terminal. It's not much but eventually this will be a fully-fledged grid-organised terminal multiplexer that I will absolutely put on GitHub for those interested in running it.&lt;/p&gt;

&lt;p&gt;Longer term I imagine a plugin-based system that allows for more functionality but let's not get ahead of the game.&lt;/p&gt;

&lt;p&gt;How does this work?&lt;/p&gt;

&lt;h2&gt;
  
  
  Terminal Control Sequences
&lt;/h2&gt;

&lt;p&gt;A while back, terminals were physical devices that came complete with a keyboard, a screen and that's about it. Until floating windows and cursors came along, this was all there was in the way of computer interaction.&lt;/p&gt;

&lt;p&gt;In 1978, the &lt;a href="https://en.wikipedia.org/wiki/VT100"&gt;VT100 video terminal&lt;/a&gt; was introduced by Digital Equipment Corporation, one of the first terminals to support &lt;a href="https://en.wikipedia.org/wiki/ANSI_escape_code"&gt;ANSI escape codes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you haven't heard of escape codes or escape sequences in the context of terminals, fair enough - you'll almost never encounter them in your daily usage of a terminal as an end user. Essentially, these control sequences allow control of the terminal itself via the output of a program. These control sequences allow programs to control, for example, the colour of the text being sent to standard output or the current location of the cursor within a terminal.&lt;/p&gt;

&lt;p&gt;At the time, each terminal shipped with different control sequences and so programs using control sequences were either only compatible with certain terminals or they were forced to use libraries such as &lt;a href="https://en.wikipedia.org/wiki/Termcap"&gt;Termcap&lt;/a&gt; in order to maintain portability.&lt;/p&gt;

&lt;p&gt;The ANSI escape codes were proposed as a standard that all terminals could use, making portability of terminal interfaces much simpler. The resulting standard was adopted in 1976 as &lt;a href="https://www.ecma-international.org/publications/standards/Ecma-048.htm"&gt;ECMA-48&lt;/a&gt; (you may recognise ECMA from ECMAScript or ECMA-262, of which JavaScript is a well-know implementation).&lt;/p&gt;

&lt;p&gt;Anyway, history lesson over. What are these control sequences and how do they manifest?&lt;/p&gt;

&lt;p&gt;You may have encountered them more than you might expect.&lt;/p&gt;

&lt;p&gt;Take for example, the very popular shell program, Bash. Bash is a widely-used shell program that allows the user to run commands, build scripts and view program output in a fairly intuitive way. A common misconception is that Bash is a &lt;em&gt;terminal&lt;/em&gt;. This is actually false, Bash is a &lt;em&gt;shell&lt;/em&gt; program that is most commonly run in a terminal.&lt;/p&gt;

&lt;p&gt;For example, I'm writing this article in the following stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vim&lt;/li&gt;
&lt;li&gt;(Bash)&lt;/li&gt;
&lt;li&gt;Terminal.app&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;macOS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So in my case, Bash (a shell) happens to be running in Terminal.app (the ...  terminal).&lt;/p&gt;

&lt;p&gt;As I type these words, they appear in within Vim, which displays a text editor GUI in my terminal. It does this by sending control sequences to my terminal (via Bash). Bash isn't doing anything at the moment but if I hit &lt;code&gt;&amp;lt;CTRL-Z&amp;gt;&lt;/code&gt;, Bash actually interrupts Vim by pausing it and giving me direct access to Bash again.&lt;/p&gt;

&lt;p&gt;You might not think about it too much when you're running commands in Bash, but Bash (and other shell programs) also use control sequences to provide features including coloured text, tab completion, history selection, that annoying "ding" or "beep" you hear when you reach the end of history or if there's nothing to complete when you hit tab, and many more!&lt;/p&gt;

&lt;h1&gt;
  
  
  Fin
&lt;/h1&gt;

&lt;p&gt;I hope that gave you a better understanding of the foundation upon which my new project rests upon.&lt;/p&gt;

&lt;p&gt;Let's call it a day and I'll post another update soon about the inner workings of this terminal multiplexer I'm writing.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Leave a comment! See you next time!&lt;/p&gt;

</description>
      <category>xmonad</category>
      <category>javascript</category>
      <category>terminals</category>
    </item>
    <item>
      <title>Using a text editor for task tracking</title>
      <dc:creator>Luke Barnard</dc:creator>
      <pubDate>Thu, 11 Oct 2018 21:44:59 +0000</pubDate>
      <link>https://dev.to/luke/using-a-text-editor-for-task-tracking-ckf</link>
      <guid>https://dev.to/luke/using-a-text-editor-for-task-tracking-ckf</guid>
      <description>&lt;p&gt;I find tracking tasks super useful, whether it's work or life admin or shopping, keeping it tracked all in one place can help a lot. &lt;/p&gt;

&lt;p&gt;Having recently abandoned iPhone for an Android, I've lost my favourite todo-list app, &lt;a href="https://itunes.apple.com/us/app/clear-todos/id493136154"&gt;Clear&lt;/a&gt;. Until I've made my replacement (my first Android app!) I've been making do with my text editor to keep things simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simple Task Tracking
&lt;/h2&gt;

&lt;p&gt;First of all, make a &lt;code&gt;log&lt;/code&gt; directory to keep task files and a &lt;code&gt;notes&lt;/code&gt; directory for taking notes about projects. &lt;/p&gt;

&lt;p&gt;Keeping all dev-related stuff in the same place also helps when finding projects, notes, logs etc. quickly. Here's an example directory structure:&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;ls&lt;/span&gt; ~/dev
 log
 notes
 projects
 snippets

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;log&lt;/code&gt;: for task files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;notes&lt;/code&gt;: for notes about projects, in-depth notes on tasks, etc.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;projects&lt;/code&gt;: for code whether personal projects or remotely pulled git repos&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;snippets&lt;/code&gt;: for odd bits of code that don't belong in projects&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Creating a Task File
&lt;/h3&gt;

&lt;p&gt;Start of by using your editor to create a new file, name it &lt;code&gt;month-year.md&lt;/code&gt;, e.g. &lt;code&gt;oct-2018.md&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Sunday 7th October 2018&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Albeit slightly redundant, I find keeping tasks organised by the date they were completed (not created) very useful. Importantly, incomplete tasks are carried over to the next day giving a feeling of baggage - an incentive to get them done ASAP.&lt;/p&gt;

&lt;p&gt;After the date, writing a little intro to the day is like a personal pep-talk/complaint section to collect thoughts quickly and succinctly. Mention general goals but nothing structured. Keep it short!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Sunday 7th October 2018&lt;/span&gt;

Feeling positive about getting this stuff done this 
evening. Hoping to share my task-tracking habits with 
others.
&lt;span class="p"&gt;
 -&lt;/span&gt; [ ] Write a blog post about task tracking
&lt;span class="p"&gt; -&lt;/span&gt; [ ] Take a look at a personal project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Completing tasks
&lt;/h3&gt;

&lt;p&gt;I'm not certain that Markdown is the best format but so far I've found it quite intuitive and simple but expressive enough for my needs. It's also vaguely future-proof for when I want to write tools to make the process easier.&lt;/p&gt;

&lt;p&gt;What Markdown (or at least the GitHub flavour) importantly allows for is marking tasks as done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt; -&lt;/span&gt; [x] Start writing a blog post
&lt;span class="p"&gt; -&lt;/span&gt; [ ] Publish blog post
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Time-based Logging
&lt;/h3&gt;

&lt;p&gt;Recently I've also started adding timestamps to my log so that my thoughts are a little more collected. I think the utility here is in writing, not in reading my thoughts back, but I do occasionally find it useful to see a more detailed view of what I was doing yesterday or last week (especially for pain/happy points in a retrospective sprint review).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;
&lt;span class="gh"&gt;# Thursday 11th October 2018&lt;/span&gt;
&lt;span class="p"&gt;
 +&lt;/span&gt; 22:18 getting pretty sleepy, might try to finish 
         this blog before nodding off
&lt;span class="p"&gt;
 -&lt;/span&gt; [ ] Finish this blog post
&lt;span class="p"&gt; -&lt;/span&gt; [ ] Sleep

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Not Completing Tasks
&lt;/h3&gt;

&lt;p&gt;Sometimes the amount of time given to a task is underestimated and it becomes necessary to plan to do the task later. To do this, I move tasks from yesterday to today.&lt;/p&gt;

&lt;p&gt;The physical actions of performing this movement of text somehow builds up a sense of baggage and I find it dissuades me from planning to do too much in the day.&lt;/p&gt;

&lt;h3&gt;
  
  
  Planned Tasks
&lt;/h3&gt;

&lt;p&gt;Occasionally, tasks cannot be done today but only in the future. For these tasks, simply stack a list of date headers at the bottom of the task file, ready for completion when that day comes around. If a task has a time associated with it (e.g. a meeting scheduled during the day), I've found it useful to include the notation &lt;code&gt;@ 10:30&lt;/code&gt; or similar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;
&lt;span class="gh"&gt;# Thursday 11th October 2018&lt;/span&gt;

&lt;span class="gu"&gt;## Log&lt;/span&gt;
&lt;span class="p"&gt; +&lt;/span&gt; 10:00 forgot to get milk yesterday!! Looking forward to
         banana bread on Saturday though

Kitchen
&lt;span class="p"&gt; -&lt;/span&gt; [ ] Buy milk

&lt;span class="gh"&gt;# Saturday 13th October 2018&lt;/span&gt;
&lt;span class="p"&gt;
 -&lt;/span&gt; [ ] Meet with friends @ 14:00
&lt;span class="p"&gt; -&lt;/span&gt; [ ] Get ingredients for banana bread

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Task Categories
&lt;/h3&gt;

&lt;p&gt;One thing I've found useful is putting headers above groups of related tasks. This is nice for separating tasks at home from those at work, "admin" tasks from leisure tasks, for example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Me
&lt;span class="p"&gt; -&lt;/span&gt; [ ] Netflix
&lt;span class="p"&gt; -&lt;/span&gt; [ ] Chocolate
Admin
&lt;span class="p"&gt; -&lt;/span&gt; [ ] Setup password manager
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The "Me" category is a personal favourite. Remembering to give yourself "Me" time should be part of your daily routine.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Prior Work
&lt;/h2&gt;

&lt;p&gt;I'll leave you with some prior work that I want to take a look at. &lt;a href="https://orgmode.org/worg/org-tutorials/tracking-habits.html"&gt;Org mode&lt;/a&gt; looks to be task tracking for hax0rs running emacs. &lt;/p&gt;

&lt;p&gt;Plain-text task tracking has been done before and I encourage the reader to consider their own system.&lt;/p&gt;

&lt;p&gt;Thanks for reading, follow me for more! 🚀&lt;/p&gt;

</description>
      <category>tasks</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
