#!/usr/bin/env python3 import argparse import curses from pathlib import Path import subprocess import sys from time import sleep # Configuration text_album_current = "Aktuelles Album" text_album_root = "(Alle Fotoalben)" text_closed_pictures = "Gehe zurück ..." text_load_pictures = "Bilder werden geladen ..." text_no_pictures_found = "Hier sind leider noch keine Bilder drinnen." wait_no_pictures_found = 5 # seconds indicator = "➤" binding_up = [ord('k'), curses.KEY_UP, curses.KEY_PPAGE] binding_down = [ord('j'), curses.KEY_DOWN, curses.KEY_NPAGE] binding_accept = [ord('l'), ord(' '), ord('\n'), curses.KEY_HOME] binding_back = [ord('h'), ord('q'), curses.KEY_BACKSPACE, curses.KEY_END] binding_exit = [ord('Q')] # Code def stretch(text, length, char = ' '): missing = length - len(text) if 0 < missing: text += char * missing elif missing < 0: text = text[0:length] return text def build_description(base_dir, desc_path): rel_path = desc_path.relative_to(base_dir) parts = list(rel_path.parts[:-1]) parts.append(rel_path.stem) parts = [part.replace('_', ' ') for part in parts] return ' - '.join(parts) def show_pictures(files): files = list(files) # copy list files.insert(0, '/usr/bin/imvr') subprocess.call(files) return True def search_pictures(out, path): files = sorted([str(f) for f in path.iterdir() if f.is_file() and f.suffix.lower() in ['.png', '.jpg', '.jpeg', '.gif']]) if len(files) <= 0: out(text_no_pictures_found) sleep(wait_no_pictures_found) return True out(text_load_pictures) ret = show_pictures(files) out(text_closed_pictures) return ret def draw_select(win, dirs, sel): y_size, x_size = win.getmaxyx() # centered scroll y_center = y_size // 2 for y in range(y_size): i = sel + y - y_center if i < 0 or len(dirs) <= i: win.addstr(y, 1, " " * (x_size - 2), curses.A_NORMAL) else: text = " " text += indicator if i == sel else (" " * len(indicator)) text += " " text += dirs[i].name text = stretch(text, x_size - 2) text_format = curses.A_STANDOUT if i == sel else curses.A_NORMAL win.addstr(y, 1, text, text_format) return True def draw_header(win, base_dir, path): x_size = win.getmaxyx()[1] desc = build_description(base_dir, path) text = text_album_current + ": " text += desc if len(desc) > 0 else text_album_root win.addstr(1, 1, stretch(text, x_size - 2)) def show_select(win, base_dir, path): if not path.is_dir(): raise Exception("'" + str(path) + "' is not a directory!") dirs = sorted([d for d in path.iterdir() if d.is_dir()]) win.clear() if len(dirs) <= 0: draw_header(win, base_dir, path) x_size = win.getmaxyx()[1] def out(text): win.addstr(3, 1, stretch(text, x_size - 2)) win.refresh() return search_pictures(out, path) sel = 0 while True: draw_header(win, base_dir, path) draw_select(win.subwin(3, 0), dirs, sel) c = win.getch() if c in binding_up: sel = max(0, sel - 1) elif c in binding_down: sel = min(len(dirs) - 1, sel + 1) elif c in binding_accept: if not show_select(win, base_dir, dirs[sel]): return False win.clear() elif c in binding_back: return True elif c in binding_exit: return False def select_ui(win, args): curses.curs_set(0) while show_select(win, args.dir, args.dir) and args.kiosk: pass if args.kiosk: subprocess.call(['systemctl', 'poweroff']) return True # exited as expected def retry(fun): last_return = False while not last_return: try: last_return = fun() except: pass # TODO save log file with error def main(): # parse args parser = argparse.ArgumentParser(add_help=False) parser.add_argument('--base-dir', type=Path, dest='dir', required=True) parser.add_argument('--kiosk', dest='kiosk', action='store_true') parser.add_argument('--build-desc-of', type=Path, dest='desc_path', default=None) args = parser.parse_args() # build description if requested if args.desc_path is not None: print(build_description(args.dir, args.desc_path)) return # run menus in protective mode if kiosk mode is enabled def run(): curses.wrapper(select_ui, args) if args.kiosk: retry(run) else: run() if __name__ == '__main__': main()