diff options
author | Daniel Schadt <kingdread@gmx.de> | 2022-05-26 22:52:46 +0200 |
---|---|---|
committer | Daniel Schadt <kingdread@gmx.de> | 2022-05-26 22:53:12 +0200 |
commit | bd865c10efe828c48e71e1c77478f0da6fdabc54 (patch) | |
tree | f32b260138186ff003f7856337a00949a881cbe9 | |
parent | 4f549de2e9b38bf81be891446cd3e2c296c9b92c (diff) | |
download | tf2sgu-bd865c10efe828c48e71e1c77478f0da6fdabc54.tar.gz tf2sgu-bd865c10efe828c48e71e1c77478f0da6fdabc54.tar.bz2 tf2sgu-bd865c10efe828c48e71e1c77478f0da6fdabc54.zip |
first version with the curses TUI
-rwxr-xr-x | tf2sgu | 251 |
1 files changed, 249 insertions, 2 deletions
@@ -22,6 +22,7 @@ logger = logging.getLogger("tf2sgu") class SaveGame(namedtuple("SaveGame", "name date base")): """Represents a savegame on disk.""" + def __str__(self): return f"{self.name} - {self.date}" @@ -39,6 +40,7 @@ class SaveGame(namedtuple("SaveGame", "name date base")): class UserException(Exception): """Exception that shouldbe displayed to the user.""" + def __init__(self, msg): super().__init__() self.msg = msg @@ -46,6 +48,7 @@ class UserException(Exception): class Profile: """An on-disk Steam profile.""" + def __init__(self, path): self.path = path @@ -144,7 +147,9 @@ def select_profile(args): def cmd_tui(args): """Load the user interface.""" - raise NotImplementedError() + import curses + + curses.wrapper(lambda screen: _curses_tui(args, screen)) def cmd_list_profiles(_args): @@ -245,12 +250,254 @@ def get_parser(): def main(): """Main entry point.""" - logging.basicConfig(level=logging.DEBUG) parser = get_parser() args = parser.parse_args() args.func(args) +########################################################################### +# Behold, below this line sleep the demons that are known as curses TUIs. # +# Tread carefully. # +########################################################################### + + +class TuiListView: + """A list-view like ncurses window.""" + + PAIR_HL = 1 + + def __init__(self, scr, items): + self.scr = scr + self.items = items + self.selection = 0 + self.top = 0 + self.is_active = False + + def set_items(self, items): + self.items = items + self.selection = 0 + self.top = 0 + + def set_window(self, window): + self.scr = window + + def activate(self): + self.is_active = True + + def deactivate(self): + self.is_active = False + + def render(self): + import curses + + self.scr.border() + height, width = self.scr.getmaxyx() + for row, item in enumerate(self.items[self.top : self.top + height - 2]): + attr = 0 + if self.is_active and row + self.top == self.selection: + attr = curses.color_pair(self.PAIR_HL) + str_item = f"{item!s:{width - 2}}" + self.scr.addnstr(row + 1, 1, str_item, width - 2, attr) + self.scr.refresh() + + def up(self): + if self.selection == 0: + return + self.selection -= 1 + if self.selection < self.top: + self.top -= 1 + + def down(self): + if self.selection == len(self.items) - 1: + return + self.selection += 1 + height, _ = self.scr.getmaxyx() + if self.selection >= self.top + height - 2: + self.top += 1 + + def selected_item(self): + return self.items[self.selection] + + +_TUI_ERROR_HL = 2 + + +def _tui_error(screen, msg): + import curses + + height, width = screen.getmaxyx() + wanted_width = width - 4 + wanted_height = 10 + window = screen.derwin( + wanted_height, + wanted_width, + int((height - wanted_height) / 2), + int((width - wanted_width) / 2), + ) + window.attron(curses.color_pair(_TUI_ERROR_HL)) + window.bkgd(" ", curses.color_pair(_TUI_ERROR_HL)) + window.border() + + title = "ERROR" + window.addstr(0, int((wanted_width - len(title)) / 2), title) + window.addstr(1, 1, msg) + window.addstr(wanted_height - 2, 1, "Press <Enter> to exit.") + + while True: + window.refresh() + c = screen.getch() + print(c) + if c in {10, curses.KEY_ENTER}: + break + + +def _tui_select_profile(screen, profiles): + import curses + + _tui_title(screen, "Select your Steam profile") + screen.addstr(2, 0, "<Up> and <Down> to select") + screen.addstr(3, 0, "<Enter> to confirm") + screen.addstr(4, 0, "<q> to quit") + + height, width = screen.getmaxyx() + + names = [profile.user_id for profile in profiles] + view = TuiListView(screen.derwin(height - 5, width, 5, 0), names) + view.activate() + view.render() + + while True: + c = screen.getch() + + if c in {ord("Q"), ord("q")}: + break + elif c == curses.KEY_UP: + view.up() + elif c == curses.KEY_DOWN: + view.down() + elif c in {10, curses.KEY_ENTER}: + return profiles[view.selection] + + +def _tui(args, screen, profile): + import curses + + def init_layout(): + _tui_title(screen, "Copy savegames") + height, width = screen.getmaxyx() + + screen.addstr(2, 0, "<Up> and <Down> to select") + screen.addstr(3, 0, "<Tab> to switch panel") + screen.addstr(4, 0, "<Enter> to copy") + screen.addstr(5, 0, "<r> to refresh") + screen.addstr(6, 0, "<q> to quit") + + screen.addstr(9, 0, "Local Savegames", curses.A_BOLD) + screen.addstr(9, int(width / 2), "Cloud Savegames", curses.A_BOLD) + + wins = [ + screen.derwin(height - 10, int(width / 2), 10, 0), + screen.derwin(height - 10, int(width / 2), 10, int(width / 2)), + ] + return wins + + windows = init_layout() + + views = [ + TuiListView(windows[0], []), + TuiListView(windows[1], []), + ] + active = 0 + views[active].activate() + + def reload(): + local_saves = profile.saves() + remote_saves = read_saves(args.remote_dir) + views[0].set_items(local_saves) + views[1].set_items(remote_saves) + + reload() + + while True: + for view in views: + view.render() + + c = screen.getch() + + if c in {ord("Q"), ord("q")}: + break + + elif c == curses.KEY_UP: + views[active].up() + + elif c == curses.KEY_DOWN: + views[active].down() + + elif c == ord("\t"): + views[active].deactivate() + active = 1 - active + views[active].activate() + + elif c in {ord("R"), ord("r")}: + reload() + + elif c in {10, curses.KEY_ENTER}: + try: + item = views[active].selected_item() + except IndexError: + pass + else: + if active == 0: + dest = args.remote_dir + else: + dest = profile.save_dir + item.copy_to(dest) + reload() + screen.addstr(8, 0, f"{item.name} copied!") + + elif c == curses.KEY_RESIZE: + screen.clear() + windows = init_layout() + for view, window in zip(views, windows): + view.set_window(window) + + +def _tui_title(screen, title): + import curses + + _, width = screen.getmaxyx() + title = f"{title!s:{width}}" + screen.addnstr(0, 0, title, width, curses.A_REVERSE) + + +def _curses_tui(args, screen): + import curses + + curses.use_default_colors() + curses.init_pair(TuiListView.PAIR_HL, curses.COLOR_BLUE, curses.COLOR_WHITE) + curses.init_pair(_TUI_ERROR_HL, curses.COLOR_WHITE, curses.COLOR_RED) + curses.curs_set(0) + + # First, we gotta find the right profile + profiles = get_steam_profiles() + if args.profile_id is not None: + profile = find(profiles, user_id=args.profile_id) + if profile is None: + _tui_error(screen, f"User profile with ID {args.profile_id} was not found.") + return + elif not profiles: + _tui_error(screen, f"No user profiles found.") + return + elif len(profiles) == 1: + profile = profiles[0] + else: + profile = _tui_select_profile(screen, profiles) + + screen.clear() + + _tui(args, screen, profile) + + if __name__ == "__main__": try: main() |