<?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: KnieLemon</title>
    <description>The latest articles on DEV Community by KnieLemon (@knielemon).</description>
    <link>https://dev.to/knielemon</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%2F4174334%2Fa9e849d7-8168-4b89-b496-b4253d438652.png</url>
      <title>DEV Community: KnieLemon</title>
      <link>https://dev.to/knielemon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/knielemon"/>
    <language>en</language>
    <item>
      <title>Porting a .NET desktop app to macOS and Linux, and the bugs a VM didn't show me</title>
      <dc:creator>KnieLemon</dc:creator>
      <pubDate>Sat, 10 Oct 2026 00:00:08 +0000</pubDate>
      <link>https://dev.to/knielemon/porting-a-net-desktop-app-to-macos-and-linux-and-the-bugs-a-vm-didnt-show-me-2jgp</link>
      <guid>https://dev.to/knielemon/porting-a-net-desktop-app-to-macos-and-linux-and-the-bugs-a-vm-didnt-show-me-2jgp</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/KnifeLemon/Filee" rel="noopener noreferrer"&gt;Filee&lt;/a&gt; is a file converter you use with a drag gesture. You hold a key, drag files, and a ring of formats opens at the cursor. Drop the files on one and the converted copies land next to the originals. It's C# on .NET 10 with Avalonia for the UI, MIT-licensed, and until last week it ran only on Windows.&lt;/p&gt;

&lt;p&gt;Avalonia is cross-platform, so the port looked like a packaging job. It wasn't. Here is what broke, roughly in the order I found it.&lt;/p&gt;

&lt;h2&gt;
  
  
  I don't own a Mac
&lt;/h2&gt;

&lt;p&gt;My test setup was a macOS 15 VM in VMware on Windows, plus GitHub's macOS runners. The VM built and ran the tests fine. The app itself died on start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Avalonia.Native was not able to start the RenderTimer. Native error code is: -6661
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avalonia drives its render loop from macOS's display link, and a VMware macOS guest without 3D acceleration has none. Turning on "Accelerate 3D graphics" in the VM settings fixed it, even though System Information still shows no Metal support. Before I found that, I made Filee show a system alert in that case instead of quitting silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  A crash caused by a null tooltip
&lt;/h2&gt;

&lt;p&gt;On the GitHub runners, which do have a display, the app also quit right after starting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.ArgumentNullException: Value cannot be null. (Parameter 'chars')
   at Avalonia.Native.Interop.Impl.__MicroComIAvnTrayIconProxy.SetToolTipText(String text)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tray icon was attached before its tooltip was set. Windows doesn't mind a null tooltip; the macOS backend passes it to native code. One line fixed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_tray&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TrayIcon&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Icon&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WindowIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iconStream&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// Set before the icon is attached: on macOS the tooltip goes to native code, which can't take null.&lt;/span&gt;
    &lt;span class="n"&gt;ToolTipText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Filee"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Menu&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;NativeMenu&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_pause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NativeMenuItemSeparator&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;_quit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;IsVisible&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow that caught it now runs on every pull request. It installs the package on Intel and Apple silicon runners, starts the app, and saves a screenshot with &lt;code&gt;screencapture -x&lt;/code&gt;. Those screenshots are the closest I have to a real Mac.&lt;/p&gt;

&lt;h2&gt;
  
  
  English on a Korean Mac
&lt;/h2&gt;

&lt;p&gt;Started from my SSH session, Filee came up in Korean. Started from Finder, it came up in English. An app launched from Finder gets no &lt;code&gt;LANG&lt;/code&gt; variable, so .NET's &lt;code&gt;CultureInfo.InstalledUICulture&lt;/code&gt; has nothing to go on. macOS keeps the user's languages elsewhere, and CoreFoundation hands them out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DllImport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;nint&lt;/span&gt; &lt;span class="nf"&gt;CFLocaleCopyPreferredLanguages&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first entry is something like &lt;code&gt;ko-KR&lt;/code&gt; or &lt;code&gt;zh-Hans-CN&lt;/code&gt;. I also had to list the app's localizations in &lt;code&gt;Info.plist&lt;/code&gt; (&lt;code&gt;CFBundleLocalizations&lt;/code&gt;) so the system panels follow the same language.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dock was under every drag
&lt;/h2&gt;

&lt;p&gt;The gesture only fires over the file manager. To check that, Filee asks which app owns the window under the cursor, walking &lt;code&gt;CGWindowListCopyWindowInfo&lt;/code&gt; from front to back. On macOS the answer was always "Dock", so Option+drag never did anything.&lt;/p&gt;

&lt;p&gt;The Dock keeps a transparent window over the whole screen. App windows sit at layer 0 and the desktop below it, so the fix is to skip everything above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;layerValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CFDictionaryGetValue&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="n"&gt;LayerKey&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="n"&gt;layerValue&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;CFNumberGetInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layerValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;layer&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I found it by adding a &lt;code&gt;FILEE_TRACE_GESTURES=1&lt;/code&gt; switch that logs each press, its modifiers and the window it landed on. The modifiers were right; the window was the Dock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finder's right-click menu: Automator didn't run
&lt;/h2&gt;

&lt;p&gt;My first "Convert with Filee" entry was a Quick Action: an Automator workflow with a shell script that started a second Filee process to hand over the files. The menu item showed up. Clicking it did nothing. A workflow made in Automator itself didn't run either, and its runner processes piled up.&lt;/p&gt;

&lt;p&gt;Instead of debugging Automator, I made Filee.app provide the service itself. &lt;code&gt;Info.plist&lt;/code&gt; declares it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSServices&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSMenuItem&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;dict&amp;gt;&amp;lt;key&amp;gt;&lt;/span&gt;default&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;&lt;/span&gt;Convert with Filee&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSMessage&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;&lt;/span&gt;convertFiles&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSPortName&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;&lt;/span&gt;Filee&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSRequiredContext&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;dict/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSSendFileTypes&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;array&amp;gt;&amp;lt;string&amp;gt;&lt;/span&gt;public.item&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and at start the app registers an object that answers &lt;code&gt;convertFiles:userData:error:&lt;/code&gt;. There's no Objective-C in the project, so the class is made at run time from C#:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;objc_allocateClassPair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;objc_getClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NSObject"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"FileeServicesProvider"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;class_addMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sel_registerName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"convertFiles:userData:error:"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFunctionPointerForDelegate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Method&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"v@:@@^@"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;objc_registerClassPair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;macOS now hands the selected files straight to the running app. No shell, no second process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing a .NET app bundle
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;codesign&lt;/code&gt; on the main executable failed with "code object is not signed at all" pointing at &lt;code&gt;Microsoft.CSharp.dll&lt;/code&gt;. A .NET app keeps its assemblies next to the executable in &lt;code&gt;Contents/MacOS&lt;/code&gt;, and codesign treats every file there as code. Signing every file first (non-Mach-O files get their signature in extended attributes), then the executable, then the bundle, works.&lt;/p&gt;

&lt;p&gt;A smaller one: my entitlements file came out of &lt;code&gt;git archive&lt;/code&gt; on Windows with CRLF line endings, and codesign refused to parse it. &lt;code&gt;*.plist text eol=lf&lt;/code&gt; in &lt;code&gt;.gitattributes&lt;/code&gt; settled that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Files nobody locks
&lt;/h2&gt;

&lt;p&gt;Filee can watch a folder and convert what lands in it, but only once a file is complete. On Windows the check is easy: a file still being written can't be opened exclusively. macOS and Linux don't lock it, so a copy that paused for a few seconds got converted half-written, then again when it finished.&lt;/p&gt;

&lt;p&gt;There the watcher now waits longer for a file to stop changing. On macOS it also skips files whose creation date is 24 January 1984, the date Finder gives a file while it's still copying it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Linux and Wayland
&lt;/h2&gt;

&lt;p&gt;Linux went more smoothly: Ubuntu 24.04, an &lt;code&gt;install.sh&lt;/code&gt; that puts the app in the menu and adds file-manager actions for Nautilus, Nemo, Thunar and Dolphin. The drag gesture needs X11, though. Wayland doesn't let one app watch another's input, so there you use the drop window and the right-click action. I haven't found a good Wayland answer yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;macOS and Linux are previews in 1.7. The macOS build isn't notarized yet, and everything I know about real Macs comes from CI screenshots and bug reports. If you try it on one and something breaks, the &lt;a href="https://github.com/KnifeLemon/Filee/issues" rel="noopener noreferrer"&gt;issues&lt;/a&gt; are open.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I wrote this article with help from an AI assistant, which I also used while writing the code. The bugs, logs and fixes above are from the project's history.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>opensource</category>
      <category>avalonia</category>
    </item>
    <item>
      <title>Confirming bank transfers from Android notifications with 15 lines of Python</title>
      <dc:creator>KnieLemon</dc:creator>
      <pubDate>Fri, 09 Oct 2026 23:58:02 +0000</pubDate>
      <link>https://dev.to/knielemon/confirming-bank-transfers-from-android-notifications-with-15-lines-of-python-321l</link>
      <guid>https://dev.to/knielemon/confirming-bank-transfers-from-android-notifications-with-15-lines-of-python-321l</guid>
      <description>&lt;p&gt;Small shops that take bank transfers usually confirm them by hand: the bank app shows "deposit received", someone opens the order list and marks the order paid. Banks do offer APIs for this, but they come with contracts and fees that a small shop doesn't want.&lt;/p&gt;

&lt;p&gt;The phone already gets the information for free, as a notification. So I wired the notification straight into code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I used Droidline, an open source tool I've been building. It has two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an Android app that reads the screen and notifications through Android's accessibility and notification access, using only permissions an ordinary app can get&lt;/li&gt;
&lt;li&gt;a server on the PC (&lt;code&gt;droidline serve&lt;/code&gt;) that the app connects to over Wi-Fi&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no ADB, no USB debugging and no root. You pair the phone once by scanning a QR code from &lt;code&gt;droidline pair&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;The same two steps in every language: tell the phone which app's notifications to forward, then react to each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;droidline&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;connect&lt;/span&gt;

&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s23&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify_filter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;com.example.bank&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# only pass this app's notifications to the PC
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;paid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&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;n&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Deposit]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;mark_paid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;            &lt;span class="c1"&gt;# your own order system
&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_notification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;paid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Node.js
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;connect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;droidline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;s23&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notifyFilter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;com.example.bank&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNotification&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;com.example.bank&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;n&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[Deposit]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;markPaid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&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;h3&gt;
  
  
  C
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Droidline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;DroidlineClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"s23"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotifyFilterAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"com.example.bank"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnNotificationAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;=&amp;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="n"&gt;n&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="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[Deposit]"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;MarkPaid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&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="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"com.example.bank"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadLine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Shell
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;droidline notify_filter com.example.bank
droidline on_notification &lt;span class="nt"&gt;--package&lt;/span&gt; com.example.bank
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second command prints each notification as a JSON line until you stop it, so you can pipe it into anything.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;notify_filter&lt;/code&gt; matters: by default nothing leaves the phone, and you choose which apps' notifications are forwarded. Replace &lt;code&gt;com.example.bank&lt;/code&gt; with your bank app's package name, and the &lt;code&gt;[Deposit]&lt;/code&gt; check with your bank's wording, since every bank phrases the notification differently. &lt;code&gt;mark_paid&lt;/code&gt; stands for your own order system.&lt;/p&gt;

&lt;p&gt;Install: &lt;code&gt;pip install droidline&lt;/code&gt;, &lt;code&gt;npm install droidline&lt;/code&gt; or &lt;code&gt;dotnet add package Droidline&lt;/code&gt;. The &lt;code&gt;droidline&lt;/code&gt; command and the Android app are on the &lt;a href="https://github.com/KnifeLemon/Droidline/releases/latest" rel="noopener noreferrer"&gt;releases page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to know
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The phone has to stay on and connected. An old phone on a charger works fine for this.&lt;/li&gt;
&lt;li&gt;The connection is encrypted end to end (P-256 and AES-256-GCM), so the notification text never crosses the network in the clear.&lt;/li&gt;
&lt;li&gt;Android 15 and later hide one-time codes inside notifications from apps. Deposit notifications are not affected, but if you try the same trick for verification codes, it only works on Android 14 and earlier.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Beyond notifications
&lt;/h2&gt;

&lt;p&gt;The same connection can tap, type, swipe, take screenshots and run OCR, and there's an MCP server so AI agents can use the phone. On a Galaxy S23 Ultra, finding an element by its text takes 16 ms (median of 10 runs), against 100 ms for Appium UiAutomator2 and 2,368 ms for adb plus uiautomator. The benchmark script is in the repo under &lt;code&gt;scripts/bench&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/KnifeLemon/Droidline" rel="noopener noreferrer"&gt;https://github.com/KnifeLemon/Droidline&lt;/a&gt; (MIT)&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://droidline.dev/docs/" rel="noopener noreferrer"&gt;https://droidline.dev/docs/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;I wrote this post with help from an AI assistant and checked every command against the project.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>android</category>
      <category>python</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
