Compare commits

..

No commits in common. "b4ebb9c5d3a4c270649911e58d30b9c1c67961ba" and "830cd1f2bd406dbf6baa9451ac46b0ac87f5eded" have entirely different histories.

2 changed files with 79 additions and 153 deletions

View File

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

227
tkdu.py
View File

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