<?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: Ishan Gautam</title>
    <description>The latest articles on DEV Community by Ishan Gautam (@ishan_gautam).</description>
    <link>https://dev.to/ishan_gautam</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4209752%2F3c518215-3bf3-4306-b3ed-7f8e4ea5eb07.jpg</url>
      <title>DEV Community: Ishan Gautam</title>
      <link>https://dev.to/ishan_gautam</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ishan_gautam"/>
    <language>en</language>
    <item>
      <title>46 of the top 925 GitHub repos pass Actions inputs that do nothing</title>
      <dc:creator>Ishan Gautam</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:45:10 +0000</pubDate>
      <link>https://dev.to/ishan_gautam/46-of-the-top-925-github-repos-pass-actions-inputs-that-do-nothing-3e88</link>
      <guid>https://dev.to/ishan_gautam/46-of-the-top-925-github-repos-pass-actions-inputs-that-do-nothing-3e88</guid>
      <description>&lt;p&gt;GitHub Actions has a quiet failure mode. If a workflow passes an input the action doesn't declare, the run doesn't fail. The log gets an &lt;code&gt;Unexpected input(s)&lt;/code&gt; warning, the step goes green, and the value is ignored, unless the action happens to read undeclared inputs, which few do.&lt;/p&gt;

&lt;p&gt;I wanted to know how often that happens in real projects, so I scanned the workflows of the 925 most-starred public repositories on GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repositories scanned&lt;/td&gt;
&lt;td&gt;925&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repositories with workflows&lt;/td&gt;
&lt;td&gt;751&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workflow files&lt;/td&gt;
&lt;td&gt;8,793&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;with:&lt;/code&gt; inputs checked&lt;/td&gt;
&lt;td&gt;102,293&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inputs the action doesn't declare&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;113&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repositories with at least one&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;46 (5%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Per input, that's rare: about 1 in 900. Per project, it's common: about 1 in 20 of these well-maintained repositories has at least one setting in CI that does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What they look like
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Typos (20).&lt;/strong&gt; &lt;code&gt;cached: true&lt;/code&gt; for an action whose input is &lt;code&gt;cache&lt;/code&gt;. &lt;code&gt;issue-lock-inactive-days&lt;/code&gt; where the action expects &lt;code&gt;issue-inactive-days&lt;/code&gt;. &lt;code&gt;file&lt;/code&gt; where a coverage action now takes &lt;code&gt;files&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newer than the pinned version (23).&lt;/strong&gt; The input exists on the action's main branch, but the workflow pins an older SHA or tag that predates it. One example is &lt;code&gt;include-hidden-files&lt;/code&gt; on an &lt;code&gt;actions/upload-artifact&lt;/code&gt; pin from before that input existed. The setting will start working the day someone bumps the pin, and not before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gone or never there (70).&lt;/strong&gt; The pinned version doesn't declare these inputs, and there is no close match. Some of them are intentional: &lt;code&gt;octokit/request-action&lt;/code&gt; forwards arbitrary inputs to the GitHub API, so its undeclared inputs do work. Leaving that action out, it's 95 inputs in 44 repositories.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;20 findings were re-checked one at a time against the exact &lt;code&gt;action.yml&lt;/code&gt; at the exact pinned ref, and all 20 held up. An earlier run reported 622. That was a caching bug in the scanner, which shared local &lt;code&gt;./path&lt;/code&gt; actions between repositories. It's worth saying, because a number that big would have been wrong.&lt;/p&gt;

&lt;p&gt;The method, data and scanner are in &lt;a href="https://github.com/awesome-proof-of-effect/proof-of-effect/tree/main/research/actions-inputs-2026-10" rel="noopener noreferrer"&gt;research/actions-inputs-2026-10&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your own repo
&lt;/h2&gt;

