Welcome, guest | Sign In | My Account | Store | Cart
#! /usr/bin/env python
from tkinter import NoDefaultRoot, Tk, ttk, filedialog
from _tkinter import getbusywaitinterval
from tkinter.constants import *
from math import sin, pi
import base64, zlib, os

################################################################################

ICON = b'eJxjYGAEQgEBBiApwZDBzMAgxsDAoAHEQCEGBQaIOAwkQDE2UOSkiUM\
Gp/rlyd740Ugzf8/uXROxAaA4VvVAqcfYAFCcoHqge4hR/+btWwgCqoez8aj//fs\
XWiAARfCrhyCg+XA2HvV/YACoHs4mRj0ywKWe1PD//p+B4QMOmqGeMAYAAY/2nw=='

################################################################################

class GUISizeTree(ttk.Frame):

    @classmethod
    def main(cls):
        # Create the application's root.
        NoDefaultRoot()
        root = Tk()
        # Restrict sizing and add title.
        root.minsize(350, 175)
        root.title('Directory Size')
        # Create the application's icon.
        with open('tree.ico', 'wb') as file:
            file.write(zlib.decompress(base64.b64decode(ICON)))
        root.iconbitmap('tree.ico')
        os.remove('tree.ico')
        # Configure the SizeTree object.
        view = cls(root)
        view.grid(row=0, column=0, sticky=NSEW)
        # Setup the window for resizing.
        root.grid_rowconfigure(0, weight=1)
        root.grid_columnconfigure(0, weight=1)
        # Enter the GUI main event loop.
        root.mainloop()

    def __init__(self, master=None, **kw):
        super().__init__(master, **kw)
        # Configure the progressbar.
        self.__progress = ttk.Progressbar(self, orient=HORIZONTAL)
        self.__progress.grid(row=0, column=0, columnspan=4, sticky=EW)
        # Configure the tree.
        self.__tree = ttk.Treeview(self, selectmode=BROWSE,
                                   columns=('d_size', 'f_size', 'path'))
        self.__tree.heading('#0', text=' Name', anchor=W)
        self.__tree.heading('d_size', text=' Total Size', anchor=W)
        self.__tree.heading('f_size', text=' File Size', anchor=W)
        self.__tree.heading('path', text=' Path', anchor=W)
        self.__tree.column('#0', minwidth=80, width=160)
        self.__tree.column('d_size', minwidth=80, width=160)
        self.__tree.column('f_size', minwidth=80, width=160)
        self.__tree.column('path', minwidth=80, width=160)
        self.__tree.grid(row=1, column=0, columnspan=3, sticky=NSEW)
        # Configure the scrollbar.
        self.__scroll = ttk.Scrollbar(self, orient=VERTICAL,
                                      command=self.__tree.yview)
        self.__tree.configure(yscrollcommand=self.__scroll.set)
        self.__scroll.grid(row=1, column=3, sticky=NS)
        # Configure the path button.
        self.__label = ttk.Button(self, text='Path:', command=self.choose)
        self.__label.bind('<Return>', self.choose)
        self.__label.grid(row=2, column=0)
        # Configure the directory dialog.
        head, tail = os.getcwd(), True
        while tail:
            head, tail = os.path.split(head)
        self.__dialog = filedialog.Directory(self, initialdir=head)
        # Configure the path entry box.
        self.__path = ttk.Entry(self, cursor='xterm')
        self.__path.bind('<Control-Key-a>', self.select_all)
        self.__path.bind('<Control-Key-/>', lambda event: 'break')
        self.__path.bind('<Return>', self.search)
        self.__path.grid(row=2, column=1, sticky=EW)
        self.__path.focus_set()
        # Configure the execution button.
        self.__run = ttk.Button(self, text='Search', command=self.search)
        self.__run.bind('<Return>', self.search)
        self.__run.grid(row=2, column=2)
        # Configure the sizegrip.
        self.__grip = ttk.Sizegrip(self)
        self.__grip.grid(row=2, column=3, sticky=SE)
        # Configure the grid.
        self.grid_rowconfigure(1, weight=1)
        self.grid_columnconfigure(1, weight=1)
        # Configure root item in tree.
        self.__root = None

    def choose(self, event=None):
        # Get a directory path via a dialog.
        path = self.__dialog.show()
        if path:
            # Fill entry box with user path.
            self.__path.delete(0, END)
            self.__path.insert(0, os.path.abspath(path))

    def select_all(self, event):
        # Select the contents of the widget.
        event.widget.selection_range(0, END)
        return 'break'

    def search(self, event=None):
        if self.__run['state'].string == NORMAL:
            # Show background work progress.
            self.__run['state'] = DISABLED
            path = os.path.abspath(self.__path.get())
            if os.path.isdir(path):
                self.__progress.configure(mode='indeterminate', maximum=100)
                self.__progress.start()
                # Search while updating display.
                if self.__root is not None:
                    self.__tree.delete(self.__root)
                tree = SizeTree(self.update, path)
                nodes = tree.total_nodes + 1
                # Build user directory treeview.
                self.__progress.stop()
                self.__progress.configure(mode='determinate', maximum=nodes)
                self.__root = self.__tree.insert('', END, text=tree.name)
                self.build_tree(self.__root, tree)
                # Indicate completion of search.
                self.__run['state'] = NORMAL
            else:
                self.shake()

    def shake(self):
        # Check frame rate.
        assert getbusywaitinterval() == 20, 'Values are hard-coded for 50 FPS.'
        # Get application root.
        root = self
        while not isinstance(root, Tk):
            root = root.master
        # Schedule beginning of animation.
        self.after_idle(self.__shake, root, 0)

    def __shake(self, root, frame):
        frame += 1
        # Get the window's location and update X value.
        x, y = map(int, root.geometry().split('+')[1:])
        x += int(sin(pi * frame / 2.5) * sin(pi * frame / 50) * 5)
        root.geometry('+{}+{}'.format(x, y))
        # Schedule next frame or restore search button.
        if frame < 50:
            self.after(20, self.__shake, root, frame)
        else:
            self.__run['state'] = NORMAL

    def build_tree(self, node, tree):
        # Make changes to the treeview and progress bar.
        text = 'Unknown!' if tree.dir_error else convert(tree.total_size)
        self.__tree.set(node, 'd_size', text)
        text = 'Unknown!' if tree.file_error else convert(tree.file_size)
        self.__tree.set(node, 'f_size', text)
        self.__tree.set(node, 'path', tree.path)
        self.__progress.step()
        # Update the display and extract any child node.
        self.update()
        for child in tree.children:
            subnode = self.__tree.insert(node, END, text=child.name)
            self.build_tree(subnode, child)

################################################################################

class SizeTree:

    "Create a tree structure outlining a directory's size."

    def __init__(self, callback, path):
        callback()
        self.path = path
        head, tail = os.path.split(path)
        self.name = tail or head
        self.children = []
        self.file_size = 0
        self.total_size = 0
        self.total_nodes = 0
        self.file_error = False
        self.dir_error = False
        try:
            dir_list = os.listdir(path)
        except OSError:
            self.dir_error = True
        else:
            for name in dir_list:
                path_name = os.path.join(path, name)
                if os.path.isdir(path_name):
                    size_tree = SizeTree(callback, path_name)
                    self.children.append(size_tree)
                    self.total_size += size_tree.total_size
                    self.total_nodes += size_tree.total_nodes + 1
                elif os.path.isfile(path_name):
                    try:
                        self.file_size += os.path.getsize(path_name)
                    except OSError:
                        self.file_error = True
            self.total_size += self.file_size

################################################################################

def convert(number):
    "Convert bytes into human-readable representation."
    if not number:
        return '0 Bytes'
    assert 0 < number < 1 << 110, 'number out of range'
    ordered = reversed(tuple(format_bytes(partition_number(number, 1 << 10))))
    cleaned = ', '.join(item for item in ordered if item[0] != '0')
    return cleaned

def partition_number(number, base):
    "Continually divide number by base until zero."
    div, mod = divmod(number, base)
    yield mod
    while div:
        div, mod = divmod(div, base)
        yield mod

def format_bytes(parts):
    "Format partitioned bytes into human-readable strings."
    for power, number in enumerate(parts):
        yield '{} {}'.format(number, format_suffix(power, number))

def format_suffix(power, number):
    "Compute the suffix for a certain power of bytes."
    return (PREFIX[power] + 'byte').capitalize() + ('s' if number != 1 else '')

PREFIX = ' kilo mega giga tera peta exa zetta yotta bronto geop'.split(' ')

################################################################################

if __name__ == '__main__':
    GUISizeTree.main()

Diff to Previous Revision

--- revision 1 2011-02-08 02:15:37
+++ revision 2 2011-02-09 13:47:54
@@ -1,8 +1,15 @@
-import os
-from tkinter import NoDefaultRoot, Tk
+#! /usr/bin/env python
+from tkinter import NoDefaultRoot, Tk, ttk, filedialog
+from _tkinter import getbusywaitinterval
 from tkinter.constants import *
-from tkinter import filedialog
-from tkinter import ttk
+from math import sin, pi
+import base64, zlib, os
+
+################################################################################
+
+ICON = b'eJxjYGAEQgEBBiApwZDBzMAgxsDAoAHEQCEGBQaIOAwkQDE2UOSkiUM\
+Gp/rlyd740Ugzf8/uXROxAaA4VvVAqcfYAFCcoHqge4hR/+btWwgCqoez8aj//fs\
+XWiAARfCrhyCg+XA2HvV/YACoHs4mRj0ywKWe1PD//p+B4QMOmqGeMAYAAY/2nw=='
 
 ################################################################################
 
@@ -10,110 +17,184 @@
 
     @classmethod
     def main(cls):
+        # Create the application's root.
         NoDefaultRoot()
         root = Tk()
+        # Restrict sizing and add title.
+        root.minsize(350, 175)
         root.title('Directory Size')
+        # Create the application's icon.
+        with open('tree.ico', 'wb') as file:
+            file.write(zlib.decompress(base64.b64decode(ICON)))
+        root.iconbitmap('tree.ico')
+        os.remove('tree.ico')
+        # Configure the SizeTree object.
         view = cls(root)
         view.grid(row=0, column=0, sticky=NSEW)
+        # Setup the window for resizing.
         root.grid_rowconfigure(0, weight=1)
         root.grid_columnconfigure(0, weight=1)
+        # Enter the GUI main event loop.
         root.mainloop()
 
     def __init__(self, master=None, **kw):
         super().__init__(master, **kw)
+        # Configure the progressbar.
+        self.__progress = ttk.Progressbar(self, orient=HORIZONTAL)
+        self.__progress.grid(row=0, column=0, columnspan=4, sticky=EW)
         # Configure the tree.
-        self.__tree = ttk.Treeview(self, columns=('d_size', 'f_size', 'path'))
-        self.__tree.heading('d_size', text='Total Size')
-        self.__tree.heading('f_size', text='File Size')
-        self.__tree.heading('path', text='Path')
-        self.__tree.grid(row=0, column=0, columnspan=3, sticky=NSEW)
+        self.__tree = ttk.Treeview(self, selectmode=BROWSE,
+                                   columns=('d_size', 'f_size', 'path'))
+        self.__tree.heading('#0', text=' Name', anchor=W)
+        self.__tree.heading('d_size', text=' Total Size', anchor=W)
+        self.__tree.heading('f_size', text=' File Size', anchor=W)
+        self.__tree.heading('path', text=' Path', anchor=W)
+        self.__tree.column('#0', minwidth=80, width=160)
+        self.__tree.column('d_size', minwidth=80, width=160)
+        self.__tree.column('f_size', minwidth=80, width=160)
+        self.__tree.column('path', minwidth=80, width=160)
+        self.__tree.grid(row=1, column=0, columnspan=3, sticky=NSEW)
         # Configure the scrollbar.
         self.__scroll = ttk.Scrollbar(self, orient=VERTICAL,
                                       command=self.__tree.yview)
         self.__tree.configure(yscrollcommand=self.__scroll.set)
-        self.__scroll.grid(row=0, column=3, sticky=NS)
+        self.__scroll.grid(row=1, column=3, sticky=NS)
         # Configure the path button.
         self.__label = ttk.Button(self, text='Path:', command=self.choose)
-        self.__label.grid(row=1, column=0)
+        self.__label.bind('<Return>', self.choose)
+        self.__label.grid(row=2, column=0)
         # Configure the directory dialog.
-        root, suffix = os.getcwd(), True
-        while suffix:
-            root, suffix = os.path.split(root)
-        self.__dialog = filedialog.Directory(self, initialdir=root)
+        head, tail = os.getcwd(), True
+        while tail:
+            head, tail = os.path.split(head)
+        self.__dialog = filedialog.Directory(self, initialdir=head)
         # Configure the path entry box.
-        self.__path = ttk.Entry(self)
+        self.__path = ttk.Entry(self, cursor='xterm')
         self.__path.bind('<Control-Key-a>', self.select_all)
         self.__path.bind('<Control-Key-/>', lambda event: 'break')
         self.__path.bind('<Return>', self.search)
-        self.__path.grid(row=1, column=1, sticky=EW)
+        self.__path.grid(row=2, column=1, sticky=EW)
+        self.__path.focus_set()
         # Configure the execution button.
         self.__run = ttk.Button(self, text='Search', command=self.search)
-        self.__run.grid(row=1, column=2)
+        self.__run.bind('<Return>', self.search)
+        self.__run.grid(row=2, column=2)
         # Configure the sizegrip.
         self.__grip = ttk.Sizegrip(self)
-        self.__grip.grid(row=1, column=3, sticky=SE)
+        self.__grip.grid(row=2, column=3, sticky=SE)
         # Configure the grid.
-        self.grid_rowconfigure(0, weight=1)
+        self.grid_rowconfigure(1, weight=1)
         self.grid_columnconfigure(1, weight=1)
         # Configure root item in tree.
         self.__root = None
 
-    def choose(self):
+    def choose(self, event=None):
+        # Get a directory path via a dialog.
         path = self.__dialog.show()
         if path:
+            # Fill entry box with user path.
             self.__path.delete(0, END)
             self.__path.insert(0, os.path.abspath(path))
 
+    def select_all(self, event):
+        # Select the contents of the widget.
+        event.widget.selection_range(0, END)
+        return 'break'
+
     def search(self, event=None):
-        self.__run['state'] = DISABLED
-        if self.__root is not None:
-            self.__tree.delete(self.__root)
-        tree = SizeTree(self.update, os.path.abspath(self.__path.get()))
-        self.__root = self.__tree.insert('', END, text=tree.name)
-        self.build_tree(self.__root, tree)
-        self.__run['state'] = NORMAL
+        if self.__run['state'].string == NORMAL:
+            # Show background work progress.
+            self.__run['state'] = DISABLED
+            path = os.path.abspath(self.__path.get())
+            if os.path.isdir(path):
+                self.__progress.configure(mode='indeterminate', maximum=100)
+                self.__progress.start()
+                # Search while updating display.
+                if self.__root is not None:
+                    self.__tree.delete(self.__root)
+                tree = SizeTree(self.update, path)
+                nodes = tree.total_nodes + 1
+                # Build user directory treeview.
+                self.__progress.stop()
+                self.__progress.configure(mode='determinate', maximum=nodes)
+                self.__root = self.__tree.insert('', END, text=tree.name)
+                self.build_tree(self.__root, tree)
+                # Indicate completion of search.
+                self.__run['state'] = NORMAL
+            else:
+                self.shake()
+
+    def shake(self):
+        # Check frame rate.
+        assert getbusywaitinterval() == 20, 'Values are hard-coded for 50 FPS.'
+        # Get application root.
+        root = self
+        while not isinstance(root, Tk):
+            root = root.master
+        # Schedule beginning of animation.
+        self.after_idle(self.__shake, root, 0)
+
+    def __shake(self, root, frame):
+        frame += 1
+        # Get the window's location and update X value.
+        x, y = map(int, root.geometry().split('+')[1:])
+        x += int(sin(pi * frame / 2.5) * sin(pi * frame / 50) * 5)
+        root.geometry('+{}+{}'.format(x, y))
+        # Schedule next frame or restore search button.
+        if frame < 50:
+            self.after(20, self.__shake, root, frame)
+        else:
+            self.__run['state'] = NORMAL
 
     def build_tree(self, node, tree):
-        self.__tree.set(node, 'd_size', convert(tree.total_size))
-        self.__tree.set(node, 'f_size', convert(tree.file_size))
+        # Make changes to the treeview and progress bar.
+        text = 'Unknown!' if tree.dir_error else convert(tree.total_size)
+        self.__tree.set(node, 'd_size', text)
+        text = 'Unknown!' if tree.file_error else convert(tree.file_size)
+        self.__tree.set(node, 'f_size', text)
         self.__tree.set(node, 'path', tree.path)
+        self.__progress.step()
+        # Update the display and extract any child node.
         self.update()
         for child in tree.children:
             subnode = self.__tree.insert(node, END, text=child.name)
             self.build_tree(subnode, child)
 
-    def select_all(self, event):
-        event.widget.selection_range(0, END)
-        return 'break'
-
 ################################################################################
 
 class SizeTree:
 
-    def __init__(self, callback, path, name=None):
+    "Create a tree structure outlining a directory's size."
+
+    def __init__(self, callback, path):
         callback()
         self.path = path
-        self.name = os.path.basename(path) if name is None else name
+        head, tail = os.path.split(path)
+        self.name = tail or head
         self.children = []
         self.file_size = 0
         self.total_size = 0
+        self.total_nodes = 0
+        self.file_error = False
+        self.dir_error = False
         try:
             dir_list = os.listdir(path)
         except OSError:
-            pass
+            self.dir_error = True
         else:
             for name in dir_list:
                 path_name = os.path.join(path, name)
                 if os.path.isdir(path_name):
-                    size_tree = SizeTree(callback, path_name, name)
+                    size_tree = SizeTree(callback, path_name)
                     self.children.append(size_tree)
                     self.total_size += size_tree.total_size
+                    self.total_nodes += size_tree.total_nodes + 1
                 elif os.path.isfile(path_name):
                     try:
                         self.file_size += os.path.getsize(path_name)
                     except OSError:
-                        pass
-        self.total_size += self.file_size
+                        self.file_error = True
+            self.total_size += self.file_size
 
 ################################################################################
 

History