Congratulations!

[Valid RSS] This is a valid RSS feed.

Recommendations

This feed is valid, but interoperability with the widest range of feed readers could be improved by implementing the following recommendations.

Source: http://akrabat.com/feed/

  1. <?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
  2. xmlns:content="http://purl.org/rss/1.0/modules/content/"
  3. xmlns:wfw="http://wellformedweb.org/CommentAPI/"
  4. xmlns:dc="http://purl.org/dc/elements/1.1/"
  5. xmlns:atom="http://www.w3.org/2005/Atom"
  6. xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
  7. xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
  8. >
  9.  
  10. <channel>
  11. <title>Rob Allen</title>
  12. <atom:link href="https://akrabat.com/feed/" rel="self" type="application/rss+xml" />
  13. <link>https://akrabat.com</link>
  14. <description>Pragmatism in today&#039;s world</description>
  15. <lastBuildDate>Tue, 30 Sep 2025 08:45:43 +0000</lastBuildDate>
  16. <language>en-US</language>
  17. <sy:updatePeriod>
  18. hourly </sy:updatePeriod>
  19. <sy:updateFrequency>
  20. 1 </sy:updateFrequency>
  21. <generator>https://wordpress.org/?v=6.8.3</generator>
  22. <item>
  23. <title>mac-volume: A CLI to control device volume</title>
  24. <link>https://akrabat.com/mac-volume-a-cli-to-control-device-volume/</link>
  25. <comments>https://akrabat.com/mac-volume-a-cli-to-control-device-volume/#respond</comments>
  26. <dc:creator><![CDATA[Rob]]></dc:creator>
  27. <pubDate>Tue, 07 Oct 2025 10:00:00 +0000</pubDate>
  28. <category><![CDATA[Mac]]></category>
  29. <category><![CDATA[Swift]]></category>
  30. <guid isPermaLink="false">https://akrabat.com/?p=7512</guid>
  31.  
  32. <description><![CDATA[I've set my Mac up such that video calls such as Zoom use the microphone and earphones attached to my Behringer UMC204HD, which all other audio plays through the my normal speakers which are the default. One issue I have with this is that it's quite hard to change the volume when a call as the volume buttons on the Mac are connected to the default output. This finally annoyed me enough that I looked… <a href="https://akrabat.com/mac-volume-a-cli-to-control-device-volume/">continue reading</a>.]]></description>
  33. <content:encoded><![CDATA[<p>I've set my Mac up such that video calls such as Zoom use the microphone and earphones attached to my <a href="https://www.behringer.com/product.html?modelCode=0805-AAS">Behringer UMC204HD</a>, which all other audio plays through the my normal speakers which are the default.</p>
  34. <p>One issue I have with this is that it's quite hard to change the volume when a call as the volume buttons on the Mac are connected to the default output. This finally annoyed me enough that I looked into how to control the volume of a device and wrote a little command line utility for this, so that I could attach it to buttons on my <a href="https://akrabat.com/controlling-the-streamdeck-via-keyboard-maestro/">StreamDeck using Keyboard Maestro</a>.</p>
  35. <h2>Changing the volume</h2>
  36. <p>The way to control the volume on a Mac programmatically is to use Core Audio, specifically <a href="https://developer.apple.com/documentation/coreaudio/audioobjectsetpropertydata(_:_:_:_:_:_:)?language=objc"><tt>AudioObjectSetPropertyData()</tt></a>. This function takes a <tt>deviceID</tt> and some other properties, including <tt>volume</tt>. The <tt>deviceID</tt> is of type <a href="https://developer.apple.com/documentation/coreaudio/audiodeviceid"><tt>AudioDeviceID</tt></a> and the <tt>volume</tt> is a floating point number between <tt>0</tt> and <tt>1</tt>.</p>
  37. <p>It's not easy to remember device IDs, so I decided to use the device name instead and map the volume to between 0 and 100. This makes it easy to say set the volume to 10%:</p>
  38. <pre>
  39. mac-volume "MacBook Pro Speakers" set 10
  40. </pre>
  41. <p>To get the list of device names, I wrote a <tt>list-devices</tt> command. The list is available from <tt>AudioObjectGetPropertyData()</tt> which returns list of <tt>AudioDeviceID</tt> items. The name is in the <tt>kAudioObjectPropertyName</tt> selector, so that's easy enough.</p>
  42. <p>The main trouble is that CoreAudio is a C API and so dealing with it from Swift involves use of references and <a href="https://developer.apple.com/documentation/swift/unsafemutablepointer">UnsafeMutablePointer</a> which is a bit hairy as it's been a while since I've had to worry about pointers!</p>
  43. <h3>Increment and decrement</h3>
  44. <p>While you can get the current volume, add to it and then set to increase the volume, it's easier to let the app do it, so <tt>mac-volume</tt> also supports incrementing and decrementing by an amount. These are the two command that I bind to StreamDeck keys.</p>
  45. <picture><source  
  46. srcset="https://akrabat.com/wp-content/uploads/2025/09/2025mac-vol-down-dark.png"
  47.        media="(prefers-color-scheme: dark)"
  48.    /><source
  49. srcset="https://akrabat.com/wp-content/uploads/2025/09/2025mac-vol-down-light.png"
  50.        media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
  51.    /><br />
  52.    <img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/09/2025mac-vol-down-light.png" loading="lazy" alt="Keyboard Maestro configuration for mac-volume down" class="border" width="550"/>
  53. </picture>
  54. <p>I found that a step size of 3 was a good choice for raising/lowering the volume with a reasonable number of presses of the key.</p>
  55. <h3>It's on GitHub</h3>
  56. <p>If you need to control the volume of a non-default audio device from the command line, <tt>mac-volume</tt> is on GitHub at <a href="https://github.com/akrabat/mac-volume">akrabat/mac-volume</a>.</p>
  57. ]]></content:encoded>
  58. <wfw:commentRss>https://akrabat.com/mac-volume-a-cli-to-control-device-volume/feed/</wfw:commentRss>
  59. <slash:comments>0</slash:comments>
  60. </item>
  61. <item>
  62. <title>Opening Slack Jira Cloud links in the right browser</title>
  63. <link>https://akrabat.com/opening-slack-jira-cloud-app-links-in-the-right-browser/</link>
  64. <comments>https://akrabat.com/opening-slack-jira-cloud-app-links-in-the-right-browser/#respond</comments>
  65. <dc:creator><![CDATA[Rob]]></dc:creator>
  66. <pubDate>Tue, 30 Sep 2025 10:00:00 +0000</pubDate>
  67. <category><![CDATA[Computing]]></category>
  68. <guid isPermaLink="false">https://akrabat.com/?p=7499</guid>
  69.  
  70. <description><![CDATA[I use OpenIn to open links in a given browser when I click on them in other applications. This is really helpful to keep various work related stuff in different browsers or profiles and I find it very helpful. One thing that's bothered me is that links from the Jira Cloud Slack App ignore my OpenIn rules and always open in Safari and I finally sat down to work out why. The investigation I create… <a href="https://akrabat.com/opening-slack-jira-cloud-app-links-in-the-right-browser/">continue reading</a>.]]></description>
  71. <content:encoded><![CDATA[<p>I use <a href="https://loshadki.app/openin4/">OpenIn</a> to open links in a given browser when I click on them in other applications. This is really helpful to keep various work related stuff in different browsers or profiles and I find it very helpful.</p>
  72. <p>One thing that's bothered me is that links from the <a href="https://slack.com/marketplace/A2RPP3NFR-jira-cloud">Jira Cloud Slack App</a> ignore my OpenIn rules and always open in Safari and I finally sat down to work out why.</p>
  73. <h2>The investigation</h2>
  74. <p>I create a new OpenIn rule and enabled multiple browsers so that OpenIn would present a choice window to me.</p>
  75. <p>It looks like this</p>
  76. <picture><source  
  77. srcset="https://akrabat.com/wp-content/uploads/2025/09/2025slack-app-linkopen-in-choices-dark.png"
  78.        media="(prefers-color-scheme: dark)"
  79.    /><source
  80. srcset="https://akrabat.com/wp-content/uploads/2025/09/2025slack-app-linkopen-in-choices-light.png"
  81.        media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
  82.    /><br />
  83.    <img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/09/2025slack-app-linkopen-in-choices-light.png" loading="lazy" alt="Slack app linkopen in choices light." class="no-border" width="266"/>
  84. </picture>
  85. <p>At the bottom, we can see the link that OpenIn has received.</p>
  86. <p>Even though the presented URL in the Slack app is <tt>https://my-client.atlassian.net/browser/PROJ-123</tt>, and if you right click and copy link, that's what you get in your clipboard, when you <em>click</em> on the link, you get a slack.com link.</p>
  87. <p>Copying that link, it's of the form: <tt>https://slack.com/openid/connect/login_initiate_redirect?login_hint=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.blah.blah</tt>. </p>
  88. <p>That's interesting! I've seen that <tt>eyJhbGci</tt> prefix often enough to know on sight that it's the start of base64 encoded <a href="https://www.rfc-editor.org/rfc/rfc7519">JWT</a> and sure enough, base64 decoding it proved that it was.</p>
  89. <p>The format of JWT is <tt>{header}.{payload}.{signature}</tt> and it's unencrypted, so we can inspect the payload easily enough. This is redacted, but it's of the form:</p>
  90. <pre>
  91. {
  92.  "aud" : "123456789.123456789",
  93.  "auth_time" : 1758615553,
  94.  "exp" : 1758619153,
  95.  "https://slack.com/target_uri" : "https://{my-client}.atlassian.net/browse/PROJ-123",
  96.  "https://slack.com/team_id" : "ABCDEFGHI",
  97.  "https://slack.com/user_id" : "U01AB2CDEF3",
  98.  "iat" : 1758615553,
  99.  "iss" : "https://slack.com",
  100.  "sub" : "rob@{my-client}.com"
  101. }
  102. </pre>
  103. <p>We can see that the URL that we want to go to is in the payload's <tt>"https://slack.com/target_uri"</tt> property, so we <em>just</em> need to set up OpenIn to use that URL and pick the appropriate browser.</p>
  104. <h2>OpenIn's custom scripts</h2>
  105. <p>One really nice feature of OpenIn is that you can write <a href="https://loshadki.app/openin4/080-javascript-for-rule/">custom scripts for a rule</a>, so we can use this to extract the payload and pick the browser.</p>
  106. <p>So I read the web page and wrote some Javascript!</p>
  107. <p>The work we need to do is:</p>
  108. <ol>
  109. <li>Extract the payload from the query parameter</li>
  110. <li>JSON decode it</li>
  111. <li>Read the target_uri</li>
  112. <li>Choose browser based on target_uri</li>
  113. </ol>
  114. <p>This needs a few helper functions</p>
  115. <h3>Helper functions</h3>
  116. <p>Extracting the payload is straightforward JS. We need the text between the two <tt>.</tt>s:</p>
  117. <pre lang="javascript">
  118. function extractJwtPayload(str) {
  119.    const parts = str.split('.');
  120.    return parts.length >= 3 ? parts[1] : '';
  121. }
  122. </pre>
  123. <p>As OpenIn uses WebKit's <a href="https://docs.webkit.org/Deep%20Dive/JSC/JavaScriptCore.html">JavaScriptCore</a>, we can decode base64 using <tt>Uint8Array.fromBase64()</tt>:</p>
  124. <pre lang="javascript">
  125. function base64Decode(str) {
  126.    const uint8Array = Uint8Array.fromBase64(str);
  127.    return String.fromCharCode(...uint8Array);
  128. }
  129. </pre>
  130. <p>We also need to select the browser that we want the link to open in. This is done in OpenIn by setting all the "visible" browsers to false, except the one you want:</p>
  131. <pre lang="javascript">
  132. function selectBrowser(browser) {
  133.    let apps = ctx.getApps()
  134.    apps.forEach(function (app) {
  135.        app.visible = (app.name == browser)
  136.    })
  137. }
  138. </pre>
  139. <h3>Do the work</h3>
  140. <p>Having set everything up, we can now find the <tt>target_uri</tt> and choose the browser. </p>
  141. <p>Firstly we only want to do this work if the source app is Slack and that we have a <tt>login_hint</tt> parameter:</p>
  142. <pre lang="javascript">
  143. if (ctx.getSourceApp().path.startsWith("/Applications/Slack.app")
  144.    && ctx.url.searchParams.has('login_hint')) {
  145. </pre>
  146. <p>Extracting the <tt>target_uri</tt> is a case of using the functions we've written:</p>
  147. <pre lang="javascript">
  148. const login_hint = ctx.url.searchParams.get('login_hint');
  149. const jwtPayload = extractJwtPayload(login_hint);
  150. const payload = base64Decode(jwtPayload).replace(/\0/g, '');
  151. const data = JSON.parse(payload);
  152. const target_uri = data['https://slack.com/target_uri'];
  153. </pre>
  154. <p>Note that I discovered some null bytes during testing, so removed them from the decoded string.</p>
  155. <p>Now we set OpenIn's URI and select the browser we want:</p>
  156. <pre>
  157. // Set OpenIn's URL
  158. ctx.url.href=target_uri;
  159.  
  160. // Select the browser
  161. if (target_uri.includes("my-client-1")) {
  162.    selectBrowser("Firefox");
  163.    return;
  164. }
  165.  
  166. if (target_uri.includes("my-client-2")) {
  167.    selectBrowser("Firefox");
  168.    return;
  169. }
  170.  
  171. // Default to Safari
  172. selectBrowser("Safari");
  173. return;
  174. </pre>
  175. <p>and we're done.</p>
  176. <h2>That's it</h2>
  177. <p>That's it! Whenever I click on a Jira link in Slack, the correct browser opens directly to where I want to go.</p>
  178. <p>The full script is here: <a href="https://akrabat.com/wp-content/uploads/2025-09-openin-slack-app-link.js">openin-slack-app-link.js</a></p>
  179. ]]></content:encoded>
  180. <wfw:commentRss>https://akrabat.com/opening-slack-jira-cloud-app-links-in-the-right-browser/feed/</wfw:commentRss>
  181. <slash:comments>0</slash:comments>
  182. </item>
  183. <item>
  184. <title>Fixing PostgreSQL collation version mismatch</title>
  185. <link>https://akrabat.com/fixing-postgresql-collation-version-mismatch/</link>
  186. <comments>https://akrabat.com/fixing-postgresql-collation-version-mismatch/#respond</comments>
  187. <dc:creator><![CDATA[Rob]]></dc:creator>
  188. <pubDate>Tue, 23 Sep 2025 10:00:00 +0000</pubDate>
  189. <category><![CDATA[PostgreSQL]]></category>
  190. <guid isPermaLink="false">https://akrabat.com/?p=7493</guid>
  191.  
  192. <description><![CDATA[After pulling a new version of the Docker PostgreSQL container, I started getting this warning: WARNING: database "dev" has a collation version mismatch DETAIL: The database was created using collation version 2.36, but the operating system provides version 2.41. HINT: Rebuild all objects in this database that use the default collation and run ALTER DATABASE dev REFRESH COLLATION VERSION, or build PostgreSQL with the right library version. This seems to have occurred because the underlying… <a href="https://akrabat.com/fixing-postgresql-collation-version-mismatch/">continue reading</a>.]]></description>
  193. <content:encoded><![CDATA[<p>After pulling a new version of the Docker PostgreSQL container, I started getting this warning:</p>
  194. <pre>
  195. WARNING:  database "dev" has a collation version mismatch
  196. DETAIL:  The database was created using collation version 2.36, but the operating system provides version 2.41.
  197. HINT:  Rebuild all objects in this database that use the default collation and run ALTER DATABASE dev REFRESH COLLATION VERSION, or build PostgreSQL with the right library version.
  198. </pre>
  199. <p>This seems to have occurred because the underlying OS was changed to Trixie from Bullseye for the default image. i.e. I used:</p>
  200. <pre>
  201. image: postgres:17
  202. </pre>
  203. <p>Rather than:</p>
  204. <pre>
  205. image: postgres:17-bookworm
  206. </pre>
  207. <p>This is on my dev machine, so it's not a major problem as usually I can just blow away the database and recreate it. However, I have some test data that I don't want to lose quite yet, so I took a backup using <tt>pg_dump</tt> and then did this instead:</p>
  208. <pre>
  209. REINDEX DATABASE dev;
  210. ALTER DATABASE dev REFRESH COLLATION VERSION;
  211. </pre>
  212. <p>The warning's now gone. </p>
  213. <p>Of course, in production, you'll want to be a bit more careful and a <tt>pg_dump</tt> and load is more likely to avoid any risks of corruption. As ever, YMMV.</p>
  214. ]]></content:encoded>
  215. <wfw:commentRss>https://akrabat.com/fixing-postgresql-collation-version-mismatch/feed/</wfw:commentRss>
  216. <slash:comments>0</slash:comments>
  217. </item>
  218. <item>
  219. <title>Jumping to the end of bash&#039;s history</title>
  220. <link>https://akrabat.com/jumping-to-the-end-of-bashs-history/</link>
  221. <comments>https://akrabat.com/jumping-to-the-end-of-bashs-history/#respond</comments>
  222. <dc:creator><![CDATA[Rob]]></dc:creator>
  223. <pubDate>Tue, 16 Sep 2025 10:00:00 +0000</pubDate>
  224. <category><![CDATA[Command Line]]></category>
  225. <category><![CDATA[Shell Scripting]]></category>
  226. <guid isPermaLink="false">https://akrabat.com/?p=7490</guid>
  227.  
  228. <description><![CDATA[I use bash's history all the time, via ctrl+r and also with the up and down keys; it's wonderful. Sometimes, I want to get back to the end of my history and I recently discovered that there's a shortcut for this: meta+&#62;. It doesn't matter where you are in your history, pressing meta+&#62; jumps you to the end and you have a blank prompt again. I use iTerm2 on my Mac and have my right… <a href="https://akrabat.com/jumping-to-the-end-of-bashs-history/">continue reading</a>.]]></description>
  229. <content:encoded><![CDATA[<p>I use bash's history all the time, via <tt>ctrl+r</tt> and also with the <a href="https://akrabat.com/context-specific-history-at-the-bash-prompt/"><tt>up and down keys</tt></a>; it's wonderful.</p>
  230. <p>Sometimes, I want to get back to the end of my history and I recently discovered that there's a shortcut for this: <tt>meta+&gt;</tt>. It doesn't matter where you are in your history, pressing <tt>meta+&gt;</tt> jumps you to the end and you have a blank prompt again.</p>
  231. <p>I use <a href="https://iterm2.com">iTerm2</a> on my Mac and have my right hand <tt>option</tt> key set to <tt>meta</tt>. This is done in Settings→Profiles→Keys, setting "Right Option (C) key:" to "Esc+".</p>
  232. <p>However, to press <tt>meta+&gt;</tt>, I need to do <tt>right-option+shift+.</tt> which isn't as easy as <tt>right-option+.</tt>, so let's rebind!</p>
  233. <p>To rebind, I looked up the bash command for this functionality (`end-of-history`),  and then added this to my <tt>.bashrc</tt>:</p>
  234. <pre>
  235. bind '"\e.": end-of-history'
  236. </pre>
  237. <p>All done. Now I just press <tt>right-option+.</tt> and I'm back at the end of history as if I'd never navigated it.</p>
  238. ]]></content:encoded>
  239. <wfw:commentRss>https://akrabat.com/jumping-to-the-end-of-bashs-history/feed/</wfw:commentRss>
  240. <slash:comments>0</slash:comments>
  241. </item>
  242. <item>
  243. <title>Converting JWKS JSON to PEM using Python</title>
  244. <link>https://akrabat.com/converting-jwks-json-to-pem-using-python/</link>
  245. <comments>https://akrabat.com/converting-jwks-json-to-pem-using-python/#respond</comments>
  246. <dc:creator><![CDATA[Rob]]></dc:creator>
  247. <pubDate>Tue, 09 Sep 2025 10:00:00 +0000</pubDate>
  248. <category><![CDATA[Command Line]]></category>
  249. <category><![CDATA[Python]]></category>
  250. <guid isPermaLink="false">https://akrabat.com/?p=7484</guid>
  251.  
  252. <description><![CDATA[Following on from my earlier exploration of JWKS (RFC7517), I found myself needing to convert the JWKS into PEM format. This time I turned to Python with my preference of using uv with inline script metadata and created jwks-to-pem.py. The really nice thing about inline script metadata is that we can use the cryptography package to do all the hard work with RSA and serialisation. We just have to remember that the base64 encoded values… <a href="https://akrabat.com/converting-jwks-json-to-pem-using-python/">continue reading</a>.]]></description>
  253. <content:encoded><![CDATA[<p>Following on from my <a href="https://akrabat.com/creating-jwks-json-file-in-php/">earlier exploration of JWKS</a> (<a href="https://www.rfc-editor.org/rfc/rfc7517">RFC7517</a>), I found myself needing to convert the JWKS into PEM format.</p>
  254. <p>This time I turned to Python with my preference of using <a href="https://github.com/astral-sh/uv">uv</a> with <a href="https://akrabat.com/defining-python-dependencies-at-the-top-of-the-file/">inline script metadata</a> and created <a href="#script"><tt>jwks-to-pem.py</tt></a>.</p>
  255. <p>The really nice thing about inline script metadata is that we can use the <a href="https://pypi.org/project/cryptography/">cryptography package</a> to do all the hard work with RSA and serialisation. We just have to remember that the base64 encoded values are <a href="https://datatracker.ietf.org/doc/html/rfc4648#section-5">base64 URL encoded</a> and account for it.</p>
  256. <p>As a single file python script, I make it executable with <tt>chmod +x jwks-to-pem.py</tt> and made it so that I can pipe the output of a <a href="https://curl.se">curl</a> call to it, or pass in a JSON file. I prefer to use the <tt>curl</tt> solution though with:</p>
  257. <pre>
  258. curl -s https://example.com/.well-known/jwks.json | jwks-to-pem.py
  259. </pre>
  260. <h3>Example</h3>
  261. <p>Here's an example from the <a href="https://developer.api.apps.cam.ac.uk/docs/oauth2/1/routes/.well-known/jwks.json/get">University of Cambridge</a>.</p>
  262. <p>On the day I wrote this article, the JWKS looks like this:</p>
  263. <pre>
  264. $ curl -s https://api.apps.cam.ac.uk/oauth2/v1/.well-known/jwks.json
  265. {
  266.  "keys": [
  267.    {
  268.      "alg": "RS256",
  269.      "e": "AQAB",
  270.      "n": "6kKjjctVPalX0ypJ2irwog8xIXS9JTABqrSnK_n3YJ4q0aH2-1bjGbWz8p1CaCUqDxQSDuqzvOgMdNGvZrxlNJ-G8hfc39jrb_KnB0T3ZsuxFz6X0mDzmHhdiPjSDK3M0syC4qg5_PB7xwKail5VWOcY0SypIYCPD6Ct5DGnQ_XONGXIVG7eaJAHdxJp2BOz0n3BVEFnZUgM5JcfGrSFfGqb0ZotX2AblwjZKQc58E0EVVykJw8gxW1Bob8rbaVXlMHssfY-9Jx0zua7ZrjO5C4OMmt9J6zYbVnGVwf62ehGtcLSP6iCG4_XM2sAMQwqJnPBss0U9WwDERk17FMHvb_FBwxAFxRygd0DclWmQmCYr5uFYck57KGARtyoxrNNAf4AFUHuObjbV24TyInYEgMhKi3SAML_4ke3dbbG-mjchXPN9OqNd4fydnQIP39WFHmFNk_nIlqvYnALI4xPE-w09T9jCvjU8hYHHlVMRvRluBnUzJkFnxLse5W-agC6ITe3wYvKH7SHVp6MYQWVD_0I2rCLV4gqjSpXzKIMs5eejjTQQq0VYumgL_f1ETvzDoewzXLOC8GGu2LZDwDbP0ea6DchReWjZfj4nJx23uQyGAj1h_uPI1jCd9oeJhbN8jFz2ltYgXYBp51qdSzsbtdec9BPPBVeXjI--c0AWU8",
  271.      "kid": "70e0ed3c",
  272.      "kty": "RSA",
  273.      "use": "sig"
  274.    }
  275.  ]
  276. }
  277. </pre>
  278. <p>They very kindly pretty-print it too!</p>
  279. <p>We can then get the PEM version by piping to <tt>jwks-to-pem.py</tt>:</p>
  280. <pre>
  281. $ curl -s https://api.apps.cam.ac.uk/oauth2/v1/.well-known/jwks.json | jwks-to-pem.py
  282. # Key 0 (kid: 70e0ed3c)
  283. -----BEGIN PUBLIC KEY-----
  284. MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6kKjjctVPalX0ypJ2irw
  285. og8xIXS9JTABqrSnK/n3YJ4q0aH2+1bjGbWz8p1CaCUqDxQSDuqzvOgMdNGvZrxl
  286. NJ+G8hfc39jrb/KnB0T3ZsuxFz6X0mDzmHhdiPjSDK3M0syC4qg5/PB7xwKail5V
  287. WOcY0SypIYCPD6Ct5DGnQ/XONGXIVG7eaJAHdxJp2BOz0n3BVEFnZUgM5JcfGrSF
  288. fGqb0ZotX2AblwjZKQc58E0EVVykJw8gxW1Bob8rbaVXlMHssfY+9Jx0zua7ZrjO
  289. 5C4OMmt9J6zYbVnGVwf62ehGtcLSP6iCG4/XM2sAMQwqJnPBss0U9WwDERk17FMH
  290. vb/FBwxAFxRygd0DclWmQmCYr5uFYck57KGARtyoxrNNAf4AFUHuObjbV24TyInY
  291. EgMhKi3SAML/4ke3dbbG+mjchXPN9OqNd4fydnQIP39WFHmFNk/nIlqvYnALI4xP
  292. E+w09T9jCvjU8hYHHlVMRvRluBnUzJkFnxLse5W+agC6ITe3wYvKH7SHVp6MYQWV
  293. D/0I2rCLV4gqjSpXzKIMs5eejjTQQq0VYumgL/f1ETvzDoewzXLOC8GGu2LZDwDb
  294. P0ea6DchReWjZfj4nJx23uQyGAj1h/uPI1jCd9oeJhbN8jFz2ltYgXYBp51qdSzs
  295. btdec9BPPBVeXjI++c0AWU8CAwEAAQ==
  296. -----END PUBLIC KEY-----
  297.  
  298. </pre>
  299. <h3>The script</h3>
  300. <p>This is the script in case anyone else finds it useful:</p>
  301. <pre lang="python" id="script">
  302. #!/usr/bin/env -S uv run --script --quiet
  303. # /// script
  304. # dependencies = [
  305. #   "cryptography",
  306. # ]
  307. # ///
  308.  
  309. """Convert JWK keys to PEM format.
  310.  
  311. This script reads .well-known/jwks.json and outputs PEM encoded versions
  312. of the public keys in that file.
  313.  
  314. Usage:
  315.    curl -s https://example.com/.well-known/jwks.json | jwks-to-pem.py
  316.    uv run jwks-to-pem.py jwks.json
  317.    uv run jwks-to-pem.py < jwks.json
  318.  
  319. Requirements:
  320.    - uv (https://github.com/astral-sh/uv)
  321.    - cryptography library
  322.  
  323. Author:
  324.    Rob Allen <rob@akrabat.com>
  325.    Copyright 2025
  326.  
  327. License:
  328.    MIT License - https://opensource.org/licenses/MIT
  329. """
  330.  
  331. import json
  332. import base64
  333. import sys
  334. from cryptography.hazmat.primitives import serialization
  335. from cryptography.hazmat.primitives.asymmetric import rsa
  336.  
  337. def base64url_decode(data):
  338.    """Decode base64url to bytes"""
  339.    # Add padding if needed
  340.    padding = 4 - len(data) % 4
  341.    if padding != 4:
  342.        data += '=' * padding
  343.  
  344.    # Replace URL-safe chars
  345.    data = data.replace('-', '+').replace('_', '/')
  346.  
  347.    # Decode
  348.    return base64.b64decode(data)
  349.  
  350. def jwk_to_pem(jwk_key):
  351.    """Convert JWK to PEM format"""
  352.    if jwk_key['kty'] != 'RSA':
  353.        raise ValueError("Only RSA keys are supported")
  354.  
  355.    # Decode the modulus (n) and exponent (e) to int
  356.    n = int.from_bytes(base64url_decode(jwk_key['n']), 'big')
  357.    e = int.from_bytes(base64url_decode(jwk_key['e']), 'big')
  358.  
  359.    # Create RSA public key
  360.    public_key = rsa.RSAPublicNumbers(e, n).public_key()
  361.  
  362.    # Serialize to PEM
  363.    pem = public_key.public_bytes(
  364.        encoding=serialization.Encoding.PEM,
  365.        format=serialization.PublicFormat.SubjectPublicKeyInfo
  366.    )
  367.    return pem.decode()
  368.  
  369. def main():
  370.    if len(sys.argv) > 2:
  371.        print("Usage: jwk_to_pem.py [jwks.json]")
  372.        print("If no file is provided, reads from stdin")
  373.        sys.exit(1)
  374.  
  375.    if len(sys.argv) == 2 and sys.argv[1] != '-':
  376.        # Read from file
  377.        with open(sys.argv[1], 'r') as f:
  378.            jwks = json.load(f)
  379.    else:
  380.        # Read from stdin
  381.        jwks = json.load(sys.stdin)
  382.  
  383.    # Convert each key
  384.    for i, key in enumerate(jwks['keys']):
  385.        kid = key.get('kid', f'key-{i}')
  386.        print(f"# Key {i} (kid: {kid})")
  387.        print(jwk_to_pem(key))
  388.  
  389. if __name__ == "__main__":
  390.    main()
  391. </pre>
  392. ]]></content:encoded>
  393. <wfw:commentRss>https://akrabat.com/converting-jwks-json-to-pem-using-python/feed/</wfw:commentRss>
  394. <slash:comments>0</slash:comments>
  395. </item>
  396. <item>
  397. <title>Stop in-place editing of bash history items</title>
  398. <link>https://akrabat.com/stop-in-place-editing-of-bash-history-items/</link>
  399. <comments>https://akrabat.com/stop-in-place-editing-of-bash-history-items/#comments</comments>
  400. <dc:creator><![CDATA[Rob]]></dc:creator>
  401. <pubDate>Tue, 02 Sep 2025 10:00:00 +0000</pubDate>
  402. <category><![CDATA[Command Line]]></category>
  403. <category><![CDATA[Shell Scripting]]></category>
  404. <guid isPermaLink="false">https://akrabat.com/?p=7480</guid>
  405.  
  406. <description><![CDATA[Recently, since getting a new computer, I've noticed that I've been losing bash history items and it took a while to work out what was going on, though I'm still not completely sure as it never seemed to be so much of a problem. I regularly use the up and down keys with context specific history. For example, I will type ma and then press up to search back through all the make commands I've… <a href="https://akrabat.com/stop-in-place-editing-of-bash-history-items/">continue reading</a>.]]></description>
  407. <content:encoded><![CDATA[<p>Recently, since getting a new computer, I've noticed that I've been losing bash history items and it took a while to work out what was going on, though I'm still not completely sure as it never seemed to be so much of a problem. </p>
  408. <p>I regularly use the up and down keys with <a href="/context-specific-history-at-the-bash-prompt/">context specific history</a>. For example, I will type <tt>ma</tt> and then press up to search back through all the <tt>make</tt> commands I've used recently and then press enter to run it. </p>
  409. <p>Sometimes, I'll realise that I don't actually want this command and edit it and press enter. Sometimes I'll decide halfway through editing that really I should use a <tt>docker compose</tt> command instead and I'll just back out of my edit via some key stroke that works. I'm not sure what I do here though, probably up/down, or maybe ctrl+c. Whatever I do, sometimes, the history for that line is now my edited mess and not the original command. Then later, when I go to try and find it via the up arrow, it's missing.</p>
  410. <p>This happened infrequently enough that I thought I was misremembering what was in the history, or that maybe it was another tab I was thinking about.</p>
  411. <p>I never want the bash history to be editable; if I cancel out, then I want it back to what it was.</p>
  412. <h2>Fixing with revert-all-at-newline</h2>
  413. <p>This finally annoyed me enough that I sat down with the Internet to work out how to fix it with the <tt>revert-all-at-newline</tt> setting.</p>
  414. <p>The revert-all-at-newline option in bash controls whether readline reverts any changes made to a history line when you press Enter. Note that this is part of readline's behavior, so it affects command line editing in bash and other programs that use <a href="https://en.wikipedia.org/wiki/GNU_Readline">readline</a>.</p>
  415. <p>The simplest thing is to add this to <tt>.bashrc</tt>:</p>
  416. <pre lang="bash">
  417. bind 'set revert-all-at-newline on'
  418. </pre>
  419. <p>Alternatively, you can create a <tt>.inputrc</tt> file with this in it:</p>
  420. <pre>
  421. $include /etc/inputrc
  422. set revert-all-at-newline on
  423. </pre>
  424. <p>To view the current value of <tt>revert-all-at-newline</tt>, use: </p>
  425. <pre>
  426. bind -V | grep revert-all-at-newline
  427. </pre>
  428. <p>It solved my problem, and I've not yet found a case when I want it set the other way.</p>
  429. ]]></content:encoded>
  430. <wfw:commentRss>https://akrabat.com/stop-in-place-editing-of-bash-history-items/feed/</wfw:commentRss>
  431. <slash:comments>1</slash:comments>
  432. </item>
  433. <item>
  434. <title>Extending an OpenAPI Component Schema</title>
  435. <link>https://akrabat.com/extending-an-openapi-component-schema/</link>
  436. <comments>https://akrabat.com/extending-an-openapi-component-schema/#respond</comments>
  437. <dc:creator><![CDATA[Rob]]></dc:creator>
  438. <pubDate>Tue, 26 Aug 2025 10:00:00 +0000</pubDate>
  439. <category><![CDATA[OpenAPI]]></category>
  440. <guid isPermaLink="false">https://akrabat.com/?p=7477</guid>
  441.  
  442. <description><![CDATA[One project that I'm working on uses RFC 9457 Problem Details for HTTP APIs for its error responses. In the OpenAPI spec, we can define this as a component and use in the relevant paths as appropriate: components: schemas: ProblemDetails: type: object properties: type: type: string format: uri-reference description: A URI reference that identifies the problem type default: about:blank example: https://example.com/probs/out-of-credit title: type: string description: A short, human-readable summary of the problem type example: You… <a href="https://akrabat.com/extending-an-openapi-component-schema/">continue reading</a>.]]></description>
  443. <content:encoded><![CDATA[<p>One project that I'm working on uses <a href="https://www.rfc-editor.org/rfc/rfc9457.html">RFC 9457 Problem Details for HTTP APIs </a> for its error responses.</p>
  444. <p>In the OpenAPI spec, we can define this as a component and use in the relevant paths as appropriate:</p>
  445. <pre lang="yaml">
  446. components:
  447.  schemas:
  448.    ProblemDetails:
  449.      type: object
  450.      properties:
  451.        type:
  452.          type: string
  453.          format: uri-reference
  454.          description: A URI reference that identifies the problem type
  455.          default: about:blank
  456.          example: https://example.com/probs/out-of-credit
  457.        title:
  458.          type: string
  459.          description: A short, human-readable summary of the problem type
  460.          example: You do not have enough credit.
  461.        status:
  462.          type: integer
  463.          format: int32
  464.          description: The HTTP status code for this occurrence of the problem
  465.          minimum: 100
  466.          maximum: 599
  467.          example: 403
  468.        detail:
  469.          type: string
  470.          description: A human-readable explanation specific to this occurrence of the problem
  471.          example: Your current balance is 30, but that costs 50.
  472.        instance:
  473.          type: string
  474.          format: uri-reference
  475.          description: A URI reference that identifies the specific occurrence of the problem
  476.          example: /account/12345/msgs/abc
  477.      additionalProperties: true
  478. </pre>
  479. <p>When we return a validation error, we add an <tt>errors</tt> property. Rather than repeating the <tt>ProblemDetails</tt> properties into <tt>ValidationError</tt>, we can add the <tt>errors</tt> using <a href="https://json-schema.org/understanding-json-schema/reference/combining#allOf"><tt>allOf</tt></a>:</p>
  480. <pre lang="yaml">
  481.    ValidationError:
  482.      allOf:
  483.        - $ref: '#/components/schemas/ProblemDetails'
  484.        - type: object
  485.          properties:
  486.            errors:
  487.              type: object
  488.              description: Field-specific validation error messages
  489.              additionalProperties:
  490.                type: string
  491.              example:
  492.                name: "name must be provided"
  493.                dateOfBirth: "date must be in the past"
  494. </pre>
  495. <p>This is quite handy!</p>
  496. ]]></content:encoded>
  497. <wfw:commentRss>https://akrabat.com/extending-an-openapi-component-schema/feed/</wfw:commentRss>
  498. <slash:comments>0</slash:comments>
  499. </item>
  500. <item>
  501. <title>Saving the current URL to a Note</title>
  502. <link>https://akrabat.com/saving-the-current-url-to-a-note/</link>
  503. <comments>https://akrabat.com/saving-the-current-url-to-a-note/#respond</comments>
  504. <dc:creator><![CDATA[Rob]]></dc:creator>
  505. <pubDate>Tue, 19 Aug 2025 10:00:00 +0000</pubDate>
  506. <category><![CDATA[Mac]]></category>
  507. <category><![CDATA[Shortcuts]]></category>
  508. <guid isPermaLink="false">https://akrabat.com/?p=7467</guid>
  509.  
  510. <description><![CDATA[Inspired by John Gruber mentioning on the Cortex podcast that he has a shortcut that saves links to a note in Tot, I thought I'd do something similar for saving to a note in Apple Notes. I want to store as a bullet item containing the name of the page, the link and the date. Something like this: (Funny that the spellchecker doesn't know that Thu is the short form for Thursday) The Save Links… <a href="https://akrabat.com/saving-the-current-url-to-a-note/">continue reading</a>.]]></description>
  511. <content:encoded><![CDATA[<p>Inspired by John Gruber mentioning on the <a href="https://www.relay.fm/cortex/169">Cortex podcast</a> that he has a shortcut that saves links to a note in <a href="https://tot.rocks">Tot</a>, I thought I'd do something similar for saving to a note in Apple Notes.</p>
  512. <p>I want to store as a bullet item containing the name of the page, the link and the date. Something like this:</p>
  513. <picture><source  
  514. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025saved-link-text-dark.png"
  515.        media="(prefers-color-scheme: dark)"
  516.    /><source
  517. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025saved-link-text-light.png"
  518.        media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
  519.    /><br />
  520.    <img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025saved-link-text-light.png" loading="lazy" alt="Saved link text light." class="border" width="408"/>
  521. </picture>
  522. <p>(Funny that the spellchecker doesn't know that Thu is the short form for Thursday)</p>
  523. <h2>The <em>Save Links to Notes</em> Shortcut</h2>
  524. <p>This is the shortcut that I created to do it:
  525. <picture><source  
  526. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-shortcut-dark.png"
  527.        media="(prefers-color-scheme: dark)"
  528.    /><source
  529. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-shortcut-light.png"
  530.        media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
  531.    /><br />
  532.    <img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-shortcut-light.png" loading="lazy" alt="Save links to notes shortcut light." class="noborder" width="558"/>
  533. </picture>
  534. <p>You can download it here:<br />
  535. <a href="https://www.icloud.com/shortcuts/6c12e888f9da431d9977985b229d636b">https://www.icloud.com/shortcuts/6c12e888f9da431d9977985b229d636b</a></p>
  536. <h2>Breaking down the actions</h2>
  537. <p>To get the URL into the shortcut, we want:</p>
  538. <ul>
  539. <li><em>Show in Share Sheet</em> so that it's available on iOS/iPadOS</li>
  540. <li><em>Receive What's Onscreen</em> so that when a browser is focussed on Mac, it finds the URL</li>
  541. <li><em>Use as a Quick Action</em> so that we can assign a keyboard shortcut (<tt style="font-family: sans-serif">⌃⌥⌘U</tt> in case)</li>
  542. </ul>
  543. <p>We can then use <em>Get Contents of web page</em> along with <em>Get Details of Safari Web Page</em> to get the pages's title which Shortcuts calls <em>Name</em> for some reason.</p>
  544. <p>There's an action for <em>Current Date</em>, so we add that to get the variable.</p>
  545. <p>Creating formatted text in a note is a little involved. Firstly we use a <em>Text</em> action to set out the Markdown that we want. I used the date format <tt>EEE, dd MMM yyyy</tt> as it's short and clear to me.</p>
  546. <p>There's a <em>Make Rich Text from Markdown</em> action which processes the Markdown for us, but if you just append it to the note, it doesn't work. The workaround is to add it to a <em>List</em> action and then append the list to the note. </p>
  547. <h2>That's it</h2>
  548. <p>All we need to do now is show a notification including the <tt>Shortcut Input</tt> variable as that's the URL that we've just saved.
  549. <picture><source  
  550. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-notification-dark.png"
  551.        media="(prefers-color-scheme: dark)"
  552.    /><source
  553. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-notification-light.png"
  554.        media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
  555.    /><br />
  556.    <img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-notification-light.png" loading="lazy" alt="Save links to notes notification light." class="noborder" width="376"/>
  557. </picture>
  558. <p>With this shortcut, I can add a new entry to my note from both my Mac, iPad and iPhone with minimal effort. </p>
  559. <p>I like it.</p>
  560. ]]></content:encoded>
  561. <wfw:commentRss>https://akrabat.com/saving-the-current-url-to-a-note/feed/</wfw:commentRss>
  562. <slash:comments>0</slash:comments>
  563. </item>
  564. <item>
  565. <title>Accessing Longplay info for SwiftBar</title>
  566. <link>https://akrabat.com/accessing-longplay-info-for-swiftbar/</link>
  567. <comments>https://akrabat.com/accessing-longplay-info-for-swiftbar/#respond</comments>
  568. <dc:creator><![CDATA[Rob]]></dc:creator>
  569. <pubDate>Tue, 12 Aug 2025 10:00:00 +0000</pubDate>
  570. <category><![CDATA[Mac]]></category>
  571. <guid isPermaLink="false">https://akrabat.com/?p=7447</guid>
  572.  
  573. <description><![CDATA[One app that I find incredibly useful is SwiftBar and one use I have is to display track info for the currently playing song in Apple Music. SwiftBar plugins work as shell scripts that execute on a timer and echo specially formatted text which SwiftBar then turns into an item on the menu bar with an attached menu I use a heavily modified Now Playing plugin that was originally written by Adam Kenyon, so all… <a href="https://akrabat.com/accessing-longplay-info-for-swiftbar/">continue reading</a>.]]></description>
  574. <content:encoded><![CDATA[<p>One app that I find incredibly useful is <a href="https://github.com/swiftbar/SwiftBar">SwiftBar</a> and one use I have is to display track info for the currently playing song in Apple Music.</p>
  575. <p>SwiftBar plugins work as shell scripts that execute on a timer and echo specially formatted text which SwiftBar then turns into an item on the menu bar with an attached menu</p>
  576. <p>I use a heavily modified <a href="https://github.com/matryer/xbar-plugins/blob/main/Music/nowplaying.5s.sh">Now Playing plugin</a> that was originally written by Adam Kenyon, so all the hard work was done by them.</p>
  577. <p>Recently, I've been using <a href="https://longplay.rocks">Longplay</a> to play albums and wanted the same functionality.</p>
  578. <p>Now Playing uses AppleScript to determine if a music player is playing and what the track info is:</p>
  579. <pre>
  580. app_playing=$(osascript -e "tell application \"$i\" to player state as string")
  581. </pre>
  582. <p>And</p>
  583. <pre>
  584. track=$(osascript -e "tell application \"$app\" to name of current track")
  585. artist=$(osascript -e "tell application \"$app\" to artist of current track")
  586. </pre>
  587. <p>When looking at adding Longplay, I was pleased to discover that it has AppleScript support, but perusing the Dictionary, I discovered that it doesn't support the features I need here.</p>
  588. <p>Upon emailing the developer, they very helpfully pointed out that Longplay also has Shortcuts support and that I could probably use that instead. They were right.</p>
  589. <p>I knocked up a couple of shortcuts:</p>
  590. <picture><source  
  591. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-status-dark.png"
  592.        media="(prefers-color-scheme: dark)"
  593.    /><source
  594. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-status-light.png"
  595.        media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
  596.    /><br />
  597.    <img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-status-light.png" loading="lazy" alt="Longplay status Apple Shortcut" class="noborder" width="431"/>
  598. </picture>
  599. <p>and</p>
  600. <picture><source  
  601. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-now-playing-dark.png"
  602.        media="(prefers-color-scheme: dark)"
  603.    /><source
  604. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-now-playing-light.png"
  605.        media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
  606.    /><br />
  607.    <img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-now-playing-light.png" loading="lazy" alt="Longplay now playing light." class="noborder" width="431"/>
  608. </picture>
  609. <p>With these set-up, I can now run them from the command line using <tt>shortcuts</tt>:</p>
  610. <pre>
  611. app_playing=$(shortcuts run "Longplay status");
  612. </pre>
  613. <p>This will set <tt>$app_playing</tt> to either "Yes" or "No" as strings as it is defined as boolean in Shortcuts.</p>
  614. <pre>
  615. track=$(shortcuts run "Longplay now playing");
  616. </pre>
  617. <p>This simply sets <tt>$track</tt> to the string of the currently playing track.</p>
  618. <h2>Updated Now Playing script</h2>
  619. <p>With the ability to get the info I needed from the command line, I <s>hacked</s> updated my copy of the Now Playing script and all is good.</p>
  620. <p>I've updated it a bit over the years, so I've uploaded my version to Gist: <a href="https://gist.github.com/akrabat/8bcfac9dfef5fd4e9b67ac5bb504ea7a">nowplaying.5s.sh</a>.</p>
  621. ]]></content:encoded>
  622. <wfw:commentRss>https://akrabat.com/accessing-longplay-info-for-swiftbar/feed/</wfw:commentRss>
  623. <slash:comments>0</slash:comments>
  624. </item>
  625. <item>
  626. <title>Responding to StreamDeck buttons with Keyboard Maestro</title>
  627. <link>https://akrabat.com/controlling-the-streamdeck-via-keyboard-maestro/</link>
  628. <comments>https://akrabat.com/controlling-the-streamdeck-via-keyboard-maestro/#respond</comments>
  629. <dc:creator><![CDATA[Rob]]></dc:creator>
  630. <pubDate>Tue, 05 Aug 2025 10:00:00 +0000</pubDate>
  631. <category><![CDATA[AppleScript]]></category>
  632. <category><![CDATA[Keyboard Maestro]]></category>
  633. <category><![CDATA[Mac]]></category>
  634. <guid isPermaLink="false">https://akrabat.com/?p=7440</guid>
  635.  
  636. <description><![CDATA[I run Apple Music on my Mac desktop and send the output to my HomePod minis. To control the volume, you need to manipulate the Apple Music volume slider rather than the global volume controls for the Mac. It's easier to press buttons than use a mouse, so I used Keyboard Maestro to respond to two buttons on my Stream Deck instead. This is possible because Keyboard Maestro has a Stream Deck Plugin, so you… <a href="https://akrabat.com/controlling-the-streamdeck-via-keyboard-maestro/">continue reading</a>.]]></description>
  637. <content:encoded><![CDATA[<p>I run Apple Music on my Mac desktop and send the output to my HomePod minis. To control the volume, you need to manipulate the Apple Music volume slider rather than the global volume controls for the Mac.</p>
  638. <p>It's easier to press buttons than use a mouse, so I used <a href="https://www.keyboardmaestro.com/">Keyboard Maestro</a> to respond to two buttons on my <a href="https://www.elgato.com/uk/en/p/stream-deck">Stream Deck</a> instead.</p>
  639. <p>This is possible because Keyboard Maestro has a <a href="https://marketplace.elgato.com/product/keyboard-maestro-35c7590b-b7fb-4be0-9e5d-9fd4b4c0f013">Stream Deck Plugin</a>, so you need to install that first.</p>
  640. <h2>Setting up the Stream Deck button</h2>
  641. <p>You can now assign Keyboard Maestro to a button in the Stream Deck software:</p>
  642. <p><img fetchpriority="high" decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025streamdeck-km-button.png" alt="Keyboard Maestro automation configuration interface showing a button setup with a speaker/volume icon. The interface displays fields for Title (empty), Button ID (R3C1), Virtual Row (3), and Virtual Column (1). The left side shows a black square button with white speaker and minus icons." title="streamdeck-km-button.png" border="0" width="500" height="239" /></p>
  643. <p>This is the configuration for my volume down button, as you can tell by the icon I chose. The Button ID defaults to the row and column number of where you have placed it on the Stream Deck.</p>
  644. <h2>Responding to the button in Keyboard Maestro</h2>
  645. <p>On the Keyboard Maestro side, we need a macro that is trigged by the Stream Deck button. This is easy to do as it looks like a USB device key and you can press the button the Stream Deck and Keyboard Maestro will recognise it and fill in the correct details.</p>
  646. <p><img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025streamdeck-km-control.png" alt="Screenshot of an Keyboard Maestro automation interface showing a "Music volume down" macro. The trigger is a Stream Deck R3C1 button press with modifiers. The action executes AppleScript code that decreases the Music app's volume by 1, with a minimum volume of 0." title="streamdeck-km-control.png" border="0" width="500" height="529" /></p>
  647. <p>Upon clicking the button, we simply run some AppleScript to control the Music app's volume.</p>
  648. <h2>That's it</h2>
  649. <p>That's all there is to responding to a button on the Stream Deck on a Mac. In this case, I'm using AppleScript, but Keyboard Maestro lets you do practically anything on the computer!</p>
  650. ]]></content:encoded>
  651. <wfw:commentRss>https://akrabat.com/controlling-the-streamdeck-via-keyboard-maestro/feed/</wfw:commentRss>
  652. <slash:comments>0</slash:comments>
  653. </item>
  654. </channel>
  655. </rss>
  656.  

If you would like to create a banner that links to this page (i.e. this validation result), do the following:

  1. Download the "valid RSS" banner.

  2. Upload the image to your own server. (This step is important. Please do not link directly to the image on this server.)

  3. Add this HTML to your page (change the image src attribute if necessary):

If you would like to create a text link instead, here is the URL you can use:

http://www.feedvalidator.org/check.cgi?url=http%3A//akrabat.com/feed/

Copyright © 2002-9 Sam Ruby, Mark Pilgrim, Joseph Walton, and Phil Ringnalda