diff --git a/data.py b/data.py index 555c4d1..b9962bb 100644 --- a/data.py +++ b/data.py @@ -141,7 +141,7 @@ class LinkData: def add(self, record) -> int: """Add a record to the data file, and to link_data. Returns a new post - ID, if record is a post, or -1""" + ID, if record is a post, or None""" if os.path.exists(config.USER.datafile): append_write = "a" # append if already exists else: @@ -157,7 +157,7 @@ class LinkData: ) ) - new_post_id = -1 + new_post_id = None if record.category: if self.link_data: new_post_id = ( diff --git a/linkulator.py b/linkulator.py index ce3efad..3af53b5 100755 --- a/linkulator.py +++ b/linkulator.py @@ -225,34 +225,62 @@ def open_link_in_browser(url): call([config.USER.browser, url]) -def new_reply(stdscr, post_id): - # TODO - pass +def command_bar(): + """Uses defined stdscr to display a command input prompt. returns command text""" + + # draw the prompt in its own window + prompt_win = curses.newwin(1, curses.COLS, curses.LINES - 1, 0) + prompt_win.addch(0, 0, ":") + prompt_win.clrtoeol() + prompt_win.refresh() + + # draw the command field and return gathered text + command_win = curses.newwin(1, curses.COLS, curses.LINES - 1, 1) + command_field = textpad.Textbox(command_win) + command_field.stripspaces = True + curses.curs_set(1) + command_field.edit() + command_text = command_field.gather() + command_text = command_text.strip() + curses.curs_set(0) + return command_text -# def reply(parent_id): -# """Prompt for reply, validate input, save validated input to disk and update -# link_data. Calls view_thread when complete.""" -# while True: -# comment = input("Enter your comment (or leave empty to abort): ") -# if comment == "": -# input("Reply aborted. Hit [Enter] to continue.") -# break -# if not is_valid_input(comment): -# print( -# "Entries consisting of whitespace, or containing pipes, '|', are " -# "not valid.Please try again." -# ) -# else: -# record = data.LinkDataRecord( -# username=getpass.getuser(), -# timestamp=str(time()), -# parent_id=parent_id, -# link_title_or_comment=comment, -# ) -# LinkData.add(record) -# input("Reply added. Hit [Enter] to return to thread.") -# break +def new_reply(): + # draw the prompt in its own window + reply_prompt = curses.newwin(1, curses.COLS, curses.LINES - 5, 0) + reply_prompt.addstr( + 0, 0, "Enter your comment (or leave empty to cancel). CTRL+G to submit." + ) + reply_prompt.clrtoeol() + reply_prompt.refresh() + + # loop until valid input can be gathered + + while True: + reply_window = curses.newwin(4, curses.COLS, curses.LINES - 4, 0) + reply_field = textpad.Textbox(reply_window) + reply_field.stripspaces = True + curses.curs_set(1) + reply_field.edit(complete_on_enter) + reply_text = reply_field.gather() + reply_text = reply_text.strip() + curses.curs_set(0) + if is_valid_input(reply_text): + break + else: + reply_prompt.addstr(0, 0, "Sorry, replies can't contain pipes '|'") + reply_prompt.clrtoeol() + reply_prompt.refresh() + return reply_text + + +def complete_on_enter(char): + # https://stackoverflow.com/questions/36121802/python-curses-make-enter-key-terminate-textbox + # # if enter, return CTRL+G equivalent + if char == 10: + char = 7 + return char def is_valid_input(entry: str) -> bool: @@ -267,11 +295,58 @@ def is_valid_input(entry: str) -> bool: """ if "|" in entry: return False - if entry.strip() == "": - return False + # if entry.strip() == "": + # return False return True +def new_post(): + # draw the prompt in its own window + new_post_title = curses.newwin(8, curses.COLS, 4, 0) + new_post_title.addstr( + 0, 0, "Enter the requested details, to cancel submit a blank field" + ) + new_post_title.clrtoeol() + new_post_title.addstr(1, 0, "URL:") + new_post_title.clrtoeol() + new_post_title.addstr(2, 0, "Title:") + new_post_title.clrtoeol() + new_post_title.addstr(3, 0, "Category:") + new_post_title.clrtobot() + new_post_title.refresh() + + # loop until valid input can be gathered + + post_data = {} + items = ["url", "title", "category"] + + for i, item in enumerate(items): + startrow = i + 5 + while True: + item_window = curses.newwin(1, curses.COLS, startrow, 10) + item_field = textpad.Textbox(item_window) + item_field.stripspaces = True + curses.curs_set(1) + item_field.edit() + item_text = item_field.gather() + item_text = item_text.strip() + curses.curs_set(0) + + if not item_text: + return None + if is_valid_input(item_text): + break + else: + new_post_title.addstr( + 0, 0, "Sorry, {} can't contain pipes '|'".format(item) + ) + new_post_title.clrtoeol() + new_post_title.refresh() + post_data[item] = item_text + + return post_data + + def is_correct_category(entry: str) -> bool: """Make sure the user purposefully created a new category and not by accident (mistyped, tried to use category number instead of name)""" @@ -435,7 +510,9 @@ class MenuHierarchy: ) = view_categories(self.active.data, curses.COLS) elif self.active.name == "links": + # reinitialise lower-level menu self.primary_hierarchy[self.primary_index + 1].data = None + # set data self.active.data = LinkData.get_links_by_category_name( self.active.selected_key ) @@ -451,7 +528,8 @@ class MenuHierarchy: elif self.active.name == "search_results": pass elif self.active.name == "search_results_thread_details": - pass + self.active.data = LinkData.get_post(self.active.selected_key) + self.active.output.content = view_post(self.active.data, curses.COLS) else: raise ValueError("This shouldn't happen?") @@ -490,6 +568,7 @@ def menu_system(stdscr): status.action = "" while True: + curses.set_escdelay(25) curses.update_lines_cols() # TODO: consider stdscr.erase() @@ -553,23 +632,34 @@ def menu_system(stdscr): if k == "KEY_RESIZE": term_resize(stdscr) elif k in [":", " "]: - stdscr.addch(curses.LINES - 1, 0, ":") - stdscr.clrtoeol() - stdscr.refresh() - iwin = curses.newwin(1, curses.COLS, curses.LINES - 1, 1) - itxt = textpad.Textbox(iwin) - itxt.stripspaces = True - curses.curs_set(1) - itxt.edit() - action = itxt.gather() + action = command_bar() handle_action(menus, action, status) - curses.curs_set(0) - del itxt - del iwin else: + stdscr.erase() + stdscr.refresh() handle_action(menus, k, status) +# menu loop +# action = get action from input +# return valid action or error message +# actions: +# navigate menu +# get data for menu change +# back/forward +# screen change +# set screen data +# scroll up/down +# get new post or reply input +# send data for input +# search +# perform search +# return search results +# open external program +# shutdown curses, start program, restart curses on return +# show help screen + + def handle_action(menus, action, status): int_action = None try: @@ -579,15 +669,9 @@ def handle_action(menus, action, status): pass if int_action: - try: - navigate(menus, int_action) - except Exception as e: - status.message = str(e) + navigate(menus, int_action) else: - try: - do_command(menus, action) - except Exception as e: - status.message = str(e) + do_command(menus, action) def navigate(menus, int_action): @@ -629,9 +713,9 @@ def do_command(menus, action): elif action in ["k", "KEY_UP"]: if menus.active.output.scrollminy > 0: menus.active.output.scrollminy -= 1 - elif action in ["b", "back"]: + elif action in ["b", "back", "KEY_LEFT"]: menus.back() - elif action in ["f", "forward"]: + elif action in ["f", "forward", "KEY_RIGHT"]: menus.forward() elif action in ["?", "help"]: pass @@ -639,13 +723,39 @@ def do_command(menus, action): elif action in ["q", "quit", "exit"]: graceful_exit() elif action in ["c", "create"]: - # post_id = post_link() - # if post_id >= 0: - # TODO: create new post - # set category_details to the relevant category - # set the selected index to the index of the new post in category_details - # set menu level to thread index - pass + new_post_data = new_post() + if new_post_data: + new_post_record = data.LinkDataRecord( + username=getpass.getuser(), + timestamp=str(time()), + category=new_post_data["category"], + link_URL=new_post_data["url"], + link_title_or_comment=new_post_data["title"], + ) + + new_post_id = LinkData.add(new_post_record) + + if new_post_id: + # set categories screen + menus.primary_index = 0 + menus.active = menus.primary_hierarchy[menus.primary_index] + menus.set_active_menu_data(LinkData) + menus.active.output.calc_dimensions() + + # set links screen + menus.primary_index = 1 + menus.active = menus.primary_hierarchy[menus.primary_index] + menus.active.selected_key = new_post_data["category"] + menus.set_active_menu_data(LinkData) + menus.active.output.calc_dimensions() + + # set post screen + menus.primary_index = 2 + menus.active = menus.primary_hierarchy[menus.primary_index] + menus.active.selected_key = new_post_id + menus.set_active_menu_data(LinkData) + menus.active.output.calc_dimensions() + elif action in ["s", "search"]: pass # search() @@ -655,10 +765,20 @@ def do_command(menus, action): elif action in ["r", "reply"] and menus.active.name in [ "search_result_thread_details", - "thread_details", + "post", ]: - # TODO: need stdscr here - new_reply(menus.active.selected_key) + reply_text = new_reply() + if reply_text: + reply_record = data.LinkDataRecord( + username=getpass.getuser(), + timestamp=str(time()), + parent_id=menus.active.data["parent_id"], + link_title_or_comment=reply_text, + ) + _ = LinkData.add(reply_record) + menus.set_active_menu_data(LinkData) + menus.active.output.calc_dimensions() + elif action in ["o", "open"] and menus.active.name in [ "search_result_thread_details", "thread_details", @@ -669,7 +789,7 @@ def do_command(menus, action): # is o the right command? -## GENERAL +# GENERAL def style_text(text: str, is_input: bool, *args) -> str: