Compare commits

...

3 Commits

Author SHA1 Message Date
b4ebb9c5d3
wip 2024-12-17 21:43:10 +04:00
5aea99819a
README.md: updating on this fork 2024-08-17 08:02:19 +04:00
7257a32810
small linting 2024-08-17 08:02:19 +04:00
2 changed files with 153 additions and 79 deletions

View File

@ -1,11 +1,12 @@
tkdu
====
Fork of Jeff Epler's tkdu program to visualize disk usage and `du` output
Fork of Harald Göttlicher's [fork](https://github.com/harry-g/tkdu) of Jeff Epler's [tkdu program](https://web.archive.org/web/20090113182447/http://unpythonic.net:80/jeff/tkdu/) to visualize disk usage and `du` output
Ported version for Python 3
See original version [here](https://github.com/daniel-beck/tkdu/commit/55ef0278c58b5a03687180bb5e5722fa3a22d7a5) or [on its website](http://www.unpythonic.net/jeff/tkdu/).
See original version [here](https://github.com/daniel-beck/tkdu/commit/55ef0278c58b5a03687180bb5e5722fa3a22d7a5)
or [on its website](https://web.archive.org/web/20090113182447/http://unpythonic.net:80/jeff/tkdu/).
Usage:

227
tkdu.py
View File

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# This is tkdu.py, an interactive program to display disk usage
# Copyright 2004 Jeff Epler <jepler@unpythonic.net>
@ -19,7 +19,14 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import math, tkinter, sys, os, stat, string, time, gzip, tkinter.filedialog
from argparse import ArgumentParser
import tkinter
import sys
import os
import stat
import time
import gzip
import tkinter.filedialog
from tkinter.filedialog import askdirectory
MIN_PSZ = 1000
@ -29,19 +36,23 @@ MIN_H = 15
VERTICAL = "vertical"
HORIZONTAL = "horizontal"
NUM_QUEUE = 25
FONT_FACE = ("helvetica", 12)
FONT_FACE = ("sans-serif", 8)
BORDER = 2
FONT_HEIGHT = 12
FONT_HEIGHT = 14
FONT_HEIGHT2 = 20
dircolors = ['#ff7070', '#70ff70', '#7070ff']
leafcolors = ['#bf5454', '#54bf54', '#5454bf']
def allocate(path, files, canvas, x, y, w, h, first, depth):
# TODO: refactor to less complex functions
tk_call = canvas.tk.call
if w < MIN_W or h < MIN_H: return
if w < MIN_W or h < MIN_H:
return
psz = w*h
if psz < MIN_PSZ: return
if psz < MIN_PSZ:
return
if path and path[-1] == "/":
basename_idx = len(path)
nslashes = path.count(os.sep) - 1
@ -53,52 +64,57 @@ def allocate(path, files, canvas, x, y, w, h, first, depth):
colors = (leafcolor, dircolor)
totsz = 0
ff = getkids(files, path)
if not ff: return
if ff[0][1] == '/': del ff[0]
if not ff:
return
if ff[0][1] == '/':
del ff[0]
ff = ff[first:]
for item in ff:
totsz = totsz + item[0]
item[2] = None
if totsz == 0: return
if totsz == 0:
return
i = 0
ratio = psz*1./totsz
while i < len(ff) and w>2*BORDER and h>2*BORDER:
while i < len(ff) and w > 2*BORDER and h > 2*BORDER:
if w > h:
orient = VERTICAL
usew = w - h*2./3
if usew < 50: usew = 50
if usew > 200: usew = 200
if usew < 50:
usew = 50
if usew > 200:
usew = 200
first_height = ff[i][0]/usew*ratio
while first_height < .65 * usew:
usew = usew / 1.5
first_height = ff[i][0]/usew*ratio
want = usew * h / ratio
maxcnt = h/30
else:
orient = HORIZONTAL
useh = h - w*2./3
if useh < 50: useh = 50
if useh > 100: useh = 100
if useh < 50:
useh = 50
if useh > 100:
useh = 100
first_width = ff[i][0]/useh*ratio
while first_width < .65 * useh:
useh = useh / 1.5
first_width = ff[i][0]/useh*ratio
want = useh * w / ratio
maxcnt = w/30
j = i+1
use = ff[i][0]
while j < len(ff) and use < want: #and j < i + maxcnt:
while j < len(ff) and use < want: # and j < i + maxcnt:
use = use + ff[j][0]
j=j+1
j = j+1
if orient is VERTICAL:
usew = use * ratio / h
if usew <= 2*BORDER: break
if usew <= 2*BORDER:
break
y0 = y
for item in ff[i:j]:
dy = item[0]/usew*ratio
@ -108,7 +124,8 @@ def allocate(path, files, canvas, x, y, w, h, first, depth):
w = w - usew
else:
useh = use * ratio / w
if useh <= 2*BORDER: break
if useh <= 2*BORDER:
break
x0 = x
for item in ff[i:j]:
dx = item[0]/useh*ratio
@ -123,19 +140,20 @@ def allocate(path, files, canvas, x, y, w, h, first, depth):
name = item[1]
haskids = bool(getkids(files, name))
color = colors[haskids]
if item[2] is None: continue
x, y, w, h = pos = item[2]
if item[2] is None:
continue
x, y, w, h = item[2]
if w > 3*BORDER and h > 3*BORDER:
tk_call(canvas._w,
"create", "rectangle",
x+BORDER+2, y+BORDER+2, x+w-BORDER+1, y+h-BORDER+1,
"-fill", "#3f3f3f",
"-outline", "#3f3f3f")
"create", "rectangle",
x+BORDER+2, y+BORDER+2, x+w-BORDER+1, y+h-BORDER+1,
"-fill", "#3f3f3f",
"-outline", "#3f3f3f")
i = tk_call(canvas._w,
"create", "rectangle",
x+BORDER, y+BORDER, x+w-BORDER, y+h-BORDER,
"-fill", color)
"create", "rectangle",
x+BORDER, y+BORDER, x+w-BORDER, y+h-BORDER,
"-fill", color)
canvas.map[int(i)] = name
if h > FONT_HEIGHT+2*BORDER:
@ -152,31 +170,32 @@ def allocate(path, files, canvas, x, y, w, h, first, depth):
if tw < w1:
text = "%s\n%s" % (stem, ssz)
i = tk_call(canvas._w, "create", "text",
x+BORDER+2, y+BORDER,
"-text", text,
"-font", FONT_FACE, "-anchor", "nw")
x+BORDER+2, y+BORDER,
"-text", text,
"-font", FONT_FACE, "-anchor", "nw")
canvas.map[int(i)] = name
y = y + FONT_HEIGHT2
h = h - FONT_HEIGHT2
if w*h > MIN_PSZ and haskids and depth != 1:
queue(canvas, allocate, name, files, canvas,
x+2*BORDER, y+2*BORDER,
w-4*BORDER, h-4*BORDER, 0, depth-1)
x+2*BORDER, y+2*BORDER,
w-4*BORDER, h-4*BORDER, 0, depth-1)
continue
text = stem
tw = int(tk_call("font", "measure", FONT_FACE, text))
if tw < w1:
i = tk_call(canvas._w, "create", "text",
x+BORDER+2, y+BORDER,
"-text", text,
"-font", FONT_FACE, "-anchor", "nw")
x+BORDER+2, y+BORDER,
"-text", text,
"-font", FONT_FACE, "-anchor", "nw")
canvas.map[int(i)] = name
y = y + FONT_HEIGHT
h = h - FONT_HEIGHT
if w*h > MIN_PSZ and haskids and depth != 1:
queue(canvas, allocate, name, files, canvas,
x+2*BORDER, y+2*BORDER,
w-4*BORDER, h-4*BORDER, 0, depth-1)
x+2*BORDER, y+2*BORDER,
w-4*BORDER, h-4*BORDER, 0, depth-1)
def queue(c, *args):
if c.aid is None:
@ -184,6 +203,7 @@ def queue(c, *args):
c.configure(cursor="watch")
c.queue.append(args)
def run_queue(c):
queue = c.queue
end = time.time() + .5
@ -192,20 +212,23 @@ def run_queue(c):
c.aid = None
c.configure(cursor="")
break
if time.time() > end: break
if time.time() > end:
break
item = queue[0]
del queue[0]
item[0](*item[1:])
if queue:
c.aid = c.after_idle(run_queue, c)
def chroot(e, r):
c = e.widget
if r is None:
return
if not getkids(c.files, r):
r = os.path.dirname(r)
if r == c.cur: return
if r == c.cur:
return
if not r.startswith(c.root):
c.bell()
return
@ -215,24 +238,27 @@ def chroot(e, r):
e.height = c.winfo_height()
reconfigure(e)
def item_under_cursor(e):
c = e.widget
try:
item = c.find_overlapping(e.x, e.y, e.x, e.y)[-1]
except IndexError:
return None
return c.map.get(item, None)
return c.map.get(item, None)
def descend(e):
c = e.widget
item = item_under_cursor(e)
chroot(e, item)
def ascend(e):
c = e.widget
parent = os.path.dirname(c.cur)
chroot(e, parent)
def size(n):
if n > 1024*1024*1024:
return "%.1fGB" % (n/1024./1024/1024)
@ -242,26 +268,32 @@ def size(n):
return "%.1fKB" % (n/1024.)
return "%d" % n
def scroll(e, dir):
c = e.widget
offset = c.first + 5*dir
l = len(getkids(c.files, c.cur))
if offset + 5 > l: offset = l-5
if offset < 0: offset = 0
length = len(getkids(c.files, c.cur))
if offset + 5 > length:
offset = length-5
if offset < 0:
offset = 0
if offset != c.first:
c.first = offset
e.width = c.winfo_width()
e.height = c.winfo_height()
reconfigure(e)
def schedule_tip(e):
c = e.widget
s = item_under_cursor(e)
if not s: return
if not s:
return
sz = getname(c.files, s)
s = "%s (%s)" % (s, size(sz))
c.tipa = c.after(500, make_tip, e, s)
def make_tip(e, s):
c = e.widget
c.tipa = None
@ -269,6 +301,7 @@ def make_tip(e, s):
c.tip.wm_geometry("+%d+%d" % (e.x_root+5, e.y_root+5))
c.tip.wm_deiconify()
def cancel_tip(e, c=None):
if c is None:
c = e.widget
@ -278,6 +311,7 @@ def cancel_tip(e, c=None):
else:
c.tip.wm_withdraw()
def reconfigure(e):
c = e.widget
w = e.width
@ -295,18 +329,19 @@ def reconfigure(e):
nslashes = -1
else:
nslashes = c.cur.count(os.sep) - 1
parent = os.path.dirname(c.cur)
color = dircolors[nslashes % len(dircolors)]
c.configure(background=color)
c.queue = [(allocate, c.cur, c.files, c, 0, 0, w, h, c.first, c.depth)]
run_queue(c)
def putname_base(dict, name, base, size):
try:
dict[base][name] = size
except:
dict[base] = {name: size}
def putname(dict, name, size):
base = os.path.dirname(name)
try:
@ -314,13 +349,16 @@ def putname(dict, name, size):
except:
dict[base] = {name: size}
def getname(dict, name):
base = os.path.dirname(name)
return dict[base][0][name]
def getkids(dict, path):
return dict.get(path, ((), {}))[1]
def doit(dir, files):
sorted_files = {}
for k, v in files.items():
@ -355,11 +393,13 @@ def doit(dir, files):
c.bind("<Button-3>", ascend)
else:
c.bind("<Button-2>", ascend)
c.bind("<u>", ascend)
c.tag_bind("all", "<Button-1>", descend)
c.tag_bind("all", "<Enter>", schedule_tip)
c.tag_bind("all", "<Leave>", cancel_tip)
c.mainloop()
def setdepth(e, c, i):
e.widget = c
e.width = c.winfo_width()
@ -367,23 +407,26 @@ def setdepth(e, c, i):
c.depth = i
reconfigure(e)
def main(f = sys.stdin):
def old_main(f=sys.stdin):
files = {}
firstfile = None
for line in f.readlines():
sz, name = line[:-1].split(None, 1)
# name = name.split("/")
sz = int(sz)*1024
putname(files, name, sz)
doit(name, files)
def du(dir, files, fs=0, ST_MODE=stat.ST_MODE, ST_SIZE = stat.ST_SIZE, S_IFMT = 0o170000, S_IFDIR = 0o040000, lstat = os.lstat, putname_base = putname_base, fmt="%%s%s%%s" % os.sep):
def du(dir, files, fs=0, ST_MODE=stat.ST_MODE, ST_SIZE=stat.ST_SIZE, S_IFMT=0o170000, S_IFDIR=0o040000, lstat=os.lstat, putname_base=putname_base, fmt="%%s%s%%s" % os.sep):
tsz = 0
try: fns = os.listdir(dir)
except: return 0
try:
fns = os.listdir(dir)
except:
return 0
if not files.has_key(dir): files[dir] = {}
if not files.has_key(dir):
files[dir] = {}
d = files[dir]
for fn in fns:
@ -402,9 +445,11 @@ def du(dir, files, fs=0, ST_MODE=stat.ST_MODE, ST_SIZE = stat.ST_SIZE, S_IFMT =
tsz = tsz + sz
return tsz
def abspath(p):
return os.path.normpath(os.path.join(os.getcwd(), p))
class DirDialog(tkinter.filedialog.LoadFileDialog):
def __init__(self, master, title=None):
tkinter.filedialog.LoadFileDialog.__init__(self, master, title)
@ -419,7 +464,7 @@ class DirDialog(tkinter.filedialog.LoadFileDialog):
self.quit(file)
def filter_command(self, event=None):
END="end"
END = "end"
dir, pat = self.get_filter()
try:
names = os.listdir(dir)
@ -430,7 +475,6 @@ class DirDialog(tkinter.filedialog.LoadFileDialog):
self.set_filter(dir, pat)
names.sort()
subdirs = [os.pardir]
matchingfiles = []
for name in names:
fullname = os.path.join(dir, name)
if os.path.isdir(fullname):
@ -439,11 +483,12 @@ class DirDialog(tkinter.filedialog.LoadFileDialog):
for name in subdirs:
self.dirs.insert(END, name)
head, tail = os.path.split(self.get_selection())
if tail == os.curdir: tail = ''
if tail == os.curdir:
tail = ''
self.set_selection(tail)
def main_builtin_du(args):
import sys
if len(args) > 1:
p = args[1]
else:
@ -459,37 +504,65 @@ def main_builtin_du(args):
if p == '-h' or p == '--help' or p == '-?':
base = os.path.basename(args[0])
print ('Usage:')
print (' ', base, '<file.gz> interpret file as gzipped du -ak output and visualize it')
print (' ', base, '<file> interpret file as du -ak output and visualize it')
print (' ', base, '<folder> analyze disk usage in that folder')
print (' ', base, '- interpret stdin input as du -ak output and visualize it')
print (' ', base, ' ask for folder to analyze')
print('Usage:')
print(' ', base, '<file.gz> interpret file as gzipped du -ak output and visualize it')
print(' ', base, '<file> interpret file as du -ak output and visualize it')
print(' ', base, '<folder> analyze disk usage in that folder')
print(' ', base, '- interpret stdin input as du -ak output and visualize it')
print(' ', base, ' ask for folder to analyze')
print
print ('Controls:')
print (' * Press `q` to quit')
print (' * LMB: zoom in to item')
print (' * RMB: zoom out one level')
print (' * Press `1`..`9`: Show that many nested levels')
print (' * Press `0`: Show man nested levels')
print('Controls:')
print(' * Press `q` to quit')
print(' * LMB: zoom in to item')
print(' * RMB: zoom out one level')
print(' * Press `1`..`9`: Show that many nested levels')
print(' * Press `0`: Show man nested levels')
return
if p == "-":
main()
old_main()
else:
p = abspath(p)
if os.path.isfile(p):
if p.endswith('.gz'):
# gzipped file
main(gzip.open(p, 'r'))
old_main(gzip.open(p, 'r'))
else:
main(open(p, 'r'))
old_main(open(p, 'r'))
else:
putname(files, p, du(p, files))
doit(p, files)
argp = ArgumentParser(
prog='tkdu',
description='''Interactive explorer of `du` utility program.
'Useful when you need to interactively inspect disk usage
'on remote system but dont have `ncdu` or alike installed''',
epilog='''
Controls:
* Press `q` to quit
* LMB: zoom in to item
* RMB: zoom out one level
* Press `1`..`9`: Show that many nested levels
* Press `0`: Show man nested levels
'''
)
argp.add_argument(
'du_output',
help='du output file in plain text, gzipped or not'
)
def main():
args = argp.parse_args()
print(args)
if __name__ == '__main__':
import sys
main()
main_builtin_du(sys.argv)
# vim:sts=4:sw=4: