Skip to content

Instantly share code, notes, and snippets.

@Tantalus13A98B5F
Last active August 14, 2020 06:14
Show Gist options
  • Save Tantalus13A98B5F/bf2239d09d50c36eb299414d0932b82e to your computer and use it in GitHub Desktop.
Save Tantalus13A98B5F/bf2239d09d50c36eb299414d0932b82e to your computer and use it in GitHub Desktop.
A simple GUI tool to inspect disk usage.
from tkinter.filedialog import askdirectory
from tkinter import *
from tkinter.ttk import *
import subprocess
import weakref
import sys
import os
def human_size(cntbytes):
lst = [
(1024**3, 'GB'),
(1024**2, 'MB'),
(1024, 'KB')
]
for base, unit in lst:
if cntbytes > base:
return '{:.2f} {}'.format(cntbytes / base, unit)
return '{} B'.format(cntbytes)
class FileType:
DIR = 'dir'
FILE = 'file'
SYMLINK = 'symlink'
OTHER = 'other'
class FileTree:
refdict = weakref.WeakValueDictionary()
root = tree = None
# class methods
@classmethod
def get_node(cls, ref):
return cls.refdict[ref]
@classmethod
def create_root(cls, path):
assert os.path.isdir(path)
name = os.path.basename(path)
cls.root = cls(name, path, FileType.DIR)
cls.root.ref = ''
return cls.root
@classmethod
def configure_treeview(cls, tree):
cls.tree = tree
cls.tree.config(selectmode='browse', columns=['type', 'size', 'num'])
cls.tree.heading('#0', text='Name')
cls.tree.heading('type', text='Type')
cls.tree.heading('size', text='Size')
cls.tree.heading('num', text='# of Files')
cls.tree.column('#0', stretch=True)
cls.tree.column('type', stretch=False, anchor='e', width=80)
cls.tree.column('size', stretch=False, anchor='e', width=80)
cls.tree.column('num', stretch=False, anchor='e', width=80)
# instance methods
def get_values_for_display(self):
return (self.typ, human_size(self.size), str(self.num))
def __init__(self, name, path, typ, size=0):
# path
self.name = name
self.path = path
# file attributes
self.typ = typ
self.size = size
self.num = 1
# parent & children
self.parent = None
self.children = None
# others
self.ref = None
self.error = None
def is_dir(self):
return self.typ == FileType.DIR
def is_displayed(self):
return self.ref
def is_displayed_or_root(self):
return self.ref is not None
# build subtree objects
@classmethod
def _from_direntry(cls, entry):
try:
if entry.is_dir(follow_symlinks=False):
ret = cls(entry.name, entry.path, FileType.DIR)
ret.build_subtree()
else:
st = entry.stat(follow_symlinks=False)
ret = cls(entry.name, entry.path, FileType.FILE, st.st_size)
except OSError as e:
ret = cls(entry.name, entry.path, FileType.OTHER)
ret.error = e
return ret
def _add_child(self, item):
item.parent = weakref.ref(self)
self.children.append(item)
self.size += item.size
self.num += item.num
def build_subtree(self):
assert self.is_dir()
self.children = []
self.size, self.num = 0, 1
try:
for item in os.scandir(self.path):
file = self._from_direntry(item)
self._add_child(file)
except OSError as e:
self.error = e
# display subtree
def _display_single(self):
if not self.is_displayed_or_root():
self.ref = self.tree.insert(self.parent().ref, 'end',
text=self.name, values=self.get_values_for_display())
self.refdict[self.ref] = self
def display_subtree(self):
assert self.is_dir() and self.is_displayed_or_root()
for item in self.children:
item._display_single()
if item.is_dir():
for item2 in item.children:
item2._display_single()
self.children.sort(key=lambda x: x.size, reverse=True)
self.tree.set_children(self.ref, *[i.ref for i in self.children])
# refresh subtree
def refresh_subtree(self):
if self.is_displayed_or_root():
self.tree.delete(*self.tree.get_children(self.ref))
self.build_subtree()
self.display_subtree()
# update subtree
def _update_up(self, diffsize, diffnum, *, already_added=False):
if not already_added:
self.size += diffsize
self.num += diffnum
if self.is_displayed():
self.tree.item(self.ref, values=self.get_values_for_display())
if self.parent is not None:
self.parent()._update_up(diffsize, diffnum)
def update_subtree(self):
oldsize, oldnum = self.size, self.num
self.refresh_subtree()
diffsize = self.size - oldsize
diffnum = self.num - oldnum
self._update_up(diffsize, diffnum, already_added=True)
class Application(Frame):
def __init__(self, master):
super().__init__(master)
self.pack(expand=1, fill='both')
self.master = master
self.create_menu()
self.create_widgets()
def create_menu(self):
menubar = Menu(self.master)
filemenu = Menu(menubar, tearoff=0)
filemenu.add_command(
label='Open Folder',
accelerator='Ctrl-O',
command=self.open_folder)
self.bind_all('<Control-o>', self.open_folder)
filemenu.add_separator()
filemenu.add_command(
label='Reveal in Explorer',
accelerator='Ctrl-E',
command=self.reveal_file)
self.bind_all('<Control-e>', self.reveal_file)
filemenu.add_command(
label='Refresh Here',
accelerator='Ctrl-R',
command=self.refresh_here)
self.bind_all('<Control-r>', self.refresh_here)
menubar.add_cascade(label='File', menu=filemenu)
self.master.config(menu=menubar)
def create_widgets(self):
self.pathvar = StringVar()
self.pathvar.set('(none)')
pathentry = Entry(self, state='readonly', textvariable=self.pathvar)
pathentry.pack(side='top', fill='x')
self.tree = Treeview(self)
FileTree.configure_treeview(self.tree)
self.tree.bind('<<TreeviewOpen>>', self.expand_current_item)
self.tree.pack(side='top', expand=1, fill='both')
def expand_current_item(self, event):
item = self.tree.focus()
node = FileTree.get_node(item)
node.display_subtree()
def open_folder(self, event=None):
rootpath = askdirectory()
if rootpath:
rootpath = os.path.abspath(rootpath)
self.pathvar.set(rootpath)
self.tree.delete(*self.tree.get_children())
rootnode = FileTree.create_root(rootpath)
rootnode.refresh_subtree()
def reveal_file(self, event=None):
item_id = self.tree.focus()
if item_id:
node = FileTree.get_node(item_id)
if os.path.isdir(node.path):
subprocess.call(['explorer', node.path])
def refresh_here(self, event=None):
item_id = self.tree.focus()
if item_id:
node = FileTree.get_node(item_id)
while not os.path.isdir(node.path):
node = node.parent()
node.update_subtree()
if __name__ == '__main__':
root = Tk()
app = Application(master=root)
app.mainloop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment