<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: hiyoyo</title>
    <description>The latest articles on DEV Community by hiyoyo (@hiyoyok).</description>
    <link>https://dev.to/hiyoyok</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3851832%2Fa2762ba1-e687-4ae9-901d-245b96cf95d6.jpg</url>
      <title>DEV Community: hiyoyo</title>
      <link>https://dev.to/hiyoyok</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hiyoyok"/>
    <language>en</language>
    <item>
      <title>One Input Box, Two AI Modes — Detecting Whether the User Wants Error Help or Command Explanation</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Sun, 10 May 2026 16:15:18 +0000</pubDate>
      <link>https://dev.to/hiyoyok/one-input-box-two-ai-modes-detecting-whether-the-user-wants-error-help-or-command-explanation-1ok</link>
      <guid>https://dev.to/hiyoyok/one-input-box-two-ai-modes-detecting-whether-the-user-wants-error-help-or-command-explanation-1ok</guid>
      <description>&lt;p&gt;If this is useful, a ❤️ helps others find it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All tests run on an 8-year-old MacBook Air.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;HiyokoHelper has one input box but handles two completely different use cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Error diagnosis&lt;/strong&gt; — "Why did this fail and how do I fix it?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Command explanation&lt;/strong&gt; — "What does this command actually do?"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The user doesn't select a mode. The app detects which one they need.&lt;/p&gt;




&lt;h2&gt;
  
  
  Detecting the mode
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;InputMode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ErrorDiagnosis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CommandExplain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;detect_mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;InputMode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;trimmed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;.trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="nf"&gt;.lines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;command_prefixes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s"&gt;"sudo "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"git "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"npm "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"yarn "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"cargo "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"brew "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pip "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"docker "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"kubectl "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"ls "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"cd "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mv "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"cp "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"chmod "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"systemctl "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"launchctl "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;looks_like_command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command_prefixes&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;.any&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="nf"&gt;.starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="nf"&gt;.starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"$ "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="nf"&gt;.starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"% "&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;looks_like_command&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nn"&gt;InputMode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CommandExplain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nn"&gt;InputMode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ErrorDiagnosis&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Different prompts for each mode
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;build_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;InputMode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;InputMode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ErrorDiagnosis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Lang&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Japanese&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"あなたはターミナルエラーの専門家です。&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;
             以下のエラーの原因を1〜2文で説明し、&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;
             解決コマンドがあれば示してください。&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;InputMode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CommandExplain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Lang&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Japanese&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"あなたはターミナルコマンドの先生です。&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;
             以下のコマンドが何をするか初心者向けに説明し、&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;
             危険な副作用があれば警告してください。&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;InputMode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ErrorDiagnosis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Lang&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;English&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"You are a terminal error specialist. &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;
             Explain the cause in 1-2 sentences and provide a fix.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;InputMode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CommandExplain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Lang&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;English&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"You are a terminal command teacher. &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;
             Explain what this command does for a beginner. &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;
             Warn about dangerous side effects.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Live mode indicator in UI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;detectMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🔍 エラー診断モード&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;📖 コマンド解説モード&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Updates live as they type. No confusion about what will happen on submit.&lt;/p&gt;




&lt;p&gt;HiyokoHelper (OSS) → github.com/hiyoyok/HiyokoHelper&lt;br&gt;
X → &lt;a class="mentioned-user" href="https://dev.to/hiyoyok"&gt;@hiyoyok&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gemini</category>
      <category>ai</category>
      <category>rust</category>
      <category>programming</category>
    </item>
    <item>
      <title>AI Overlay UI in Tauri — Designing the "Ask AI" Button That Doesn't Annoy Users</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Thu, 07 May 2026 16:07:38 +0000</pubDate>
      <link>https://dev.to/hiyoyok/ai-overlay-ui-in-tauri-designing-the-ask-ai-button-that-doesnt-annoy-users-17i5</link>
      <guid>https://dev.to/hiyoyok/ai-overlay-ui-in-tauri-designing-the-ask-ai-button-that-doesnt-annoy-users-17i5</guid>
      <description>&lt;p&gt;All tests run on an 8-year-old MacBook Air.&lt;br&gt;
All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.&lt;br&gt;
Every app has an AI button now. Most of them are annoying. Here's how I approach AI UI in my Tauri apps.&lt;/p&gt;

&lt;p&gt;The problem with most AI UI&lt;br&gt;
The bad pattern: a prominent "Ask AI" button that's always visible, always tempting users to click it, and produces results that take 3 seconds to load with a full-screen loading state.&lt;br&gt;
The result: users click once, wait, get a mediocre response, and never click again. The button becomes visual noise.&lt;/p&gt;

&lt;p&gt;What works: contextual, fast, optional&lt;br&gt;
Contextual: the AI button appears near the content it analyzes, not in a toolbar. In HiyokoLogcat, the "Diagnose" button appears next to each log entry. In HiyokoHelper, the analyze button appears next to the clipboard content. The button makes sense where it lives.&lt;br&gt;
Fast feedback: show something immediately. Even if the full response takes 2 seconds, stream the first tokens within 200ms. The user sees progress. Waiting feels shorter when something is happening.&lt;br&gt;
Optional: AI features are enhancements. The app is fully usable without clicking the AI button. Users who want it will find it. Users who don't won't be forced through it.&lt;/p&gt;

&lt;p&gt;The loading state that works&lt;br&gt;
Don't show a spinner for 2 seconds then replace everything with text. Stream the response:&lt;br&gt;
typescriptconst [response, setResponse] = useState('')&lt;br&gt;
const [isStreaming, setIsStreaming] = useState(false)&lt;/p&gt;

&lt;p&gt;// As chunks arrive from Gemini streaming&lt;br&gt;
listen('ai-chunk', (event) =&amp;gt; {&lt;br&gt;
    setResponse(prev =&amp;gt; prev + event.payload)&lt;br&gt;
})&lt;br&gt;
Text appearing character by character feels fast even when it isn't.&lt;/p&gt;

