Chris Hannah

Day 1 Impressions of the iPhone 15 Pro Max #

After owning an iPhone 13 Pro for two years, I received my new iPhone 15 Pro Max in the post earlier today. And I already want to share some of my very early impressions. Primarily because I’m very glad I upgraded to this model, and it’s (obviously?) a very good upgrade from the 13 Pro.


Starting with the actual design of the device, it’s not too much different from recent models. I don’t personally care what material is used, but I’m definitely a fan of the finish. And while the edges of the 13 didn’t cause me any trouble, I am finding myself appreciating the slightly softer edges of the 15.

In both the 13 Pro and my new 15 Pro Max, I’ve chosen the colour closest to black. The 15 seems slightly darker in appearance, which I appreciate. My favourite colour so far was the black iPhone 5, but I think this is pretty close.

Action Button

I haven’t set this up properly yet. Although I do envisage settling on having it launch either the standard Camera app or possibly Halide.

I can’t say I have particularly strong opinions about it at the moment, but I think I’d prefer if the action was immediate, rather than requiring a long-press. Although, it would be better if you could configure different actions for a long-press or a typical button press.


Most of the time I’ve charged my 13 Pro via a MagSafe charger on my bedside table. However, I do have a Lightning cable in my work bag that I use occasionally, and I also prefer to take a cable when on trips. So USB C won’t exactly make a huge difference to me. But it will certainly be handy to be able to have one cable for practically all of my devices.


I haven’t used this too much yet. But I did play around with Portrait mode, and so far I’ve been very happy with the results. The adjustable aperture and focus point both work well, and while I don’t know how often I will use them, they’re good additions. I think all three cameras are different to my 13 Pro, so while I’ve been initially impressed, I want to spend more time with it to have any real opinions.


I don’t know if the speakers in my 13 Pro had deteriorated, or if an improvement came in last years or this years models. But the speakers seem much better. They’re certainly louder than my iPhone 13 Pro, and they seem to also be clearer.

Dynamic Island

I want to separate my feelings on the Dynamic Island into two parts.

Firstly, it’s clearly a bad thing to have a cutout in a display. It means that software either has to work around it, or completely disregard that area of the screen.

On the other hand, I do like what they did with the cutout. I like having quickly glanceable information there, like timers, food delivery times, and also the currently playing music. And I also like having the ability to tap it and quickly navigate to whatever is appearing. For example, tapping the now playing “bar” (not sure what this is called) to open the Music app is handy.

However, if I was given the option, I’d rather the cutout didn’t exist at all.

My opinions on the 15 Pro Max will no doubt change as I use it. But as for right now, this is how I feel. I may write about it here again, or if not, I’ll probably just post about it on Mastodon.

Text Shot 1.2 #

Text Shot v1.2 is now available!

With it, comes a new author field, and also a source field. Which means a text shot can contain a title, author, source, and the quote. This hopefully makes Text Shot become more usable when sharing quotes from books, and maybe a few other places that I haven’t thought about.

Alongside the new fields, there’s also a “Copy Alt Text” button. This, of course, generates and copies a description of the text shot, that you can upload alongside the image when you post it to sites like Mastodon.

Here’s an example text shot:

And here is what the alt text would be for it:

A text shot containing the following information:
Title: Text Case CLI via Homebrew
Author: Chris Hannah
Highlight: To use these formats, you can pass in input in three different ways - you can use the --input option to pass a string of text, the --in option to specify a file to use as input, or you can pipe in data from stdin.

There’s also a bunch minor UI changes that no-one will probably notice. 😅

If you haven’t already, you can find Text Shot for iOS and macOS on the App Store.

Text Case CLI via Homebrew #

I wrote just a few days ago, about Text Case coming to the command line. And it’s already time to announce that it’s now available to install from Homebrew.

Okay, so it’s not in the core tap, I have my own custom tap (maybe that will happen eventually). But it’s still a pretty easy process.

All you need to do is:

brew tap chrishannah/textcase
brew install textcase

Then you’ll be able to format text using the textcase command. Which is pretty easy. I used it myself to format the slug inside Neovim when writing this post.

While I’m here, I may as well explain what functionality is supported in the very first release of Text Case CLI.

To start off, the currently supported formats are:

To use these formats, you can pass in input in three different ways - you can use the --input option to pass a string of text, the --in option to specify a file to use as input, or you can pipe in data from stdin.

The outputted string will be sent to stdout, but you can also use the --out option to have the resulting text written as a file instead.

If you have any questions or feedback about Text Case CLI, then feel free to email me, or you can find me on Twitter/X or Mastodon.

Written: while relaxing in a caravan in Wells-next-the-Sea.

Using a Swift LSP in Neovim #

I spent quite a bit of time trying to work out how to get a Swift LSP working in Neovim the other day. Enough time that I wanted to share it here.

I won’t go into too much detail about what an LSP is, how it works, or its benefits. There are a lot of other people who can do a much better job of that than me.

However, I would like to say that this is just how I have it working myself. There are bound to be many other ways you can get a Swift LSP working in Neovim. It just happens that this is a pretty simple configuration, that I will likely also improve in the future.

The first step is to obviously install the Swift LSP. Apple’s one (I’m not sure if there are any others) is SourceKit-LSP. You can build this from source (this is what I did), or the README also explains that it’s also included in the toolchains found on So do whatever is best for you. But, be sure that it is accessible on your $PATH.

After that, you’ll need to configure Neovim to find the LSP, tell it what files to look for (swift), how it can detect the root directory, the name of the command, etc. And then, more importantly, start the LSP, and attach it to the buffer.

My config looks like this:

 local swift_lsp = vim.api.nvim_create_augroup("swift_lsp", { clear = true })
 vim.api.nvim_create_autocmd("FileType", {
 	pattern = { "swift" },
 	callback = function()
 		local root_dir = vim.fs.dirname(vim.fs.find({
 		}, { upward = true })[1])
 		local client = vim.lsp.start({
 			name = "sourcekit-lsp",
 			cmd = { "sourcekit-lsp" },
 			root_dir = root_dir,
 		vim.lsp.buf_attach_client(0, client)
 	group = swift_lsp,

This is Lua code, so use it in whatever .lua file makes sense to you. However, if you just want to include it in a .vim file, you’ll need to wrap it like so:

" lsp
lua << EOF
-- lua code goes here

After that, you should be able to write terrible Swift code and have it tell you all the things that you’re doing wrong.

Like this, for example:

swift lsp

For reference, my Neovim config is available on GitHub.

Text Case Is Coming to the Command Line #

If you didn’t already know, I make a text transformation app for iOS, iPadOS, and macOS, called Text Case. It’s grown quite a bit over it’s lifetime, supporting now well over 60 different formats, custom flows, and many more features. And I’ve now decided, the next place I want to bring it to, is the command line

And not just to be an additional benefit of purchasing the existing apps. This is an open-source project, which eventually I want to distribute via Homebrew.

That may sound weird, seeing as you need to pay to have it on other platforms. Why would I release it for free?

Basically, I’m a fan of free and open-source software. And while I don’t think it’s crazy to ask for £2.99 for an app that I’ve put quite a number of hours into making, I do also want to give something back. So, I plan on building a version of Text Case that will allow people to quickly transform text, using the command line.

At the same time, I’m bound to improve my own programming skills, seeing as my code will now be in the public eye. And maybe I’ll learn more about building and distributing open-source software.

However, I do have to say that not all of Text Case will be coming to the command line. I only plan on adding support for the core formats. At least, that’s the plan for now.

Right now, it supports just 17 different formats, and I’ll be working on bringing the rest over as time goes on. And because it’s open-source, you can keep up to date with the current state by checking it out on GitHub. (You can even use it right now, if you’re comfortable with building from source.)

Here’s a quick screenshot of the current version in action:

textcase cli

I hope this news sounds good to at least some of you. In the mean time, I’m going to get back to adding more format options.

Configuring Nginx to Work With Hugo’s “Ugly” URLs #

I recently migrated my blog from running on Ghost to Hugo. With that, came a few changes to how the pages were built, how the URLs were formed, and also the rules around showing 404s and redirecting where possible.

In a default Hugo installation, individual blog posts are each written as index.html files inside a directory named after the given slug. So you would end up with something like so:

├─ hello-world/
│  ├─ index.html
├─ about/
│  ├─ index.html
├─ my-first-blog-post/
│  ├─ index.html

That means when someone visits either /hello-world or /hello-world/, they’ll be taken to the content (Which is actually stored as /hello-world/index.html).

Just to make things difficult, I didn’t want my file structure to look like that. It felt wrong. Instead I wanted each blog post to be generated as it’s own html file, with the slug being the filename.

Fortunately, Hugo has an option called uglyURLs, which does exactly that. So the same structure above would instead look like this:

├─ about.html
├─ my-first-blog-post.html
├─ hello-world.html

However, while this may look good to me. It introduced a few issues that I had to deal with. Primarily being that the .html extension had to be given for a page to load. There was no more /about being redirected to /about.html. And definitely no /about/ being supported.

That obviously would break a lot of past links to my blog posts. So I had to find a way to deal with this.

So I set about finding a way in nginx, to essentially remove the need to have the extension in the URL. But I then realised, that it wasn’t that simple.

Because, let’s say I have an about page with the filename about.html. Ideally, I want /about.html, /about, and /about/ to redirect to this page. But at the same time, I have a mini-site for my app, Text Shot, that sites at /text-shot/index.html, and ideally I want /text-shot/index.html, /text-shot/, and /text-shot to all redirect to this site.

That led me to a lot of Googling, regular expressions, and quite a bit of time spent trying various options in my nginx configuration. (If you were super unlucky, you may have hit a 404 while I was testing).

By default, my nginx configuration handled the requests with the try_files function:

try_files $uri $uri.html $uri/ $uri =404;

Basically, if request was /about, it would try to fetch a file from these locations (in order): /about, /about.html, /about/, and then if all of those fail, produce a 404.

The code to deal with removing the need to include .html was relatively simple. All it does is check if the request includes the extension, and if so, remove it and perform a 302 redirect to the plain URI.

if ($request_uri ~ ^/(.*)\.html(\?|$)) {
    return 302 /$1;
try_files $uri $uri.html $uri/ $uri =404;

However, that still meant that if you visited /about/, it would think you were looking for /about/index.html, rather than the /about.html page.

So, I had to then add another redirect. So if the url ended in a trailing slash (but wasn’t just /), it would also perform a 302 redirect to a url without the trailing slash. I ended up with this:

location / {
    if ($request_uri ~ ^/(.*)\.html(\?|$)) {
        return 302 /$1;
    if ($request_uri ~ ^/([-a-zA-Z0-9@:%_\+.~?&//=]*)\/$) {
        return 302 /$1;
    try_files $uri $uri.html $uri/index.html $uri/ $uri =404;

For reference, $uri is the full request URL, and $request_uri is anything after the host.

There’s bound to be a way for this logic to be improved. But as far as I can see, it works how I expect, and it gives me the exact behaviour I was looking for.

Now for some examples. First off, the ways in which you would get to a file located at /about.html:

(Note: all redirects are stated, the rest is the priority order of the try_files function.)

  1. /about.html: 302 to /about -> try /about -> try about.html.
  2. /about/: 302 to /about -> try /about -> try about.html.
  3. /about: try /about -> try /about.html.

And for the page located at /text-shot/index.html:

  1. /text-shot: try /text-shot -> try /text-shot.html -> try /text-shot/index.html.
  2. /text-shot/: 302 to /text-shot -> try /text-shot -> try /text-shot.html -> try /text-shot/index.html.
  3. /text-shot/index: try /text-shot/index -> try /text-shot/index.html.
  4. /text-shot/index.html: 302 to /text-shot/index -> try /text-shot/index -> try /text-shot/index.html.
  5. /text-shot/index/: 302 to /text-shot/index -> try /text-shot/index -> try /text-shot/index.html.

Note: nginx will search for an index file when try_files checks $uri/, so there’s no need to handle that. However, there were circular redirects when /text-shot/ would be redirected to /text-shot, and then eventually back to /text-shot/, so I added an explicit attempt to $uri/index.html to avoid this.

Admittedly, a few of those examples are a bit odd, and likely will never happen. But I had to cover all bases.

As you can see, it’s not perfect. There are some requests that go through a 302 just to end up at the exact page that was requested. Like #2 above.

In an ideal world, I’ll never touch this configuration again. And there’s also a low chance that anyone reading this would ever need to do anything similar. But, I can’t say I found many resources online for my specific scenario, so I thought I’d write my own.

I guess I brought this all on myself when I decided I wanted to both install my own custom Hugo blog on a linux VM, and to also want static files with the full filename, not just convenient “pretty” URLs that Hugo generates by default. Oh well, at least I learned a bit more about how to configure nginx I guess.

Blog Update #

As of tonight, I’ve now completed the final stage of transitioning my blog from Ghost to Hugo.

As you may have known, my blog has been powered by Ghost for a few years. It’s definitely served me well. However, a while ago I started to like the idea of static sites. Seeing as my blog is essentially a list of static posts, it felt a bit weird that the pages were being served dynamically. So, I started on the journey of moving to Hugo.

It took a bit of work to adapt my existing theme to work with Hugo’s template system. But I managed to get it pretty similar, while also adding a few improvements at the same time.1

Of course, being a static site, pages now load super fast, and they’re extremely lightweight. But that’s not the only good thing about using Hugo. It’s also super easy to host other static pages alongside the source Markdown post files. This means I can build mini-sites for my apps, and keep them managed within my blog. I can control the structure of the site better, either by using categories/tags or by structuring the source files in the way I want them to be generated. And another great one, I can now create custom pages and templates. For example, I built a custom archive page for all of the posts on the blog, and a few extras that as a bit different for essays and travel updates.

In the early version of this new Hugo blog, I had it being deployed to a Digital Ocean app via a GitHub action that was triggered after pushing new posts. But, that didn’t allow me full control of the VM it was running from, so I decided to switch to a droplet (VM), and have built my own minimal installation.

Obviously, Hugo is installed, but apart from that the only other things I had to install was nginx and certbot. So very minimal. I was wondering how I’d manage the deployment, because I still wanted to have the site automatically rebuild after pushing my changes. Luckily, I found a guide by Josh Hausotter that shows you how to configure a remote repository on your Digital Ocean droplet and a “post receive” action that runs a script whenever changes are received to generate the static files and move them to the correct directory. I honestly never thought about using git this way.

As for how I write and publish my posts from my own machine. I do that using Neovim on my Mac, and then just pushing to the remote repository. Neovim might not be the most trendy tool for writers, but I find it works for me, and I also use it for writing code, so I’m pretty comfortable with the keybinds.

You may be wondering, what does this mean for you? Do you need to change anything? Well, in theory, no. The posts are now stored as static html files, however I have configured nginx for these to work without the extension, and the filenames/slugs haven’t changed. Technically, the RSS feed is now different. Hugo generates the RSS feed in an index.xml file at the base of the site, which you can find here. However, I have set up directs for /feed and /rss, so you shouldn’t need to do anything.

Hopefully, this change will go by mostly unnoticed. But if you do notice something odd, you know where to find me. (Links are at the bottom of the page)

Written: while watching Oblivion with my cat.

  1. The theme is named Hurley. (Lost reference) ↩︎

Using Older iPhone Cameras #

I’ve been thinking about the cameras on older iPhones, and what makes people prefer certain models, rather than whatever model is the newest.

A quick answer would be that it’s just subjective, and we all like different things.

But if you think about it, it’s also affected by how people use cameras, what they class as photography, and also what they expect in a photo.

From a photography point of view, it’s easy to imagine that one model had a desired focal length or aperture. And then, if you go deeper into photography, you might be looking for certain colour reproductions, temperatures, tones, lighting, etc. For example, I’m a fan of natural grain in images, so I would prefer one that produces a certain type of grain.

So there’s already a few different metrics that divide opinion. But on top of that, there are a lot of people that use the iPhone’s camera that aren’t photographers.

Some may want the best tool to capture memories, others might just want to document their life, just like the cliché of people posting photos of their lunch to Instagram.

And in those cases, it may seem that the latest model would always be the best answer. But that might not always be true. Recent phones tend to come with a lof of built-in adjustments and corrections that are applied automatically to photos. Therefore, even if you’re aim is to take a photo that documents a certain scene as accurately as possible, the “better” camera might not actually be the best option.

It’s weird to think that a better camera doesn’t always produce better photos.

Written: Sat on my sofa, drinking a cold Coke, listening to Full Moon by The Black Ghosts.

Celebrating Offline Tech #

Maybe it’s just because my own opinions have been changing recently, but I get the feeling that there seems to be a general resurgence of analogue over digital. Film photography is having a moment, so are mechanical watches it seems, and the act of writing in a physical notebook also seems to be growing in recent years. That’s in no way a definitive list, but it’s just a few things I’ve noticed.

This may sound weird, but it feels to me like it’s in some way related to the current period of nostalgia that we seem to be going through as a society. In a lot of ways, it’s like people are trying to bring back the 90s1.

Technology is always going to be massively affected by feelings of nostalgia, simply because of the rate of change that it goes through. However, while there has always been the divide between analogue and digital, there now seems to be a new divide2 in digital technology, offline vs online.

Matt Birchler wrote about this when talking about using 100% offline technology:

There’s no WiFi or Bluetooth, so it’s just out here on its own. It never has updates to install, so it’s never going to get better, but it’s also not going to change in ways I don’t like. It’s also going to work just as well in 20 years as it does today.

As he mentions in his post, while completely offline technology won’t improve, it also won’t get any worse. Which definitely happens to more recently technology that requires a connection to the internet.

I don’t think we’re at some major turning point in society where we’re all going to start writing in paper notebooks, switching to a dumb phone, etc. But it’s worth at least noticing the new era of always needing to be connected3. And at the same time, celebrating the good sides of having technology that can exist on its own, in the same state, for as long as the hardware still works correctly.

Written: In my dimly lit living room, listening to music from Isenseven videos.

  1. I’m not complaining, the 90’s were pretty good. ↩︎

  2. Great song. ↩︎

  3. Both products and people. ↩︎

The More Social Networks There Are the Less I Want to Use Them #

There’s been an influx of new1 social networks recently, such as Threads, Mastodon, and Bluesky. And to some extent, I’ve tried to keep up with them. But if anything, I’ve noticed that my use of social media, in general, has been dropping.

I used to use Twitter for talking about everything, and Instagram for posting my photography. But then Glass came along and for a while my serious photography was going there, and then BeReal made me want to save a photo for them every day. As for text content, there’s been, Mastodon, and Threads that I’ve tried as alternatives to Twitter.

However, all I tend to use now is just Instagram, Twitter/X, and Mastodon. Instagram is where my friends and family tend to be, and that’s where I post photos of what I’m up to, and also any real photography. Mastodon is where I’ve found a lot of tech bloggers and developers have flocked to, so I’m there for that crowd. And I still haven’t given up on Twitter/X, because I’ve found it to still be the best place for current events, football content, and a bunch of tech people are still there. And when I say, I use these three platforms, that’s not the same level as before. I used to try and read every tweet in my timeline, both on Twitter and Mastodon, and I’d spend countless hours scrolling through Instagram.

Now, I’ve got notifications turned off for everything, and I’d say I browse Twitter/X slightly regularly. But I only really go on Instagram and Mastodon now when I want to post something. I haven’t found them to be good places to browse. I get too sucked in when using Instagram, and I haven’t yet cultivated a good enough following list for me to spend a lot of time in Mastodon2.

I’ve found that right now, I’m more interested in people in the real world than on the internet. That’s not a dig at anyone I’ve talked to online. But it doesn’t replace talking to people in the physical world.

I think the reason why I’m preferring to write for my blog over social media, is that it’s a more biased relationship. It allows me to collect my thoughts, and then express them in whatever form I feel fits the content and context. And then if people want to reply in any way, they can do so via email, Mastodon, X, etc. But, at a slower pace, and also in any which way they feel relevant.

The real-time speed and perceived urgency of social media are reasons why I’ve stepped back from it a bit. So, if you’ve sent me a message online or by email, know that I’m probably not ignoring you. I either haven’t got around to reading it yet, or I haven’t yet found time to think and reply.

Written: On a train from London to Kings Lynn.

  1. Well, some aren’t exactly new. But to a lot of people they are. ↩︎

  2. That’s definitely on me. But I’m not particularly interested in spending much time on it. ↩︎