Technology Tales

Adventures in consumer and enterprise technology

Installing PowerShell on Linux Mint for some cross-platform testing

25th November 2025

Given how well shell scripting works on Linux and my familiarity with it, the need to install PowerShell on a Linux system may seem surprising. However, this was part of some testing that I wanted to do on a machine that I controlled before moving the code to a client's system. The first step was to ensure that any prerequisites were in place:

sudo apt update
sudo apt install -y wget apt-transport-https software-properties-common

After that, the next moves were to download and install the required package for instating Microsoft repository details:

wget -q https://packages.microsoft.com/config/ubuntu/24.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb

Then, I could install PowerShell itself:

sudo apt update
sudo apt install -y powershell

When it was in place, issuing the following command started up the extra shell for what I needed to do:

pwsh

During my investigations, I found that my local version of PowerShell was not the same as on the client's system, meaning that any code was not as portable as I might have expected, Nevertheless, it is good to have this for future reference and proves how interoperable Microsoft has needed to become.

Restoring photo dates with ExifTool after an Olympus camera loses its settings following a complete battery discharge

24th November 2025

Here is the story behind what I am sharing here. My Olympus OM-D E-M10 II had been left aside for long enough to allow its battery to discharge fully. That also had the effect of causing it to lose its date and time settings. Then, I recharged the battery and went about using it without checking on those date and time settings. The result was a set of photos with a capture date and time of 1970-01-01T00:00 (midnight on New Year's Day in 1970!).

This was noticed when I loaded them onto my computer for appraisal with Lightroom. Thankfully, this had not gone on for too long, so I could work out the dates on which the images had been made. Thus, I could use ExifTool to update the capture dates while leaving the times alone. A command like the following will accomplish this while overwriting the images (the originals were retained elsewhere).

exiftool -overwrite_original \
-DateTimeOriginal='2025:06:02 ${DateTimeOriginal;s/^.* //}' \
-CreateDate='2025:06:02 ${CreateDate;s/^.* //}' \
-ModifyDate='2025:06:02 ${ModifyDate;s/^.* //}' \
*.ORF

The above command updates the original date, the capture date and the modified date. In practice, I only set two of these, leaving aside the modified date. Omitting the -overwrite_original switch would cause the creation of backup files, should that be what you require. Some think that specifying the *.ORF wildcard search is not desirable, preferring the following instead:

exiftool -overwrite_original \
-DateTimeOriginal='2025:06:02 ${DateTimeOriginal;s/^.* //}' \
-ext orf .

It is the -ext switch that picks up the ORF extension while . refers to the folder in which you are located in your shell session, and you can define your own path in the place of the dot if that is what is needed. Also, using -ext orf -ext dng will allow you to work with more than one file type at a time, a handy thing when more than one is found in the same directory, not that I organise my files like that.

With the date metadata fixed, removing the affected photos from Lightroom and reimporting them brought in the altered metadata. In the future, I will pay more attention to the Y/M/D display on the camera when it starts up, now that I realise what the display is trying to tell me. Then involves a trip to the settings using the Menu button on the back of the camera. Once in there, navigating to the spanner icon and then to the clock one gets you to the time settings where you can adjust it as needed. Pressing OK commits the setting to memory for the future, and you are then ready to go.

While on the subject of settings, the Info button is where you can set the levels to appear in the image display (on the viewfinder in my case); somehow I managed to lose these until I recalled how to get them back. Next on the list is another button that needs care on the top of the camera near the shutter release: one with a magnifying glass icon on there: this is the electronic zoom that has caught me out in the past. Naturally, other exposure settings dials also need care too, so it is never a good idea to rush the operation of a modern digital camera. Keeping their batteries charged will help too, especially in avoiding the predicament whose resolution led to the writing of this piece.

Adding visual appeal to bash command line scripts with colour variables on Linux

23rd November 2025

While I was updating some scripts to improve their functionality, I made some unexpected discoveries. One involved adding some colour to the output, and a second will come up later. The colours can be defined as values of variables, as you can see below:

# Colours
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # no colour

In all cases, \033 is the shell escape sequence while [ is the control sequence initiator and m closes the sequence for colour definitions like we have here. A numeric value of 0 resets things to the default, which is how it is used in the no colour (NC) case that we have above to ensure that the colouration does not overflow beyond the intended text. Otherwise, 31 specifies red, 32 specifies green and 33 specifies yellow, giving options to use later on in the code. All of this is in line with the ANSI standard.

This is how these colour variables get used:

echo -e "\n${YELLOW}$(printf '*' {1..40}) All done $(printf '*' {1..40})${NC}\n"

The above is for an example with yellow text produced using ${YELLOW} segment after the newline sequence (\n)  that is activated b y the -e switch passed to the echo command. This is turned off by the ${NC} portion at the end of the text, again before a terminating newline sequence. One extra addition here is the part that outputs forty asterisks: $(printf '*' {1..40}). You could have $(printf '*%.0s' {1..40}) instead, which is clearer to some because of the null output character sequence %.0s. In the earlier example, I opted for the simpler option.

Latest developments in the AI landscape: Consolidation, implementation and governance

22nd November 2025

Artificial intelligence is moving through another moment of consolidation and capability gain. New ways to connect models to everyday tools now sit alongside aggressive platform plays from the largest providers, a steady cadence of model upgrades, and a more defined conversation about risk and regulation. For companies trying to turn all this into practical value, the story is becoming less about chasing the latest benchmark and more about choosing a platform, building the right connective tissue, and governing data use with care. The coming year looks set to reward those who simplify the user experience, embed AI directly into work and adopt proportionate controls rather than blanket bans.

I. Market Structure and Competitive Dynamics

Platform Consolidation and Lock-In

Enterprise AI appears to be settling into a two-platform market. Analysts describe a landscape defined more by integration and distribution than raw model capability, evoking the cloud computing wars. On one side sit Microsoft and OpenAI, on the other Google and Gemini. Recent signals include the pricing of Gemini 3 Pro at around two dollars per million tokens, which undercuts much of the market, Alphabet's share price strength, and large enterprise deals for Gemini integrated with Google's wider software suite. Google is also promoting Antigravity, an agent-first development environment with browser control, asynchronous execution and multi-agent support, an attempt to replicate the pull of VS Code within an AI-native toolchain.

The implication for buyers is higher switching costs over time. Few expect true multi-cloud parity for AI, and regional splits will remain. Guidance from industry commentators is to prioritise integration across the existing estate rather than incremental model wins, since platform choices now look like decade-long commitments. Events lined up for next year are already pointing to that platform view.

Enterprise Infrastructure Alignment

A wider shift in software development is also taking shape. Forecasts for 2026 emphasise parallel, multi-agent systems where a planning agent orchestrates a set of execution agents, and harnesses tune themselves as they learn from context. There is growing adoption of a mix-of-models approach in which expensive frontier models handle planning, and cheaper models do the bulk of execution, bringing near-frontier quality for less money and with lower latency. Team structures are changing as a result, with more value placed on people who combine product sense with engineering craft and less on narrow specialisms.

ServiceNow and Microsoft have announced a partnership to coordinate AI agents across organisations with tighter oversight and governance, an attempt to avoid the sprawl that plagued earlier automation waves. Nvidia has previewed Apollo, a set of open AI physics models intended to bring real-time fidelity to simulations used in science and industry. Albania has appointed an AI minister, which has kicked off debate about how governments should manage and oversee their own AI use. CIOs are being urged to lead on agentic AI as systems become capable of automating end-to-end workflows rather than single steps.

New companies and partnerships signal where capital and talent are heading. Jeff Bezos has returned to co-lead Project Prometheus, a start-up with $6.2 billion raised and a team of about one hundred hires from major labs, focused on AI for engineering and manufacturing in the physical world, an aim that aligns with Blue Origin interests. Vik Bajaj is named as co-CEO.

Deals underline platform consolidation. Microsoft and Nvidia are investing up to $5 billion and $10 billion respectively (totalling $15 billion) in Anthropic, whilst Anthropic has committed $30 billion in Azure capacity purchases with plans to co-design chips with Nvidia.

Commercial Model Evolution

Events and product launches continue at pace. xAI has released Grok 4.1 with an emphasis on creativity and emotional intelligence while cutting hallucinations. On the tooling front, tutorials explain how ChatGPT's desktop app can record meetings for later summarisation. In a separate interview, DeepMind's Demis Hassabis set out how Gemini 3 edges out competitors in many reasoning and multimodal benchmarks, slightly trails Claude Sonnet 4.5 in coding, and is being positioned for foundations in healthcare and education though not as a medical-grade system. Google is encouraging developers towards Antigravity for agentic workflows.

Industry leaders are also sketching commercial models that assume more agentic behaviour, with Microsoft's Satya Nadella promising a "positive-sum" vision for AI while hinting at per-agent pricing and wider access to OpenAI IP under Microsoft's arrangements.

II. Technical Implementation and Capability

Practical Connectivity Over Capability

A growing number of organisations are starting with connectors that allow a model to read and write across systems such as Gmail, Notion, calendars, CRMs, and Slack. Delivered via the Model Context Protocol, these links pull the relevant context into a single chat, so users spend less time switching windows and more time deciding what to do. Typical gains are in hours saved each week, lower error rates, and quicker responses. With a few prompts, an assistant can draft executive email summaries, populate a Notion database with leads from scattered sources, or propose CRM follow-ups while showing its working.

The cleanest path is phased: enable one connector using OAuth, trial it in read-only mode, then add simple routines for briefs, meeting preparation or weekly reports before switching on write access with a "show changes before saving" step. Enterprise controls matter here. Connectors inherit user permissions via OAuth 2.0, process data in memory, and vendors point to SOC 2, GDPR and CCPA compliance alongside allow and block lists, policy management, and audit logs. Many governance teams prefer to begin read-only and require approvals for writes.

There are limits to note, including API rate caps, sync delays, context window constraints and timeouts for long workflows. They are poor fits for classified data, considerable bulk operations or transactions that cannot tolerate latency. Some industry observers regard Claude's current MCP implementation, particularly on desktop, as the most capable of the group. Playbooks for a 30-day rollout are beginning to circulate, as are practitioner workshops introducing go-to-market teams to these patterns.

Agentic Orchestration Entering Production

Practical comparisons suggest the surrounding tooling can matter more than the raw model for building production-ready software. One report set a 15-point specification across several environments and found that Claude Code produced all features end-to-end. The same spec built with Gemini 3 inside Antigravity delivered two thirds of the features, while Sonnet 4.5 in Antigravity delivered a little more than half, with omissions around batching, progress indicators and robust error handling.

Security remains a live issue. One newsletter reports that Anthropic said state-backed Chinese hackers misused Claude to autonomously support a large cyberattack, which has intensified calls for governance. The background hum continues, from a jump in voice AI adoption to a German ruling on lyric copyright involving OpenAI, new video guidance steps in Gemini, and an experimental "world model" called Marble. Tools such as Yorph are receiving attention for building agentic data pipelines as teams look to productionise these patterns.

Tooling Maturity Defining Outcomes

In engineering practice, Google's Code Wiki brings code-aware documentation that stays in sync with repositories using Gemini, supported by diagrams and interactive chat. GitLab's latest survey suggests AI increases code creation but also pushes up demand for skilled engineers alongside compliance and human oversight. In operations, Chronosphere has added AI remediation guidance to cut observability noise and speed root-cause analysis while performance testing is shifting towards predictive, continuous assurance rather than episodic tests.

Vertical Capability Gains

While the platform picture firms up, model and product updates continue at pace. Google has drawn attention with a striking upgrade to image generation, based on Gemini 3. The system produces 4K outputs with crisp text across multiple languages and fonts, can use up to 14 reference images, preserves identity, and taps Google Search to ground data for accurate infographics.

Separately, OpenAI has broadened ChatGPT Group Chats to as many as 20 people across all pricing tiers, with privacy protections that keep group content out of a user's personal memory. Consumer advocates have used the moment to call out the risks of AI toys, citing safety, privacy and developmental concerns, even as news continues to flow from research and product teams, from the release of OLMo 3 to mobile features from Perplexity and a partnership between Stability and Warner Music Group.

Anthropic has answered with Claude Opus 4.5, which it says is the first model to break the 80 percent mark on SWE-Bench Verified while improving tool use and reasoning. Opus 4.5 is designed to orchestrate its smaller Haiku models and arrives with a price cut of roughly two thirds compared to the 4.1 release. Product changes include unlimited chat length, a Claude Code desktop app, and integrations that reach across Chrome and Excel.

OpenAI's additions have a more consumer flavour, with a Shopping Research feature in ChatGPT that produces personalised product guidance using a GPT-5 mini variant and plans for an Instant Checkout flow. In government, a new US executive order has launched the "Genesis Mission" under the Department of Energy, aiming to fuse AI capabilities across 17 national labs for advances in fields such as biotechnology and energy.

Coding tools are evolving too. OpenAI has previewed GPT-5.1-Codex-Max, which supports long-running sessions by compacting conversational history to preserve context while reducing overhead. The company reports 30 percent fewer tokens and faster performance over sessions that can run for more than a day. The tool is already available in the Codex CLI and IDE, with an API promised.

Infrastructure news out of the Middle East points to large-scale investment, with Saudi HUMAIN announcing data centre plans including xAI's first international facility alongside chips from Nvidia and AWS, and a nationwide rollout of Grok. In computer vision, Meta has released SAM 3 and SAM 3D as open-source projects, extending segmentation and enabling single-photo 3D reconstruction, while other product rollouts continue from GPT-5.1 Pro availability to fresh funding for audio generation and a marketing tie-up between Adobe and Semrush.

On the image side, observers have noted syntax-aware code and text generation alongside moderation that appears looser than some rivals. A playful "refrigerator magnet" prompt reportedly revealed a portion of the system prompt, a reminder that prompt injection is not just a developer concern.

Video is another area where capabilities are translating into business impact. Sora 2 can generate cinematic, multi-shot videos with consistent characters from text or images, which lets teams accelerate marketing content, broaden A/B testing and cut the need for studios on many projects. Access paths now span web, mobile, desktop apps and an API, and the market has already produced third-party platforms that promise exports without watermarks.

Teams experimenting with Sora are being advised to measure success by outcomes such as conversion rates, lower support loads or improved lead quality rather than just aesthetic fidelity. Implementation advice favours clear intent, structured prompts and iterative variation, with more advanced workflows assembling multi-shot storyboards, using match cuts to maintain rhythm, controlling lighting for continuity and anchoring character consistency across scenes.

III. Governance, Risk and Regulation

Governance as a Product Requirement

Amid all this activity, data risk has become a central theme for AI leaders. One governance specialist has consolidated common problem patterns into the PROTECT framework, which offers a way to map and mitigate the most material risks.

The first concern is the use of public AI tools for work content, which raises the chance of leakage or unwanted training on proprietary data. The recommended answer combines user guidance, approved internal alternatives, and technical or legal controls such as data scanning and blocking.

A second pressure point is rogue internal projects that bypass review, create compliance blind spots and build up technical debt. Proportionate oversight is key, calibrated to data sensitivity and paired with streamlined governance, so teams are not incentivised to route around it.

Third-party vendors can be opportunistic with data, so due diligence and contractual clauses need to prevent cross-customer training and make expectations clear with templates and guidance.

Technical attacks are another strand, from prompt injection to data exfiltration or the misuse of agents. Layered defences help here, including input validation, prompt sanitisation, output filtering, monitoring, red-teaming, and strict limits on access and privilege.

Embedded assistants and meeting bots come with permission risks when they operate over shared drives and channels, and agentic systems can amplify exposure if left unchecked, so the advice is to enforce least-privilege access, start on low-risk data, and keep robust audit trails.

Compliance risks span privacy laws such as GDPR with their demands for a lawful basis, IP and copyright constraints, contractual obligations, and the AI Act's emphasis on data quality. Legal and compliance checks need to be embedded at data sourcing, model training and deployment, backed by targeted training.

Finally, cross-border restrictions matter. Transfers should be mapped across systems and sub-processors, with checks for Data Privacy Framework certification, standard contractual clauses where needed, and transfer impact assessments that take account of both GDPR and newer rules such as the US Bulk Data Transfer Rule.

Regulatory Pragmatism

Regulators are not standing still, either. In the European Commission has proposed amendments to the AI Act through a Digital Omnibus package as the trilogue process rolls on. Six changes are in focus:

  • High-risk timelines would be tied to the approval of standards, with a backstop of December 2027 for Annex III systems and August 2028 for Annex I products if delays continue, though the original August 2026 date still holds otherwise.
  • Transparency rules on AI-detectable outputs under Article 50(2) would be delayed to February 2027 for systems placed on the market before August 2026, with no delay for newer systems.
  • The plan removes the need to register Annex III systems in the public database where providers have documented under Article 6(3) that a system is not high risk.
  • AI literacy would shift from a mandatory organisation-wide requirement to encouragement, except where oversight of high-risk systems demands it.
  • There is also a move to centralise supervision by the AI Office for systems built on general-purpose models by the same provider, and for huge online platforms and search engines, which is intended to reduce fragmentation across member states.
  • Finally, proportionality measures would define Small Mid-Cap companies and extend simplified obligations and penalty caps that currently apply to SMEs.

If adopted, the package would grant more time and reduce administrative load in some areas, at the expense of certainty and public transparency.

IV. Strategic Implications

The picture that emerges is one of pragmatic integration. Connectors make it feasible to keep work inside a single chat while drawing on the systems people already use. Platform choices are converging, so it makes sense to optimise for the suite that fits the current stack and to plan for switching costs that accumulate over time.

Agentic orchestration is moving from slides to code, but teams will get further by focusing on reliable tooling, clear governance and value measures that match business goals. Regulation is edging towards more flexible timelines and centralised oversight in places, which may lower administrative load without removing the need for discipline.

The sensible posture is measured experimentation: start with read-only access to lower-risk data, design routines that remove drudgery, introduce write operations with approvals, and monitor what is actually changing. The tools are improving quickly, yet the organisations that benefit most will be those that match innovation with proportionate controls and make thoughtful choices now that will hold their shape for the decade ahead.

Keyboard remapping on macOS with Karabiner-Elements for cross-platform work

20th November 2025

This is something that I have been planing to share for a while; working across macOS, Linux and Windows poses a challenge to muscle memory when it comes to keyboard shortcuts. Since the macOS set up varies from the others, it was that which I set to harmonise with the others. Though the result is not full compatibility, it is close enough for my needs.

The need led me to install Karabiner-Elements and Karabiner-EventViewer. The latter has its uses for identifying which key is which on a keyboard, which happens to be essential when you are not using a Mac keyboard. While it is not needed all the time, the tool is a godsend when doing key mappings.

Karabiner-Elements is what holds the key mappings and needs to run all the time for them to be activated. Some are simple and others are complex; it helps the website is laden with examples of the latter. Maybe that is how an LLM can advise on how to set up things, too. Before we come to the ones that I use, here are the simple mappings that are active on my Mac Mini:

left_command → left_control

left_comtrol → left_command

This swaps the left-hand Command and Control keys while leaving their right-hand ones alone. It means that the original functionality is left for some cases when changing it for the keys that I use the most. However, I now find that I need to use the Command key in the Terminal instead of the Control counterpart that I used before the change, a counterintuitive situation that I overlook given how often the swap is needed in other places like remote Linux and Windows sessions.

grave_accent_and_tilde → non_us_backslash

non_us_backslash → non_us_pound

non_us_pound → grave_accent_and_tilde

It took a while to get this three-way switch figured out, and it is a bit fiddly too. All the effort was in the name of getting backslash and hash (pound in the US) keys the right way around for me, especially in those remote desktop sessions. What made the thing really tricky was the need to deal with Shift key behaviour, which necessitated the following script:

{
    "description": "Map grave/tilde key to # and ~ (forced behaviour, detects Shift)",
    "manipulators": [
        {
            "conditions": [
                {
                    "name": "shift_held",
                    "type": "variable_if",
                    "value": 1
                }
            ],
            "from": {
                "key_code": "grave_accent_and_tilde",
                "modifiers": { "optional": ["any"] }
            },
            "to": [{ "shell_command": "osascript -e 'tell application \"System Events\" to keystroke \"~\"'" }],
            "type": "basic"
        },
        {
            "conditions": [
                {
                    "name": "shift_held",
                    "type": "variable_unless",
                    "value": 1
                }
            ],
            "from": {
                "key_code": "grave_accent_and_tilde",
                "modifiers": { "optional": ["any"] }
            },
            "to": [
                {
                    "key_code": "3",
                    "modifiers": ["option"]
                }
            ],
            "type": "basic"
        },
        {
            "from": { "key_code": "left_shift" },
            "to": [
                {
                    "set_variable": {
                        "name": "shift_held",
                        "value": 1
                    }
                },
                { "key_code": "left_shift" }
            ],
            "to_after_key_up": [
                {
                    "set_variable": {
                        "name": "shift_held",
                        "value": 0
                    }
                }
            ],
            "type": "basic"
        },
        {
            "from": { "key_code": "right_shift" },
            "to": [
                {
                    "set_variable": {
                        "name": "shift_held",
                        "value": 1
                    }
                },
                { "key_code": "right_shift" }
            ],
            "to_after_key_up": [
                {
                    "set_variable": {
                        "name": "shift_held",
                        "value": 0
                    }
                }
            ],
            "type": "basic"
        }
    ]
}

Here, I resorted to AI to help get this put in place. Even then, there was a deal of toing and froing before the setup worked well. After that, it was time to get the quote (") and at (@) symbols assigned to what I was used to having on a British English keyboard:

{
    "description": "Swap @ and \" keys (Shift+2 and Shift+quote)",
    "manipulators": [
        {
            "from": {
                "key_code": "2",
                "modifiers": {
                    "mandatory": ["shift"],
                    "optional": ["any"]
                }
            },
            "to": [
                {
                    "key_code": "quote",
                    "modifiers": ["shift"]
                }
            ],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "quote",
                "modifiers": {
                    "mandatory": ["shift"],
                    "optional": ["any"]
                }
            },
            "to": [
                {
                    "key_code": "2",
                    "modifiers": ["shift"]
                }
            ],
            "type": "basic"
        }
    ]
}

The above possibly was one of the first changes that I made, and took less time than some of the others that came after it. There was another at the end that was even simpler again: neutralising the Caps Lock key. That came up while I was perusing the Karabiner-Elements website, so here it is:

{
    "manipulators": [
        {
            "description": "Change caps_lock to command+control+option+shift.",
            "from": {
                "key_code": "caps_lock",
                "modifiers": { "optional": ["any"] }
            },
            "to": [
                {
                    "key_code": "left_shift",
                    "modifiers": ["left_command", "left_control", "left_option"]
                }
            ],
            "type": "basic"
        }
    ]
}

That was the simplest of the lot to deploy, being a simple copy and paste effort. It also halted mishaps when butter-fingered actions on the keyboard activated capitals when I did not need them. While there are occasions when the facility would have its uses, it has not noticed its absence since putting this in place.

At the end of all the tinkering, I now have a set-up that works well for me. While possible enhancements may include changing the cursor positioning and corresponding highlighting behaviours, I am happy to leave these aside for now. Compatibly with British and Irish keyboards together with smoother working in remote sessions was what I sought, and I largely have that. Thus, I have no complaints so far.

Ansible automation for Linux Mint updates with repository failover handling

7th November 2025

Recently, I had a Microsoft PPA output disrupt an Ansible playbook mediated upgrade process for my main Linux workstation. Thus, I ended up creating a failover for this situation, and the first step in the playbook was to define the affected repo:

  vars:
    microsoft_repo_url: "https://packages.microsoft.com/repos/code/dists/stable/InRelease"

The next move was to start defining tasks, with the first testing the repo to pick up any lack of responsiveness and flag that for subsequent operations.

  tasks:
  - name: Check Microsoft repository availability
    uri:
      url: "{{ microsoft_repo_url }}"
      method: HEAD
      return_content: no
      timeout: 10
    register: microsoft_repo_check
    failed_when: false

  - name: Set flag to skip Microsoft updates if unreachable
    set_fact:
      skip_microsoft_repos: "{{ microsoft_repo_check.status is not defined or microsoft_repo_check.status != 200 }}"

In the event of a failure, the next task was to disable the repo to allow other processing to take place. This was accomplished by temporarily renaming the relevant files under /etc/apt/sources.list.d/.

    - name: Temporarily disable Microsoft repositories
    become: true
    shell: |
      for file in /etc/apt/sources.list.d/microsoft*.list; do
        [ -f "$file" ] && mv "$file" "${file}.disabled"
      done
      for file in /etc/apt/sources.list.d/vscode*.list; do
        [ -f "$file" ] && mv "$file" "${file}.disabled"
      done
    when: skip_microsoft_repos | default(false)
    changed_when: false

With that completed, the rest of the update actions could be performed near enough as usual.

  - name: Update APT cache (retry up to 5 times)
    apt:
      update_cache: yes
    register: apt_update_result
    retries: 5
    delay: 10
    until: apt_update_result is succeeded

  - name: Perform normal upgrade
    apt:
      upgrade: yes
    register: apt_upgrade_result
    retries: 3
    delay: 10
    until: apt_upgrade_result is succeeded

  - name: Perform dist-upgrade with autoremove and autoclean
    apt:
      upgrade: dist
      autoremove: yes
      autoclean: yes
    register: apt_dist_result
    retries: 3
    delay: 10
    until: apt_dist_result is succeeded

After those, another renaming operation restores the earlier filenames to what they were.

  - name: Re-enable Microsoft repositories
    become: true
    shell: |
      for file in /etc/apt/sources.list.d/*.disabled; do
        base="$(basename "$file" .disabled)"
        if [[ "$base" == microsoft* || "$base" == vscode* || "$base" == edge* ]]; then
          mv "$file" "/etc/apt/sources.list.d/$base"
        fi
      done
    when: skip_microsoft_repos | default(false)
    changed_when: false

Needless to say, this disabling only happens in the event of there being a system failure. Otherwise, the steps are skipped and everything else is completed as it should be. While there is some cause for extended the repository disabling actions to other third repos as well, that is something that I will leave aside for now. Even this shows just how much can be done using Ansible playbooks and how much automation can be achieved. As it happens, I even get Flatpaks updated in much the same way:

    -   name: Ensure Flatpak is installed
      apt:
        name: flatpak
        state: present
        update_cache: yes
        cache_valid_time: 3600

  -   name: Update Flatpak remotes
      command: flatpak update --appstream -y
      register: flatpak_appstream
      changed_when: "'Now at' in flatpak_appstream.stdout"
      failed_when: flatpak_appstream.rc != 0

  -   name: Update all Flatpak applications
      command: flatpak update -y
      register: flatpak_result
      changed_when: "'Now at' in flatpak_result.stdout"
      failed_when: flatpak_result.rc != 0

  -   name: Install unused Flatpak applications
      command: flatpak uninstall --unused
      register: flatpak_cleanup
      changed_when: "'Nothing' not in flatpak_cleanup.stdout"
      failed_when: flatpak_cleanup.rc != 0

  -   name: Repair Flatpak installations
      command: flatpak repair
      register: flatpak_repair
      changed_when: flatpak_repair.stdout is search('Repaired|Fixing')
      failed_when: flatpak_repair.rc != 0

The ability to call system commands as you see in the above sequence is an added bonus, though getting the response detection completely sorted remains an outstanding task. All this has only scratched the surface of what is possible.

Automating Positron and RStudio updates on Linux Mint 22

6th November 2025

Elsewhere, I have written about avoiding manual updates with VSCode and VSCodium. Here, I come to IDE's produced by Posit, formerly RStudio, for data science and analytics uses. The first is a more recent innovation that works with both R and Python code natively, while the second has been around for much longer and focusses on native R code alone, though there are R packages allowing an interface of sorts with Python. Neither are released via a PPA, necessitating either manual downloading or the scripted approach taken here for a Linux system. Each software tool will be discussed in turn.

Positron

Now, we work through a script that automates the upgrade process for Positron. This starts with a shebang line calling the bash executable before moving to a line that adds safety to how the script works using a set statement. Here, the -e switch triggers exiting whenever there is an error, halting the script before it carries on to perform any undesirable actions. That is followed by the -u switch that causes errors when unset variables are called; normally these would be assigned a missing value, which is not desirable in all cases. Lastly, the -o pipefail switch causes a pipeline (cmd1 | cmd2 | cm3) to fail if any command in the pipeline produces an error, which can help debugging because the error is associated with the command that fails to complete.

#!/bin/bash
set -euo pipefail

The next step then is to determine the architecture of the system on which the script is running so that the correct download is selected.

ARCH=$(uname -m)
case "$ARCH" in
  x86_64) POSIT_ARCH="x64" ;;
  aarch64|arm64) POSIT_ARCH="arm64" ;;
  *) echo "Unsupported arch: $ARCH"; exit 1 ;;
esac

Once that completes, we define the address of the web page to be interrogated and the path to the temporary file that is to be downloaded.

RELEASES_URL="https://github.com/posit-dev/positron/releases"
TMPFILE="/tmp/positron-latest.deb"

Now, we scrape the page to find the address of the latest DEB file that has been released.

echo "Finding latest Positron .deb for $POSIT_ARCH..."
DEB_URL=$(curl -fsSL "$RELEASES_URL" \
  | grep -Eo "https://cdn\.posit\.co/[A-Za-z0-9/_\.-]+Positron-[0-9\.~-]+-${POSIT_ARCH}\.deb" \
  | head -n 1)

If that were to fail, we get an error message produced before the script is aborted.

if [ -z "${DEB_URL:-}" ]; then
  echo "Could not find a .deb link for ${POSIT_ARCH} on the releases page"
  exit 1
fi

Should all go well thus far, we download the latest DEB file using curl.

echo "Downloading: $DEB_URL"
curl -fL "$DEB_URL" -o "$TMPFILE"

When the download completes, we try installing the package using apt, much like we do with a repo, apart from specifying an actual file path on our system.

echo "Installing Positron..."
sudo apt install -y "$TMPFILE"

Following that, we delete the installation file and issue a message informing the user of the task's successful completion.

echo "Cleaning up..."
rm -f "$TMPFILE"

echo "Done."

When I do this, I tend to find that the Python REPL console does not open straight away, causing me to shut down Positron and leaving things for a while before starting it again. There may be temporary files that need to be expunged and that needs its own time. Someone else might have a better explanation that I am happy to use if that makes more sense than what I am suggesting. Otherwise, all works well.

RStudio

A lot of the same processing happens during the script updating RStudio, so we will just cover the differences. The set -x statement ensures that every command is printed to the console for the debugging that was needed while this was being developed. Otherwise, much code, including architecture detection, is shared between the two apps.

#!/bin/bash
set -euo pipefail
set -x

# --- Detect architecture ---
ARCH=$(uname -m)
case "$ARCH" in
  x86_64) RSTUDIO_ARCH="amd64" ;;
  aarch64|arm64) RSTUDIO_ARCH="arm64" ;;
  *) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac

Figuring out the distro version and the web page to scrape was where additional effort was needed, and that is reflected in some of the code that follows. Otherwise, many of the ideas applied with Positron also have a place here.

# --- Detect Ubuntu base ---
DISTRO=$(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || true)
[ -z "$DISTRO" ] && DISTRO="noble"
# --- Define paths ---
TMPFILE="/tmp/rstudio-latest.deb"
LOGFILE="/var/log/rstudio_update.log"

echo "Detected Ubuntu base: ${DISTRO}"
echo "Fetching latest version number from Posit..."

# --- Get version from Posit's official RStudio Desktop page ---
VERSION=$(curl -s https://posit.co/download/rstudio-desktop/ \
  | grep -Eo 'rstudio-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+' \
  | head -n 1 \
  | sed -E 's/rstudio-([0-9]+\.[0-9]+\.[0-9]+-[0-9]+)/\1/')

if [ -z "$VERSION" ]; then
  echo "Error: Could not extract the latest RStudio version number from Posit's site."
  exit 1
fi

echo "Latest RStudio version detected: ${VERSION}"

# --- Construct download URL (Jammy build for Noble until Noble builds exist) ---
BASE_DISTRO="jammy"
BASE_URL="https://download1.rstudio.org/electron/${BASE_DISTRO}/${RSTUDIO_ARCH}"
FULL_URL="${BASE_URL}/rstudio-${VERSION}-${RSTUDIO_ARCH}.deb"

echo "Downloading from:"
echo "  ${FULL_URL}"

# --- Validate URL before downloading ---
if ! curl --head --silent --fail "$FULL_URL" >/dev/null; then
  echo "Error: The expected RStudio package was not found at ${FULL_URL}"
  exit 1
fi

# --- Download and install ---
curl -L "$FULL_URL" -o "$TMPFILE"
echo "Installing RStudio..."
sudo apt install -y "$TMPFILE" | tee -a "$LOGFILE"

# --- Clean up ---
rm -f "$TMPFILE"
echo "RStudio update to version ${VERSION} completed successfully." | tee -a "$LOGFILE"

When all ended, RStudio worked without a hitch, leaving me to move on to other things. The next time that I am prompted to upgrade the environment, this is the way I likely will go.

A primer on Regular Expressions in PowerShell: Learning through SAS code analysis

5th November 2025

Many already realise that regular expressions are a powerful tool for finding patterns in text, though they can be complex in their own way. Thus, this primer uses a real-world example (scanning SAS code) to introduce the fundamental concepts you need to work with regex in PowerShell. Since the focus here is on understanding how patterns are built and applied, you do not need to know SAS to follow along.

Getting Started with Pattern Matching

Usefully, PowerShell gives you several ways to test whether text matches a pattern, and the simplest is the -match operator:

$text = "proc sort data=mydata;"
$text -match "proc sort"  # Returns True

When -match finds a match, it returns True. PowerShell's -match operator is case-insensitive by default, so "PROC SORT" would also match. If you need case-sensitive matching, use -cmatch instead.

For more detailed results, Select-String is useful:

$text | Select-String -Pattern "proc sort"

This shows you where the match occurred and provides context around it.

Building Your First Patterns

Literal Matches

The simplest patterns match exactly what you type. If you want to find the text proc datasets, you write:

$pattern = "proc datasets"

This will match those exact words in sequence.

Special Characters and Escaping

Some characters have special meaning in regex. The dot (.) matches any single character, so if you wish to match a literal dot, you need to escape it with a backslash:

$pattern = "first\."  # Matches "first." literally

Without the backslash, first. would match "first" followed by any character (for example, "firstly", "first!", "first2").

Alternation

The pipe symbol (|) lets you match one thing or another:

$pattern = "(delete;$|output;$)"

This matches either delete; or output; at the end of a line. The dollar sign ($) is an anchor that means "end of line".

Anchors: Controlling Where Matches Occur

Anchors do not match characters. Instead, they specify positions in the text.

  • ^ matches the start of a line
  • $ matches the end of a line
  • \b matches a word boundary (the position between a word character and a non-word character)

Here is a pattern that finds proc sort only at the start of a line:

$pattern = "^\s*proc\s+sort"

Breaking this down:

  • ^ anchors to the start of the line
  • \s* matches zero or more whitespace characters
  • proc matches the literal text
  • \s+ matches one or more whitespace characters
  • sort matches the literal text

The \s is a shorthand for any whitespace character (spaces, tabs, newlines). The * means "zero or more" and + means "one or more".

Quantifiers: How Many Times Something Appears

Quantifiers control repetition:

  • * means zero or more
  • + means one or more
  • ? means zero or one (optional)
  • {n} means exactly n times
  • {n,} means n or more times
  • {n,m} means between n and m times

By default, quantifiers are greedy (they match as much as possible). Adding ? after a quantifier makes it reluctant (it matches as little as possible):

$pattern = "%(m|v).*?(?=[,(;\s])"

Here .*? matches any characters, but as few as possible before the next part of the pattern matches. This is useful when you want to stop at the first occurrence of something rather than continuing to the last.

Groups and Capturing

Parentheses create groups, which serve two purposes: they let you apply quantifiers to multiple characters, and they capture the matched text for later use:

$pattern = "(first\.|last\.)"

This creates a group that matches either first. or last.. The group captures whichever one matches so you can extract it later.

Non-capturing groups, written as (?:...), group without capturing. This is useful when you need grouping for structure but do not need to extract the matched text:

$pattern = "(?:raw\.|sdtm\.|adam\.)"

Lookaheads: Matching Without Consuming

Lookaheads are powerful but can be confusing at first. They check what comes next without including it in the match.

A positive lookahead (?=...) succeeds if the pattern inside matches what comes next:

$pattern = "%(m|v).*?(?=[,(;\s])"

This matches %m or %v followed by any characters, stopping just before a comma, parenthesis, semicolon or whitespace. The delimiter is not included in the match.

A negative lookahead (?!...) succeeds if the pattern inside does not match what comes next:

$pattern = "(?!(?:r_options))\b(raw\.|sdtm\.|adam\.|r_)(?!options)"

This is more complex. It matches library prefixes like raw., sdtm., adam. or r_, but only if:

  • The text at the current position is not r_options
  • The matched prefix is not followed by options

This prevents false matches on text like r_options whilst still allowing r_something_else.

Inline Modifiers

You can change how patterns behave by placing modifiers at the start:

  • (?i) makes the pattern case-insensitive
  • (?-i) makes the pattern case-sensitive
$pattern = "(?i)^\s*proc\s+sort"

This matches proc sort, PROC SORT, Proc Sort or any other case variation.

A Complete Example: Finding PROC SORT Without DATA=

Let us build up a practical pattern step by step. The goal is to find PROC SORT statements that are missing a DATA= option.

Start with the basic match:

$pattern = "proc sort"

Add case-insensitivity and line-start flexibility:

$pattern = "(?i)^\s*proc\s+sort"

Add a word boundary to avoid matching proc sorting:

$pattern = "(?i)^\s*proc\s+sort\b"

Now add a negative lookahead to exclude lines that contain data=:

$pattern = "(?i)^\s*proc\s+sort\b(?![^;\n]*\bdata\s*=\s*)"

This negative lookahead checks the rest of the line (up to a semicolon or newline) and fails if it finds data followed by optional spaces, an equals sign and more optional spaces.

Finally, match the rest of the line:

$pattern = "(?i)^\s*proc\s+sort\b(?![^;\n]*\bdata\s*=\s*).*"

Working with Multiple Patterns

Real-world scanning often involves checking many patterns. PowerShell arrays make this straightforward:

$matchStrings = @(
    "%(m|v).*?(?=[,(;\s])",
    "(raw\.|sdtm\.|adam\.).*?(?=[,(;\s])",
    "(first\.|last\.)",
    "proc datasets"
)

$text = "Use %mvar in raw.dataset with first.flag"

foreach ($pattern in $matchStrings) {
    if ($text -match $pattern) {
        Write-Host "Match found: $pattern"
    }
}

Finding All Matches in a String

The -match operator only tells you whether a pattern matches. To find all occurrences, use [regex]::Matches:

$text = "first.x and last.y and first.z"
$pattern = "(first\.|last\.)"
$matches = [regex]::Matches($text, $pattern)

foreach ($match in $matches) {
    Write-Host "Found: $($match.Value) at position $($match.Index)"
}

This returns a collection of match objects, each containing details about what was found and where.

Replacing Text

The -replace operator applies a pattern and substitutes matching text:

$text = "proc datasets; run;"
$text -replace "proc datasets", "proc sql"
# Result: "proc sql; run;"

You can use captured groups in the replacement:

$text = "raw.demographics"
$text -replace "(raw\.|sdtm\.|adam\.)", "lib."
# Result: "lib.demographics"

Validating Patterns Before Use

Before running patterns against large files, validate that they are correct:

$matchStrings = @(
    "%(m|v).*?(?=[,(;\s])",
    "(raw\.|sdtm\.|adam\.).*?(?=[,(;\s])"
)

foreach ($pattern in $matchStrings) {
    try {
        [regex]::new($pattern) | Out-Null
        Write-Host "Valid: $pattern"
    }
    catch {
        Write-Host "Invalid: $pattern - $($_.Exception.Message)"
    }
}

This catches malformed patterns (for example, unmatched parentheses or invalid syntax) before they cause problems in your scanning code.

Scanning Files Line by Line

A typical workflow reads a file and checks each line against your patterns:

$matchStrings = @(
    "proc datasets",
    "(first\.|last\.)",
    "%(m|v).*?(?=[,(;\s])"
)

$code = Get-Content "script.sas"

foreach ($line in $code) {
    foreach ($pattern in $matchStrings) {
        if ($line -match $pattern) {
            Write-Warning "Pattern '$pattern' found in: $line"
        }
    }
}

Counting Pattern Occurrences

To understand which patterns appear most often:

$results = @{}

foreach ($pattern in $matchStrings) {
    $count = ($code | Select-String -Pattern $pattern).Count
    $results[$pattern] = $count
}

$results | Format-Table

This builds a table showing how many times each pattern matched across the entire file.

Practical Tips

Start simple. Build patterns incrementally. Test each addition to ensure it behaves as expected.

Use verbose mode for complex patterns. PowerShell supports (?x) which allows whitespace and comments inside patterns:

$pattern = @"
(?x)        # Enable verbose mode
^           # Start of line
\s*         # Optional whitespace
proc        # Literal "proc"
\s+         # Required whitespace
sort        # Literal "sort"
\b          # Word boundary
"@

Test against known examples. Create a small set of test strings that should and should not match:

$shouldMatch = @("proc sort;", "  PROC SORT data=x;")
$shouldNotMatch = @("proc sorting", "# proc sort")

foreach ($test in $shouldMatch) {
    if ($test -notmatch $pattern) {
        Write-Warning "Failed to match: $test"
    }
}

Document your patterns. Regular expressions can be cryptic. Add comments explaining what each pattern does and why it exists:

# Match macro variables starting with %m or %v, stopping at delimiters
$pattern1 = "%(m|v).*?(?=[,(;\s])"

# Match library prefixes (raw., sdtm., adam.) before delimiters
$pattern2 = "(raw\.|sdtm\.|adam\.).*?(?=[,(;\s])"

Two Approaches to the Same Problem

The document you started with presents two arrays of patterns. One is extended with negative lookaheads to handle ambiguous cases. The other is simplified for cleaner codebases. Understanding why both exist teaches an important lesson: regex is not one-size-fits-all.

The extended version handles edge cases:

$pattern = "(?i)(?!(?:r_options))\b(raw\.|sdtm\.|adam\.|r_)(?!options)\w*?(?=[,(;\s])"

The simplified version assumes those edge cases do not occur:

$pattern = "(raw\.|sdtm\.|adam\.).*?(?=[,(;\s])"

Choose the approach that matches your data. If you know your text follows strict conventions, simpler patterns are easier to maintain. If you face ambiguity, add the precision you need (but no more).

Moving Forward

This primer has introduced the building blocks: literals, special characters, anchors, quantifiers, groups, lookaheads and modifiers. You have seen how to apply patterns in PowerShell using -match, Select-String and [regex]::Matches. You have also learnt to validate patterns, scan files and count occurrences.

The best way to learn is to experiment. Take a simple text file and try to match patterns in it. Build patterns incrementally. When something does not work as expected, break the pattern down into smaller pieces and test each part separately.

Regular expressions are not intuitive at first, but they become clear with practice. The examples here are drawn from SAS code analysis, yet the techniques apply broadly. Whether you are scanning logs, parsing configuration files or extracting data from reports, the principles remain the same: understand what you want to match, build the pattern step by step, test thoroughly and document your work.

Rendering Markdown in WordPress without plugins by using Parsedown

4th November 2025

Much of what is generated using GenAI as articles is output as Markdown, meaning that you need to convert the content when using it in a WordPress website. Naturally, this kind of thing should be done with care to ensure that you are the creator and that it is not all the work of a machine; orchestration is fine, regurgitation does that add that much. Naturally, fact checking is another need as well.

Writing plain Markdown has secured its own following as well, with WordPress plugins switching over the editor to facilitate such a mode of editing. When I tried Markup Markdown, I found it restrictive when it came to working with images within the text, and it needed a workaround for getting links to open in new browser tabs as well. Thus, I got rid of it to realise that it had not converted any Markdown as I expected, only to provide rendering at post or page display time. Rather than attempting to update the affected text, I decided to see if another solution could be found.

This took me to Parsedown, which proved to be handy for accomplishing what I needed once I had everything set in place. First, that meant cloning its GitHub repo onto the web server. Next, I created a directory called includes under that of my theme. Into there, I copied Parsedown.php to that location. When all was done, I ensured that file and directory ownership were assigned to www-data to avoid execution issues.

Then, I could set to updating the functions.php file. The first line to get added there included the parser file:

require_once get_template_directory() . '/includes/Parsedown.php';

After that, I found that I needed to disable the WordPress rendering machinery because that got in the way of Markdown rendering:

remove_filter('the_content', 'wpautop');
remove_filter('the_content', 'wptexturize');

The last step was to add a filter that parsed the Markdown and passed its output to WordPress rendering to do the rest as usual. This was a simple affair until I needed to deal with code snippets in pre and code tags. Hopefully, the included comments tell you much of what is happening. A possible exception is $matches[0]which itself is an array of entire <pre>...</pre> blocks including the containing tags, with $i => $block doing a $key (not the same variable as in the code, by the way) => $value lookup of the values in the array nesting.

add_filter('the_content', function($content) {
    // Prepare a store for placeholders
    $placeholders = [];

    // 1. Extract pre blocks (including nested code) and replace with safe placeholders
    preg_match_all('//si', $content, $pre_matches);
    foreach ($pre_matches[0] as $i => $block) {
        $key = "§PREBLOCK{$i}§";
        $placeholders[$key] = $block;
        $content = str_replace($block, $key, $content);
    }

    // 2. Extract standalone code blocks (not inside pre)
    preg_match_all('/).*?<\/code>/si', $content, $code_matches);
    foreach ($code_matches[0] as $i => $block) {
        $key = "§CODEBLOCK{$i}§";
        $placeholders[$key] = $block;
        $content = str_replace($block, $key, $content);
    }

    // 3. Run Parsedown on the remaining content
    $Parsedown = new Parsedown();
    $content = $Parsedown->text($content);

    // 4. Restore both pre and code placeholders
    foreach ($placeholders as $key => $block) {
        $content = str_replace($key, $block, $content);
    }

    // 5. Apply paragraph formatting
    return wpautop($content);
}, 12);

All of this avoided dealing with extra plugins to produce the required result. Handily, I still use the Classic Editor, which makes this work a lot more easily. There still is a Markdown import plugin that I am tempted to remove as well to streamline things. That can wait, though. It best not add any more of them any way, not least avoid clashes between them and what is now in the theme.

Building an email summariser for Apple Mail using both OpenAI and Shortcuts

3rd November 2025

One thing that I am finding useful in Outlook is the ability to summarise emails using Copilot, especially for those that I do not need to read in full. While Apple Mail does have something similar, I find it to be very terse in comparison. Thus, I started to wonder about just that by using the OpenAI API and the Apple Shortcuts app. All that follows applies to macOS Sequoia, though the Tahoe version is with us too.

Prerequisite

While you can have the required OpenAI API key declared within the Shortcut, that is a poor practice from a security point of view. Thus, you will need this to be stored in the macOS keychain, which can be accomplished within a Terminal session and issuing a command like the following:

security add-generic-password -a openai -s openai_api_key -w [API Key]

In the command above, you need to add the actual API key before executing it to ensure that it is available to the steps that follow. To check that all is in order, issue the following command to see the API key again:

security find-generic-password -a openai -s openai_api_key -w

This process also allows you to rotate credentials without editing the workflow, allowing for a change of API keys should that ever be needed.

Building the Shortcut

With the API safely stored, we can move onto the actual steps involved in setting up the Email Summarisation Shortcut that we need.

Step 1: Collect Selected Email Messages

First, open the Shortcuts app and create a new Shortcut. Then, add a Run AppleScript action and that contains the following code:

tell application "Mail"
    set selectedMessages to selection
    set collectedText to ""
    repeat with msg in selectedMessages
        set msgSubject to subject of msg
        set msgBody to content of msg
        set collectedText to collectedText & "Subject: " & msgSubject & return & msgBody & return & return
    end repeat
end tell
return collectedText

This script loops through the selected Mail messages and combines their subjects and bodies into a single text block.

Step 2: Retrieve the API Key

Next, add a Run Shell Script action and paste this command:

security find-generic-password -a openai -s openai_api_key -w | tr -d 'n'

This reads the API key from the keychain and strips any trailing newline characters that could break the authentication header, the first of several gotchas that took me a while to sort.

Step 3: Send the Request to GPT-5

The, add a Get Contents of URL action and configure it as follows:

URL: https://api.openai.com/v1/chat/completions

Method: POST

Headers:

  • Authorization: Bearer [Shell Script result]
  • Content-Type: application/json

Request Body (JSON):

{
  "model": "gpt-5",
  "temperature": 1,
  "messages": [
    {
      "role": "system",
      "content": "Summarise the following email(s) clearly and concisely."
    },
    {
      "role": "user",
      "content": "[AppleScript result]"
    }
  ]
}

When this step is executed, it replaces [Shell Script result] with the output from Step 2, and [AppleScript result] with the output from Step 1. Here, GPT-5 only accepts a temperature value of 1 (a lower value would limit the variability in the output if it could be used), unlike other OpenAI models and what you may see documented elsewhere.

Step 4: Extract the Summary from the Response

The API returns a JSON response that you need to parse, an operation that differs according to the API; Anthropic Claude has a different structure, for example. To accomplish this for OpenAI's gateway, add these actions in sequence to replicate what is achieved using in Python by loading completion.choices[0].message.content:

  1. Get Dictionary from Input (converts the response to a dictionary)
  2. Get Dictionary Value for key "choices"
  3. Get Item from List (select item 1)
  4. Get Dictionary Value for key "message"
  5. Get Dictionary Value for key "content"

One all is done (and it took me a while to get that to happen because of the dictionary → list → dictionary → dictionary flow; figuring out that not everything in the nesting was a dictionary took some time), click the information button on this final action and rename it to Summary Text. This makes it easier to reference in later steps.

Step 5: Display the Summary

Add a Show action and select the Summary Text variable. This shows the generated summary in a window with Close and Share buttons. The latter allows you to send to output to applications like Notes or OneNote, but not to Pages or Word. In macOS Sequoia, the list is rather locked down, which means that you cannot extend it beyond the available options. In use or during setup testing, beware of losing the open summary window behind others if you move to another app because it is tricky to get back to without using the CTRL + UP keyboard shortcut to display all open windows at once.

Step 6: Copy to Clipboard

Given the aforementioned restrictions, there is a lot to be said for adding a Copy to Clipboard action with the Summary Text variable as input. This allows you to paste the summary immediately into other apps beyond those available using the Share facility.

Step 7: Return Focus to Mail

After all these, add another Run AppleScript action with this single line:

tell application "Mail" to activate

This brings the Mail app back to the front, which is particularly useful when you trigger the Shortcut via a keyboard shortcut or if you move to another app window.

Step 8: Make the New Shortcut Available for Use

Lastly, click the information button at the top of your Shortcut screen. One useful option that can be activated is the Pin in Menu Bar one, which adds a menu to the top bar with an entry for the new Email Summary Shortcut in there. Ticking the box for the Use as Quick Action option allows you to set a keyboard shortcut. Until, the menu bar option appealed to me, that did have its uses. You just have to ensure that what you select does not override any combination that is in use already. Handily, I also found icons for my Shortcuts in Launchpad as well, which means that they also could be added to the Dock, something that I also briefly did.

Using the Shortcut

After expending the effort needed to set it up, using the new email summariser is straightforward. In Apple Mail, select one or more messages that you want to summarise; there is no need to select and copy the contained textual content because the Shortcut does that for you. Using the previously assigned keyboard combination, menu or Launchpad icon then triggers the summarisation processing. Thus, a window appears moments later displaying the generated summary while the same text is copied to your clipboard, ready to paste anywhere you need it to go. When you dismiss the pop-up window, the Mail app then automatically comes back into focus again.

  • The content, images, and materials on this website are protected by copyright law and may not be reproduced, distributed, transmitted, displayed, or published in any form without the prior written permission of the copyright holder. All trademarks, logos, and brand names mentioned on this website are the property of their respective owners. Unauthorised use or duplication of these materials may violate copyright, trademark and other applicable laws, and could result in criminal or civil penalties.

  • All comments on this website are moderated and should contribute meaningfully to the discussion. We welcome diverse viewpoints expressed respectfully, but reserve the right to remove any comments containing hate speech, profanity, personal attacks, spam, promotional content or other inappropriate material without notice. Please note that comment moderation may take up to 24 hours, and that repeatedly violating these guidelines may result in being banned from future participation.

  • By submitting a comment, you grant us the right to publish and edit it as needed, whilst retaining your ownership of the content. Your email address will never be published or shared, though it is required for moderation purposes.