Creating a Simple Pushdown Stack

I recently asked about how to capture the Recent Files list of an app as a list, but it turned out that was not the solution to my real problem. Fortunately, I found a simple solution that may be useful in other cases: a pushdown stack.

What I wanted was a list of files in order by most recently accessed.

Unfortunately, it turns out that the app in question (my clone of TextEdit called DeskSpaceID), like many apps, maintains its Files > Open Recent files list in most recently opened order. Since the opening of the files is ancient history compared to when they were accessed, the Open Recent files list turned out to be useless for my purposes, and that list was obtainable by simpler means, if I needed it. (I certainly am not going to figure out how to change the app. It's built-in Apple stuff that is way beyond my pay grade.)

Creating the pushdown stack seemed daunting at first, and then turned out to be super easy. Here's the block from the subroutine that uses it:

This subroutine gets called by a macro that provides the newly accessed filename. If the global list does yet exist or is empty, that filename becomes the first item in the list.

If there already is a list, the list gets edited to delete the new filename from the list, if it's already there. Then the new filename gets added at the top of the list.

That's it. It's been working well for me for a couple of days, with lots of use.

I did run into a problem when the calling macro couldn't find the file and provided an error message instead of a file name. The error messages were unique, so the stack filled up with those, and they were never deleted because the older ones were never exactly replaced. I fixed the calling macro to not go here when there's an error.

Also, I made extensive use in fine-tuning this, including discovering the above error problem, by referring to the KBM > Preferences > Variables window. Selecting the global variable that is the stack made it easy to watch the stack change as it was used.

In the above image of the Variables window, there's an error message instead of a filename in the 10th entry. The Preferences: Variables window allows me to manually edit the variable and delete that line while I also fix the condition that produced it.

I've used this method in a couple macros, and there's just one more bit you might want to add (or maybe you have this elsewhere): A way to limit the length of the global. You can do this using a simple shell command:

echo "$KMVAR_GlobalRecentDeskSpaces" | tail -n 20

That would echo out the last 20 lines of the variable, which you could then save back to the global. And to determine when you need to run it, you can filter the global with Line Count, and only run it when Line Count > your limit.

-rob.

1 Like

Thanks. That's a good idea, and no, I don't have that built yet.

Hi, @August, thanks for the post. I thought I'd pass along a few things I've learned when doing similar tasks.

Unless, @peternlewis or someone else on the forum can tell us otherwise, it seems that there is no difference between the conditions does not exist and is empty (at least in this context).

Try these macros (one for a global variable and one for a local variable) and see if you agree.

DNE vs IE Tests Macros.kmmacros (45.9 KB)


Better yet, you don't need the If Then Else action as you can prepend (or append) to a variable that doesn't exist.


Finally, one thing I've learned in cases like this is to do a little housekeeping after updating a stack. Of course if the stack is updated very carefully, housekeeping shouldn't be required, but from a practical standpoint, it seems prudent.

In this example, after updating the stack:

  1. leading and trailing whitespace is removed (for all stack entries),
  2. leading whitespace-only lines are removed,
  3. trailing whitespace-only lines are removed, and
  4. whitespace-only lines between stack entries are removed.

2023-12-04 17:5329 EST, Update: Added local_MaxStackSize

Download: Push to Stack.kmmacros (166 KB)

Macro-Image


Since @August is prepending (i.e., stacking) you'd want to use the head command, right?

The housecleaning script could be modified to:

#!/bin/bash
PATH=$PATH:/usr/local/bin

#
# For a multiline variable:
#   1. For each line, delete the leading and trailing whitespace
#   2. Delete all whitespace-only lines
#

text_in="$KMVAR_stackRecentDeskSpaces"

text_out=""

while IFS= read -r line; do
    line="${line#"${line%%[![:space:]]*}"}"
    line="${line%"${line##*[![:space:]]}"}"
    if [ -n "$line" ]; then
        text_out+="$line"$'\n'
    fi
done <<< "$text_in"

echo "$text_out" | head -n "$KMVAR_local_MaxStackSize"

I've updated the macro Push to Stack (above) to include local_MaxStackSize.

I don't think so, unless I put my brain on backwards again this morning :). Let's say you have this list:

Ocean
Serenity
Whisper
Meadow
Twilight
Harmony
Radiance
Journey
Silence
Enigma
Echo

Echo was the 11th item added, but your limit is 10, so you want to keep rows 2 through 11. That's tail:

$ echo "$myvar" | tail n -10
Serenity
Whisper
Meadow
Twilight
Harmony
Radiance
Journey
Silence
Enigma
Echo

But if I use head:

$ echo "$myvar" | head -n 10
Ocean
Serenity
Whisper
Meadow
Twilight
Harmony
Radiance
Journey
Silence
Enigma

Now I've lost my newest entry, because that reads the first 10 lines in the variable, not the last 10 lines.

-rob.

I used to have a macro(s) that handled stacks and queues of strings for me. But that was before subroutines existed. Now that subroutines exist, which can return variables, it would be even simpler to do.

In fact, one of the parameters my routines accepted was the name of the queue or stack. So it was one macro that handled all queues and stacks by passing the name as one parameter.

Hi Rob. Jim is right, I'm PREpending new entries at the top of the list. It's a most recent list.

In your example list of "Ocean ... Echo", "Ocean" would be the most recent addition, so to truncate to the most-recent 10, I'd use head -10.

Yep, and I tend to use appending, hence why I went backwards :).

-rob.

Generally, for variables, yes that is correct.

A variable that does not exist is obviously empty, and a variable that has a value obviously exists.

So the only interesting case would be a variable that exists but is empty. However setting a variable to empty deletes it (from the engine’s perspective, the editor may still know about the variable in other ways).

Thanks for the tips on further simplifying my already pretty simple macro.

That suggests my basic subroutine can be simplified to simply deleting any literally matching line that currently exists, which does not require that the variable exist or be non-empty, and then prepending the line to the variable, which again does not require that the variable exist or be non-empty.

I could also prevent the list from growing too long without having to filter any line counts or evaluate any conditionals simply by adding a third action that deletes the 21st (or whatever) entry, if there is one.

The third action is new to v11 and the trigger for the whole subroutine (not shown) is the Space Changed trigger, which is also v11.

2 Likes

Is there a way to delete the line and the %Return% delimiter in one step? This action:

image

leaves an empty line, I think. I suppose that I could add a second action to look for empty lines and delete them, but I'd love to do it all in one step.

Any ideas?

The shell command "grep ." deletes all empty lines from a file or variable, like this:

image

1 Like

That requires initiating a shell. I'm hoping to do it all in one command, deleting the content of the line along with the %Return% delimiter, but haven't come up with it yet.

Right now, this is what I have, all KBM built-in actions, without (I hope) having to initiate a shell:

image

Just curious, why is that a problem? It will take .002 seconds or so to execute, so it won't slow your macro in any meaningful way. Why the aversion to a shell/desire to do it in one command?

-rob.

Because it's part of a sequence that happens every time I change Desktop Workspaces and which sometimes can take as long as 2 sec, it seems -- which is long enough that I get where I'm going and start to read, work, find my place, etc. when >>POP<< comes the notification that I'm in the new workspace and all is right with the world. That's annoying. I'm trying to improve it.

If that notification happens in about 1/2 sec, then my experience is simply a flow of: I change Spaces, I get the notice that I am where I want to be, I go on. Calm, reassuring, comfortable.

So I'm looking at what I can shave off. I can see what that 0.5 - 1.0 sec feels like if I leave out all kinds of housekeeping, error conditions, and stuff that makes a difference elsewhere, or predefine things for testing that have to be queried for in reality, which I don't want to do or can't do on a regular basis. So I'm exploring what I can tighten up.

To find out where the time is going, you might try timing some of the routines—my MultiTimer macro does just that, and lets you get subtotals just by inserting calls wherever you need them. You might be surprised to find where the time is going. I know I was when I worked on optimizing my biggest time-consuming macros.

-rob.

Sounds good. Thanks. (Just what I need, a branching rabbit hole.)

1 Like

What's wrong with this picture?

I made the erroneous assumption that the initial Set Variable action would somehow only execute IF there already was an Item #21 in the list. Silly me. When does KBM ever require variables to exist before they can be set?

So what does it do? It adds enough delimiters to the end of the list to create an Item #21 and then it sets that to the supplied text, which in this case is empty.

And the second action deletes approximately half of the added items, changing each pair of Returns to a single Return.

So the number of blank lines that was added was not entirely consistent and the resulting length of the list was not entirely consistent -- and it only happened the first time a shorter list got accessed, then no further changes were made, mostly. A bit difficult to debug until the Ah-Ha!

So truncating the list will require a slightly different approach. Later ...

1 Like

Just in case you have forgotten, with KM the zero element of any array ([0]) contains the current size of the array, so you can tell beforehand how many items there are. So if

GlobalRecentDeskSpaces[0]\r

Is equal to 21, you know it's safe to "delete" that element. The trouble is, the way you are deleting the last element isn't reducing the size of the array, so to do that you need to trim off the last \r from the variable.

BTW (apropos your remark about variables existing)

GlobalRecentDeskSpaces[21]\r

is not really a variable - it's an array element and it behaves like a variable. What is a variable is this:

GlobalRecentDeskSpaces