My Top Drawer Helps Me Change Directories
Every so often I get curious about my shell command usage on Arch
Linux so that I can start thinking of ways to improve my command-line
efficiency. I had already recorded a few thousand lines in
~/.zsh_history
on this particular computer so I knew I could extract
some useful data from which to learn.
Getting the Data
I am not well-versed in zsh beyond minor customizations, so I wasn’t aware if zsh had any built-in statistics that would give me a summary and I didn’t feel like writing a parser to analyze the file so I looked on the internet. I eventually found that atuin could generate a summary that would work for my research.
After installing it, I ran atuin stats
which presented my top 10
commands:
[▮▮▮▮▮▮▮▮▮▮] 1418 cd
[▮▮▮▮▮▮▮ ] 1041 ls
[▮▮▮▮ ] 673 exit
[▮▮ ] 331 iex
[▮ ] 263 cargo run
[▮ ] 235 git status
[▮ ] 211 emacs
[▮ ] 205 killall
[▮ ] 181 mix
[▮ ] 163 pacman
Total commands: 7818
Unique commands: 1610
I suspected the first 3 commands would be near the top, but the rest surprised me a little until I thought about the recent write-compile-repeat cycle I had been on some software projects lately.
These results manifested multiple questions within me:
- Why am I moving around directories so much?
- What files am I looking for?
- Do I have some bad terminal habits I need to address?
- How long have I’ve been collecting this data?
- How does this compare to my shell history on my work computer?
- And many more…
Getting More Granular
I decided I needed to focus on cd
, but in order for me to do that, I
need to find out all the directories I was visiting and how often I
was visiting them.
I remembered reading that atuin
extracts the history data from my
shell and stores it in a SQLite database somewhere. After some
hunting in my local dotfiles, I found a history.db
file in
~/.local/share/atuin
.
So I ran sqlite3 history.db
, got my prompt and started investigating
more:
SQLite version 3.45.0 2024-01-15 17:01:13
Enter ".help" for usage hints.
sqlite> .tables
_sqlx_migrations history
sqlite> .schema history
CREATE TABLE history (
id text primary key,
timestamp integer not null,
duration integer not null,
exit integer not null,
command text not null,
cwd text not null,
session text not null,
hostname text not null, deleted_at integer,
unique(timestamp, cwd, command)
);
CREATE INDEX idx_history_timestamp on history(timestamp);
CREATE INDEX idx_history_command on history(command);
CREATE INDEX idx_history_command_timestamp on history(
command,
timestamp
);
sqlite>
This was excellent. Now, I just needed to see what the data looks like:
SELECT *
FROM history
LIMIT 1;
-- Results
018d32cffd8e7c1599e40599343ba55f|1694721762001000000|0|-1|cat .zsh|unknown|018d32cffd8e785dbeab027db0d677a1|killship:angelo|
I cared only about timestamp
and command
, so I SELECT
ed them only:
SELECT timestamp, command
FROM history
ORDER BY timestamp ASC
LIMIT 1;
-- Results
1694721762001000000|cat .zsh
I dont’t understand unix timestamp epochs intuitively, so after some
googling, I found the Date and Time
Functions page for SQLite
which pointed me to the datetime
function:
SELECT datetime(timestamp/1000000000, 'unixepoch'), command
FROM history
ORDER BY timestamp ASC
LIMIT 1;
-- Results
2023-09-14 20:02:42|cat .zsh
This was so much better. Now I just needed some filtering…
SELECT datetime(timestamp/1000000000, 'unixepoch'), command
FROM history
WHERE command LIKE 'cd%'
ORDER BY timestamp ASC
LIMIT 1
-- Results
2023-09-14 20:27:17|cd code/aur
But this didn’t quite give me what I wanted. I really want to see my
most frequent cd
arguments and I don’t really care about the
timestamps, so I dropped that part of the query and the LIMIT
, then
I added a COUNT
and GROUP BY
clause to tally up the most frequent
arguments:
SELECT command, COUNT(command)
FROM history
WHERE command LIKE 'cd%'
GROUP BY command
ORDER BY count(command) DESC;
-- Results
cd ..|238
cd code|109
cd projects|93
cd|73
cd totemic|60
cd tada-beam|38
cd tada|35
cd code/projects|30
cd alakra|28
cd Downloads|27
cd code/aur|25
cd dotfiles|19
cd written|18
cd learn|15
cd github|14
...
And because I wanted to be fancy, I asked ChatGPT to generate the output a bar chart using ASCII with the following prompt:
Write a SQL query that generates a ASCII-based bar chart within sqlite from a table with 2 fields. The two fields are as follows: the first field is a name and is of type string and the second field is a number is of type integer. Please give an example of the expected output.
This gave me some explanations and the following query:
WITH RECURSIVE
cnt(x) AS (
SELECT 1
UNION ALL
SELECT x+1 FROM cnt
LIMIT (SELECT MAX(number) FROM your_table_name)
),
bar_chart AS (
SELECT
name,
number,
(SELECT GROUP_CONCAT('▮', '') FROM cnt WHERE x <= number) AS bar
FROM your_table_name
)
SELECT name, bar FROM bar_chart;
I then modified this with my own query:
WITH RECURSIVE
cnt(x) AS (
SELECT 1
UNION ALL
SELECT x+1 FROM cnt
LIMIT (SELECT COUNT(command) FROM history WHERE command LIKE 'cd%')
),
bar_chart AS (
SELECT
command,
COUNT(command),
(SELECT GROUP_CONCAT('▮', '') FROM cnt WHERE x <= COUNT(command)) AS bar
FROM history
WHERE command LIKE 'cd%'
GROUP BY command
ORDER BY COUNT(command) DESC
)
SELECT command, bar FROM bar_chart;
And got these results (reformatted for clarity):
| command | bar |
|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| cd .. | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd code | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd projects | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd totemic | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd tada-beam | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd tada | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd code/projects | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd alakra | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd Downloads | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd code/aur | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd dotfiles | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd written | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd learn | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd github | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd angelolakra | ▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd discord_arch_electron | ▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd go | ▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd notes | ▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd notes/pain | ▮▮▮▮▮▮▮▮▮▮▮ |
| ... | ... |
Most of these paths live under ~/code/projects
on my system and that
directory tree at 2-levels of depth (via tree -d -L 2
) looks like:
├── alakra
│ ├── ditl
│ ├── dotfiles
│ ├── haikus
│ ├── journal
│ ├── scraps
│ └── todo
├── angelolakra
│ ├── devops
│ ├── diatheke
│ ├── time_tracking
│ ├── written
│ └── www
├── data
│ ├── analysis
│ ├── astronomical-objects
│ ├── closed
│ ├── generated
│ ├── jobs
│ ├── lists
│ └── mastermind
├── eve
│ ├── assets
│ ├── data
│ ├── digiship
│ ├── junk
│ ├── notebooks
│ └── reports
├── fpga
│ ├── ecet-4730
│ └── test
├── games
│ └── ticket-to-ride-core
├── jobs
│ ├── interview-project-api-battleship_Angelo-Lakra
│ └── sturdy-eureka
├── junk
│ ├── aquaduct
│ ├── extraction
│ ├── gettit
│ ├── royal
│ └── scraps
├── learn
│ ├── advent-of-code
│ ├── build-a-text-editor
│ ├── cl
│ ├── coursera
│ ├── exercism
│ ├── mazes
│ ├── my-app
│ ├── my-redis
│ ├── nasati
│ ├── programming-pearls
│ ├── project-euler
│ ├── quote-editor
│ ├── runit
│ └── scheme
├── masterminds
│ └── masterminds
├── roguepowered
│ ├── astroheart
│ ├── ditl
│ ├── imports-to-games
│ ├── interstellar-stuff
│ ├── the-coat
│ ├── the-hour-of-tower
│ ├── the-list
│ └── workspace
└── totemic
├── app
├── derts
├── devops
├── notes
├── reboot
├── rogue
├── rogue-strategies
├── rust-test
├── safari
├── site
├── standard
├── tada
├── tada-elixir
├── tada-grammars
├── tada-old
├── tools
├── tribal
├── winnow
└── www
Realizations and Actions I’m Taking
So from the tree
output and the SQL query I realized that:
- I have spent a lot of time moving up from current directories.
- I have spent a lot of time in my projects folder
- I have spent a lot of time on specific projects.
After thinking about this a bit, I thought about my sock drawer. I organize everything in there by my frequency of access. My top drawer contains white undershirts, socks and underwear. I use these all the time and they are very accessible. What if I treated my projects folder like this too?
So I decided to:
-
Move my projects folder to
~/
-
Move my currently active projects to
~/
I think this small change will be a little faster for me to access things, but maybe I’m kidding myself because it’s only a few more characters. I felt like I realized this anecdotally, but this was a fun exercise to prove it to myself. I did get some good benefits out of this experience:
- I learned about and how to Use atuin
- I learned how to prompt ChatGPT to build non-trivial SQL
- I practiced writing.