&lt;p&gt;The scan uses the GitHub Actions check from proof-of-effect, a small open-source checker. It needs Python 3.9+ and git, nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/awesome-proof-of-effect/proof-of-effect
python3 proof-of-effect/skills/proof-of-effect/scripts/poe.py check &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--repo&lt;/span&gt; /path/to/your/repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--all&lt;/code&gt; audits everything already in the repo. Without it, the checker looks only at what changed since &lt;code&gt;HEAD&lt;/code&gt; (or since &lt;code&gt;--base origin/main&lt;/code&gt;), which is the mode meant for pull requests and coding agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it exists
&lt;/h2&gt;

&lt;p&gt;The same failure shows up everywhere there is config, and coding agents hit it constantly: they "fix" something by adding a setting the tool quietly ignores. Some examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;strict: true&lt;/code&gt; at the top level of &lt;code&gt;tsconfig.json&lt;/code&gt; instead of inside &lt;code&gt;compilerOptions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;an &lt;code&gt;.eslintrc&lt;/code&gt; edit while ESLint runs in flat-config mode&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;[tool.pytest.ini_options]&lt;/code&gt; change while pytest actually reads &lt;code&gt;pytest.ini&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;an env var without the &lt;code&gt;VITE_&lt;/code&gt; prefix the browser build needs&lt;/li&gt;
&lt;li&gt;a CLI flag the installed version doesn't have&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CI is green in every one of these cases.&lt;/p&gt;

&lt;p&gt;proof-of-effect asks the tool itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For tsc, ESLint, Jest, Ruff and pytest, it resolves the config with the old and the new version of the changed file and compares the two.&lt;/li&gt;
&lt;li&gt;For GitHub Actions, it checks inputs against &lt;code&gt;action.yml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For env vars, it checks the code that reads them.&lt;/li&gt;
&lt;li&gt;For CLI flags, it checks the installed tool's &lt;code&gt;--help&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It ships as a &lt;code&gt;SKILL.md&lt;/code&gt; agent skill, so an agent runs it before claiming a fix. It also works as a plain CLI in CI.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/awesome-proof-of-effect/proof-of-effect" rel="noopener noreferrer"&gt;https://github.com/awesome-proof-of-effect/proof-of-effect&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: the checker, the scan and this write-up were produced with an AI coding agent working in my repository. Every number above comes from the published scan data, and the checks are tested against the real tools in CI.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>devops</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built an open-source dictation app in Tauri and Rust. Here are 5 things that broke.</title>
      <dc:creator>Ishan Gautam</dc:creator>
      <pubDate>Tue, 27 Oct 2026 02:14:02 +0000</pubDate>
      <link>https://dev.to/ishan_gautam/i-built-an-open-source-dictation-app-in-tauri-and-rust-here-are-5-things-that-broke-1b2p</link>
      <guid>https://dev.to/ishan_gautam/i-built-an-open-source-dictation-app-in-tauri-and-rust-here-are-5-things-that-broke-1b2p</guid>
      <description>&lt;p&gt;I type a lot less than I used to. I hold two keys, say what I want, let go, and the text shows up wherever my cursor is. Slack, VS Code, a Gmail draft, a terminal. It works with the Wi-Fi off.&lt;/p&gt;

&lt;p&gt;That app is &lt;strong&gt;Talvio&lt;/strong&gt;. It's open source (MIT), built with Tauri 2, Rust and React, and it runs on Windows and Linux today. A macOS build is coming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ishangautam7/Talvio" rel="noopener noreferrer"&gt;github.com/ishangautam7/Talvio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyh70d5tnn9ujelmsvz5n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyh70d5tnn9ujelmsvz5n.png" alt="Talvio's home screen next to the phone app" width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post is half "here's what I made" and half "here's what broke". If you build desktop apps with Tauri, the second half is probably the useful part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I wanted dictation that worked in every app, not just in one website's text box. I also didn't want my voice going to a server every time I wrote a Slack message.&lt;/p&gt;

