Preview

import datetime as D
import fnmatch
import os
import pathlib
import stat
import string
import tkinter as T
import time

if os.name == 'posix':
    import pwd, grp

HEIGHT = 32
FONT = ('HackGen Console', 12)

cwd = pathlib.Path('/')

tl = T.Tk() # toplevel window
tl.geometry('640x480')
tl.title('Preview')

buf  = T.StringVar()
mask = T.StringVar()

font = FONT
wildcard    = T.Entry(  tl, font=font, bg='cyan',   textvariable=mask)
filelist    = T.Listbox(tl, font=font, bg='yellow', activestyle=T.NONE)
previewpain = T.Label(  tl, font=font, bg='pink',   textvariable=buf,
    justify=T.LEFT, anchor=T.NW, height=HEIGHT, compound=T.CENTER)

def font_resize(size):
    global font
    if font[1] < 8 and size < 0:
        return
    font = (font[0], font[1] + size)
    wildcard['font'] = font
    filelist['font'] = font
    previewpain['font'] = font

wildcard.pack(fill='both')
filelist.pack(fill='both')
previewpain.pack(fill='both')

def date5col(d):
    if d < 0: return '[???]'
    dx  = D.datetime.fromtimestamp(d)
    now = D.datetime.fromtimestamp(time.time())
    delta = now - dx
    if (delta < D.timedelta(days=1)):
        d = dx.strftime('%H:%M')
    elif (delta < D.timedelta(days=7)):
        d = dx.strftime('%a%d')
    elif (delta < D.timedelta(weeks=26)):
        d = dx.strftime('%d%b')
    else:
        d = dx.strftime(' %Y')
    return d

def size5col(n):
    u = 'BKMGT'
    v = 0
    while n >= 1024:
        n //= 1024
        v += 1
    if v >= len(u):
        return 'LARGE'
    return f'{n:4}{u[v]}'

def stat_str(k):
    s = os.stat(k)
    m = stat.filemode(s.st_mode)
    n = s.st_nlink
    u = s.st_uid
    g = s.st_gid
    d = date5col(s.st_ctime)
    s = size5col(s.st_size)
    i = k.name
    t = f'{ m } { n } { u } { g } { s } { d } { i }\n'
    return t

def fnmask(x):
    l = mask.get().split()
    if l == []: return True
    for i in l:
        i = i.strip()
        # print(x, i)
        if fnmatch.fnmatch(x, i):
            return True
    return False

def update_filelist():
    global filenames
    filenames = ['..']
    filelist.delete(0, 'end')
    filelist.insert(0, '..')
    for i in os.listdir(cwd):
        t = stat_str(cwd / i)
        if not fnmask(i): continue
        filelist.insert('end', t)
        filenames.append(i)
    #print(filenames)

def printable(x):
    if x in string.whitespace:
        return '.'
    elif x in string.printable:
        return x
    else:
        return '.'

def dump(data):
    r = ''
    for i in range(len(data) // 16 + 1):
        d = data[:16]
        t = ' '.join(map(lambda x: '%02x' % x, d))
        a =  ''.join(map(lambda x: printable(chr(x)), d))
        r += f'{i * 16:8x} | {t:47} | { a }\n' # 47 = 16 * 3 - 1
        data = data[16:]
    return r

def read_file(file):
    try:
        with file.open() as fi:
            t = fi.read(4096)
    except UnicodeDecodeError:
        with file.open('rb') as fi:
            t = fi.read(4096)
    except:
        return f'<< FILE >>\nlen={ len(t) }'

    if type(t) == bytes:
        try:
            t = str(t, errors='strict')
        except UnicodeDecodeError:
            pass

    if type(t) == str:
        return '\n'.join(t.split('\n')[:HEIGHT])
    elif type(t) == bytes:
        return f'<< FILE >>\nlen={ len(t) }\n{ dump(t) }'

def read_dir(the_dir):
    t = ''
    try:
        for i in os.listdir(the_dir):
            k = the_dir / i
            t += stat_str(k)
    except PermissionError:
        return 'NO DATA'
    return t

def update_preview(event=None):
    t = filelist.curselection()
    # print(f'cursel={t}')
    if not t: return
    n = t[0]
    f = filelist.get(n)
    f = filenames[n]
    t = cwd / f
    s = os.stat(t)
    if stat.S_ISDIR(s.st_mode):
        buf.set(f'{ t }\n<< DIR >>')
        buf.set(f'{ t }\n{ read_dir(t) } ')
    elif f[-4:] == '.png':
        buf.set(f'{ t }\n<< FILE .png >>')
    else:
        buf.set(f'{ t }\n<< FILE >>')
        buf.set(f'{ t }\n{ read_file(t) } ')

def change_dir(event=None):
    global cwd
    t = filelist.curselection()
    # print(f'cursel={t}')
    if not t: return
    n = t[0]
    f = filelist.get(n)
    f = filenames[n]
    t = cwd / f
    s = os.stat(t)
    if stat.S_ISDIR(s.st_mode):
        # print(f'change_dir: { t }')
        cwd = t.resolve()
        update_filelist()
        filelist.selection_set(0)
        update_preview()

def change_dir_up(event=None):
    filelist.selection_set(0)
    change_dir()

def cursor_move(n, update=True):
    global cwd
    t = filelist.curselection()
    # print(f'cursel={t} curmove={n}')

    if not t: t = (0,)
    n += t[0]
    s = filelist.size()
    if n < 0:  n = 0
    if n >= s: n = s - 1
    filelist.selection_clear(0, 'end')
    filelist.selection_set(n)
    filelist.activate(n)
    filelist.see(n)
    if update:
        update_preview()

update_filelist()
filelist.selection_set(0)
update_preview()

RET = '<Key-Return>'
BS  = '<Key-BackSpace>'
LBS = '<<ListboxSelect>>'
filelist.bind(LBS, update_preview)
filelist.bind(RET, change_dir)
filelist.bind(BS,  change_dir_up)
filelist.bind('l', change_dir)
filelist.bind('h', change_dir_up)
filelist.bind('J', lambda x: cursor_move(+1, update=False))
filelist.bind('K', lambda x: cursor_move(-1, update=False))
filelist.bind('j', lambda x: cursor_move(+1))
filelist.bind('k', lambda x: cursor_move(-1))
filelist.bind('u', lambda x: font_resize(+2))
filelist.bind('i', lambda x: font_resize(-2))
filelist.bind('r', lambda x: update_filelist())
filelist.bind('q', lambda x: tl.quit())
filelist.focus_set()
wildcard.bind('<KeyRelease>', lambda x: update_filelist())

tl.mainloop()
# vim:et:sw=4:
parent directory