&lt;p&gt;The error state that doesn't panic users&lt;br&gt;
When Gemini returns a 429 or 503, don't show a technical error. Show:&lt;/p&gt;

&lt;p&gt;"Analysis unavailable right now. Try again in a moment."&lt;/p&gt;

&lt;p&gt;One sentence. No stack trace. No HTTP status code. The user doesn't care why it failed — they care what to do next.&lt;/p&gt;

&lt;p&gt;The API key UX&lt;br&gt;
Apps that require an API key have an onboarding problem. Users don't want to get an API key before trying your app.&lt;br&gt;
Solution: make the AI features clearly optional, show them working in screenshots/demos, and make the API key setup flow short. Three steps maximum: get key → paste key → done.&lt;/p&gt;

&lt;p&gt;The verdict&lt;br&gt;
AI UI that respects the user's attention works. Streaming responses, contextual buttons, graceful degradation on errors. The "AI" label doesn't make a feature valuable — solving a real problem does.&lt;/p&gt;

&lt;p&gt;If this was useful, a ❤️ helps more than you'd think — thanks!&lt;br&gt;
Hiyoko PDF Vault → &lt;a href="https://hiyokoko.gumroad.com/l/HiyokoPDFVault" rel="noopener noreferrer"&gt;https://hiyokoko.gumroad.com/l/HiyokoPDFVault&lt;/a&gt;&lt;br&gt;
X → &lt;a class="mentioned-user" href="https://dev.to/hiyoyok"&gt;@hiyoyok&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tauri</category>
      <category>rust</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>"You Got This Error Last Week" — Building an AI That Remembers Your Past Errors</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Thu, 07 May 2026 14:52:21 +0000</pubDate>
      <link>https://dev.to/hiyoyok/you-got-this-error-last-week-building-an-ai-that-remembers-your-past-errors-1gmp</link>
      <guid>https://dev.to/hiyoyok/you-got-this-error-last-week-building-an-ai-that-remembers-your-past-errors-1gmp</guid>
      <description>&lt;p&gt;If this is useful, a ❤️ helps others find it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All tests run on an 8-year-old MacBook Air.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The same error appears twice. Most AI tools diagnose it twice — two API calls, same answer.&lt;/p&gt;

&lt;p&gt;HiyokoHelper remembers. When the same error appears again, it responds instantly from cache: "💡 先日も同じケースが発生し、〇〇で解決しました"&lt;/p&gt;

&lt;p&gt;Here's how the history cache works.&lt;/p&gt;




&lt;h2&gt;
  
  
  The data structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Serialize,&lt;/span&gt; &lt;span class="nd"&gt;Deserialize,&lt;/span&gt; &lt;span class="nd"&gt;Clone)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;HistoryEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;error_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;error_preview&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;diagnosis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;hit_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stored in &lt;code&gt;history.json&lt;/code&gt; via &lt;code&gt;tauri-plugin-store&lt;/code&gt;. Local only, never leaves the machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Normalizing before hashing
&lt;/h2&gt;

&lt;p&gt;Same error, different timestamps → same hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;normalize_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;timestamp_re&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;r"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timestamp_re&lt;/span&gt;&lt;span class="nf"&gt;.replace_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"[TIMESTAMP]"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;line_re&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;r"line \d+"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line_re&lt;/span&gt;&lt;span class="nf"&gt;.replace_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"line [N]"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;pid_re&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;r"\bpid[: ]\d+"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pid_re&lt;/span&gt;&lt;span class="nf"&gt;.replace_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pid [PID]"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="nf"&gt;.split_whitespace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="py"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The lookup flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;diagnose_with_history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;HistoryCache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DiagnosisResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;error_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="py"&gt;.hit_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="py"&gt;.resolved&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"💡 先日も同じエラーが発生し、解決済みです。&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="py"&gt;.diagnosis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"⚠️ このエラーは以前も発生しています（{}回目）。&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="py"&gt;.hit_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="py"&gt;.diagnosis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nn"&gt;DiagnosisResult&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FromHistory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;diagnosis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="py"&gt;.diagnosis&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;diagnosis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_gemini&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HistoryEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;error_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;error_preview&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;.chars&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;diagnosis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;diagnosis&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;unix_now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;hit_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nn"&gt;DiagnosisResult&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fresh&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;diagnosis&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The "resolved" button
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;mark_resolved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;HistoryCache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="nf"&gt;.get_mut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="py"&gt;.resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="nf"&gt;.save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next time: "You had this issue and resolved it. Here's what worked."&lt;/p&gt;




&lt;h2&gt;
  
  
  Cache eviction
&lt;/h2&gt;

&lt;p&gt;Unresolved entries older than 30 days evicted. Resolved entries kept forever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;evict_old_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;HistoryCache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unix_now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="py"&gt;.entries&lt;/span&gt;&lt;span class="nf"&gt;.retain&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="py"&gt;.created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="py"&gt;.resolved&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;HiyokoHelper (OSS) → github.com/hiyoyok/HiyokoHelper&lt;br&gt;
X → &lt;a class="mentioned-user" href="https://dev.to/hiyoyok"&gt;@hiyoyok&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gemini</category>
      <category>rust</category>
      <category>programming</category>
    </item>
    <item>
      <title>Detecting Dangerous Terminal Commands Before Sending Them to an AI — My Safety Layer</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Thu, 07 May 2026 01:14:37 +0000</pubDate>
      <link>https://dev.to/hiyoyok/detecting-dangerous-terminal-commands-before-sending-them-to-an-ai-my-safety-layer-109j</link>
      <guid>https://dev.to/hiyoyok/detecting-dangerous-terminal-commands-before-sending-them-to-an-ai-my-safety-layer-109j</guid>
      <description>&lt;p&gt;If this is useful, a ❤️ helps others find it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All tests run on an 8-year-old MacBook Air.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;HiyokoHelper sends terminal errors to Gemini for diagnosis. But what if the user pastes &lt;code&gt;rm -rf /&lt;/code&gt; or a fork bomb?&lt;/p&gt;