&lt;p&gt;I found &lt;a href="https://github.com/cjpais/Handy" rel="noopener noreferrer"&gt;Handy&lt;/a&gt; by CJ Pais. It's a small, open source, offline speech-to-text app, and it did the core thing well: hold a key, talk, paste. I forked it. The whole local dictation core (the Whisper/Parakeet pipeline, voice activity detection, the recording overlay, the settings architecture) comes from that project, and I still pull fixes from upstream. Go give it a star too.&lt;/p&gt;

&lt;p&gt;Then I kept adding the things I wanted on my own machine. That fork grew into Talvio.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Dictation.&lt;/strong&gt; Hold &lt;code&gt;Ctrl + Win&lt;/code&gt; on Windows (&lt;code&gt;Ctrl + Space&lt;/code&gt; on Linux), talk, let go. A small pill at the bottom of the screen shows it's listening, and with a streaming model you see your words as you speak.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyh5p15o88zess4hc82xh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyh5p15o88zess4hc82xh.png" alt="The recording pill: listening, live text, transcribing" width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Speech recognition runs locally through &lt;a href="https://github.com/handy-computer/transcribe.cpp" rel="noopener noreferrer"&gt;transcribe.cpp&lt;/a&gt;. Whisper, Parakeet, Nemotron and the rest of a 69-model catalog, on your GPU through Vulkan or on the CPU. No account needed. Cloud speech providers are optional and use your own key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An assistant on another shortcut.&lt;/strong&gt; Select some text, hold &lt;code&gt;Ctrl + Alt&lt;/code&gt;, say "write a polite reply saying I can't make Thursday". It answers out loud, and an Insert button puts the answer back where the text was.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Voice commands.&lt;/strong&gt; "Play Believer on YouTube." "Turn the volume down." "Email Ram that I'm late." These run in your own apps: Gmail opens with the email written, the song starts in your browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftz1ufdhfc3uyf4biuszj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftz1ufdhfc3uyf4biuszj.png" alt="The Commands page" width="800" height="553"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meeting notes.&lt;/strong&gt; It records your mic and the system audio as two separate lanes, so it knows which words were yours. No bot joins the call. You get notes at the end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5l1ppp25utzem3fi7ug.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5l1ppp25utzem3fi7ug.png" alt="A meeting being recorded with two lanes" width="800" height="553"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smaller things.&lt;/strong&gt; Optional AI cleanup that removes "um" and follows "at ten, actually no, eleven". A dictionary so names come out spelled right. And an Android app that turns your phone into a wireless mic, keyboard and remote for the PC.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F34wq6sne53zbktionqcv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F34wq6sne53zbktionqcv.png" alt="The phone app" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The AI features (assistant, commands, cleanup) use your own API key, a local model through the built-in llama.cpp engine, or $1 of free credit if you sign in. Plain dictation needs none of that.&lt;/p&gt;

&lt;p&gt;OK, the lessons.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Creating a window from a hotkey thread froze the whole app
&lt;/h2&gt;

&lt;p&gt;The assistant panel opens when you press a global shortcut. The shortcut callback runs on the keyboard hook's thread, not on the main thread.&lt;/p&gt;

&lt;p&gt;My first version built the WebView window right there in the callback. It worked most of the time. Sometimes it deadlocked the event loop, and then everything was dead: no overlay, no dictation, windows you couldn't drag.&lt;/p&gt;

&lt;p&gt;The fix is boring. Every function that creates, shows or destroys that window queues the work on the main thread:&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;enter_conversation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&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;AppHandle&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;app_main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&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;if&lt;/span&gt; &lt;span class="k"&gt;let&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="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.run_on_main_thread&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;apply_panel_geometry&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;app_main&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;place_call_window&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;app_main&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;error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Could not queue conversation panel sizing: {}"&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="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 functions that actually touch the window are private. The public ones only queue. That had a side benefit I didn't plan for: a create is always ordered behind a destroy, so toggling the feature off and on fast can't leave the app thinking a window exists when it doesn't.&lt;/p&gt;

&lt;p&gt;If you call anything window-related from a shortcut handler, a tray handler or a background thread in Tauri, go through &lt;code&gt;run_on_main_thread&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Floating windows must never take focus
&lt;/h2&gt;

&lt;p&gt;A dictation app pastes with a synthetic &lt;code&gt;Ctrl+V&lt;/code&gt;. That keystroke goes to whatever has keyboard focus at that moment. If your own overlay grabbed focus, the transcript gets pasted into your overlay and disappears.&lt;/p&gt;

&lt;p&gt;This happened. The overlay window was bigger than the visible pill (a transparent frame around it), so a click on "empty" space actually landed on my window, activated it, and the next paste went nowhere.&lt;/p&gt;

&lt;p&gt;Each platform needs a different answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows:&lt;/strong&gt; the window gets &lt;code&gt;WS_EX_NOACTIVATE&lt;/code&gt;, so even a click that lands on it can't make it the foreground window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS:&lt;/strong&gt; an &lt;code&gt;NSPanel&lt;/code&gt; that never becomes key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux (Wayland):&lt;/strong&gt; a GTK layer-shell surface with &lt;code&gt;KeyboardMode::None&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain X11:&lt;/strong&gt; none of the above works reliably, so the card there stays click-through until the paste is done.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same rule now applies to every window that pops up on its own: the reminder popup, the "allow this phone?" card, the assistant. Anything that shows up while you are typing somewhere else should not move your cursor.&lt;/p&gt;

&lt;p&gt;One more trap: keeping it on top. &lt;code&gt;WS_EX_TOPMOST&lt;/code&gt; is a position another app can take from you. I set it once on show and users reported the pill "randomly hiding". Now a small guard re-asserts it every 700 ms while the overlay is up, and logs once when something displaced it. That log line turned a vague bug report into a fixable one.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Run the speech engine in a separate process
&lt;/h2&gt;

&lt;p&gt;Local speech recognition on the GPU means running Vulkan compute shaders through whatever driver the user has. Some of those drivers crash. Some hit a &lt;code&gt;GGML_ASSERT&lt;/code&gt; and abort. Some just hang.&lt;/p&gt;

&lt;p&gt;When the engine ran inside the app, any of those took the whole app down, or left it sitting on "Transcribing…" forever.&lt;/p&gt;

&lt;p&gt;Now every call into the speech engine goes to a worker process: the same executable started with &lt;code&gt;--transcribe-worker&lt;/code&gt;. &lt;code&gt;main.rs&lt;/code&gt; checks for that flag before anything else (before Tauri, logging or single-instance), so the worker is just a small loop reading requests from stdin.&lt;/p&gt;

&lt;p&gt;The protocol is plain request/response, one frame per message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[u32 json length][json][u32 pcm length][f32 pcm samples]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten minutes of audio costs about 10 ms to send this way. There's a 1 GiB cap per section so a garbage length can't turn into a huge allocation.&lt;/p&gt;

&lt;p&gt;When a worker using the GPU crashes or hangs, the app marks the GPU unavailable for the session and retries once in a worker started with &lt;code&gt;--cpu-only&lt;/code&gt;. That flag restricts the engine to CPU backends before any GPU code runs, so a driver that dies during init can't take the fallback down too. The user gets their text, a bit slower, and a log line saying why.&lt;/p&gt;

&lt;p&gt;Two details that matter more than they look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On Windows the worker goes into a kill-on-close Job Object, and it exits when its stdin closes. A crashed app never leaves a worker eating your GPU memory.&lt;/li&gt;
&lt;li&gt;Esc goes through the engine's abort callback first and only kills the worker after 750 ms. Killing it every time meant reloading the model after every cancelled dictation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. My settings file got wiped on shutdown
&lt;/h2&gt;

&lt;p&gt;Early on I used &lt;code&gt;tauri-plugin-store&lt;/code&gt; for settings. Then a shutdown reset every setting. Shortcuts, models, everything back to defaults.&lt;/p&gt;

&lt;p&gt;Two things combined. The plugin wrote the file in place, so a shutdown at the wrong moment (or Windows Defender holding the file) could leave it empty or half-written. And on the next launch, a file that didn't parse was silently treated as "no settings", so defaults were written over it.&lt;/p&gt;

&lt;p&gt;The replacement is small:&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;write_atomically&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="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;io&lt;/span&gt;&lt;span class="p"&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// write to a temp file next to the real one&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;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retry_while_held&lt;/span&gt;&lt;span class="p"&gt;(||&lt;/span&gt; &lt;span class="nn"&gt;File&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&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;temp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="nf"&gt;.write_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="nf"&gt;.sync_all&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="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// then swap it in with one rename&lt;/span&gt;
    &lt;span class="nf"&gt;retry_while_held&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;rename&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;temp&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="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;p&gt;&lt;code&gt;retry_while_held&lt;/code&gt; retries for a few hundred milliseconds, because on Windows an antivirus, the search indexer or a sync client often has the file open for a moment.&lt;/p&gt;

&lt;p&gt;The rest is about what happens when the file is damaged anyway. A broken file is moved aside, not overwritten. Settings come back from a &lt;code&gt;.bak&lt;/code&gt; copy that is only refreshed from a launch that read a good file. A read &lt;em&gt;error&lt;/em&gt; (as opposed to bad content) never triggers a reset at all. And the user gets a notice instead of silently losing their setup.&lt;/p&gt;

&lt;p&gt;If you store anything users care about, don't let "can't parse it" mean "start fresh".&lt;/p&gt;

&lt;h2&gt;
  
  
  5. You don't need paid code signing to ship auto-updates
&lt;/h2&gt;

&lt;p&gt;I assumed self-update was tied to Apple and Windows code signing certificates. It isn't. Tauri's updater checks a minisign signature made with a key you generate yourself, for free. Code signing is a separate thing that only affects the "unknown publisher" warning on first install.&lt;/p&gt;

&lt;p&gt;I had tied them together in CI. The result: from 1.0 to 1.4, no release had a working update path, and nobody noticed because the update UI was also hidden. Fun.&lt;/p&gt;

&lt;p&gt;What the pipeline does now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build with the updater key from repo secrets whenever it exists, signed or not.&lt;/li&gt;
&lt;li&gt;Re-sign the Linux AppImage and &lt;code&gt;.deb&lt;/code&gt; &lt;em&gt;after&lt;/em&gt; they are repacked. The bundler's signatures describe the bytes before repacking.&lt;/li&gt;
&lt;li&gt;Write &lt;code&gt;latest.json&lt;/code&gt; once, at the end, from the signatures actually attached to the release, and verify each one against the published file before it goes live.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app polls &lt;code&gt;releases/latest/download/latest.json&lt;/code&gt;. GitHub only serves that from the newest published, non-prerelease release, so drafts and betas never reach anyone.&lt;/p&gt;

&lt;p&gt;And back up that private key. If you lose it, installed copies can never update again.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Download: &lt;a href="https://github.com/ishangautam7/Talvio/releases/latest" rel="noopener noreferrer"&gt;GitHub releases (v2.2.0)&lt;/a&gt; or &lt;a href="https://talvio.sentramind.dev" rel="noopener noreferrer"&gt;talvio.sentramind.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Windows installer isn't code signed yet, so SmartScreen will ask. More info, then Run anyway.&lt;/li&gt;
&lt;li&gt;Linux: AppImage or &lt;code&gt;.deb&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a solo project and there are rough edges. If something breaks on your machine, an issue with your GPU and OS helps a lot. If it's useful to you, a star on GitHub helps more people find it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/ishangautam7/Talvio" rel="noopener noreferrer"&gt;github.com/ishangautam7/Talvio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd also like to know: which of these did you hit in your own Tauri or Electron app? The focus one took me embarrassingly long to understand.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>opensource</category>
      <category>rust</category>
      <category>tauri</category>
    </item>
  </channel>
</rss>
