diff --git a/tkdu.py b/tkdu.py old mode 100644 new mode 100755 index 2e42156..06b142d --- a/tkdu.py +++ b/tkdu.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + # This is tkdu.py, an interactive program to display disk usage # Copyright 2004 Jeff Epler # @@ -15,19 +17,19 @@ # 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, FileDialog +import math, Tkinter, sys, os, stat, string, time, gzip, FileDialog from tkFileDialog import askdirectory MIN_PSZ = 1000 MIN_IPSZ = 240 MIN_W = 50 -MIN_H = 10 +MIN_H = 15 VERTICAL = "vertical" HORIZONTAL = "horizontal" NUM_QUEUE = 25 -FONT_FACE = ("helvetica", 8) +FONT_FACE = ("helvetica", 12) BORDER = 2 -FONT_HEIGHT = 8 +FONT_HEIGHT = 12 FONT_HEIGHT2 = 20 dircolors = ['#ff7070', '#70ff70', '#7070ff'] @@ -39,11 +41,11 @@ def allocate(path, files, canvas, x, y, w, h, first, depth): psz = w*h if psz < MIN_PSZ: return if path and path[-1] == "/": - basename_idx = len(path) - nslashes = string.count(path, os.sep) - 1 + basename_idx = len(path) + nslashes = string.count(path, os.sep) - 1 else: - basename_idx = len(path) + 1 - nslashes = string.count(path, os.sep) + basename_idx = len(path) + 1 + nslashes = string.count(path, os.sep) dircolor = dircolors[nslashes % len(dircolors)] leafcolor = leafcolors[nslashes % len(dircolors)] 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] ff = ff[first:] for item in ff: - totsz = totsz + item[0] - item[2] = None + totsz = totsz + item[0] + item[2] = None if totsz == 0: return @@ -62,149 +64,149 @@ def allocate(path, files, canvas, x, y, w, h, first, depth): ratio = psz*1./totsz 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 - 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 - 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 + if w > h: + orient = VERTICAL + usew = w - h*2./3 + 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 + 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: - use = use + ff[j][0] - j=j+1 + j = i+1 + use = ff[i][0] + while j < len(ff) and use < want: #and j < i + maxcnt: + use = use + ff[j][0] + j=j+1 - if orient is VERTICAL: - usew = use * ratio / h - if usew <= 2*BORDER: break - y0 = y - for item in ff[i:j]: - dy = item[0]/usew*ratio - item[2] = (x, y0, usew, dy) - y0 = y0 + dy - x = x + usew - w = w - usew - else: - useh = use * ratio / w - if useh <= 2*BORDER: break - x0 = x - for item in ff[i:j]: - dx = item[0]/useh*ratio - item[2] = (x0, y, dx, useh) - x0 = x0 + dx - y = y + useh - h = h - useh - i = j + if orient is VERTICAL: + usew = use * ratio / h + if usew <= 2*BORDER: break + y0 = y + for item in ff[i:j]: + dy = item[0]/usew*ratio + item[2] = (x, y0, usew, dy) + y0 = y0 + dy + x = x + usew + w = w - usew + else: + useh = use * ratio / w + if useh <= 2*BORDER: break + x0 = x + for item in ff[i:j]: + dx = item[0]/useh*ratio + item[2] = (x0, y, dx, useh) + x0 = x0 + dx + y = y + useh + h = h - useh + i = j for item in ff: - sz = item[0] - 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 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") + sz = item[0] + 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 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") - i = tk_call(canvas._w, - "create", "rectangle", - x+BORDER, y+BORDER, x+w-BORDER, y+h-BORDER, - "-fill", color) - canvas.map[int(i)] = name + i = tk_call(canvas._w, + "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: - w1 = w - 2*BORDER - stem = name[basename_idx:] - ssz = size(sz) - text = "%s %s" % (name[basename_idx:], ssz) - tw = int(tk_call("font", "measure", FONT_FACE, text)) - if tw > w1: - if h > FONT_HEIGHT2 + 2*BORDER: - tw = max( - int(tk_call("font", "measure", FONT_FACE, stem)), - int(tk_call("font", "measure", FONT_FACE, ssz))) - 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") - 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) - 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") - 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) + if h > FONT_HEIGHT+2*BORDER: + w1 = w - 2*BORDER + stem = name[basename_idx:] + ssz = size(sz) + text = "%s %s" % (name[basename_idx:], ssz) + tw = int(tk_call("font", "measure", FONT_FACE, text)) + if tw > w1: + if h > FONT_HEIGHT2 + 2*BORDER: + tw = max( + int(tk_call("font", "measure", FONT_FACE, stem)), + int(tk_call("font", "measure", FONT_FACE, ssz))) + 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") + 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) + 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") + 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) def queue(c, *args): if c.aid is None: - c.aid = c.after_idle(run_queue, c) - c.configure(cursor="watch") + c.aid = c.after_idle(run_queue, c) + c.configure(cursor="watch") c.queue.append(args) def run_queue(c): queue = c.queue end = time.time() + .5 while 1: - if not queue: - c.aid = None - c.configure(cursor="") - break - if time.time() > end: break - item = queue[0] - del queue[0] - apply(item[0], item[1:]) + if not queue: + c.aid = None + c.configure(cursor="") + break + if time.time() > end: break + item = queue[0] + del queue[0] + apply(item[0], item[1:]) if queue: - c.aid = c.after_idle(run_queue, c) + c.aid = c.after_idle(run_queue, c) def chroot(e, r): c = e.widget if not getkids(c.files, r): - r = os.path.dirname(r) - if r == c.cur: return + r = os.path.dirname(r) + if r == c.cur: return if r is None: return if not r.startswith(c.root): - c.bell() - return + c.bell() + return c.cur = r c.first = 0 e.width = c.winfo_width() @@ -214,9 +216,9 @@ def chroot(e, r): def item_under_cursor(e): c = e.widget 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: - return None + return None return c.map.get(item, None) def descend(e): @@ -231,11 +233,11 @@ def ascend(e): def size(n): if n > 1024*1024*1024: - return "%.1fGB" % (n/1024./1024/1024) + return "%.1fGB" % (n/1024./1024/1024) elif n > 1024*1024: - return "%.1fMB" % (n/1024./1024) + return "%.1fMB" % (n/1024./1024) elif n > 1024: - return "%.1fKB" % (n/1024.) + return "%.1fKB" % (n/1024.) return "%d" % n def scroll(e, dir): @@ -245,10 +247,10 @@ def scroll(e, dir): if offset + 5 > l: offset = l-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) + c.first = offset + e.width = c.winfo_width() + e.height = c.winfo_height() + reconfigure(e) def schedule_tip(e): c = e.widget @@ -267,12 +269,12 @@ def make_tip(e, s): def cancel_tip(e, c=None): if c is None: - c = e.widget + c = e.widget if c.tipa: - c.after_cancel(c.tipa) - c.tipa = None + c.after_cancel(c.tipa) + c.tipa = None else: - c.tip.wm_withdraw() + c.tip.wm_withdraw() def reconfigure(e): 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.delete("all") for cb in c.cb: - c.after_cancel(cb) + c.after_cancel(cb) c.cb = [] c.aid = None c.queue = [] c.map = {} c.tipa = None if c.cur == "/": - nslashes = -1 + nslashes = -1 else: - nslashes = string.count(c.cur, os.sep) - 1 + nslashes = string.count(c.cur, os.sep) - 1 parent = os.path.dirname(c.cur) color = dircolors[nslashes % len(dircolors)] c.configure(background=color) @@ -299,16 +301,16 @@ def reconfigure(e): def putname_base(dict, name, base, size): try: - dict[base][name] = size + dict[base][name] = size except: - dict[base] = {name: size} + dict[base] = {name: size} def putname(dict, name, size): base = os.path.dirname(name) try: - dict[base][name] = size + dict[base][name] = size except: - dict[base] = {name: size} + dict[base] = {name: size} def getname(dict, name): base = os.path.dirname(name) @@ -316,14 +318,14 @@ def getname(dict, name): def getkids(dict, path): return dict.get(path, ((), {}))[1] - + def doit(dir, files): sorted_files = {} for k, v in files.items(): - sv = map(lambda (k, v): [v, k, None], v.items()) - sv.sort() - sv.reverse() - sorted_files[k] = (v, sv) + sv = map(lambda (k, v): [v, k, None], v.items()) + sv.sort() + sv.reverse() + sorted_files[k] = (v, sv) t = Tkinter.Tk() c = Tkinter.Canvas(t, width=1024, height=768) @@ -344,10 +346,10 @@ def doit(dir, files): t.bind("", lambda e, c=c: cancel_tip(e, c)) t.bind("", lambda e, t=t: t.destroy()) for i in range(10): - t.bind("" % i, lambda e, c=c, i=i: setdepth(e, c, i)) + t.bind("" % i, lambda e, c=c, i=i: setdepth(e, c, i)) c.bind("", lambda e: scroll(e, -1)) c.bind("", lambda e: scroll(e, 1)) - c.bind("", ascend) + c.bind("", ascend) c.tag_bind("all", "", descend) c.tag_bind("all", "", schedule_tip) c.tag_bind("all", "", cancel_tip) @@ -360,14 +362,14 @@ def setdepth(e, c, i): c.depth = i reconfigure(e) -def main_pipe(): +def main(f = sys.stdin): files = {} firstfile = None - for line in sys.stdin.readlines(): - sz, name = string.split(line[:-1], None, 1) -# name = name.split("/") - sz = long(sz)*1024 - putname(files, name, sz) + for line in f.readlines(): + sz, name = string.split(line[:-1], None, 1) +# name = name.split("/") + sz = long(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 = 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] for fn in fns: - fn = fmt % (dir, fn) + fn = fmt % (dir, fn) - try: - info = lstat(fn) - except: - continue + try: + info = lstat(fn) + except: + continue - if info[ST_MODE] & S_IFMT == S_IFDIR: - sz = du(fn, files) + long(info[ST_SIZE]) - else: - sz = info[ST_SIZE] - d[fn] = sz - tsz = tsz + sz + if info[ST_MODE] & S_IFMT == S_IFDIR: + sz = du(fn, files) + long(info[ST_SIZE]) + else: + sz = info[ST_SIZE] + d[fn] = sz + tsz = tsz + sz return tsz def abspath(p): @@ -400,19 +402,19 @@ def abspath(p): class DirDialog(FileDialog.LoadFileDialog): def __init__(self, master, title=None): - FileDialog.LoadFileDialog.__init__(self, master, title) - self.files.destroy() - self.filesbar.destroy() + FileDialog.LoadFileDialog.__init__(self, master, title) + self.files.destroy() + self.filesbar.destroy() def ok_command(self): - file = self.get_selection() - if not os.path.isdir(file): - self.master.bell() - else: - self.quit(file) + file = self.get_selection() + if not os.path.isdir(file): + self.master.bell() + else: + self.quit(file) def filter_command(self, event=None): - END="end" + END="end" dir, pat = self.get_filter() try: names = os.listdir(dir) @@ -438,22 +440,48 @@ class DirDialog(FileDialog.LoadFileDialog): def main_builtin_du(args): import sys if len(args) > 1: - dir = args[1] + p = args[1] else: - t = Tkinter.Tk() - t.wm_withdraw() - dir = askdirectory() - if Tkinter._default_root is t: - Tkinter._default_root = None - t.destroy() - if dir is None: return + t = Tkinter.Tk() + t.wm_withdraw() + p = askdirectory() + if Tkinter._default_root is t: + Tkinter._default_root = None + t.destroy() + if p is None: + return files = {} - if dir == "-": - main_pipe() + + if p == '-h' or p == '--help' or p == '-?': + base = os.path.basename(args[0]) + print 'Usage:' + print ' ', base, ' interpret file as gzipped du -ak output and visualize it' + print ' ', base, ' interpret file as du -ak output and visualize it' + print ' ', base, ' 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: - dir = abspath(dir) - putname(files, dir, du(dir, files)) - doit(dir, files) + p = abspath(p) + if os.path.isfile(p): + 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__': import sys