<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>~iany/ Script</title><link>https://blog.iany.me/tags/script/</link><description>Recent content in Script «~iany/»</description><language>en-US</language><managingEditor>me@iany.me (Ian Yang)</managingEditor><webMaster>me@iany.me (Ian Yang)</webMaster><copyright>CC-BY-SA 4.0</copyright><lastBuildDate>Tue, 08 Sep 2026 09:00:00 +0800</lastBuildDate><atom:link href="https://blog.iany.me/tags/script/index.xml" rel="self" type="application/rss+xml"/><item><title>Forwarding Desktop Notifications to Pushover</title><link>https://blog.iany.me/2026/09/forwarding-desktop-notifications-to-pushover/</link><pubDate>Tue, 08 Sep 2026 09:00:00 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/09/forwarding-desktop-notifications-to-pushover/</guid><description>&lt;p&gt;I already forwarded &lt;strong&gt;mako&lt;/strong&gt; notifications to Pushover on Linux. The hook is tiny: mako runs a command with the notification id, the script reads &lt;code&gt;makoctl list -j&lt;/code&gt;, skips loops and Pushover itself, and POSTs to the Pushover API.&lt;/p&gt;
&lt;p&gt;Windows has no equivalent of &lt;code&gt;on-notify=exec&lt;/code&gt;. I wanted one binary, &lt;code&gt;dn-pushover&lt;/code&gt;, that does the same job on both desktops: titles become &lt;code&gt;[DN][hostname] …&lt;/code&gt; so a notification that bounces back is easy to drop.&lt;/p&gt;
&lt;p&gt;Linux stayed simple. Windows did not.&lt;/p&gt;
&lt;h2 id="how-it-works"&gt;How it works&lt;/h2&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Notification Forwarding Setup Paths"
loading="lazy"
src="https://blog.iany.me/2026/09/forwarding-desktop-notifications-to-pushover/Notification%20forwarding%20setup%20paths.svg" /&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Linux.&lt;/strong&gt; &lt;code&gt;dn-pushover setup&lt;/code&gt; writes a managed block into &lt;code&gt;~/.config/mako/config&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;on-notify=exec dn-pushover &amp;quot;$id&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then it reloads mako. If &lt;code&gt;makoctl&lt;/code&gt; is missing, setup fails instead of pretending to work. An old &lt;code&gt;mako-pushover&lt;/code&gt; block is migrated so deleting the old binary cannot leave a dead hook behind.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows.&lt;/strong&gt; There is no desktop-wide exec hook. The supported API is &lt;code&gt;UserNotificationListener&lt;/code&gt;: after the user grants access, an app can read other apps&amp;rsquo; toast notifications. &lt;code&gt;setup&lt;/code&gt; therefore:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Checks Pushover credentials.&lt;/li&gt;
&lt;li&gt;Builds a small C# WinForms helper (first time only; later setups reuse the exe).&lt;/li&gt;
&lt;li&gt;Packs a &lt;strong&gt;sparse identity MSIX&lt;/strong&gt; (manifest + logos; the exe lives next to it via &lt;code&gt;-ExternalLocation&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Signs it and registers it.&lt;/li&gt;
&lt;li&gt;Launches the helper. You click &lt;strong&gt;Allow Access&lt;/strong&gt; and accept the Windows prompt.&lt;/li&gt;
&lt;li&gt;The helper baselines whatever is already in the notification center, then polls for new toasts and runs &lt;code&gt;dn-pushover --forward&lt;/code&gt; with JSON on stdin.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;teardown&lt;/code&gt; stops the helper. &lt;code&gt;teardown --cleanup&lt;/code&gt; also unregisters the package and deletes &lt;code&gt;%LOCALAPPDATA%\dn-pushover&lt;/code&gt;. There is no autostart: after logout, run &lt;code&gt;setup&lt;/code&gt; again.&lt;/p&gt;
&lt;p&gt;The JS side is shared: prefix, truncation, skip &lt;code&gt;[DN]&lt;/code&gt; / &lt;code&gt;[mako]&lt;/code&gt; / recognizable Pushover traffic, then &lt;code&gt;send()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="gotchas"&gt;Gotchas&lt;/h2&gt;
&lt;h3 id="linux-is-a-hook-windows-is-an-identity"&gt;Linux is a hook. Windows is an identity.&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;UserNotificationListener&lt;/code&gt; is not &amp;ldquo;read the notification database.&amp;rdquo; It is a capability on a &lt;strong&gt;packaged&lt;/strong&gt; app. An unpackaged Win32 process can call &lt;code&gt;RequestAccessAsync&lt;/code&gt; and even get &lt;code&gt;Allowed&lt;/code&gt; while &lt;code&gt;GetNotificationsAsync&lt;/code&gt; returns an empty list and Settings never shows the app.&lt;/p&gt;
&lt;p&gt;A loose &lt;code&gt;Add-AppxPackage -Register&lt;/code&gt; development package (&lt;code&gt;SignatureKind=None&lt;/code&gt;) has the same problem. Windows will not list it under &lt;strong&gt;Privacy &amp;amp; security → Notifications&lt;/strong&gt; (the &lt;em&gt;read&lt;/em&gt; list, not &lt;strong&gt;System → Notifications&lt;/strong&gt;, which is the &lt;em&gt;send&lt;/em&gt; list).&lt;/p&gt;
&lt;p&gt;What worked was a &lt;strong&gt;signed sparse MSIX&lt;/strong&gt; whose publisher matches the exe&amp;rsquo;s &lt;code&gt;app.manifest&lt;/code&gt; &lt;code&gt;msix&lt;/code&gt; element, with &lt;code&gt;uap3:Capability Name=&amp;quot;userNotificationListener&amp;quot;&lt;/code&gt; and &lt;code&gt;AppListEntry&lt;/code&gt; &lt;strong&gt;not&lt;/strong&gt; set to &lt;code&gt;none&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="signing-is-not-optional-and-per-user-trust-is-not-enough"&gt;Signing is not optional, and per-user trust is not enough&lt;/h3&gt;
&lt;p&gt;Self-signed is fine for a personal machine. The private key stays in &lt;code&gt;CurrentUser\My&lt;/code&gt;. Importing the &lt;em&gt;public&lt;/em&gt; cert into &lt;code&gt;CurrentUser\TrustedPeople&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; make you a public CA; it only means this user trusts this publisher.&lt;/p&gt;
&lt;p&gt;MSIX deployment still failed with &lt;code&gt;0x800B0109&lt;/code&gt; (untrusted root) until the public cert was also in &lt;strong&gt;&lt;code&gt;LocalMachine\TrustedPeople&lt;/code&gt;&lt;/strong&gt;. That needs a one-time UAC. Later setups skip the import if the thumbprint is already there.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;powershell.exe -NoProfile&lt;/code&gt; has no &lt;code&gt;Cert:&lt;/code&gt; drive. &lt;code&gt;Import-Module PKI&lt;/code&gt; under &lt;code&gt;$ErrorActionPreference = 'Stop'&lt;/code&gt; blew up on duplicate type data. Certificate create/import had to use &lt;code&gt;X509Store&lt;/code&gt; / &lt;code&gt;CertificateRequest&lt;/code&gt; instead of &lt;code&gt;New-SelfSignedCertificate&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="the-sdk-is-not-the-runtime"&gt;The SDK is not the runtime&lt;/h3&gt;
&lt;p&gt;A .NET 10 &lt;strong&gt;runtime&lt;/strong&gt; cannot &lt;code&gt;dotnet publish&lt;/code&gt;. First Windows setup needs a &lt;strong&gt;.NET 10 SDK&lt;/strong&gt; (Scoop: &lt;code&gt;versions/dotnet-sdk-lts&lt;/code&gt;) plus Windows SDK tools &lt;code&gt;makeappx&lt;/code&gt; and &lt;code&gt;signtool&lt;/code&gt;. Setup runs PowerShell with &lt;code&gt;-NoProfile&lt;/code&gt;, so Scoop&amp;rsquo;s shims are not on &lt;code&gt;PATH&lt;/code&gt;; it has to look in &lt;code&gt;~/scoop/apps/dotnet-sdk-lts/current/&lt;/code&gt; itself.&lt;/p&gt;
&lt;p&gt;Setup also &lt;strong&gt;skips rebuilding&lt;/strong&gt; if the helper exe already exists, and used to &lt;strong&gt;skip re-registering&lt;/strong&gt; if the package was already installed. That is how a stale unsigned package and an old helper survived &amp;ldquo;I already ran setup.&amp;rdquo; &lt;code&gt;teardown --cleanup&lt;/code&gt; is the rebuild switch. Signed packages under &lt;code&gt;WindowsApps&lt;/code&gt; must actually be removed; if cleanup no-ops, &lt;code&gt;Get-AppxPackage&lt;/code&gt; will still show them.&lt;/p&gt;
&lt;h3 id="the-compiled-bun-binary-is-a-different-program"&gt;The compiled Bun binary is a different program&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;import.meta.main&lt;/code&gt; is false inside a Bun-compiled &lt;code&gt;.exe&lt;/code&gt;. Guarding &lt;code&gt;main()&lt;/code&gt; with only that flag made &lt;code&gt;dn-pushover setup&lt;/code&gt; exit 0 with &lt;strong&gt;no output&lt;/strong&gt;. Detect the virtual &lt;code&gt;B:/~BUN/&lt;/code&gt; / &lt;code&gt;$bunfs&lt;/code&gt; entrypoint as well.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Bun.stdin.text()&lt;/code&gt; in that same compiled Windows exe did not read the helper&amp;rsquo;s piped JSON. &lt;code&gt;--forward&lt;/code&gt; exited 0, sent nothing. &lt;code&gt;readStdin()&lt;/code&gt; from Node&amp;rsquo;s &lt;code&gt;process.stdin&lt;/code&gt; did. A WinForms &lt;strong&gt;AcceptButton&lt;/strong&gt; also treated the Enter key from the terminal as &lt;strong&gt;Allow Access&lt;/strong&gt;, so setup &amp;ldquo;returned immediately&amp;rdquo; after the first successful grant.&lt;/p&gt;
&lt;p&gt;Substring &lt;code&gt;/pushover/i&lt;/code&gt; on the app id matched &lt;code&gt;DnPushover.NotificationListener&lt;/code&gt; and dropped every toast. Skip the Pushover &lt;em&gt;product&lt;/em&gt;, not any name that contains those letters.&lt;/p&gt;
&lt;h2 id="debug-process"&gt;Debug process&lt;/h2&gt;
&lt;p&gt;The failure mode was always the same from the outside: a Windows toast appeared, Pushover stayed quiet. The pipeline has three stages; each can succeed while the next never runs.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Notification Forwarding Failure Stages"
loading="lazy"
src="https://blog.iany.me/2026/09/forwarding-desktop-notifications-to-pushover/Notification%20forwarding%20failure%20stages.svg" /&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Is the listener alive?&lt;/strong&gt; Process + &lt;code&gt;%LOCALAPPDATA%\dn-pushover\listener.log&lt;/code&gt;. Empty log with a running process meant polls were not seeing new toasts (or the form was hidden and the timer never ticked). &lt;code&gt;Listener ready; N existing toast(s)&lt;/code&gt; means those N will never be forwarded — send a &lt;strong&gt;new&lt;/strong&gt; toast after ready.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does JS send?&lt;/strong&gt; Pipe JSON into &lt;code&gt;dn-pushover --forward&lt;/code&gt; and watch the Pushover API. A 200 body &lt;code&gt;{&amp;quot;status&amp;quot;:1,&amp;quot;request&amp;quot;:&amp;quot;…&amp;quot;}&lt;/code&gt; means credentials and the API are fine. If that works and the listener still does not, the helper is not invoking the CLI you think it is (&lt;code&gt;config.json&lt;/code&gt; points at &lt;code&gt;dist\dn-pushover.exe&lt;/code&gt;), or the CLI is skipping the payload.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What did &lt;code&gt;--forward&lt;/code&gt; actually do?&lt;/strong&gt; Temporary logging of argv, stdin length, skip vs send, and the API status. That is how &lt;code&gt;Bun.stdin.text()&lt;/code&gt; was caught: argv logged, stdin never read, process exit 0, no HTTP. After the stdin fix, the same toast showed &lt;code&gt;apiStatus: 200&lt;/code&gt;. Edge then showed the Pushover copy with a &lt;code&gt;[DN]&lt;/code&gt; title; that one is supposed to be skipped so you do not loop.&lt;/p&gt;
&lt;p&gt;Direct &lt;code&gt;fetch&lt;/code&gt; to &lt;code&gt;https://api.pushover.net/1/messages.json&lt;/code&gt; (user key + desktop-notification token, no secrets in logs) is the ground truth. If that returns &lt;code&gt;status: 1&lt;/code&gt; and the device still shows nothing, you are looking at the wrong Pushover app or device, not at this script.&lt;/p&gt;
&lt;h2 id="practical-checklist"&gt;Practical checklist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Linux: &lt;code&gt;makoctl&lt;/code&gt; on mako&amp;rsquo;s PATH; &lt;code&gt;dn-pushover setup&lt;/code&gt;; section-specific &lt;code&gt;on-notify&lt;/code&gt; rules win over the global hook.&lt;/li&gt;
&lt;li&gt;Windows: .NET 10 SDK + Windows SDK; one UAC to trust &lt;code&gt;CN=dn-pushover&lt;/code&gt; in &lt;code&gt;LocalMachine\TrustedPeople&lt;/code&gt;; &lt;code&gt;dn-pushover setup&lt;/code&gt;; click Allow Access; Privacy list is &lt;em&gt;access&lt;/em&gt;, not senders.&lt;/li&gt;
&lt;li&gt;After changing helper C# or the Bun CLI: &lt;code&gt;teardown --cleanup&lt;/code&gt; then &lt;code&gt;setup&lt;/code&gt;, and use the rebuilt &lt;code&gt;dist&lt;/code&gt; binary.&lt;/li&gt;
&lt;li&gt;Test with a toast created &lt;strong&gt;after&lt;/strong&gt; the listener is ready. Expect &lt;code&gt;[DN][hostname]&lt;/code&gt; on Pushover.&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/bun/">Bun</category><category domain="https://blog.iany.me/tags/desktop/">Desktop</category><category domain="https://blog.iany.me/tags/linux/">Linux</category><category domain="https://blog.iany.me/tags/script/">Script</category><category domain="https://blog.iany.me/tags/windows/">Windows</category></item><item><title>Use Bun for Shell Scripts</title><link>https://blog.iany.me/2026/02/use-bun-for-shell-scripts/</link><pubDate>Sat, 07 Feb 2026 00:00:00 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/02/use-bun-for-shell-scripts/</guid><description>&lt;p&gt;I&amp;rsquo;ve moved most of my small scripts to Bun. It sidesteps Windows&amp;rsquo; lack of shebang support and the Git-for-Windows/WSL bash dance, and gives you one runtime for both ad-hoc shell-style commands and full scripts.&lt;/p&gt;
&lt;h2 id="problems-bun-solves"&gt;Problems Bun solves&lt;/h2&gt;
&lt;p&gt;Windows doesn&amp;rsquo;t execute &lt;code&gt;#!/usr/bin/env python3&lt;/code&gt; or &lt;code&gt;#!/bin/bash&lt;/code&gt;, so scripts written for Linux or macOS usually need a separate PowerShell version. Bun compiles JavaScript to native executables, so a single script can run cross-platform without shebangs.&lt;/p&gt;
&lt;p&gt;Mise &lt;a href="https://mise.jdx.dev/tasks/file-tasks.html"&gt;file tasks&lt;/a&gt; have started to support shebangs on Windows in the latest release, but they rely on system &lt;code&gt;bash.exe&lt;/code&gt;, which often points at WSL. Bun does not. You run &lt;code&gt;bun ./script.js&lt;/code&gt; and embed shell commands via &lt;code&gt;Bun.$&lt;/code&gt;; the script uses Bun&amp;rsquo;s bundled bash environment.&lt;/p&gt;
&lt;h2 id="why-bun-for-shell-scripts"&gt;Why Bun for shell scripts&lt;/h2&gt;
&lt;p&gt;The command &lt;code&gt;bun build ./script.ts --compile --outfile script&lt;/code&gt; &lt;a href="https://bun.com/docs/bundler/executables"&gt;produces a single binary&lt;/a&gt;, so no shebang or interpreter is needed on Windows. The &lt;code&gt;Bun.build&lt;/code&gt; API is available too; I use a &lt;code&gt;build.js&lt;/code&gt; script to walk a directory and compile all executables. Pre-built binaries start quickly—Bun&amp;rsquo;s docs cite ~5ms vs Node&amp;rsquo;s ~25ms for a simple script, which helps for small, frequently run scripts.&lt;/p&gt;
&lt;p&gt;Bun has strong support for shell-style scripting. &lt;a href="https://bun.com/docs/runtime/shell"&gt;Bun Shell&lt;/a&gt; runs bash commands; the main entry point is &lt;code&gt;Bun.$&lt;/code&gt;, which constructs shell commands from template literals for safe interpolation, similar to &lt;a href="https://github.com/google/zx"&gt;google/zx&lt;/a&gt;. On Windows, Bun ships with MSYS, so you get a consistent bash environment and common Linux CLI tools without extra setup.&lt;/p&gt;
&lt;p&gt;Other useful features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Bun.secrets&lt;/code&gt;—Securely store and retrieve secrets using the OS keystore (Windows Credential Manager, macOS Keychain, Linux libsecret). &lt;a href="https://bun.com/docs/api/secrets"&gt;Secrets - Bun&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Built-in support for glob patterns, YAML parsing, ANSI colors, and more. &lt;a href="https://bun.com/docs/runtime"&gt;Bun Runtime&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="short-example"&gt;Short example&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;import { $ } from &amp;quot;bun&amp;quot;;
const out = await $`echo hello`.text();
console.log(out); // hello
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it with &lt;code&gt;bun run script.js&lt;/code&gt; or &lt;code&gt;bun ./script.js&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="obsidian-notes"&gt;Obsidian Notes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/d/Development&amp;#43;Environment/JavaScript&amp;#43;Shell&amp;#43;Scripting"&gt;JavaScript Shell Scripting&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/bun/">Bun</category><category domain="https://blog.iany.me/tags/javascript/">JavaScript</category><category domain="https://blog.iany.me/tags/script/">Script</category><category domain="https://blog.iany.me/tags/windows/">Windows</category><category domain="https://blog.iany.me/tags/dev-environment/">Dev Environment</category></item><item><title>Switch Window Using Fuzz Matching</title><link>https://blog.iany.me/2010/08/switch-window-using-fuzz-matching/</link><pubDate>Wed, 25 Aug 2010 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2010/08/switch-window-using-fuzz-matching/</guid><description>&lt;p&gt;This article demonstrates how to quickly switch to a window using &lt;strong&gt;gpicker&lt;/strong&gt;
and &lt;strong&gt;wmctrl&lt;/strong&gt;. You type significant letters of workspace name, application name
or title and gpicker provides a list of windows you most likely mean to pick.&lt;/p&gt;
&lt;h2 id="gpicker"&gt;Gpicker&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="http://savannah.nongnu.org/projects/gpicker" title="Gpicker"&gt;Gpicker&lt;/a&gt; is a program that allows you to quickly and conveniently pick file
in a (possibly very large) project. You type significant letters of file name
(typically from the start of words) and gpicker provides you with a list of
files you most likely mean to pick. The program filters and orders project&amp;rsquo;s
list of files in real-time as you type.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You need to &lt;a href="http://ftp.twaren.net/Unix/NonGNU/gpicker/"&gt;download&lt;/a&gt; and install
it manually (for Ubuntu, you can download the Debian package).&lt;/p&gt;
&lt;p&gt;Gpicker is designed as a filter, so it can be used to select item from an
arbitrary list. The selected item is printed on standard output.&lt;/p&gt;
&lt;p&gt;For example, use gpicker to select a number between 1 and 10:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ seq -w 1 10 | gpicker -n &amp;quot;\n&amp;quot; -
&lt;/code&gt;&lt;/pre&gt;
&lt;figure class="kg-image-card kg-width-fit"&gt;
&lt;img alt="Gpicker picks 1 to 10" class="kg-image" loading="lazy" src="https://blog.iany.me/2010/08/switch-window-using-fuzz-matching/picker_1_to_10_hu_4491a6a7d73f6aa5.png" /&gt;
&lt;figcaption &gt;Gpicker picks 1 to 10&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The last dash tells gpicker to read list from standard input, and option &amp;ldquo;-n&amp;rdquo;
sets the list separator. Gpicker uses &amp;ldquo;\0&amp;rdquo; as item separator by default.&lt;/p&gt;
&lt;h2 id="wmctrl"&gt;Wmctrl&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Wmctrl&lt;/strong&gt; is a command line utility to interact with a EWMH/NetWM compatible X
Window Manager. It can list all windows, switch to a window or bring a window to
current workspace. Wmctrl can be installed by package manager in most Linux
distribution.&lt;/p&gt;
&lt;p&gt;Wmctrl lists all windows using option &amp;ldquo;-l&amp;rdquo;. We can add &amp;ldquo;-x&amp;rdquo; to list windows
resource and class as well. The output of &lt;code&gt;wmctrl -l -x&lt;/code&gt; looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x03200009 0 urxvt.URxvt ian-desktop urxvt
0x0340008e 1 Navigator.Firefox ian-desktop Vimperator / test
0x03e000a4 2 emacs.Emacs ian-desktop Emacs: _theme.sass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first column is the window ID. We can switch to or bring a window using the
ID.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# switch to Emacs, raise and focus it
wmctrl -i -a 0x03e000a4
# bring Emacs to current workspace, raise and focus it
wmctrl -i -R 0x03e000a4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="windows-picker"&gt;Windows Picker&lt;/h2&gt;
&lt;p&gt;Now we can use wmctrl to list windows, gpicker to select one window, and
wmctrl again to switch to the selected window.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wmctrl -i -a `wmctrl -l -x | gpicker -n &amp;quot;\n&amp;quot; -d &amp;quot;\n&amp;quot; -`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The directory separator is set to &amp;ldquo;\n&amp;rdquo; in the first windows picker above,
because gpicker is designed to pick files primarily and it has special rules
in the matching algorithm.&lt;/p&gt;
&lt;p&gt;First the item is split by directory separator. Before we enter a directory
separator (&amp;quot;/&amp;quot; by default), only the last component (it is the file name if the
item is a file path) is used in matching. For example, &amp;ldquo;bin&amp;rdquo; does not match item
&amp;ldquo;bin/ruby&amp;rdquo;, but &amp;ldquo;bin/&amp;rdquo;, even &amp;ldquo;b/&amp;rdquo; does match. If the window title contains the
directory separator, for example the Firefox in the &lt;code&gt;wmctrl -l -x&lt;/code&gt; output above:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x0340008e 1 Navigator.Firefox ian-desktop Vimperator / test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To match it, we must enter &amp;ldquo;firefox/&amp;rdquo;. So I set directory separator to &amp;ldquo;\n&amp;rdquo; to
disable this feature, and the whole item is used to match.&lt;/p&gt;
&lt;p&gt;However, we can take advantage of this feature. If we construct the window item
likes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id/workspace/window_class/window_title
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I can match window title by type significant letters directory. I also can
use directory separator &amp;ldquo;/&amp;rdquo; to filter all windows in a specific workspace, or
windows with the same window class.&lt;/p&gt;
&lt;p&gt;It is very useful for me. Because I uses tiling windows manager
&lt;a href="http://xmonad.org/"&gt;Xmonad&lt;/a&gt; and places windows in different windows. Following
are some examples:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;I have entered&lt;/th&gt;
&lt;th&gt;Windows I want to match&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;www/ff/&lt;/td&gt;
&lt;td&gt;Firefox in workspace &amp;ldquo;2.www&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ruby&lt;/td&gt;
&lt;td&gt;Windows which title contains ruby&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xpdf/ruby&lt;/td&gt;
&lt;td&gt;A document opened using xpdf and the title contains ruby&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sys/usrshare&lt;/td&gt;
&lt;td&gt;The terminal I opened visit /usr/share in workspace &amp;ldquo;1.sys&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure class="kg-card kg-gallery-card kg-width-wide"&gt;
&lt;div class="kg-gallery-container"&gt;
&lt;div class="kg-gallery-row"&gt;
&lt;div class="kg-gallery-image"&gt;&lt;img alt="Windows list" class="tippy" loading="lazy" width="794" height="279" src="https://blog.iany.me/2010/08/switch-window-using-fuzz-matching/picker-windows-list_hu_7b0d10b83e528caa.png" srcset="https://blog.iany.me/2010/08/switch-window-using-fuzz-matching/picker-windows-list_hu_8349fc22a18d1778.png 400w, https://blog.iany.me/2010/08/switch-window-using-fuzz-matching/picker-windows-list_hu_7b0d10b83e528caa.png 794w" sizes="(max-width: 400px) 100vw, 794px" data-tippy-content="Windows list" /&gt;&lt;/div&gt;
&lt;div class="kg-gallery-image"&gt;&lt;img alt="Select Ruby document opened in Xpdf" class="tippy" loading="lazy" width="794" height="279" src="https://blog.iany.me/2010/08/switch-window-using-fuzz-matching/picker-xpdf-ruby_hu_17605e7b4d7d2ab5.png" srcset="https://blog.iany.me/2010/08/switch-window-using-fuzz-matching/picker-xpdf-ruby_hu_7a1c301493e4f7cb.png 400w, https://blog.iany.me/2010/08/switch-window-using-fuzz-matching/picker-xpdf-ruby_hu_17605e7b4d7d2ab5.png 794w" sizes="(max-width: 400px) 100vw, 794px" data-tippy-content="Select Ruby document opened in Xpdf" /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Sure, we have to remove &amp;ldquo;/&amp;rdquo; from workspace name and window title.&lt;/p&gt;
&lt;p&gt;I use a Ruby &lt;a href="https://gist.github.com/doitian/551432"&gt;script&lt;/a&gt; to format the
list. Workspace name can be listed using &lt;code&gt;wmctrl -d&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now we can use the ruby script in the revised version:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wmctrl -i -a `wlist.rb | gpicker - | sed 's;/.*$;;'`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace &amp;ldquo;-a&amp;rdquo; with &amp;ldquo;-R&amp;rdquo; to bring window to current workspace.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/desktop/">Desktop</category><category domain="https://blog.iany.me/tags/linux/">Linux</category><category domain="https://blog.iany.me/tags/productivity/">Productivity</category><category domain="https://blog.iany.me/tags/script/">Script</category></item></channel></rss>