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>Fri, 29 Aug 2025 11:38:40 +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.2</generator>
  22. <item>
  23. <title>Jumping to the end of bash&#039;s history</title>
  24. <link>https://akrabat.com/jumping-to-the-end-of-bashs-history/</link>
  25. <comments>https://akrabat.com/jumping-to-the-end-of-bashs-history/#respond</comments>
  26. <dc:creator><![CDATA[Rob]]></dc:creator>
  27. <pubDate>Tue, 16 Sep 2025 10:00:00 +0000</pubDate>
  28. <category><![CDATA[Command Line]]></category>
  29. <category><![CDATA[Shell Scripting]]></category>
  30. <guid isPermaLink="false">https://akrabat.com/?p=7490</guid>
  31.  
  32. <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>
  33. <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>
  34. <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>
  35. <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>
  36. <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>
  37. <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>
  38. <pre>
  39. bind '"\e.": end-of-history'
  40. </pre>
  41. <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>
  42. ]]></content:encoded>
  43. <wfw:commentRss>https://akrabat.com/jumping-to-the-end-of-bashs-history/feed/</wfw:commentRss>
  44. <slash:comments>0</slash:comments>
  45. </item>
  46. <item>
  47. <title>Converting JWKS JSON to PEM using Python</title>
  48. <link>https://akrabat.com/converting-jwks-json-to-pem-using-python/</link>
  49. <comments>https://akrabat.com/converting-jwks-json-to-pem-using-python/#respond</comments>
  50. <dc:creator><![CDATA[Rob]]></dc:creator>
  51. <pubDate>Tue, 09 Sep 2025 10:00:00 +0000</pubDate>
  52. <category><![CDATA[Command Line]]></category>
  53. <category><![CDATA[Python]]></category>
  54. <guid isPermaLink="false">https://akrabat.com/?p=7484</guid>
  55.  
  56. <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>
  57. <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>
  58. <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>
  59. <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>
  60. <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>
  61. <pre>
  62. curl -s https://example.com/.well-known/jwks.json | jwks-to-pem.py
  63. </pre>
  64. <h3>Example</h3>
  65. <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>
  66. <p>On the day I wrote this article, the JWKS looks like this:</p>
  67. <pre>
  68. $ curl -s https://api.apps.cam.ac.uk/oauth2/v1/.well-known/jwks.json
  69. {
  70.  "keys": [
  71.    {
  72.      "alg": "RS256",
  73.      "e": "AQAB",
  74.      "n": "6kKjjctVPalX0ypJ2irwog8xIXS9JTABqrSnK_n3YJ4q0aH2-1bjGbWz8p1CaCUqDxQSDuqzvOgMdNGvZrxlNJ-G8hfc39jrb_KnB0T3ZsuxFz6X0mDzmHhdiPjSDK3M0syC4qg5_PB7xwKail5VWOcY0SypIYCPD6Ct5DGnQ_XONGXIVG7eaJAHdxJp2BOz0n3BVEFnZUgM5JcfGrSFfGqb0ZotX2AblwjZKQc58E0EVVykJw8gxW1Bob8rbaVXlMHssfY-9Jx0zua7ZrjO5C4OMmt9J6zYbVnGVwf62ehGtcLSP6iCG4_XM2sAMQwqJnPBss0U9WwDERk17FMHvb_FBwxAFxRygd0DclWmQmCYr5uFYck57KGARtyoxrNNAf4AFUHuObjbV24TyInYEgMhKi3SAML_4ke3dbbG-mjchXPN9OqNd4fydnQIP39WFHmFNk_nIlqvYnALI4xPE-w09T9jCvjU8hYHHlVMRvRluBnUzJkFnxLse5W-agC6ITe3wYvKH7SHVp6MYQWVD_0I2rCLV4gqjSpXzKIMs5eejjTQQq0VYumgL_f1ETvzDoewzXLOC8GGu2LZDwDbP0ea6DchReWjZfj4nJx23uQyGAj1h_uPI1jCd9oeJhbN8jFz2ltYgXYBp51qdSzsbtdec9BPPBVeXjI--c0AWU8",
  75.      "kid": "70e0ed3c",
  76.      "kty": "RSA",
  77.      "use": "sig"
  78.    }
  79.  ]
  80. }
  81. </pre>
  82. <p>They very kindly pretty-print it too!</p>
  83. <p>We can then get the PEM version by piping to <tt>jwks-to-pem.py</tt>:</p>
  84. <pre>
  85. $ curl -s https://api.apps.cam.ac.uk/oauth2/v1/.well-known/jwks.json | jwks-to-pem.py
  86. # Key 0 (kid: 70e0ed3c)
  87. -----BEGIN PUBLIC KEY-----
  88. MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6kKjjctVPalX0ypJ2irw
  89. og8xIXS9JTABqrSnK/n3YJ4q0aH2+1bjGbWz8p1CaCUqDxQSDuqzvOgMdNGvZrxl
  90. NJ+G8hfc39jrb/KnB0T3ZsuxFz6X0mDzmHhdiPjSDK3M0syC4qg5/PB7xwKail5V
  91. WOcY0SypIYCPD6Ct5DGnQ/XONGXIVG7eaJAHdxJp2BOz0n3BVEFnZUgM5JcfGrSF
  92. fGqb0ZotX2AblwjZKQc58E0EVVykJw8gxW1Bob8rbaVXlMHssfY+9Jx0zua7ZrjO
  93. 5C4OMmt9J6zYbVnGVwf62ehGtcLSP6iCG4/XM2sAMQwqJnPBss0U9WwDERk17FMH
  94. vb/FBwxAFxRygd0DclWmQmCYr5uFYck57KGARtyoxrNNAf4AFUHuObjbV24TyInY
  95. EgMhKi3SAML/4ke3dbbG+mjchXPN9OqNd4fydnQIP39WFHmFNk/nIlqvYnALI4xP
  96. E+w09T9jCvjU8hYHHlVMRvRluBnUzJkFnxLse5W+agC6ITe3wYvKH7SHVp6MYQWV
  97. D/0I2rCLV4gqjSpXzKIMs5eejjTQQq0VYumgL/f1ETvzDoewzXLOC8GGu2LZDwDb
  98. P0ea6DchReWjZfj4nJx23uQyGAj1h/uPI1jCd9oeJhbN8jFz2ltYgXYBp51qdSzs
  99. btdec9BPPBVeXjI++c0AWU8CAwEAAQ==
  100. -----END PUBLIC KEY-----
  101.  
  102. </pre>
  103. <h3>The script</h3>
  104. <p>This is the script in case anyone else finds it useful:</p>
  105. <pre lang="python" id="script">
  106. #!/usr/bin/env -S uv run --script --quiet
  107. # /// script
  108. # dependencies = [
  109. #   "cryptography",
  110. # ]
  111. # ///
  112.  
  113. """Convert JWK keys to PEM format.
  114.  
  115. This script reads .well-known/jwks.json and outputs PEM encoded versions
  116. of the public keys in that file.
  117.  
  118. Usage:
  119.    curl -s https://example.com/.well-known/jwks.json | jwks-to-pem.py
  120.    uv run jwks-to-pem.py jwks.json
  121.    uv run jwks-to-pem.py < jwks.json
  122.  
  123. Requirements:
  124.    - uv (https://github.com/astral-sh/uv)
  125.    - cryptography library
  126.  
  127. Author:
  128.    Rob Allen <rob@akrabat.com>
  129.    Copyright 2025
  130.  
  131. License:
  132.    MIT License - https://opensource.org/licenses/MIT
  133. """
  134.  
  135. import json
  136. import base64
  137. import sys
  138. from cryptography.hazmat.primitives import serialization
  139. from cryptography.hazmat.primitives.asymmetric import rsa
  140.  
  141. def base64url_decode(data):
  142.    """Decode base64url to bytes"""
  143.    # Add padding if needed
  144.    padding = 4 - len(data) % 4
  145.    if padding != 4:
  146.        data += '=' * padding
  147.  
  148.    # Replace URL-safe chars
  149.    data = data.replace('-', '+').replace('_', '/')
  150.  
  151.    # Decode
  152.    return base64.b64decode(data)
  153.  
  154. def jwk_to_pem(jwk_key):
  155.    """Convert JWK to PEM format"""
  156.    if jwk_key['kty'] != 'RSA':
  157.        raise ValueError("Only RSA keys are supported")
  158.  
  159.    # Decode the modulus (n) and exponent (e) to int
  160.    n = int.from_bytes(base64url_decode(jwk_key['n']), 'big')
  161.    e = int.from_bytes(base64url_decode(jwk_key['e']), 'big')
  162.  
  163.    # Create RSA public key
  164.    public_key = rsa.RSAPublicNumbers(e, n).public_key()
  165.  
  166.    # Serialize to PEM
  167.    pem = public_key.public_bytes(
  168.        encoding=serialization.Encoding.PEM,
  169.        format=serialization.PublicFormat.SubjectPublicKeyInfo
  170.    )
  171.    return pem.decode()
  172.  
  173. def main():
  174.    if len(sys.argv) > 2:
  175.        print("Usage: jwk_to_pem.py [jwks.json]")
  176.        print("If no file is provided, reads from stdin")
  177.        sys.exit(1)
  178.  
  179.    if len(sys.argv) == 2 and sys.argv[1] != '-':
  180.        # Read from file
  181.        with open(sys.argv[1], 'r') as f:
  182.            jwks = json.load(f)
  183.    else:
  184.        # Read from stdin
  185.        jwks = json.load(sys.stdin)
  186.  
  187.    # Convert each key
  188.    for i, key in enumerate(jwks['keys']):
  189.        kid = key.get('kid', f'key-{i}')
  190.        print(f"# Key {i} (kid: {kid})")
  191.        print(jwk_to_pem(key))
  192.  
  193. if __name__ == "__main__":
  194.    main()
  195. </pre>
  196. ]]></content:encoded>
  197. <wfw:commentRss>https://akrabat.com/converting-jwks-json-to-pem-using-python/feed/</wfw:commentRss>
  198. <slash:comments>0</slash:comments>
  199. </item>
  200. <item>
  201. <title>Stop in-place editing of bash history items</title>
  202. <link>https://akrabat.com/stop-in-place-editing-of-bash-history-items/</link>
  203. <comments>https://akrabat.com/stop-in-place-editing-of-bash-history-items/#comments</comments>
  204. <dc:creator><![CDATA[Rob]]></dc:creator>
  205. <pubDate>Tue, 02 Sep 2025 10:00:00 +0000</pubDate>
  206. <category><![CDATA[Command Line]]></category>
  207. <category><![CDATA[Shell Scripting]]></category>
  208. <guid isPermaLink="false">https://akrabat.com/?p=7480</guid>
  209.  
  210. <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>
  211. <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>
  212. <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>
  213. <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>
  214. <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>
  215. <p>I never want the bash history to be editable; if I cancel out, then I want it back to what it was.</p>
  216. <h2>Fixing with revert-all-at-newline</h2>
  217. <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>
  218. <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>
  219. <p>The simplest thing is to add this to <tt>.bashrc</tt>:</p>
  220. <pre lang="bash">
  221. bind 'set revert-all-at-newline on'
  222. </pre>
  223. <p>Alternatively, you can create a <tt>.inputrc</tt> file with this in it:</p>
  224. <pre>
  225. $include /etc/inputrc
  226. set revert-all-at-newline on
  227. </pre>
  228. <p>To view the current value of <tt>revert-all-at-newline</tt>, use: </p>
  229. <pre>
  230. bind -V | grep revert-all-at-newline
  231. </pre>
  232. <p>It solved my problem, and I've not yet found a case when I want it set the other way.</p>
  233. ]]></content:encoded>
  234. <wfw:commentRss>https://akrabat.com/stop-in-place-editing-of-bash-history-items/feed/</wfw:commentRss>
  235. <slash:comments>1</slash:comments>
  236. </item>
  237. <item>
  238. <title>Extending an OpenAPI Component Schema</title>
  239. <link>https://akrabat.com/extending-an-openapi-component-schema/</link>
  240. <comments>https://akrabat.com/extending-an-openapi-component-schema/#respond</comments>
  241. <dc:creator><![CDATA[Rob]]></dc:creator>
  242. <pubDate>Tue, 26 Aug 2025 10:00:00 +0000</pubDate>
  243. <category><![CDATA[OpenAPI]]></category>
  244. <guid isPermaLink="false">https://akrabat.com/?p=7477</guid>
  245.  
  246. <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>
  247. <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>
  248. <p>In the OpenAPI spec, we can define this as a component and use in the relevant paths as appropriate:</p>
  249. <pre lang="yaml">
  250. components:
  251.  schemas:
  252.    ProblemDetails:
  253.      type: object
  254.      properties:
  255.        type:
  256.          type: string
  257.          format: uri-reference
  258.          description: A URI reference that identifies the problem type
  259.          default: about:blank
  260.          example: https://example.com/probs/out-of-credit
  261.        title:
  262.          type: string
  263.          description: A short, human-readable summary of the problem type
  264.          example: You do not have enough credit.
  265.        status:
  266.          type: integer
  267.          format: int32
  268.          description: The HTTP status code for this occurrence of the problem
  269.          minimum: 100
  270.          maximum: 599
  271.          example: 403
  272.        detail:
  273.          type: string
  274.          description: A human-readable explanation specific to this occurrence of the problem
  275.          example: Your current balance is 30, but that costs 50.
  276.        instance:
  277.          type: string
  278.          format: uri-reference
  279.          description: A URI reference that identifies the specific occurrence of the problem
  280.          example: /account/12345/msgs/abc
  281.      additionalProperties: true
  282. </pre>
  283. <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>
  284. <pre lang="yaml">
  285.    ValidationError:
  286.      allOf:
  287.        - $ref: '#/components/schemas/ProblemDetails'
  288.        - type: object
  289.          properties:
  290.            errors:
  291.              type: object
  292.              description: Field-specific validation error messages
  293.              additionalProperties:
  294.                type: string
  295.              example:
  296.                name: "name must be provided"
  297.                dateOfBirth: "date must be in the past"
  298. </pre>
  299. <p>This is quite handy!</p>
  300. ]]></content:encoded>
  301. <wfw:commentRss>https://akrabat.com/extending-an-openapi-component-schema/feed/</wfw:commentRss>
  302. <slash:comments>0</slash:comments>
  303. </item>
  304. <item>
  305. <title>Saving the current URL to a Note</title>
  306. <link>https://akrabat.com/saving-the-current-url-to-a-note/</link>
  307. <comments>https://akrabat.com/saving-the-current-url-to-a-note/#respond</comments>
  308. <dc:creator><![CDATA[Rob]]></dc:creator>
  309. <pubDate>Tue, 19 Aug 2025 10:00:00 +0000</pubDate>
  310. <category><![CDATA[Mac]]></category>
  311. <category><![CDATA[Shortcuts]]></category>
  312. <guid isPermaLink="false">https://akrabat.com/?p=7467</guid>
  313.  
  314. <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>
  315. <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>
  316. <p>I want to store as a bullet item containing the name of the page, the link and the date. Something like this:</p>
  317. <picture><source  
  318. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025saved-link-text-dark.png"
  319.        media="(prefers-color-scheme: dark)"
  320.    /><source
  321. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025saved-link-text-light.png"
  322.        media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
  323.    /><br />
  324.    <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"/>
  325. </picture>
  326. <p>(Funny that the spellchecker doesn't know that Thu is the short form for Thursday)</p>
  327. <h2>The <em>Save Links to Notes</em> Shortcut</h2>
  328. <p>This is the shortcut that I created to do it:
  329. <picture><source  
  330. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-shortcut-dark.png"
  331.        media="(prefers-color-scheme: dark)"
  332.    /><source
  333. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-shortcut-light.png"
  334.        media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
  335.    /><br />
  336.    <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"/>
  337. </picture>
  338. <p>You can download it here:<br />
  339. <a href="https://www.icloud.com/shortcuts/6c12e888f9da431d9977985b229d636b">https://www.icloud.com/shortcuts/6c12e888f9da431d9977985b229d636b</a></p>
  340. <h2>Breaking down the actions</h2>
  341. <p>To get the URL into the shortcut, we want:</p>
  342. <ul>
  343. <li><em>Show in Share Sheet</em> so that it's available on iOS/iPadOS</li>
  344. <li><em>Receive What's Onscreen</em> so that when a browser is focussed on Mac, it finds the URL</li>
  345. <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>
  346. </ul>
  347. <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>
  348. <p>There's an action for <em>Current Date</em>, so we add that to get the variable.</p>
  349. <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>
  350. <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>
  351. <h2>That's it</h2>
  352. <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.
  353. <picture><source  
  354. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-notification-dark.png"
  355.        media="(prefers-color-scheme: dark)"
  356.    /><source
  357. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-notification-light.png"
  358.        media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
  359.    /><br />
  360.    <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"/>
  361. </picture>
  362. <p>With this shortcut, I can add a new entry to my note from both my Mac, iPad and iPhone with minimal effort. </p>
  363. <p>I like it.</p>
  364. ]]></content:encoded>
  365. <wfw:commentRss>https://akrabat.com/saving-the-current-url-to-a-note/feed/</wfw:commentRss>
  366. <slash:comments>0</slash:comments>
  367. </item>
  368. <item>
  369. <title>Accessing Longplay info for SwiftBar</title>
  370. <link>https://akrabat.com/accessing-longplay-info-for-swiftbar/</link>
  371. <comments>https://akrabat.com/accessing-longplay-info-for-swiftbar/#respond</comments>
  372. <dc:creator><![CDATA[Rob]]></dc:creator>
  373. <pubDate>Tue, 12 Aug 2025 10:00:00 +0000</pubDate>
  374. <category><![CDATA[Mac]]></category>
  375. <guid isPermaLink="false">https://akrabat.com/?p=7447</guid>
  376.  
  377. <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>
  378. <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>
  379. <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>
  380. <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>
  381. <p>Recently, I've been using <a href="https://longplay.rocks">Longplay</a> to play albums and wanted the same functionality.</p>
  382. <p>Now Playing uses AppleScript to determine if a music player is playing and what the track info is:</p>
  383. <pre>
  384. app_playing=$(osascript -e "tell application \"$i\" to player state as string")
  385. </pre>
  386. <p>And</p>
  387. <pre>
  388. track=$(osascript -e "tell application \"$app\" to name of current track")
  389. artist=$(osascript -e "tell application \"$app\" to artist of current track")
  390. </pre>
  391. <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>
  392. <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>
  393. <p>I knocked up a couple of shortcuts:</p>
  394. <picture><source  
  395. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-status-dark.png"
  396.        media="(prefers-color-scheme: dark)"
  397.    /><source
  398. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-status-light.png"
  399.        media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
  400.    /><br />
  401.    <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"/>
  402. </picture>
  403. <p>and</p>
  404. <picture><source  
  405. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-now-playing-dark.png"
  406.        media="(prefers-color-scheme: dark)"
  407.    /><source
  408. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-now-playing-light.png"
  409.        media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
  410.    /><br />
  411.    <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"/>
  412. </picture>
  413. <p>With these set-up, I can now run them from the command line using <tt>shortcuts</tt>:</p>
  414. <pre>
  415. app_playing=$(shortcuts run "Longplay status");
  416. </pre>
  417. <p>This will set <tt>$app_playing</tt> to either "Yes" or "No" as strings as it is defined as boolean in Shortcuts.</p>
  418. <pre>
  419. track=$(shortcuts run "Longplay now playing");
  420. </pre>
  421. <p>This simply sets <tt>$track</tt> to the string of the currently playing track.</p>
  422. <h2>Updated Now Playing script</h2>
  423. <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>
  424. <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>
  425. ]]></content:encoded>
  426. <wfw:commentRss>https://akrabat.com/accessing-longplay-info-for-swiftbar/feed/</wfw:commentRss>
  427. <slash:comments>0</slash:comments>
  428. </item>
  429. <item>
  430. <title>Responding to StreamDeck buttons with Keyboard Maestro</title>
  431. <link>https://akrabat.com/controlling-the-streamdeck-via-keyboard-maestro/</link>
  432. <comments>https://akrabat.com/controlling-the-streamdeck-via-keyboard-maestro/#respond</comments>
  433. <dc:creator><![CDATA[Rob]]></dc:creator>
  434. <pubDate>Tue, 05 Aug 2025 10:00:00 +0000</pubDate>
  435. <category><![CDATA[AppleScript]]></category>
  436. <category><![CDATA[Keyboard Maestro]]></category>
  437. <category><![CDATA[Mac]]></category>
  438. <guid isPermaLink="false">https://akrabat.com/?p=7440</guid>
  439.  
  440. <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>
  441. <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>
  442. <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>
  443. <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>
  444. <h2>Setting up the Stream Deck button</h2>
  445. <p>You can now assign Keyboard Maestro to a button in the Stream Deck software:</p>
  446. <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>
  447. <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>
  448. <h2>Responding to the button in Keyboard Maestro</h2>
  449. <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>
  450. <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>
  451. <p>Upon clicking the button, we simply run some AppleScript to control the Music app's volume.</p>
  452. <h2>That's it</h2>
  453. <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>
  454. ]]></content:encoded>
  455. <wfw:commentRss>https://akrabat.com/controlling-the-streamdeck-via-keyboard-maestro/feed/</wfw:commentRss>
  456. <slash:comments>0</slash:comments>
  457. </item>
  458. <item>
  459. <title>Step-debugging Docker Compose NestJS services</title>
  460. <link>https://akrabat.com/step-debugging-docker-compose-nestjs-services/</link>
  461. <comments>https://akrabat.com/step-debugging-docker-compose-nestjs-services/#respond</comments>
  462. <dc:creator><![CDATA[Rob]]></dc:creator>
  463. <pubDate>Tue, 29 Jul 2025 10:00:00 +0000</pubDate>
  464. <category><![CDATA[NodeJS]]></category>
  465. <category><![CDATA[TypeScript]]></category>
  466. <guid isPermaLink="false">https://akrabat.com/?p=7454</guid>
  467.  
  468. <description><![CDATA[I'm working on a NestJS project that uses monorepo mode. It consists of a number of separate microservice applications that each have their own Docker container that are managed in development using Docker Compose. I like step-debugging in my IDE and so needed to set it up for this application. This is what I did. The application setup Each service in our project has its own container in docker-compose.yaml and we use a reverse proxy to… <a href="https://akrabat.com/step-debugging-docker-compose-nestjs-services/">continue reading</a>.]]></description>
  469. <content:encoded><![CDATA[<p>I'm working on a <a href="https://nestjs.com">NestJS</a> project that uses <a href="https://docs.nestjs.com/cli/monorepo#monorepo-mode">monorepo mode</a>. It consists of a number of separate microservice applications that each have their own <a href="https://www.docker.com">Docker</a> container that are managed in development using <a href="https://docs.docker.com/compose/">Docker Compose</a>.</p>
  470. <p>I like step-debugging in my IDE and so needed to set it up for this application. This is what I did.</p>
  471. <h2>The application setup</h2>
  472. <p>Each service in our project has its own container in docker-compose.yaml and we use a reverse proxy to have a single endpoint that routes requests to the correct microservice. Each custom <tt>Dockerfile</tt> runs <tt>ppm run start:dev:{service name}></tt> which is defined in <tt>package.json</tt> like this:</p>
  473. <pre>
  474. "start:dev:service1": "nest start service1 --watch --tsc --watchOptions.poll=1000 --preserveWatchOutput",
  475. </pre>
  476. <p>Using <tt>--watchOptions.poll=1000</tt> is just more reliable when running in Docker with volumes mounted into the container. We also set the <tt>--preserveWatchOutput</tt> flag to ensure that the service doesn't take control of the terminal as this is unhelpful when you have multiple services in play.</p>
  477. <h2>Set the apps up for debugging</h2>
  478. <p>We need to make some modification for step debugging. Firstly, I created a set of <tt>start:debug:{service name}</tt> scripts in <tt>package.json</tt> that look like this:</p>
  479. <pre>
  480. "start:debug:service1": "nest start service1 --debug 0.0.0.0:9229 --watch --tsc --watchOptions.poll=1000 --preserveWatchOutput",
  481. </pre>
  482. <p>We enabled the <tt>--debug</tt> flag to enable node's <tt>--inspect</tt> flag so that port <tt>9229</tt> is available to the debugger. However, buy default this is bound to <tt>127.0.0.1</tt> which is not useful in a container, so we bind to <tt>0.0.0.0:9229</tt> so that it's available outside the container.</p>
  483. <p>Next, we need to our new <tt>start:debug:{service name}</tt> scripts and expose port 92229 to our local environment for each service. We do this in <tt>docker-compose.override.yaml</tt>:</p>
  484. <pre lang="yaml">
  485. services:
  486.  service1:
  487.    ports:
  488.      - "9230:9229"
  489.    command: pnpm run start:debug:service1
  490.  
  491.  service2:
  492.    ports:
  493.      - "9231:9229"
  494.    command: pnpm run start:debug:service2
  495. </pre>
  496. <p>I don't tend to like binding to the default port as that invariably confuses me when I run some test thing locally, so I've picked ports starting from <tt>9230</tt> onwards for my services.</p>
  497. <p>Running <tt>docker compose up</tt> will now start the containers.</p>
  498. <h2>Debugging in WebStorm</h2>
  499. <p>To set up <a href="https://www.jetbrains.com/webstorm/">WebStorm</a> for step debugging, create a <em>Run/Debug configuration</em> entry of type <tt>Attach to Node.js/Chrome</tt> for each container.</p>
  500. <p>The settings for service1 are:</p>
  501. <ul>
  502. <li>Name: Debug service1</li>
  503. <li>Host: localhost</li>
  504. <li>Port: 9230</li>
  505. </ul>
  506. <p>For the other services, change the name and port.</p>
  507. <picture><source  
  508. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025webstorm-nodejs-debug-config-dark.png"
  509.        media="(prefers-color-scheme: dark)"
  510.    /><source
  511. srcset="https://akrabat.com/wp-content/uploads/2025/07/2025webstorm-nodejs-debug-config-light.png"
  512.        media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
  513.    /><br />
  514.    <img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025webstorm-nodejs-debug-config-light.png" loading="lazy" alt="Webstorm nodejs debug config light." class="noborder" width="600"/>
  515. </picture>
  516. <p>From the Debugging dropdown at in the title bar select the service and press the green "bug" button. You'll see a "Debugger attached." message in the Docker logs.</p>
  517. <p>Attach a breakpoint and access the endpoint using <tt>curl</tt> or another HTTP client and the IDE should stop execution at the breakpoint.</p>
  518. <h2>Debugging in VS Code</h2>
  519. <p>To set up <a href="https://code.visualstudio.com">VS Code</a> for step debugging, create a <tt>.vscode/launch.json</tt> file in your project. It should look like this:</p>
  520. <pre lang="json">
  521. {
  522.  "version": "0.2.0",
  523.  "configurations": [
  524.    {
  525.      "name": "Debug service1",
  526.      "type": "node",
  527.      "request": "attach",
  528.      "port": 9230,
  529.      "address": "localhost",
  530.      "localRoot": "${workspaceFolder}",
  531.      "remoteRoot": "/usr/src/app",
  532.      "restart": true
  533.    },
  534.    {
  535.      "name": "Debug service2",
  536.      "type": "node",
  537.      "request": "attach",
  538.      "port": 9231,
  539.      "address": "localhost",
  540.      "localRoot": "${workspaceFolder}",
  541.      "remoteRoot": "/usr/src/app",
  542.      "restart": true
  543.    }
  544.  ]
  545. }
  546. </pre>
  547. <p>Set <tt>remoteRoot</tt> to the directory within the Docker container where the project is mounted.</p>
  548. <p>Start debugging by selecting the <em>Run and Debug</em> pane in the left hand toolbar and choose the service from the dropdown at the top. Then press the green <em>Start debugging</em> button (or press F5).You'll see a "Debugger attached." message in the Docker logs.</p>
  549. <p>Attach a breakpoint and access the endpoint using <tt>curl</tt> or another HTTP client and the IDE should stop execution at the breakpoint.</p>
  550. <h2>That's it</h2>
  551. <p>Having done this, I can now enjoy step debugging this new-to-me codebase and understand what it does!</p>
  552. ]]></content:encoded>
  553. <wfw:commentRss>https://akrabat.com/step-debugging-docker-compose-nestjs-services/feed/</wfw:commentRss>
  554. <slash:comments>0</slash:comments>
  555. </item>
  556. <item>
  557. <title>QuickSS: Screenshot the active window on Mac</title>
  558. <link>https://akrabat.com/quickss-screenshot-the-active-window-on-mac/</link>
  559. <comments>https://akrabat.com/quickss-screenshot-the-active-window-on-mac/#respond</comments>
  560. <dc:creator><![CDATA[Rob]]></dc:creator>
  561. <pubDate>Tue, 22 Jul 2025 09:00:00 +0000</pubDate>
  562. <category><![CDATA[Mac]]></category>
  563. <category><![CDATA[Software]]></category>
  564. <guid isPermaLink="false">https://akrabat.com/?p=7418</guid>
  565.  
  566. <description><![CDATA[Back in 2016, I wrote about using QuickGrab to take a screenshot of the active window via a single key press with no mouse use required. It's now 2025 and I'm still using this and Apple has announced that Rosetta 2 will be phased out in a couple of years. As QuickGrab is one of the few Intel-only apps I still use, I thought I'd recompile for Apple Silicon and it was then that I… <a href="https://akrabat.com/quickss-screenshot-the-active-window-on-mac/">continue reading</a>.]]></description>
  567. <content:encoded><![CDATA[<p>Back in 2016, I wrote about using QuickGrab to take a screenshot of the active window via a single key press with no mouse use required.</p>
  568. <p>It's now 2025 and I'm still using this and Apple has announced that Rosetta 2 will be phased out in a couple of years. As QuickGrab is one of the few Intel-only apps I still use, I thought I'd recompile for Apple Silicon and it was then that I ran into trouble:</p>
  569. <pre>
  570. $ gcc -framework cocoa -x objective-c -o quickgrab quickgrab.m
  571. quickgrab.m:92:26: error: 'CGWindowListCreateImage' is unavailable:
  572. obsoleted in macOS 15.0 - Please use ScreenCaptureKit instead.
  573. </pre>
  574. <p><em>Obsoleted in macOS 15.0</em> isn't something you want to see! The APIs that QuickGrab use are no longer part of the SDK and so it cannot be compiled. Hence I decided to replace it with a Swift version and also take the opportunity to add additional features that I wanted.</p>
  575. <p>While on holiday, I wrote <a href="https://github.com/akrabat/QuickSS">QuickSS</a>. I spent some time playing around with ScreenCaptureKit, but couldn't get it to replicate the window shadows that the standard screenshot tool on <tt>shift+cmd+4</tt> does. To solve this I decided to use <tt>screencapture</tt> to take the screenshot which is provided by macOS. </p>
  576. <h2>Screenshot to file</h2>
  577. <p>To save a screenshot of the active window directly to a file, just run <tt>quickss</tt>. You probably want to select cmd+tab to a different window, so prefix with <tt>sleep</tt>:</p>
  578. <pre>
  579. rob@Caledonia QuickGrab (master *) $ sleep 3; quickss
  580. Capturing window ID: 5940 [Safari: 'Rob Allen – Pragmatism in the real world']
  581. Screenshot saved to: /Users/rob/Downloads/Screenshot 2025-07-22 at 11.00.00.png
  582. </pre>
  583. <p>As it's my app, I made some changes to match the way I work with it. Firstly it defaults to saving the file to the Downloads folder and names it the same as the default screenshot utility. You can use <tt>--file</tt> to override this should you need to and <tt>--quiet</tt> will output just the filename which is useful for onward scripting, or displaying in notifications.</p>
  584. <h2>Screenshot directly to clipboard</h2>
  585. <p>I also added the ability to put the screenshot directly onto the clipboard with <tt>--clipboard</tt></p>
  586. <pre>
  587. $ sleep 3; quickss --clipboard
  588. Capturing window ID: 5940 [Safari: 'Rob Allen – Pragmatism in the real world']
  589. Screenshot copied to clipboard
  590. </pre>
  591. <p>I can now paste directly into Slack/Discord/Messages/Messenger/WhatsApp/Signal/etc. (Yes, there are far too many of these services nowadays!)</p>
  592. <h2>Keyboard shortcut</h2>
  593. <p>Obviously, the best way to run this is to use global keyboard shortcuts. My preference is to use <tt style="font-family: sans-serif">⌃⌥⌘4</tt> to put the screenshot onto the clipboard and <tt style="font-family: sans-serif">⇧⌃⌥⌘4</tt> to save it to file.</p>
  594. <h3>Using Shortcuts.app</h3>
  595. <p>You can use the Shortcuts app for this and create a shortcut for copying to clipboard like this:</p>
  596. <p><img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025quickss-take-screenshot-to-cliboard.png" alt="Quickss take screenshot to cliboard." title="quickss-take-screenshot-to-cliboard.png" border="0" width="700" height="511" /></p>
  597. <p>The equivalent for saving to file is the same, except that you don't need the <tt>--clipboard</tt> parameter.</p>
  598. <h3>Using Alfred</h3>
  599. <p>I use <a href="https://www.alfredapp.com/">Alfred</a> for these sort of things, so I wrote a Workflow to do this based on the previous one:</p>
  600. <p><img loading="lazy" decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025quickss-alfred-workflow.png" alt="Quickss alfred workflow." title="quickss-alfred-workflow.png" border="0" width="700" height="363" /></p>
  601. <p>It's downloadable from the <a href="https://github.com/akrabat/QuickSS/releases/latest">QuickSS latest release</a> page.</p>
  602. <h2>That's it</h2>
  603. <p>Over the years, I've found that having a global hotkey to screenshot the current active window is really helpful. I like it even more now that it goes directly to the clipboard.</p>
  604. ]]></content:encoded>
  605. <wfw:commentRss>https://akrabat.com/quickss-screenshot-the-active-window-on-mac/feed/</wfw:commentRss>
  606. <slash:comments>0</slash:comments>
  607. </item>
  608. <item>
  609. <title>Notarising a macOS standalone binary</title>
  610. <link>https://akrabat.com/notarising-a-macos-standalone-binary/</link>
  611. <comments>https://akrabat.com/notarising-a-macos-standalone-binary/#respond</comments>
  612. <dc:creator><![CDATA[Rob]]></dc:creator>
  613. <pubDate>Tue, 15 Jul 2025 09:00:00 +0000</pubDate>
  614. <category><![CDATA[Command Line]]></category>
  615. <category><![CDATA[Development]]></category>
  616. <category><![CDATA[Shell Scripting]]></category>
  617. <guid isPermaLink="false">https://akrabat.com/?p=7413</guid>
  618.  
  619. <description><![CDATA[I've been writing a simple Swift command line tool called QuickSS. It's a single file swift file, that I compile to a standalone binaryusing: swiftc quickss.swift -o quickss To distribute it on modern Macs, I need to sign it and then get Apple to notarise it. Signing the binary To sign the binary, you need a "Developer ID Application" certificate from your paid developer account. If you don't have one there already create a new… <a href="https://akrabat.com/notarising-a-macos-standalone-binary/">continue reading</a>.]]></description>
  620. <content:encoded><![CDATA[<p>I've been writing a simple Swift command line tool called QuickSS. It's a single file swift file, that I compile to a standalone binaryusing:</p>
  621. <pre>
  622. swiftc quickss.swift -o quickss
  623. </pre>
  624. <p>To distribute it on modern Macs, I need to sign it and then get Apple to notarise it.</p>
  625. <h2>Signing the binary</h2>
  626. <p>To sign the binary, you need a "Developer ID Application" certificate from your paid <a href="https://developer.apple.com/account/resources/certificates/list">developer account</a>. If you don't have one there already create a new one, which requires a CSR from Keychain. Fortunately, the <a href="https://developer.apple.com/help/account/certificates/create-a-certificate-signing-request">info on what to do</a> is clear.</p>
  627. <p>Download your Developer ID Application certificate and add to Keychain Access.</p>
  628. <p>Next you need its name. This is done using <tt>security find-identity -v -p codesigning</tt> which gives you a list of certs. You want the text between the <tt>"</tt> that starts "Developer ID Application:". The easiest way to get this if you are scripting it is:</p>
  629. <pre>
  630. IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | grep -m 1 -oE '"[^"]+"' | tr -d '"')
  631. </pre>
  632. <p>To actually sign the binary, use the <tt>codesign</tt> utility:</p>
  633. <pre>
  634. codesign --timestamp --options runtime --sign "$IDENTITY" quickss
  635. </pre>
  636. <h2>Notarising</h2>
  637. <p>To notarise the binary, you use the <tt>notarytool</tt> utility within <tt>xcrun</tt>. This expects a zip or pkg file, so zip up the binary first. You also need to know your Apple developer account's Team ID, your Apple ID and you need an app-specific password from <a href="https://account.apple.com">account.apple.com</a>.</p>
  638. <p>Submit your app for notarising with:</p>
  639. <pre>
  640. zip -q quickss.zip quickss
  641.  
  642. xcrun notarytool submit  --wait --no-progress -f json \
  643.    --team-id "$TEAM_ID" \
  644.    --apple-id "$APPLE_ID" \
  645.    --password "$APP_SPECIFIC_PASSWORD" \
  646.    quickss.zip
  647.  
  648. rm quickss.zip
  649. </pre>
  650. <p>If it fails, use the submission ID to query the log:</p>
  651. <pre>
  652. xcrun notarytool log \
  653.    --team-id "$TEAM_ID" \
  654.    --apple-id "$APPLE_ID" \
  655.    --password "$APP_SPECIFIC_PASSWORD" \
  656.    "&lt;submission ID&gt;"
  657. </pre>
  658. <p>Apple has now notarised your app.</p>
  659. <h2>Note: No need to staple</h2>
  660. <p>For a standard Mac applications (<tt>.app</tt> bundles), you can "staple" the notarisation into it. This makes things more efficient for the GateKeeper technology. However, for a standalone binary, <em>this cannot be done</em> as there is no directory into which to store the notarisation file.</p>
  661. <p>If you try to staple, you'll get <tt>The staple and validate action failed! Error 73</tt> with no further information which is not entirely helpful.</p>
  662. <p>For standalone binaries GateKeeper checks directly with Apple's servers the first time they are run, so stapling is unnecessary.</p>
  663. <h2>That's it</h2>
  664. <p>That's it. You now have a notarised standalone binary that can be easily distributed.</p>
  665. ]]></content:encoded>
  666. <wfw:commentRss>https://akrabat.com/notarising-a-macos-standalone-binary/feed/</wfw:commentRss>
  667. <slash:comments>0</slash:comments>
  668. </item>
  669. </channel>
  670. </rss>
  671.  

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