<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>WANcatServer</title>
    <link>https://wancat.cc/en/</link>
    <description>Recent content on WANcatServer</description>
    <image>
      <title>WANcatServer</title>
      <url>https://wancat.cc/%3Clink%20or%20path%20of%20image%20for%20opengraph,%20twitter-cards%3E</url>
      <link>https://wancat.cc/%3Clink%20or%20path%20of%20image%20for%20opengraph,%20twitter-cards%3E</link>
    </image>
    <generator>Hugo -- 0.152.2</generator>
    <language>en</language>
    <lastBuildDate>Sun, 19 Apr 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://wancat.cc/en/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Linux Odyssey</title>
      <link>https://wancat.cc/en/projects/linuxodyssey/</link>
      <pubDate>Sat, 01 Jul 2023 00:00:00 +0000</pubDate>
      <guid>https://wancat.cc/en/projects/linuxodyssey/</guid>
      <description>Interactive online Linux command teaching website with gamification experience</description>
      <content:encoded><![CDATA[<p><img alt="Linux Odyssey screenshot" loading="lazy" src="/projects/linuxodyssey/linuxodyssey.png"></p>
<p>My graduation project. An interactive terminal learning platform that provides guided courses, visual file tree functionality, and error message guidance.
Each course creates a container on the server side for user interaction.</p>
<p>Stack: TypeScript, Vue, Express, WebSocket, Docker in Docker<br>
License: GPL</p>
<p><a href="https://github.com/linux-odyssey/linux-odyssey">GitHub</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Synchan</title>
      <link>https://wancat.cc/en/projects/synchan/</link>
      <pubDate>Thu, 01 Aug 2024 00:00:00 +0000</pubDate>
      <guid>https://wancat.cc/en/projects/synchan/</guid>
      <description>Cross-device multichannel video sync engine with automatic latency measurement and playhead alignment</description>
      <content:encoded><![CDATA[<h2 id="synchan">Synchan</h2>
<p>2024 - 2025</p>
<p>Multichannel video synchronization tool that works across devices and platforms, with mobile support.</p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/gh-UvZkEhOs?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p><a href="https://peiyao.run/2024-the-dual-double-channel/">Video: Lin Pei-Yao. Triangular Relationship / Three-Channel Loop Video / 03'39&quot; / 2021</a></p>
<h2 id="background">Background</h2>
<p>Multichannel video playing is a common needs in modern art exhibitions.
The usual way to implement it is using dual monitors connected to the same computer,
and use proprietary video player to control it.</p>
<p>However, this method requires different monitors to be physically connected,
and doesn&rsquo;t allow using mobile devices as display.</p>
<p>Therefore, I developed a Web-based system to allow multidevice multichannel video synchronisation.</p>
<h2 id="structure">Structure</h2>
<p>The system consists of two parts: Server and Client</p>
<ul>
<li><strong>Server</strong>
<ul>
<li>NodeJS, TypeScript, Electron, tRPC, Express, Socket.io</li>
<li>Store the video files</li>
<li>Manage the current playhead</li>
<li>Calculate the latency to each connected client</li>
<li>Periodically update current playhead to every client via WebSocket</li>
<li>Provide a tRPC and RESTful interface for control</li>
<li>Wrapped inside an Electron desktop application to simplify starting.</li>
</ul>
</li>
<li><strong>Clients</strong>
<ul>
<li>TypeScript, ReactJS, Vite, tRPC, Socket.io, Redux</li>
<li>Connect to the server instance, load available videos</li>
<li>Dynamically syncing current playhead to the remote</li>
<li>Provide Admin interface when open inside Electron</li>
</ul>
</li>
</ul>
<h2 id="easy-to-use-interface">Easy-to-Use Interface</h2>
<p><img alt="Admin Interface of Synchan" loading="lazy" src="/projects/synchan/synchan-admin.webp">
<em>Admin Interface of Synchan</em></p>
<p>In an art exhibition environment, the exhibition managers usually are not from tech background.
It is required to provide an easy-to-use interface to start the whole system.
Also, the installations usually don&rsquo;t have mouse and keyboard connected once the set-up is completed.
Thus, the unattended start-up is important to minimise the effort for the exhibition managers.</p>
<p>There are many limitations for a Web-based application to achieve this.
Modern browsers require user interaction to play the sound, auto full-screen is also blocked.</p>
<p>Therefore, I wrapped the backend and the admin interface as an Electron application.
The server is spawned once the application starts, requiring no terminal or background service configuration.
Furthermore, Electron can be configured to allow autoplay and open-to-fullscreen.
The application also memorises last played video, and automatically play it on the start.</p>
<p>As a result, exhibition managers can just turn on the computer, and the whole installation is up and running.</p>
<h2 id="real-time-sync-engine">Real-Time Sync Engine</h2>
<p>Latency between devices is the biggest problem for cross-device synchronisation, especially when both have audio tracks, the difference becomes trivial.</p>
<p>The latency mainly comes from the network latency between server and clients, while different clients have different latency.
In the time code synchronisation protocol, I implemented a <strong>round-trip latency measurement</strong>.
Every time the server sends a new time code, it starts a stopwatch, and wait for client&rsquo;s ping back.
Then server uses a <strong>moving-window algorithm</strong> to calculate the median of recent transmissions, so the spikes in networking can be eliminated.
Finally server includes the calculated latency in the next time code packet sent to the client.</p>
<p>Once the client receives the time code with latency, it can calculate the real time code.
But the next problem arises:</p>
<p>The video seek operation in the browser takes long time (&gt;0.5s) on low-end devices like Raspberry Pi.
So it is not possible to do precise control using seek, and it also breaks the continuity of the video playback.</p>
<p>Therefore, I use micro speed adjustment to align the target playhead,
by a linear speed control between 1.05x to 0.95x.
The number chosen is a balance between adjustment balance and user experience.
It is unnoticeable by the users, and it doesn&rsquo;t breaks the video playing or lag the browser.</p>
<p>After the fix, latency can be minimised to under 5ms, which is indistinguishable by human ear.</p>
<h2 id="performance-optimisation-via-preloading">Performance Optimisation via Preloading</h2>
<p>The networking may not be stable in an exhibition environment.
While playing on low-end devices like Raspberry Pi,
mid-playback buffering often causes little lags.
Besides, exhibition installations usually play the same videos,
and the set-up time is before the exhibition starts, which can be ignored.</p>
<p>Therefore, instead of streaming, it has many benefits to preload the whole video before playing.</p>
<p>I implemented a cache layer in the video player, which downloads the whole file and saves into IndexedDB.
This method works in every modern browser out-of-box.</p>
<p>By using preloading, it solves the mid-playback buffering issue and significantly improves the stability.</p>
<h2 id="cross-platform-clients">Cross-Platform Clients</h2>
<p>Synchan itself ships a Web-based client, which can run on every platform with a modern browser,
including both desktop and mobile devices.
Its server-client structure also allows it to be used by different clients.</p>
<p>On low-end devices like Raspberry Pi, running a Chromium instance to play video may be too heavy.
Thus, I developed a headless client <strong>VLChan</strong> using VLC Python SDK.
It connects to a Synchan Server and plays local files with synchronised playhead, allows best performance on a Raspberry Pi.</p>
<h2 id="show-cases">Show Cases</h2>
<h3 id="lin-pei-yao-solo-exhibition-the-dual-double-channel-2024"><a href="https://yao-bite.github.io/exhibitions/2024-the-dual-double-channel/#gaze-triangle">Lin Pei-Yao Solo Exhibition: The Dual Double-Channel (2024)</a></h3>
<p>Used in work <strong>Gaze Triangle</strong></p>
<ul>
<li>3 video channels + 2 audio channels</li>
<li>Server: Raspberry Pi 4</li>
<li>Clients
<ul>
<li>Raspberry Pi 4 (same machine, connected monitor)</li>
<li>Mac Mini (projector + Bluetooth headset)</li>
<li>Android Phone (play audio by built-in speaker)</li>
</ul>
</li>
</ul>
<p><img loading="lazy" src="/projects/synchan/synchan.jpg">
<img loading="lazy" src="/projects/synchan/synchan-3.webp">
<img loading="lazy" src="/projects/synchan/synchan-4.webp"></p>
<h3 id="lin-pei-yao-solo-exhibition-who-is-the-speaker-2025"><a href="https://yao-bite.github.io/exhibitions/2025-who-is-the-speaker/#inter-view-with-a-philosopher">Lin Pei-Yao Solo Exhibition: Who is the speaker? (2025)</a></h3>
<p>Used in work <strong>Inter-view with a Philosopher</strong></p>
<ul>
<li>1 video + 2 audio channels</li>
<li>Server: Mac Mini</li>
<li>Clients
<ul>
<li>Mac Mini (same machine): Video + audio</li>
<li>Raspberry Pi 4 (audio only VLC client)</li>
</ul>
</li>
<li>Extra time-code control with <a href="/en/projects/actionwire">Actionwire</a></li>
</ul>
<p><img loading="lazy" src="/projects/synchan/who-is-the-speaker.webp"></p>
<h2 id="want-to-try">Want to Try?</h2>
<p>Currently available by invitation only. For inquiries, please contact <a href="mailto:wancat@wancat.cc">wancat@wancat.cc</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>What I&#39;m up to these days</title>
      <link>https://wancat.cc/en/now/</link>
      <pubDate>Sun, 19 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://wancat.cc/en/now/</guid>
      <description>&lt;ul&gt;
&lt;li&gt;🇦🇺 Studying Bachelor of Computer Science in Monash University.&lt;/li&gt;
&lt;li&gt;🐧 Became to the new Team Lead of &lt;a href=&#34;https://biomonash.github.io&#34;&gt;Biological Observation Monash&lt;/a&gt; since March&lt;/li&gt;
&lt;li&gt;🥊 Practicing Muay Thai and Jiu-Jitsu in Monash Kickboxing Club and Jiu-Jitsu Club&lt;/li&gt;
&lt;li&gt;👩‍💻 Rewriting &lt;a href=&#34;../en/projects/synchan&#34;&gt;Synchan&lt;/a&gt; server part using Go with state machine pattern&lt;/li&gt;
&lt;li&gt;🦞 Trying how to use OpenClaw to do something meaningful&lt;/li&gt;
&lt;/ul&gt;</description>
      <content:encoded><![CDATA[<ul>
<li>🇦🇺 Studying Bachelor of Computer Science in Monash University.</li>
<li>🐧 Became to the new Team Lead of <a href="https://biomonash.github.io">Biological Observation Monash</a> since March</li>
<li>🥊 Practicing Muay Thai and Jiu-Jitsu in Monash Kickboxing Club and Jiu-Jitsu Club</li>
<li>👩‍💻 Rewriting <a href="/en/projects/synchan">Synchan</a> server part using Go with state machine pattern</li>
<li>🦞 Trying how to use OpenClaw to do something meaningful</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Actionwire</title>
      <link>https://wancat.cc/en/projects/actionwire/</link>
      <pubDate>Thu, 09 Apr 2026 17:11:38 +1000</pubDate>
      <guid>https://wancat.cc/en/projects/actionwire/</guid>
      <description>Reactive automation system linking offline speech recognition, smart lighting, and video control for live installations.</description>
      <content:encoded><![CDATA[<h2 id="background">Background</h2>
<p>This project is developed specific for <a href="https://yao-bite.github.io/exhibitions/2025-who-is-the-speaker/#inter-view-with-a-philosopher">Lin Pei-Yao Solo Exhibition: Who is the speaker? (2025)</a>.</p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/mKmAC1MVB6E?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>In this exhibition, it required speech recognition for selected keywords, and perform specific actions, including smart light control and video playhead control.
The speech recognition is done in real-time, deployed locally on a Raspberry Pi.</p>
<p>For example, in the command <em>&ldquo;Drink Tea&rdquo;</em>, it blinks one set of lights and seeks the video to a specific time (00:25) and jumps back to original position after 10 seconds.</p>
<p>Different voice commands have different actions, and some of them may depends on each other.</p>
<p>To make the concurrent events manageable, I used <a href="https://en.wikipedia.org/wiki/Reactive_programming">Reactive Programming</a> design pattern via <a href="https://rxpy.readthedocs.io/en/latest/">RxPy</a>.</p>
<h2 id="structure">Structure</h2>
<p>The program is divided into three parts: Events, Commands, and Actions.</p>
<p>Events are the input to the system, including microphone and WebSocket inputs.
It will be transform into an Observable stream.</p>
<p>Actions are the output behaviours.
Including light control and video playhead control.</p>
<p>Commands are the business logic. Freely connecting, composing, mixing all the inputs, and producing one output.
Can be easily customised by user needs.</p>
<ul>
<li>Events (inputs)
<ul>
<li>Microphone -&gt; Vosk -&gt; Keyword extraction</li>
<li>WebSocket -&gt; Current timecode</li>
</ul>
</li>
<li>Commands
<ul>
<li>Define the pipeline logic for every command</li>
<li>Written in Reactive Programming styles</li>
<li>No hidden state management. Easy to update</li>
</ul>
</li>
<li>Actions (outputs)
<ul>
<li>Light control -&gt; LIFX LAN API</li>
<li>Video playhead control -&gt; HTTP request</li>
</ul>
</li>
</ul>
<h2 id="keywords-recognition">Keywords Recognition</h2>
<p>I used <a href="https://alphacephei.com/vosk/">Vosk</a> as the offline speech recognition model, because it is small enough to run on a Raspberry Pi.</p>
<p>The original accuracy of the model is not good, and it is designed as a speech-to-text model, not for recognise specific keywords.
I customised the vocabulary list to make it only select tokens that appears the keyword list.
It&rsquo;s also important to include <code>[unk]</code> in the list, to prevent the model output unknown words.</p>
<h2 id="synchan-integration">Synchan Integration</h2>
<p>The video playing system is <a href="/en/projects/synchan">Synchan</a>, a multichannel multidevice synchronised video playing system.
It allows control via HTTP requests, and it updates the current time code to every clients via WebSocket.
The time code is parsed as an Observable stream, and used to perform action according to video time code.</p>
<p>For example, in the beginning of the video, it turns on the light in the exhibition as the light is turned on in the video.
And in the command &ldquo;Drink Tea&rdquo;, it seeks the video to back to 00:25, where the performer asked &ldquo;Would you like some tea?&rdquo;, and seeks back to the original playhead after 10 seconds.</p>
<h2 id="tech-stack">Tech Stack</h2>
<ul>
<li>Python</li>
<li><a href="https://rxpy.readthedocs.io/en/latest/">RxPy</a></li>
<li><a href="https://alphacephei.com/vosk/">Vosk</a></li>
<li><a href="https://python-socketio.readthedocs.io/en/latest/index.html">Socket.IO</a></li>
<li><a href="https://github.com/mclarkk/lifxlan">lifxlan</a>: Smart Light Control in LAN</li>
</ul>
<h2 id="gallery">Gallery</h2>
<p><img loading="lazy" src="/projects/actionwire/2025-ZoneArt-1.webp"></p>
<p><img loading="lazy" src="/projects/actionwire/2025-ZoneArt-11.webp"></p>
<p><img loading="lazy" src="/projects/actionwire/2025-ZoneArt-14-1994.webp"></p>
<h2 id="want-to-try">Want to Try?</h2>
<p>Currently available by invitation only. For inquiries, please contact <a href="mailto:wancat@wancat.cc">wancat@wancat.cc</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Local-First - Collaboration &#43; Ownership, The Next Generation Software Development Paradigm</title>
      <link>https://wancat.cc/en/post/local-first/</link>
      <pubDate>Mon, 03 Nov 2025 13:52:10 +1000</pubDate>
      <guid>https://wancat.cc/en/post/local-first/</guid>
      <description>&lt;figure&gt;
    &lt;img loading=&#34;lazy&#34; src=&#34;../post/local-first/cover.jpg&#34;
         alt=&#34;Cover image&#34;/&gt; &lt;figcaption&gt;
            &lt;p&gt;Streets of Melbourne. Photo by author&lt;/p&gt;
        &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;I came across an article published in 2019 by independent research lab &lt;a href=&#34;https://www.inkandswitch.com/&#34;&gt;Ink &amp;amp; Switch&lt;/a&gt; titled &amp;ldquo;&lt;a href=&#34;https://www.inkandswitch.com/essay/local-first/&#34;&gt;Local-first software: You own your data, in spite of the cloud&lt;/a&gt;&amp;rdquo;. After reading it, I was deeply moved and inspired. Here I&amp;rsquo;ll share a brief summary and my thoughts.&lt;/p&gt;
&lt;p&gt;Looking at the modern software ecosystem, you&amp;rsquo;ll notice it&amp;rsquo;s completely different from ten years ago. Instead of applications installed on your computer, more and more software is delivered as web services. Most of our computer time is now spent inside a browser.&lt;/p&gt;
&lt;p&gt;Over the past decade or so, the software industry has undergone a paradigm shift—from traditional desktop software to web services. This brought better cross-platform support, instant access without installation, and seamless cross-device experiences. Most importantly, it made &lt;strong&gt;real-time multi-user collaboration&lt;/strong&gt; possible.&lt;/p&gt;
&lt;p&gt;Iconic examples include how Microsoft Word, PowerPoint, and Adobe Illustrator are gradually being replaced by Google Docs, Canvas, and Figma. These services support real-time collaboration, saving the mental overhead of emailing files back and forth. More importantly, they allow multiple people to edit simultaneously without conflicts.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<figure>
    <img loading="lazy" src="/post/local-first/cover.jpg"
         alt="Cover image"/> <figcaption>
            <p>Streets of Melbourne. Photo by author</p>
        </figcaption>
</figure>

<p>I came across an article published in 2019 by independent research lab <a href="https://www.inkandswitch.com/">Ink &amp; Switch</a> titled &ldquo;<a href="https://www.inkandswitch.com/essay/local-first/">Local-first software: You own your data, in spite of the cloud</a>&rdquo;. After reading it, I was deeply moved and inspired. Here I&rsquo;ll share a brief summary and my thoughts.</p>
<p>Looking at the modern software ecosystem, you&rsquo;ll notice it&rsquo;s completely different from ten years ago. Instead of applications installed on your computer, more and more software is delivered as web services. Most of our computer time is now spent inside a browser.</p>
<p>Over the past decade or so, the software industry has undergone a paradigm shift—from traditional desktop software to web services. This brought better cross-platform support, instant access without installation, and seamless cross-device experiences. Most importantly, it made <strong>real-time multi-user collaboration</strong> possible.</p>
<p>Iconic examples include how Microsoft Word, PowerPoint, and Adobe Illustrator are gradually being replaced by Google Docs, Canvas, and Figma. These services support real-time collaboration, saving the mental overhead of emailing files back and forth. More importantly, they allow multiple people to edit simultaneously without conflicts.</p>
<p>However, the shift from desktop software to web services has also introduced the following problems:</p>
<ol>
<li>
<p><strong>Speed</strong>: Every operation needs to interact with a remote data center. Even checking a box requires waiting for network latency.</p>
</li>
<li>
<p><strong>Network dependency</strong>: Whether it&rsquo;s your data or the software that manipulates it, the only &ldquo;source of truth&rdquo; is stored in a remote data center. When you lose network connection or the server goes down, you can&rsquo;t continue working.</p>
</li>
<li>
<p><strong>Loss of ownership</strong>: You no longer <strong>own</strong> your files—you just have credentials that <strong>grant access</strong> to them, and the service provider can revoke that access at any time.</p>
</li>
</ol>
<p>Here&rsquo;s an insightful summary from the article:</p>
<blockquote>
<p>The cloud gives us collaboration, but old-fashioned apps give us ownership. Can&rsquo;t we have the best of both worlds?</p>
</blockquote>
<p>Software that achieves this is what the article proposes as <strong>local-first software</strong>.</p>
<h2 id="seven-ideals-of-local-first">Seven Ideals of Local-First</h2>
<p>The article defines local-first as: prioritizing local storage and local networks, giving users complete ownership while still enjoying the user experience of cloud services.</p>
<p>It proposes seven specific ideals:</p>
<ol>
<li>
<p><strong>Speed</strong>: Your data is on your device, all operations complete instantly, syncing with other devices in the background.</p>
</li>
<li>
<p><strong>Multi-device</strong>: Your data should be accessible and editable across multiple devices.</p>
</li>
<li>
<p><strong>Offline functionality</strong>: When the network is unavailable, you should still have full access to single-device functionality.</p>
</li>
<li>
<p><strong>Real-time collaboration</strong>: Ability to work simultaneously with others while avoiding or resolving conflicts.</p>
</li>
<li>
<p><strong>Longevity</strong>: If the company goes out of business, can users still open their data? After 100 years, will this data still be readable?</p>
</li>
<li>
<p><strong>Security and privacy</strong>: User data should default to existing only on the user&rsquo;s own devices, avoiding the surveillance, hacking, and data misuse issues that come with centralized servers.</p>
</li>
<li>
<p><strong>Ownership</strong>: Do you fully own your data? If your account is suspended by the company or a court, can you still use it?</p>
</li>
</ol>
<p>Below is an excerpt from the article&rsquo;s evaluation of existing software, services, and technologies against these seven ideals:</p>
<p>✓ Good — Partial ✗ Bad</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Speed</th>
          <th>Multi-device</th>
          <th>Offline</th>
          <th>Collaboration</th>
          <th>Longevity</th>
          <th>Privacy</th>
          <th>Ownership</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Files + email attachments</td>
          <td>✓</td>
          <td>—</td>
          <td>✓</td>
          <td>✗</td>
          <td>✓</td>
          <td>—</td>
          <td>✓</td>
      </tr>
      <tr>
          <td>Files + cloud sync</td>
          <td>✓</td>
          <td>—</td>
          <td>—</td>
          <td>✗</td>
          <td>✓</td>
          <td>—</td>
          <td>✓</td>
      </tr>
      <tr>
          <td>Google Docs</td>
          <td>—</td>
          <td>✓</td>
          <td>—</td>
          <td>✓</td>
          <td>—</td>
          <td>✗</td>
          <td>—</td>
      </tr>
      <tr>
          <td>Web apps</td>
          <td>✗</td>
          <td>✓</td>
          <td>✗</td>
          <td>✓</td>
          <td>✗</td>
          <td>✗</td>
          <td>✗</td>
      </tr>
      <tr>
          <td>Most mobile apps</td>
          <td>✓</td>
          <td>—</td>
          <td>✓</td>
          <td>✗</td>
          <td>—</td>
          <td>✗</td>
          <td>✗</td>
      </tr>
      <tr>
          <td>Git+GitHub</td>
          <td>✓</td>
          <td>—</td>
          <td>✓</td>
          <td>—</td>
          <td>✓</td>
          <td>—</td>
          <td>✓</td>
      </tr>
  </tbody>
</table>
<ul>
<li>
<p><strong>Files + email / cloud sync</strong>: Actually works quite well. You have complete file ownership and smooth operation on your own computer. The only issue is that when collaborating with others, you need to send files back and forth and constantly rename them (<code>report_1.docs</code>, <code>report_2.docx</code>, <code>report_final_4.docx</code>).<br>
Cloud sync somewhat solves the file-sending hassle, but when the same file is modified in two places simultaneously, resolving conflicts becomes very difficult.</p>
</li>
<li>
<p><strong>Google Docs / typical web apps</strong>: Cross-device usage and multi-user collaboration are possible, but without internet, you can&rsquo;t do anything. You also need to worry about losing your work due to account suspension or other reasons.</p>
</li>
<li>
<p><strong>Most mobile apps</strong>: Although they&rsquo;re programs downloaded to your phone, most features rely on server functionality. Without internet, you can&rsquo;t even open videos you posted yourself.</p>
</li>
<li>
<p><strong>Git+GitHub</strong>: Git is the go-to tool for managing code versions and multi-user collaboration in the software industry. It perfectly meets the needs of software development. However, it&rsquo;s primarily designed for plain text files (code) and has a steep learning curve for everyday use by ordinary people.</p>
<p>But its design—especially how each endpoint has complete data and how it uses edit history for conflict comparison and recovery—is very worth learning from.</p>
</li>
</ul>
<p>What technology can meet all seven of these conditions? In the article, the lab team discovered a promising foundational technology: <a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">Conflict-free Replicated Data Types (CRDTs)</a>.</p>
<h2 id="understanding-crdts">Understanding CRDTs</h2>
<p>CRDTs were proposed in 2011 in a <a href="https://pages.lip6.fr/Marc.Shapiro/papers/RR-7687.pdf">computer science journal</a>. It&rsquo;s a data structure that enables multiple computers to collaborate while automatically resolving conflicts. CRDTs don&rsquo;t just store the final state of data—they also preserve the complete change history, and by replaying the history, you can arrive at the same final state.</p>
<p>Taking text editing as an example, suppose the original string is:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-plain" data-lang="plain"><span style="display:flex;"><span>1234567890
</span></span></code></pre></div><p>A inserts a 0 after 3:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-plain" data-lang="plain"><span style="display:flex;"><span>1230456789
</span></span></code></pre></div><p>B (from the original string) removes 8:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-plain" data-lang="plain"><span style="display:flex;"><span>12345679
</span></span></code></pre></div><p>When the computer tries to merge these two operations, it finds that none of the characters after 123 match up. Traditionally, we&rsquo;d use more advanced string comparison algorithms to resolve this, but if we know the modification history, we just need to apply each change to get the conflict-resolved result:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-plain" data-lang="plain"><span style="display:flex;"><span>123456789   -&gt; original string
</span></span><span style="display:flex;"><span>1230456789  -&gt; insert 0 after 3
</span></span><span style="display:flex;"><span>123045679   -&gt; remove 8
</span></span></code></pre></div><p>If there&rsquo;s a conflict that truly can&rsquo;t be automatically resolved, CRDTs can also provide more accurate and localized comparison information for the application or user to resolve.</p>
<p>This simple case might not show the value clearly, but anyone who has used cloud file sync knows how troublesome and tricky it is when two computers modify the same file. Comparison based solely on results is difficult, which is why simple file/state synchronization can&rsquo;t implement local-first.</p>
<p>Therefore, with CRDTs as the foundation, we can solve cross-device data conflict issues and develop systems that can both operate offline and collaborate with others.</p>
<h2 id="case-study-heptabase">Case Study: Heptabase</h2>
<figure>
    <img loading="lazy" src="/post/local-first/heptabase.webp"
         alt="Heptabase Screenshot"/> <figcaption>
            <p>Heptabase product screenshot, from official website</p>
        </figcaption>
</figure>

<p><a href="https://heptabase.com/">Heptabase</a> is a subscription-based note-taking and knowledge base software founded by Taiwanese, dedicated to helping <strong>anyone make sense of complex topics</strong>. I&rsquo;ve been using it for a year and a half and absolutely love it. It has profoundly changed how I learn and helped me build a more sustainable and deep knowledge system.</p>
<p>What I enjoy most about using Heptabase is its smooth user experience—each device can operate independently, cross-device syncing doesn&rsquo;t create conflicts, and this year they added real-time multi-user collaboration.</p>
<p>I used to use Notion but couldn&rsquo;t stand the long loading times every time I opened it. Later I returned to the embrace of plain text markdown, syncing markdown files with Nextcloud. However, the tree structure of computer files isn&rsquo;t suitable for building complex knowledge systems. It wasn&rsquo;t until I discovered Heptabase that I found a tool that truly fit my needs.</p>
<p>It was also while researching how to develop such systems, using Heptabase&rsquo;s architecture as a prototype, that I discovered the local-first concept. I&rsquo;m not sure if this was one of their original design philosophies, but in my experience using it, it satisfies many of these ideals.</p>
<p>Below I evaluate Heptabase using the seven local-first ideals:</p>
<p>✓ Good — Partial ✗ Bad</p>
<ol>
<li>
<p>✓ <strong>Speed</strong>: Complete data locally, opens immediately (under 2 seconds)</p>
</li>
<li>
<p>✓ <strong>Multi-device</strong>: Works across computers, phones, tablets, and web</p>
</li>
<li>
<p>✓ <strong>Offline functionality</strong>: Still able to fully access and modify all data offline, including writing cards and editing whiteboards</p>
</li>
<li>
<p>✓ <strong>Real-time collaboration</strong>: Supports simultaneous editing of the same whiteboard with others</p>
</li>
<li>
<p>— <strong>Longevity</strong>: Heptabase is proprietary subscription software—you can&rsquo;t use it without paying. However, data is backed up daily on your own device in markdown and JSON formats, so it can still be parsed with other software.</p>
</li>
<li>
<p>— <strong>Security and privacy</strong>: Heptabase maintains a copy of your data on servers to facilitate syncing, and currently doesn&rsquo;t offer end-to-end encryption.<del>Users can choose not to enable syncing, but then can&rsquo;t enjoy cross-device functionality.</del> This option was removed in a <a href="https://wiki.heptabase.com/changelog">recent update (v1.75.8)</a>.</p>
</li>
<li>
<p>— <strong>Ownership</strong>: Users have all data in markdown and JSON, but without the Heptabase software itself, it&rsquo;s not as easy to use—though technically it can be parsed yourself.</p>
</li>
</ol>
<p>Overall, Heptabase demonstrates how local-first architecture can be implemented in a product with excellent user experience and make it one of the product&rsquo;s core selling points.</p>
<h2 id="the-coming-of-the-next-era">The Coming of the Next Era</h2>
<p>The internet went from one-way transmission in web1 to user-generated content in web2, and now to the web3 movement that returns ownership to users. However, application development still predominantly uses server-centric web apps.</p>
<p>The local-first and CRDTs architecture can add multi-user collaboration and cross-device functionality while preserving user sovereignty. Whether it&rsquo;s family photo albums, notes, expense tracking, or fitness logs, everything can be collaborated on with others in a more private and secure way. Such applications are like <strong>app3</strong>—they will be the application development paradigm of the future generation.</p>
<h2 id="references">References</h2>
<ul>
<li>Martin Kleppmann, Adam Wiggins, Peter van Hardenberg, Mark McGranaghan. (April 2019). Local-first software. You own your data, in spite of the cloud. <em>Ink &amp; Switch</em>. <a href="https://www.inkandswitch.com/essay/local-first/">https://www.inkandswitch.com/essay/local-first/</a></li>
<li>Marc Shapiro, Nuno Preguiça, Carlos Baquero, Marek Zawirski. (2011). Conflict-free Replicated Data Types. <em>French Institute for Research in Computer Science and Automation</em>. <a href="https://pages.lip6.fr/Marc.Shapiro/papers/RR-7687.pdf">https://pages.lip6.fr/Marc.Shapiro/papers/RR-7687.pdf</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>mdsh: Run Shell Scripts in Markdown Templates</title>
      <link>https://wancat.cc/en/post/mdsh/</link>
      <pubDate>Fri, 25 Jul 2025 16:23:21 +1000</pubDate>
      <guid>https://wancat.cc/en/post/mdsh/</guid>
      <description>&lt;p&gt;I have been using &lt;a href=&#34;https://hledger.org/&#34;&gt;hledger&lt;/a&gt; as my primary personal accounting software for years.
I love that I can manage my ledger in plaintext and even use Git to version control and backup.&lt;/p&gt;
&lt;p&gt;But when it comes to generating reports, it often takes me time to figure out all the commands I need.
Also, having a way to archive previous data is important.
I used to write a shell script with all the report commands, and add a lot of &lt;code&gt;echo&lt;/code&gt; statements to generate a markdown report.
However, this approach is hard to read and makes the template difficult to maintain.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s why I made &lt;a href=&#34;https://github.com/lancatlin/mdsh&#34;&gt;&lt;strong&gt;mdsh&lt;/strong&gt;&lt;/a&gt;, a markdown template engine written in Go, which allows you to &lt;strong&gt;execute shell scripts within Markdown&lt;/strong&gt;.
It allows you to use Go&amp;rsquo;s template syntax in markdown, and puts the execution results in the generated output.&lt;/p&gt;
&lt;p&gt;You can download it from the &lt;a href=&#34;https://github.com/lancatlin/mdsh/releases&#34;&gt;release page&lt;/a&gt;.
Once you downloaded ant decompressed it, you can put the &lt;code&gt;mdsh&lt;/code&gt; binary to some place in your &lt;code&gt;$PATH&lt;/code&gt;. On Linux, for example, you can put it under &lt;code&gt;/usr/loca/bin&lt;/code&gt; via:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I have been using <a href="https://hledger.org/">hledger</a> as my primary personal accounting software for years.
I love that I can manage my ledger in plaintext and even use Git to version control and backup.</p>
<p>But when it comes to generating reports, it often takes me time to figure out all the commands I need.
Also, having a way to archive previous data is important.
I used to write a shell script with all the report commands, and add a lot of <code>echo</code> statements to generate a markdown report.
However, this approach is hard to read and makes the template difficult to maintain.</p>
<p>That&rsquo;s why I made <a href="https://github.com/lancatlin/mdsh"><strong>mdsh</strong></a>, a markdown template engine written in Go, which allows you to <strong>execute shell scripts within Markdown</strong>.
It allows you to use Go&rsquo;s template syntax in markdown, and puts the execution results in the generated output.</p>
<p>You can download it from the <a href="https://github.com/lancatlin/mdsh/releases">release page</a>.
Once you downloaded ant decompressed it, you can put the <code>mdsh</code> binary to some place in your <code>$PATH</code>. On Linux, for example, you can put it under <code>/usr/loca/bin</code> via:</p>
<pre><code>sudo cp mdsh /usr/local/bin
</code></pre>
<p>You can also install it with Go CLI:</p>
<pre><code>go install github.com/lancatlin/mdsh@latest
</code></pre>
<p>This will put it into <code>$GOPATH/bin</code> (usually <code>$HOME/go/bin</code>)</p>
<h2 id="the-first-template">The First Template</h2>
<p>Suppose we want to generate a system information report.
You can define a markdown template <code>system-info.md</code> as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span># 💥 System Information Report for {{ sh &#34;hostname&#34; }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> **Hostname**: {{ sh &#34;hostname&#34; }}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> **Username**: {{ sh &#34;whoami&#34; }}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> **Uptime**: {{ sh &#34;uptime -p&#34; }}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> **System**: {{ sh &#34;uname -a&#34; }}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> **CPU**: {{ sh &#34;uname -m&#34; }} — {{ sh &#34;nproc&#34; }} cores
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> **IP Address**: {{ sh &#34;hostname -I || ip a | grep inet&#34; }}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> **Default Gateway**: {{ sh &#34;ip route | grep default || netstat -rn | grep default&#34; }}
</span></span></code></pre></div><p>Run the template:</p>
<pre><code>mdsh system-info.md
</code></pre>
<p>It will render into:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span># 💥 System Information Report for `fedora`
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> **Hostname**: <span style="color:#e6db74">`fedora`</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> **Username**: <span style="color:#e6db74">`wancat`</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> **Uptime**: <span style="color:#e6db74">`up 1 hour, 13 minutes`</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> **System**: <span style="color:#e6db74">`Linux fedora 6.15.6-200.fc42.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Jul 10 15:22:32 UTC 2025 x86_64 GNU/Linux`</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> **CPU**: <span style="color:#e6db74">`x86_64`</span> — <span style="color:#e6db74">`16`</span> cores
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> **IP Address**: <span style="color:#e6db74">`172.26.198.115 2405:dc00:ec83:ec80:af9c:87ed:9bae:bd0d`</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> **Default Gateway**: <span style="color:#e6db74">`default via 172.26.198.50 dev wlp1s0 proto dhcp src 172.26.198.115 metric 600`</span>
</span></span></code></pre></div><p>It makes generating reports as easy as a breeze.</p>
<h2 id="different-template-functions">Different Template Functions</h2>
<p>Currently, it supports 3 types of template functions, which change the format they generate to:</p>
<ol>
<li><code>sh</code>: puts the output in <code>inline code</code></li>
<li><code>shell</code>: puts the output in a</li>
</ol>
<pre tabindex="0"><code>code block
</code></pre><ol start="3">
<li><code>raw</code>: puts output without any decorations</li>
</ol>
<p>Also, any functions and syntax supported in <a href="https://pkg.go.dev/text/template">text/template</a> are supported.</p>
<h2 id="custom-parameters-from-command-line-arguments">Custom Parameters from Command Line Arguments</h2>
<p>The best part of mdsh is that it supports <strong>custom parameters</strong>, which means you can define the parameters you&rsquo;re going to use in the template, and pass them through command line arguments.</p>
<p>You can access the parameters through both template data and environment variables.
So you can access these variables from the script with ease.</p>
<p>For example, I need to generate a monthly report for my ledger.
I need to specify <code>begin</code> and <code>end</code> times for the report.</p>
<p>Define the template as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span>params:
</span></span><span style="display:flex;"><span>  b:
</span></span><span style="display:flex;"><span>    required: true
</span></span><span style="display:flex;"><span>  e:
</span></span><span style="display:flex;"><span>    required: true
</span></span><span style="display:flex;"><span>  f:
</span></span><span style="display:flex;"><span>    default: examples/ledger.j
</span></span><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span>// The template body
</span></span></code></pre></div><p>As you can see in the code, you can define custom parameters in the <code>params:</code> section in the frontmatter.
I defined 3 parameters: <code>b</code>, <code>e</code>, and <code>f</code>, which stand for begin, end, and file. (This follows the convention in hledger)</p>
<p>Then I can use those parameters in the template body.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span># Finance Report from {{.b}} to {{.e}}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Net Income:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>{{ shell &#34;hledger -f $f income -b $b -e $e --monthly&#34; }}
</span></span></code></pre></div><p>I use <code>{{ .b }}</code> to access the parameter directly and put it in the heading.
Then I can use <code>$b</code> in the command.
All the parameters will be passed as environment variables to the executing shell.
So you can access them very easily.</p>
<pre><code>mdsh hledger_monthly.md -b 2011-01 -e 2011-02
</code></pre>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span># Finance Report from 2011-01 to 2011-02
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Net Income:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">```
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"></span>Income Statement 2011-01
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                         ||        Jan
</span></span><span style="display:flex;"><span>=========================++============
</span></span><span style="display:flex;"><span> Revenues                ||
</span></span><span style="display:flex;"><span>-------------------------++------------
</span></span><span style="display:flex;"><span> Income:Salary           ||  $2,000.00
</span></span><span style="display:flex;"><span>-------------------------++------------
</span></span><span style="display:flex;"><span>                         ||  $2,000.00
</span></span><span style="display:flex;"><span>=========================++============
</span></span><span style="display:flex;"><span> Expenses                ||
</span></span><span style="display:flex;"><span>-------------------------++------------
</span></span><span style="display:flex;"><span> Expenses:Auto           ||  $5,500.00
</span></span><span style="display:flex;"><span> Expenses:Books          ||     $20.00
</span></span><span style="display:flex;"><span> Expenses:Food:Groceries ||    $109.00
</span></span><span style="display:flex;"><span>-------------------------++------------
</span></span><span style="display:flex;"><span>                         ||  $5,629.00
</span></span><span style="display:flex;"><span>=========================++============
</span></span><span style="display:flex;"><span> Net:                    || $-3,629.00`
</span></span><span style="display:flex;"><span><span style="color:#e6db74">```</span>
</span></span></code></pre></div><p>It can even generate usage for each template based on the frontmatter.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">params</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">b</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">required</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">usage</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      Required.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      The begin time of report.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      Examples:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        2025-07-01
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        Jul</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">f</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">usage</span>: <span style="color:#ae81ff">The ledger file to parse</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">default</span>: <span style="color:#ae81ff">~/.hledger.journal</span>
</span></span></code></pre></div><p>Run the help command:</p>
<pre tabindex="0"><code>$ mdsh hledger_monthly.md -h
Usage of params:
  -b string
    	Required.
    	The begin time of report.
    	Examples:
    	  2025
    	  Jul
    	 (default &#34;2011-01&#34;)
  -e string
    	Required.
    	The end time of report.
    	Same format as -b
    	 (default &#34;2011-02&#34;)
  -f string
    	The ledger file to parse (default &#34;examples/ledger.j&#34;)
</code></pre><h2 id="output-filename-template">Output Filename Template</h2>
<p>The default setting is writing the output to stdout.
If you want to save it in a file.
You can do so by specifying <code>output:</code> in frontmatter.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">output</span>: <span style="color:#ae81ff">monthly_report_{{.b}}.md</span>
</span></span></code></pre></div><p>When running <code>mdsh hledger_monthly.md -b 2025-07 -e 2025-08</code>, it saves the output to <code>monthly_report_2025-07.md</code>.
It helps you remain the naming consistency with ease.</p>
<p>Not only parameters, you can also put shell script into it.
For example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">output</span>: <span style="color:#ae81ff">sys-report-{{ raw &#34;date --iso-8601&#34; }}.md</span>
</span></span></code></pre></div><p>Then it will write the result to <code>sys-report-2025-07-25.md</code>.</p>
<h2 id="use-case-ideas">Use Case Ideas</h2>
<p>mdsh has great potential in areas that require lots of shell scripting and documentation, like sysadmin, devops, and security.
It&rsquo;s also good for creating documentation and tutorials, reducing the hassle of pasting command outputs over and over.</p>
<p>You can make it fit into your workflow by defining your own templates, with usage notes inside.
Thanks to the rich features of Go&rsquo;s template engine, it allows huge extensibility.
You can apply condition checks (<code>{{ if }}</code>, <code>{{ with }}</code>) or even loops.</p>
<p>Some potential usages are:</p>
<ol>
<li><strong>Sysadmin/DevOps</strong>: system snapshots, cluster health</li>
<li><strong>Documentation</strong>: release notes, test results</li>
<li><strong>Education</strong>: lab reports, tutorials</li>
<li><strong>Security/Compliance</strong>: audits, vulnerability scans</li>
<li><strong>Personal Finance</strong>: hledger, budget reports (what I&rsquo;m using it for)</li>
</ol>
<p>And many more waiting for you to discover!</p>
<hr>
<p>If you found this project useful, please give me a star on <a href="https://github.com/lancatlin/mdsh">GitHub</a> 🌟</p>
<hr>
<p><em>Side note: I developed the first version of mdsh within 2 hours at midnight while trying out <a href="https://zed.dev/">Zed</a>, and was impressed by its performance. I didn&rsquo;t use much AI for this project—sometimes you just need time and space to enjoy programming.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>My Reading List</title>
      <link>https://wancat.cc/en/books/</link>
      <pubDate>Thu, 17 Jul 2025 21:02:45 +1000</pubDate>
      <guid>https://wancat.cc/en/books/</guid>
      <description>&lt;p&gt;Tracking books I&amp;rsquo;ve read, am reading, and want to read.&lt;/p&gt;
&lt;h2 id=&#34;currently-reading&#34;&gt;Currently Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Yuval Noah Harari: Nexus&lt;/li&gt;
&lt;li&gt;Ryder Carroll: The Bullet Journal Method&lt;/li&gt;
&lt;li&gt;Dung Kai-cheung: The Learning Age&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;2025&#34;&gt;2025&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Shauna Shapiro: Good Morning, I Love You&lt;/li&gt;
&lt;li&gt;Richard V. Reeves: &lt;a href=&#34;../post/of-boys-and-men&#34;&gt;Of Boys and Men&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Dung Kai-cheung: The Annals of Time: The Light of Mute Porcelain&lt;/li&gt;
&lt;li&gt;Pan Bo-cheng: Becoming a Boxer&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;2024&#34;&gt;2024&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Erich Fromm: The Art of Loving&lt;/li&gt;
&lt;li&gt;James Clear: Atomic Habits&lt;/li&gt;
&lt;li&gt;Tsai Yu-che: Good Rest&lt;/li&gt;
&lt;li&gt;Chu Chia-an: Video Game Philosophy&lt;/li&gt;
&lt;li&gt;R.F. Kuang: Yellowface&lt;/li&gt;
&lt;li&gt;Dale Carnegie: How to Win Friends and Influence People&lt;/li&gt;
&lt;li&gt;Seth Godin: Linchpin&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;want-to-read&#34;&gt;Want to Read&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Mihaly Csikszentmihalyi: Flow: The Psychology of Optimal Experience&lt;/li&gt;
&lt;li&gt;R.F. Kuang: Babel&lt;/li&gt;
&lt;li&gt;Yeh Hao: Political Jet Lag&lt;/li&gt;
&lt;li&gt;Xu Chenggang: Institutional Genes&lt;/li&gt;
&lt;li&gt;Ekkehard Martens: Good Philosophy Bites&lt;/li&gt;
&lt;/ul&gt;</description>
      <content:encoded><![CDATA[<p>Tracking books I&rsquo;ve read, am reading, and want to read.</p>
<h2 id="currently-reading">Currently Reading</h2>
<ul>
<li>Yuval Noah Harari: Nexus</li>
<li>Ryder Carroll: The Bullet Journal Method</li>
<li>Dung Kai-cheung: The Learning Age</li>
</ul>
<h2 id="2025">2025</h2>
<ul>
<li>Shauna Shapiro: Good Morning, I Love You</li>
<li>Richard V. Reeves: <a href="/post/of-boys-and-men">Of Boys and Men</a></li>
<li>Dung Kai-cheung: The Annals of Time: The Light of Mute Porcelain</li>
<li>Pan Bo-cheng: Becoming a Boxer</li>
</ul>
<h2 id="2024">2024</h2>
<ul>
<li>Erich Fromm: The Art of Loving</li>
<li>James Clear: Atomic Habits</li>
<li>Tsai Yu-che: Good Rest</li>
<li>Chu Chia-an: Video Game Philosophy</li>
<li>R.F. Kuang: Yellowface</li>
<li>Dale Carnegie: How to Win Friends and Influence People</li>
<li>Seth Godin: Linchpin</li>
</ul>
<h2 id="want-to-read">Want to Read</h2>
<ul>
<li>Mihaly Csikszentmihalyi: Flow: The Psychology of Optimal Experience</li>
<li>R.F. Kuang: Babel</li>
<li>Yeh Hao: Political Jet Lag</li>
<li>Xu Chenggang: Institutional Genes</li>
<li>Ekkehard Martens: Good Philosophy Bites</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>What I use</title>
      <link>https://wancat.cc/en/use/</link>
      <pubDate>Wed, 16 Jul 2025 16:58:32 +1000</pubDate>
      <guid>https://wancat.cc/en/use/</guid>
      <description>&lt;p&gt;My gear, software, and services&lt;/p&gt;
&lt;h3 id=&#34;hardware&#34;&gt;Hardware&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Laptop: Framework 13&lt;/li&gt;
&lt;li&gt;Gaming laptop: Razer Blade 15, battery swelled up, now using as desktop gaming rig&lt;/li&gt;
&lt;li&gt;Mouse: Elecom EX-G XL extra large, super comfy grip&lt;/li&gt;
&lt;li&gt;Running watch: Garmin Forerunner 165&lt;/li&gt;
&lt;li&gt;Ebook Reader: BOOX Tab Ultra C&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;software&#34;&gt;Software&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;OS: Fedora 42 / Linux Mint 22&lt;/li&gt;
&lt;li&gt;IDE: Cursor&lt;/li&gt;
&lt;li&gt;Editor: VIM&lt;/li&gt;
&lt;li&gt;Browser: Brave&lt;/li&gt;
&lt;li&gt;Knowledge base: Heptabase, the best note-taking app. &lt;a href=&#34;https://join.heptabase.com?invite-acc-id=112336e4-6a6d-47e4-a412-05ebdef8646d&#34;&gt;My referral code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Epub reader: &lt;a href=&#34;https://johnfactotum.github.io/foliate/&#34;&gt;Foliate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Chinese input: &lt;a href=&#34;https://staratt.cc/posts/%E8%A1%8C%E5%88%97%E8%BC%B8%E5%85%A5%E6%B3%95/&#34;&gt;Array input method&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;self-host&#34;&gt;Self-host&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Router: MikroTik hEX. Super solid, can customize all port connections&lt;/li&gt;
&lt;li&gt;Cloud storage: &lt;a href=&#34;../post/raspi-nextcloud&#34;&gt;Nextcloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Search aggregator: &lt;a href=&#34;https://github.com/searxng/searxng&#34;&gt;SearXNG&lt;/a&gt;, combines Google, DuckDuckGo, Bing results and protects your privacy&lt;/li&gt;
&lt;li&gt;Bookmark: &lt;a href=&#34;https://linkwarden.app/&#34;&gt;Linkwarden&lt;/a&gt;. Collect links, archive, backup web content&lt;/li&gt;
&lt;li&gt;Matrix Server: &lt;a href=&#34;https://element-hq.github.io/synapse/latest/index.html&#34;&gt;Synapse&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Email Server: Postfix+Dovecot using &lt;a href=&#34;https://github.com/LukeSmithxyz/emailwiz&#34;&gt;emailwiz&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;services&#34;&gt;Services&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Email: ProtonMail Essential Plan&lt;/li&gt;
&lt;li&gt;Communication
&lt;ul&gt;
&lt;li&gt;Signal&lt;/li&gt;
&lt;li&gt;Matrix&lt;/li&gt;
&lt;li&gt;Telegram&lt;/li&gt;
&lt;li&gt;Plus whatever everyone else uses&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Password Manager: &lt;a href=&#34;../post/should-you-use-password-manager&#34;&gt;Bitwarden&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;mobile-apps&#34;&gt;Mobile Apps&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://f-droid.org/&#34;&gt;F-Droid&lt;/a&gt;: Free software app store for Android&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/spacecowboy/Feeder&#34;&gt;Feeder&lt;/a&gt;: Best RSS reader, no contest&lt;/li&gt;
&lt;/ul&gt;</description>
      <content:encoded><![CDATA[<p>My gear, software, and services</p>
<h3 id="hardware">Hardware</h3>
<ul>
<li>Laptop: Framework 13</li>
<li>Gaming laptop: Razer Blade 15, battery swelled up, now using as desktop gaming rig</li>
<li>Mouse: Elecom EX-G XL extra large, super comfy grip</li>
<li>Running watch: Garmin Forerunner 165</li>
<li>Ebook Reader: BOOX Tab Ultra C</li>
</ul>
<h3 id="software">Software</h3>
<ul>
<li>OS: Fedora 42 / Linux Mint 22</li>
<li>IDE: Cursor</li>
<li>Editor: VIM</li>
<li>Browser: Brave</li>
<li>Knowledge base: Heptabase, the best note-taking app. <a href="https://join.heptabase.com?invite-acc-id=112336e4-6a6d-47e4-a412-05ebdef8646d">My referral code</a></li>
<li>Epub reader: <a href="https://johnfactotum.github.io/foliate/">Foliate</a></li>
<li>Chinese input: <a href="https://staratt.cc/posts/%E8%A1%8C%E5%88%97%E8%BC%B8%E5%85%A5%E6%B3%95/">Array input method</a></li>
</ul>
<h2 id="self-host">Self-host</h2>
<ul>
<li>Router: MikroTik hEX. Super solid, can customize all port connections</li>
<li>Cloud storage: <a href="/post/raspi-nextcloud">Nextcloud</a></li>
<li>Search aggregator: <a href="https://github.com/searxng/searxng">SearXNG</a>, combines Google, DuckDuckGo, Bing results and protects your privacy</li>
<li>Bookmark: <a href="https://linkwarden.app/">Linkwarden</a>. Collect links, archive, backup web content</li>
<li>Matrix Server: <a href="https://element-hq.github.io/synapse/latest/index.html">Synapse</a></li>
<li>Email Server: Postfix+Dovecot using <a href="https://github.com/LukeSmithxyz/emailwiz">emailwiz</a></li>
</ul>
<h3 id="services">Services</h3>
<ul>
<li>Email: ProtonMail Essential Plan</li>
<li>Communication
<ul>
<li>Signal</li>
<li>Matrix</li>
<li>Telegram</li>
<li>Plus whatever everyone else uses</li>
</ul>
</li>
<li>Password Manager: <a href="/post/should-you-use-password-manager">Bitwarden</a></li>
</ul>
<h3 id="mobile-apps">Mobile Apps</h3>
<ul>
<li><a href="https://f-droid.org/">F-Droid</a>: Free software app store for Android</li>
<li><a href="https://github.com/spacecowboy/Feeder">Feeder</a>: Best RSS reader, no contest</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Divisignal</title>
      <link>https://wancat.cc/en/projects/divisignal/</link>
      <pubDate>Sat, 01 Feb 2025 00:00:00 +0000</pubDate>
      <guid>https://wancat.cc/en/projects/divisignal/</guid>
      <description>&lt;h2 id=&#34;divisignal-stock-traffic-light&#34;&gt;&lt;a href=&#34;https://divi-signal.pages.dev/&#34;&gt;DiviSignal Stock Traffic Light&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;2025&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;DiviSignal screenshot&#34; loading=&#34;lazy&#34; src=&#34;../projects/divisignal/divisignal.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;Stock traffic light analysis tool. Supports dividend yield calculations, import/export of watchlists, and stock filtering by yield.&lt;/p&gt;
&lt;p&gt;Stack: TypeScript, React, Redux, Cheerio, Cloudflare Worker&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="divisignal-stock-traffic-light"><a href="https://divi-signal.pages.dev/">DiviSignal Stock Traffic Light</a></h2>
<p>2025</p>
<p><img alt="DiviSignal screenshot" loading="lazy" src="/projects/divisignal/divisignal.png"></p>
<p>Stock traffic light analysis tool. Supports dividend yield calculations, import/export of watchlists, and stock filtering by yield.</p>
<p>Stack: TypeScript, React, Redux, Cheerio, Cloudflare Worker</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to Handle Conversation in Chatbot in Python</title>
      <link>https://wancat.cc/en/post/python-chatbot-context/</link>
      <pubDate>Thu, 29 Jul 2021 16:13:36 +0800</pubDate>
      <guid>https://wancat.cc/en/post/python-chatbot-context/</guid>
      <description>&lt;p&gt;When you develop a chatbot, sometimes for user experience, you cannot ask your user send messages like  commands. For example, we want to build a guess number bot. We want the bot works like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;user:&lt;/strong&gt; guess&lt;br&gt;
&lt;strong&gt;bot:&lt;/strong&gt; From what number?&lt;br&gt;
&lt;strong&gt;user:&lt;/strong&gt;: 25&lt;br&gt;
&lt;strong&gt;bot:&lt;/strong&gt; To what number?&lt;br&gt;
&lt;strong&gt;user:&lt;/strong&gt; 100&lt;br&gt;
&lt;strong&gt;bot:&lt;/strong&gt; Guess a number between 25 to 100&lt;br&gt;
&lt;strong&gt;user:&lt;/strong&gt; 64&lt;br&gt;
&lt;strong&gt;bot:&lt;/strong&gt; too small&lt;br&gt;
&lt;strong&gt;user:&lt;/strong&gt; 91&lt;br&gt;
&lt;strong&gt;bot:&lt;/strong&gt; too large&lt;br&gt;
&amp;hellip;&amp;hellip;&lt;br&gt;
&lt;strong&gt;user:&lt;/strong&gt; 83&lt;br&gt;
&lt;strong&gt;bot:&lt;/strong&gt; Correct! You spent 6 times to guess this number.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;However, the common way we dealing with requests in the backend is one-request-one-response. That would be a disaster to separate a lot of handlers from the conversation. Why? Think about how to store the states? In global variables? Or database? Or Redis? Once you ask users one more question, you need to change the schema of your state, and the code becomes more complex.&lt;/p&gt;
&lt;p&gt;In the following, I will show you how to deal with conversations and write the handler in a simple and straightforward way like this:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>When you develop a chatbot, sometimes for user experience, you cannot ask your user send messages like  commands. For example, we want to build a guess number bot. We want the bot works like this:</p>
<blockquote>
<p><strong>user:</strong> guess<br>
<strong>bot:</strong> From what number?<br>
<strong>user:</strong>: 25<br>
<strong>bot:</strong> To what number?<br>
<strong>user:</strong> 100<br>
<strong>bot:</strong> Guess a number between 25 to 100<br>
<strong>user:</strong> 64<br>
<strong>bot:</strong> too small<br>
<strong>user:</strong> 91<br>
<strong>bot:</strong> too large<br>
&hellip;&hellip;<br>
<strong>user:</strong> 83<br>
<strong>bot:</strong> Correct! You spent 6 times to guess this number.</p>
</blockquote>
<p>However, the common way we dealing with requests in the backend is one-request-one-response. That would be a disaster to separate a lot of handlers from the conversation. Why? Think about how to store the states? In global variables? Or database? Or Redis? Once you ask users one more question, you need to change the schema of your state, and the code becomes more complex.</p>
<p>In the following, I will show you how to deal with conversations and write the handler in a simple and straightforward way like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">guess</span>(self):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;Game function&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        min_value <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>ask_number(<span style="color:#e6db74">&#39;From what number?&#39;</span>)
</span></span><span style="display:flex;"><span>        max_value <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>ask_number(<span style="color:#e6db74">&#39;To what number?&#39;</span>)
</span></span><span style="display:flex;"><span>        secret <span style="color:#f92672">=</span> randint(min_value, max_value)
</span></span><span style="display:flex;"><span>        msg <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Guess a number between </span><span style="color:#e6db74">{</span>min_value<span style="color:#e6db74">}</span><span style="color:#e6db74"> to </span><span style="color:#e6db74">{</span>max_value<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>
</span></span><span style="display:flex;"><span>        counter <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
</span></span><span style="display:flex;"><span>            counter <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            answer <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>ask_number(msg)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> answer <span style="color:#f92672">&gt;</span> secret:
</span></span><span style="display:flex;"><span>                msg <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;Too large&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">elif</span> answer <span style="color:#f92672">&lt;</span> secret:
</span></span><span style="display:flex;"><span>                msg <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;Too small&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>reply(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;You spent </span><span style="color:#e6db74">{</span>counter<span style="color:#e6db74">}</span><span style="color:#e6db74"> times to guess the secret number.&#39;</span>)
</span></span></code></pre></div><p>I will write a LINE bot for example, but it doesn&rsquo;t matter what platform you develop to. I will use Django and it&rsquo;s okay if you use other frameworks.</p>
<h2 id="setup-environment">Setup Environment</h2>
<p>It&rsquo;s for setting up the bot, you can skip this if you know it.</p>
<p>Clone my repo:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git clone https://github.com/lancatlin/python-chatbot-context.git
</span></span><span style="display:flex;"><span>cd python-chatbot-context
</span></span><span style="display:flex;"><span>pipenv install
</span></span><span style="display:flex;"><span>pipenv shell
</span></span></code></pre></div><p>Go to <a href="https://developers.line.biz">LINE Developers</a> to create a bot. Issue the token and your secret, put them in a <code>.env</code> file.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>LINE_TOKEN<span style="color:#f92672">=</span>YOUR_TOKEN
</span></span><span style="display:flex;"><span>LINE_SECRET<span style="color:#f92672">=</span>YOUR_SECRET
</span></span></code></pre></div><p>Then start Django.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>python manage.py migrate 	<span style="color:#75715e"># for first execution</span>
</span></span><span style="display:flex;"><span>python manage.py runserver
</span></span></code></pre></div><p>Use Ngrok or something similar to tunnel the localhost:8000 to a public endpoint, and register the URL to LINE Messaging API.</p>
<h2 id="idea-describe">Idea Describe</h2>
<h6><img loading="lazy" src="/en/post/python-chatbot-context/diagram.en.jpg"></h6>
<p>The main idea is to block the command thread until another message is received. When the program receives the &lsquo;guess&rsquo; command, it will be executed in the command thread. Once the program needs input from the user, it put a message in the room&rsquo;s &ldquo;requests queue&rdquo;. Then when the message comes in at another thread, it checks the room&rsquo;s requests queue and puts the message in the responses queue if not empty.</p>
<h2 id="implement">Implement</h2>
<p>We implement it as <code>MessageQueue</code> class:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># guess/message_queue.py</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> queue
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> threading <span style="color:#f92672">import</span> RLock
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> .line <span style="color:#f92672">import</span> get_room
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">RequestTimout</span>(<span style="color:#a6e22e">Exception</span>):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pass</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MessageQueue</span>:
</span></span><span style="display:flex;"><span>    __lock <span style="color:#f92672">=</span> RLock()
</span></span><span style="display:flex;"><span>    __requests <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>    __responses <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@classmethod</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">create_if_not_exists</span>(cls, room):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;Create the requests and responses queues for the room if not exists&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">with</span> cls<span style="color:#f92672">.</span>__lock:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> room <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> cls<span style="color:#f92672">.</span>__requests:
</span></span><span style="display:flex;"><span>                cls<span style="color:#f92672">.</span>__requests[room] <span style="color:#f92672">=</span> queue<span style="color:#f92672">.</span>Queue(maxsize<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> room <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> cls<span style="color:#f92672">.</span>__responses:
</span></span><span style="display:flex;"><span>                cls<span style="color:#f92672">.</span>__responses[room] <span style="color:#f92672">=</span> queue<span style="color:#f92672">.</span>Queue(maxsize<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@classmethod</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">handle</span>(cls, event):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;Handle the message, check whether there is room request for&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        room <span style="color:#f92672">=</span> get_room(event)
</span></span><span style="display:flex;"><span>        cls<span style="color:#f92672">.</span>create_if_not_exists(room)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> cls<span style="color:#f92672">.</span>__requests[room]<span style="color:#f92672">.</span>empty():
</span></span><span style="display:flex;"><span>                cls<span style="color:#f92672">.</span>__responses[room]<span style="color:#f92672">.</span>put(event, timeout<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>                cls<span style="color:#f92672">.</span>__requests[room]<span style="color:#f92672">.</span>get()
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> queue<span style="color:#f92672">.</span>Empty:
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;&#39;&#39;No request, ignore the message&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@classmethod</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">request</span>(cls, room, timeout<span style="color:#f92672">=</span><span style="color:#ae81ff">30</span>):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;Request a message, block until message comes in or timeout&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            cls<span style="color:#f92672">.</span>create_if_not_exists(room)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            cls<span style="color:#f92672">.</span>__requests[room]<span style="color:#f92672">.</span>put_nowait(<span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> cls<span style="color:#f92672">.</span>__responses[room]<span style="color:#f92672">.</span>get(timeout<span style="color:#f92672">=</span>timeout)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> queue<span style="color:#f92672">.</span>Empty:
</span></span><span style="display:flex;"><span>            MessageQueue<span style="color:#f92672">.</span>clear(room)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">raise</span> RequestTimout
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@classmethod</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">clear</span>(cls, room):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;Clear the requests&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        cls<span style="color:#f92672">.</span>create_if_not_exists(room)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            cls<span style="color:#f92672">.</span>__requests[room]<span style="color:#f92672">.</span>get_nowait()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> queue<span style="color:#f92672">.</span>Empty:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">pass</span>
</span></span></code></pre></div><p>With this, we can implement our guess app very easily.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># guess/guess.py</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> .message_queue <span style="color:#f92672">import</span> MessageQueue, RequestTimout
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> .line <span style="color:#f92672">import</span> reply_text, get_room, get_msg
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> random <span style="color:#f92672">import</span> randint
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Guess</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;&#39;&#39;Guess handle a guess number game&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">__init__</span>(self, event):
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>event <span style="color:#f92672">=</span> event
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            self<span style="color:#f92672">.</span>guess()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> RequestTimout:
</span></span><span style="display:flex;"><span>            self<span style="color:#f92672">.</span>reply(<span style="color:#e6db74">&#39;Timeout&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">guess</span>(self):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;Game function&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        min_value <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>ask_number(<span style="color:#e6db74">&#39;From what number?&#39;</span>)
</span></span><span style="display:flex;"><span>        max_value <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>ask_number(<span style="color:#e6db74">&#39;To what number?&#39;</span>)
</span></span><span style="display:flex;"><span>        secret <span style="color:#f92672">=</span> randint(min_value, max_value)
</span></span><span style="display:flex;"><span>        msg <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Guess a number between </span><span style="color:#e6db74">{</span>min_value<span style="color:#e6db74">}</span><span style="color:#e6db74"> to </span><span style="color:#e6db74">{</span>max_value<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>
</span></span><span style="display:flex;"><span>        counter <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
</span></span><span style="display:flex;"><span>            counter <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            answer <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>ask_number(msg)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> answer <span style="color:#f92672">&gt;</span> secret:
</span></span><span style="display:flex;"><span>                msg <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;Too large&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">elif</span> answer <span style="color:#f92672">&lt;</span> secret:
</span></span><span style="display:flex;"><span>                msg <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;Too small&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>reply(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;You spent </span><span style="color:#e6db74">{</span>counter<span style="color:#e6db74">}</span><span style="color:#e6db74"> times to guess the secret number.&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">ask</span>(self, <span style="color:#f92672">*</span>msg):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;Ask a question to current user&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>reply(<span style="color:#f92672">*</span>msg)
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>event <span style="color:#f92672">=</span> MessageQueue<span style="color:#f92672">.</span>request(get_room(self<span style="color:#f92672">.</span>event))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> get_msg(self<span style="color:#f92672">.</span>event)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">ask_number</span>(self, <span style="color:#f92672">*</span>msg):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;Ask a number, if not number, ask again&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            content <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>ask(<span style="color:#f92672">*</span>msg)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> int(content)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">ValueError</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>ask_number(<span style="color:#e6db74">&#39;Please input an integer.&#39;</span>, <span style="color:#f92672">*</span>msg)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">reply</span>(self, <span style="color:#f92672">*</span>msg):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;Reply words to user&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        reply_text(self<span style="color:#f92672">.</span>event, <span style="color:#f92672">*</span>msg)
</span></span></code></pre></div><p>You can see the main function is straightforward, with only 17 lines of code. What&rsquo;s more, it can handle multiple user inputs at the same time.</p>
<p><img loading="lazy" src="/post/python-chatbot-context/result.gif"></p>
<p>Get full code on <a href="https://github.com/lancatlin/python-chatbot-context">GitHub</a>.</p>
<p>Special thanks to <a href="https://github.com/YukinaMochizuki">YukinaMochizuki</a> for giving me the initial idea from <a href="https://github.com/YukinaMochizuki/DCDos">his Notion bot project</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to Redirect Stdout to Streaming Response in Django</title>
      <link>https://wancat.cc/en/post/django-redirect-stdout-to-streaming/</link>
      <pubDate>Tue, 25 May 2021 11:28:37 +0800</pubDate>
      <guid>https://wancat.cc/en/post/django-redirect-stdout-to-streaming/</guid>
      <description>&lt;p&gt;Sometimes we need to execute some long tasks at the backend, and the tasks are complicated and error-prone. So we hope users can see the real-time console log. Thus we need to redirect the stdout in our functions to the user&amp;rsquo;s browser.&lt;/p&gt;
&lt;p&gt;Given a function like the following. How to see the stdout in real-time in the browser?&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; time
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;job&lt;/span&gt;(times):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; range(times):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;Task #&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;i&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        time&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sleep(&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;Done&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    time&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sleep(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.5&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img loading=&#34;lazy&#34; src=&#34;../post/django-redirect-stdout-to-streaming/output.gif&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;streaming-response&#34;&gt;Streaming Response&lt;/h2&gt;
&lt;p&gt;Normally, responses are sent after all the data has been collected. However, sometimes we can&amp;rsquo;t wait until the data is prepared. In this case, we&amp;rsquo;ll use &lt;strong&gt;streaming response&lt;/strong&gt;. In Django, it&amp;rsquo;s StreamingHttpResponse. I&amp;rsquo;ll call it SHR in the following article. StreamingHttpResponse accepts an iterator for input. It sends the value each time getting a new value from the iterator. To use it, we only need to implement an iterator function. It will send the value from &lt;code&gt;yield &lt;/code&gt; to the user&amp;rsquo;s browser in real-time.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Sometimes we need to execute some long tasks at the backend, and the tasks are complicated and error-prone. So we hope users can see the real-time console log. Thus we need to redirect the stdout in our functions to the user&rsquo;s browser.</p>
<p>Given a function like the following. How to see the stdout in real-time in the browser?</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">job</span>(times):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(times):
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Task #</span><span style="color:#e6db74">{</span>i<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>)
</span></span><span style="display:flex;"><span>        time<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#39;Done&#39;</span>)
</span></span><span style="display:flex;"><span>    time<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">0.5</span>)
</span></span></code></pre></div><p><img loading="lazy" src="/post/django-redirect-stdout-to-streaming/output.gif"></p>
<h2 id="streaming-response">Streaming Response</h2>
<p>Normally, responses are sent after all the data has been collected. However, sometimes we can&rsquo;t wait until the data is prepared. In this case, we&rsquo;ll use <strong>streaming response</strong>. In Django, it&rsquo;s StreamingHttpResponse. I&rsquo;ll call it SHR in the following article. StreamingHttpResponse accepts an iterator for input. It sends the value each time getting a new value from the iterator. To use it, we only need to implement an iterator function. It will send the value from <code>yield </code> to the user&rsquo;s browser in real-time.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># Example of StreamingHttpResponse</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> django.http.response <span style="color:#f92672">import</span> StreamingHttpResponse
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">example</span>():
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(<span style="color:#ae81ff">5</span>):
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Add &lt;br&gt; to break line in browser</span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">yield</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;</span><span style="color:#e6db74">{</span>i<span style="color:#e6db74">}</span><span style="color:#e6db74">&lt;br&gt;&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">stream</span>(request):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> StreamingHttpResponse(example())
</span></span></code></pre></div><p>Output (in browser):</p>
<pre tabindex="0"><code>0
1
2
3
4
</code></pre><h2 id="threading">Threading</h2>
<p>As we execute the task and stream output at the same time, we need <strong>concurrence</strong>. In Python, there are multiple choices, like threading, multiprocessing, etc. I&rsquo;ll use threading in this article because it is easier for me.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># Example of threading</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> threading <span style="color:#f92672">import</span> Thread
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">example</span>(times):
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(times):
</span></span><span style="display:flex;"><span>		print(i)
</span></span><span style="display:flex;"><span>		time<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Create Thread</span>
</span></span><span style="display:flex;"><span>thread <span style="color:#f92672">=</span> Thread(target<span style="color:#f92672">=</span>example, args<span style="color:#f92672">=</span>(<span style="color:#ae81ff">5</span>,))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Start Thread</span>
</span></span><span style="display:flex;"><span>thread<span style="color:#f92672">.</span>start()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>time<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;This is printed in the main thread&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Waiting thread to be done</span>
</span></span><span style="display:flex;"><span>thread<span style="color:#f92672">.</span>join()
</span></span></code></pre></div><p>Output:</p>
<pre tabindex="0"><code>0
1
This is printed in the main thread
2
3
4
</code></pre><h2 id="redirect-stdout">Redirect Stdout</h2>
<p>To change the location where Python print, we need to change <code>sys. stdout</code>. It accepts any File-like object. Specifically, we need to define an object with the <code>write</code> method.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># Example of redirect stdout</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sys
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Printer</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">__init__</span>(self):
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>contents <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">write</span>(self, value):
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>contents<span style="color:#f92672">.</span>append(value)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>printer <span style="color:#f92672">=</span> Printer()
</span></span><span style="display:flex;"><span>sys<span style="color:#f92672">.</span>stdout <span style="color:#f92672">=</span> printer
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#39;This should be saved in printer&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>sys<span style="color:#f92672">.</span>stdout <span style="color:#f92672">=</span> sys<span style="color:#f92672">.</span>__stdout__
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#39;This should be printed to stdout&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(printer<span style="color:#f92672">.</span>contents)
</span></span></code></pre></div><p>Output:</p>
<pre tabindex="0"><code>This should be printed to stdout
[&#39;This should be saved in printer&#39;, &#39;\n&#39;]
</code></pre><h2 id="implement-redirecting-stdout-to-streaming-response">Implement Redirecting Stdout to Streaming Response</h2>
<h3 id="environment">Environment</h3>
<p>Python 3.8.5</p>
<p>Django 3.2</p>
<p>First, create Django Project</p>
<pre tabindex="0"><code>pip install django
django-admin startproject console_streaming
cd console_streaming
python manage.py startapp web
</code></pre><p>Install web</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># console_streaming/settings.py</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>INSTALLED_APPS <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">...</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Add web</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;web&#39;</span>,
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>Create a view</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># web/views.py</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">stream</span>(request):
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># implement later</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pass</span>
</span></span></code></pre></div><p>Bind to URL</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># console_streaming/urls.py</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> django.urls <span style="color:#f92672">import</span> path
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> web <span style="color:#f92672">import</span> views
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>urlpatterns <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    path(<span style="color:#e6db74">&#39;stream/&#39;</span>, views<span style="color:#f92672">.</span>stream),
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><h3 id="testing-task">Testing Task</h3>
<p>This is the testing function we&rsquo;re going to use. It will repeat printing a line and waiting a second for n times, and then print &ldquo;Done&rdquo;.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># web/views.py</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">job</span>(times):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(times):
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Task #</span><span style="color:#e6db74">{</span>i<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>)
</span></span><span style="display:flex;"><span>        time<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#39;Done&#39;</span>)
</span></span><span style="display:flex;"><span>    time<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">0.5</span>)
</span></span></code></pre></div><h3 id="printer-class">Printer class</h3>
<p>We implement a Printer class to handle stdout, and we&rsquo;ll only use <strong>one instance</strong> in the whale program life cycle. Because <code>sys.stdout</code> does not thread-specific, if we use different stdout in different requests, one would grab stdout from another. So I use a dictionary to store the queue for different threads, and use <code>current_thread()</code> to identify and pick the right queue. If the current thread hasn&rsquo;t registered to Printer, use the default stdout.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># web/views.py</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> queue <span style="color:#f92672">import</span> Queue
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> threading <span style="color:#f92672">import</span> current_thread
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sys
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Printer</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">__init__</span>(self):
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>queues <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">write</span>(self, value):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;handle stdout&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        queue <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>queues<span style="color:#f92672">.</span>get(current_thread()<span style="color:#f92672">.</span>name)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> queue:
</span></span><span style="display:flex;"><span>            queue<span style="color:#f92672">.</span>put(value)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            sys<span style="color:#f92672">.</span>__stdout__<span style="color:#f92672">.</span>write(value)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">flush</span>(self):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;Django would crash without this&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">pass</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">register</span>(self, thread):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;register a Thread&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        queue <span style="color:#f92672">=</span> Queue()
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>queues[thread<span style="color:#f92672">.</span>name] <span style="color:#f92672">=</span> queue
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> queue
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">clean</span>(self, thread):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;delete a Thread&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">del</span> self<span style="color:#f92672">.</span>queues[thread<span style="color:#f92672">.</span>name]
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Initialize a Printer instance</span>
</span></span><span style="display:flex;"><span>printer <span style="color:#f92672">=</span> Printer()
</span></span><span style="display:flex;"><span>sys<span style="color:#f92672">.</span>stdout <span style="color:#f92672">=</span> printer
</span></span></code></pre></div><h3 id="streamer-class">Streamer class</h3>
<p>Next, we&rsquo;re going to implement concurrent execution and streaming response by a Streamer class. It will initialize a thread, and register it to the printer to get a queue. Then it repeats to read the value from queue and yield to response until the thread ends.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> threading <span style="color:#f92672">import</span> Thread
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Steamer</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">__init__</span>(self, target, args):
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>thread <span style="color:#f92672">=</span> Thread(target<span style="color:#f92672">=</span>target, args<span style="color:#f92672">=</span>args)
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>queue <span style="color:#f92672">=</span> printer<span style="color:#f92672">.</span>register(self<span style="color:#f92672">.</span>thread)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">start</span>(self):
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>thread<span style="color:#f92672">.</span>start()
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#39;This should be stdout&#39;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">while</span> self<span style="color:#f92672">.</span>thread<span style="color:#f92672">.</span>is_alive():
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>                item <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>queue<span style="color:#f92672">.</span>get_nowait()
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">yield</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;</span><span style="color:#e6db74">{</span>item<span style="color:#e6db74">}</span><span style="color:#e6db74">&lt;br&gt;&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">except</span> Empty:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">pass</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">yield</span> <span style="color:#e6db74">&#39;End&#39;</span>
</span></span><span style="display:flex;"><span>        printer<span style="color:#f92672">.</span>clean(self<span style="color:#f92672">.</span>thread)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">stream</span>(request):
</span></span><span style="display:flex;"><span>    streamer <span style="color:#f92672">=</span> Steamer(job, (<span style="color:#ae81ff">10</span>,))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> StreamingHttpResponse(streamer<span style="color:#f92672">.</span>start())
</span></span></code></pre></div><p>Run Django server</p>
<pre tabindex="0"><code>$ python manage.py runserver
</code></pre><p>Open http://localhost:8000/stream/</p>
<p>Then you can see</p>
<p><img loading="lazy" src="/post/django-redirect-stdout-to-streaming/output.gif"></p>
<p>Each time you make a request, you can see one output in the terminal.</p>
<pre tabindex="0"><code>This should be stdout
</code></pre><h2 id="full-viewspy">Full views.py</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> django.http.response <span style="color:#f92672">import</span> StreamingHttpResponse
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> queue <span style="color:#f92672">import</span> Queue, Empty
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> threading <span style="color:#f92672">import</span> Thread, current_thread
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sys
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Printer</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">__init__</span>(self):
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>queues <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">write</span>(self, value):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;handle stdout&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        queue <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>queues<span style="color:#f92672">.</span>get(current_thread()<span style="color:#f92672">.</span>name)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> queue:
</span></span><span style="display:flex;"><span>            queue<span style="color:#f92672">.</span>put(value)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            sys<span style="color:#f92672">.</span>__stdout__<span style="color:#f92672">.</span>write(value)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">flush</span>(self):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;Django would crash without this&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">pass</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">register</span>(self, thread):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;register a Thread&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        queue <span style="color:#f92672">=</span> Queue()
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>queues[thread<span style="color:#f92672">.</span>name] <span style="color:#f92672">=</span> queue
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> queue
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">clean</span>(self, thread):
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;&#39;&#39;delete a Thread&#39;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">del</span> self<span style="color:#f92672">.</span>queues[thread<span style="color:#f92672">.</span>name]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>printer <span style="color:#f92672">=</span> Printer()
</span></span><span style="display:flex;"><span>sys<span style="color:#f92672">.</span>stdout <span style="color:#f92672">=</span> printer
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Steamer</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">__init__</span>(self, target, args):
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>thread <span style="color:#f92672">=</span> Thread(target<span style="color:#f92672">=</span>target, args<span style="color:#f92672">=</span>args)
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>queue <span style="color:#f92672">=</span> printer<span style="color:#f92672">.</span>register(self<span style="color:#f92672">.</span>thread)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">start</span>(self):
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>thread<span style="color:#f92672">.</span>start()
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#39;This should be stdout&#39;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">while</span> self<span style="color:#f92672">.</span>thread<span style="color:#f92672">.</span>is_alive():
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>                item <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>queue<span style="color:#f92672">.</span>get_nowait()
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">yield</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;</span><span style="color:#e6db74">{</span>item<span style="color:#e6db74">}</span><span style="color:#e6db74">&lt;br&gt;&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">except</span> Empty:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">pass</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">yield</span> <span style="color:#e6db74">&#39;End&#39;</span>
</span></span><span style="display:flex;"><span>        printer<span style="color:#f92672">.</span>clean(self<span style="color:#f92672">.</span>thread)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">job</span>(times):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(times):
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Task #</span><span style="color:#e6db74">{</span>i<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>)
</span></span><span style="display:flex;"><span>        time<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#39;Done&#39;</span>)
</span></span><span style="display:flex;"><span>    time<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">0.5</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">stream</span>(request):
</span></span><span style="display:flex;"><span>    streamer <span style="color:#f92672">=</span> Steamer(job, (<span style="color:#ae81ff">10</span>,))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> StreamingHttpResponse(streamer<span style="color:#f92672">.</span>start())
</span></span></code></pre></div><p>Complete code on <a href="https://github.com/lancatlin/python_console_streaming">GitHub</a></p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://docs.python.org/3/library/threading.html#thread-objects">Python Docs: threading</a></li>
<li><a href="https://docs.python.org/3/library/queue.html#queue-objects">Python Docs: queue</a></li>
<li><a href="https://chase-seibert.github.io/blog/2010/08/06/redirect-console-output-to-a-django-httpresponse.html">Chase Seibert: Redirect console output to a Django HttpResponse</a></li>
<li><a href="https://bytes.com/topic/python/answers/36067-thread-specific-sys-stdout">thread specific sys.stdout?</a></li>
<li><a href="https://blog.gtwang.org/programming/python-threading-multithreaded-programming-tutorial/">G. T. Wang: Python 多執行緒 threading 模組平行化程式設計教學</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>About me</title>
      <link>https://wancat.cc/en/about/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://wancat.cc/en/about/</guid>
      <description>&lt;p&gt;Software Engineer specializing in web development.
Runner. Muay Thai practitioner. Sometimes write songs.
Free software enthusiast and self-hosting advocate.
One of my hobbies is installing Linux on other people&amp;rsquo;s computers.&lt;/p&gt;
&lt;h2 id=&#34;about-this-site&#34;&gt;About this site&lt;/h2&gt;
&lt;p&gt;My personal blog featuring technical insights, book notes, and personal thoughts.&lt;/p&gt;
&lt;p&gt;You may want to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/projects&lt;/code&gt; &lt;a href=&#34;../en/projects/&#34;&gt;My projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/now&lt;/code&gt; &lt;a href=&#34;../en/now/&#34;&gt;What I&amp;rsquo;m up to these days&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/use&lt;/code&gt; &lt;a href=&#34;../en/use/&#34;&gt;What I use&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/books&lt;/code&gt; &lt;a href=&#34;../en/books/&#34;&gt;What I read&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;contact-me&#34;&gt;Contact me&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Email: &lt;a href=&#34;mailto:wancat@wancat.cc&#34;&gt;wancat@wancat.cc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Matrix: @wancat:linchpins.cloud&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/lancatlin&#34;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://matters.news/@wancat/&#34;&gt;Matters&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <content:encoded><![CDATA[<p>Software Engineer specializing in web development.
Runner. Muay Thai practitioner. Sometimes write songs.
Free software enthusiast and self-hosting advocate.
One of my hobbies is installing Linux on other people&rsquo;s computers.</p>
<h2 id="about-this-site">About this site</h2>
<p>My personal blog featuring technical insights, book notes, and personal thoughts.</p>
<p>You may want to know:</p>
<ul>
<li><code>/projects</code> <a href="/en/projects/">My projects</a></li>
<li><code>/now</code> <a href="/en/now/">What I&rsquo;m up to these days</a></li>
<li><code>/use</code> <a href="/en/use/">What I use</a></li>
<li><code>/books</code> <a href="/en/books/">What I read</a></li>
</ul>
<h2 id="contact-me">Contact me</h2>
<ul>
<li>Email: <a href="mailto:wancat@wancat.cc">wancat@wancat.cc</a></li>
<li>Matrix: @wancat:linchpins.cloud</li>
<li><a href="https://github.com/lancatlin">GitHub</a></li>
<li><a href="https://matters.news/@wancat/">Matters</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Resume</title>
      <link>https://wancat.cc/en/resume/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://wancat.cc/en/resume/</guid>
      <description>Penultimate-year Computer Science student at Monash University, experienced in Go, TypeScript, and Python, with production work in web application development and Unix/Linux environments across server-side container orchestration, latency-sensitive synchronisation protocols, and blockchain data pipelines.</description>
    </item>
  </channel>
</rss>
