Improved command line options, formatting, increased GUI font size

* Added -h/--help/-? arguments explaining usage
* Added ability to accept plain and gzipped du output as first argument
* Increased font size and min height
* Changed mixed tabs/spaces formatting to all spaces
This commit is contained in:
Daniel Beck 2013-07-31 20:50:35 +02:00
parent 12a6501522
commit eb684a6e49

418
tkdu.py Normal file → Executable file
View File

@ -1,3 +1,5 @@
#!/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>
# #
@ -15,19 +17,19 @@
# 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
import math, Tkinter, sys, os, stat, string, time, FileDialog import math, Tkinter, sys, os, stat, string, time, gzip, FileDialog
from tkFileDialog import askdirectory from tkFileDialog import askdirectory
MIN_PSZ = 1000 MIN_PSZ = 1000
MIN_IPSZ = 240 MIN_IPSZ = 240
MIN_W = 50 MIN_W = 50
MIN_H = 10 MIN_H = 15
VERTICAL = "vertical" VERTICAL = "vertical"
HORIZONTAL = "horizontal" HORIZONTAL = "horizontal"
NUM_QUEUE = 25 NUM_QUEUE = 25
FONT_FACE = ("helvetica", 8) FONT_FACE = ("helvetica", 12)
BORDER = 2 BORDER = 2
FONT_HEIGHT = 8 FONT_HEIGHT = 12
FONT_HEIGHT2 = 20 FONT_HEIGHT2 = 20
dircolors = ['#ff7070', '#70ff70', '#7070ff'] dircolors = ['#ff7070', '#70ff70', '#7070ff']
@ -39,11 +41,11 @@ def allocate(path, files, canvas, x, y, w, h, first, depth):
psz = w*h psz = w*h
if psz < MIN_PSZ: return if psz < MIN_PSZ: return
if path and path[-1] == "/": if path and path[-1] == "/":
basename_idx = len(path) basename_idx = len(path)
nslashes = string.count(path, os.sep) - 1 nslashes = string.count(path, os.sep) - 1
else: else:
basename_idx = len(path) + 1 basename_idx = len(path) + 1
nslashes = string.count(path, os.sep) nslashes = string.count(path, os.sep)
dircolor = dircolors[nslashes % len(dircolors)] dircolor = dircolors[nslashes % len(dircolors)]
leafcolor = leafcolors[nslashes % len(dircolors)] leafcolor = leafcolors[nslashes % len(dircolors)]
colors = (leafcolor, dircolor) colors = (leafcolor, dircolor)
@ -53,8 +55,8 @@ def allocate(path, files, canvas, x, y, w, h, first, depth):
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: return if totsz == 0: return
@ -62,149 +64,149 @@ def allocate(path, files, canvas, x, y, w, h, first, depth):
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: usew = 50 if 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 maxcnt = h/30
else: else:
orient = HORIZONTAL orient = HORIZONTAL
useh = h - w*2./3 useh = h - w*2./3
if useh < 50: useh = 50 if 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 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: break if usew <= 2*BORDER: 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
item[2] = (x, y0, usew, dy) item[2] = (x, y0, usew, dy)
y0 = y0 + dy y0 = y0 + dy
x = x + usew x = x + usew
w = w - usew w = w - usew
else: else:
useh = use * ratio / w useh = use * ratio / w
if useh <= 2*BORDER: break if useh <= 2*BORDER: 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
item[2] = (x0, y, dx, useh) item[2] = (x0, y, dx, useh)
x0 = x0 + dx x0 = x0 + dx
y = y + useh y = y + useh
h = h - useh h = h - useh
i = j i = j
for item in ff: for item in ff:
sz = item[0] sz = item[0]
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: continue if item[2] is None: continue
x, y, w, h = pos = item[2] x, y, w, h = pos = 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:
w1 = w - 2*BORDER w1 = w - 2*BORDER
stem = name[basename_idx:] stem = name[basename_idx:]
ssz = size(sz) ssz = size(sz)
text = "%s %s" % (name[basename_idx:], ssz) text = "%s %s" % (name[basename_idx:], ssz)
tw = int(tk_call("font", "measure", FONT_FACE, text)) tw = int(tk_call("font", "measure", FONT_FACE, text))
if tw > w1: if tw > w1:
if h > FONT_HEIGHT2 + 2*BORDER: if h > FONT_HEIGHT2 + 2*BORDER:
tw = max( tw = max(
int(tk_call("font", "measure", FONT_FACE, stem)), int(tk_call("font", "measure", FONT_FACE, stem)),
int(tk_call("font", "measure", FONT_FACE, ssz))) int(tk_call("font", "measure", FONT_FACE, ssz)))
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:
c.aid = c.after_idle(run_queue, c) c.aid = c.after_idle(run_queue, c)
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
while 1: while 1:
if not queue: if not queue:
c.aid = None c.aid = None
c.configure(cursor="") c.configure(cursor="")
break break
if time.time() > end: break if time.time() > end: break
item = queue[0] item = queue[0]
del queue[0] del queue[0]
apply(item[0], item[1:]) apply(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 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: return if r == c.cur: return
if r is None: if r is None:
return return
if not r.startswith(c.root): if not r.startswith(c.root):
c.bell() c.bell()
return return
c.cur = r c.cur = r
c.first = 0 c.first = 0
e.width = c.winfo_width() e.width = c.winfo_width()
@ -214,9 +216,9 @@ def chroot(e, r):
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):
@ -231,11 +233,11 @@ def ascend(e):
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)
elif n > 1024*1024: elif n > 1024*1024:
return "%.1fMB" % (n/1024./1024) return "%.1fMB" % (n/1024./1024)
elif n > 1024: elif n > 1024:
return "%.1fKB" % (n/1024.) return "%.1fKB" % (n/1024.)
return "%d" % n return "%d" % n
def scroll(e, dir): def scroll(e, dir):
@ -245,10 +247,10 @@ def scroll(e, dir):
if offset + 5 > l: offset = l-5 if offset + 5 > l: offset = l-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
@ -267,12 +269,12 @@ def make_tip(e, s):
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
if c.tipa: if c.tipa:
c.after_cancel(c.tipa) c.after_cancel(c.tipa)
c.tipa = None c.tipa = None
else: else:
c.tip.wm_withdraw() c.tip.wm_withdraw()
def reconfigure(e): def reconfigure(e):
c = e.widget c = e.widget
@ -281,16 +283,16 @@ def reconfigure(e):
c.t.wm_title("%s (%s)" % (c.cur, size(getname(c.files, c.cur)))) c.t.wm_title("%s (%s)" % (c.cur, size(getname(c.files, c.cur))))
c.delete("all") c.delete("all")
for cb in c.cb: for cb in c.cb:
c.after_cancel(cb) c.after_cancel(cb)
c.cb = [] c.cb = []
c.aid = None c.aid = None
c.queue = [] c.queue = []
c.map = {} c.map = {}
c.tipa = None c.tipa = None
if c.cur == "/": if c.cur == "/":
nslashes = -1 nslashes = -1
else: else:
nslashes = string.count(c.cur, os.sep) - 1 nslashes = string.count(c.cur, os.sep) - 1
parent = os.path.dirname(c.cur) parent = os.path.dirname(c.cur)
color = dircolors[nslashes % len(dircolors)] color = dircolors[nslashes % len(dircolors)]
c.configure(background=color) c.configure(background=color)
@ -299,16 +301,16 @@ def reconfigure(e):
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:
dict[base][name] = size dict[base][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)
@ -316,14 +318,14 @@ def getname(dict, 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():
sv = map(lambda (k, v): [v, k, None], v.items()) sv = map(lambda (k, v): [v, k, None], v.items())
sv.sort() sv.sort()
sv.reverse() sv.reverse()
sorted_files[k] = (v, sv) sorted_files[k] = (v, sv)
t = Tkinter.Tk() t = Tkinter.Tk()
c = Tkinter.Canvas(t, width=1024, height=768) c = Tkinter.Canvas(t, width=1024, height=768)
@ -344,10 +346,10 @@ def doit(dir, files):
t.bind("<Unmap>", lambda e, c=c: cancel_tip(e, c)) t.bind("<Unmap>", lambda e, c=c: cancel_tip(e, c))
t.bind("<q>", lambda e, t=t: t.destroy()) t.bind("<q>", lambda e, t=t: t.destroy())
for i in range(10): for i in range(10):
t.bind("<Key-%d>" % i, lambda e, c=c, i=i: setdepth(e, c, i)) t.bind("<Key-%d>" % i, lambda e, c=c, i=i: setdepth(e, c, i))
c.bind("<Button-4>", lambda e: scroll(e, -1)) c.bind("<Button-4>", lambda e: scroll(e, -1))
c.bind("<Button-5>", lambda e: scroll(e, 1)) c.bind("<Button-5>", lambda e: scroll(e, 1))
c.bind("<Button-3>", ascend) c.bind("<Button-2>", 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)
@ -360,14 +362,14 @@ def setdepth(e, c, i):
c.depth = i c.depth = i
reconfigure(e) reconfigure(e)
def main_pipe(): def main(f = sys.stdin):
files = {} files = {}
firstfile = None firstfile = None
for line in sys.stdin.readlines(): for line in f.readlines():
sz, name = string.split(line[:-1], None, 1) sz, name = string.split(line[:-1], None, 1)
# name = name.split("/") # name = name.split("/")
sz = long(sz)*1024 sz = long(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 = 0170000, S_IFDIR = 0040000, 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 = 0170000, S_IFDIR = 0040000, lstat = os.lstat, putname_base = putname_base, fmt="%%s%s%%s" % os.sep):
@ -380,19 +382,19 @@ def du(dir, files, fs=0, ST_MODE=stat.ST_MODE, ST_SIZE = stat.ST_SIZE, S_IFMT =
d = files[dir] d = files[dir]
for fn in fns: for fn in fns:
fn = fmt % (dir, fn) fn = fmt % (dir, fn)
try: try:
info = lstat(fn) info = lstat(fn)
except: except:
continue continue
if info[ST_MODE] & S_IFMT == S_IFDIR: if info[ST_MODE] & S_IFMT == S_IFDIR:
sz = du(fn, files) + long(info[ST_SIZE]) sz = du(fn, files) + long(info[ST_SIZE])
else: else:
sz = info[ST_SIZE] sz = info[ST_SIZE]
d[fn] = sz d[fn] = sz
tsz = tsz + sz tsz = tsz + sz
return tsz return tsz
def abspath(p): def abspath(p):
@ -400,19 +402,19 @@ def abspath(p):
class DirDialog(FileDialog.LoadFileDialog): class DirDialog(FileDialog.LoadFileDialog):
def __init__(self, master, title=None): def __init__(self, master, title=None):
FileDialog.LoadFileDialog.__init__(self, master, title) FileDialog.LoadFileDialog.__init__(self, master, title)
self.files.destroy() self.files.destroy()
self.filesbar.destroy() self.filesbar.destroy()
def ok_command(self): def ok_command(self):
file = self.get_selection() file = self.get_selection()
if not os.path.isdir(file): if not os.path.isdir(file):
self.master.bell() self.master.bell()
else: else:
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)
@ -438,22 +440,48 @@ class DirDialog(FileDialog.LoadFileDialog):
def main_builtin_du(args): def main_builtin_du(args):
import sys import sys
if len(args) > 1: if len(args) > 1:
dir = args[1] p = args[1]
else: else:
t = Tkinter.Tk() t = Tkinter.Tk()
t.wm_withdraw() t.wm_withdraw()
dir = askdirectory() p = askdirectory()
if Tkinter._default_root is t: if Tkinter._default_root is t:
Tkinter._default_root = None Tkinter._default_root = None
t.destroy() t.destroy()
if dir is None: return if p is None:
return
files = {} files = {}
if dir == "-":
main_pipe() 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
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()
else: else:
dir = abspath(dir) p = abspath(p)
putname(files, dir, du(dir, files)) if os.path.isfile(p):
doit(dir, files) if p.endswith('.gz'):
# gzipped file
main(gzip.open(p, 'r'))
else:
main(open(p, 'r'))
else:
putname(files, p, du(p, files))
doit(p, files)
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys