Estimated time to read this document: 10m
This is a small utility that I can use from the command-line to keep track of where in the bigger picture my current efforts fit.
Too often I find myself completing some small and intricate part of my hobby/personal project, and realise that I don't remember where I stopped last in the project, which I last looked at as an overview two weeks ago.
I deep-dive into something tiny and complex that takes all of my attention, and once that is done and integrated, I'm not too sure what I had planned next.
I store context as a tree of nodes. I call each node in this tree a frame
(as in frame of context, frame of mind, or framing the problem, etc). At any
given time only a single frame can be active. To see all the notes I made
while working during that frame of context I use frame status
.
When I start a task I first do push frame 'title of task name here'
and
enter a short message, and that new frame is the current frame. When I am done I
do frame pop
and the current frame switches to the parent of the popped
frame. The popped frame is discarded.
All frames descend from an initial frame called root
1.
The first thing to do is to integrate the frame name into the shell's
PS1
variable. The command frame current
prints the name of the current
frame and the date it was changed, with the two fields separated by a colon.
$ frame current
root/projects/frame: Thu May 11 22:26:29 2023
I have the following function which is called within my PS1
variable:
frame_ps1() {
frame current --quiet | cut -f 1 -d :
}
Calling frame_ps1
within the PS1
variable prints out the current frame
after each command, in every terminal I am logged into2.
[root/projects/frame]$ echo "Hello World"
Hello World
[root/projects/frame]$
Continuing with the frame root/projects/frame
, in this case I
want to create a GUI browser for this project:
[root/projects/frame]$ frame push 'create GUI' --message="
Create a GUI for frame browsing
"
Frame 0.1.2, (© 2023 Lelanthran Manickum)
Created new frame [root/projects/frame/create GUI]
[root/projects/frame/create GUI]$
Had I neglected to provide a
--message
parameter then $EDITOR is opened to allow me to enter a message, similar to howgit commit
works when no message is specified. Each frame can store a single note of unlimited size; the--message
argument provides the notes when creating a new frame.
The current frame's name is create GUI
. It has a parent, projects
, which
itself has the parent root
.
graph TD
A[root] --> B[projects];
B --> C[frame];
C --> D[create GUI];
Some useful commands at this point:
frame status
returns the notes (message) for this frame.frame edit
opens the editor to allow editing the notes for this frame.
I have some idea of what needs to be done, namely, startup a new Lazarus
project, create the GUI elements, include the libframe.so
library, ensure
that the build compiles, etc.
[root/projects/frame/create GUI]$ frame append --message="
1. Create new lazarus project
2. Add `libframe.so` to the lazarus project
3. Create GUI elements
"
Frame 0.1.2, (© 2023 Lelanthran Manickum)
root/frame/create GUI: Sat May 13 08:20:32 2023
It takes a few minutes to create the Lazarus project, add the libframe.so
file and ensure that the linker is finding and accepting the library.
My next task is to create the GUI elements. This is much more involved, and so I create a new frame for it:
[root/projects/frame]$ frame push 'GUI Elements' --message="
Needs to display:
1. The history, with a filter
2. A treeview of all frames from root, with the current frame selected and
expanded.
3. An editor box that displays the notes for the current frame, and allows the
user to edit those notes.
"
Frame 0.1.2, (© 2023 Lelanthran Manickum)
Created new frame [root/frame/create GUI/GUI Elements]
[root/projects/frame/create GUI/GUI Elements]$
I do the form design (ridiculously easy in Lazarus, takes about 10m). I
attempt to populate the history
GUI element, and realise I forgot to declare
the C
functions in the library file. My Lazarus code cannot call undeclared
functions even if the library is linked correctly.
[root/projects/frame/create GUI/GUI Elements]$ frame push 'decls needed" --message='
Declare all the `C` functions in `libframe.so`.
'
Frame 0.1.2, (© 2023 Lelanthran Manickum)
Created new frame [root/frame/create GUI/GUI Elements/decls needed]
To make things easier, I paste the C
header file into ChatGPT and ask for
the Lazarus definitions. It provides the definitions, helpfully hallucinating
a few types. and I copy the results into a Lazarus sourcefile and fix all the
compilation errors.
Finally, the needed declarations are in. To return to whatever I was previously doing (what was it again):
[root/frame/create GUI/GUI Elements/decls needed] $ frame pop
Frame 0.1.2, (© 2023 Lelanthran Manickum)
[src/frm.c:55] Failed to switch to new dir [root/frame/create GUI/GUI Elements/decls needed]
[src/frm.c:1440] Warning: using relative path [root/frame/create GUI/GUI Elements/decls needed] failed, trying absolute path
Current frame
root/frame/create GUI/GUI Elements
Notes (Sat May 13 08:26:59 2023)
Needs to display:
1. The history, with a filter
2. A treeview of all frames from root, with the current frame selected and
expanded.
3. An editor box that displays the notes for the current frame, and allows the
user to edit those notes.
Right, I was on the first item. Maybe create a frame for that:
[root/frame/create GUI/GUI Elements] $ frame push 'populate history element' --message='
> create a populate_history() procedure
> '
Frame 0.1.2, (© 2023 Lelanthran Manickum)
Created new frame [root/frame/create GUI/GUI Elements/populate history element]
I create the named function, run it, watch in disbelief as it crashes, and then investigate the crash. Turns out there's a bug in the library. New frame time:
[root/frame/create GUI/GUI Elements/populate history element] $ frame push 'debug frm_history' --message='
> frm_history, when called multiple times in the same session, has a double-free
> bug. This does not show up in the c/line `frame history` command because the `frame`
> program runs the specified command and then exits.
> '
Frame 0.1.2, (© 2023 Lelanthran Manickum)
Created new frame [root/frame/create GUI/GUI Elements/populate history element/debug frm_history]
I switch to working on the frame library instead. I update the test script to reproduce the error, and using valgrind, and then some gdb, I come up with an appropriate fix.
graph TD
A[root] -->B[projects];
B[projects] --> C[frame];
C --> D[create GUI];
D --> E[populate history element];
E --> F[debug frm_history];
F --> G[update tests to reproduce bugs];
G --> H[fix bug];
Now that the bug in frm_history has been fixed, I perform multiple frame pop
s,
until I reach a frame that is still incomplete: `populate history element'.
After each pop
, the current frame is deleted, never to be seen again.
This helps keep the signal/noise ratio to an acceptable number.
Once I am at populate history element
, I relink and rerun the program. It
populates the history just fine. I continue my development in this manner;
push
ing new frames when I start something even slightly long, pop
ping
frames once I am done with whatever I am currently working on.
When I need to record more notes or thoughts related to the current frame, I
use frame edit
and record my notes in the editor that opens.
When I come
back to my computer after (for example) a good nights sleep, my terminal will
display the name of the current frame. If that is not sufficient to remind me
where I stopped last, then frame status
displays all my notes.
Sometimes I come back to a frame after multiple days; I create a frame, store
my notes, then 'push' new frames diving ever deeper into whatever it is I am
doing, and then two days later I have finally pop
ped my way back to the
original frame, and thankfully I can use frame status
to remember what it
was I wanted to do next.
It's all very linear, and intentionally so. You can create new frames
without immediately switching to the newly created frame by using frame new
.
I do this when I know in advance that I need to do $X, $Y and $Z: then it's
simpler to just do frame create $X
, frame create $Y
and frame create $Z
,
I create multiple frames off root
; typically one for each project. The
frame switch
command accepts a target frame, and attempts to switch to the
sub-branch of that frame that you were last working on.
For example, having frames
graph TD
A[root] --> B[frame];
A --> C[game];
B --> D[create GUI];
D --> E[populate history];
E --> G[debug crash];
B --> H[write docs];
C --> I[Scrabble Clone];
I --> J[implement basic user mgmt];
J --> K[email verification on signup];
I --> L[create board];
L --> M[store game state];
My current frame is [root/frame/create GUI/populate history]. The last time I was in a sub-branch of [root/game], it was in the [email verification on signup] sub-branch.
When I do frame switch root/game
, it returns me to the branch
[root/game/Scrabble Clone/implement basic user mgmt/ email verification on
signup].
The next time I do frame switch root/frame
, it returns me to
[root/frame/create GUI/populate history].
You can get a list of commands using frame --help
. The important commands
are
push <title>
- push a new child frame with specified title.pop
- discard current frame and make parent current.edit
- edit the context attached to the current frame.switch <path>
- (smart) switch to a new path.
Footnotes
-
I made a conscious decision at the start of this project that
root
will always be explicit and not implicit. This avoids errors where a typo results in addressing root when the user did not mean to. For example the following error is impossible when root is explicit:rm -rf myhomedir/Downloads /lib
. ↩ -
In reality, my
PS1
variable is a lot more complex than this, as it includes the git branch (if any) and other information (current directory, username, etc). My prompt uses a separate line for theframe
name, as this name can get quite long. ↩