I’m writing these notes at around 32 thousand feet, again on my way to the Vancouver Spark HQ. Looks like it’s cold there, too. Another short trip but I’m excited to see the team and hope to get a lot done.
My earlier hopes of running a double lap around Stanley Park were shattered when I injured my ankle, and despite a positive recovery after a Couch-to-5K reset, I think I’ll have to settle for a walk.
I rebuilt this website and blogged about it. It’s built using Phoenix and Tailwind.
ChatGPT is all the rage at the moment, and I’ve been having fun with it myself.
I asked it to explain a database deadlock to me, and then asked it to reproduce
a deadlock using ActiveRecord
. It added some boilerplate code which was
missing some important details, but as I asked it to fill them in, it did a
pretty good job of doing so. Even down to me having to ask things like this:
Your code does not load dependencies before using them. It also creates 2 threads, but you never execute them so the program exits without any deadlocks occuring. Can you fix it?
It took some massaging but we got there eventually. Whilst I don’t think this will be replacing Stack Overflow any time soon, it’s very impressive technology and I’m excited to see how it evolves. You can do some wild stuff with it.
The Advent of Code challenges have been fun thus far. I usually start slowing down around day 7 or 8 when I can no longer fit them in to a 1-hour lunch break; a tradition that continues this year. I am currently working through the next couple of days on this very long flight though, so maybe I’ll be able to catch up.
I finished the last book in The First Law trilogy and immediately moved on to the next chronologically ordered book: Best Served Cold. It’s excellent so far.
Otherwise I’m continuing to enjoy Andor, and started Wednesday on Netflix. Only a couple of episodes in but enjoying that so far.
I tried to watch Black Adam but turned off about 20 minutes in.
This week brings a new season of Destiny 2. Whilst I won’t get the opportunity to play whilst I’m away, I’m excited to catch up with the new content (including a new Dungeon, which is always fun) when I get back.
]]>As such, this website — and its predecessors — have evolved over the last 18 or so years. From hand-crafted static HTML pages, to PHP, Perl CGI, modern JS frameworks and back again to static HTML via tools like Jekyll and Nanoc.
It stood to reason then that the next evolution of the website would be built with Phoenix, the Elixir web framework I’ve been using for the last few years for personal and work projects.
I love building apps with Phoenix. I remember the feeling of using Rails 10+ years ago — the sense of immense joy assembling a prototype in no more than and hour or two. That feeling for Rails has long dissipated, and a Phoenix has risen, so to speak (sorry).
As mentioned in recent weeknotes, I’ve been keeping a keen eye on the frameworks development, and I’ve been excited about the latest version, which brings built-in Tailwind, Verified Routes and more.
Below are some notes about the build.
It’s important to me that this website does not depend on a database. It adds an overhead I’m just not interested in maintaining. This meant I needed to import my markdown posts from Jekyll in to this new system. That’s easy enough, but given that Elixir code requires a compilation step, I was concerned that trying to build this in Phoenix might introduce some frustrating hurdles.
I was wrong, of course. The excellent NimblePublisher library solves this with ease. My markdown files are not only processed during the compilation stage and included as part of the application bundle, they’re watched for changes such that Phoenix will intelligently reload the page when I make a change.
Phoenix 1.7 introduces Tailwind support out of the box. CSS isn’t something I particularly enjoy writing, so Tailwind is a massive pleasure to use. Furthermore, maintaining well-organised CSS is something of a nightmare. Not really a problem for a tiny website like this, but I still have nightmares from larger projects. Embedding styles in the markup and using Phoenix components is just so nice.
The new design is based on the Spotlight template. I usually prefer designing stuff like this from scratch but I really like the template defaults. I’ve changed bits here and there to give it my personal touch, but the resemblance remains clear.
The website is deployed to Fly.io. I use Fly for several production apps and they’re stellar. Given the low traffic received here, I can’t imagine I’ll ever need anything larger than the free tier, which is nice.
I may move the app to my Linode VPS at some point in future — which is where I run a handful of Ruby and Go services. Just to give me a better look at what a “native” deployment looks like. For now though I’m more than happy to let Fly do the hard work.
The app is automatically deployed via a GitHub Workflow.
I used to think the idea of rebuilding my website every few years was silly. Really though, it’s the ideal way to sharpen skills and toy with new and exciting technology; and Phoenix is about as exciting as it comes right now.
]]>I’ve had some laptop woes this week. I grabbed my MacBook and headed to the local library to seek out some peace and quiet, only to find that the macOS beta had basically hosed the computer. Apple ID login was broken, the Settings app was completely messed up and I couldn’t connect to the library WiFi due to some weird DNS issues. I know it’s a beta and I should have known better, but this was pretty disappointing.
No more disappointing than the library being the noisiest place on earth though.
In happier news — and after resetting the MacBook — I used migration assistant to duplicate the contents of my desktop computer and it worked perfectly. Took a few hours, but everything is there, preferences included.
I learned about Fire Toolbox, a tool built to fix FireOS, the monstrosity of an operating system on Amazon’s Fire tablet range. We have one of these tablets for our daughter and it is absolutely riddled with adverts, including the screensaver. WTF? Fire Toolbox helped remove a lot of this garbage and let me install Google Play, which is marginally better — though don’t get me started on the despicable range of children’s apps that are filled with ads and sneaky in-app purchases (looking at you too, Apple).
The official Tailwind Components package went on sale for Black Friday and I decided to scoop it up for work. I’ve been a huge fan of Tailwind so far, and given its default integration with the latest version of the Phoenix framework, I could see myself using it more often in future. I’m working on a new version of this website using this tech and it’s been good fun so far.
I learned about Raycast — an excellent macOS app that’s similar to Alfred (or Spotlight). Not only has this app replaced Alfred, but the built-in window and clipboard management tools allowed me to replace two other apps. I don’t know why they’re not charging for some of these features but I have been incredibly impressed so far.
Advent of code is back! Suffice to say that I am a big fan of these coding challenges.
I recently learned that the Ruby Papertrail gem captures versions even when there are no object changes, which meant our work database was filled with a lot of useless data. Almost 100 million rows, in fact — which is more than 60% of the total table size. Removing them took some time, but it was incredibly cathartic.
This investigation began off the back of a significant outage, caused by some rogue database queries working away indefinitely (and being spawned/duplicated by web page reloads). Turns out we’ve managed to last 10+ years without having a fixed database timeout configured. Oops.
I moved my Mastodon account from Ruby.social to Hachyderm. I’d been browsing different servers for a while and wanted to settle on one that was more general. They’ve had some scaling woes over the last few days but have dealt with them very well, and very openly. Seems like a nice little community so far.
I’ve spent some time investigating new RSS/Read Later apps and tested the following:
The summary so far is that most of these apps are either over-engineered or they are missing some crucial features. ReadKit is probably my favourite at the moment but I’m pretty close to just sticking with NetNewsWire despite it not having Read Later capabilities.
I finished watching The Crown and can’t help but feel disappointed. The narrative exposition feels very forced and clunky. For a season filled with momentous events, the story is unnecessarily narrow and yet overtly theatrical. If there was a season to point at as fictional dramatisation, this one was it.
The new season of Mythic Quest isn’t much better, but at least it’s shorter.
I started watching The Peripheral and thought the pilot was very good. I want to finish up some other shows before I continue this, but I’ll definitely be returning.
I’ve almost finished Last Argument of Kings — the third book in the The First Law trilogy. It’s so good and I’m pleased that there’s plenty more to come.
I downloaded No Man’s Sky on Steam. I played it briefly on Xbox many months ago and enjoyed it, but it recently went on sale and I had to snap it up. Destiny has been slow recently, so I hope to dig in to this properly soon. From what I’ve heard, it came be a bit of a time consumer. I am nothing it not willing to throw time at a sci-fi RPG.
Posts I enjoyed:
]]>I mentioned last week that I’d found a new Twitter client that I like. The best thing about 3rd-party clients is that they circumvent the primary factory algorithm and opt for a more sane order-by-descending view. And yet here I am fully embracing the irony of using Twitter Web to suck in as much anarchy as possible during what many believe will be the final hours of its life. Like poking at a dying animal with a morbid curiosity.
In happier news, I restarted my running journey after being out for a few months with an ankle injury. An unhelpful visit to a Podiatrist not doing enough to dissuade me. I’ve gone back to basics with a couch-to-5K plan in the hope of running a smooth 5K again by Christmas. Gone are the plans to run Stanley Park seawall during my visit next month, sadly.
It’s starting to feel cold and my body has been sending me angry reminders of displeasure. The Raynaud’s kicking off again during my daily walks and the Sciatica starting to creep back in. I’ve started doing Yoga and some other exercises for 10-20 mins each morning to try and ease things up. It’s going pretty well so far.
Work has been busy this last week, with several performance problems popping up. Despite the stresses, debugging and fixing performance problems — especially database ones — is something I find very interesting and rewarding. I can’t say I’ve hated the change of pace.
I’m having a lot of fun with Phoenix 1.7 and have been tempted for the first time in a long while to write some technical blog posts on a handful of its new features and quirks.
Affinity — the Mac developer who build photo editing and publishing apps — have a huge sale on their new apps. This was an instant purchase for me. I love their apps, and I’d spend double the price to avoid an Adobe subscription.
I’m on the 2nd book in the First Law series and absolutely addicted. I’ll be on the third within the week I think.
I finished watching The Bear, which — despite its intentional anxiety-inducing script — I loved.
We started watching the latest season of The Crown and I’m ambivalent. It’s very well produced, but it’s just starting to feel a bit weird to watch. Elizabeth Debicki is absolutely incredible as Princess Diana, though.
Did I mention it’s really dark? Also, Christmas is in 5 weeks. Gah!
]]>I really wish I could disable boosts for all new followers by default.
The Elixir Phoenix dev team released version 1.7-rc this week. I’ve been playing with 1.7 for a while now and I’m incredibly excited to be able to start building apps against a stable version in the coming weeks.
Component-driven frameworks seem to be the future (and the present, I suppose), and this version really leans on that. And, importantly, it does so in a way that doesn’t force me to write—or even really care about—JavaScript.
Switched back to Things 3 after giving Apple Reminders a go for a few months. Turns out Apple is really just not great at software at the moment.
I replaced Tweetbot with Spring for Twitter and it’s better in almost every measurable way.
Purchased an Air Fryer (Ninja MAX AF160UK) and despite every review on YouTube suggesting it’s only good for cooking chips and fried chicken, it has been a fantastic addition to our kitchen.
I started re-reading The First Law series. As I’ve found with many excellent books and TV shows (The Wire, The Sopranos), sometimes you have to have a couple of goes at it before you really get it. The first book in the series, The Blade Itself, had a similar start.
I also read Richard Osman’s The Thursday Murder Club. I usually prefer my murder mysteries a little less light-hearted, but I thoroughly enjoyed this read.
If you have book recommendations (Fantasy, Thrillery, Mysteryy or otherwise), I would love to hear them.
I finished a handful of TV shows recently. House of the Dragon was probably the highlight. In particular, the incredible performances by Paddy Considine (King Viserys) and Emma D’Arcy (Princess Rhaenyra). I’ve also been loving The Bear—though it is incredibly stressful to watch.
I started Andor and so far it is superb. I had to stop watching it when I realised it was good enough to require my full attention and plan on restarting this week.
Took a break from Destiny and played the new Call of Duty. Started the campaign on the hardest difficulty and now I’m stuck on the infamous Alone mission. It’s fun, though I’m not sure I’ll be going anywhere near PvP.
DST ended a week ago and I’m not really over how dark it is. Like, all the time.
]]>I did visit the dentist though, which isn’t something I particularly enjoy doing. I need a couple of fillings, which wasn’t a great surprise. Otherwise things are pretty healthy. The dentist didn’t disappoint though and was sure to give me the classic grilling on not brushing enough in some places (has anyone ever not received this? I’m fucking bored of being told off every time I go).
We have unwelcome mice in our loft space. At least I assume they’re mice. Or a lone mouse. We can hear the tippy-tappy footsteps scurrying above our master bedroom – it’s particularly noisy right before bed. I like to think that Remy from Ratatouille is up there having fun. More realistically, it’s some feral bastard rubbing feces all over the place.
We’re having some loft boarding installed next week because we need some extra storage (not to block the mouse in, promise). We’ll see what that does to its movements before taking any further action.
I finally found a dry enough day to mown the lawn! That’s it. That’s the whole paragraph.
Unfortunately the lease car I placed on order last November has extended its build date from April to October/November (“or beyond”). How on earth a lease company can claim an estimated date that is inaccurate by 6+ months boggles the mind, but it appears the world-wide chip shortage brings unprecedented lead times.
Unfortunately we have to own a car because of where we live. We have gone the entire pandemic without a second car though, but that’s going to be coming to an end in the next 4-6 months. Especially now that Vicky is getting back to work. I feel like I’m 1-2 years too early for an electric car I don’t feel ashamed about getting in (yeah I know, but seriously, why do they all have to look like ass?). Shopping for cars is depressing, honestly. We’re remortgaging later this year too which makes financing a little more complicated. A first-world problem, but a problem nonetheless.
I deployed the Ruby 3 upgrade at Spark! Really pleased to finally get this out. The branch had a lot of changes (mostly related to the separation of positional and keyword arguments) but the deploy itself went off without a hitch. Next up: Ruby 3.1 and maybe even some pattern matching additions. I’d also like to upgrade to Rails 7 in the next few months (up from 6.1).
In addition to the Ruby upgrade, I created a new bin/dev
script to match what Rails does in version 7 (runs your dev server). This made me think about creating a unified interface for managing and communicating with the application as bin scripts. I subsequently added a bin/deploy
script that allows us to customise our capistrano deploy and I plan on adding things like bin/console
and improving our bin/setup
– this really helps reduce our documentation burden, especially when these scripts are interactive.
I created a new Asana Project for managing our technical debt! This was satisfying, and it was well received across the team. I’ll be working on adding descriptions to these tasks and creating a priority order over the coming weeks. It feels good to get moving on this.
The folks over at Fly.io (and in particular, Chris McCord – the creator of the Phoenix Web Framework) have been working on a full-stack Phoenix reference app called LiveBeats. This has already proved a useful resource for approaching basic Phoenix problems, and also demonstrates the cool factor of multi-region Fly applications. Highly recommend taking a look. I even created a tiny PR for them.
I’ve also deployed my first “proper” Fly application this week; a wedding website for a friend (I’m the best man at their wedding). It didn’t quite go without a hitch, but when things work as smoothly as intended, it really feels like magic.
Rugby is back in the form of the Six Nations. The kiddo (who loves her weekly visits to the Rugby Tots club) noticed similarities and I think she’s already hooked.
We finished watching Ozark which was good, and also started and finished Maid, which is probably one of the best Netflix series we’ve watched – it is fantastic.
Also started Station Eleven (good and weird) and Pam & Tommy (bad and weird).
Sony bought Bungie. Not sure how to feel about this one; it’s one of my favourite games and I really hope they stick to their promise of having creative freedom. Especially for the Next Big Thing they launch after Destiny 2 concludes.
]]>After a busy start to the year, this week was very quiet. Quiet enough that I was dreading writing these notes. Still, I never promised myself I’d write a lot. Just that I’d write. So here goes.
My wife did a London Marathon Walk to raise money for JDRF in support of her niece. She smashed it and I’m super proud of her.
I went on a mission to find good instant noodles. I bought some Bachelors Super Noodles, Itsu Rice Noodles and Kabuto Noodles. The Itsu are probably the best, but none of them are great. This isn’t exactly shocking given that they’re pre-packaged and ready to eat in a few minutes, but I am speaking on the instant-noodle grade and not on noodle-based meals in general.
I found that rejecting the instructions and cooking them all on the hob with butter made a reasonable difference – cholesterol be damned.
We checked out a new burger joint that opened close to our home. The food was very good (which isn’t too difficult given the access to actual good quality ingredients where we live). We tried Deliveroo for the first time though, and the experience was pretty bad. Our initial delivery window extended every 5 or so minutes for what felt like indefinitely (it was around 1 hour). Food delivery services aren’t very popular where we live and I can’t see us using Deliveroo again anytime soon.
I learned:
tabular-nums
value of the font-variant-numeric
CSS property and wish I had known about this many years ago.<br> <button>
HTML elements have some interesting attributes that you can use to customize (and even override) form behaviour. For example, the formaction
attribute. We watched the final season of After Life. We thought it wasn’t as good as previous seasons, but not bad either. And some tear-jerker moments in there for sure.
I started and finished Yellowjackets and really enjoyed it. This sort of weird psychological drama is right up my alley. Its slow in places but didn’t disappoint.
We also started watching Ozark (good, though not as good as last season if only because of the outstanding performance Tom Pelphrey gave in the episode “Fire Pink”).
I’m currently fighting with the atrocious Music app on macOS and wondering why it’s so difficult for multi-billion (or trillion) dollar companies to make a functioning music app. I’m growing so incredibly tired of dealing with wonky software and I long to bin it all off and work on a farm in the middle of nowhere.
]]>I have spent the last seven days visiting my work mates in Vancouver. I’ve had a blast, and whilst I could probably do another week, I’m looking forward to getting home to the family.
Besides work, my visit was largely spent eating good food and drinking good booze. All in very good company. It almost felt a challenge to find a type of cuisine that I do not enjoy (spoiler: the search continues).
Here’s a few nice places I ate: Gotham Steakhouse, MeeT, Babylon, Pigot’s, Zarak, Merci Beaucoup. If I had to recommend only one, it’d be Zarak. The Afghan food is incredible.
Didn’t do much walking besides the usual Stanley Park sea wall. That had to be the only day I didn’t return to the hotel smelling of weed.
Canada — or at least Vancouver specifically — are dealing with the pandemic very differently from us in the U.K. I couldn’t eat or drink in a restaurant or cafe without showing proof of vaccination. And people actually wear masks. Like, everywhere.
The week was productive. I’ve been working pretty relentlessly since before the Christmas break though, and would have preferred a more relaxed week. Sometimes I felt like I pretty much had my head down all day, not really being able to make the most of being in-person. Though I probably more than made up for it during the evenings.
I merged and deployed our first ViewComponent, and had some good talks about ways we can utilise this system in future, including a more component focused system that extends in to the design team.
I learned about the ActiveRecord missing
method, which uses an outer left join that returns a relation with missing associations. <br>So User.joins(:project).where(projects: { id: nil })
becomes User.missing(:project)
– this is especially nice when you want to combine an or
on the join table and bump in to the Relation passed to #or must be structurally compatible
issue.
I started writing a Rust command line tool that generates (potentially very large) fake CSV files to use as imports in to our CRM. We already have this in Ruby but it’s very slow and I wanted to keep the Rust momentum. I’m wrapping my head around some of the more complex syntax but there’s still plenty of frowning involved. It’s going well otherwise and I suspect I’ll keep poking at it as and when I have time.
Media corner: I watched and enjoyed Moneyball and Richard Jewell, and continued reading Billy Summers. Note the new link to The StoryGraph. Thanks to Kaitlyn (the only person who reads these posts) for that.
I was shocked to learn that Microsoft Gaming had acquired Activision Blizzard. I know they’ve had a lot of HR issues recently, but Microsoft? Talk about antitrust, anticompetitive monopolies. I’m a fan of the Xbox ecosystem, but this doesn’t feel like a good long-term deal for gamers.
As I write this on the way home, I realise I return to a bunch of tedious life admin. Dentist and optician appointments, deep-cleaning and runs to the waste centre. Such is life.
]]>My daughter returned to nursery for the first time in almost six weeks and absolutely loved it. Very thankful that she enjoys it so much, and that they send us daily pictures of her activities. She also joined Rugby Tots and enjoyed it equally. We’re hoping to get her in to more hobbies over the next few months.
I finally got my wedding ring back from the jewellers after sending it off for resizing. When it returned the first time, it didn’t look at all like my ring. Turns out they’d removed (or perhaps not re-applied) the matt finish. I’m not sure how that goes unnoticed when they’d taken a very clear photograph of it before sending it off. It’s back now though after many months of not being able to wear it (after losing weight, I think).
I’m writing this post at 30 thousand feet above Greenland. Currently en route to Vancouver for the second time in 4 months. Looking forward to a week with the team. And eating lots and lots of poutine.
My Sony WH-1000XM3 headphones are holding up to the test of a screaming baby and I’m feeling for her and her parents, who are trying their absolute best to calm her down.
A few weeks ago whilst listening to the Cortex Podcast, I learned that Myke or Grey had an additional iPhone home screen for travel purposes and thought that might be a nice idea. Turns out it was! Here’s the Home Screen I created:
The Flighty widget (which I cleared to take the screenshot) contains information about the flight, including boarding times and gate details. Also whilst I generally detest all things Google, it supports offline maps which Apple Maps does not. I activated this screen a few days before travel and will turn it off when I’m back home.
I have — from time to time — an inexplicable desire to write code in low-level languages, and spent some time this week digging back in to Rust. I’ve used it for small things here and there, but I find some of the syntax a little nauseating and never really spent time learning things like Lifetime Annotations and understanding the ownership model. I worked through the Rustlings exercises and then solved a couple of basic Advent of Code challenges. It all went a lot smoother than expected, which is perhaps a sign that my brain is retaining some of this information, even if it feels a little overwhelming at times. I’d like to try and keep on top of it and have a couple of ideas for little side projects.
I re-learned about the Boop Mac App. Re-learned because I found out I already had it installed. One of those apps I need to be reminded to use. Recommended especially if you’re a weirdo like me and have a handful of custom scripts for doing this and that.
I finally found time to integrate a ViewComponent in to the work Rails app. I opened a PR for discussion and so far the responses are positive. I think we’re going to be adopting this and replacing some of our controller tests.
I re-watched this excellent Elixir/Erlang/BEAM video talk from Saša Jurić. It’s one of my favourite video talks and I highly recommend it, even if you don’t know Erlang/Elixir.
Media corner: After our Lord of the Rings trilogy watch last week, I watched the first Hobbit movie. It was good, if not quite as epic as the original movies.
I also watched The French Dispatch (artsy and beautiful, but a bit hollow otherwise) and The Unforgivable (meh).
Started reading Billy Summers by Stephen King. I find Stephen King books really easy to pick up and put down, which is exactly what I’m looking for at the moment. I wonder if there’s a better book website to link against; Goodreads seems a bit gross?
]]>In reality though, what I didn’t do was publish enough. In an effort to address this, I’m going to have a go at writing weeknotes.
I use the daily note feature Craft provides as a sort-of journal, so I hope to have enough content to create weekly-ish posts like this.
I published a post about pattern matching in Ruby 3.1. A lot of this content is taken straight from the Ruby documentation, but actually sitting down and writing everything out myself really helped me understand these new features and helps me decide whether or not I’ll be using them in the near future or not.
At Spark, we’re in the process of upgrading to Ruby 3, so we should have access to some of these features soon. There’s quite a lot of awkward looking syntax so I’m not sure if we’ll rush to adopt any of them, but I look forward to discussing with the team.
New year daily workouts have started fairly strong. I haven’t been overdoing it, but moving a lot more has been nice. Actually doing the exercises prescribed by my physio has already improved back movement. Vicky is training for a sponsored marathon walk to support JDRF, so we’re both getting out a bit more.
We watched the original Lord of the Rings trilogy. I’d seen the first movie and some of the second back when it released, and just never gotten around to finishing them. We committed to it this time, and did not regret it. They hold up incredibly well for 20-year old movies.
I started reading A Common-Sense Guide to Data Structures and Algorithms. I find many Computer Science topics are just not my forte, and I pick up books like this from time to time to try and identify non-academia approaches to understanding complex algorithms. The book is good, but I was pleased to find that it didn’t really teach me anything new. I’d recommend it, though.
I watched a video titled Make Your WiFi Faster! and it was very good, despite what you might think about the title.
We’re still playing It Takes Two and cannot recommend it enough. It’s a lot longer than I expected (or maybe we’re just slow) and it’s a lot of fun. We think we’ve almost completed it.
I also learned about Zachtronics games. I haven’t installed any yet but I’ve heard good things and hope to give them a try soon. I have been recommended Opus Magnum and Infinifactory.
We both got our COVID booster with some very minor side-effects.
Nx (a Machine Learning library written in Elixir) was released at version 0.1 which I found really interesting, despite not really having any experience with ML and numerical computing. It was a fun Wikipedia dive though!
I learned about the Object#presence_in Rails method which I could see cleaning up some of our controller param filtering code.
]]>
There are two types of standalone matching expressions. One using <expression> => <pattern>
and the other uses <expression> in <pattern>
:
# Assign `first` to "foo" and `last` to "qux". The star (*) represents "rest" as in, the rest of the elements we don't care about
%w(foo bar baz qux) => [first, *, last]
# We can actually assign the "rest" to a variable:
%w(foo bar baz qux) => [first, *rest, last]
rest #=> ["bar", "baz"]
# Raises a NoMatchingPatternError because the lengths differ:
%w(foo bar baz qux) => [first, last]
The in
is similar but returns true
if a match is found:
%w(foo bar baz qux) in [first, *, "qux"]
#=> true
first #=> "foo"
%w(foo bar baz qux) in [first, "qux"]
#=> false
We can also include optional classes to match against:
%w(foo bar) => [String => first, String => last]
%w(foo bar) in [String, String] #=> true
%w(foo bar) in [String, Integer] #=> false
# Raises a NoMatchingPatternError:
%w(foo bar) => [String => first, Integer => last]
We can also match against a Hash pattern:
{foo: "bar"} => {foo:}
foo #=> "bar"
{foo: "bar"} => {foo: renamed}
renamed #=> "bar"
{foo:"bar"} in {foo:}
#=> true
{foo:"bar"} in {bar:}
#=> false
# The braces are optional:
{foo:"bar"} => foo:
foo #=> "bar"
# with explicit class matching:
{foo:"bar"} => {foo:String => renamed}
renamed #=> "bar"
# nested values:
{this: {is: {nested: "value"}}} => {this: {is: {nested:}}}
nested #=> "value"
One important thing to mention is that Array matches work against the entire Array. Whereas you can match against a Hash subset:
{foo: "bar", baz: "qux"} => {baz:} # ignore the :foo key
baz => "qux"
We can also mix patterns using the |
operator. Let’s say we want to check if a Hash value is an Integer or a Float:
>> {foo: 1.2} in {foo:Integer | Float}
=> true
>> {foo: "bar"} in {foo:Integer | Float}
=> false
Now we’ve learned the new syntax, let’s extend this by taking advantage of how the pattern matching syntax works with case
statements.
Let’s write up a real program this time. This script lists all GitHub repos for a given username, and then asks the user to enter the name of the repo they want more information about. Let’s start by building up to the name selection:
#!/usr/bin/env ruby
require "net/http"
require "json"
username = "leejarvis"
endpoint = URI("https://api.github.com/users/#{username}/repos")
json = Net::HTTP.get_response(endpoint).body
repos = JSON.parse(json, symbolize_names: true)
# ignore archived and disabled repos
selectable_repos = repos.select { _1 in archived: false, disabled: false }
puts "Enter a repo name to get more information: "
selectable_repos.each { |repo| puts repo[:name] }
repo_name = gets.strip
selectable_repos.each do |repo|
case repo
in name: repo_name
p repo
else
# ignore for now
end
end
As you can see here, we’re already taking advantage of the new in
syntax by ignoring archived and disabled repos. We could write this as .reject { |repo| repo[:archived] || repo[:disabled] }
but let’s not be a party pooper, eh?
Now, when we enter a repo name we’ll dump the data for that repo to the console. Here we go:
~$ Enter a repo name to get more information:
adventofcode
#=> {:id=>113033185, :name=>"adventofcode", …}
#=> {:id=>1114377, :name=>"albeano", …}
#=> lots more repos
~$ Enter a repo name to get more information:
chronic
#=> {:id=>113033185, :name=>"adventofcode", …}
#=> {:id=>1114377, :name=>"albeano", …}
~$ Enter a repo name to get more information:
zomgroflbbq
#=> {:id=>113033185, :name=>"adventofcode", …}
#=> {:id=>1114377, :name=>"albeano", …}
Uh, ok. Looks like this isn’t quite working. Every one of our repos is being dumped to the console, it appears to completely ignore our input.
Enter Variable pinning.
In our case
expression above, we’re trying to match against our entered repo_name
— however, as we’ve seen in previous examples, the match in name: repo_name
would assign name
to repo_name
rather than match against it.
In order to match against our entered value, we need to pin the repo_name
variable:
case repo
in name: ^repo_name
p repo
else
# ignore for now
end
Now our program works as expected. Let’s use our deconstructing syntax to pull some information from the repo:
repo => {
html_url: url,
default_branch: branch,
updated_at:,
watchers_count: Integer => watching
}
puts "Repo #{repo_name} is available at: #{url}. " \
"The default branch is #{branch} and it was " \
"last updated on #{updated_at} and has #{watching} watchers"
Looking good so far.
We wouldn’t want to release our script to the world without a sprinkle of Object Oriented programming though, would we? Let’s create a class for our repo and update our list of selectable repos:
class Repo
attr_reader :name, :attributes
def initialize(attributes)
@attributes = attributes
attributes => {
name: name,
html_url: url,
default_branch: branch,
updated_at: updated_at,
watchers_count: Integer => watching
}
@name, @url, @branch, @updated_at, @watching = name, url, branch, updated_at, watching
end
def to_s
"Repo #{name} is available at: #{@url}. " \
"The default branch is #{@branch}, it was " \
"last updated on #{@updated_at} and has #{@watching} watchers"
end
end
selectable_repos = selectable_repos.map { Repo.new(_1) }
selectable_repos.each do |repo|
case repo
in name: ^repo_name
puts repo
else
# ignore for now
end
end
Looking good. Unfortunately pattern matching doesn’t work with instance variables (maybe one day?), so we have to assign to locals first in our initialize
method.
Now when we run our program again it.. well, it doesn’t work. This is because our in name: ^repo_name
code is still expecting repo
to be a Hash
.
Thankfully, the Ruby team have thought about this.
So far we’ve discussed matching against Array and Hash. But how do we match against our new Repo
objects? Well, we just need to add two new methods: deconstruct
and deconstruct_keys
:
def deconstruct
@attributes.keys
end
def deconstruct_keys(keys)
@attributes.slice(*keys)
end
We use deconstruct
for Array and find patterns, and deconstruct_keys
for Hash deconstruction. In our in name: ^repo_name
match, the keys passed to deconstruct_keys
will be [:name]
— this allows us to return only the relevant deconstructed values from the attributes hash.
Let’s play with this a bit in IRB with information we have about inline matching expressions:
>> repo => {name:}
=> nil
>> name
=> "adventofcode"
>> [:name] in repo
=> true
Now when we run our program again, everything is working fine. We can also include class names as part of our pattern which would restrict the matching a little (right now we’ll match against any object that implements a matching deconstruct_keys
):
case repo
in Repo(name: ^repo_name)
puts repo
else
# ignore for now
end
At first glance, a lot of this syntax just left me squinting. I’m really not sure how I’ll feel seeing some of these patterns used in production code because it seems quite easy to abuse.
That said, it’s nice to see the Ruby language evolving and I really like the idea of being able to use pattern matching to avoid long conditional statements especially when consuming JSON:
data = {
books: [
{
name: "To Kill a Mockingbird",
meta: {
tags: [
{ name: "Novel" },
{ name: "Thriller" }
]
}
},
{
name: "The Lord of the Rings",
meta: {
tags: [
{ name: "Novel" },
{ name: "Fantasy" }
]
}
}
]
}
fantasy1 = data[:books].select do |book|
book[:meta] && book[:meta][:tags] && book[:meta][:tags].any? { |tag| tag[:name] == "Fantasy" }
end
fantasy2 = data[:books].select do |book|
book in { meta: { tags: [*, { name: "Fantasy" }, *] } }
end
]]>Watching my daughter grow in to a real human being has—naturally—been the highlight of my year. She’s already smart, sassy and all around wonderful. She gets that from her mother. We don’t share photos of her on social media, so you’ll just have to believe me when I say she is also cute as a button.
In July, Spark (the company I co-founded and currently work at as Tech Lead) closed Series A financing. This new investment allows us to scale up the dev team and start to implement sensible plans for dealing with technical debt, something we’ve been struggling with recently.
I finally managed to get out and visit the team in October. I dropped in as a surprise during a “dev appreciation day”, where we were celebrating 10-years since the first commit of the Spark code base:
commit 696c07f5ddd5b0ed5604497941a987dfd5f3f0e8
Author: Lee Jarvis <lee@redacted>
Date: Sun Oct 16 18:53:36 2011 -0700
initial commit
The last time I visited Vancouver was during the months that the company was founded, meaning this was the first time meeting many of the 30+ team I have been working with for years. It was wonderful being able to surprise them.
And no, my initial project commit messages are no more creative than they were back then.
I predominantly write Ruby (and more specifically, Rails) code at Spark. My side projects though—of which there are 2-4 at any given time—have so far this year consisted entirely of Elixir (and more specifically, the Rails equivalent Phoenix) applications.
Ruby means a lot to me, but working with Rails has become tedious and I don’t really know how much longer I have in me. Writing Elixir and Phoenix code on the side has really allowed me to keep focused at work and avoid burnout.
I expect this pattern to continue through 2022 and beyond, and I suspect further down the line I will likely look to be working on more Elixir code and less Ruby code at my job.
I have read an embarrassing number of books this year. I might even be in single digits. I have though managed to keep well up-to-date with my RSS feed which mostly consists of blog posts from independent tech writers and developers.
I continued to work through my Letterboxd watch list. To pick 3 perhaps lesser-known movies in my top-10 of 2021:
I picked up a new Xbox and have been enjoying the new Halo Infinite, as well as Forza and a few other games. Right now my wife and I are playing It Takes Two, which is very fun.
Not much to report here. Besides an especially gluttonous December, I have managed to keep fairly healthy. When I can feel myself slipping (especially with eating habits), I really force myself in to an Intermittent fasting schedule. It works well for me and really ties in to my highly-productive food-less mornings.
I have a bunch of gym equipment I keep in my home office and managed to work out 4-5 days each week for several months. It usually slips when I become unwell for one reason or another.
I caught COVID in mid-December, after having managed to avoid it for 2 years. I had a couple of rough days but the worst part was having to try and isolate from my wife and daughter in my tiny office for 10 days. Do not recommend.
I don’t do the whole New Year’s Resolution thing. Instead, I try to have some sort of Yearly Theme that I try to follow and apply to everything I do.
That said, there’s a few things I want to do in 2022:
I worked with Martin at Loco2 for 5ish years. He embodied the stereotypically punctual, well-organised German. He was an excellent software developer.
We’d speak only briefly and when necessary. That’s how we both liked to operate. It was only after becoming his manager that I saw a softer, more emotional Martin. Monthly one-to-one video calls would circumvent his defences and expose a real, fallible human. Not a relentless coding machine in pursuit of perfection (though he was a bit of that, too).
Martin didn’t mince his words. I recall a meeting between ourselves and a Product Manager at Loco2 where he was asked whether he’d complete a project given a disturbingly tight deadline. He’d smirk, “that depends, do you want it to be shit?”.
His appetite for no-nonsense, direct and efficient communication made working with (and certainly managing) Martin a breeze. On the odd occasion I might need to approach a difficult subject, I could feel him urging me toward a point, no matter how uncomfortable it might be.
Hearing of Martin’s sudden death was an excruciating punch to the gut. I felt paralyzed for the rest of the day. I’ve thought about him and the unceremonious here-now-not nature of his death almost daily since. And today especially, I really miss Martin.
]]>I haven’t been truly happy with an RSS Reader since Google shut down Reader. And I’ve tried them all.
That is until I heard NetNewsWire was back in town. The original app was released almost 17 years ago now, and after exchanging hands a few times since then, it’s now back home with its original creator Brent Simmons.
The new application is blazingly fast and very simple. It doesn’t try to do anything fancy or over-complicated—an increasingly rare feature of modern software. It’s also Open Source!
The hybrid application supports iOS, iPadOS and macOS. Here’s how it looks on my Mac:
There are a couple of missing features you might expect from a modern RSS Reader. It doesn’t have any first-party sync support (Feedbin works, but I don’t use it). Right now I’m exporting my subscriptions to iCloud and then importing them in on my phone.
I’d also like to see support for the ability to create smart-feeds with custom criteria for filtering articles. They have a very solid base though and the app is improving constantly.
I’ve recently been finding it difficult to avoid sinking time into social media (where I often go for news and interesting articles), and it seems increasingly difficult to know who and what to trust. NetNewsWire provides a nice way for me to filter out the noise and make sure I see content from creators I care about. I highly recommend it.
]]>America might be in the spotlight, but make no mistake; systemic racism exists all across the globe and we in the U.K. are no less guilty of it.
I’ve taken this as an opportunity to listen and learn. My first stop is the book So You Want to Talk About Race by Ijeoma Oluo which I’m half way through so far (I recommend it).
Here’s a few other books I’ve seen recommended by others:
I hope we can get better at Talking About Race. Because Black Lives Matter.
]]>I suppose the words I’ve been writing just feel so incredibly unimportant given the various global crises.
I’ve had a very busy few work months. It feels weird to say that, having witnessed close friends and family lose work or be placed on furlough.
I’ve been back at Spark since leaving Loco2 last year. It’s been seven years since I worked full-time on the Spark code base, but things fell back into place nicely after a few weeks.
Myself and family were incredibly unwell in March; right before the COVID-19 pandemic took full force. I’d guess we’re on our 10th or 11th week of pseudo-isolation now.
As an introvert with a busy remote-working job, I haven’t struggled too much to adapt to our new restricted lifestyle. In many ways it’s been positive for my mental health. That’s a tough thing to admit when there are millions in dire straits. A certain privilege check.
The quarantine has allowed me more time with my family. More time reading, cooking, learning, listening. More time refining my relationships. More time appreciating.
I suppose I’m an anomaly. I see a calendar event for a pleasant gathering and it weighs as heavy as a day of meetings. The view of an empty schedule does a lot to assuage pressure and inspire freedom. Quite an oxymoron in the present climate.
I see them though. The sacrifices being made. A world of sacrifices. Front-line workers with their lives, business owners with their livelihoods, humankind with its liberty.
Such sacrifices bring out the best and worst in people. There’s no shortage of finger pointing at lockdown snubbers, or complaints to large corporations and governments for their lacklustre responses.
Amongst that, windows are decorated in colourful hand-crafted appreciation drawings, organisations ramp up refuge and council to victims of domestic abuse, mental-health guidance advances from the middle-pages.
We’re adapting; preparing ourselves for a new normal. A necessary change. Things won’t go back to the way they were. Not in my lifetime.
But we’re adapting, because that’s what we do.
]]>
I want to create an application that supports a number of resources under a project
scoping. I want every project to support the following:
Since all of these resources belong to the project scope, we need to create a sensible URL structure. Something like this should suffice:
/projects/{project_id}/users
/projects/{project_id}/users/{user_id}
/projects/{project_id}/posts
/projects/{project_id}/posts/{post_id}/comments
/projects/{project_id}/events
/projects/{project_id}/events/{event_id}
I’ve omitted some routes, but hopefully you get the point. Let’s jump in.
Firstly we need to update our router to support our project scoping. Fortunately Phoenix supports Scoped Routes out of the box, so a straightforward change to our router will add this for us:
scope "/projects/:project_id" do
get "/", ProjectController, :show
# resources here
end
Now if we run mix phx.routes
we’ll see our project scoping:
project_path GET /projects/:project_id AppWeb.ProjectController :show
Next we’ll need to add the resources inside of our scoping to complete our URL structure:
scope "/projects/:project_id" do
get "/", ProjectController, :show
resources "/users", UserController
resources "/events", EventController
resources "/posts", PostController do
resources "/comments", CommentController
end
end
And mix phx.routes
:
project_path GET /projects/:project_id AppWeb.ProjectController :show
user_path GET /projects/:project_id/users AppWeb.UserController :index
user_path GET /projects/:project_id/users/:id/edit AppWeb.UserController :edit
user_path GET /projects/:project_id/users/new AppWeb.UserController :new
user_path GET /projects/:project_id/users/:id AppWeb.UserController :show
user_path POST /projects/:project_id/users AppWeb.UserController :create
user_path PATCH /projects/:project_id/users/:id AppWeb.UserController :update
PUT /projects/:project_id/users/:id AppWeb.UserController :update
user_path DELETE /projects/:project_id/users/:id AppWeb.UserController :delete
event_path GET /projects/:project_id/events AppWeb.EventController :index
event_path GET /projects/:project_id/events/:id/edit AppWeb.EventController :edit
event_path GET /projects/:project_id/events/new AppWeb.EventController :new
event_path GET /projects/:project_id/events/:id AppWeb.EventController :show
event_path POST /projects/:project_id/events AppWeb.EventController :create
event_path PATCH /projects/:project_id/events/:id AppWeb.EventController :update
PUT /projects/:project_id/events/:id AppWeb.EventController :update
event_path DELETE /projects/:project_id/events/:id AppWeb.EventController :delete
post_path GET /projects/:project_id/posts AppWeb.PostController :index
post_path GET /projects/:project_id/posts/:id/edit AppWeb.PostController :edit
post_path GET /projects/:project_id/posts/new AppWeb.PostController :new
post_path GET /projects/:project_id/posts/:id AppWeb.PostController :show
post_path POST /projects/:project_id/posts AppWeb.PostController :create
post_path PATCH /projects/:project_id/posts/:id AppWeb.PostController :update
PUT /projects/:project_id/posts/:id AppWeb.PostController :update
post_path DELETE /projects/:project_id/posts/:id AppWeb.PostController :delete
post_comment_path GET /projects/:project_id/posts/:post_id/comments AppWeb.CommentController :index
post_comment_path GET /projects/:project_id/posts/:post_id/comments/:id/edit AppWeb.CommentController :edit
post_comment_path GET /projects/:project_id/posts/:post_id/comments/new AppWeb.CommentController :new
post_comment_path GET /projects/:project_id/posts/:post_id/comments/:id AppWeb.CommentController :show
post_comment_path POST /projects/:project_id/posts/:post_id/comments AppWeb.CommentController :create
post_comment_path PATCH /projects/:project_id/posts/:post_id/comments/:id AppWeb.CommentController :update
PUT /projects/:project_id/posts/:post_id/comments/:id AppWeb.CommentController :update
post_comment_path DELETE /projects/:project_id/posts/:post_id/comments/:id AppWeb.CommentController :delete
Excellent, this is exactly what we want. Let’s take a peek at our comments controller:
defmodule AppWeb.CommentController do
use AppWeb, :controller
def index(conn, %{"project_id" => project_id, "post_id" => post_id}) do
render(conn, "index.html", comments: list_comments(project_id, post_id))
end
def show(conn, %{"project_id" => project_id, "post_id" => post_id, "id" => comment_id}) do
render(conn, "show.html", comment: get_comment!(project_id, post_id, comment_id))
end
end
OK, it’s not bad, but one thing is very clear: every one of our controller actions are going to need to handle this project_id
parameter.
I come from a Rails background, and the canonical way to solve this in Rails is to add a before_action
:
class CommentController
before_action :set_project
# actions
def set_project
@project = Project.find(params[:project_id])
end
end
We can’t do this in Phoenix.
However, Phoenix supports Plug. Plug is an Elixir library that implements a specification for composable modules to be used in web applications. Phoenix uses Plug heavily under the hood (in fact, Phoenix controllers themselves implement the Plug behaviour).
Our Phoenix controllers expose a helpful function named plug
that allows us to implement behaviour similar to our before_action
:
defmodule AppWeb.CommentController do
use AppWeb, :controller
plug :put_project
def show(conn, %{"post_id" => post_id, "id" => comment_id}) do
%{current_project: project} = conn.assigns
render(conn, "show.html", comment: get_comment!(project, post_id, comment_id))
end
defp put_project(conn, _opts) do
current_project = fetch_current_project(conn.params["project_id"])
assign(conn, :current_project, current_project)
end
end
This code is pretty straightforward. put_project/2
is called before our action is executed, and we put the current project into conn.assigns
(a storage mechanism provided to us by Plug). Now we can use this function in all of our project-scoped controllers to remove the %{"project_id" => project_id}
matches.
This is nice, but we can go one step further and move this functionality into our router using pipelines.
Phoenix supports something called Pipelines. Pipelines allow us to attach a series of plugs to a scope. Let’s add a new pipeline for our project scoping:
pipeline :project do
# plug :authenticate_user
plug AppWeb.CurrentProject
end
scope "/projects/:project_id" do
pipe_through :project
get "/", ProjectController, :show
# ...
end
And define AppWeb.CurrentProject
like so:
defmodule AppWeb.CurrentProject do
@moduledoc """
This module implements functionality to fetch the current project
from the URL and add it to Conn.assigns, making it available to any
controller within the project scope.
"""
@behaviour Plug
import Plug.Conn
import Phoenix.Controller
@assigns_key :current_project
def init(opts), do: opts
def call(%Plug.Conn{params: %{"project_id" => id}} = conn, _opts) do
assign(conn, @assigns_key, get_project!(id))
end
defp get_project!(id) do
# fetch project from database
end
end
There we have it. All of our project-scoped controllers will be able to fetch the current project from conn.assigns
without having to add any controller-specific code.
By default, Phoenix wraps all of our views in the layout defined in templates/layout/app.html.eex
. This layout template contains an assign named @inner_content
which, as you’d expect, returns the content of our view templates.
I want users to see a familiar project-based UI in all of our scoped pages. I don’t want to have to create a new app layout because it’s going to contain a lot of duplicate code. Similarly, I don’t want to have to add a bunch of conditional statements inside of app.html.eex
that add content based on whether we’re inside the project-scoping.
What I really want is a nested layout: app.html.eex > project.html.eex > our view template
.
Thankfully Phoenix has our backs on this and provides Phoenix.Controller.put_root_layout/2
Let’s tweak our AppWeb.CurrentProject
plug:
def call(%Plug.Conn{params: %{"project_id" => id}} = conn, _opts) do
conn
|> put_layout({AppWeb.ProjectView, "layout.html"})
|> put_root_layout({AppWeb.LayoutView, "app.html"})
|> assign(@assigns_key, get_project!(id))
end
defp get_project!(id)
# fetch project from database
end
And create a new file in templates/project/layout.html.eex
with the following content:
<h1><%= @current_project.name %></h1>
<div class="project-content">
<%= @inner_content %>
</div>
And that’s it. All of our project-scopes view templates will be rendered inside of this layout.
Have any suggestions for improving this post? Let me know.
]]>None are worse though than my bluetooth devices disconnecting 3-4 times each day, leaving me without mouse, keyboard and headphone connectivity until it rights itself after one or two minutes.
I spent the first few days Googling and considered rolling back to an earlier version of macOS. Eventually I figured it would probably just fix itself soon. Nope, not yet. This dropping connection has instead become part of my environment; it’ll disconnect, I’ll stare at an unresponsive display for a minute and then continue with my day.
Disconnect, wait, continue, repeat.
I use a Logitech MX Master 2S alongside an Apple Magic Keyboard. The keyboard problem is fixed fairly easily; I can plug it in. I prefer a wireless keyboard for aesthetics, but I care more about not losing my mind so this was an easy decision to make.
Unlike the Apple Magic Mouse (which requires stabbing in the underbelly with a USB cable in order to provide power—a breathtaking design choice), the MX Master sensibly sticks a USB port right on its bow. I don’t want to use a wired mouse though. The drag is annoying, the extra moving cables are annoying and dammit why am I even having to consider this?
Those of you accustomed to wireless Logitech peripherals will know about the little USB dongle they pack into the box. You know, the third-party wireless receiver that goes straight into the bin after unpacking? They call it a Unifying USB Receiver. It looks like this:
Well, I recently learned that people actually use (and like!) this thing. Recent episodes of Accidental Tech Podcast and videos from MKBHD showed clear preference for using this over a bluetooth connection.
I couldn’t fathom why, so I tried it.
Low and behold, the IR connection to this dongle is significantly better that my existing bluetooth connection. Whilst I’d noticed input lag and jankiness from time to time, I had never considered the bluetooth connection being an issue (it’s a pretty tried and tested technology, after all).
It’s been a week and the USB receiver seems to be as solid as a wired connection. Not only that, but the wireless bluetooth connection to my headphones has not dropped once since.
So thanks, Logitech. I’m happy you’re occupying one of my few USB ports.
]]>I’m writing this story—my story—as a tale of caution, aimed directly at future me. Maybe you’ll find something here for you too.
I thought I’d understood burnout. I’d seen it before; felt it before. I considered myself suitably cautioned to the impact it could have on my life. I wouldn’t be burnt again. Not me.
The early stages of burnout are rarely obvious. I had a busy and important job. I had 10+ direct reports and a lot to prove. The business was scaling up quickly and trying to handle a new management structure. Feeling a bit overwhelmed or anxious just comes with the territory. Part of the job, some might say.
And yeah maybe my patience slipped a little at times. And perhaps I made one or two rash decisions. I’m only human.
And what if you disappoint one person? You’re helping ten others. 9/10 seems like a pretty good success rate to me. You’ll make it up to them later, anyway.
And don’t feel bad if you’re not excited for the seventh call of the day. It’s a 1:1 with someone you’ve been working with for years. How important can it be?
The symptoms of burnout differ from person to person, but there’s usually a common theme: you feel overwhelmed and you’re less effective at your job.
When I feel like this, a day with an empty todo list becomes just as stressful as a day full of tasks. I become completely overwhelmed by nothingness.
The small things matter. Whilst it’s perfectly normal—common in fact—to have an off-day, it’s important to take a step back from time to time and analyse your work life from top to bottom.
Time for some hard honesty. This burnout was directly caused by overloading myself with responsibilities whilst working as VP of Engineering at Loco2. I realised it long before leaving, and failed to correct the damage. This was one of five reasons I decided to leave (perhaps I’ll talk about the others in future).
I’ve never had a problem with saying no to people if I believe something isn’t a good idea. I consider this an important skill of a good leader and manager. But you know what’s really hard to say no to? Helping people.
If I can spend 5 minutes on a job that might save you 10, we’re at a net win. You’re happy, I’m happy that you’re happy, and the company is happy. Happy.
But what happens when you don’t have those 5 minutes? Well, you help anyway and then you eventually write a blog post like this.
I was fortunate enough to be surrounded by kind people. Not a single one of them would have piled on had they known how I was feeling; had I told them. Whilst there was a distinct lack of support from the senior management team, my peers would have gone out of their way to help.
This might sound like a load of self-pity, but I really could and should have done more to make things easier on myself. This is something I’m really trying hard to get better at.
Organisations don’t get away scot-free though. The mental wellbeing of employees is seldom regarded of critical importance. An unnecessary pitstop on the path to success. And what is out there is often reactive; better than nothing, but often too late. Organisations are really pissing away money by not solving this proactively.
Being part of a post-acquisition company that’s losing some of it’s longest serving staff is difficult. There’s a new level of reliance and pressure on the “original” or pre-acquisition staff. Meanwhile senior management are crossing their fingers and hoping the pressure doesn’t send their employees insane. Or, at least, that someone else is there to pick up the pieces when the inevitable happens.
Burnout can have a profound impact on every facet of your life. I found myself working significantly more and achieving significantly less. The extra hours of work replaced time I would usually go to the gym, work on side-projects or spend time with family.
None of my contributions felt fulfilling. I was somehow bored by monotony and overpowered by new stresses, all at the same time. I started getting severe headaches and stomach pains.
Doing anything felt exhausting and overwhelming. I doubted myself as a manager (amongst other things) and became increasingly isolated. I continued taking on more responsibilities in an attempt to feel helpful; and there begins the compounding effect.
They say that every cloud has a silver lining. I think that’s bullshit, but I learnt a lot about myself during this time. I learnt about the sort of environment I really want to work in and the sort of things I really want to spend my time working on.
I took some time to reevaluate my priorities and made decisions accordingly. I set more boundaries and took a long break from social media (which is still somewhat in progress).
I replaced hours of frustration with walks, worked on interesting side-projects and handed off a bunch of responsibilities to others (I was fortunate to have a few people who noticed my struggling and forced my hand in this—I am indebted to them).
As a result, I worked less and, perhaps unsurprisingly, started achieving more again. I also became a father, which does a lot to put life into perspective—though I don’t recommend that as your first step to resolving burnout.
If nothing else, take this as a reminder to look after yourself. Lean on other people, they’re more willing to help than you think. Focus on your mental wellbeing just as much as your physical health. Analyse yourself and your surroundings every now and then, and never be afraid to ask yourself whether you’re truly happy or not.
]]>Each year since 2015, Advent of Code has created 25 small programming challenges for the days leading up to Christmas. Each challenge has two parts, the second of which is unlocked after completing the first. The challenges increase in difficulty as Christmas Day closes in and the final puzzle is posted on Christmas morning, in case you fancy foregoing breakfast with the family in a race to the leaderboard.
Advent of Code is great for people who want to:
All previous Advent of Code events are available on their website, so you can jump into more than 100 interesting puzzles of varying difficulty:
Oh and in case you wonder what it takes to achieve the leaderboard, check out Jonathan Paulson’s speed solving of the 2018 event challenges on YouTube 🤯
If you enjoy the puzzles and have the means to, I urge you to contribute in the ongoing building and running of Advent of Code. I can’t imagine the work that goes into building these creative puzzles, and I’m really excited for the 2019 event.
]]>For the many who picked up Swift when it was still young, the fast changes to the SwiftUI implementation won’t come as a surprise. For a lot of us, though, the opaque changes and lack of documentation are immensely frustrating.
In this post I’ll continue with a variation of the ScrollView application previously posted, and implement some State and Binding properties to allow the application to respond to changes in state.
Our current app looks something like this:
The date cards are displayed in a ScrollView much life the gift items in our previous app. Here’s the complete code:
import SwiftUI
let lightGrey = Color(#colorLiteral(red: 0.9160255393, green: 0.9160255393, blue: 0.9160255393, alpha: 1))
struct ContentView: View {
var dates: [Date] {
(0...10).map { offset in
Calendar.current.date(byAdding: .day, value: offset, to: Date()) ?? Date()
}
}
var body: some View {
VStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(dates, id: \.self) { date in
DayView(date: date)
}
}
}
.padding()
SelectedDayView()
}
}
}
struct DayView: View {
var date: Date
let size: CGFloat = 110
var body: some View {
VStack {
Text(dayName).font(.system(.callout)).foregroundColor(Color.red)
Text(dayNumber).font(.system(.largeTitle))
}
.frame(width: size, height: size)
.background(lightGrey)
.cornerRadius(10)
}
var dayName: String { return formatDate("EEEE") }
var dayNumber: String { return formatDate("d") }
func formatDate(_ format: String) -> String {
let dateFormatterGet = DateFormatter()
dateFormatterGet.dateFormat = format
return dateFormatterGet.string(from: date)
}
}
struct SelectedDayView: View {
var body: some View {
Text("Today")
.font(.system(.largeTitle))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
As you can see, it’s pretty straightforward. We create 10 Date objects (starting from Today), and put them inside a ScrollView. Each date object is wrapped in a DayView that’s designed to encapsulate the style and functional behaviour of our day views.
We have a label below with the static text “Today”. You probably guessed where this was going: We want to update this label when the user clicks on one of the dates.
In classic UIKit, you would achieve this by doing something like:
There’s nothing wrong with this flow. It’s distinctively imperative, and that’s not exactly a bad thing. Things are a little different in SwiftUI, though.
The declarative nature of SwiftUI means we have to change our way of thinking a little. Our steps turn into something like this:
This is what we’re after:
Ideally, our ContentView would be responsible for holding the value of the currently selected date, and the child views would be given access to read and write this date in order to update the view.
This is exactly what the @State and @Binding property wrappers give us. Let’s work through the steps.
Firstly, we need to add a State property to our ContentView to store the currently selected date:
struct ContentView: View {
@State private var selectedDate: Date
}
Then add Binding properties to our child views:
struct DayView: View {
@Binding var selectedDate: Date
}
struct SelectedDayView: View {
@Binding var selectedDate: Date
}
Then of course, we must pass the currently selected date into
the child views. To do this, we apply the $
prefix to our
selectedDate property, which returns a Binding:
DayView(date: date, selectedDate: self.$selectedDate)
SelectedDayView(selectedDate: $selectedDate)
This change tells our child views that they must be re-rendered when the state of this property changes. It also allows the views to update the state (and as you’ll see below, that’s exactly what we do in onTapGesture).
Here’s the full code:
import SwiftUI
let lightGrey = Color(#colorLiteral(red: 0.9160255393, green: 0.9160255393, blue: 0.9160255393, alpha: 1))
let dates: [Date] = {
(0...10).map { offset in
Calendar.current.date(byAdding: .day, value: offset, to: Date()) ?? Date()
}
}()
func formatDate(_ date: Date, format: String) -> String {
let dateFormatterGet = DateFormatter()
dateFormatterGet.dateFormat = format
return dateFormatterGet.string(from: date)
}
struct ContentView: View {
@State private var selectedDate: Date
var body: some View {
VStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(dates, id: \.self) { date in
DayView(date: date, selectedDate: self.$selectedDate)
}
}
}
.padding()
SelectedDayView(selectedDate: $selectedDate)
}
}
}
struct DayView: View {
var date: Date
let size: CGFloat = 110
@Binding var selectedDate: Date
var isSelected: Bool { selectedDate == date }
var body: some View {
VStack {
Text(dayName).font(.system(.callout)).foregroundColor(isSelected ? Color.blue : Color.red)
Text(dayNumber).font(.system(.largeTitle))
}
.frame(width: size, height: size)
.background(lightGrey)
.cornerRadius(10)
.onTapGesture { self.selectedDate = self.date }
}
var dayName: String { return formatDate(date, format: "EEEE") }
var dayNumber: String { return formatDate(date, format: "d") }
}
struct SelectedDayView: View {
@Binding var selectedDate: Date
var body: some View {
Text(dayName).font(.system(.largeTitle))
}
var dayName: String {
if Calendar.current.isDateInToday(selectedDate) {
return "Today"
} else if Calendar.current.isDateInTomorrow(selectedDate) {
return "Tomorrow"
} else {
return formatDate(selectedDate, format: "EEEE")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(selectedDate: dates.first!)
}
}
Note that our ContentView now requires the property selectedDate to be defined, so we must make sure this value is set inside our ContentView_Previews and SceneDelegate.
Binding and State properties are crucial parts of even the simplest of apps, and you’ll quickly find yourself wanting more as the complexity and feature set of your app grows. This is where you’ll want to turn to the Combine framework and ObservableObjects.
I’ll talk more about that in another post.
]]>There’s a lot of static website builders out there. I picked Jekyll because it’s easy to use and extend (in Ruby), and is supported by both GitHub Pages and Netlify, the latter of which is used to host this website.
I recently added an Archive page to show a list of posts grouped by year. There’s a number of ways you can do this, and I’ve seen many hacky implementations on Stack Overflow. Thankfully it’s actually pretty easy.
Jekyll 3.4.0 introduced the group_by_exp
Liquid Filter, allowing us to
group our posts according to the expression we pass as an argument. This
argument is itself a Liquid expression, allowing us to re-use Liquid filters.
Here’s how to group our posts by year:
{% raw %}
{% assign grouped_posts = site.posts | group_by_exp: "post", "post.date | date: '%Y'" %}
{% for year in grouped_posts %}
<strong>{{ year.name }}</strong>
<ul>
{% for post in year.items %}
<li>{{ post.title }}</li>
{% endfor %}
</ul>
{% endfor %}
{% endraw %}
We can even repeat our grouping inside the loop to include per-month subheadings:
{% raw %}
{% assign grouped_by_year = site.posts | group_by_exp: "post", "post.date | date: '%Y'" %}
{% for year in grouped_by_year %}
<strong>{{ year.name }}</strong>
{% assign grouped_by_month = year.items | group_by_exp: "post", "post.date | date: '%B'" %}
{% for month in grouped_by_month %}
<strong>{{ month.name }}</strong>
<ul>
{% for post in month.items %}
<li>{{ post.title }}</li>
{% endfor %}
</ul>
{% endfor %}
{% endfor %}
{% endraw %}
Whilst I’m not a huge fan of the Liquid syntax, Jekyll includes a number of really convenient filters. It’s worth checking out their list when creating these sorts of pages.
]]>Capistrano was a crucial part of our deployment process at Loco2 long before overhauling our hosting infrastructure with Terraform and Docker.
Having recently left Loco2, I found myself back on a project that uses Capistrano. As powerful as it is, it frustratingly doesn’t include support for interactive shells. Whilst this is by design, it’s a bit annoying if you want to boot a rails console or run another command that allows input from the user.
After doing some Googling I managed to piece together something that calls ssh
directly, allowing us to run a command in an interactive shell:
namespace :rails do
desc "Start a rails console"
task :console do
exec_interactive("rails console")
end
desc "Start a rails dbconsole"
task :dbconsole do
exec_interactive("rails dbconsole")
end
def exec_interactive(command)
host = primary(:web).hostname
env = "RAILS_ENV=#{fetch(:rails_env)}" # add other ENV variables
command = "cd #{release_path}; #{env} bundle exec #{command}"
puts "Running command on #{host}:"
puts " #{command}\n\n"
exec %(ssh #{host} -t "sh -c '#{command}'")
end
end
The primary(:web).hostname
just fetches the hostname for the first/primary web
host. It’s worth mentioning this because it might not be desirable. You could
quite easily fetch all of the remote hosts and provide a menu that allows the
user to select the host they want to connect to. For my purposes though, I just
wanted to run a rails console without having a custom script that fetched
our hostname via the AWS command line tools.
You may want to consider safeguarding production access by adding a warning
message when fetch(:rails_env) == "production"
, or perhaps add
the --sandbox
option to protect your data.
I believe developers should be trusted to have direct access to all live environments, including production. These sorts of small convenience scripts really help to avoid friction when troubleshooting or debugging data issues on non-local environments.
]]>The last 6+ years have been wild. I’ve met some very talented people along the way and have made plenty of friends.
I’m incredibly thankful for my time at Loco2. The culture and working environment is like nothing I’d witnessed before, and something I will truly miss. I’ve learned a lot and I’m very proud of what we’ve achieved (and, importantly, how we achieved it).
There’s a bunch of reasons I made this decision, and I’ll talk about the interesting ones in upcoming blog posts. For now though, I will look back on my time at Loco2 fondly, and look forward to having some flexibility to spend time with my family.
So long and thanks for all the trains.
]]>The last month has flown by, and unsurprisingly I haven’t kept the blog up-to-date. I have some good excuses though.
My wife and I welcomed our daughter into the world. Lily Grace Jarvis was born on 17 August at 14:00 and weighing a healthy 8lbs 5oz.
They’re both doing wonderfully. And whilst the last few weeks have been a bit of a blur, it’s also been filled with some of our happiest moments. I’ve also learned some things about Fatherhood.
I celebrated my 31st birthday on August 7. No it doesn’t feel any different, stop asking that.
This one might seem a little at odds with the whole having a baby thing, but at the end of July I overcame my aversion for consuming digital books and picked up some new titles to read on my iPhone.
After struggling through the first couple of days, I am now fully onboard with reading books on a handheld device. I have increased my reading rate by about 300% which is pretty crazy. I’m currently reading (or have recently completed):
I really wish that I’d forced myself to do this sooner.
I love photography, but I’ve never thought myself any good. It’s also a fairly expensive hobby (if you let it be, which I absolutely could see myself doing).
I wanted to challenge myself to become better so I decided to bite the bullet and pick up a mirrorless camera. I went with mirrorless because DSLR’s are just too chunky for me and I think I’d seldom use it.
After some toing and froing, I picked up a Sony a6000. Whilst it’s a fairly old camera (which Sony finally upgraded 1 week after I purchased), the reviews are really good. Also:
That’s it for now. I have a very busy September ahead, and hope to add a couple of new (more interesting) posts later this month.
]]>It can’t have been more than 5 years ago that my local library were dishing out free leaflets featuring bewildered old folk staring at computer screens. The caption Are you backing up? written in classic clip art text and complemented by none other than a giant floppy disk. Backing up isn’t just for the geeks and the paranoid.
And still, in 2019, this advice remains critical for anyone storing digital copies of their files. With the increase in paperless forms and bills, one might even argue that it’s more important now than ever before. We should all ask ourselves what would happen to the documents we care about if our devices were to be stolen or damaged. Would we lose them forever? Does it even really matter?
Nowadays these backup and syncing solutions are simple enough that asking these questions is an exercise in futility. And rightfully so, backing up your data should be a matter of course, an inevitability. Any good backup strategy should:
I follow the 3-2-1 backup strategy, which means I have at least 3 total copies of my data. I actually have 4 just for the hell of it:
The Backblaze article above compares backing up your data to diversifying your investment portfolio. There’s no perfect solution, but lowering your risk is a prudent way to reduce the chance of losing data.
I have Time Machine pointed at a 500GB WD external hard drive for each computer and it just ticks along nicely in the background. With external hard drive prices continuing to bomb, you really don’t have to break the bank to get this solution up-and-running. If I was buying a new external drive now, I’d probably pick the Samsung T5 Portable SSD.
Carbon Copy Cloner is used to create a bootable backup drive, allowing me to simply plug-in and get going again at a moments notice. I’ve heard good things about SuperDuper but haven’t yet felt inclined to switch away from CCC, even if it’s a little hard on the eyes.
Finally, I have Backblaze running on all of my machines. It’s one of the first things I install, and at $6/month, it’s a steal. You can even let Backblaze back up your external drives at no extra cost. If you need a single file, you can cherry-pick them from the web UI or download a full backup in the event of a catastrophe. If you have a lot of data, Backblaze will mail you a hard-drive full of your data so you don’t have to re-download everything.
All of this might seem somewhat excessive, but having had my fair share of hardware failures, I can tell you that I learned a hard lesson from not backing up earlier. Don’t wait for a disaster before you have a backup plan.
]]>As the saying goes, code is read much more often than it is written. It takes many years of experience to write great code, and nobody gets there without having read a lot of it.
I started writing code before JavaScript became popular, when learning HTML meant copying the contents of your favourite website’s source code and tweaking it to fit your purpose. New websites introduce new HTML tags and CSS rules, long before the prominence of w3schools.
Naturally I became a bit stuck when I started learning server-side languages. At the time I couldn’t really afford the books; I really wanted to sample everything and see what sticks.
Reading code I couldn’t understand became regular, even nuking several OS installs during my frivolous experimentations didn’t stop me. I learned to appreciate clean, well-written code before I could really write it myself. GitHub eventually came along to make this experience effortless.
When speaking to new developers I have a few examples I use to demonstrate what I appreciate as well-readable Ruby code. There’s a level of subjectivity to this of course, but for me, clean code:
So, without further ado, here are a few of my favourites:
Sidekiq is a library for processing background jobs. It’s pretty much considered the go-to tool for Ruby and Rails developers who want to store their queue data in Redis. What is a fairly complex piece of software is written so well that it basically holds your hand along the way.
Sequel is a database toolkit for Ruby. It’s a strong replacement for ActiveRecord and is my ORM of choice for non-Rails projects. It’s 12 years old and was one of the first Ruby libraries I ever encountered.
Oga is an XML/HTML parser written in Ruby (crazy, right?). Whilst Oga is no longer maintained, it remains a good example of nice-to-read code.
Hanami is a young Ruby web framework. It’s designed to combat the extreme bloat that Ruby on Rails introduces with its “everything including the kitchen sink” approach. The project is split into smaller pieces making it easy to digest and poke around.
HTTP is my go-to Ruby library for making HTTP requests. It’s fast, easy
to use (who on earth remembers the net/http
syntax?), and with a lack
of “magic” code, it’s really easy to understand what’s happening under
the hood.
Split ticketing allows customers to save money on their train journey by purchasing two or more tickets instead of one.
For example, imagine leaving Swansea at 07:30, bound for Perth. You’re travelling during peak time so you know your ticket will come at a premium. It’s £228.30. This is a nine hour journey, and only 1.5 hours of it take place during peak time. That hardly seems fair.
Well, we can actually split this journey into six tickets:
For a total of £86.50 + £1.50 commission That’s an incredible saving of £140.30.
Example from Swansea to Perth at 07:42 on 29 July 2019. Prices may change closer to travel.
Pricehack provides a way for Loco2 to bring split ticketing to the masses. Imagine for a moment that you’re not a train geek (I know, a tough ask). How would you know that you could save money by breaking up your journey? And even if you did, which stations would you select?
That Swansea to Perth journey has 29 stops, do you have the time or patience to search for every combination? I know I don’t. With Pricehack, you don’t have to worry about the details. We’ll do all of that complicated work for you, without you having to ask.
Pricehack works across classes, too. You won’t have to look hard to find a cheaper first-class ticket than the price you’d usually pay for standard class. Treat yourself, you deserve it.
Pricehack is the combination of cutting-edge software and intuitive customer experience.
In 2017, a football fan booked a staggering 56-ticket split journey; saving a total of £56. I can’t imagine the pain he would have gone through to do this manually, and having to juggle a handful of tickets makes the upfront work even less palatable.
Seasoned travellers might scoff at the notion of printing all of these tickets. And frankly, I wouldn’t blame them; the UK has lagged severely behind in adopting mobile technology for our rail travel. Thankfully we’re finally starting to catch up with the rest of Europe in our endeavor to support fully mobile tickets.
Whilst not all train-operating companies grant mobile tickets just yet, those that do will be available to you on Loco2 right now. With the Loco2 mobile app, your mobile Pricehack tickets are a mere swipe away. All 56 of them.
Pricehack works by taking advantage of a very complicated UK rail fare system. Whilst some of these complications do exist in other countries, none are so glaringly apparent as to warrant complex technology like Pricehack.
We’re always on the look-out for chances to apply our technology to new problems though, and we’re actively looking at ways we can extend this software to make travellers lives easier.
In the meantime, Loco2 remains the best place to book your train journeys across the UK and Europe in a single booking. No fuss, no fees, and a supportive team of train geeks guiding your way.
As a software developer, projects like Pricehack don’t come along often. A truly complex and wonderful piece of technology that produces an unequivocal benefit to many thousands of people.
It’s been a long hard road (track?), but I’m incredibly proud of what the team has built and I’m really looking forward to seeing how this technology evolves.
Happy (price) hacking!
]]>A few years ago I created an iOS app that tracks gift ideas for family and friends. It’s not something I ever published to the app store, but I do keep it for personal use.
The app isn’t particularly exciting. It stores gift ideas and keeps track of birthdays, anniversaries, etc. On the app homescreen I have a horizontal scroll view that shows some of my upcoming events; “Mum’s birthday”, “My Wedding Anniversary”.
These event “cards” were made up of a name and a custom emoji/background colour. To drastically simplify, they looked something like this (but better, I promise):
The ScrollView wasn’t especially difficult to build, but asking AutoLayout to play nicely across all devices in any orientation wasn’t exactly my idea of fun. Not only was it tedious and time-consuming, it was really sucking the fun out of creating the app.
With SwiftUI, I was able to re-create a similar ScrollView with just several lines of code:
import SwiftUI
struct Event: Identifiable {
let id: Int
let name: String
let emoji: String
let color: Color
}
let events = [
Event(id: 0, name: "Jon's Birthday", emoji: "🥳", color: Color.red),
Event(id: 1, name: "Wedding", emoji: "👰", color: Color.blue),
Event(id: 2, name: "Aimee's Birthday", emoji: "🎉", color: Color.green),
Event(id: 3, name: "Christmas", emoji: "🎄", color: Color.purple),
]
struct ContentView : View {
var body: some View {
ScrollView(.horitonzal) {
HStack {
ForEach(events) { event in
VStack {
Text(event.emoji)
.font(.system(size: 50))
Text(event.name)
.font(.system(.caption))
}
.padding(40)
.background(event.color)
.cornerRadius(10)
}
}
}
.padding()
}
}
This is the entire file. Go ahead, paste it into Xcode 11 beta.
No AutoLayout constraints, no Storyboards, no testing across every device just to be sure I didn’t break something. The UI exists right here in the code.
The declarative syntax really makes the code easy to read and write, and using Xcode’s powerful built-in documentation and suggestion tools, I found myself creating the user-interface is a very natural way.
This was built on macOS Mojave, which doesn’t have access to the live preview canvas and drag+drop behaviour. This meant I had to rebuild the app each time to preview my changes. No bother, I’ve been doing that for years. I can only imagine how much better this is on macOS Catalina.
I’ve been watching some of the WWDC session videos too. Here’s some good ones on SwiftUI:
All WWDC videos can be found here.
I’ll be continuing to play with SwiftUI and may write a post or two in the coming weeks. I am incredibly excited for this technology; it makes building mobile applications much more accessible. Finally, for me, iOS development is fun again.
Update: I’ve expanded the app to introduce state and bindings. You can read about it here.
]]>Where available, I tend to use Safari’s Reader Mode for reading online content. It has tools to change the size and colour of the text and background. Sadly even some of the more popular news and blog websites are often unreadable due to their fancy font styles and tiny text size.
Reader Mode allows me to quickly switch between light and dark themes, a godsend when procrastinating late at night.
You’ll notice a new lightening-bolt icon in the website header. Zap that (see what I did there) and you’ll switch between dark and light themes.
Adding the code for this was pretty straightforward. The toggle has an onClick
handler:
<a title="Switch theme" onclick="toggleTheme(); return false">
with a sprinkle of JavaScript in <head>
:
function toggleTheme() {
if (localStorage.getItem("theme") === null) {
localStorage.setItem("theme", "dark")
document.querySelector("body").classList.add("dark")
} else {
localStorage.removeItem("theme")
document.querySelector("body").classList.remove("dark")
}
}
document.addEventListener("DOMContentLoaded", function(e) {
if (localStorage.getItem("theme") === "dark") {
document.querySelector("body").classList.add("dark")
} else {
document.querySelector("body").classList.remove("dark")
}
})
Using localStorage allows the theme setting to persist across page views and browser sessions.
If you’re observant you might notice the small flash of unstyled content when using the dark theme, since it waits for the DOM to load before applying the style. I consider this a fair trade-off to keep things simple.
Now all that’s left to do is update the existing styles when the darker theme is in use:
body.dark {
/* dark style overrides here */
}
I’m using SCSS, so I could just as easily use a mixin for applying dark styles inline.
In addition to this, Safari is introducing a new media query
called prefers-color-scheme
, allowing us to automatically apply these themes
when a visitor is using dark mode on macOS.
You’re welcome, eyeballs.
]]>This isn’t that, though. There’s no hello world. There’s no creating a new blogging engine. And most importantly, there’s no commitment to start blogging frequently.
Instead, I’ve tweaked some of the website styling and added a place to
write new posts. They won’t appear often, but as I find myself increasingly keen on sharing
smaller bits of information that don’t fit inside 140 280 characters, I am becoming
content with the idea that excessive prose doesn’t always make for good reading.
Additionally, I’ve noticed an increase in quality technical blog posts lately, and bookmarks just aren’t cutting it. Instead, I’ve added an external link feature to the blog, allowing me to add a small summary about something I’ve read or found interesting.
]]>I usually feel quite ambivalent toward the keynote — where Apple announces its upcoming software plans to an eager crowd of enthusiasts. It’s been hit-and-miss the last few years, and amongst the often mediocre announcements were cringe-worthy AR demonstrations and a slew of statistics designed to compensate for an otherwise hollow keynote presentation.
Not this year.
Apple’s presenters set record pace. Some might argue they rushed it. After 15 minutes of extreme slide-shifting, it was clear Apple had a lot to talk about.
These are just a handful of my favourite parts of the keynote. As someone who dabbles in iOS app development, I’m very much looking forward to digesting the information coming from the sessions that followed over the coming weeks.
]]>