Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
42

A window switcher on the Mac App Store? Is it even possible?

from Copper • A blog about conductive layers [alt+shift+b] in programming

Not really, no. Not without annoying workarounds and a confusing user experience. Another email, another annoyed user: Firefox not loading websites when launched through rcmd! It works when launched from Alfred.. Please fix ASAP!! I’m gonna fix this Firefox issue once and for all! Launch Xcode, open the rcmd project, check the launchApp function code, it’s just a NSWorkspace.open call on Firefox.app, what does Alfred do differently? Disassemble Alfred.app in Hopper, look for NSWorkspace.open, of course it’s there, it’s the exact same thing. Try open /Applications/Firefox.app in a terminal, it works, websites load as expected. Breakpoint on launchApp, check the debugger again, let’s be rigorous, what am I really calling open on? Argument is /System/Volumes/Data/Applications/Firefox.app which is just a symlink to /Applications/Firefox.app right? .. or was it the other way around? Anyway let’s just try it for the sake of it, I’m desperate. Run open /System/Volumes/Data/Applications/Firefox.app, huh?? no websites load? THAT WAS IT?! Add path.replacingOccurrences(of: "/System/Volumes/Data", with: ""), build, run, hold Right Command, press F, Firefox launches and holy cow everything works!! I don’t even care why anymore, let’s just release this fix on the App Store. And while I’m at it, why not try to add that window switching capability that people have been asking about? I remember something about Accessibility permissions not being available in the sandbox, but I just used an App Store app that was able to request the permissions so there has to be a way, how hard could it be? Well it turns out it’s pretty darn hard, and I’m still working on this window switching thing to this day.. sigh.. let me tell you about it. Apps vs windows There’s an important distinction between switching windows and switching apps on the Mac. As opposed to Microsoft Windows where you just Alt-Tab through .. well, windows, on macOS you Command Tab through apps by default. When an app with...
2nd Aug 2022

Stay updated

Get a weekly newsletter with the top 5 articles worth reading every week.

More from Copper • A blog about conductive layers

How good is Claude, really?

At the beginning of 2026, I didn’t even know what vibe coding meant. So much has happened in the time I was busy carving wooden spoons, that I felt it simpler to just ignore the hype. One morning, in this seemingly never ending winter, a friend told me excitedly how his employer got them Claude subscriptions and encouraged them to try it for work. And his reaction was something along the lines: “it’s crazy! I love it! we’re doomed…” .inner-shadow { position: absolute; width: 100%; height: 100%; box-shadow: inset 0px 0px 10px 3px rgba(1,0,5,0.35); top: 0; left: 0; border-radius: 12px; } I felt that was an overreaction, I tried LLMs for coding before and aside from small snippets, it couldn’t do more than 50 lines without stumbling on itself. But it was a really gray hazy winter morning, and I didn’t feel like splitting wood in the quiet of the house, so I made some coffee and got my laptop out. Stages I gave it the usual task that I really want to see done but I’m always dreading to start: Stages in rcmd rcmd is a macOS app switcher that allows you to hold Right Command, and press the first letter of the app name. So press V for VSCode, G for Ghostty, C for Chrome: instant app switching without cycling through app icons. Stages was dreamed up as a way to switch workspaces with the same paradigm. So press C for Coding where VSCode and Ghostty would focus and everything else was hidden, D for Design and so on. But the functionality is way harder than it seems, the app needs to record the on-screen windows, and be able to restore them when the computer is restarted. The task was complex, just to outline some things Claude needed to do: understand the pre-existing app codebase with all its UI quirks refactor the UI to be able to present sets of apps and windows find ways to record custom window data like: the project opened in VSCode the working directory in Ghostty the current URL in Chrome files/folders/projects opened in other apps like Finder, Pixelmator, Sketch, Xcode etc. find ways to re-open windows at that specific project/file/folder figure out a way to hide individual windows on macOS It’s weird, but there’s no instant hide/show for windows on macOS. You can minimize them, but that has slow animations and unwanted side effects. I mentioned to Claude that I knew a window manager called Aerospace which had a workaround for this. I gave Claude a 200 line stages.md, let it loose on the rcmd code, and stared at it crunching stuff for a few minutes. I went to make another coffee, and when I came back it was… done? I hit Build in Xcode and was prepared to see a barrage of errors, that’s how it always was. I got ready to screenshot the errors and the stupid code diff and show my friend how “no, we’re not doomed, it just got lucky on that specific task”. No errors though, the app compiled and launched like nothing happened. Uhh.. now what? The code diff was immense, I guess I should just see if anything actually works anymore. I hid every app except VSCode and Ghostty, pressed rcmd + lalt - C and it said Stage C was assigned. No way this works… I brought back all apps on screen, quit VSCode, pressed rcmd - C and instantly VSCode and Ghostty were the only apps visible and focused, in the correct folder and project. I looked through its thinking process and it even did a web search for Aerospace macOS window manager, found its Github, reverse engineered its workings until it found the window hiding workaround I mentioned in passing and implemented it from scratch in rcmd. Astonished is a small word for what I felt. Of course, this was not a feature ready for release. There was so much code review and so much polishing work to do (not all apps have ways to restore their state after quitting, some focus flickering happened, and Stage creation should be more intuitive). But the fact that an LLM could do such complex work, changed everything. Picture-in-picture 🔗 Pipiri is a macOS app that can show any window in a floating picture-in-picture window I had this barebones app I did as a standalone Swift file, for creating a Picture-in-Picture view of a window where there’s data I’d like to keep an eye on. A Picture-in-Picture view of a Claude session floating over Xcode I mostly used it to watch long-running terminal commands while I continued coding, and for keeping Xcode logs visible while I tested parts of my app. I thought it was too niche to release, so I didn’t have the motivation to do the work on creating an UI, licensing, webpage, icon etc. I created a blank Xcode macOS app project and gave Claude minimal instructions: adapt pip.swift to a menubar app with a SwiftUI architecture add a Settings window where I can adjust the trigger hotkey, the position of the PiP panel etc. implement auto-updates with Sparkle add license verification through Gumroad link to a Contact form and a privacy policy, similar to how I do it in my other apps What can I say? It did everything. Granted, the main PiP functionality had already been written manually by me years ago, this was just doing the things that every macOS dev hates to do but has to repeat on every app. Then onto the webpage. Manually wrote a list of features with explanations in a README.md because I prefer to address future users of my apps personally, then told it to build the landing page by taking inspiration from my other app pages, using what I wrote in the README. I was more than relieved to see 95% of the webpage look exactly as I would have done it myself. Not a single word changed, only HTML structure, colors, image paths etc. Adapting old webpage structure for a new app was not something I enjoyed doing. I also got a little carried away and asked Claude to implement a VM bytecode interpreter and write the offline license validation in that bytecode. I didn’t expect it to actually do it! It even added a license generator CLI on top. I launched a v1.0 and it was received well enough. Got 2 feature requests from the first day: Multi-window mode (allow keeping multiple PiP panels visible from different) Idle/Change detection (get notified when the window contents haven’t changed in a while, or if they did change after being still for a while) These are features I would have liked myself, but was too busy to start working on them before. Now, Claude was not able to do these properly by simply mentioning the features. I had to explain the features in detail, how I want the multi-window PiPs to appear on top of each other, what APIs it should use, describe how the idle notification should look and that it’s not a system notification. In the end, the code needed a lot of review, testing and manual fixing, but just getting me started on the features was enough to motivate me to finish and release them. Getting a semi-functional feature in readable code that I can polish is much more motivating than getting a huge refactor and full implementation that doesn’t even compile. After releasing the change detection feature, I got a nice message from someone working at a hospital telling me how they use it to know when a patient has been added to the admission list without having to check it very second. This is always humbling to me when it happens, reminds me how naive and limited we are as devs thinking that we considered all use cases. When in fact we should probably just build the tools, and people will find use for them in their own way. Event-based automation 🔗 Crank is a macOS app that can run scripts and Shortcuts on event triggers My little brother was telling me about how he’s trying to find ways to make money without having to work long hours in coffeeshops and restaurants. He has already started a few ideas like websites for small local shops that want an online presence, copywriting, product photo shoots. can you believe those product shots are generated from a bland photo of a jar of honey? So I thought why not also throw in a simple macOS app. I can bring in the ideas that I’ve been sitting on for the past 5 years, help with code review and fixing, and he can just prompt Claude and test the app until it’s working as expected. The main theme was simple, low maintenance apps only. I had this idea for an event-based automation app: just the basic parts of what has already been done in Keyboard Maestro or Shortery, at a much cheaper price and free for basic needs. The UI of the Crank macOS app In support emails and chats, I’ve had a lot of people ask for some very niche functionality in my other apps. Usually, it’s some kind of automation, things like: “can I disable the MacBook screen only when I connect my Dell monitor at home?” “how can I make all screens black when I want to audition a piece in my recording studio?” “can optimised images in ~/Blog be renamed to YYYY-MM-dd-name and converted to webp?” “please add an option to toggle HDR when I open IINA or any other movie app” So I wanted to give those people the ability to resolve their frustrations without having to wait for a dev to do it. I hopped on a screen sharing session with my brother and let him ask Claude to build this app bit by bit. The first thing that jumped out is: you have to know the terminology to get quality results. He’s not a dev, he doesn’t know what a debounce or throttle is, so incidentally, he doesn’t know to ask Claude to implement it for events that arrive in bursts. He has no idea what a private API is so he can’t guide Claude to use one where we need to access low-level system APIs. And obviously concepts like dry run or event log are not familiar, but definitely useful in an app like this. Interestingly though, he was able to get Gemini to write a prompt for Claude that included a lot of these features that you’d find in an automation app. I wouldn’t have thought of that. But dev intervention was still necessary. Some added features were just dangling in the UI without any effect internally, and it was impossible for a non-dev to know what they were for. We started that way, then he continued on his own while I worked on my own apps. Even without my assistance, the result was still a very functional app that could run scripts or launch apps automatically on event triggers. And the thing is, people could have used this as is, it was useful enough to be released. But I knew it wouldn’t be received well, people expect a certain polish for apps that have paid tiers. The dev touch To be really useful, the app needed some triggers that Claude did not know to implement on its own because such code uses obscure private APIs and IOKit data. Things like: ambient light data through the MacBook’s integrated sensor (ALS.swift) if camera is turned on or off (IsCameraOn.swift) Focus mode or Do Not Disturb changes Thankfully I had already implemented those in mac-utils, but adapting them to Crank’s event trigger architecture would be quite the work. By now, I learned that this is where Claude shines: once it has working code to reference, it does a really good job at adapting that code to another project’s structure. After I pointed Claude to the files and told it to create triggers out of them, I needed less than half an hour of testing and fixing logic bugs on my part to get everything in good order. It can write quite readable code when it wants. I remembered seeing this crazy app that told you the MacBook lid angle, so I thought what the heck, someone might find a use for that. I told Claude to implement a lid angle trigger as well based on that code. I’m thinking one might want to disconnect stubborn Bluetooth peripherals when the lid is closing, and you only have a chance to do that before the lid was fully closed. Open source becomes incredibly more useful with an LLM to understand the code in seconds instead of days. It also feels like stealing, not gonna lie. If I did the research myself, it wouldn’t have felt like that, and I can’t figure out why. Cherri Some actions simply can’t be done through scripts. One example of that is enabling Do Not Disturb (or changing the Focus Mode). There’s no CLI that can do it because the system does not expose those APIs. A cherri script on the left and the compiled Shortcut on the right But it can easily be done through Shortcuts, which has a Turn Do Not Disturb On/Off action built-in. But (big but…) a named Shortcut containing that action needs to already be created by the user, we can’t call individual actions. And I can’t have Crank ask the user to create Shortcuts and drag actions around for such basic things. Shortcuts can be pre-created as file.shortcut, and installed by simply opening them, so I could provide those Shortcuts bundled with the app. But doing a rough calculation, I would have to create about 100 individual Shortcuts by hand, using mind numbingly slow drag and drop. Programmer laziness got the best of me and I went looking for a quicker way. Somehow I stumbled on this gem: Cherri - a programming language that compiles directly to a signed Shortcut I tried to get Claude to write a few basic Shortcuts with Cherri, but it clearly didn’t know the syntax. So I told Claude to just learn it from the website, and gave it the whole list of actions I need. In less than 10 minutes it: learned the Cherri programing language syntax learned to use the cherri compiler got action definitions with cherri --action wrote and compiled 98 individual Shortcuts, fully functional with inputs and outputs damn The scripts problem The problem with creating an automation app, is that you usually have to spend most of your time building a huge library of built-in actions. I didn’t want to go that route. That’s why Crank’s actions seem pretty limited: Shell script AppleScript Shortcuts Launch App Open URL Open File Notification But in my opinion, this set of actions allows you to do most common things on a Mac, if you know what to use. However, not all users of this app will know what a script is, or how to write one, so I thought I should lower the barrier of entry as much as I can. If LLMs can write such good code, why not have them write the automation scripts for us? (well, because they could delete all our files with a misplaced semicolon, that’s why…) But non-devs would try an LLM anyway, so I thought I could at least make their life easier and add some safety guards. In this test, I wanted to see if an LLM understands how to make another less capable LLM get good results. So I told Claude to create a context.md describing common CLI utilities that would help in automating a Mac, and express that as a prompt for Gemini. I added a Generate Code button in the script editor and tried to make the free Gemini tier write short useful scripts, instead of hundred-lines hallucinations on CLIs that don’t exist. It’s not perfect, but when it works, it feels magical. Generating a script to compress invoices and print them Turns out, LLMs know how to talk to other LLMs. The generated context.md tells Gemini things like: You are a macOS shell automation expert. Claude even went as far as to customize the context based on the Mac it runs on: Assume the script will run on \(ARCH) Macs with macOS \(MACOS_VERSION) Use the event variables set by Crank if relevant to the task Tool Availability on this System: basic stuff like ffmpeg, jpegoptim etc. … It’s not complex code or writing, but not very creative either. And I like that I can delegate the boring parts to a smart text generator so I can focus on the harder reverse engineering and making Macs do things they weren’t supposed to do. At least until it learns to work with Frida and a disassembler and I’m fully replaced. Consequences I started this exploration as an AI skeptic, or better said, ignorant. But things are progressing faster than I can adapt to them. So while I’m no longer a skeptic, I’ll probably stay ignorant in the long term. I see Claude Code as a valuable tool for experienced devs, just like I saw syntax highlighting, autocomplete, IDEs and other dev tools when they appeared. It enables me to spend less time in front of a screen without neglecting the people using my app, which is my end goal. But it is frightening to think; what would my mind go through if I was a junior dev in college and seeing a $20/month algorithm do what I’d need to learn in years? And now I’m thinking “I’m fine, Claude is smart but it’s still far from being able to do all the complex work I do for creating a truly useful app”. But far is relative, and such a different measure in the world of AI. I’m probably in denial, I remember laughing at LLMs hallucinating non-existent Swift syntax less than 3 months ago. To calm some of the people using my apps who might also read this: I’m not becoming a vibe coder and giving Claude free reign on Lunar and Clop. At least at the moment, the codebases of those apps are still too messy and complex for an LLM, I don’t want even the remote possibility of introducing a critical bug. rcmd still has a small codebase and doesn’t work with hardware and sensitive files, so it’s safer to try it there, but every line is still reviewed and validated by hand. I’ll probably try to get Claude to help me on things I started but never had the time to finish, like: building a custom filesystem index and fuzzy search algorithm for Cling adding natural-language calendar event scheduling in Grila fixing long-standing bugs in StartupFolder and IsThereNet But it will probably function more as a talking rubber duck, than as an employee I can trust to do my work for me.

4th Mar 2026 • 1 votes
Developing a food-safe finish for my wooden spoons and cups

You know what they say in the world of products: fast, cheap, good; pick two Which is very similar to my experience when trying to find the most suitable wood finish for my hand carved wooden spoons and coffee cups. hand carved wooden spoon It’s hard to find a finish that: cures fast (in less than 2 days) is food safe (can be used on things you eat or drink from, like spoons and cups) is free of solvents (doesn’t smell of nasty chemicals that make you dizzy every time you apply it) and with the hard requirement of making wood hydrophobic and leaving a visually pleasant sheen without creating a plasticky layer Natural drying oils Sure, we have pure tung oil which doesn’t need any solvent, has a pleasant nutty smell, and cures into a food-safe hydrophobic polymer inside the wood fibers. But it takes 2 to 4 weeks for the curing to happen depending on oxygen intake, and the finish is quite matte and boring. While tung oil is natural, that doesn’t mean you can ingest it. It is a powerful irritant in its liquid form, it is only food-safe after curing into its inert polymer state. There’s also the more rare polymerized (or heat-bodied) tung oil, which is a honey-like viscous oil that cures in a few days because it was heated to very high temperatures in the absence of oxygen. That would be ideal except I can’t find it anywhere around me to buy, and it would most likely need thinning with citrus oil solvent (or D-limonene). Citrus oil has a nice but way too potent smell of orange peel. It might seem pleasant at first compared to the chemical smell of naphtha and white spirits, but it can be too much if you finish small wooden objects often like I do. ash cup, in the process of being oiled with tung oil And there are plenty other drying oils, the most well-known being linseed oil, which is indeed edible but has the unfortunate disadvantages of yellowing over time, taking ages to cure and having a persistent oily-grassy flavor. Polymerized linseed oil (aka stand oil) cures faster but still retains the nasty flavor. Careful not to confuse it with boiled linseed oil or BLO, which uses metallic drying agents like cobalt or manganese salts. The boiled name comes from the old practice of heating the linseed oil with lead oxide inside, effectively shortening the oxidation process. But nowadays there’s no heat involved in BLO. More details in The lowdown on BLO The oil painting world has experience with many drying oils like safflower oil, walnut oil, poppy seed oil, hemp seed oil. Unfortunately they cure slower and into a less durable film while also being rare and expensive so they aren’t good alternatives for wooden utensils. Hardwax oils For a good few months I used Osmo Polyx Oil which is a solvent-based hardwax oil. Its safety datasheet and full declaration of ingredients mention about 50% of it being white spirit solvent, and the rest is composed of natural oils processed to become drying oils, and a small amount of hard carnauba wax. It is fast curing in less than a day, leaves a beautiful shimmer on the wood surface, it is food safe after curing, but smells awful because of the solvents. In the winter months I carve indoors and have to finish the pieces indoors as well, and the horrible solvent smell fills my house for a whole day. I seem to have become sensitized to the smell and now I have almost a full can of Osmo that I can’t use without a mask, even outdoors. Rubio Monocoat two-component hardwax oil on a half finished wooden board There’s also Rubio Monocoat and other two-component hardwax oils where the base component is usually a solvent-free blend of drying oils and waxes, and the accelerator component is Hexamethylene diisocyanate or HDI. The base component can cure on its own in about 3 weeks and the accelerator shortens the curing time to less than a day. This alternative cures fast into an inert and food safe polymer, smells like apple pie, and the finish has a slight satin effect. But it’s really hard to mix properly and apply on small wooden objects like spoons and cups. I almost always use too much accelerator, the mixture ends up too viscous and hard to spread on the wood and the constant opening of the bottles and cans makes the components dry prematurely in their containers. However, this idea of blending drying oils with hard wax seems to be promising and it’s the path I continued on developing. Notable mentions Food-safe epoxy is another alternative, but I hate both the idea of using synthetic plastics and their plasticky look takes away from the experience of using wooden utensils. Why even use wood if you’re going to cover it in a layer of clear plastic? Many people also recommend other edible oils like coconut oil, olive oil etc. But these don’t cure and don’t provide any protection for hot liquids. They either go rancid inside the wood fiber or get washed out after using the wooden object. Some recommend non-edible petrol-based mineral oil (aka liquid parrafin) because it doesn’t go rancid, but has the same effect of not actually doing much for protection and will leak into hot liquids. Beeswax is often used by kuksa makers (traditional Scandinavian wooden cups) but wax molecules are too large to penetrate wood fibers well, and it has a low melting point of 60℃ (140°F) so hotter liquids are out of the question. Carnauba wax has a higher melting point at 80℃ (176°F) and is harder than beeswax, but it has a glossy finish, having a similar look to epoxy resin after buffing. The ideal finish should combine the durable hydrophobic properties of fiber-penetrating tung oil with the fast drying properties of a top wax layer, in an easy to apply but solvent-free blend. Urushi wooden spoons finished with urushi lacquer Some carvers use urushi lacquer which is the sap from a tree common to Japan. The result is a hard and glossy finish that can resist temperatures of over 300℃ (572°F), but the look and process of application is far from what I’m looking for. To ensure curing is complete, it needs 10 to 20 very thin coats over many months in a warm and humid environment. The wood then takes this dark brown color, regardless of what wood you started with, which is the opposite of what I want. I work with fruit and flower woods that are less common in woodworking (apple, fig, jasmine etc.) and I want people to know that and experience what that specific wood looks and feels like. I want to keep their original hues and tints and accentuate the wood grain of each piece. fig wood spoon between other hand carved spoons Someone from Israel bought a spoon I made from fig tree wood, but still asked about it a bit incredulously. It had greenish light wood grain, with a bit of untouched bark kept on the handle and a wonky curve that followed the shape of the tree branch it was made from. They have many fig trees there but had never seen such a thing made from their wood, so they felt a connection to their motherland in that usual object. lilac coooking spoon, front and back My mother-in-law cut a large lilac tree and gave me some thick branches. It’s a wood that cracks very easily while drying, and I couldn’t use most of it. But the few things I made from it, I gifted back to my wife’s mother and sisters in the form of cooking spoons and are still cherished by them today. Its pastel purple grain accents remind them clearly of the tree that used to flower every spring in their yard. A brown lacquer would never be able to create this effect, and that is why I’m still trying to find the perfect natural oil based finish. Natural hardwax oil My experiments started simple, with melting about 10g of carnauba wax flakes into 40g of pure tung oil. Tung oil and carnauba wax flakes Most people recommend a double-boiler or bain-marie approach to melting the wax. That’s because the heat source would be a container of boiling water which can never get above 100℃ (212°F), a safe temperature to avoid reaching the flash point of these oils. I just drop a spoonful of carnauba flakes into a glass jar of tung oil and place it in the microwave for a minute until everything becomes liquid. For small quantities, the microwave works wonders, and there’s no danger of overheating the oil or gelifying it because it cools off rapidly. experiment #1 Ingredients: 40g pure tung oil 10g carnauba wax flakes Process: pour the tung oil into a glass jar or ceramic container drop the carnauba wax flakes into the oil and stir to disperse the flakes place in the microwave for 30 seconds then stir again for a few seconds repeat until the wax melts and everything is liquid let it cool down until the mix is solid Result: Brittle paste that’s hard to scoop from the jar and apply on the wood. Doesn’t spread easily, feels sticky. After the whole wood surface was covered and left for 10 minutes, the excess was wiped with a paper towel. After wiping, the finish is very matte, which means that not much of the wax remained, and the oil didn’t penetrate the fiber enough because of the consistency. Smells nice though. Improving the consistency The consistency changes a lot based on the oil-to-wax ratio. Too much wax and the blend cools into a brittle paste that’s very hard to scoop from the jar. Too little and the blend is too liquid with not enough wax remaining on the wood surface to form a film. A 4:1 oil-to-wax ratio is good, having enough wax for the top layer, but in this simple formulation it is still too hard and brittle. Instead of increasing the oil quantity which could defeat the purpose of the wax layer, I added a bit of beeswax to soften the blend. This works, the paste becomes softer but it makes the top layer of wax not durable enough because of its lowered melting point, so I needed an alternative. My idea is to have the wood absorb the tung oil from the blend while leaving the wax molecules on the wood surface. The wax should be soft enough to not create a glossy layer and not be brittle, but hard enough to protect the wood for the time it takes the tung oil to cure. experiment #2 Ingredients: 40g pure tung oil 7g carnauba wax flakes 2g beeswax 1g lanolin To soften the blend without having to add too much beeswax, I found lanolin to be a good addition. It is a highly hydrophobic wax extracted from sheep wool that even in tiny amounts can make the paste consistency softer and easier to apply. Process: Similar to #1, just immerse the waxes into the oil in a glass jar and microwave in increments of 30 seconds until everything is liquid. Leave to cool until solid. Result: Softer and more homogenous paste that can be scooped easily from the jar with a finger or rag. Spreads relatively easy onto the wood surface, feels like a more viscous hand cream. Smells amazing, nutty, vanilla and honey scent, faint enough to not bother anyone. After covering the whole wood surface, I used a heat gun to liquify the finish and enhance wood fiber penetration. The paste melts very easily and the oil in it gets absorbed quickly in the wood pores, leaving the wax to solidify. After the wood has cooled, I applied another layer because it seemed that some parts of the wood had little to no wax remaining. I removed the excess with a paper towel and buffed the wax to a satin look. small issues curing speed This finish is not fast curing by any means. It is fast drying, as the oil gets absorbed and the wax on top solidifies into a very thin medium-hard layer. But the melting point of the wax is about 75℃ (167°F) so still not perfect for very hot liquids. You definitely can’t brew your tea directly into a cup finished this way, not until the oil cures completely after 2-4 weeks. I tested it by brewing a cup of espresso which seems to be fine, the temperature of the coffee is far below 75℃ (167°F) when it reaches the cup. Using a spoon for eating soup also works fine, because the soup needs to be below 45℃ (113°F) for me to be able to eat it. olive wood spoon with hydrophobic layer sycamore coffee cup with hydrophobic layer After usage, the wooden spoon and cup could be easily washed with warm water and soap, the wood didn’t stain at all. The wax still worked as I could see the water droplets sliding off the wood. sheen and appearance Visually, it doesn’t leave a very strong sheen. It has a soft waxy look, instead of the shimmer of oiled wood. That’s because wax doesn’t follow the micro porous surface of the wood, it solidifies into a smooth surface. So instead of scattering light you get mirror-like reflections creating that glossy wax look. Commercial finishes use flattening agents like fumed silica to get around this, but I’m trying to use as little additives as possible for now. The look also comes from the carnauba wax being softened by the beeswax and lanolin which also translates into a softer top layer. natural resins The woodworking world has moved on almost completely from natural resins. Almost every finish is either based on polyurethane or epoxy resin if you’re looking for a harder top layer. At best, you’re looking at a shellac layer which doesn’t mix with oils. Thankfully oil painting forums and blogs still have a trove of information on tree resins, drying oils and siccatives. damar resin crystals That’s how I found out about damar resin, a semi-hard resin extracted from Dipterocarpaceae trees growing in southeast Asia. It is a clear resin, melts at about 120℃ (248°F) which is easily attainable in a microwave, and has a bit of flexibility so that it doesn’t create a cracking layer on wood. It can be found in the form of crystals or pellets in art shops and it’s pretty cheap. People dissolve it in turpentine to create varnishes, but I wanted to incorporate it into my oil-wax blend through heat instead of solvents. experiment #3 Ingredients: 40g pure tung oil 7g carnauba wax flakes 2g beeswax 1g lanolin 2g damar resin 5g coconut oil 2 drops of MnZrCa drier 2 drops of vitamin E To increase sheen and make the top layer stronger, I melted damar resin crystals directly into oil, without any solvent. I also added a very small amount of manganese, zirconium and calcium drier to make the curing faster, and some vitamin E to increase shelf life. Process: Add the carnauba wax, beeswax and lanolin into the tung oil in a glass jar and microwave in increments of 30 seconds until everything is liquid. Add the damar resin crystals into the coconut oil and microwave in increments of 10 seconds until everything is liquid. Let the mix cool down for about 30 seconds then add the drops of MnZrCa drier and vitamin E. Stir until the mix is homogenous. Pour the damar resin-oil blend into the tung oil blend and stir until homogenous. Let cool down until solid. Result: Medium-soft paste that can be scooped relatively easily from the jar with a finger or rag. Spreads easy enough onto the wood surface. Smells the same as the last experiment: nutty, vanilla and honey scent, faint enough to not bother anyone. The nasty smell of the drier doesn’t transfer into the blend because its quantity is too small. The resin has no smell in its solid form. After covering the whole wood surface, I used a heat gun to liquify the finish and enhance wood fiber penetration. The paste melts very easily and the oil in it gets absorbed quickly in the wood pores, leaving the wax and resin to solidify into a very thin layer on top. After that there was almost no excess left and I buffed the top layer to a satin look using a paper towel. Curing: The tung oil inside the wood cured in about 48 hours. This can be verified by melting the wax layer with a heat gun then removing the liquid wax with a rag. The bare wood then can be sanded with 240 grit to check if the wood dust falls off easily. If the wood dust seems wet and gums on the sandpaper, the oil is still not completely cured. Look: The satin sheen looks better because of the resin. Regions with medullary rays look wavy and move in the light, regions with straight fibers look exactly between glossy and matte. Osmo Polyx Oil still has a prettier shimmer that this finish doesn’t achieve, I’ll look into adding trace amounts of fumed silica to see if it makes the finish settle better into the wood fiber microstructure and scatter light instead of being more on the smooth glossy side. The fumed silica might also help with adding shear thinning capability so that the paste is even easier to scoop from the jar without softening it. notes on added ingredients metallic driers The metallic driers are probably the first thing that jump out, because we always read about how bad they are when seeing people talk about natural wood finishes. Not all driers are bad, and it depends on things like quantity, leaching probability and type of metal used. Sure, lead and cobalt driers are out of the question, I don’t want them anywhere near my food and beverages no matter how small the risk of leaching. They contain heavy metals that can have catastrophic effects if they accumulate in your body. I use a drier containing manganese neodecanoate to speed up surface-drying, and zirconium and calcium salts for through-drying. Zirconium and calcium are known to be safe. Zirconium is the same metal that dental implants are usually made of, even ceramic knives are made using zirconium, it seems to be a pretty inert metal when it comes to contact with our body. Manganese is toxic if inhaled or ingested in high amounts, but the quantity of manganese in 2 drops of drier is so incredibly low you could take them orally and not feel any effect other than the horrible solvent taste. That, combined with the fact that the drier disperses throughout the oil makes the quantity too low to matter. The strictest Tolerable Upper Intake Level for manganese is set to 8 mg/day. In fact we get some of that quantity daily through food that is enriched with vitamins and minerals. If the drier were to ever leach completely from the finish, one would get less than 0.1mg per lifetime of object. You’d probably have to eat the spoon completely for that to happen, in which case the quantity of ingested wood might become more of a problem. But the whole food-safe standard on drier based finishes is actually based on the fact that a cured finish traps that metal into an inert polymer which can’t leach into food. Just like Teflon pans or aluminum and copper pots, everything is safe until the layer chips off somehow. I compromised on having a quantity small enough to not pose any safety risk and that wouldn’t shorten shelf life by making the oil cure inside the jar, but high enough to make the oil cure in 48 hours instead of 2-4 weeks. This, in my opinion, makes the finish safer by being more predictable in curing time, and minimizing the probability of food contacting uncured tung oil. coconut oil and vitamin E Because damar resin melts at 120℃ (248°F), tung oil is not suitable for melting the resin directly into it because tung oil can gelify at high temperatures, hindering its curing capabilities. That’s why I melt the resin crystals in a small amount of coconut oil, which has a high enough smoking point of 180℃ (356°F), smells amazing and doesn’t go rancid easily. oil smoke point chart I added 2 drops of vitamin E (aka tocopherol) because it is a powerful antioxidant and will lower the chance of ingredients like coconut oil going rancid. It can also lower the chance of the tung oil curing inside the container by inhibiting the autoxidation reaction. This is the current formulation that I’m using for finishing my wooden spoons and cups, and which I’m selling on my online shop in Romania. experiment #4 to be continued…

8th Dec 2025 • 1 votes
Making my own sharpening blocks

When I started wood carving, the only sharpening method I remembered was from seeing my mother use some kind of smooth broken stone that she passed over the length of the knife blade before sacrificing a chicken. I also remember seeing my father use a very coarse stone wheel placed on a motor shaft which threw many sparks when he sharpened some large axe for splitting wood. I had neither of those around anymore in my rented place in the city so I jumped headfirst in the mind numbing and sometimes esoteric art of getting a sharp blade. crude sharpening methods that my parents used Carving knives The first blade type I had to sharpen was for my BeaverCraft carving knives. They fortunately came with a strop, basically a plywood base in the form of a paddle, with leather stuck to it on both sides, and a green waxy bar. Unfortunately I had no idea what to do with it. BeaverCraft carving knive set Stropping is, at the most basic level, dragging the blade back and forth on a semi-hard surface (like leather). Clean leather won’t make your blade sharper though, and that’s why you have that green bar which you need to rub onto the leather. It contains a fine abrasive that slowly removes tiny amounts of steel from the very tip of your blade to make it sharper. Reading about stropping and watching videos about it gets into a lot of debate on: how often to strop on what angle should the blade be held on the leather should you move from the heel of the blade to the tip, or the reverse can you strop too much and round the apex what is the green compound for and how should it be applied And so on. It’s hard to find much exact science on this, everyone figures it out as they go, and experts share their beliefs based on their extensive experience. I settled on stropping after every 15 minutes of constant carving, but I had no idea what I was doing or if I got any result. That’s one thing I learned that would have relieved me of much frustration at the start: find a way to test the outcome of your motions, otherwise you’ll just keep repeating things that don’t work And it also applies to programming: so many times I’ve seen colleagues writing code but with fear and uncertainty as they didn’t know that what they did would have the expected result. And only because they didn’t know how to test how the code would work. For example they were backend programmers, that only knew how to run their backend python server, but had no idea how to run the whole stack to check if the front end did work correctly with their changes. Or app devs who didn’t know how to use a local database, and instead always feared release day because of how their changes will impact the production db. Nowadays I know how to test for sharpness. If I want a basic working blade I test if the knife cuts printer paper easily and without tearing it. If I want a carving blade, I check if it pops hairs off my arm easily. And I check that often so I don’t do hundreds of blade passes for nothing. Anyway, back to my carving knives. For a long time I did a pretty bad job at keeping them sharp. I would carve for a while then feel the need to push too much into the blade or get tear out in the wood, then I would move the blade back and forth on the strop a few dozens of times, then getting back to carving I would notice a slight improvement which went away after just a few cuts, then back to stropping a thousand times for nothing. Stropping is not sharpening, and after so many cuts those blades needed serious sharpening which a piece of leather doesn’t do. Well, actually, stropping is sharpening, but it’s a very very fine kind of sharpening. It’s similar to using a super fine grit stone, something between 14000 and 100000 grit. The green paste is a super fine abrasive that does remove metal from the apex of the blade, but so little at a time that I would need to do thousands of passes to get the same result that a coarse grit sharpening stone would do in a few passes. I started looking into sharpeners, and because “stones” felt like something old which only my mother used because she didn’t have any better method, I would look into “modern” sharpeners like: pull-through V-blades honing rods powered belt grinders precision sharpening rigs rolling sharpeners modern sharpening methods I tried I tried a few of those expensive methods, ruined a few blades with them, wasted days on reading reviews and watching tutorials… none of those methods felt like a deterministic way to get a sharp blade every time. The precision sharpening rig is probably the closest to that, but I don’t want to waste precious workbench space on that. Also, I can’t take it with me when traveling. Wood planes and chisels Once I got into larger projects, I discovered how to use wood planes (which seems to be thought of as an antiquated method in Romania, manual woodworking is dead here). I also discovered they need frequent sharpening and their blade shape seemed to be perfect for using a flat surface sharpening method like, you know, a stone. tiny block plane I use for chamfering edges Every woodworker on YouTube showed how to sharpen planes on stones so I thought I should probably get one eventually. But which type? There are whetstones, natural stones, ceramic stones, oiled stones, diamond stones, you name it. After trying a ceramic stone which needed to be soaked in water for 5 minutes before doing any sharpening, and constantly wetted between every few passes, and after making a wet mess and chipping the stone and dreading to sharpen because I had to keep the stone in water, I started looking into drier methods. Diamond stones seemed to be more up my alley: a modern method based on an old, tried and true idea. Whetstones and oiled stones are great as well, I just wanted something with less maintenance and more resistant to abuse. At this point I got sick of wasting money on this hobby, so I got the cheapest set of Chinese diamond plates online. It came in a set of 4 thin steel plates with foam backing, each plate having a different grit: 400, 600, 1000 and 1200. Let’s not get into grit standards, it’s probably FEPA but who knows. And what do you know, it worked, it was easy to use, cheap and no special instructions needed. cheap diamond sharpening plates I also got lucky in finding out about the OUTDOORS55 YouTube channel and the science of sharp website who do microscope analysis of blades and methods of sharpening. They’re getting as close as possible to actually knowing what the heck happens when you move the blade on top of a stone, leather, denim or any type of material people have tried sharpening on. This helped me cut through the bullshit fast and get to a simple working method of getting any blade from rusty to shaving sharp in a few minutes. My own sharpening blocks At this point I had these diamond plates scattered on my workbench, I had a leather strop, a green stropping compound bar that painted my fingers green every time I used it, a round diamond file for my hook knives (because you can’t sharpen the inside of curved blades on a flat stone) and a piece of flexible leather for stropping those hook knives. It worked, but it was a mess. And I also work on the go a lot, I always do some wood project at my parents house, carve some branch at a walk in the woods or need to sharpen both a kitchen knife indoors and a chisel outdoors. I wanted to simplify this, but could not find any ready made product I wanted. So I got to doing what I know best, half-assing an improvised product that works for me but would be ashamed of showing it to anyone. First iteration I cut a piece of 18mm thick beech wood into the shape and size of a diamond plate, then stuck a 600 grit plate on one side, and the leather on the other side. beech block with leather strop This not only got the stone and strop higher up from the table which helped a lot with sharpening long knives, but also put them in the same package which I could carry with me. I called this a sharpening block in my mind. To keep the block from slipping while I moved the blade over it, I cut a piece from a non-slip silicone baking mat. Even the small pressure of the blade makes the silicone adhere well to both the block and whatever surface it’s sitting on. non-slip silicone baking mat piece I still didn’t know what to do if I needed a coarser/finer grit, what to do about the green waxy mess of the stropping compound, and how to carry the hook knife sharpeners. The first sharpening block, beech wood base, diamond plate, leather strop Second iteration This summer, my sister-in-law visited from Italy and asked if I could make a coffee table for her. Timing was tight because I had to make it in a week so she could transport it from Romania back to Italy by car. I had no time to find wood slabs so I got two oak wood panels from a big-box store, glued them one on top of the other to make the table-top thicker, and figured I’ll find some table legs afterwards. I found some 15cm diameter smooth beech logs at a firewood seller nearby. Turns out they were leftovers from a veneer factory that can take a really long and thin log slice automatically, leaving the log core as smooth as a turned piece. Making veneer out of tree logs The table turned out pretty nice and solid, her children love sitting on top of it or hiding under it with their toys. I guess it could also be used for holding coffee cups, they didn’t get a chance to try that yet. oak coffee table with beech wood legs So this left me with some 36mm thick oak wood that I thought could make for a better sharpening block. This time, I glued two coarse and fine grit plates back to back (either 400/1000 or 600/1200) and embedded some neodymium magnets inside the wood to keep the diamond steel plates firmly attached but able to flip easily. oak block with embedded magnets to hold the diamond steel plate For the strop, I didn’t have any leather left and it was crazy expensive, so I looked into alternatives. You can basically use any semi-hard porous surface for a strop, even plain thick cardboard. It just needs to hold the fine particles of the compound and not flex too much when pressing the blade into it. People seem to use leather, denim, cotton, felt, cardboard, balsa wood etc. After building my workbench, and the leg vise for it which needed some rubberized cork for the vise faces, I now found myself with 3m² of that rubberized cork on my hands because I could only buy it in bulk here. I tested the cork for stropping and I was amazed to see it’s even better than leather: it holds more compound which makes it last more the bars of waxy compound break down easier when dragging them across cork, almost like drawing with wax crayons. On leather I needed to heat the bars often. it doesn’t get cut as easily. I would often drag the blade wrongly across the leather, cutting into it. The cork is like a self-healing material where cuts disappear it’s cheaper it can be shaped by sanding, which allows me to give the strop rounded edges that are great for stropping recurve blades (those knives which curve to the inside and annoyingly only make contact with the edge of the strop) it doesn’t dry and curl like leather So I cut and glued a 3mm thick piece of that cork on the other side of the oak base and loaded it with pink corundum stropping compound. Yes, this is the time I discovered the even more esoteric world of stropping, lapping and honing compounds which are not green bars of waxy stuff. We’ll get to that. rubberized cork strop with pink compound The 36mm thick oak gave me enough space to drill 20mm diameter holes with a forstner bit, where I could place a wooden dowel lined with that same cork for honing the hook knives. Another hole was for the stropping compound. stropping compound and round strop embedded in the block I finally reached an all-in-one sharpening block, super stable, with coarse enough grit for getting a bad blade into shape fast, fine grit for sharpening, enough strop for a few years and a way to sharpen recurves and hook knives. Presentation of the oak sharpening block It was a bit too thick though… Stropping compounds The green stuff Most people get a green bar of compound and use it all their life and never think about it. But nooo, I had to do research and see what that compound contains and why is it so waxy and is there better stuff? My understanding is that the green stuff is made of very fine particles of chromium oxide, which is also used as green pigment in cosmetics and painting, and is why it makes everything in your life green when touched. Dialux Vert green stropping compound I had a Dialux Vert bar, and searching for its material safety data sheet surfaces the following: Composition/information on ingredients: So it is made of very fine particles of chromium and aluminum oxide suspended in paraffin wax. Does that mean it can be melted and poured into cylindrical molds? Yes it does! Adding some green flakes in a small cup with walnut oil in it allows it to be melted in a microwave oven. Any type of oil works, I tested sunflower oil, olive oil, coconut oil, mineral oil etc. you just need some kind of non-solid fat to make the paste less viscous when melted. After lining the 20mm hole with wax paper and pouring the melted mix, it solidified as a green cylinder with a chapstick-like consistency, that I could easily get out and apply to the cork. Diamonds In the last decade, diamond pastes and sprays started becoming popular for stropping. diamond powder, paste and spray Diamond powder was widely used for lapping gemstones for a long time, and diamond paste was being used in the dental industry for polishing implants. Eventually people figured they can suspend the very fine diamonds in a sprayable emulsion, advertise it for stropping and ask a ton of money for it. Diamond sprays are not accessible here in Romania, but I was able to get some lapping pastes of 3 microns and 0.25 microns particle size to test. For reference, chromium oxide green polishing compound is usually formulated with 0.5 microns particle size, but that doesn’t mean that all particles are of that size. That’s like the average particle size which gives you an idea of what kind of polish you can expect from it. I admit, I like diamonds. They’re easy to apply by squirting the paste from a syringe, I can throw it in a bag without making a green mess, it does seem to cut metal very slightly faster and lasts a bit longer between applications. But a tiny syringe that lasts 3 months costs more than a green bar that lasts me years. I can’t make this compromise. There are people that bought the diamond powder directly and made their own pastes and emulsions but it all seems too complicated for little gain. I did try some cheap Chinese AliExpress diamond pastes, but for the life of me I can’t figure out how they calculate the grit and particle size. I bought the finest I could find which is listed as W0.5 micron mesh. I don’t know what means but it doesn’t get to a shaving sharp blade easily. Cubic boron nitride Looking into other abrasives, there’s another loved stropping compound: cubic boron nitride or CBN emulsions. It’s impossible to buy that here, but it’s way too expensive anyway and I’m pretty sure the difference would be marginal. People tout that CBN and diamonds are actually necessary for harder steels, but I’ve yet to see that tested. My guess is that it’s just people’s way of justifying the purchase of a new and expensive toy, we’re all guilty of that. Wikipedia says that Cr₂O₃ has a hardness of 8 to 8.5 Mohs which is far higher than plain steel at 4 Mohs and even higher than tungsten at 7.5 Mohs. If it can scratch tungsten, I’d say it can hone a steel knife. Corundum I eventually found a pale pink compound that has the same fine grit as the green one, but without the color problem. 800g brick of pink corundum stropping compound It’s made from corundum, which is aluminum oxide without the chromium. It’s harder than the green stuff (at 9 Mohs), doesn’t leave its color everywhere, and it’s dirt cheap. I bought the Lea Chromax brand and I’m very happy with it so far. I paid €10 for a huge 800g brick that will last me a lifetime. I break small chalk-like pieces from it that I pocket or leave throughout the house to ensure I always hame some on hand. It applies easily to both cork and leather and doesn’t flake off or stick to the blade. I gifted sharpening blocks along with pieces of stropping compound to many friends and relatives and the pink Chromax block still looks as large as when I bought it. Third iteration I am working on a way to merge the all-in-one quality of the thick oak block with the pocketability of the thin beech block. I also got my hands on some narrow 20mm width diamond plates of very fine 3000grit. I find them useful for when I need a very sharp blade for doing finishing cuts on a spoon or when planing dense wood. I’d like to integrate this plate somehow in the block. fine diamond plate of 3000 grit It could maybe be glued to the side of a 20mm thick block of wood, and used hand held instead of on a table. I did try that on the beech block I already had, and it seems to work nicely. It’s not that hard to sharpen freehand and handheld as long as it’s just for doing the last fine honing on an already sharp blade. beech block with the 3000 grit diamond plate on the side The wax paper method for pouring melted compound is also not ideal. I’m thinking that pouring the compound into a chapstick tube would make it easier to use, and be thinner so it can fit inside a hole in the wood block. I also have these round diamond files which are great for sharpening round blades, serrated knifes, drill bits, even some rip cut saws. They come as a double-ended rod with a conical file on one end, and a round + flat file on the other end, and they’re meant to be placed in a pencil-like holder. round diamond file, cut in half I cut the rod in half with an angle grinder as I can’t fit the whole length of the rod inside the wood block. With only half of it, I can drill a 6mm hole in the block to embed the file. I can’t fit the cork lined wooden rod for honing though. I only have 3mm thick cork for now which is way too thick for this use case. There’s this thing called Nanocloth which is like a thin microfiber cloth specifically for CBN and diamond emulsions. It’s hella expensive and I would never buy such a thing, but it gave me an idea: I could try wrapping some thin felt cloth around a 10mm diameter wooden dowel. It should hopefully fit in the block and hold enough compound for stropping. And I guess that’s it, that would be my ideal sharpening method: a compact block of solid wood with 2 wide diamond plates on one face (coarse and fine grit) a cork strop on the other face a narrow diamond plate on the side (very fine grit) an embedded round diamond file an embedded strop rod and a stropping compound, inside the wood block somehow, that’s easy to get out and apply And all that in a cheap package that should cost less than $30 to assemble. I’m still working on it, I prepared some new 18mm oak blocks and I’ll update this post with the results.

11th Oct 2024 • 86 votes
Woodworking as an escape from the absurdity of software

Some of you might remember the legendary comment of Eric Diven on a Docker CLI issue he opened years ago: @solvaholic: Sorry I missed your comment of many months ago. I no longer build software; I now make furniture out of wood. The hours are long, the pay sucks, and there’s always the opportunity to remove my finger with a table saw, but nobody asks me if I can add an RSS feed to a DBMS, so there’s that :-) I say legendary because it has over 9000 reactions and most are positive. There’s a reason why so many devs resonate with that comment. A lot of us said at some time things like “I’m gonna throw my laptop out the window and start a farm”. Even my last team leader sent me a message out of the blue saying “I think I’ll run a bar. I want to be a bartender and listen to other people’s stories, not figure out why protobuf doesn’t deserialize data that worked JUST FINE for the past three years”. You know the drill, sometimes the world of software development feels so absurd that you just want to buy a hundred alpaca and sell some wool socks and forget about solving conflicts in package.json for the rest of your life. I went through those stages too: when the Agile meetings got so absurd that we were being asked to estimate JIRA task time in T-shirt sizes, I quit my well paying job so I can spend that time creating macOS apps. Recently, when people started coming with so many unrealistic and absurd expectations and demands about my apps, I think I’ll soon quit software development altogether. It’s just not making sense anymore for me. A bit of history Most of my pre-college time was spent on things I didn’t want to do. I had a bit of childhood, but then I started going to school 6 hours per day, with 1-2 hours spent on commute after 5th grade. I only liked the 10-minute breaks between classes where I played basketball or practiced parkour. Every day after I came back from school, I had to work in agriculture, either out in the field with crazy winds and sun and UV radiation, or inside the greenhouse where it’s either a 50°C sauna or a muddy rainforest. I was very bad at every job I was given, but it’s what my parents did for a living and I had to help them, no questions asked. The few hours that remained, usually very late at night, tired both physically and mentally, I spent practicing acoustic guitar, doing bodybuilding exercises, writing poetry or drawing portraits. me, ages ago, playing a classical guitar on someone's old couch I almost never did homework or memorize whatever had to be memorized for the next day of school. I just couldn’t justify spending those few hours I had left on even more stuff I did not want to do. When I found my liberty in college, hundreds of kilometers away from my parents, it’s like something clicked. I suddenly became incapable of doing work that I found meaningless. Failing classes became acceptable, quitting jobs was something I did with little remorse if I felt I wasn’t helping anyone with the work I was assigned, and bureaucracy became a disease I had to avoid at all costs. I still washed the dishes though. Cleaning and other “chores” never felt meaningless for some reason. The first wood thing I did … was a chess board and piece set. With magnets inside them. Where the pieces look nothing like ordinary chess pieces. chess board, first iteration I was trying to get the pieces to snap into place in a satisfying way, and make sure the game stays that way when kids or dogs inevitably bump the table where the board sits. You know how Magnus Carlsen always adjusts his pieces so meticulously before a game? Well I have half of that obsession as well so I wanted to avoid doing that. Magnus Carlsen adjusting his pieces before a game pawn snapping into its square because of the magnet inside How it was done I started with a cheap but hefty pine board which I rounded with a lot of sandpaper. Then I asked my wife to help me colour in the darker squares because I’m pretty bad at colouring inside the edges (both literally and figuratively). We used some wood floor markers for that and the colour seems to be holding well. Most chess board builds you see on YouTube are done by gluing squares of different wood species with alternating colors, but I had neither the skill nor the tools to do that. Then I drilled holes for the super strong neodymium magnets from the underside of the board, having to get really close to the top side without passing through. I failed on two squares, but some wood putty took care of that. sculpting chess pieces with my dremel on the balcony I spent a few sunny days on the balcony sculpting the pieces with a badly sharpened knife and my Dremel. This was quite satisfying, there’s something really nice about seeing a non-descript rectangle take the shape of a little horse in your hands. I mean knight, but in Romanian that piece is called “horse”, and I really don’t see any knight there. chess board, start to finish Regarding the design, I got some inspiration after seeing these modernist chess sets, which not only looked beautiful in my eyes, but also had these geometric shapes that didn’t need that much sculpting to replicate. I found ready-to-buy spheres and cubes of wood at a craft shop around me (which took care of pawns and rooks), and the rest were carved out of rectangles and cones of wood. Modernist chess set designs Kaval Two Octobers ago, a Romanian music band called Subcarpați was holding a free “make a Kaval with your own hands” course, where a flute artisan taught the basics of his trade for a week. The Kaval or “caval” is a long flute with 5 holes and a distinct lower register where notes can sound melancholic and coming from far away, as opposed to the thin cheerful sound of the small shepherd flute. Kaval sample in G minor Ever since I bought my first Kaval, I always wanted to learn how to build one myself. It’s one of those trades where there’s very little info on the internet, so it feels almost mystical compared to what I’m used to in programming. I would also have the chance to walk home with the finished flute, so of course I went to the course. Making my own Kaval, in B minor I loved the fact that we worked in teams of two, and that everything had to be done by hand with no power tools. Even the long bore through the 70cm branch of elder tree had to be done with a hand drill, taking turns to rest our hands. The artisan had been a shepherd himself since childhood, and taught himself with a lot of trial and error about how to build good sounding flutes and how to make the holes so that the flute stays in tune. But he didn’t know why the holes should be at those specific distances or why the wood tube should be of that specific length for each scale. I wanted to know those things, because I had an idea of making a universal Kaval that can play in any scale. You see, if you want to play on top of songs in various scales, you need a Kaval made for each specific scale. So you’ll need an A minor flute, and a B minor one and a C minor one and so on, for a total of 12 different flute lengths. I eventually found info on how a flute works by thinking about it as an open or closed tube where the vibrating air creates nodes and antinodes that should coincide with the hole position. At the moment I’m still studying this and working towards my “universal flute” goal. The physical world has no undo button A few days ago I was walking with my dog around the university and I saw an elderberry tree with a really straight and already dead branch. I thought that might be great for trying to do another kaval, so I went back home to grab my folding saw to cut this branch. I brought it home, cut it to about 78cm for an A minor kaval, straightened it in the vise and started boring a hole through it. I used a 12mm drill first because elder branches have this spongious core that was exactly 12mm in my case. I was able to drill end to end in less than 10 minutes, first time I managed to do this successfully. Drilling a hole through an elder tree branch For such a large flute, you usually need a larger 16mm or 18mm hole to get enough volume, so I went ahead and used the 16mm drill to enlarge the hole. After about 10cm, things started squeaking loudly and smoking so I got the branch out of the vise to inspect it. Because the branch was not completely straight, the drill came out of the side and it was hitting the harder wood of the vise. Damn.. another wasted branch, there’s not a lot of straight material around me in the city. Man, how I wish I could hit undo and just experiment with the good 12mm hole. This reality hit me many times while working with wood in the past 6 months. I didn’t even realise that my mind got so used to having git and backups and Cmd-Z that those expectations transferred to the physical world as well. Move fast and break things is no longer a good mantra for me. I gave the broken branch to my dog, she loves chewing large sticks and got a real kick out of it. Her face definitely said BEST STICK EVER!! for about 5 minutes of pure bliss, so apparently nothing was wasted in the end. What does this have to do with software? For the past 10 years I lived in rented apartments, usually at the 3rd or 4th story with no access to a courtyard. I was never able to get used to that, given that all my childhood I lived and played in a 2000m² courtyard, on a road where there were more slow horse carriages than noisy cars. This year I moved into a rented house with a tiny but welcoming garden and a bit of paved court and only now I notice the effect this has had on my mind and behaviour. I develop macOS apps for a living, and there are some unhealthy things in this field that piled up over the years. I get a lot of messages in a demanding and negative tone, and because walking outside the apartment meant unbearable car noise, obnoxious smells and zero privacy, I always defaulted to simply acting on the feedback, putting up with it and working long hours into the night, instead of going for a walk to calm down. A few months ago, the most absurd demands started coming up for my apps: things like “why does your app not control the volume of my <weird sound device>? why don’t you just do it, people pay you for it” when the app in question is Lunar, an app for controlling monitor brightness, not sound devices. Or “why do you disable your apps from working on Windows?”, or “make Clop compress text and copy it to clipboard” (where Clop is my app that automatically compresses copied images, videos and PDFs, I have no idea what compressing text even means in that context). But this time, I was able to simply walk out the front door, grab a branch of beech wood, and, because I remembered my wife saying we forgot to package the french rolling pin when moving, I took out my pocket knife and started carving a simple rolling pin for her. It was so liberating to be able to just ignore those messages for a while and do something with my hands. the rolling pin is such a simple tool and to this day, my wife still tells me how much she likes it because it's exactly the right length and thickness for making her tasty egg noodles.. and best of all, it was free I understand that those people don’t know better, and they would have no idea that there’s no checkbox where you can choose whether an app works on macOS, Windows or Linux. I understand how if the app does something with audio volume or compression, some think that it should do everything related to those workloads, even if it’s completely outside the scope of the app. But the combination of the negative tone and getting message after message, some people being so persistent that they insist on sending me those messages through all possible mediums (email, Discord, Twitter, contact form, they’ll find me everywhere), makes it hard to just ignore them. There’s also this oily smell of AI and machine learning in the tech atmosphere, where I no longer feel relevant and I seem to have stopped caring about new tech when I noticed that 8 in 10 articles are about some new LLM or image generation model. I guess I like the smell of wood better. Side tangent on privileges of being a software dev I know I’m privileged to even be able to have the choice of what to do with my time. I got lucky when I chose a computer science university at the right time which allowed me to progress towards a huge semi-passive income in the last 10 years. that doesn’t mean I didn’t work my ass off, but luck plays a huge role too I got “lucky” to have my mind traumatised into some kind of OCD-like state where I hate leaving a thing unfinished. So I plow through exhaustion, skip meals, miss house chores and annoy dear people around me because I know “I just need to fix this little thing” and I’ll finish this app/feature/task I started. Even though I also know there’s no real deadline and I can leave it half-finished and the world won’t end. But even if it sounds annoying for a person like me to whine about how I don’t feel good or I feel burnt out, the privilege doesn’t negate the feelings. The regression to the norm will make everyone, rich or poor, get used to the status quo and complain about every thing that’s just a little worse than their current state. That’s happiness and sadness in a nutshell. I’m also vaguely aware that software dev as we know it is about to disappear soon, and I got tired of learning the newest thing just to have it replaced next year. I got tired of back pain and chronic finger pain from so many hours of sitting and typing, I’d rather have pain from work that also builds some muscle. And I got so tired of everything being online, immaterial, ephemeral and lonely, like indie development tends to be. Woodworking with cheap tools and free wood This house we rented is small and the owners had to fit the bedroom upstairs. I really don’t like climbing stairs up and down, especially when I have to let my dog out three times per night. So we gave up a room and started furnishing our own bedroom downstairs. I didn’t want to buy bedside tables for the price of the bed itself, so I thought I could maybe make by own. I’m not yet skilled enough to build my own bed though, so we had to buy that. Another day on a walk with my dog, I noticed that some trees were getting trimmed in the vicinity of our house and there were a lot of white birch branches on the side of the road. I said why not? ¯\_(ツ)_/¯, grabbed some branches and walked like a lunatic with white long sticks dangling up and down and a black frenetic dog zig-zagging left and right, all the way home. I had another small pine panel left from that chess project so I started thinking about the simplest way to turn what I have into a bedside table. pine board with birch branches I used low-grit sandpaper to give the board some nice round corners because I love squircles, swallowed about a spoonful of sawdust because I couldn’t find any breathing mask left, criss-crossed 4 branches in a way that would give a stable base, and screwed them to the underside of the board with long wood screws. The legs would wobble around though, so I drilled small 3mm holes into each branch where they met in the middle, and weaved a florist wire through them to keep the table steady. Bedside table, made out of pine with birch legs The laptop bed table After I’ve shown the bedside table to a friend of mine, he said he also needed a laptop table for those mornings when he’d rather not get out of bed. I wanted to say that’s not very healthy, but what got out instead was sure thing, I’ll do it!. Oh well.. I still had the large desk top I glued from smaller beech boards, on which I worked for the past 4 years. It stayed unused currently, so I cut part of it and built this cute thing: cute but heavy laptop table, made out of glued beech wood You’ll notice three defining features that every laptop table should have: a hole for a charging cable a carved coaster for the coffee cup a mildly surprised face? 😦 To tell the truth, all those are side effects of me drilling holes where there should be no hole, and dropping the board on the ground multiple times because my workbench was not large enough. All the things that could go wrong, went wrong with this table. I hid the defects by turning them into features. The whole truth actually is that the table looks nothing like what I planned. I bought these nice hidden brass cylindrical hinges to make the table foldable. That way, you could fold the sides flat inside and use it as some kind of armchair desk if you wanted. Brass hinges I wasn’t able to drill the correctly sized or positioned holes for the hinges because I still lack a lot of knowledge and skill in working with wood. So after losing my temper with the frickin’ hinges that still didn’t fit after a full day of drilling and chiseling, I glued the sides and inserted 2 trusty long wood screws per side, which I patched with a glue gun that made the screw holes look like eyes. After I also carved the handles, the table grew kind of a personality of its own, as you can see above. Why didn’t I do some wood joint, like a dovetail instead of ugly screws and glue? Because I had no idea they existed. Also, I wasn’t even able to fit a simple hinge, I would’ve probably never finished this table if I tried learning wood joinery on it. This reminds me of how whenever I did pair programming with a colleague, I noticed how they were doing some “nonoptimal” action and I would say: Why don’t you just use ripgrep instead of sifting through all these files? Because they don’t know it exists, stupid. Or because they just want to get this thing done and move on, they don’t grep files all day like you do. Learn from my mistakes, don’t think you know better or assume the other person needs your way of doing things. Maybe let them know after the fact, in a short message linking to the tool, and let them try it in their own time. But in my ignorance, I seem to have chosen a good enough joining method. As you can see in this wood joinery comparison, 5cm (2inch) screws can hold more than 50kg (110lbs) of force, and I used even longer screws so I think it’s going to hold a 3kg laptop just fine. Oh right, forgot about this little detail.. I also added a cork pocket for holding a notebook, tablet, phone etc. which I lined with a microfiber cloth on the inside for strength and sewn to the wood with that leftover alpaca wool for style. Cork pocket sewn to the table side The bookshelf without books Large bookshelf (200x120x40 cm), made out of pine boards While we were stuck in the apartment in the 2020 pandemic, me and my wife bought a lot of stuff that we thought would help us learn new things and start new hobbies. I thought I’m going to build smart LED lighting all my life and my wife would become a professional wool knitter. We were losing our minds, for sure. So now we were stuck with crates of stuff we haven’t used in years, and didn’t want to start unpacking them around the house. The clutter that followed after the pandemic, tired our minds just as much as the lockdown itself. We dumped the crates on an unused stairway spot, and I thought that a bookshelf as large as that spot would clear the clutter. Before: clutter | After: organized clutter But I could not find any bookshelf that large, certainly not for cheap. So I traced a few lines in Freeform, took some measurements, and ordered a bunch of large pine boards and a ton of long screws. I also ordered the cheapest portable workbench I could find ($30) that had a vise, so I can stop making sawdust inside. A few days later, I got to sawing the shelves to size with my cheap Japanese pull saw I bought from Lidl years ago. Hint: Hand sawing a long wood board with no skill will certainly end up with a crooked edge. Stacking up 5 boards one on top of the other will still end up crooked. Uhm, I guess the hint is, buy a track saw, or make sure the crooked edge isn’t visible. Hand sawing a straight long edge is not easy. making the bookshelf My wife helped a lot with measuring and figuring out where to drill holes and place the screws, while my dog inspected the work regularly to make sure the defects were hidden correctly. It took two days of screwing.. erm.. driving screws, I mean. But in the end we got the result we wanted! And I got sores in my right arm for days, driving those long screws is harder than I thought. The desk that became a workbench In the thumbnail of this post you can see the current “workbench” I use, which is basically that $30 vise workbench I bought for the bookshelf, with the top of my previous “coding desk” attached in the front. my current workbench In the image you can see (bottom-left to top, then right): the cheapest block plane I could find ($8) a red no-name plane I found in the shed of that 100-year old house that we never finished rebuilding because of legal reasons an axe I found rusted and partly broken in the same shed, on which I learnt how to sharpen and restore axes a folding japanese pull saw that I take everywhere with me some grip blocks on which I place boards for sanding a bottle of Osmo Polyx oil I use for finishing (this is the Rubio Monocoat for poor people) a set of carving knives from Beavercraft (really good and they were available at a nice discount) a combination square (tucked somewhere at the top of the bench) a branch of elder tree, which is prepared for drilling a hole through it for making a kaval I also own some no-name chisels that work well enough for now and some card scrapers that I still struggle sharpening. The only power tools I have are a Makita drill and a random orbit sander on which I did spend some money, an old circular saw I found in that same old shed (it was good enough to cut miters on that laptop table) and a Dremel I use rarely because I don’t like its power cord. I prefer battery powered tools. The window bench Our dog Cora loves sitting at the window, growling at old people and barking at children passing around. Yeah, she’s terrified of children for some reason. But the window sill is not wide enough and her leg kept falling with a “clang” on the radiator below. So I widened it by placing two glued up boards of pine on top of the radiator, that I planed and smoothed beforehand. Cora sitting at the window Cora at the window, with the widened sill This is when I learned that a hand plane is not some antique tool that nobody uses anymore, but a quite versatile piece that can easily smoothen grain where I would waste 5 sheets of sandpaper and choke on sawdust. I had to still let the heat radiate somehow, so I drilled large holes with a forstner bit, but I also blew the grain fibers on the underside because I had no idea of this possible problem. Turns out there is a simple solution to drilling large holes without ripping the fibers: Drill a small 3-6mm hole in the center with a normal wood drill, all the way to the other side (this will help you see where the forstner bit should be placed from both sides of the board) Place the forstner bit in the hole (this also helps with keeping the bit centered) and drill the large hole, stopping midway through the board Turn the board around and repeat step 2 until you meet the other end of the hole We also wanted to sit with Cora and there was not much space between the bed and the radiator for a regular chair, so I built a narrow bench. I used another two pine boards of the same size, but this time glued them on the side to create a wider board. For the legs, well the tree trimming continued throughout the spring, so one day I found some thick cherry branches which I brought home, scraped the bark from them, then attached them to the bench using screws from the top side. I was ok with a rustic look so I didn’t spend much on finishing, patching holes, or even proper wood drying. I did use the hand plane to chamfer the edges though, I love taking those thin continuous wood shavings from the edge. Window bench, in the morning sun The trunk coffee table Coffee table made out of a beech log We recently visited my parents, and loved how the grass finally started growing in some spots where their house and court renovation was finished and was no longer spewing cement dust. It was an abnormally sunny April and I wanted to chat with them at a coffee outside in the early morning before they started the field work, but there was nowhere to place the coffee outside. First world problems right? If you’ve read about The tail end, you might already understand why a trivial thing like coffee time with my parents feels so important to me. So one day, while walking on a gravel road near their house, I noticed one neighbour had these huge logs of beech that were recently cut. I thought that would be easy to turn into a small exterior coffee table, so I went to ask if I could buy one. Well I kind of had to yell “HELLO!” at their gate because I didn’t know their name, and did that a few times until a seemingly sleepy old man in pyjamas (it was 5 in the afternoon) appeared at the front door asking what I want. I asked how much he’d want for one of those logs, but he just said to get one, no money needed. Ok, there’s no point in insisting, I chose a wide enough but not too wide log, because these things are heavy and I wasn’t sure I could lift it, and rolled it slowly back home. I didn’t have my usual tools at my parents house, so I improvised. I found a battered cleaver which my dad used for chopping kindling for the barbecue. I sharpened it as well as I could, then used a hammer to roll a burr on the back of the cleaver that I could use for scraping. Scraping the bark off the beech log Beech wood has such a smooth hard wood under the bark that it didn’t even need sanding. I used my dad’s power planer to smooth out the top and make a quasi-flat surface then finished it with some walnut oil and it was (almost) ready! Because the wood was so green, it was certain that it will crack and roughen as it dried. So I cut a groove and wrapped a flat iron band around the top to keep it from moving too much. The bottom can expand as much as it wants, I’m actually quite curious to watch the table morph throughout the summer as we use it. The orchard bench Bench made from reclaimed wood, for my parents-in-law orchard Because we were born in villages that aren’t that far apart, me and my wife always visit both our parents in the same trip. This time when I got to my parents-in-law, I took a stroll through their little orchard. They added new trees this year! I can’t wait to taste the large apricots. What struck me as odd about the orchard was that there was no patch of grass to lay on. They like digging up the soil every year, and leaving it like that: an arid looking patch of land made of dry dirt boulders. I thought a bench would be a good solution and what do you know, there was an old broken door thrown in the firewood pile just outside the orchard, that had the perfect length and width for a bench. I forgot to take a photo of the door, but it looked kind of like this one, only worse and with a large rhomboid ◊ hole at the top. old broken wooden door I got to work immediately, dismantling the door piece by piece and pulling out nail after nail (they really liked their nails in those old times). I was left with two long and narrow wooden boards, a pile of rotten wood and two pocketfuls of rusted nails. I sawed the broken ends of the boards, then I used my father-in-law’s power planer (do all Romanian dads have a power planer or what?) to remove the old gray wood from the top, bottom and sides to get to the fresh wood below. There were a lot of holes and valleys so I had to scrape them by hand with sandpaper rolled around a screwdriver. This took a few more days than I expected, but I eventually got two cleanish boards of.. fir? pine? No idea. I used a velcro sandpaper attachment for the battery powered drill to sand out the rotten sides and give the boards a curvy and smooth live edge. Curvy edge on the bench, made by sanding out the rotten wood For the legs, I stole some more firewood from my in-laws pile, where I found some thick branches of unidentified species that were roughly the same length. Stripping the bark with an axe made them look good enough so I screwed them in at the four corners of the board. The bench was wobbly with just the legs, so I strengthened it sideways by adding shorter and thinner branches of more unidentified wood between the legs and the center of the board. I had to do something with the rhomboid ◊ hole, so I filled it with a square 4-by-4 salvaged from a recently dismantled shed, and now the bench has 5 legs. Instead of sawing the leg to size, I left it protruding above the bench and placed another thick salvaged board on top of it to serve as an arm rest, or coffee table, or a place for the bowl of cherries. For the finish, I burned the bench and the bottom of the legs to get a honey-brown aspect and to make it water resistant. I put a very thin layer of whatever wood lacquer I found in my in-laws shed, just for resistance because I don’t like glossy wood. Side photo of the bench for a better view of the legs Other small wood things Water glass shelf We don’t have much space on the current eating table, so I built a two-shelf stand where we can place the always present water filter jug and the glasses and free up some of the center space. It’s incredible how strong just a few screws can be. Table shelf for holding water filter and glasses Kaval stand I thought I should finally do something about the kavals always rolling around on some table or couch throughout the house, so I made a stand from long thin wood boards glued on the side, and finished it with sunflower oil to give it a golden/orange colour. This way I can always expand it by adding more boards to the side if I want to add more flutes. Stand for holding my kaval collection Sharpening block I need to sharpen blades almost daily, be it the pocket knife, axe, plane blade or chisels. So I made a custom sharpening block with the perfect tools for my sharpening technique. Sharpening block, diamond plate with leather strop on a beech base It has a $5 diamond plate with 600 grit on one side and a $5 leather strop (a piece of leather belt might work just as well) on the other side. I attached the leather with two small screws at the top so I can take it out easily if I need a flexible strop for my carving gouge for example. It is loaded with 0.25 micron diamond paste which can be found for cheap at gemstone cutting online stores (the knife-specific pastes are a lot more expensive and I’m not sure why). To be honest, a $0.5 green compound (chromium oxide) works just as well for stropping, that’s what I used before and still use for my detail carving knives. It gives a smoother edge than the diamond, the disadvantage being that it needs to be re-applied more often on the leather and that you need a bit more blade passes to get the same result. The diamonds seem be cutting faster, but really not much faster. A bit of a tangent on the sharpening topic I went through all the phases with sharpening tools. I’ve used water stones, natural stones, ceramic stones, pull-through carbide sharpeners (don’t use these), powered belt sharpeners, wheel sharpeners. Aside from the pull-through sharpeners and the steel rods, all the others work just as well with the right technique. I settled on the diamond plate because they’re cheap, portable, stay flat, need zero maintenance, and can cut through any type of metal. Paired with a leather strop, for me it’s the simplest way to sharpen. I recommend this OUTDOORS55 video for a no-bullshit sharpening tutorial and the Science of Sharp blog if you’re curious what the different sharpening techniques do to an edge under a microscope.

28th Apr 2024 • 75 votes
The complex simplicity of my static websites

It was the spring of 2014, over 9 years ago, just 6 months into my first year of college, when my Computer Architecture teacher stopped in the middle of an assembly exercise to tell us that Bitdefender is hiring juniors for Malware Researcher positions. I had no idea what that is, but boy, did it sound cool?… I fondly remember how at that time we weren’t chasing high salaries and filtering jobs by programming languages and frameworks. We just wanted to learn something. As students, we needed money as well of course, but when I got the job for 1750 lei (~€350), I suddenly became the richest 18 year old in my home town, so it wasn’t the top priority. And we learnt so much in 2 years.. obscure things like AOP, a lot of x86 assembly, reverse engineering techniques which dumped us head first into languages like Java, .NET, ActionScript? (malware authors were creative). But most of all, we did tons of Python scripting, and we loved every minute of it. It was my first time getting acquainted with fast tools like Sublime Text and FAR Manager. Coming from Notepad++ and Windows Explorer, I felt like a mad hacker with the world at my fingertips. I’m known as a macOS app dev nowadays, but 9 years ago, I actually started by writing petty Python scripts which spurred the obsessive love I have nowadays for clean accolade-free code and indentation based languages. What does all that have to do with static websites though? Pythonic HTML Well, 5 years ago, when I launched my first macOS app, I found myself needing to create a simple webpage to showcase the app and at the very least, provide a way to download it. And HTML I did not want to write. The XML like syntax is something I always dreaded, so overfilled with unnecessary </> symbols that make both writing and reading much more cumbersome. I wanted Python syntax for HTML so I went looking for it. I went through pug… 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 doctype html html head title Lunar - The defacto app for controlling monitor brightness meta(itemprop='description' content='...') style. a.button { background: bisque; padding: 0.5rem 1rem; color: black; border-radius: 0.5rem; } body { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; } body h1(style='color: white; font: bold 3rem monospace') Lunar img(src='https://files.lunar.fyi/display-page.png' style='width: 80%') a.button(href='https://files.lunar.fyi/releases/Lunar.dmg') Download pretty, but still needs () for attributes, and I still need accolades in CSS and JS then haml… 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 !!! %html{lang: "en"} %head %meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ %title Lunar - The defacto app for controlling monitor brightness %meta{content: "...", itemprop: "description"}/ :css a.button { background: bisque; padding: 0.5rem 1rem; color: black; border-radius: 0.5rem; } body { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; } %body{style: "background: #2e2431; min-height: 90vh"} %h1{style: "color: white; font: bold 3rem monospace"} Lunar %img{src: "https://files.lunar.fyi/display-page.png", style: "width: 80%"}/ %a.button{href: "https://files.lunar.fyi/releases/Lunar.dmg"} Download even more symbols: %, :, => and / for self-closing tags …and eventually stumbled upon Slim and its Python counterpart: Plim 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 doctype html html lang="en" head title Lunar - The defacto app for controlling monitor brightness meta itemprop="description" content="..." -stylus a.button background bisque padding 0.5rem 1rem color black border-radius 0.5rem body display flex flex-direction column align-items center justify-content center text-align center body style="background: #2e2431; min-height: 90vh" h1 style="color: white; font: bold 3rem monospace" Lunar img src="https://files.lunar.fyi/display-page.png" style="width: 80%" a.button href="https://files.lunar.fyi/releases/Lunar.dmg" Download ahhh.. so clean! Here’s how that example looks like if I would have to write it as HTML: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <!DOCTYPE html> <html> <head> <title>Lunar - The defacto app for controlling monitor brightness</title> <meta itemprop="description" content="..."> <style> a.button { background: bisque; padding: 0.5rem 1rem; color: black; border-radius: 0.5rem; } body { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; } </style> </head> <body> <h1 style="color: white; font: bold 3rem monospace">Lunar</h1> <img src="https://files.lunar.fyi/display-page.png" style="width: 80%"> <a class="button" href="https://files.lunar.fyi/releases/Lunar.dmg">Download</a> </body> </html> not particulary hard to read, but writing would need a lot of Shift-holding and repeating tags The thing I like most about Plim, and why I stuck with it, is that it can parse my other favorite symbol-hating languages without additional configuration: Python for abstracting away repeating structures Stylus for writing style tags CoffeeScript for the script tags Markdown for long text content Here’s a more complex example to showcase the above features (might require sunglasses): example of writing a HDR page section, similar to the one on lunar.fyi 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 ---! # use Python to generate the dynamic image sizes for the srcset attr WIDTHS = [1920, 1280, 1024, 768, 640, 320] def srcset(image, ext, page_fraction=1.0): return ','.join( f'/img/{image}/{width}.{ext} {width // page_fraction:.0f}w' for width in WIDTHS ) doctype html html lang="en" head -stylus # use Stylus to do a readable media query that checks for wide color gamut @media screen and (color-gamut: p3) @supports (-webkit-backdrop-filter: brightness(1.5)) section#xdr -webkit-backdrop-filter: brightness(1) filter: brightness(1.5) body section#xdr picture source type="image/webp" srcset=${srcset('xdr', 'webp', 0.3)} source type="image/png" srcset=${srcset('xdr', 'png', 0.3)} -md # write markdown that renders as inline HTML Unlock the full brightness of your XDR display The **2021 MacBook Pro** and the **Pro Display XDR** feature an incredibly bright panel *(1600 nits!)*, but which is locked by macOS to a third of its potential *(500 nits...)*. Lunar can **remove the brightness lock** and allow you to increase the brightness past that limit. -coffee # use CoffeeScript to detect if the browser might not support HDR $ = document.querySelector safari = /^((?!chrome|android).)*safari/i.test navigator.userAgent window.onload = () -> if not safari $('#xdr')?.style.filter = "none" a[href="https://youtu.be/cVAcRH9b44w?t=55"] { display: none; } @media screen and (color-gamut: p3) { video#xdr { display: block !important; } a[href="https://youtu.be/cVAcRH9b44w?t=55"] { display: inline !important; } } And best of all, there is no crazy toolchain, bundler or dependency hell involved. No project structure needed, no configuration files. I can just write a contact.plim file, compile it with plimc to a readable contact.html and have a /contact page ready! So that’s how it went with my app: I wrote a simple index.plim, dropped it on Netlify and went on with my day. Complexity Cost 1 pip install for getting the Plim CLI 1 npm install for getting stylus and coffeescript (optional) 1 build command for generating the HTML files Complex simplicity The app managed to get quite a bit of attention, and while I kept developing it, for the next 4 years the website remained the same heading - image - download button single page. It was only a side project after all. Working for US companies from Romania made good money, but it was so tiring to get through 3h of video meetings daily, standups, syntax nitpicking in PR review, SCRUM bullshit, JIRA, task writing, task assigning, estimating task time in T-shirt sizes?? In April 2021 I finally got tired of writing useless code and selling my time like it was some grain silo I could always fill back up with even more work… I bet on developing my app further. Since my college days, I always chose the work that helps me learn new concepts. At some point I had to understand that I learnt enough and had to start sharing. This time I really wanted to write software that helped people, and was willing to spend my savings on it. Comically Stuffed Stylesheets A more complete app also required a more complete presentation website, but the styling was getting out of hand. You would think that with flexbox and grids, you can just write vanilla CSS these days, but just adding a bit of variation requires constant jumping between the CSS and HTML files. A presentation page is usually only 10% HTML markup. The rest is a ton of styling and copy text, so I wanted to optimize my dev experience for that. There’s no “go to definition” on HTML .classes or #ids because their styles can be defined ✨anywhere✨. So you have to Cmd-F like a madman and be very rigorous on your CSS structure. The controversial but very clever solution to this was Tailwind CSS: a large collection of short predefined classes that mostly style just the property they hint at. For example in the first code block I had to write a non-reusable 5-line style to center the body contents. 1 2 3 4 5 6 7 body { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; } With Tailwind, I would have written the body tag like so: 1 body.flex.flex-col.justify-center.items-center.text-center That might not seem like much, some would argue that it’s even a lot less readable than the CSS one. Can’t I just define a .center class that I can reuse? Well, think about a few things: this might repeat on many sections of the page, but with slight variations (what if I want a centered row, or longer paragraphs of text aligned to the left) responsive sections might need to alter layout (e.g. vertical on mobile, horizontal on desktop) and media queries will quickly blow up the style size .md:flex-row.flex-col is what you would write in Tailwind adding dark/light mode support is yet another media query .dark:bg-white.bg-black looks simple enough interactions like hover effects, complex shadows and filters like blur and brightness is syntax that’s often forgotten .shadow.hover:shadow-xl creates a lift off the page effect on hover by making the shadow larger .blur.active:blur-none un-blurs an element when you click on it choosing colors and reusing them needs a lot of attention .bg-red-500.text-white sets white text on saturated red red-100 is less saturated, towards white red-900 is darker, towards black li:nth-child(4) > ul > li > code { transition: box-shadow 0.55s ease-in-out, filter 0.55s ease-in-out; } #tailwind-usefulness + ul > li:nth-child(4) > ul > li > code:hover { transition: box-shadow 0.25s ease-in-out, filter 0.15s ease-out; } #tailwind-usefulness + ul > li:nth-child(3) > ul > li:nth-of-type(1) > code { background: black !important; color: rgb(255, 200, 149) !important; } @media (prefers-color-scheme: dark) { #tailwind-usefulness + ul > li:nth-child(3) > ul > li:nth-of-type(1) > code { background: white !important; color: hsl(19, 72%, 32%) !important; } } #tailwind-usefulness + ul > li:nth-child(4) > ul > li:nth-of-type(1) > code { box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); } #tailwind-usefulness + ul > li:nth-child(4) > ul > li:nth-of-type(1) > code:hover { box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); } @media (prefers-color-scheme: dark) { #tailwind-usefulness + ul > li:nth-child(4) > ul > li:nth-of-type(1) > code:hover { box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.5), 0 8px 10px -6px rgb(0 0 0 / 0.7); } } #tailwind-usefulness + ul > li:nth-child(4) > ul > li:nth-of-type(2) > code { filter: blur(4px); } #tailwind-usefulness + ul > li:nth-child(4) > ul > li:nth-of-type(2) > code:active { filter: blur(0) !important; } #tailwind-usefulness + ul > li:nth-child(5) > ul > li:nth-of-type(1) > code { background: #ef4444 !important; color: white !important; } #tailwind-usefulness + ul > li:nth-child(5) > ul > li:nth-of-type(2) > code { background: #fee2e2 !important; color: black !important; } #tailwind-usefulness + ul > li:nth-child(5) > ul > li:nth-of-type(3) > code { background: #7f1d1d !important; color: white !important; } Sure, long lines of classes might not be so readable, but neither are long files of CSS styling. At least the Tailwind classes are right there at your fingertips, and you can replace a -lg with a -xl to quickly fine tune your style. Complexity Cost: 1 command added for building the minimal CSS from the classes used 1 npm install for getting the Tailwind CLI 1 config file for defining custom colors, animations etc. (optional) Responsive images So many people obsess over the size of their JS or CSS, but fail to realize that the bulk of their page is unnecessarily large and not well compressed images. Of course, I was one of those people. For years, my app’s website had a screenshot of its window as an uncompressed PNG, loading slowly from top to bottom and chugging the user’s bandwidth. I had no idea, but screenshots and screen recordings are most of the time up to 10x larger than their visually indistinguishable compressed counterparts. I even wrote an app to fix that since I’m constantly sending screenshots to people and was tired of waiting for 5MB images to upload in rapid chats. It’s called Clop if you want to check it out. Yes, just like that famous ransomware, it wasn’t that famous at the time of naming the app. I needed a lot more images to showcase the features of an app controlling monitor brightness and colors, so I had to improve on this. Delivering the smallest image necessary to the user is quite a complex endeavour: Optimize the image using ImageOptim Resize it to fit multiple screen sizes using vipsthumbnail Figure out what fraction of the page width will be occupied by the image Write a suitable srcset attribute to load the suitable image Optional: convert the image to formats like webp, avif or JPEG XL for smallest file size I did so much of that work manually in the past… thankfully nowadays I have imgproxy to do the encoding, optimization and resizing for me. I just have to write the srcset, for which I defined Plim and Python functions to do the string wrangling for me. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 -def image(img, ext='png', factor=0.4, mobile_factor=1) picture -call img=${img} ext=${ext} factor=${factor} mobile_factor=${mobile_factor} self:sources img srcset=${srcset(img, ext, factor)} -def sources(img, ext='png', factor=0.4, mobile_factor=1) source type="image/avif" srcset=${srcset(img, ext, mobile_factor, convert_to="avif")} media="(max-width: 767px)" source type="image/avif" srcset=${srcset(img, ext, factor, convert_to="avif")} media="(min-width: 768px)" source type="image/webp" srcset=${srcset(img, ext, mobile_factor, convert_to="webp")} media="(max-width: 767px)" source type="image/webp" srcset=${srcset(img, ext, factor, convert_to="webp")} media="(min-width: 768px)" source type="image/${ext}" srcset=${srcset(img, ext, mobile_factor)} media="(max-width: 767px)" source type="image/${ext}" srcset=${srcset(img, ext, factor)} media="(min-width: 768px)" 1 2 3 4 5 6 7 8 9 10 11 12 WIDTHS = [1920, 1280, 1024, 768, 640, 320] def imgurl(image, width, ext="png", convert_to=""): conversion = f"@{convert_to}" if convert_to else "" return f"https://img.panaitiu.com/_/{width}/plain/https://lunar.fyi/img/{urllib.parse.quote(image)}.{ext}{conversion}" def srcset(image, ext="png", factor=1.0, convert_to=""): return ",".join( f"{imgurl(image, width, ext, convert_to)} {width // factor:.0f}w" for width in WIDTHS ) Complexity Cost 1 imgproxy server that needs to run somewhere publicly available, be kept alive and secure some Python and Plim code for generating srcsets Hot reloading After 2 weeks of editing the page, Cmd-Tab to the browser, Cmd-R to refresh, I got really tired of this routine. I worked with Next.js before on Noiseblend and loved how each file change automatically gets refreshed in the browser. Instantly and in-place as well, not a full page refresh. I got the same experience when I worked with React Native. There should be something for static pages too, I thought. Well it turns out there is, it’s called LiveReload and I had to slap my forehead for not searching for it sooner. After installing the browser extension, and running the livereloadx --static file watcher, I got my hot reloading dev experience back. Actually now that I think about it, Hugo has super fast hot reloading, how does it accomplish that? Yep, turns out Hugo uses LiveReload as well. Complexity Cost 1 more command to run in 1 more terminal panel, multiplex helps with that 1 browser extension to install and hope it’s not compromised or sold to a data thief 1 npm install for getting the livereloadx CLI Contact pages After releasing the new app version, many things were broken, expectedly. People tried to reach me in so many ways: Github issues, personal email, through the app licensing provider, even Facebook Messenger. I had no idea that including an official way of contact would be so vital. And I had no idea how to even do it. A contact form needs, like, a server to POST to, right? And that server needs to notify me in some way, and then I have to respond to the user in some other way… sigh I thought about those chat bubbles that a lot of sites have, but I used them on Noiseblend and did not like the experience. Plus I dislike seeing them myself, they’re an eyesore and a nuisance obscuring page content and possibly violating privacy. After long searches, not sure why it took so long, I stumbled upon Formspark: a service that gives you a link to POST your form to, and they send you an email with the form contents. The email will contain the user email in ReplyTo so I can just reply normally from my mail client. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 form action="https://submit-form.com/some-random-id" label for="name" Name input#from hidden="true" name="_email.from" type="text" input#name name="name" placeholder="John Doe" required="" type="text" label for="email" Email input#email name="email" placeholder="[email protected]" required="" type="email" label for="subject" Subject input#email-subject hidden="true" name="_email.subject" type="text" input#subject name="subject" placeholder="What's this message about?" required="" type="text" label for="message" Message textarea#message name="message" placeholder="Something about our apps perhaps" required="" type="text" rows="6" -coffee # Custom subject and name: https://documentation.formspark.io/customization/notification-email.html#subject nameInput = document.getElementById("name") fromInput = document.getElementById("from") nameInput.addEventListener 'input', (event) -> fromInput.value = event.target.value subjectInput = document.getElementById("subject") emailSubjectInput = document.getElementById("email-subject") subjectInput.addEventListener 'input', (event) -> emailSubjectInput.value = event.target.value Complexity Cost None, I guess. I just hope that the prolific but unique Formspark dev doesn’t die or get kidnapped or something. And you call this “simple”? It’s not. Really. It’s crazy what I had to go through to get to a productive setup that fits my needs. One could say I could have spent all that time on writing vanilla HTML, CSS and JS and I would have had the same result in the same amount of time. I agree, if time would be all that mattered. But for some people (like me), feeling productive, seeing how easy it is to test my ideas and how code seems to flow from my fingertips at the speed of thought, is what decides if I’ll ever finish and publish something, or if I l’ll lose my patience and fallback to comfort zones. Having to write the same boilerplate code over and over again, constant context switching between files, jumping back into a project after a few days and not knowing where everything was in those thousand-lines files.. these are all detractors that will eventually make me say ”f••k this! at least my day job brings money”. Reusability So many JS frameworks were created in the name of reusable components, but they all failed for me. I mean sure, I can “npm install” a React calendar, and I am now “reusing” and not “reimplementing” the hard work of someone better than me at calendar UIs. But just try to stray away a little from the happy path that the component creator envisioned, and you will find it is mind-bendingly hard to bend the component to your specific needs. You might raise a Github issue and the creator will add a few params so you can customize that specific thing, but so will others with different and maybe clashing needs. Soon enough, that component is declared unwieldy and too complex to use, the dev will say “f••k this! I’d rather do furniture” and someone else will come out and say: here’s the next best thing in React calendar libraries, so much simpler to use than those behemoths! I never had this goal in mind but unexpectedly, the above setup is generic enough that I was able to extract it into a set of files for starting a new website. I can now duplicate that folder and start changing site-specific bits to get a new website. Here are the websites I’ve done using this method: lowtechguys.com / where I publish my apps subsol.one / app I made for my brother to publish parties on robert.panaitiu.com / my brother’s personal website lunar.fyi / the app I have been talking about above And the best thing I remember is that for each website I published a working version, good looking enough, with a contact page and small bandwidth requirements, in less than a day. How does this solve the problem of straying away from the happy path? Well, this is not an immutable library residing in node_modules, or a JS script on a CDN. It is a set of files I can modify to the site’s needs. There is no high wall to jump (having to fork a library, figuring out its unique build system etc.) or need to stick to a specific structure. Once the folder is duplicated, it has its own life. For those interested, here is the repo containing the current state of my templates: github.com/alin23/plim-website I don’t recommend using it, it’s possible that I’m the only one who finds it simple because I know what went into it. But if you do, I’d love to hear your thoughts. Gatsby? Jekyll? Hugo? Weirdly, this website I’m writing on is not made with Plim. At some point I decided to start a personal website, and I thought it probably needs a blog-aware site builder. At the time, I didn’t know that RSS is an easily templatable XML file, and that all I need for a blog is to write Markdown. I remember trying Gatsby and not liking the JS ecosystem around it. Jekyll was my second choice with Github Pages, but I think I fumbled too much with ruby and bundle to get it working and lost patience. Both problems stemmed from my lack of familiarity with their ecosystems, but my goal was to write a blog, not learn Ruby and JS. Hugo seemed much simpler, and it was also written in Go and distributed as a standalone binary, which I always like for my tools. I marveled at Hugo’s speed, loved the fact that it supports theming (although it’s not as simple as it sounds) and that it has a lot of useful stuff built-in like syntax highlighting, image processing, RSS generator etc. But it took me sooo long to understand its structure. There are many foreign words (to me) in Hugo: archetypes, taxonomies, shortcodes, partials, layouts, categories, series. Unfortunately, by the time I realized that I don’t need the flexibility that this structure provides, I had already finished this website and written my first article. I also used a theme that uses the Tachyons CSS framework, for which I can never remember the right class to use. I thought about rewriting the website in Plim but converting everything to Tailwind or simple CSS would have been a lot of work. I eventually started writing simple Markdown files for my notes, and have Caddy convert and serve them on the fly. Helps me write from my phone and not have to deal with Git and Hugo. I still keep this for longform content, where a laptop is usually needed anyway.

8th Aug 2023 • 47 votes

More in programming

Writing a Demoscene Intro for the ZX Spectrum

Download Now View on pouet.net Intro In the late 80s, I spent an embarrassing number of hours hunched over my ZX Spectrum, typing in Sinclair BASIC programs and rudimentary assembler routines copied from magazines, trying to re-create the Amiga crack intros I’d seen round at a friend’s house. Obviously, they never came even slightly close - but now, many decades later, I’ve finally managed it! I’m now the proud author of a hand-rolled retro demoscene intro that runs on a stock 128Kb Spectrum. It features all those late 80s tropes: A smooth scrolltext (with ‘leet custom fonts!), a three-channel AY soundtrack, bouncing VU meters and the essential parallax starfield. It’s currently 10287 bytes of code, or 19552 bytes as a complete tape image - which, given I’ve been working on this since December 13th last year, means my average rate of code-production is a paltry ~ 455 bytes per week! This blog post is a technical overview of how I got there, some cool tricks I picked up, and everything I had to learn (and re-learn) about the Speccy and Z80 assembly along the way. It simultaneously rates as one of my favourite side-projects ever, and also the one that most often nearly triggered a foot put through my monitor in frustration. I’ll reflect on all that at the end, but for now if you just want to see the finished product, the download button is at the top of the article and the included .tap cassette tape image should load into any emulator. There’s also a YouTube capture, links to the production on pouet.net, and you can run it from within a web browser by visiting my TNFS site portal - check option 4 for recent uploads. Be Kind, Rewind But let’s skip back briefly to the late 80s. Coming from the world of an 8-bit, cassette tape loading computer, seeing an Amiga for the first time was like encountering something from outer space. Like everyone who saw one back in the day, I was blown away by the amazing graphics and sound on display, not to mention the library of ground-breaking games. But I found myself even more captivated by the mysterious (and totally badass) “intros” displayed at the beginning of pirated games. Created by the software piracy groups that had cracked the copy protection and distributed the floppy-disk based games, they were mini works of art in their own right - music, graphics and usually some cool-looking effect to show off the group’s prowess. There’s a great collection of these intros (or “cracktros”) archived on YouTube. Watch a few of those videos and you’ll quickly notice the “design” tropes that almost every cracktro shares: a cool, edgy logo at the top of the screen (bonus points if it’s dripping blood or in any way “metal”), some boastful text displayed on top of an effect of some sort - usually a scrolling parallax starfield, or rotating 3d object - and a further scroll-text full of shout outs, greetings to fellow pseudonymous crackers and adverts for illegal BBS systems galore. Needless to say, to a kid in the 80s/early 90s this was possibly the coolest thing ever. I thought this one was the absolute bomb when I first saw it. Sine-scroller, pirate BBS adverts, cool logo with a sword - and of course, a starfield. There’s something for everyone here. The music from this is burned into my brain. I think pretty much everyone of my classmates knew this intro given the popularity of Lemmings in the early 90s. Skid Row as a group are still active to this day and this is probably the quintessential Amiga cracktro of the period. Endless Piracy. Edgy. Another cool logo, sine-wave scroller, starfield and boasting text. The classics never go out of style. Another great combination of all the essential ingredients. Cool logo, even cooler text font, scrolling starfield and a scroll-text running along the bottom. Vintage stuff. Logo. Text. Scroller. But no starfield - otherwise a solid effort from Anthrox here :) The demoscene grew out of these intros. The cracking eventually faded into the background, and coders, “graphicians” and musicians all contributed to feature-length productions that showcased their abilities, competed with other groups and pushed the machine to its limits. In amazing feats of ingenuity and pure “code as an art form”, the scene lives on today, with groups producing breathtaking productions like this and this - bear in mind, both of those are running on a 7Mhz home computer equipped with a floppy disk, 1Mb of memory and a sound chip from 1985. But much as I admire the sheer skill and dedication that takes, it’s the early “single screen” scrolly-text-and-starfield cracktros I’ve always found captivating. They took on a sort of mystical quality for me, hinting at a scene and a culture I could only ever experience from the outside - which is exactly why I spent all those hours in Sinclair BASIC, trying in vain to re-create them, and dreaming of the day I’d eventually have an Amiga of my own. The Jump Back I eventually did trade up to a range of Amiga systems, joined a few “scene” groups myself and threw myself into a variety of activities until exams and the end of my teenage years beckoned. I dabbled with Z80 code back then, and at one point knew enough 68k assembly to bluff my way through - partly from the Amiga’s heyday, partly from a few “embedded systems” modules at University where I had to program a set of traffic lights or lift-controllers. Given there’s not much call for that sort of thing at my $DAYJOB, though, it had all pretty much faded from my memory. I have kept working on a bunch of projects in my spare time, and one of my favourites is my TNFS site - sort of an old-school BBS system or interactive “website” for the Spectrum, which grew out of a fun experiment to see how far I could push Sinclair BASIC. It relies on a modern network card fitted to the Spectrum, which is how I came to be chatting to “Desertkun”, another TNFS-protocol enthusiast. He’s now producing a great new WiFi network card for the Speccy, and was kind enough to send me a few of the prototypes to test out for him. I was massively inspired by his hardware and low-level coding chops, and during one of our video calls where we’d been comparing our development setups, he asked why I didn’t write any Z80 assembly myself. A throw-away comment, but the challenge was set! Now, I couldn’t face re-writing the whole site in asm, but I figured I could produce a self-contained intro to promote it in the style of the early “BBStros”, and finally be one of the cool kids with a scene production of my own :) I mean, how hard could it be ? (ooooh - foreshadowing!) Dev Environment My development environment through this was pretty straightforward, and is the same stack I use for developing my TNFS site. I just used a Makefile-driven build system which ran my code through the Pasmo assembler, my fork of zmakebas to build BASIC files, and the FUSE emulator for testing. For final testing on the real hardware, I uploaded my .tap files to my local TNFS server where I could run them on my +3. A few additional tools I have to call out because they ended up making a big difference - one is the amazing web-based “asm-editor” I discovered while looking up Z80 instructions. Not only does this site have a great reference of all the Z80 instructions, but it also has an editor where you can run example code and see how the memory and/or various registers change state. Take for example, the djnz instruction - you can build it and step through the example loop construct one instruction at a time. Or try my example of an add instruction in the screenshot to see how the carry flag works when a bit gets shifted out of a register. Although it uses a slightly different syntax to Pasmo (so I couldn’t drop my code directly in it), it massively helped when manually stepping through things. And lastly, probably the most significant debugging tools I used on this project - a notepad and pen, and my two dogs (say “Hi!” to Biscuit & Pebble…) I’ll write some more about this in the wrap-up, but this really was a highly unusual project for me, coming from decades of much higher-level languages and constructs. Most of my z80 code I only ever really understood after drawing out the bytes and bitmasks, keeping a table of register flags and drawing chunks of screen memory out by hand. And I found my evening walks with the two dogs provided ideal rubber duck exercises, although I’m not sure what they both made of my protracted mutterings about the hl register pair. Either way, debugging-by-dog is highly recommended! The Starting Point So, let’s get started. I followed a great “Hello, world!” example from futurice.com to validate my build environment. Beyond that, the first thing I wanted to do was load in my logo and background image I’d use as a “stage” for the remainder of the planned intro. I wasn’t really sure what I was going to do at this point, just that I wanted my logo at the top, and a scrolltext along the bottom row of the screen. As my TNFS site header already looked kinda demoscene-y, I made a quick edit putting in place two blue guide-bars inside which I’d put further content. Memory map courtesy of breakintoprogram.co.uk A Spectrum SCREEN$ file (usually displayed as loading screens) is just a dump of screen memory, starting from byte 16384, and running for 6912 bytes. The weird layout of this is very relevant later, but in order to display it all you need to do is load your file into that memory location. After a few hour’s work of reviewing Speccy internals and following various guides, I had a very simple starting point for my demo that served as the basis for everything else that came after. org $8000 screen EQU $4000 ; address of the first byte of screen memory, 16384 in dec scrlen EQU 6912 ; total length of the screen memory inc. attributes ula EQU $FE ; I/O port for the ULA to control the border black EQU 0 ; Spectrum colour number, the exact same ones BASIC uses start: ld a,black ; Set the border colour to black using the ULA out (ula),a ; Copy my logo header picture to screen memory ld hl,header ; HL = location of my screen file in this code ld de,screen ; DE = start of screen memory ld bc,scrlen ; BC = 6912 bytes to copy ldir ; block copy, see https://jnz.dk/z80/ldir.html ; busy loop - rest of intro goes here :P hold: halt ; sleep until the next interrupt (1/50 second) jr hold ; then jump back and sleep again, for ever header: incbin "assets/header.scr" ; my logo header/background end start Not much to look at now, but this simple starting point introduced me to important concepts like interfacing with the Spectrum’s hardware (ULA chip to start with, and AY sound chip later) via the out instruction, how to pull in binary files using Pasmo’s INCBIN directive, using the Z80’s block copy ldir instruction, the use of register pairs for 16-bit addressing, and simple branching to labels. Hello World The next step was to print something on the screen and explore some of the Speccy’s built-in ROM routines, which are somewhat analagous to syscalls on modern operating systems. There are plenty of disassemblies and guides to the Spectrum ROM available on the internet, one of the best commented and linked one is at https://skoolkid.github.io/rom/. Using this as a reference alongside the various “Hello, world!” tutorials showed me how this sort of thing is built up: at EQU 22 ; "at" control character used to position the cursor row EQU 11 ; coordinates where to print the message col EQU 9 prstring EQU $203C ; where the pr_string routine lives in ROM ld de,msg ; DE = address of the first character ld bc,msglen ; BC = how many characters to print call prstring ; print the lot ; busy loop is the same... msg: db at,row,col ; equivalent of "PRINT AT 11,9" in BASIC db "Hello, world!" msglen EQU $-msg ; 16 bytes in all, pasmo shortcut, $=program counter I ended up mostly not using the ROM routines in my code as they dragged in all sorts of things I didn’t need, but it was massively useful to understand how they worked as they introduced me to yet more commonly-used constructs. For example, the PR_STRING routine defined here shows how you can build a loop over each character. If you’re just starting out, I highly recommend going over the 8 lines in that routine with a notepad and pen, working out the contents of the registers and following the loop constructs. I also made a version of it in asm-editor you can try. If you click that link, you’ll see a version of the code loaded into the web editor. Click “Build”, and then step through the code using the “Step” button - although no text will be actually displayed (I skipped over the call to the Spectrum ROM “restart” vector that does this), you’ll see the various states of the registers and for example the BC register counting down as each byte of the string is processed. Adding Some Style Problem is, the Speccy’s built-in font is not exactly dripping with demoscene credibility. I wanted to use some stylish fonts and this turned out to be super easy - a quick search turned up Jim Blimey’s awesome font collection, which included a ready-to-use set of assembler files and some instructions which soon led me to these additions in my code: ; CHARS is the address the ROM's printing routine reads to find the font. ; Picking our own font is simply a matter of over-writing this chars EQU $5C36 ; from Jim's docs - POKE 23607=5C36 in hex ld hl,myfont-256 ; HL = font address - 256 as per docs ld (chars),hl ; from now on the ROM draws with it! ; rest of code is the same myfont: include "textfont.asm" ; "Cartoon" font from jimblimey.com There we go! Now that’s looking a little more elite. Sorry, I mean 31337. Strap in folks, we’re leaving lamer city… So at this point, a month or so in to my dabblings and I’d started to get a reasonable understanding of common idioms and was starting to feel like this was a project that might actually go somewhere. The problem started when I moved on to trying to implement actual demo effects: It became very apparent, very quickly, just how much I didn’t know about how this all worked. Not to mention the fact that at this level, you’re really closely exposed to the physical hardware intricacies, including the design decisions and trade-offs that the hardware designers had to make when working with the extremely limited specs of the early 1980s. Cursed Screen Layout The first such problem - and the cause of probably 90% of my pain - is the Speccy’s extremely weird screen layout. Rather than standard bitmapped display you might expect on more modern hardware, it is instead split into 3 sections, each with a set of interleaved rows (so if you keep adding 1 to a screen address, eventually instead of reaching the next row, you’ll instead end up 8 rows down, and so on until you hit the bottom of the section and then loop back to the row underneath where you started!) followed by a block of “attribute” bytes which determine the colours of each 8x8 block. The attribute blocks are the reason why each 8x8 character cell can only have two colours (a PAPER and INK attribute) which led to the garish “colour-clash” of spectrum games and distinctive style where artists try to keep inside 8x8 blocks as much as possible. But it was the layout of the bitmap portion that is such a mind twister. Download the video You can see this illustrated whenever a game reads in a loading screen from tape. You can see each section built up with the interleaved rows one at a time, and then finally the colour and other attributes (like flashing ). There’s a lot of detail at the fantastic Break Into Program site. In the defense of the designers, they were working with extremely limited hardware and I don’t think the Speccy was ever imagined as a games machine. When you start shuffling bytes around, you realise that this approach is really optimised for displaying text which is neatly arranged into 8x8 character boundaries. For example, using this “scrambled” bit order, if you have a screen address in the hl register pair, you can quickly get to the next pixel line of a given address by simply incrementing the “high” byte. There’s a bunch of other “cheap moves” you can make by simply shifting bits around: Move Instruction Right 8 pixels inc l Left 8 pixels dec l Down 1 pixel line inc h Up 1 pixel line dec h Down 1 character row add 32 to l It was a reasonable choice for a machine that would probably spend a lot of it’s time displaying BASIC program listings - it’s only when you start trying to write demo or sprite routines that it starts to feel really weird. This also means you’ll pretty quickly need a routine to convert X,Y addresses to the relevant memory addresses. Again, L Break Into Program has a useful routine and if you disassemble more or less any spectrum game or demo, you’ll find a version of this procedure somewhere. And yes, this all became horribly relevant later on… The Scroller Download the video Admittedly, not the most interesting demonstration - but this was the first time I had made something move on the Spectrum’s screen with assembly! I watched this with pride more times than I care to admit :) I had a snazzy logo and “Hello World” text, but that doesn’t make for a very interesting intro! The first effect I wanted was the scrolltext - without that, it’s not really an intro. Now, the Amiga had all kinds of hardware sprites, blitter operations, custom chips but the Spectrum had none of that. I was going to have to work out how to do things the hard way - “banging on the metal”, literally by shifting bits around the screen memory by hand and having the CPU do all the work. This is where understanding the Speccy’s weird screen and character cell layout is so important. It turns out there’s a neat trick as long as I stay inside one character row - there’s a RL instruction, which takes a byte and “rotates” it left. It inserts whatever is in the carry flag into the rightmost bit, and whatever gets shifted out ends up in the carry bit again. That means if you string a bunch of them together, you can move a set of pixels across the screen - the moment they “fall off” one character cell, they end up in the carry flag and then the next rl instruction inserts them at the beginning of the next character cell to the left. Repeat 32 times, and you’ve just moved a pixel (or set of pixels) across a line of the screen: linestart EQU $4880 ; leftmost byte of the line 12 rows down lineend EQU linestart+31 ; rightmost byte column ($489F) linelen EQU 32 ; one whole line is 32 bytes (character cells) across dot EQU %00000001 ; the dot "shape", just one pixel lit at the far right end newdot: ld a,dot ; load the dot shape (only rightmost pixel lit) into a ld (lineend),a ; store it in the rightmost byte of the line, pixel is now lit frame: halt ; wait for the next frame, otherwise it whizzes across too fast to see ld hl,lineend ; hl = last (rightmost) byte of the line ld b,linelen ; loop counter, 32 bytes to work through or a ; trick to clear the carry flag, or against itself leaves a register ; unchanged but clears various bits including the carry flag ; see https://jnz.dk/z80/or_r.html shift: rl (hl) ; rotate this byte left (00000001 -> 00000010) dec hl ; point hl at the next byte to the left djnz shift ; loop construct: b=b-1 and then jump back to shift label until b=0 jr c,newdot ; now we've shifted the whole line, so if the carry flag is set now it means ; the dot has just left the screen, so we jump back to the start jr frame ; otherwise it is still somewhere on the screen, so keep shifting! Download the video Hell yeah. It’s almost a proper scrolltext now! Wrapping this in another loop that increments h each time (remember - that gets us moving down a scanline) gets us moving an entire character cell. Here you can see it moving an entire row of my “Hello world” message. There was however a lot more work needed to make a proper scrolltext, with each letter appearing smoothly. I started off by just printing an entire character at a time on the right-hand edge of the screen - it worked, but was pretty ugly as you saw each new character appear at once in the right-hand side of the screen, scrolling 8 pixels over and then the next character flashed up. Instead, I wanted to make each character appear pixel by pixel and that required a bit more bit juggling. The routine I ended up with effectively streams each character in, line by line, pixel by pixel using the font data directly instead of calling the ROM print character routine. It does this by looking up the ASCII code for a character, and then fetching the corresponding font representation into a 8-byte buffer, one byte for each line of data. The font data starts with the space character (character code 32), so a lookup is simply font code base address + (ascii code - 32) * 8 to get to the bits for that letter. Arranged into an 8x8 grid, the bits for the letter H in the font data look like this. For each row of bits, I shift the line left using a neat trick. The byte is loaded into the a register, and then by adding itself to itself - add a,a we shift the value left (in binary, multiplying by 2 moves everything over one bit) so that the pixel I want to draw ends up dropping into the carry flag. This is an idiomatic way of doing it in Z80 code as it’s faster and smaller than the sla instruction. And every cycle quickly adds up when you’re re-drawing 256 bytes every frame! The character buffer then looks like this, with the top line shifted. Now, with the hl register pair pointing at the correct location on the screen, I can then use the rl trick discussed above. This shifts a scanline from the bottom character row of the screen left by one pixel and draws my pixel in the rightmost position by moving it in from the carry flag. Looping 32 times over this moves the entire row by 1 pixel with my new pixel inserted at the right. I then repeat this step again using inc h to move down one scan line until the entire character column has been drawn. At this point, the character buffer looks like this, and all 8 lines of the bottom screen row have all been shifted left by one bit by looping over all 32 character columns. The whole thing then repeats another 7 times until the entire character has been smoothly scrolled onto the screen. So just to re-state this because I think it’s worth the emphasis - just to scroll a message across the bottom of the screen, we’re having to loop over and re-draw 256 pixels every frame! It shows how much faster assembly is than the BASIC interpreter built into the spectrum’s ROM. I found it amazing to see my code finally working and imagining all the bits flying around and registers flapping away. It really makes me appreciate just how much effort went into creating the games I played back then. Music The original Amiga module I converted. A proper 1980s ear-worm! Of course, any good intro needs some music. On the Amiga, the standard was the “tracker” module format, which has a long and involved history of different forks, versions and tweaks. A song is built up from a series of patterns, each made up of rows of instructions. Each instruction usually includes note or pitch information, which sample to play and various parameters such as volume, effects - or in the case of some instructions, which position in the song to jump to. In many ways, it’s almost like a programming language for music. Using ProTracker as an example, a typical note cell is 4 bytes, shown on-screen as something like C-2 01 C20. That breaks down into three fields: the note and octave (C-2), the sample number (01), and the effect and its parameter (C20 - in this case, “set the volume to $20”, which is half of the maximum $40). A close-up of Protracker playing a tune. Note the VUmeters - another classic demo effect I would pinch later on! For an impressive demonstration of what’s possible with a tracker and to see what the note data looks like on-screen, check out the work of h0ffman, and in particular his winning “Professional Tracker” entry from the Revision 2014 competition. He’s an undisputed master of the science (or art!) of tracking, and his “Secrets of Protracker” talk gives a great overview of what’s possible. The Amiga’s sound hardware far eclipsed anything else on the market at the time, and the ZX Spectrum is paltry by comparison. The original 48K models only offered a rudimentary “beeper”, but the later 128K models included a 3-channel AY-3-8912 sound chip. This was very popular in 8-bit micros of the time, and a more-or-less identical variant was also used in the Atari ST. While it only offers basic waveforms and sound processing, it does enable multi-channel “chip music”, and so it became the mainstay of the ZX Spectrum demoscene. It also spawned a variety of SoundTracker clones on the Spectrum itself, although most musicians these days prefer to work in modern tools like Vortex Tracker II, which runs natively under Windows. The Conversion Pex looking back on his demos - this starts at the point where he features my letter and plays a bit of my cover version! For my intro, I wanted to port an Amiga module - and I had the perfect one in mind. One of my favourite Amiga music disks was “Sounds Of Gnome”, released in 1989 by the Swedish duo “Mahoney & Kaktus”. Mahoney also wrote NoiseTracker, one of the first really widely used trackers on the Amiga, and the program that served as my teenage introduction to making computer music of my own. Back in 2018 I made a rock cover of one of the other tunes on that disk, and wrote to Mahoney (real name “Pex”) about it. He ended up showing my “music video” at a demo conference, which I am eternally grateful for! So it seemed only fitting that I mangle another song from that collection with a Spectrum conversion… Sorry, guys! I picked the track “Milkman” which was written by “Glue Master”, an associate of Mahoney’s who had a few of his tracks featured on the compilation. It’s an ideal track for converting: It’s short (only 7 patterns in total), most of those patterns share the same chord sequence, and it looked fairly easy to squeeze the original 4 channels down into the Spectrum’s 3. Plus, it’s an incredibly catchy tune and I knew it well - I’ve been listening to it for 35+ years and often jam along to it on guitar or bass. The conversion started off pretty easily: all I really had to do was copy the patterns note-for-note from the Amiga mod, which I had open in OpenMPT as my Windows tracker. Below is a session with both trackers open on the same pattern - the original Amiga mod on the left, and my 3-channel Spectrum conversion on the right. Note the 11.7 in the 3rd channel of the Spectrum version. This is a “pitch slide down” effect in Vortex Tracker syntax, which drops the pitch by about a quarter-tone. It’s repeated on the next line, so the overall effect is a slide down from the A# that leads in to the next bass note. The original Amiga mod does the same thing using its own syntax - those are the 202s you can see on the same notes, at the bottom of the 4th track. Using just a single “beep” sound for everything was enough to check everything was working, and after a few weeks of hacking on it and re-arranging things in the evenings, I had a basic version which sounded surprisingly good for a first draft: Download the MP3 instead. I have to give a special mention to this forum post which was a massive help getting me started. It’s probably the best introduction to Vortex Tracker I’ve found - many, many thanks to the original author for taking the time to write it all up! The real challenge was making it sound as close to the original as possible. That meant building up a library of better-sounding samples for the different parts, and making extensive use of Vortex Tracker’s “ornaments”. Vortex Tracker comes with a great selection of sounds, and fortunately I’m also a weirdo who likes to listen to chiptunes in the background while I work! So whenever I found something particularly enticing, I filed it away for later pilfering inspiration. A Vortex Tracker sample isn’t really a “sample” in the Amiga sense of a recorded sound - it’s a definition of how a note sounds, one line per frame. Each line can mix in a tone or the white noise generator (useful for snare effects and suchlike), along with various other parameters. Ornaments, on the other hand, give you extra control over what pitch a note plays - and this enables really useful tricks like “arpeggio chords”. For example, an ornament of 0,4 makes the channel switch rapidly (25 times a second) between the note being played and the one 4 semitones above it. It happens so quickly that it sounds much like two notes played at the same time, which means you can build up full-sounding triad chords using only two channels. Take the opening chord in my fully converted version, Cm: It plays C in the first channel and the minor 3rd (D#) in the second, then I apply an ornament to the second channel to bring it up 4 semitones to the 5th (G). That gives all three notes of the chord, while keeping the 3rd channel free for other parts. As well as making much more efficient use of the limited 3 channels, this also gives Spectrum music that characteristic “warbling” sound, which I find really appealing in a retro kind of way! Between tricks like this and samples that mix tones with white noise (to make a combined “bass-snare” sound, for example), I managed to condense the full track down to 3 channels. Many months later, I have to say I’m really pleased with the final result. Playback The original site that was home to Vortex Tracker, documentation and other resources appears to be offline at the moment. I managed to grab a copy of everything I needed from archive.org - you’ll want the PTxTools.7z file for the playback routines discussed here. The playback was simple in theory: As this overview notes, you basically just pull in the player routine, include your module file and call the playback routine once per frame in your main loop. The catch is, none of the source code versions supplied in the archive worked with my Pasmo assembler. Fortunately, there is a pre-compiled binary object shipped alongside the source (VTII10bG in the PTxTools/PT3Play directory). This just has to be loaded in at address $C000 - this cannot be changed, as it has lots of absolute addresses baked in. My code to handle module playback now looks something like this, with the binary module loaded in place: ; Define the start address of the player routine and functions we need player EQU $C000 ; hard-coded org address of binary player plinit EQU player+3 ; init function in docs, module address in hl plplay EQU player+5 ; play and move on one frame ; from the docs - this "analyzes PT3 module header and prepares ; corresponding note and volume tables" ld hl,moduleimg call plinit ; main loop hold: halt call plplay call scroll ; other effects / routines go here... jr hold moduleimg: incbin "assets/music.pt3" Starfield Download the video Well, that didn’t work as hoped. TNFStro - tortoise edition. This brings me to the main event. No self-respecting cracktro is complete without a starfield! My first, naive approach to the classic starfield effect was to scatter random stars across the screen, then move down each scan-line applying the same rl trick I’d used for the scroll-text. I figured I could get parallax by shifting each row a different amount: One pixel for the first row, two for the second, three for the third, then back to one again, all the way down the screen to give the impression of several “fields” of stars moving at different speeds. It turned out there were two major problems with that approach. Firstly, shifting an entire row smeared whatever else was on it along with the stars - which I’d expected, and had filed away as a problem for future me. The bigger issue was speed: by the time I’d finished the first three rows I’d already performed hundreds of shift operations, and it was nowhere near fast enough to fit inside a single frame. Everything crawled across the screen, and the music playback slowed down so much that you could actually hear the arpeggio ornaments warbling away as separate notes. Clearly I was going to have to keep track of the individual stars instead. I found this routine, which was more-or-less what I had in mind and started me off with the idea of a structure to hold the state of each star. It also uses a screen address lookup much like the one from “Break Into Program”. Download the video Dammit. Back with another funeral dirge rendition :( That got things moving, but the co-ordinate lookup and tracking proved expensive to do (it has to run for every star, every frame as well as keep track of the old position for erasing) and I’d been ignoring the bigger question how to display stuff on top of the starfield. My original idea was simply to draw the stars and then paint text over them each frame, which blew the frame budget just as comprehensively as my first attempt. If I wanted something more interesting than an empty starfield, I’d have to keep track of which cells were occupied by text and avoid drawing stars in them in the first place, rather than over-painting them every time. After lots of scribbling in my notepad and visualising rows of bits, I eventually solved both challenges by “cheating” fairly liberally. So now, I don’t generate a random starfield at all. Instead, I built a table of pre-calculated Y and X position pairs: stdata: defb 109, 25, 88,182, 42, 96, 155,226 defb 59,254, 137,169, 160, 84, 125,193 ; ... and so on, for 32 stars ... This saves faffing about with an RNG, and once I have a layout that looks good I know it’ll always look good. I can also “fudge” things and ensure no two stars share the same y co-ordinate which sidesteps a whole class of problems like stars sharing a scanline, and having to deal with them overtaking each other. The “random” layout is generated by a simple Ruby script: #!/usr/bin/env ruby PAIRS = 32 PER_LINE = 4 # pairs per line Y_RANGE = 40..175 # exclude rows taken up by my logo & bars X_RANGE = 0..255 raise "y range too small" if Y_RANGE.size < PAIRS # no duplicate y values ys = Y_RANGE.to_a.sample(PAIRS) pairs = ys.map { |y| [y, rand(X_RANGE)] } pairs.each_slice(PER_LINE) do |row| body = row.map { |y, x| format('%3d,%3d,', y, x) }.join(' ') puts "defb #{body.chomp(',')}" end When the routine initialises, I convert those co-ordinate pairs into a table that tracks the state of each star. So rather than storing a position and translating it to a screen address every frame, each of my star records now looks like this (offsets from the base address): STSCR EQU 0 ; screen address of the byte the star is in, e.g. $4123 STOCC EQU 2 ; a pointer to the "occupancy check" table cell STMASK EQU 4 ; the star's bit pattern, e.g. %00010000 STSTEP EQU 5 ; "speed" - num. of pixels this star travels per frame The key thing is that there are no X/Y co-ordinates at all. For each star I store the screen byte it sits in and the bit mask within that byte, so it’s already in the form the screen writes need. That means no address conversion per frame, just simple bit shifting and rotation. Moving a star is then dead simple. I rotate its mask left with rlca, and when the bit rotates out of bit 7, it lands back in bit 0 with the carry flag set. That’s the signal I need to move the star one character cell to the left, which is one of the “cheap moves” from earlier. Because I’m tracking which cell each star is in, the collision check comes almost for free. That’s what the second pointer is for: STOCC (Star Occupancy) moves in step with STSCR through a table holding one byte per character cell. The “typewriter” routine sets a byte to 1 for every cell containing a text character or the cursor, so checking whether a star is hidden behind text is a single or instruction. If the cell is occupied, I skip drawing the star but carry on rotating and stepping it along until it reaches a cell where it can be displayed: ld l,(ix+STOCC) ld h,(ix+STOCC+1) ld a,(hl) ; A = the occupancy check for this cell or a ; test A against zero jr nz,stmove ; it's occupied, so leave the screen alone Parallax is still handled by moving each star a different number of pixels per frame, but the cost is a lot lower this time as I’m only touching the cells the stars actually occupy, rather than every byte of every row. Download the video That’s looking more like it! The typewriter routine is a simple copy of the font data from memory, and is very cheap as we can use the “cheap moves” to quickly fetch the 8x8 block. I also skip drawing any space character so it leaves the “occupancy cell” unmarked. This means the stars keep drifting through the gaps in the text - you can see here the odd star passing in between the words. I switched to ALL CAPS writing for the final version which helps the effect even more as the font is quite blocky, so you don’t really notice stars skipping the spaces inside a glyph at all. Misc Notes A few odds and ends that didn’t warrant a section of their own… VU Meters Some sort of graphic display bouncing along with the audio playback was another common early demo trope. I did originally consider a real-time graphic equaliser, but that would have been far too complex, probably in terms of spare cycles per frame, and definitely in terms of my limited mental capacity at this point! So I settled for a simple set of bouncing meter bars instead. It’s worth a mention mostly because it gives me an excuse to link to the awesome chibiakumas website! I just love the late 90s web design on display - one can argue that readability and accessibility take a hit, but it’s just so much fun to encounter a site that still looks like this. It’s a clear labour of love and there’s a wealth of content there as well, if your eyeballs are up to it. Their guide on Making Sound with the AY-3-8910 was what showed me how to get the data I needed to draw the meter bars. The examples write a register number to $FFFD to select a register, and I realised that once a register is selected, I could use the in instruction to read its value back out instead of writing a new one. As the page says, R8, R9 and R10 hold the channel amplitudes, so they feed nicely into the height parameter for drawing a bar. Getting the amplitude for channel A is then as simple as: ld a,8 ; Channel A register call ayread ; ... rest of code here ... ayread: ld bc,$FFFD ; Register select port out (c),a in a,(c) ret Twinkle, Twinkle, Little Star This is a simple effect that makes the header logo a little more interesting. I’m not sure where I originally got the idea - I’ve definitely seen something like it on a few late 80s/early 90s demos, though. All I wanted was to pick random star dots in the logo background and animate them. Here’s the header image loaded in ZXPaintbrush with the character cell grid showing. By happy chance, there are four stars which each sit neatly inside a single character cell with enough surrounding space, so they can be used to give an animated “twinkle” effect by drawing a cross out from the centre point and back in again. These dots map to the following co-ordinates, which target the top scanline of each fully-grown star (two pixels up from the centre): twkdata: defb 1,5 ; char row 0, col 5, centre y=3 ink 5 defb 1,20 ; char row 0, col 20, centre y=3 ink 1 bright defb 26,7 ; char row 3, col 7, centre y=4 ink 4 defb 25,22 ; char row 3, col 22, centre y=3 ink 6 bright The “twinkle” routine itself is simple enough: it adds a pixel on either axis to grow the star, waits a few frames, then shrinks it back down again. The interesting bit was how I made the twinkle order pseudo-random. Remember that my starfield was entirely pre-calculated from a data table generated by a Ruby script, which meant I’d never actually needed a random number generator in assembly. From Jonathan Cauldwell’s “How To Write Spectrum Games” e-book, I found another great “hack” which simply walks through the start of the Spectrum’s ROM, picking out random-looking bytes as it goes. As he says, he uses it in most of his games, and it does the job in a really simple way - as long as you stay below the block of structured, repeating data that sits 8Kb into the ROM. twkrand: push hl ld hl,(twkseed) ld a,h and 31 ; keep it within the first 8k of ROM ld h,a ld a,(hl) ; the "random" byte inc hl ld (twkseed),hl pop hl ret Out Of Space About halfway through writing the intro, I hit a fairly predictable problem: I had run out of memory. In an attempt to keep everything self-contained in one block of code, I was pulling all of the assets (the screen background, the player binary, the music file etc.) into the program itself, and then using the ldir block copy instruction to move each one into its correct location. Since that meant effectively storing every block twice, and the player routine had a hard-coded start address of $C000, I quickly ran out of room and my code started crashing into the playback routine. The simplest option was to split things out into separate code blocks, load each one directly to the address it needed to live at, and wrap the lot in a multi-stage BASIC tape loader: 10 CLEAR 32767 20 BORDER 0: PAPER 0: INK 0: CLS 30 LOAD "" CODE 40 LOAD "" CODE 50 LOAD "" CODE 60 RANDOMIZE USR 32768 This loads the main program, then the player routine directly into $C000, and finally the background screen straight into screen memory. It does mean the background starts appearing before the demo has fully loaded, but as I imagine most people will be seeing this running in an emulator or on my TNFS site, it doesn’t really matter. Plus, at this point I was in danger of disappearing down yet more rabbit holes (I still really want to work out how to do multi-colour border effects!) and I’d never have got this thing released. The Zen Of Z80 This whole project is a really weird one to look back at. On the one hand, I’ve fulfilled a childhood ambition and have something I’m very proud of. It’s also pretty unusual these days to end up with a binary where you can say pretty much what every single byte is doing and the story of how it got there. But on the other hand - I now have an answer for why I don’t code in assembly: It’s because I value (what’s left) of my sanity! The thing is, in order to write code like this you have to start thinking like the machine itself. You have to visualise the registers and the status flags flapping away, graphics as lines of bits slotted into strange orders, loops and conditionals as binary logic and so on. Getting this close to the hardware definitely brought about a greater sense of attachment to the hardware of my childhood, and it’s easy to see why assembly coders and sceners get so passionate about it. That said, I’m in no hurry to start re-writing my body of work in assembly! The projects I really enjoy are those that give me an excuse to learn something for the sheer hell of it, and to slow down a little from the ever-increasing velocity and demands of the modern work world. Going back to Z80 code is probably about the furthest I could have taken this, short of going into hardware hacking. I found it at times an intriguing puzzle, often a frustrating exercise that nearly had me throwing in the towel at several stages. This project has certainly taken the longest to go from the “idea” stage of any given feature to any kind of meaningful output. True, I didn’t spend every day of the last year working on it - my actual coding time was probably only a few hours over a couple of evenings a week. But I’ve certainly been thinking about it and pondering it most days (as my dogs will attest). The biggest breakthroughs and “aha!” moments usually didn’t come from behind the keyboard; they came from scribbles in my notebook and thinking about processor states while doing something unrelated. It has however given me massive appreciation and respect for the coders who pull this stuff off and make it look easy. And that goes double for those kids and bedroom coding superstars who managed all of this back in the 80s, and had to work it all out by experiment themselves. I only managed to get this far because of numerous books, online tutorials, GitHub repositories with sample code, YouTube videos… I can’t imagine how much longer this would have all taken me if the only resources I had were my Spectrum, a cassette tape recorder and a notepad. So, as I draw a line under this project - for now! - I raise a glass, and in the words of h0ffman: “Shout out to the hackers, graphicians and protrackers.”

an hour ago • 1 votes
CalVer 26.0: 10th Anniversary Edition

A decade ago, a little bit of history was made. I didn't realize it, but a colleague made a great point, one of those real mind-changing points that seem too obvious to admit same-day. But, the next day, calver.org was born. At the time my team maintained the Python infrastructure for eBay and PayPal, and we were stuck deciding whether we were really ready for a "major" 1.0 release. Semantic Versioning was the only game in town and "major" means "big", right?! Thankfully, a wiser colleague mentioned: Ubuntu and Twisted don't struggle with version number debates. They slap a date on it and keep shipping. In fact, their date-based versions were even better because you always knew where it stood, in terms of updatedness and support. The only problem is that no one really knew about it. Somehow, this problem solving versioning alternative, arguably as old as history itself, had gone nameless for millenia, conspiring to make me feel foolish in an office meeting. Never again! Ten years of adoption Fast forward 10 years, we've seen CalVer adopted by Apple, Nvidia, JetBrains, and countless others. (We have a timeline!) The site may have more inbound links than any other project of mine. Apple made the biggest jump, at WWDC 2025: iOS went from 18 to 26 macOS from 15 to 26 watchOS from 11 to 26 and visionOS from 2 to 26 All landing on one, consistent number like a car's model year. I still remember the texts from the Venn diagram fanbase of my friends who love Apple and reasonable versioning. No such texts from when NVIDIA announced calendar versions across the GPU Operator, RAPIDS, and its monthly NGC containers, but still very cool. Open source, too: Home Assistant, pip, CockroachDB, and yt-dlp all ship on dates, with plenty more on the users page. The conversation even reached the language core; PEP 2026 proposed versioning CPython as 3.YY, and it almost happened, too. And it's never too late, time marches on! Fixing the notation But I don't think I got every detail right from day 1. That's the main motivator for CalVer 26. It's high time to start righting a couple idiosyncratic token design choices, starting with some additions: Meaning Before 26.0 26.0 Full year YYYY YYYY Short year (6, 16) YY YY Zero-padded year (06, 16) 0Y 0Y Short month (1 ... 12) MM M Zero-padded month (01 ... 12) 0M 0M Short week (1 ... 52) WW W Zero-padded week (01 ... 52) 0W 0W Short day (1 ... 31) DD D Zero-padded day (01 ... 31) 0D 0D Seeing double First, the doubled letters. From the first version (16.6), MM and DD meant the unpadded month and day, which reads backwards to anyone who knows date formats (ISO 8601's YYYY-MM-DD, Java, moment.js, day.js), as some community members correctly pointed out. I was ready to flip them, until I checked what people actually use: most projects with a YY.MM.MICRO badge (conda, Twisted, Ansible's tooling) don't pad, and more than a dozen other version management tools (like bumpver and bump-my-version) implement the old meaning. So, it's too late to flip MM's meaning. Instead, 26.0 deprecates it and offers a more explicit and hopefully clearer option: M is the short month, 0M the padded one, and MM is a technically-retired synonym for M. In case you're wondering, the explicit 0M was me being overinspired by Ubuntu's approach, perhaps: 6.06 pads its month but not its year, and YY.0M says exactly that. To pad or not to pad I think it's worth a detour into why padding is even a thing anyways. It's become important now that new ecosystems have emerged that enforced SemVer formatting semantics, and I wanted clear guidance about on the spec site. SemVer forbids leading zeros outright, so Cargo rejects 26.04.0 and Go modules reject v26.04.0. Even Python's packaging spec normalizes leading zeros away, so you can tag 2026.08.19 if you want, but PyPI will still show 2026.8.19. NVIDIA's GPU Operator docs put it this way: "Zero padding is omitted for month to be still compatible with semantic versioning." CalVer was always intended to drop in where SemVer was used. So 26.0 recommends unpadded (YYYY.M.D) as a sane "pure" default for software libraries. But libraries are not the only objects of versioning schemes. The exception is a version that becomes a filename, an image tag, or an object-store key that gets listed lexically. There, padding keeps 26.10 sorted after 26.09, which is why Ubuntu, NixOS, and NVIDIA's own NGC containers pad. More evidence of teams designing their versions. We love to see it. Our FAQ has a longer discussion of the padding issue, as well. Optional segments There was never any rule against them, but 26.0 makes optional trailing segments more explicit with square brackets. Now, yt-dlp's scheme can finally be written down: YYYY.0M.0D[.MICRO]. For the CalVer badges I could find on GitHub, they all stay valid for now. I've got a note on the deprecated spellings and a new copy-paste badge section for new ones. What else is new? It's always a great time to add more citations to the site. A spec changelog; the spec now versions itself: 16.6, 19.7, 26.0. A FAQ: breaking changes, same-day releases, and padding. The users page, rebuilt by category, with past users of note (schemes change; that's fine) and tooling. Case studies: Apple and NVIDIA in, yt-dlp replacing youtube-dl. Much of the thinking behind these changes happened in the GitHub issue tracker over the years, and 19 or so issues close with this release. That's where ideas for CalVer should go, so by all means, open an issue, and we'll get it sorted! In due time, of course. In closing, I can't believe I still love belaboring these numbers so much. Thanks to all (but especially Mark, Glyph, Hugo, issue reporters, translators, and maintainers) for the discussion, and ultimately making the most timely versioning system a timeless classic. See also 2016 announcement Designing a version My Yap on Why CalVer beats Semver

yesterday • 1 votes
"You didn't do it right"

Four sincere attempts at OKRs, four failures, and every time: "you didn't do it right." Maybe the framework doesn't fit you -- so how do you find one that does?

yesterday • 1 votes
We solved SQLite's single-writer limitation

Multiple concurrent writers. Multiple processes. Same SQLite. No modifications.

3 days ago • 1 votes
If your team is happy, are you doing a good job?

Brilliant jerks, ZIRP-era managers, and how psychological safety lost the plot. Part 3 of my conversation with Dr. Cat Hicks.

4 days ago • 1 votes
📚 BoredReading

You seem to be enjoying this.

Join free to unlock everything.

Create free account

Already have an account? Sign in