tkdu/tkdu.py

496 lines
14 KiB
Python
Raw Normal View History

#!/usr/bin/env python
# This is tkdu.py, an interactive program to display disk usage
# Copyright 2004 Jeff Epler <jepler@unpythonic.net>
#
2022-09-02 21:11:56 +00:00
# This is the version ported to Python 3
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2022-09-02 21:11:56 +00:00
import math, tkinter, sys, os, stat, string, time, gzip, tkinter.filedialog
from tkinter.filedialog import askdirectory
MIN_PSZ = 1000
MIN_IPSZ = 240
MIN_W = 50
MIN_H = 15
VERTICAL = "vertical"
HORIZONTAL = "horizontal"
NUM_QUEUE = 25
FONT_FACE = ("helvetica", 12)
BORDER = 2
FONT_HEIGHT = 12
FONT_HEIGHT2 = 20
dircolors = ['#ff7070', '#70ff70', '#7070ff']
leafcolors = ['#bf5454', '#54bf54', '#5454bf']
def allocate(path, files, canvas, x, y, w, h, first, depth):
tk_call = canvas.tk.call
if w < MIN_W or h < MIN_H: return
psz = w*h
if psz < MIN_PSZ: return
if path and path[-1] == "/":
basename_idx = len(path)
2022-09-02 21:11:56 +00:00
nslashes = path.count(os.sep) - 1
else:
basename_idx = len(path) + 1
2022-09-02 21:11:56 +00:00
nslashes = path.count(os.sep)
dircolor = dircolors[nslashes % len(dircolors)]
leafcolor = leafcolors[nslashes % len(dircolors)]
colors = (leafcolor, dircolor)
totsz = 0
ff = getkids(files, path)
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
i = 0
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
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
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")
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)
def queue(c, *args):
if c.aid is None:
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]
2022-09-02 21:11:56 +00:00
item[0](*item[1:])
if queue:
c.aid = c.after_idle(run_queue, c)
def chroot(e, r):
c = e.widget
2022-09-02 21:11:56 +00:00
if r is None:
return
if not getkids(c.files, r):
r = os.path.dirname(r)
if r == c.cur: return
if not r.startswith(c.root):
c.bell()
return
c.cur = r
c.first = 0
e.width = c.winfo_width()
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)
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)
elif n > 1024*1024:
return "%.1fMB" % (n/1024./1024)
elif n > 1024:
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
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
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
c.tipl.configure(text=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
if c.tipa:
c.after_cancel(c.tipa)
c.tipa = None
else:
c.tip.wm_withdraw()
def reconfigure(e):
c = e.widget
w = e.width
h = e.height
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.cb = []
c.aid = None
c.queue = []
c.map = {}
c.tipa = None
if c.cur == "/":
nslashes = -1
else:
2022-09-02 21:11:56 +00:00
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:
dict[base][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():
2022-09-02 21:11:56 +00:00
t = [k, v]
sv = sorted(map((lambda t: [t[1], t[0], None]), v.items()))
sv.reverse()
sorted_files[k] = (v, sv)
2022-09-02 21:11:56 +00:00
t = tkinter.Tk()
c = tkinter.Canvas(t, width=1024, height=768)
c.tip = tkinter.Toplevel(t)
c.tip.wm_overrideredirect(1)
2022-09-02 21:11:56 +00:00
c.tipl = tkinter.Label(c.tip)
c.tipl.pack()
c.pack(expand="yes", fill="both")
c.files = sorted_files
c.cur = c.root = dir
c.t = t
c.cb = []
c.aid = None
c.queue = []
c.depth = 0
c.first = 0
c.bind("<Configure>", reconfigure)
t.bind("<Unmap>", lambda e, c=c: cancel_tip(e, c))
t.bind("<q>", lambda e, t=t: t.destroy())
for i in range(10):
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-5>", lambda e: scroll(e, 1))
2014-04-24 21:38:42 +00:00
if os.name == 'nt':
c.bind("<Button-3>", ascend)
else:
c.bind("<Button-2>", 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()
e.height = c.winfo_height()
c.depth = i
reconfigure(e)
def main(f = sys.stdin):
files = {}
firstfile = None
for line in f.readlines():
2022-09-02 21:11:56 +00:00
sz, name = line[:-1].split(None, 1)
# name = name.split("/")
2022-09-02 21:11:56 +00:00
sz = int(sz)*1024
putname(files, name, sz)
doit(name, files)
2022-09-02 21:11:56 +00:00
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
if not files.has_key(dir): files[dir] = {}
d = files[dir]
for fn in fns:
fn = fmt % (dir, fn)
try:
info = lstat(fn)
except:
continue
if info[ST_MODE] & S_IFMT == S_IFDIR:
2022-09-02 21:11:56 +00:00
sz = du(fn, files) + int(info[ST_SIZE])
else:
sz = info[ST_SIZE]
d[fn] = sz
tsz = tsz + sz
return tsz
def abspath(p):
return os.path.normpath(os.path.join(os.getcwd(), p))
2022-09-02 21:11:56 +00:00
class DirDialog(tkinter.filedialog.LoadFileDialog):
def __init__(self, master, title=None):
2022-09-02 21:11:56 +00:00
tkinter.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)
def filter_command(self, event=None):
END="end"
dir, pat = self.get_filter()
try:
names = os.listdir(dir)
except os.error:
self.master.bell()
return
self.directory = dir
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):
subdirs.append(name)
self.dirs.delete(0, END)
for name in subdirs:
self.dirs.insert(END, name)
head, tail = os.path.split(self.get_selection())
if tail == os.curdir: tail = ''
self.set_selection(tail)
def main_builtin_du(args):
import sys
if len(args) > 1:
p = args[1]
else:
2022-09-02 21:11:56 +00:00
t = tkinter.Tk()
t.wm_withdraw()
p = askdirectory()
2022-09-02 21:11:56 +00:00
if tkinter._default_root is t:
tkinter._default_root = None
t.destroy()
if p is None:
return
files = {}
if p == '-h' or p == '--help' or p == '-?':
base = os.path.basename(args[0])
2022-09-02 21:11:56 +00:00
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
2022-09-02 21:11:56 +00:00
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:
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
main_builtin_du(sys.argv)
# vim:sts=4:sw=4: