Friday, January 27, 2023

is it time to party like it's windows 95?

Over a decade a go I wrote why windows' taskbar beats osx' dock. In the intervening years I've gotten pretty used to the Mac Dock, (using SizeUp for awesome and intuitive keyboard based window management, and then HyperSwitch to fix the DUMBEST thing in MacOS, because switching to an application with no windows open should open a default window when you switch to it with cmd-` just like it does if you click on its icon in the dock) but just recently I realized there's an app called uBar that lets Mac have a very Windows 95-y/XP-ish button bar.

You can set it with window titles ala Windows 95, or just icons, and with our without the "grouping by app" that later Windows copied from Mac.

Conceptually I like it. I've gotten used to App, then Window thinking that Mac encourages, but honestly I leave a LOT of windows, especially browser windows, just hanging out. (Combine that with how both the Mac and browser assume I want to reopen everything whenever possible, which honestly is totally missing the point of restart/reboots, and it's significantly annoying) Seeing a button or icon for each window helped me keep things neat.

There were enough small annoyance (some programs used wonky icons or would open up a gratuitous second uBar icon even if only one window was visible) that I probably won't stick with, but I found it cleaner and more straight forward than Ventura's Stage Manager. One icon per window still makes a lot of sense. (I guess in the mean while I'll try to remember to ctrl-down-arrow a lot to see how many windows I have lying around, and then cmd-w to close so they won't try and reopen en masse)

public service announcement

I just backed up my computer and my website - kinda the old fashioned way, dumping a single root (or two) - ~/data on my Mac, ~/sites and ~/data for my website - onto an external solid state HD. I guess I prefer that to those more automated systems, just like I don't really trust my photos to iCloud (and anything I consider important I make sure is in ~/data/photos )

Just a friendly reminder to pick a backup plan and run it :-D

working with europeans

 My new team is based in Western Europe. I enjoyed seeing this entry for tags in our design system:

Wednesday, January 25, 2023

$5,000 hammer

I spent a workday getting understanding of a complex Figma to JSON recursive translation process. And the fix was one line. it made me think of this joke:

The Graybeard engineer retired and a few weeks later the Big Machine broke down, which was essential to the company’s revenue.  The Manager couldn’t get the machine to work again so the company called in Graybeard as an independent consultant.

Graybeard agrees. He walks into the factory, takes a look at the Big Machine, grabs a sledge hammer, and whacks the machine once whereupon the machine starts right up. Graybeard leaves and the company is making money again.

The next day Manager receives a bill from Graybeard for $5,000. Manager is furious at the price and refuses to pay. Graybeard assures him that it’s a fair price. Manager retorts that if it’s a fair price Graybeard won’t mind itemizing the bill. Graybeard agrees that this is a fair request and complies.

The new, itemized bill reads….

Hammer:  $5

Knowing where to hit the machine with hammer: $4995

Hammer time!

Sunday, January 22, 2023

"good enough" guid/uuid in PHP?

So once upon a time I used 

$key = time().".".getmypid();   

For a UID (unique ID) in PHP. Maybe I wanted things to sort a bit? But of course there's always plain old
$key = uniqid("",true);

(which says "" for prefix, and more entropy.)

if(preg_match('/[^\.a-z_\-0-9]/i', $id)){
    echo "bad id";

could be a check if you're using this for a filename, and want to make sure it's not too dangerous to try and open... (shenanigans changing the folder or escaping out)

Friday, January 20, 2023

bookmarkable/link-based random order list shuffler (php + stateless)

I made up a trivial list shuffler so that my zoom-based scrum standup could avoid the awkwardness of figuring out "who goes next".

You can bookmark the page, e.g. and everytime you reload it will get a new shuffle. 

(I do like that paradigm of putting everything in a link so I don't have to store anything in server or on browser, and putting everything in a textarea so that I don't have to build much UI, and the content can be copy and pasted freely - same idea I used for my quick poll and pill tracking sheet generator but here it's even cleaner since I don't have to make up a format, just items separated by returns)

PHP remains the simplest, lowest-friction way for me to make and deploy something like this in like 10 minutes, borrowing from my own html boilerplate and my no-warnings cgi param functions

<!doctype html>
<meta charset="utf-8">
<title>bookmarkable random list order shuffler</title>
body {
    background-color: white;
    color: black;
    font-family: sans-serif;    
.content {
    width: 800px;
    margin: auto;
textarea {
  <div class="content">
        <h1>bookmarkable random list order shuffler</h1>
            $rawitems = trim(getval("items"));            
            if($rawitems != '') {
                $items = explode("\n",$rawitems);
                echo "<ol>";
                foreach($items as $item) {
                    echo "<li>".htmlspecialchars($item);
            echo "</ol>";
        Trivial random order list shuffler. Add one item per line, and hit the button. You
        can then bookmark the resulting page to get a new order each time.
        Great for deciding order at team standup meetings!
        <form method="GET">
            <textarea name="items"><?echo htmlspecialchars($rawitems) ?></textarea>
 function getval($key, $default = ''){
    return isset($_GET[$key]) ? $_GET[$key] : $default;    

(Incidentally that code textaarea is a nice freebie I get from using's HTML Escape - that's weirdly convenient, better than times in the past where I put in code that breaks my blog template.)

Wednesday, January 18, 2023

wikipedia update and webdesign through the ages

 A page on Wikipedia's new look on desktop (with its sadly non-interactive slider letting you peek at old looks of it) reminded me of this page I posted before, a fantasy of potential NASA web design through the ages of the web...

Thursday, January 12, 2023

ux? dx? how about tx, team experience

Joining a new team for a 3-6 month stint.

They have a lot of decisions to make about their process. 

It was a chance for me to link to my own blog entry overlapping sprints to deal with the harsh realities of scrum - I'm actually a bit of consultant for the team, so I can't push this idea too much (and there are certainly arguments against it, especially in terms of meeting expectations of syncing with other tteams)  but it at least gets people thinking about some challenges that every team faces. Also I got to low key show off that I have a developer blog (not that the design of this site is very modern...)

Team mates definitely saw truth in the "QA always gets hosed" angle. The challenge is... how to get away with that. In the group chat I then wrote:

Yeah, it's human nature. I suppose just like there "UI/UX", as well as folks talking about "DX" for design experience, there's like... TX for Team Experience? So like UX, you have to accept there are some things people are inclined to do, and that "yelling at them not to do that, and insisting they be better behaved" should only be part of the solution. (Case in point, saying "devs! stop muscling out QA time and forgetting that the end of the sprint is for everything not just your work!")

 So yeah... TX!

Wednesday, January 11, 2023

regex-like stuff manually is kind of tough!

The other month I wrote about tuning my (admittedly bare-bones) UI to my homebrew CMS for my website - upgrading the previous behavior where highlighted text could be changed into a link (and heuristically figuring out if the highlighted text was a URL, or should be used as the clickable text for a link) so that ALL URL-looking strings that weren't already part of a clickable link would be linkified.

But the regex I thought of for that (URLs start with http:// or https:// , end with white space or the end of string, but don't match if the character before is a single or double quote) was beyond me.

A friend on a private Slack (after apolgetically reminding me of the famous You can't parse [X]HTML with regex Stackoverflow) gave me a version that worked using a "negative lookbehind". But, lo and behold, THE DANG THING DOESN'T WORK ON SAFARI. (I realized, trying to figure out why it wasn't working on my iPhone, where it promised to be most useful...)

When I pointed that out to my friend, he grumbled about that's why he steered his career the hell away from frontend dev, which is a fair point. It's the ubiquity of javascript in browsers that powers much of its popularity, and we're past the age when we needed JQuery to iron out the differences, but still, you're kind of at the mercy of the browser for this kind of thing.

Anyway, I decided to try and do the matching and rewriting "by hand" - it turned out to be kind of a fun "CompSci class" problem - aided a bit with the new ECMAScript array functions, which actually guided me to a more modular design, rather than trying to do it all in one big wacky loop.

here's a codepen for it - the steps were:

1. Find the indices of everything starting with https:// 

2. Filter out those matches when the character before the match is a single or double quote

3. Map those matches from simple offsets to [offset, length of string until whitespace or end of string]

4. reverse the list of those [offset, length]  (since we're going to be operating on a string in place, we start at the end so that earlier offsets are still valid) and for each wrap the url with <a href=" before ad "></a> after.

It was nice that I was still able to use my earlier "testLinkify()" function to make sure I was hitting some edge cases properly (I think there might still be some weird edge cases lurking but nothing I'd hit in normal use)

function testLinkify(){ console.log('starting test---------------------------'); test(``, `<a href=""></a>`); test(`\n`,`\n<a href=""></a>`); test(`<a href=""></a>`,`<a href=""></a>`); test(`<a href=""></a>`,`<a href=""></a> <a href=""></a>`); test(`\n <a href="">BAZ</a>`,`<a href=""></a>\n<a href=""></a> <a href="">BAZ</a>`); } function test(input,expect){ const testFunction = linkifyBareHttps; const output = testFunction(input); // console.log (output === expect ? 'PASS':'FAIL'); if(output !== expect) { console.log(`FAIL INPUT: ${input} EXPECT: ${expect} OUTPUT: ${output}`); } else { console.log('PASS') ; } } function linkifyBareHttps(inputText){ //console.log(frameMatches('foo', 'fooAB fooB "foo bar fooC\nfooD',['"'], '<a href="', '"></a>')); return frameMatches('https://', inputText,["'",'"'],'<a href="', '"></a>'); //.replace(replacePattern1, '<a href="$2"></a>'); } function getIndicesOf(needle, haystack) { let needleLen = needle.length; let startIndex, index = 0; const indices = []; while ((index = haystack.indexOf(needle, startIndex)) > -1) { indices.push(index); startIndex = index + needleLen; } return indices; } function isPrefixedBy(offset,haystack,prefixes){ if(offset === 0) return false; const match = prefixes.includes(haystack.charAt(offset-1)); return match; } function getIndicesOfNotFollowing(needle, haystack, prefixes){ return getIndicesOf(needle,haystack).filter( (x)=> ! isPrefixedBy(x,haystack,prefixes)); } function isWhiteSpace(c){ return c === ' ' || c === '\t' || c === '\n' || c === '\r'; } function getLengthTilWhiteSpace(haystack,start,maxOffset){ let ptr = start; while(ptr < maxOffset && !isWhiteSpace(haystack.charAt(ptr))) ptr++; return ptr-start; } function getPrefixFilterOffsetsAndLengths(needle, haystack, prefixes){ const indices = getIndicesOfNotFollowing(needle, haystack, prefixes); return,i)=>[idx, getLengthTilWhiteSpace(haystack,indices[i],i < indices.length -1 ? indices[i+1] : haystack.length)]); } function frameMatches(needle,haystack,blockingPrefixes,before,after){ const placements = getPrefixFilterOffsetsAndLengths(needle, haystack, blockingPrefixes).reverse(); let newString = haystack; placements.forEach(([offset,length]) => { newString = newString.substring(0,offset) + before + newString.substring(offset,offset+length) +after + newString.substring(offset+length); } ); return newString; } //console.log(frameMatchesNotPrefixedBy('foo', 'foo foo afoo bar foo','(',')',['a'])); //console.log(frameMatches('foo', 'fooAB fooB "foo bar fooC\nfooD',['"'], '<a href="', '"></a>')); //console.log(frameMatches('foo', 'XXX fooAB YYY',['"'], '<a href="', '"></a>')); testLinkify();

Friday, January 6, 2023

keeping track of what has the focus...

 Running this little snippet in the console, so I could keep track of where focus was as I clicked around and close things:

Wednesday, January 4, 2023

"You are not buying from a supplier, you are a raccoon digging through dumpsters for free code."

On a private slack (fun way to get much of the pleasure of social media without being in a global space... if you can find the right one) someone posted I Am Not A Supplier, about the the sometimes surprisingly brittle "Software Supply Chain". My thoughts were:

It's a tough one. Like my first thought is, what if there was a way of paying for the software libraries. (admittedly the renumeration would end up looking more like Spotify :-D ). But then a lot of individual folks wouldn't actually want to be legally liable if their code was responsible for something...

I know I suffer from "NIH"/Not Invented Here, and I prefer to keep things vanilla and what's baked into a well-established thing like a browser or PHP itself. But I also know that doesn't scale, and when you get to something seriously mathy and big, like an SSL library or what not, it's hopeless.

It does make me think back to the Linux vs Microsoft days. I guess one thing is Microsoft was willing to take on more liability, top to bottom. But cheaness (along with some more elegant design philosophies) won out...

Definite shades of that "I’m harvesting credit card numbers and passwords from your site. Here’s how." I wrote about a few years ago.

Sunday, January 1, 2023

getting inclusive date range in php

As I assemble this year's Photos of the Year for my blog, I decided to make a tool to simplify a task I do a lot, which is taking the HTML for an image in one gallery and putting it into a new gallery. (Using the homebrew css-based photogallery I made a while back.)

Anyway, I had to look up how to get a range of dates in PHP (inclusive of the endpint, which needed an extra "modify"). So here's my code at the moment, which builds a form with two input fields (defaulting to last month), with sensible defaults for most things, and shows you the dates betwee 

    $now = date('Y-m-d');
    $end = requestval('end',$now);
    $before = date('Y-m-d',strtotime("-1 month"));
    $start = requestval('start',$before);
    <label>start <input type="date" name="start" value="<? echo $start ?>"></label>
    <label>end <input type="date" name="end" value="<? echo $end ?>"></label>
$periodstart = new DateTime($start);
$periodend = new DateTime($end);
$periodend->modify( '+1 day' ); 
$period = new DatePeriod($periodstart,new DateInterval('P1D'),$periodend);
foreach ($period as $key => $value) {
    $day = $value->format('Y-m-d')       ;
    echo "<li>$day";

function requestval($key, $default = ''){
    return isset($_REQUEST[$key]) ? $_REQUEST[$key] : $default;    

I like how PHP has such useful date stuff baked in, even if the API is kind of messy.