&lt;p&gt;Two problems: wasting API quota on non-errors, and potentially getting an "explanation" of a command that could destroy the system.&lt;/p&gt;

&lt;p&gt;Here's the safety layer I built.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 1: Is it actually dangerous?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;PartialEq)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;SafetyLevel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Safe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;Danger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;assess_safety&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SafetyLevel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;input_lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;.to_lowercase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;critical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rm -rf /"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"This deletes your entire filesystem."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rm -rf ~"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"This deletes your home directory."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rm -rf *"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"This deletes everything in the current directory."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mkfs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"This formats a disk, erasing all data."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dd if=/dev/zero"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"This overwrites a disk with zeros."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":(){:|:&amp;amp;};:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"This is a fork bomb — it will crash your system."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"chmod -R 777 /"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"This removes all security from your filesystem."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;critical&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;input_lower&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nn"&gt;SafetyLevel&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Danger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;warnings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sudo rm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"This runs a deletion command as administrator."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sudo chmod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"This changes file permissions as administrator."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kill -9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"This forcefully terminates a process."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pkill"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"This kills processes by name."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;warnings&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;input_lower&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nn"&gt;SafetyLevel&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nn"&gt;SafetyLevel&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Safe&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Layer 2: Different UI for each level
&lt;/h2&gt;

&lt;p&gt;Danger → block entirely. Warning → user decides. Safe → proceed silently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SafetyBanner&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SafetyResult&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;danger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;


        &lt;span class="err"&gt;⚠️&lt;/span&gt; &lt;span class="nx"&gt;危険なコマンドが含まれています&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;



&lt;span class="nx"&gt;AIへの送信をブロックしました&lt;/span&gt;&lt;span class="err"&gt;。&lt;/span&gt;




    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;


        &lt;span class="err"&gt;🔶&lt;/span&gt; &lt;span class="nx"&gt;注意&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;表示する&lt;/span&gt;


    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Layer 3: sudo gets automatic explanation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;detect_mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DiagnosisMode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;trimmed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;.trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="nf"&gt;.starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sudo "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'\n'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nn"&gt;DiagnosisMode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CommandExplain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;looks_like_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nn"&gt;DiagnosisMode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ErrorDiagnosis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nn"&gt;DiagnosisMode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CommandExplain&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sudo systemctl restart nginx&lt;/code&gt; → explains what it does, warns about administrator privileges.&lt;/p&gt;




&lt;p&gt;HiyokoHelper (OSS) → github.com/hiyoyok/HiyokoHelper&lt;br&gt;
X → &lt;a class="mentioned-user" href="https://dev.to/hiyoyok"&gt;@hiyoyok&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gemini</category>
      <category>ai</category>
      <category>rust</category>
      <category>security</category>
    </item>
    <item>
      <title>Offline-First Architecture in a Tauri App — What It Actually Means</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Wed, 06 May 2026 17:09:11 +0000</pubDate>
      <link>https://dev.to/hiyoyok/offline-first-architecture-in-a-tauri-app-what-it-actually-means-4gkj</link>
      <guid>https://dev.to/hiyoyok/offline-first-architecture-in-a-tauri-app-what-it-actually-means-4gkj</guid>
      <description>&lt;p&gt;All tests run on an 8-year-old MacBook Air.&lt;br&gt;
All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.&lt;br&gt;
"Offline-first" gets thrown around a lot. For a Tauri desktop app, it has a specific meaning — and it's not complicated.&lt;br&gt;
Here's what I mean when I say my apps are offline-first, and how I built it.&lt;/p&gt;

&lt;p&gt;What offline-first means for a desktop app&lt;br&gt;
A desktop app is already local by default. The question is what happens when it needs external resources — AI APIs, sync endpoints, update servers.&lt;br&gt;
Offline-first means: the app is fully functional without any network connection. Network access enhances the experience; it doesn't enable it.&lt;br&gt;
For my apps:&lt;/p&gt;

&lt;p&gt;HiyokoAutoSync: syncs files when ADB device is connected. No internet required.&lt;br&gt;
PDF Vault: all PDF operations run locally. Gemini OCR is optional enhancement.&lt;br&gt;
HiyokoHelper: all history, caching, and UI runs locally. AI analysis requires network.&lt;/p&gt;

&lt;p&gt;The distinction: required vs optional network.&lt;/p&gt;

&lt;p&gt;The practical implementation&lt;br&gt;
Local-first data. Everything goes to SQLite first. Network sync (if any) happens after.&lt;br&gt;
rustasync fn process_action(input: Input) -&amp;gt; Result {&lt;br&gt;
    // Write to local DB immediately&lt;br&gt;
    db.save(&amp;amp;input).await?;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Try network enhancement — fail gracefully
match enhance_with_api(&amp;amp;input).await {
    Ok(enhanced) =&amp;gt; Ok(enhanced),
    Err(_) =&amp;gt; Ok(Output::from_local(&amp;amp;input)), // degrade gracefully
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Graceful degradation for AI features. If Gemini is unavailable, show the raw data. Don't block the UI waiting for a network response.&lt;br&gt;
No blocking network calls on the hot path. The user's primary workflow should never wait for the network. Background sync, optional enrichment — yes. Mandatory network call before the user can do anything — no.&lt;/p&gt;

&lt;p&gt;Why it matters for desktop apps&lt;br&gt;
Desktop apps get used in planes, conferences, basements, rural areas. Users expect desktop software to work without internet. Web app expectations don't apply.&lt;br&gt;
More practically: a flaky network call that hangs the UI is the fastest way to get a bad review.&lt;/p&gt;

&lt;p&gt;The one exception&lt;br&gt;
Update checks and license validation. These require network by definition. Handle them gracefully: check in the background, don't block launch, degrade to last-known state on network failure.&lt;/p&gt;

&lt;p&gt;If this was useful, a ❤️ helps more than you'd think — thanks!&lt;br&gt;
Hiyoko PDF Vault → &lt;a href="https://hiyokoko.gumroad.com/l/HiyokoPDFVault" rel="noopener noreferrer"&gt;https://hiyokoko.gumroad.com/l/HiyokoPDFVault&lt;/a&gt;&lt;br&gt;
X → &lt;a class="mentioned-user" href="https://dev.to/hiyoyok"&gt;@hiyoyok&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tauri</category>
      <category>rust</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I Price My Indie Mac Apps — The Thinking Behind $7, $39, and $50</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Wed, 06 May 2026 15:33:11 +0000</pubDate>
      <link>https://dev.to/hiyoyok/how-i-price-my-indie-mac-apps-the-thinking-behind-7-39-and-50-4bpk</link>
      <guid>https://dev.to/hiyoyok/how-i-price-my-indie-mac-apps-the-thinking-behind-7-39-and-50-4bpk</guid>
      <description>&lt;p&gt;If this is useful, a ❤️ helps others find it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All tests run on an 8-year-old MacBook Air.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I have apps priced at $7, $20, $29, $39, and $50. Each price reflects a different decision. Here's the thinking.&lt;/p&gt;




&lt;h2&gt;
  
  
  The framework I use
&lt;/h2&gt;

&lt;p&gt;Two questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who is the buyer?&lt;/strong&gt; Consumer (buying for personal use, price-sensitive) vs. professional (buying for work, outcome-focused).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What pain does it solve?&lt;/strong&gt; Occasional convenience vs. recurring time savings vs. risk reduction.&lt;/p&gt;




&lt;h2&gt;
  
  
  $7 — HiyokoShot (screenshot transfer)
&lt;/h2&gt;

&lt;p&gt;Consumer buyer. Occasional use. Solves a mild inconvenience (getting screenshots from Android to Mac).&lt;/p&gt;

&lt;p&gt;At $7, the decision is instant. No comparison shopping, no deliberation. The friction of evaluation costs more than the price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; If the buyer will use the app once a week or less, keep it under $10.&lt;/p&gt;




&lt;h2&gt;
  
  
  $39 — HiyokoBar (menubar tool)
&lt;/h2&gt;

&lt;p&gt;Mixed buyer — some consumers, some professionals. Daily use. Saves 5-10 minutes per day for people who use it seriously.&lt;/p&gt;

&lt;p&gt;At $39, it's in "considered purchase" territory — buyers spend 2-3 minutes evaluating. The sales page needs to be clear about the value proposition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; Daily-use tools for a mixed audience price between $20-50.&lt;/p&gt;




&lt;h2&gt;
  
  
  $50 — HiyokoAutoSync (zero-touch Android sync)
&lt;/h2&gt;

&lt;p&gt;Professional buyer. Solves a real workflow problem (automatic sync without thinking). Time savings compound daily.&lt;/p&gt;

&lt;p&gt;At $50, buyers are outcome-focused. They're not asking "is $50 a lot?" — they're asking "does this solve my problem?" The price signals quality and commitment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; If the app replaces a workflow that currently takes manual effort daily, $50+ is defensible.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I got wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Starting too low.&lt;/strong&gt; My first app launched at $9. After 50 sales I raised it to $19. Conversion rate barely changed. The $9 price was anchoring expectations low without driving meaningfully more sales.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Underestimating professional buyers.&lt;/strong&gt; I assumed everyone was price-sensitive. Some buyers emailed asking if there was a "pro" tier. There wasn't. Left money on the table.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Odd prices ($39 not $40, $7 not $8) perform slightly better — no strong evidence why, but it's consistent in my data&lt;/li&gt;
&lt;li&gt;Offering a permanent license (not subscription) is a strong selling point for Mac utility apps — buyers are tired of subscriptions&lt;/li&gt;
&lt;li&gt;"One-time purchase" in the product title or description noticeably improves conversion for this audience&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Hiyoko PDF Vault → &lt;a href="https://hiyokoko.gumroad.com/l/HiyokoPDFVault" rel="noopener noreferrer"&gt;https://hiyokoko.gumroad.com/l/HiyokoPDFVault&lt;/a&gt;&lt;br&gt;
X → &lt;a class="mentioned-user" href="https://dev.to/hiyoyok"&gt;@hiyoyok&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>product</category>
      <category>rust</category>
      <category>tauri</category>
    </item>
    <item>
      <title>What I Learned Building HiyokoBar — A Menubar App That Does One Thing Per Click</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Wed, 06 May 2026 14:30:05 +0000</pubDate>
      <link>https://dev.to/hiyoyok/what-i-learned-building-hiyokobar-a-menubar-app-that-does-one-thing-per-click-2a4h</link>
      <guid>https://dev.to/hiyoyok/what-i-learned-building-hiyokobar-a-menubar-app-that-does-one-thing-per-click-2a4h</guid>
      <description>&lt;p&gt;If this is useful, a ❤️ helps others find it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All tests run on an 8-year-old MacBook Air.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;HiyokoBar is a menubar app. Click the icon — panel appears. Do the thing. Click away — panel disappears.&lt;/p&gt;

&lt;p&gt;It sounds trivial. It took longer than expected to get right. Here's what I learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  The constraint that shaped everything
&lt;/h2&gt;

&lt;p&gt;A menubar panel has maybe 400px of vertical space. That's it.&lt;/p&gt;

&lt;p&gt;This constraint forced decisions I wouldn't have made otherwise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every feature had to earn its place. No "maybe someone will want this" features.&lt;/li&gt;
&lt;li&gt;Each action had to complete in one click or one step. Two-step actions don't belong in a menubar panel.&lt;/li&gt;
&lt;li&gt;Visual hierarchy matters more than in a full window — users scan, not read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Constraints produce clarity. The limited space was the best design tool I had.&lt;/p&gt;




&lt;h2&gt;
  
  
  The technical decisions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Activation policy:&lt;/strong&gt; &lt;code&gt;accessory&lt;/code&gt; — no Dock icon, no Cmd+Tab entry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Panel positioning:&lt;/strong&gt; calculate from tray icon position on every click — the user might have moved it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Focus behavior:&lt;/strong&gt; hide on blur, but suppress blur-hiding during native dialogs. Took 3 iterations to get right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Launch at login:&lt;/strong&gt; LaunchAgent plist, not SMAppService — better compatibility with older macOS versions.&lt;/p&gt;




&lt;h2&gt;
  
  
  What users actually use
&lt;/h2&gt;

&lt;p&gt;I added analytics (opt-in, local only) to see which features got tapped.&lt;/p&gt;

&lt;p&gt;The top 3 features account for 80% of all interactions. The bottom 5 features combined account for under 5%.&lt;/p&gt;

&lt;p&gt;I removed two features entirely after seeing the data. The app got better immediately — less to scan, less to understand, faster to use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; ship with more features than you think users need. Then watch what they actually use. Then remove everything else.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Product Hunt launch
&lt;/h2&gt;

&lt;p&gt;HiyokoBar launched on Product Hunt on April 20, 2026.&lt;/p&gt;

&lt;p&gt;Traffic spike: yes. Sustained sales from it: modest. The real value was the comments and feedback — several feature ideas came directly from PH discussions that I'd never have thought of.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My view on PH:&lt;/strong&gt; worth doing once per app for the feedback loop, not for the traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one thing that surprised me
&lt;/h2&gt;

&lt;p&gt;The users who emailed me feedback. Not bug reports — genuine "here's what this does for my workflow" messages.&lt;/p&gt;

&lt;p&gt;Menubar apps attract a specific kind of user: people who care about their tools, who customize their environment, who notice when something is done right. These are the best users to have.&lt;/p&gt;

&lt;p&gt;Build for them.&lt;/p&gt;




&lt;p&gt;Hiyoko PDF Vault → &lt;a href="https://hiyokoko.gumroad.com/l/HiyokoPDFVault" rel="noopener noreferrer"&gt;https://hiyokoko.gumroad.com/l/HiyokoPDFVault&lt;/a&gt;&lt;br&gt;
HiyokoBar → &lt;a href="https://hiyokoko.gumroad.com/l/hiyokobar" rel="noopener noreferrer"&gt;https://hiyokoko.gumroad.com/l/hiyokobar&lt;/a&gt;&lt;br&gt;
X → &lt;a class="mentioned-user" href="https://dev.to/hiyoyok"&gt;@hiyoyok&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tauri</category>
      <category>rust</category>
      <category>programming</category>
      <category>product</category>
    </item>
    <item>
      <title>Bates Numbering in Rust — Automating Legal Document Stamping</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Wed, 06 May 2026 13:20:00 +0000</pubDate>
      <link>https://dev.to/hiyoyok/bates-numbering-in-rust-automating-legal-document-stamping-4080</link>
      <guid>https://dev.to/hiyoyok/bates-numbering-in-rust-automating-legal-document-stamping-4080</guid>
      <description>&lt;p&gt;All tests run on an 8-year-old MacBook Air.&lt;br&gt;
All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.&lt;br&gt;
Bates numbering is sequential page stamping used in legal documents. Every page gets a unique identifier: CASE-001, CASE-002, etc.&lt;br&gt;
I built this into Hiyoko PDF Vault. Here's how it works in Rust.&lt;/p&gt;

&lt;p&gt;What Bates numbering actually is&lt;br&gt;
A Bates stamp is a text label added to a fixed position on each page — usually bottom-right or bottom-left. The label increments sequentially across a document set.&lt;br&gt;
Format: [PREFIX][NUMBER][SUFFIX] where number is zero-padded to a fixed width.&lt;br&gt;
Examples: SMITH-000001, EXHIBIT_A_0042, DOC00100&lt;/p&gt;

&lt;p&gt;The implementation with lopdf&lt;br&gt;
rustuse lopdf::{Document, Object, Stream, Dictionary, content::Content};&lt;/p&gt;

&lt;p&gt;pub struct BatesConfig {&lt;br&gt;
    pub prefix: String,&lt;br&gt;
    pub suffix: String,&lt;br&gt;
    pub start_number: u64,&lt;br&gt;
    pub pad_width: usize,&lt;br&gt;
    pub position: BatesPosition,&lt;br&gt;
    pub font_size: f32,&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;pub enum BatesPosition {&lt;br&gt;
    BottomRight,&lt;br&gt;
    BottomLeft,&lt;br&gt;
    TopRight,&lt;br&gt;
    TopLeft,&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;pub fn apply_bates(doc: &amp;amp;mut Document, config: &amp;amp;BatesConfig) -&amp;gt; Result&amp;lt;(), AppError&amp;gt; {&lt;br&gt;
    let page_ids: Vec&amp;lt;_&amp;gt; = doc.page_iter().collect();&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (i, page_id) in page_ids.iter().enumerate() {
    let number = config.start_number + i as u64;
    let label = format!(
        "{}{}{}",
        config.prefix,
        format!("{:0&amp;gt;width$}", number, width = config.pad_width),
        config.suffix
    );

    stamp_page(doc, *page_id, &amp;amp;label, &amp;amp;config)?;
}

Ok(())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Stamping a page&lt;br&gt;
Adding text to a PDF page requires appending to its content stream:&lt;br&gt;
rustfn stamp_page(&lt;br&gt;
    doc: &amp;amp;mut Document,&lt;br&gt;
    page_id: (u32, u16),&lt;br&gt;
    label: &amp;amp;str,&lt;br&gt;
    config: &amp;amp;BatesConfig,&lt;br&gt;
) -&amp;gt; Result&amp;lt;(), AppError&amp;gt; {&lt;br&gt;
    let (x, y) = calculate_position(doc, page_id, &amp;amp;config.position)?;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let stamp_content = format!(
    "BT /F1 {} Tf {} {} Td ({}) Tj ET",
    config.font_size, x, y, label
);

// Append to existing page content
// Ensure font is available in page resources
append_content_to_page(doc, page_id, &amp;amp;stamp_content)?;

Ok(())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;The font dependency&lt;br&gt;
PDF text rendering requires a font reference in the page's resource dictionary. If the page doesn't already have a suitable font, you need to embed one or reference a standard PDF font (Helvetica, Times, Courier — guaranteed to be available in any PDF viewer).&lt;br&gt;
For Bates stamps, Helvetica works fine and requires no font embedding.&lt;/p&gt;

&lt;p&gt;Batch processing&lt;br&gt;
The real use case is stamping hundreds of pages across multiple documents. Process sequentially with progress events back to the frontend. Don't try to parallelize PDF mutation — document state is not thread-safe with lopdf's mutable references.&lt;/p&gt;

&lt;p&gt;If this was useful, a ❤️ helps more than you'd think — thanks!&lt;br&gt;
Hiyoko PDF Vault → &lt;a href="https://hiyokoko.gumroad.com/l/HiyokoPDFVault" rel="noopener noreferrer"&gt;https://hiyokoko.gumroad.com/l/HiyokoPDFVault&lt;/a&gt;&lt;br&gt;
X → &lt;a class="mentioned-user" href="https://dev.to/hiyoyok"&gt;@hiyoyok&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tarui</category>
      <category>rust</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>PDF Redaction in Rust — Why "Delete the Text" Isn't Enough</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Wed, 06 May 2026 02:18:52 +0000</pubDate>
      <link>https://dev.to/hiyoyok/pdf-redaction-in-rust-why-delete-the-text-isnt-enough-2bof</link>
      <guid>https://dev.to/hiyoyok/pdf-redaction-in-rust-why-delete-the-text-isnt-enough-2bof</guid>
      <description>&lt;p&gt;All tests run on an 8-year-old MacBook Air.&lt;br&gt;
All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.&lt;br&gt;
Real PDF redaction is harder than it looks. The naive approach — draw a black rectangle over text — doesn't actually remove the text from the file.&lt;br&gt;
Here's what proper redaction requires.&lt;/p&gt;

&lt;p&gt;The problem with naive redaction&lt;br&gt;
A PDF with a black rectangle drawn over sensitive text still contains that text in the file structure. Anyone with a PDF editor can remove the rectangle and read the original content.&lt;br&gt;
This has caused real security incidents. Legal documents, medical records, government reports — all leaked because someone drew a box over text and called it redacted.&lt;/p&gt;

&lt;p&gt;What actual redaction requires&lt;/p&gt;

&lt;p&gt;Identify the content to redact (text, images, or regions)&lt;br&gt;
Remove the actual content from the PDF's content streams&lt;br&gt;
Replace with a filled rectangle&lt;br&gt;
Remove any references in the document structure&lt;br&gt;
Rebuild the PDF without the redacted content in the object stream&lt;/p&gt;

&lt;p&gt;Step 2 is where naive implementations fail. Removing visible rendering is not the same as removing the data.&lt;/p&gt;

&lt;p&gt;The lopdf approach&lt;br&gt;
With lopdf, you're working directly with PDF objects. Redaction means modifying content streams:&lt;br&gt;
rustfn redact_text_in_stream(content: &amp;amp;[u8], target: &amp;amp;str) -&amp;gt; Vec {&lt;br&gt;
    // Parse PDF content stream operations&lt;br&gt;
    // Find text rendering operations containing target&lt;br&gt;
    // Replace text content with spaces or remove operations&lt;br&gt;
    // Rebuild content stream&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// This is genuinely complex — PDF content streams
// interleave text positioning and rendering commands
todo!("non-trivial implementation")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
PDF content streams aren't plain text. They're a sequence of operators and operands. Text appears across multiple operators: font selection, positioning, encoding, rendering. A complete redaction implementation needs to parse all of these.&lt;/p&gt;

&lt;p&gt;What I ship in PDF Vault&lt;br&gt;
Hiyoko PDF Vault implements region-based redaction: the user selects a region, we remove all content operations that render within that region, then fill with a solid rectangle.&lt;br&gt;
It's not forensic-grade redaction. It removes content from the file structure rather than just drawing over it. For the use case — personal documents, not classified government files — it's appropriate.&lt;br&gt;
For truly sensitive documents requiring certified redaction, professional tools with documented audit trails are the right choice. I'm honest about this in the app description.&lt;/p&gt;

&lt;p&gt;The verdict&lt;br&gt;
True PDF redaction is a solved problem in professional tools. In a Rust implementation, it's achievable but requires careful PDF content stream parsing. The naive approach (draw a rectangle) should never be called redaction.&lt;br&gt;
Know what level of redaction your users actually need before deciding how to implement it.&lt;/p&gt;

&lt;p&gt;If this was useful, a ❤️ helps more than you'd think — thanks!&lt;br&gt;
Hiyoko PDF Vault → &lt;a href="https://hiyokoko.gumroad.com/l/HiyokoPDFVault" rel="noopener noreferrer"&gt;https://hiyokoko.gumroad.com/l/HiyokoPDFVault&lt;/a&gt;&lt;br&gt;
X → &lt;a class="mentioned-user" href="https://dev.to/hiyoyok"&gt;@hiyoyok&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tauri</category>
      <category>rust</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Selling Mac Apps on Gumroad — What Works, What Doesn't, Honest Numbers</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Wed, 06 May 2026 01:09:39 +0000</pubDate>
      <link>https://dev.to/hiyoyok/selling-mac-apps-on-gumroad-what-works-what-doesnt-honest-numbers-3f0m</link>
      <guid>https://dev.to/hiyoyok/selling-mac-apps-on-gumroad-what-works-what-doesnt-honest-numbers-3f0m</guid>
      <description>&lt;p&gt;If this is useful, a ❤️ helps others find it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All tests run on an 8-year-old MacBook Air.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I sell Mac apps on Gumroad. Not the App Store — Gumroad, direct to buyers.&lt;/p&gt;

&lt;p&gt;Here's the honest account: what the platform is like, what drives sales, and what I'd do differently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Gumroad over the App Store
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No code signing requirement.&lt;/strong&gt; Apple Developer Program costs $99/year. Before you've made $99, that's a high bar. Gumroad accepts unsigned apps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No 30% cut.&lt;/strong&gt; Gumroad takes 10% (or less with volume). Apple takes 30%. On a $30 app, that's $3 vs $9 per sale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No review process.&lt;/strong&gt; Ship when it's ready. Update when you want. No waiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; users see a "cannot verify developer" dialog on first launch. Right-click → Open bypasses it. Some users won't do this. It costs conversions — I'd estimate 15-20% drop-off at that step.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually drives sales
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Technical articles.&lt;/strong&gt; Every spike in sales correlates with a published article. Not product announcements — articles about how I built something. "Adobe税を払うのをやめた" (I stopped paying the Adobe tax) outperformed every product post I've written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specific problem, specific solution.&lt;/strong&gt; "PDF tool" is too broad. "Remove metadata from PDFs before sharing" is a specific problem someone is actively searching for. The more specific the problem the app solves, the easier the sales page writes itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price anchoring.&lt;/strong&gt; Listing a Japanese version and English version separately at slightly different prices helps. The existence of two options makes choosing feel like a decision rather than a yes/no on buying.&lt;/p&gt;




&lt;h2&gt;
  
  
  What doesn't drive sales
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Product Hunt launches.&lt;/strong&gt; One-day spike, then nothing. Useful for social proof and backlinks, not for sustained revenue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generic social media posting.&lt;/strong&gt; "Check out my new app!" gets ignored. Technical content with a mention of the app at the end works better.&lt;/p&gt;




&lt;h2&gt;
  
  
  The numbers (directionally)
&lt;/h2&gt;

&lt;p&gt;Articles that perform well drive 5-15 sales in the following week. Product announcements drive 0-3. The difference in effort between writing a good technical article and a product announcement is small. The difference in result is large.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Gumroad tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Offer both a Japanese and English listing for Japan-market apps — different buyers find different listings&lt;/li&gt;
&lt;li&gt;Include a clear "how to open on first launch" note in the product description (for the security dialog)&lt;/li&gt;
&lt;li&gt;Respond to every support email — Gumroad shows buyer reviews and support responsiveness matters&lt;/li&gt;
&lt;li&gt;Use the "pay what you want" minimum for free tools to collect emails&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Hiyoko PDF Vault → &lt;a href="https://hiyokoko.gumroad.com/l/HiyokoPDFVault" rel="noopener noreferrer"&gt;https://hiyokoko.gumroad.com/l/HiyokoPDFVault&lt;/a&gt;&lt;br&gt;
X → &lt;a class="mentioned-user" href="https://dev.to/hiyoyok"&gt;@hiyoyok&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>product</category>
      <category>tauri</category>
      <category>rust</category>
    </item>
    <item>
      <title>Rust Async Patterns in Tauri — Keeping the UI Responsive While Rust Does Heavy Work</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Tue, 05 May 2026 16:35:14 +0000</pubDate>
      <link>https://dev.to/hiyoyok/rust-async-patterns-in-tauri-keeping-the-ui-responsive-while-rust-does-heavy-work-e9d</link>
      <guid>https://dev.to/hiyoyok/rust-async-patterns-in-tauri-keeping-the-ui-responsive-while-rust-does-heavy-work-e9d</guid>
      <description>&lt;p&gt;If this is useful, a ❤️ helps others find it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All tests run on an 8-year-old MacBook Air.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A Tauri app has two threads that matter: the main thread (UI) and whatever tokio spawns. Block the main thread and the UI freezes. Block for too long in a command and the frontend times out.&lt;/p&gt;

&lt;p&gt;Here's how I keep things responsive in practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  The basic rule
&lt;/h2&gt;

&lt;p&gt;Never do blocking work in a &lt;code&gt;#[tauri::command]&lt;/code&gt; without &lt;code&gt;async&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad — blocks the thread pool&lt;/span&gt;
&lt;span class="nd"&gt;#[tauri::command]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;compress_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;heavy_compression_work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// takes 3 seconds, blocks&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Good — async, non-blocking&lt;/span&gt;
&lt;span class="nd"&gt;#[tauri::command]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;compress_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;task&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn_blocking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;heavy_compression_work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;.await&lt;/span&gt;
    &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
    &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;spawn_blocking&lt;/code&gt; moves CPU-heavy work to a dedicated thread pool, freeing the async executor for other tasks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Progress reporting during long operations
&lt;/h2&gt;

&lt;p&gt;For operations that take more than a second, report progress via events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tauri::command]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;batch_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.enumerate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;process_single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="nf"&gt;.emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"batch-progress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="s"&gt;"current"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}))&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Frontend shows live progress&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;batch-progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;batch_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;paths&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Cancellation
&lt;/h2&gt;

&lt;p&gt;Users cancel long operations. Support it with a shared flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;AtomicBool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;}};&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nf"&gt;CancelToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;CancelToken&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;AtomicBool&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="nf"&gt;.store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Relaxed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;is_cancelled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="nf"&gt;.load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Relaxed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[tauri::command]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;batch_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cancel_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancelToken&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.enumerate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cancel_token&lt;/span&gt;&lt;span class="nf"&gt;.is_cancelled&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cancelled"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;process_single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="nf"&gt;.emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"batch-progress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cancel button&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cancel_batch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Parallel processing with Semaphore
&lt;/h2&gt;

&lt;p&gt;Process multiple files concurrently, but not all at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;process_parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;semaphore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// max 4 concurrent&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;handles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_permit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="nf"&gt;.acquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nf"&gt;process_single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="n"&gt;handles&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nn"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;future&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;join_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;
        &lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or_else&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())))&lt;/span&gt;
        &lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4 concurrent transfers on a 2017 MacBook Air runs well without saturating the disk or CPU.&lt;/p&gt;




&lt;p&gt;Hiyoko PDF Vault → &lt;a href="https://hiyokoko.gumroad.com/l/HiyokoPDFVault" rel="noopener noreferrer"&gt;https://hiyokoko.gumroad.com/l/HiyokoPDFVault&lt;/a&gt;&lt;br&gt;
X → &lt;a class="mentioned-user" href="https://dev.to/hiyoyok"&gt;@hiyoyok&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>programming</category>
    </item>
    <item>
      <title>Rust Error Handling Patterns I Actually Use in Production</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Tue, 05 May 2026 14:45:20 +0000</pubDate>
      <link>https://dev.to/hiyoyok/rust-error-handling-patterns-i-actually-use-in-production-efl</link>
      <guid>https://dev.to/hiyoyok/rust-error-handling-patterns-i-actually-use-in-production-efl</guid>
      <description>&lt;p&gt;If this is useful, a ❤️ helps others find it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All tests run on an 8-year-old MacBook Air.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every Rust tutorial covers &lt;code&gt;Result&lt;/code&gt; and &lt;code&gt;?&lt;/code&gt;. Few cover what to actually do when you have 5 different error types flying around a real application.&lt;/p&gt;

&lt;p&gt;Here's what I settled on after shipping multiple Tauri apps.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: heterogeneous errors
&lt;/h2&gt;

&lt;p&gt;A PDF processing command might fail due to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IO error (file not found)&lt;/li&gt;
&lt;li&gt;lopdf parse error (malformed PDF)&lt;/li&gt;
&lt;li&gt;Encryption error (wrong key)&lt;/li&gt;
&lt;li&gt;Swift sidecar error (process failed)&lt;/li&gt;
&lt;li&gt;Serialization error (JSON parse)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Returning &lt;code&gt;Box&lt;/code&gt; works but loses type information. Returning &lt;code&gt;String&lt;/code&gt; works but is unstructured. Neither is great.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 1: domain error enum
&lt;/h2&gt;

&lt;p&gt;Define one error type per domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;thiserror::Error)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;PdfError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"file not found: {path}"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;NotFound&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"malformed PDF: {reason}"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;ParseError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"decryption failed — wrong password or corrupted file"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;DecryptionFailed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"sidecar process failed: {stderr}"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;SidecarError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"IO error: {0}"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="nf"&gt;Io&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;#[from]&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;io&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;thiserror&lt;/code&gt; generates the &lt;code&gt;Display&lt;/code&gt; and &lt;code&gt;Error&lt;/code&gt; impls. &lt;code&gt;#[from]&lt;/code&gt; generates &lt;code&gt;From&lt;/code&gt; automatically — &lt;code&gt;?&lt;/code&gt; just works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 2: convert at the boundary
&lt;/h2&gt;

&lt;p&gt;Tauri commands need &lt;code&gt;Result&lt;/code&gt; for the frontend. Convert at the command boundary, not inside business logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Business logic — rich error types&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;process_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Tauri command — converts at boundary&lt;/span&gt;
&lt;span class="nd"&gt;#[tauri::command]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;process_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend gets a human-readable error string. The backend keeps structured errors for matching and logging.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 3: context with anyhow
&lt;/h2&gt;

&lt;p&gt;For scripts and one-off tools where rich error types aren't worth the boilerplate, &lt;code&gt;anyhow&lt;/code&gt; adds context without ceremony:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;anyhow&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;load_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;read_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.with_context&lt;/span&gt;&lt;span class="p"&gt;(||&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to read config from {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.with_context&lt;/span&gt;&lt;span class="p"&gt;(||&lt;/span&gt; &lt;span class="s"&gt;"config file is not valid JSON"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Error messages chain automatically: "config file is not valid JSON: unexpected character at line 3"&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 4: never panic in library code
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;unwrap()&lt;/code&gt; and &lt;code&gt;expect()&lt;/code&gt; in library functions crash the whole app. In a long-running desktop app, a crash from a malformed PDF file is unacceptable.&lt;/p&gt;

&lt;p&gt;Rule: &lt;code&gt;unwrap()&lt;/code&gt; only in &lt;code&gt;main()&lt;/code&gt; for setup that must succeed, or in tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad — panics if PDF is malformed&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Good — surfaces error to caller&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;PdfError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ParseError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I actually use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;thiserror&lt;/code&gt; for domain error enums in library code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;anyhow&lt;/code&gt; for scripts and one-off tools&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.map_err(|e| e.to_string())&lt;/code&gt; at Tauri command boundaries&lt;/li&gt;
&lt;li&gt;Never &lt;code&gt;unwrap()&lt;/code&gt; in non-test, non-main code&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Hiyoko PDF Vault → &lt;a href="https://hiyokoko.gumroad.com/l/HiyokoPDFVault" rel="noopener noreferrer"&gt;https://hiyokoko.gumroad.com/l/HiyokoPDFVault&lt;/a&gt;&lt;br&gt;
X → &lt;a class="mentioned-user" href="https://dev.to/hiyoyok"&gt;@hiyoyok&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>programming</category>
      <category>product</category>
    </item>
  </channel>
</rss>
