Tweaking settings (or not) on my new MacBook Pro
November 12, 2024 at 11:42 AM by Dr. Drang
I got my new MacBook Pro last Friday, and so far only two things have annoyed me. The first I got used to much faster than I thought I would, and the second I’ve turned off in System Settings.
The initial surprise was how thick the menu bar was. For literally decades, I’ve been using Macs in which the menu bar is only slightly thicker than the height of the menu fonts. Here’s what the menu bar looks like on my 2020 MacBook Air:
And here it is on my new MacBook Pro:
I assume the menu bar is thicker because of the notch. I remember a lot of complaints about the notch when it first came out but not about the thicker menu bar. Maybe I just wasn’t paying attention because I wasn’t in the market for a new computer back then.
The notch itself doesn’t bother me, although it looks like I’ll have to start using some Bartender-like utility, as I’ve found that several apps I use regularly have menus that jump over to the right side of the notch and cover up some of my menu bar apps. I stopped using Bartender a couple of years ago when I noticed that I was no longer using enough menu bar apps to make it worthwhile. So I didn’t care about the recent kerfuffle regarding its change of ownership. Now I do, and I’ll have to look into alternatives.
Anyway, I’m happy to say that although I found the thick menu bar extremely annoying at first, it took less than a week for me to accept it. Now when I open my MacBook Air (as I did to take the screenshot above) its menu bar looks weird.
The second annoyance has to do with window tiling. I’m not a fan of tiling and have always thought that people who complained about the Mac’s lack of it were either crybabies or control freaks. Is it really that hard to manage windows? But live and let live. I assumed that whatever tiling features Apple added to Sequoia wouldn’t affect me. And they haven’t in the month or so that I’ve been using Sequoia on my MacBook Air.
But for some reason, as soon as I started using the MacBook Pro, windows were jumping to full size as I dragged them around. Apparently, I was dragging them up to touch the menu bar, which is the trigger for making a window fill the screen.1 After this happened a few times, I went into System Settings to see what I could do about it. Basically, all the tiling options were turned on, and I turned almost all of them off.
I kept the option for tiling when holding down the Option (⌥) key because I’m a tolerant person at heart. Maybe someday I’ll use it.
-
Could it be that I hit the menu bar while dragging windows because the menu bar is thicker? Or is that just a coincidence? ↩
Kilometers, miles, Fibonacci, and Lucas
November 11, 2024 at 3:31 PM by Dr. Drang
Earlier this month, I got an email from Anthony SEROU about my post on the golden ratio and converting between miles and kilometers. They suggested using consecutive Fibonacci numbers as a quick way to do the conversion, e.g., 5 miles is 8 kilometers. And if the amount you’re converting from is one away from a Fibonacci number, you can add or subtract 0.6 or 1.6, depending on which way you’re converting. So 6 miles is about 9.6 kilometers and 7 kilometers is about 4.4 miles.
I had my doubts about the value of this until this weekend. I went out on a bike ride with a friend, and when we were about finished, I looked at my watch and saw that we’d gone 13 km. When I told him, and he asked “What’s that in miles?” I had the answer almost immediately because of Anthony’s email. I say “almost” because I spent fraction of a second being surprised that I actually had a Fibonacci number in front of me.
But my answer was fast enough to raise a question in my friend’s mind. He texted me later to say that I was right about it being 8 miles. I didn’t explain how I knew it so quickly—I have enough of a reputation as a oddball. Letting him think I could multiply quickly was easier than explaining Fibonacci numbers, the golden ratio, and the coincidence regarding the conversion between miles and kilometers.
Today it occurred to me that the ratio of consecutive Lucas numbers also converge to the golden ratio, so maybe they could be used the same way. So I fired up Wolfram and made a plot of the ratio of consecutive numbers against the larger of the two. In other words, I plotted y values of
against x values of for the Fibonacci numbers and similarly for the Lucas numbers. Here’s the result, where the golden ratio is the dashed black line:
So while the Lucas number ratios converge to the golden ratio, they don’t do so as quickly as the Fibonacci numbers. Just as well. Only real oddballs know the Lucas numbers.
Incrementing file version numbers with Python and Perl
November 9, 2024 at 4:57 PM by Dr. Drang
Yesterday my old vaudeville partner,1 Dan Sturm, wrote a post describing a Keyboard Maestro macro he wrote that increments the version number in a file name. It’s the sort of thing I could have used years ago when I was writing reports and analysis scripts that often had to be updated—but not overwritten—as new data came in. I’m not sure I’d have much use for it now, but it’s nice to know where to go if I need it.
The thing that caught my interest, though, was how Dan, with the help of Peter Lewis via the Keyboard Maestro forum, adjusted the macro to handle filenames with version numbers of different widths. If you give it a filename of
blue-image 03.jpg
the incremented version will be
blue-image 04.jpg
But if you start with
blue-image 0003.jpg
the increment will be to
blue-image 0004.jpg
The version numbers are always zero-padded integers, but the macro is smart enough to figure out how many digits the original name has so it can use the same number of characters in the incremented name.
Thinking about the post last night, I wanted to see if I could write a little code that was as smart as Dan’s macro. I didn’t want to reproduce his macro’s functionality, I just wanted to see if I could do the part that increments the version number while maintaining its character width. As it turns out, both Python and Perl have relatively simple ways to do it.
Here’s my Python script:
python:
1: #!/usr/bin/env python3
2:
3: from sys import stdin
4: from re import fullmatch
5:
6: def increment(fn):
7: 'Return the given filename with an incremented version number'
8: try:
9: front, vers, ext = fullmatch(r'(.+?)v?(\d+)(\..+)', fn).group(1, 2, 3)
10: return f'{front}{int(vers)+1:0{len(vers)}d}{ext}'
11: except AttributeError:
12: return f'!!!{fn}!!!'
13:
14: # Print the incremented filename
15: print(increment(stdin.read().rstrip('\n')))
The important part is the increment
function. Because I wasn’t looking at Dan’s post when I wrote this, I didn’t use his regular expression to parse the filename and break it into parts. But I remembered that he split it into three parts:
- Everything in front of the version number. This can include path components.
- The version number itself, possibly with a leading “v” and padded with zeros. The leading “v” should be removed from the filename in the incremented version.
- Everything after the version number, which should be just a period and the extension.
The regex that does this parsing is in Line 9 and works this way:
Looking back on it, I see that I was more restrictive than Dan about what comes after the version number, but that’s OK.
I used Python’s fullmatch
function from the re
library. There are other ways to do it, but since my regex covered the entire string, fullmatch
seemed like the best choice. If there is a match, fullmatch
returns a match object. The group
function then pulls out the three parenthesized groups, which are assigned to the front
, vers
and ext
variables.
The successful return value of increment
is defined by the f-string in Line 10. The replacement fields (the parts inside curly braces) of the f-string are defined this way:
What’s great about f-strings is that you can use expressions in the replacement fields, not just variable names, and you can nest them. The expression
int(vers)+1
does the incrementing. The formatting applied to that value is
0{len(vers)}d
where the len(vers)
expression is evaluated at run time to determine the character width of the version number in the input string.
Of course, we get that return value only if the fullmatch
is successful. If it isn’t, the return value is defined by the except
block. In this case, the original filename surrounded by exclamation points is returned. So, for example, if we give increment
an argument without a version number,
blue-image.jpg
we’ll get something out that should alert us that something went wrong:
!!!blue-image.jpg!!!
Line 15 handles both the collection of input from stdin and the writing of output to stdout. The rstrip(\n)
function makes sure that any trailing newlines, which are common in stdin, are removed before we run the string through increment
.
As you might expect, the Perl code is shorter and a little more cryptic:
perl:
1: #!/usr/bin/env perl -nl
2:
3: # Extract the individual parts of the filename from stdin
4: ($front, $vers, $ext) = /^(.+?)v?(\d+)(\..+)$/;
5:
6: # Print the incremented filename
7: printf("%s%0*d%s\n", $front, length($vers), $vers+1, $ext)
The -nl
options in the shebang line get the input string from stdin and strip any trailing newlines. Line 4 is basically the same as Line 9 in the Python code; the ^
and $
anchors are needed here to make sure we’re matching the entire string.
The trick in the Perl code is the asterisk in the printf
statement on Line 7. It acts as a placeholder for the second argument after the format string, which is length($vers)
. The rules for this are covered in the minimum width section of the sprintf
documentation.
Part of the reason the Perl code is shorter is that I didn’t give it any error handling. But Perl is just naturally more terse than Python. “Explicit is better than implicit” is not part of the Perl mantra.
My thanks to Dan for giving me something to puzzle over. I don’t want anyone to think I’m trying to “improve” his code. I just wanted to see if I could do dynamic formatting in languages I use regularly.
-
You remember our act, Sturm und Drang, don’t you? ↩
Semi-automated plotting
November 6, 2024 at 11:18 PM by Dr. Drang
The Matplotlib code in the last post was initially generated with a Typinator abbreviation that I tweaked to make the final script. After writing the post, I decided it would be nice to have a second, similar abbreviation. This post shows you both.
Typinator is like its more well-known competitor, TextExpander, except it hasn’t shifted its focus to “the enterprise,” doesn’t require a subscription, and doesn’t advertise on podcasts. Although the abbreviations discussed here are built for Typinator, they could be easily adapted to TextExpander, TypeIt4Me, or Keyboard Maestro. (Keyboard Maestro isn’t, strictly speaking, an abbreviation utility, but it can be used as one. In fact, the first abbreviation we’ll see started life as a Keyboard Maestro macro.)
For several years I’ve had an abbreviation1 called ;plot
, which, when invoked, brings up a little window that looks like this:
After you enter the title and axis labels, it inserts the following text:
python:
1: #!/usr/bin/env python3
2:
3: import matplotlib.pyplot as plt
4: from matplotlib.ticker import MultipleLocator, AutoMinorLocator
5:
6: # Create the plot with a given size in inches
7: fig, ax = plt.subplots(figsize=(6, 4))
8:
9: # Add a line
10: ax.plot(x, y, '-', color='blue', lw=2, label='Item one')
11:
12: # Set the limits
13: # plt.xlim(xmin=0, xmax=100)
14: # plt.ylim(ymin=0, ymax=50)
15:
16: # Set the major and minor ticks and add a grid
17: # ax.xaxis.set_major_locator(MultipleLocator(20))
18: # ax.xaxis.set_minor_locator(AutoMinorLocator(2))
19: # ax.yaxis.set_major_locator(MultipleLocator(10))
20: # ax.yaxis.set_minor_locator(AutoMinorLocator(5))
21: # ax.grid(linewidth=.5, axis='x', which='major', color='#dddddd', linestyle='-')
22: # ax.grid(linewidth=.5, axis='y', which='major', color='#dddddd', linestyle='-')
23:
24: # Title and axis labels
25: plt.title('Sample')
26: plt.xlabel('Date')
27: plt.ylabel('Value')
28:
29: # Make the border and tick marks 0.5 points wide
30: [ i.set_linewidth(0.5) for i in ax.spines.values() ]
31: ax.tick_params(which='both', width=.5)
32:
33: # Add the legend
34: # ax.legend()
35:
36: # Save as PDF
37: plt.savefig('20241106-Sample.pdf', format='pdf')
This is not quite a full program for producing a graph—the x and y data aren’t defined—but it’s a good start. You’ll note that the items we entered in the input fields show up in Lines 25–27 and 37.
Much of the code is commented out. As I said in the previous post, Matplotlib has decent defaults for many graph features, but I usually like to customize some of them. The commented code is there to remind me of the functions that are needed for some common customizations. I uncomment the lines for the things I want to customize and adjust the function parameters as needed.
The abbreviation is defined with this as the text that replaces ;plot
:
1: #!/usr/bin/env python3
2:
3: import matplotlib.pyplot as plt
4: from matplotlib.ticker import MultipleLocator, AutoMinorLocator
5:
6: # Create the plot with a given size in inches
7: fig, ax = plt.subplots(figsize=(6, 4))
8:
9: # Add a line
10: ax.plot(x, y, '-', color='blue', lw=2, label='Item one')
11:
12: # Set the limits
13: # plt.xlim(xmin=0, xmax=100)
14: # plt.ylim(ymin=0, ymax=50)
15:
16: # Set the major and minor ticks and add a grid
17: # ax.xaxis.set_major_locator(MultipleLocator(20))
18: # ax.xaxis.set_minor_locator(AutoMinorLocator(2))
19: # ax.yaxis.set_major_locator(MultipleLocator(10))
20: # ax.yaxis.set_minor_locator(AutoMinorLocator(5))
21: # ax.grid(linewidth=.5, axis='x', which='major', color='#dddddd', linestyle='-')
22: # ax.grid(linewidth=.5, axis='y', which='major', color='#dddddd', linestyle='-')
23:
24: # Title and axis labels
25: plt.title('{{?Plot title}}')
26: plt.xlabel('{{?X label}}')
27: plt.ylabel('{{?Y label}}')
28:
29: # Make the border and tick marks 0.5 points wide
30: [ i.set_linewidth(0.5) for i in ax.spines.values() ]
31: ax.tick_params(which='both', width=.5)
32:
33: # Add the legend
34: # ax.legend()
35:
36: # Save as PDF
37: plt.savefig('{YYYY}{MM}{DD}-{{?Plot title}}.pdf', format='pdf')
Virtually all the text is expanded verbatim. The exceptions are the plot title and axis labels in Lines 25–27 and the filename in Line 37. As you can see, Typinator uses special codes with curly braces to insert the non-verbatim material. For example
{{?Plot title}}
in Lines 25 and 37 tells Typinator to ask for input and inserts the text provided in the
field. Similarly{YYYY}{MM}{DD}
in Line 37 inserts the four-digit year, the two-digit month, and the two-digit day. The last two will have leading zeros, if necessary.
You don’t have to remember the special curly brace syntax. When you’re defining an abbreviation, Typinator lets you choose the special marker code from a popup menu.
As you can see, there are submenus for the various date and time formats. When you choose an input field, you get this dialog to define the input and what kind of data it can hold:
Note that there’s a way to define a default value. That can be helpful in filling out the input fields quickly. What’s also helpful is that if a field doesn’t have a default value, Typinator remembers what you entered the last time the abbreviation was used and opens the input window with those values in the appropriate fields.
Although ;plot
was helpful in writing the code for the last post, I realized when I was done with all the customizations that it wasn’t especially helpful with the x-axis. Matplotlib has different functions for customizing a date and time axis, and none of those functions are in ;plot
.
I thought about adding commented-out lines with date and time functions to ;plot
, but decided it would be cleaner to have a completely different abbreviation, ;dateplot
, for date and time series plotting. Here’s its definition:
1: #!/usr/bin/env python3
2:
3: from datetime import datetime
4: import matplotlib.pyplot as plt
5: from matplotlib.ticker import MultipleLocator, AutoMinorLocator
6: from matplotlib.dates import DateFormatter, YearLocator, MonthLocator
7:
8: # Create the plot with a given size in inches
9: fig, ax = plt.subplots(figsize=(6, 4))
10:
11: # Add a line
12: ax.plot(x, y, '-', color='blue', lw=2, label='Item one')
13:
14: # Set the limits
15: # plt.xlim(xmin=datetime(2010,1,1), xmax=datetime(2016,1,1))
16: # plt.ylim(ymin=0, ymax=50)
17:
18: # Set the major and minor ticks and add a grid
19: # ax.xaxis.set_major_locator(YearLocator(1))
20: # ax.xaxis.set_minor_locator(MonthLocator())
21: # ax.xaxis.set_major_formatter(DateFormatter('%-m/%Y'))
22: # ax.yaxis.set_major_locator(MultipleLocator(10))
23: # ax.yaxis.set_minor_locator(AutoMinorLocator(5))
24: # ax.grid(linewidth=.5, axis='x', which='major', color='#dddddd', linestyle='-')
25: # ax.grid(linewidth=.5, axis='y', which='major', color='#dddddd', linestyle='-')
26:
27: # Title and axis labels
28: plt.title('{{?Plot title}}')
29: plt.xlabel('{{?X label}}')
30: plt.ylabel('{{?Y label}}')
31:
32: # Make the border and tick marks 0.5 points wide
33: [ i.set_linewidth(0.5) for i in ax.spines.values() ]
34: ax.tick_params(which='both', width=.5)
35:
36: # Add the legend
37: # ax.legend()
38:
39: # Save as PDF
40: plt.savefig('{YYYY}{MM}{DD}-{{?Plot title}}.pdf', format='pdf')
It imports a few more libraries near the top, uses datetime
data to define the x-axis limits on Line 15, and has date-related functions in the code on Lines 19–21 for customizing the x-axis ticks and tick labels. Otherwise, it’s the same as ;plot
.
With this new abbreviation, I should be able to build time series graphs more quickly than before.
-
You may know them as “snippets,” because that’s the term TextExpander uses. Typinator uses “abbreviations,” so that’s what I’ll call them. ↩