|
#!/usr/bin/gawk -E |
|
## |
|
## flatpak-du - print disk utilization of system/user Flatpak apps & runtimes |
|
## |
|
## Author: Kevin Ernst (ernstki@mail.uc.edu) |
|
## Date: 21 June 2026 |
|
## Assisted-by: Kagi Quick 06-2026 → selection sort algorithm; 'dehumanize' |
|
## and 'bargraph' functions |
|
## License: Zero-Clause BSD, but attribution is appreciated |
|
## Homepage: https://gist.github.com/ernstki/dab42b66b4397d77caf64aad8cdcafc7 |
|
## |
|
BEGIN { |
|
FS = "\t" |
|
OFS = "\t" |
|
headers = 1 |
|
human = 1 |
|
bars = 0 |
|
quiet = 0 |
|
|
|
for (i = 1; i < ARGC; i++) { |
|
#print "ARGV[" i "]=" ARGV[i] > "/dev/stderr" |
|
if (ARGV[i] ~ /^--?h(elp)?$/) help = 1 |
|
else if (ARGV[i] ~ /^--?v(erbose)?$/) { verbose = 1; quiet = 0 } |
|
else if (ARGV[i] ~ /^--?q(uiet)?$/) { quiet = 1; verbose = 0 } |
|
else if (ARGV[i] ~ /^--?u(ser)?$/) user = " --user" |
|
else if (ARGV[i] ~ /^--?s(ystem)?$/) sys = " --system" |
|
else if (ARGV[i] ~ /^--r(un)?t(imes?)?$/) runtime = " --runtime" |
|
else if (ARGV[i] ~ /^--?a(ll)$/) all = " --all" |
|
else if (ARGV[i] ~ /^-(b|-bar.*)$/) bars = 1 |
|
else if (ARGV[i] ~ /^-(nh|H|-no-human.*)$/) human = 0 |
|
else if (ARGV[i] ~ /^-(nc|C|-no-col.*)$/) headers = 0 |
|
else if (ARGV[i] ~ /^-n?[[:digit:]]+$/) |
|
{ |
|
topn = ARGV[i] |
|
gsub(/^-n?/, "", topn) |
|
topn = topn + 0 |
|
} |
|
else if (ARGV[i] == "-n") |
|
{ |
|
if (ARGV[i+1] && ARGV[i+1] ~ /[[:digit:]]+/) { |
|
topn = ARGV[i+1] + 0 |
|
delete ARGV[i]; delete ARGV[i+1]; i++ |
|
} else { |
|
print "ERROR: -n was missing an (integer) argument." \ |
|
> "/dev/stderr" |
|
exitcode = 1; exit |
|
} |
|
} |
|
else { |
|
print "ERROR: Unknown option '" ARGV[i] "'." > "/dev/stderr" |
|
exitcode = 1; exit |
|
} |
|
delete ARGV[i] |
|
} |
|
|
|
if (help) { |
|
printhelp() |
|
exit |
|
} |
|
|
|
# stop trying to read stdin |
|
ARGC = 2; ARGV[1] = "/dev/null" |
|
# we'll use columns 1,3,4,12,13 |
|
flatlist = "flatpak list --show-details" all runtime user sys |
|
|
|
if (!quiet) print "\x1b[1;35m+ " flatlist "\x1b[0m" > "/dev/stderr" |
|
|
|
# the manual page makes it seem as if NR is unconditionally set by |
|
# `getline`, but it isn't here; NF is, though |
|
nr = 0 |
|
while ((flatlist | getline) > 0) { |
|
nr++ |
|
flatpaks[nr]["name"] = $1 |
|
flatpaks[nr]["appid"] = $3 |
|
flatpaks[nr]["version"] = $4 |
|
flatpaks[nr]["size"] = dehumanize($12) |
|
|
|
# fix this bad behavior: the 13th column will not be displayed if you |
|
# specified `--user` or `--system` and it's not shown at all if output |
|
# is stdout (!) |
|
split($13, types, /,/) |
|
if (runtime) |
|
# only runtimes are shown |
|
flatpaks[nr]["type"] = types[1] " runtime" |
|
else if (user && !sys) { |
|
if (types[1] == "runtime") |
|
flatpaks[nr]["type"] = "user runtime" |
|
else |
|
flatpaks[nr]["type"] = "user app" |
|
} |
|
else if (sys && !user) |
|
if (types[1] == "runtime") |
|
flatpaks[nr]["type"] = "system runtime" |
|
else |
|
flatpaks[nr]["type"] = "system app" |
|
else { |
|
if (types[2] == "runtime") |
|
flatpaks[nr]["type"] = types[1] " runtime" |
|
else |
|
flatpaks[nr]["type"] = types[1] " app" |
|
} |
|
} |
|
close(flatlist) |
|
|
|
# selection sort b/c it's easy; largest byte size first, then alphabetical |
|
for (i = 1; i <= nr; i++) { |
|
best = i |
|
for (j = i + 1; j <= nr; j++) { |
|
if (!flatpaks[j]["size"] && !flatpaks[best]["size"]) { |
|
# compare alphabetically by name instead |
|
if (flatpaks[j]["name"] < flatpaks[best]["name"]) |
|
best = j |
|
} |
|
else if (flatpaks[j]["size"] > flatpaks[best]["size"]) |
|
best = j |
|
} |
|
for (k in flatpaks[i]) tmp[k] = flatpaks[i][k] |
|
for (k in flatpaks[i]) flatpaks[i][k] = flatpaks[best][k] |
|
for (k in flatpaks[i]) flatpaks[best][k] = tmp[k] |
|
} |
|
|
|
# limit to top n by byte size here, after sorting |
|
if (!topn) topn = nr |
|
|
|
if (bars) |
|
for (i = 1; i <= topn; i++) totalbytes += flatpaks[i]["size"] |
|
|
|
if (headers) |
|
print "name\tapplication\tversion\tsize\ttype" |
|
|
|
for (i = 1; i <= topn; i++) { |
|
bytes = flatpaks[i]["size"] |
|
size = human ? humanize(bytes) : sprintf("%.f", bytes) |
|
|
|
if (bars) |
|
size = "[" bargraph(bytes, totalbytes, size) "]" |
|
|
|
print flatpaks[i]["name"], \ |
|
flatpaks[i]["appid"], \ |
|
flatpaks[i]["version"], \ |
|
size, \ |
|
flatpaks[i]["type"] |
|
} # top n by byte size, largest first |
|
} |
|
|
|
END { |
|
exit exitcode |
|
} |
|
|
|
function printhelp() { |
|
print "\n" \ |
|
" flatpak-du - print disk utilization for flatpak apps and runtimes\n" \ |
|
"\n" \ |
|
" usage: \n" \ |
|
" flatpak-du [-h|--help] [-q|--quiet] [-v|--verbose]\n" \ |
|
" [-u] [-s] [-r] [-a] [-b|--bars|--bargraph|--bar-graph]\n" \ |
|
" [-H|--no-human-readable] [-C|--no-column-headers]\n" \ |
|
" [-LINES|-nLINES]\n" \ |
|
"\n" \ |
|
" where: \n" \ |
|
" -h, --help shows this help\n" \ |
|
" -q, --quiet suppresses some informational output\n" \ |
|
" -v, --verbose shows more details while processing\n" \ |
|
" -u, --user shows user-installed packages/runtimes only\n" \ |
|
" -s, --system shows system-wide packages/runtimes only\n" \ |
|
" -r, --runtime shows only runtimes rather than apps + runtimes\n" \ |
|
" -a, --all shows hidden/debug runtimes, too\n" \ |
|
" -b, --bar[…] shows Unicode bar graphs for byte sizes\n" \ |
|
" -H, --no-human[…] shows sizes in bytes rather than IEC units\n" \ |
|
" -C, --no-col[…] suppresses column headers\n" \ |
|
" -LINES, -nLINES shows only top LINES lines, sorted by size\n" \ |
|
"\n" \ |
|
" The default is to show apps + runtimes, both user-installed and\n" \ |
|
" system-wide, sorted largest-first, displaying human-readable sizes\n" \ |
|
" and bar graphs, with no limit on the number of records.\n" \ |
|
"\n" \ |
|
" tips: \n" \ |
|
" For best appearance, pipe through 'column'. For Bourne-like shells,\n" \ |
|
" try adding this wrapper function\n" \ |
|
"\n" \ |
|
" function flatdu() { flatpak-du --bars \"$@\" | column -ts$'\\t'; }\n" \ |
|
"\n" \ |
|
" to your login scripts.\n" \ |
|
"\n" \ |
|
" homepage:\n" \ |
|
" https://gist.github.com/ernstki/dab42b66b4397d77caf64aad8cdcafc7\n" |
|
} |
|
|
|
function dehumanize(s) { |
|
# convert "1.0 GB" into bytes again, since `flatpak` has no way of doing |
|
# that; all the better because someone thought it was a good idea to have a |
|
# non-breaking space between the number and the unit in the 'size' column |
|
# ¯\_(ツ)_/¯ |
|
bytesize = s + 0 # casting to number removes suffix |
|
if (s ~ /GB/) return bytesize * 1024^3 |
|
if (s ~ /MB/) return bytesize * 1024^2 |
|
if (s ~ /KB/) return bytesize * 1024 |
|
return val |
|
} |
|
|
|
function humanize(b) { |
|
if (b % 1024 == b) return b |
|
else if (b % 1024^2 == b) return sprintf("%0.1fK", b/1024) |
|
else if (b % 1024^3 == b) return sprintf("%0.1fM", b/1024^2) |
|
else if (b % 1024^4 == b) return sprintf("%0.1fG", b/1024^3) |
|
else return sprintf("%0.1fT", b/1024^4) |
|
} |
|
|
|
# create a 10-character wide unicode bar graph with text overlaid |
|
function bargraph(val, max_val, text_label, width) { |
|
if (!text_label) text_label = val |
|
if (!width) width = 10 |
|
|
|
text_padded = sprintf("%*.*s", width, width, text_label) |
|
if (max_val == 0) return text_padded |
|
|
|
ratio = val / max_val |
|
if (ratio > 1) ratio = 1 |
|
|
|
total_eighths = int(ratio * 80 + 0.5) |
|
full_blocks = int(total_eighths / 8) |
|
remainder = total_eighths % 8 |
|
|
|
bar = "" |
|
for (b = 1; b <= width; b++) { |
|
c = substr(text_padded, b, 1) |
|
|
|
if (b <= full_blocks) { |
|
# fully-filled segment: invert the text character |
|
bar = bar "\033[7m" c "\033[27m" |
|
} else if (b == full_blocks + 1 && remainder > 0) { |
|
# Partially filled segment |
|
if (c == " ") { |
|
# if no text here, show the high-res fractional block |
|
if (remainder == 7) bar = bar "▉" |
|
else if (remainder == 6) bar = bar "▊" |
|
else if (remainder == 5) bar = bar "▋" |
|
else if (remainder == 4) bar = bar "▌" |
|
else if (remainder == 3) bar = bar "▍" |
|
else if (remainder == 2) bar = bar "▎" |
|
else if (remainder == 1) bar = bar "▏" |
|
} else { |
|
# if text, invert it if the block is at least half full |
|
if (remainder >= 4) { |
|
bar = bar "\033[7m" c "\033[27m" |
|
} else { |
|
bar = bar c |
|
} |
|
} |
|
} else { |
|
# empty segment: just print the text character normally |
|
bar = bar c |
|
} |
|
} |
|
return bar |
|
} |
|
|
|
# vim: expandtab |