Add help page and status messages

This commit is contained in:
asdf 2021-08-22 20:19:05 +10:00
parent d34aecb24d
commit 9ac11fa06e
1 changed files with 356 additions and 316 deletions

View File

@ -23,8 +23,12 @@ import data
# linkdata columns:
# id (if parent), username, datestamp, parent-id, category, link-url, link-title
LinkData = data.LinkData()
link_data: list = LinkData.link_data
# LinkData = data.LinkData()
# link_data: list = LinkData.link_data
# get locale and set CODE for use in string encoding
locale.setlocale(locale.LC_ALL, "")
CODE = locale.getpreferredencoding()
# VIEWS
@ -32,11 +36,11 @@ link_data: list = LinkData.link_data
def view_categories(categories, cols) -> Tuple[str, list[str]]:
"""Produces categories screen display data. Returns as tuple of header and
content."""
body content."""
thead = "{:>4s} New {}".format("ID#", "Category")
head = "{:>4s} New {}".format("ID#", "Category")
content: list[str] = []
body: list[str] = []
newmarker = "*"
name_cols = max((cols - 11), 5)
@ -44,7 +48,7 @@ def view_categories(categories, cols) -> Tuple[str, list[str]]:
index_number = i + 1
name = textwrap.shorten(record["name"], width=name_cols, placeholder="...")
newmarker = "*" if record["last_updated"] >= config.USER.lastlogin else " "
content.append(
body.append(
"{:4d} {} {} ({})".format(
index_number,
newmarker,
@ -53,15 +57,19 @@ def view_categories(categories, cols) -> Tuple[str, list[str]]:
)
)
if not content:
thead = ""
content = "\nThere are no posts yet - enter p to post a new link\n".splitlines()
return thead, content
if not body:
head = ""
body = [
"",
"There are no posts yet - press p to post a new link",
"",
]
return head, body
def view_links(links, category_name, cols) -> Tuple[str, list[str]]:
def view_links(links, category_name, cols) -> Tuple[str, str, list[str]]:
"""Produces links screen display data. Accepts links list, category name and
max column width. Returns a tuple of header and content."""
max column width. Returns a tuple of subtitle, header and body content."""
max_author_cols = max([len(link["link_author"]) for link in links])
author_cols = max(max_author_cols, 6)
@ -71,8 +79,9 @@ def view_links(links, category_name, cols) -> Tuple[str, list[str]]:
# and Date fields and padding, minus the max name length, minus Resp field
# and padding width, minus the unread mark width
thead = " {}\n {:>3s} {:>10s} {:<{author_cols}s} {:<5} {:<s}".format(
category_name.title(),
subtitle = category_name.title()
head = "{:>3s} {:>10s} {:<{author_cols}s} {:<5} {:<s}".format(
"ID#",
"Date",
"Author",
@ -81,7 +90,7 @@ def view_links(links, category_name, cols) -> Tuple[str, list[str]]:
author_cols=author_cols,
)
content: list[str] = []
body: list[str] = []
for i, link in enumerate(links):
description = textwrap.shorten(
@ -92,8 +101,8 @@ def view_links(links, category_name, cols) -> Tuple[str, list[str]]:
"%Y-%m-%d"
)
content.append(
" {:3d} {:>10s} {:<{author_cols}s} [{:3d}] {:s}{}".format(
body.append(
"{:3d} {:>10s} {:<{author_cols}s} [{:3d}] {:s}{}".format(
i + 1,
date,
link["link_author"],
@ -104,14 +113,14 @@ def view_links(links, category_name, cols) -> Tuple[str, list[str]]:
)
)
return thead, content
return subtitle, head, body
def view_post(post, cols) -> list[str]:
"""Produces post screen display data. Accepts post id and max column
width. Returns content list."""
width. Returns body content list."""
output: list = []
body: list = []
# set up line wrapping
line_wrapper = textwrap.TextWrapper(
@ -119,30 +128,30 @@ def view_post(post, cols) -> list[str]:
)
# post detail view
output.append(" {:<12}: {}".format("Title", post["title"]))
output.append(" {:<12}: {}".format("Link", post["url"]))
output.append(" {:<12}: {}".format("Category", post["category"]))
output.append(" {:<12}: {}".format("User", post["author"]))
body.append(" {:<12}: {}".format("Title", post["title"]))
body.append(" {:<12}: {}".format("Link", post["url"]))
body.append(" {:<12}: {}".format("Category", post["category"]))
body.append(" {:<12}: {}".format("User", post["author"]))
date = datetime.fromtimestamp(float(post["timestamp"])).strftime("%c")
output.append(" {:<12}: {}".format("Date", date))
body.append(" {:<12}: {}".format("Date", date))
# post reply view
if post["replies"]:
output.append("\n {}:\n".format("Replies"))
body.append("\n {}:\n".format("Replies"))
for replies in post["replies"]:
reply_author = replies[1]
reply_date = datetime.fromtimestamp(float(replies[2])).isoformat(
sep=" ", timespec="minutes"
)
reply_text = replies[6]
output.append(
body.append(
line_wrapper.fill(
" {} {}: {}".format(reply_date, reply_author, reply_text)
)
)
else:
output.append("\n No replies yet. Be the first!")
return output
body.append("\n No replies yet. Be the first!")
return body
def view_search_results(keyword: str, search_results: list):
@ -246,7 +255,7 @@ def command_bar():
return command_text
def new_reply():
def get_reply():
# draw the prompt in its own window
reply_prompt = curses.newwin(1, curses.COLS, curses.LINES - 5, 0)
reply_prompt.addnstr(
@ -282,7 +291,7 @@ def new_reply():
def complete_on_enter(char):
# https://stackoverflow.com/questions/36121802/python-curses-make-enter-key-terminate-textbox
# # if enter, return CTRL+G equivalent
# if enter, return CTRL+G equivalent
if char == 10:
char = 7
return char
@ -305,7 +314,8 @@ def is_valid_input(entry: str) -> bool:
return True
def new_post():
def get_new_post(categories):
"""input form for new post. returns new post data or None"""
# draw the prompt in its own window
new_post_title = curses.newwin(8, curses.COLS, 4, 0)
new_post_title.addnstr(
@ -339,6 +349,9 @@ def new_post():
if not item_text:
return None
if item == "category":
# TODO: some sort of category picker, who knows
pass
if is_valid_input(item_text):
break
else:
@ -352,99 +365,42 @@ def new_post():
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)"""
if entry not in [record["name"] for record in LinkData.categories]:
question = "Do you want to create a new category '{}'? Y/[N]".format(entry)
answer = input(question).lower().strip()
return answer != "" and answer[0] == "y"
return True
def get_input(item: str) -> str:
"""Get user input with the specified prompt, validate and return it, or
break if invalid"""
while True:
i: str = input("{}: ".format(style_text(item, True, "underline")))
if i == "":
raise ValueError("Empty field")
if i == "?":
category_text = (
"No categories yet, feel free to make a new one!"
if not LinkData.categories
else ", ".join(sorted(record["name"] for record in LinkData.categories))
)
print(
"Available categories: {}\n"
"♻️ Please help keep Linkulator tidy".format(category_text)
)
continue
if is_valid_input(i):
return i
print(
"Entries consisting of whitespace, or containing pipes, '|', are "
"not valid.Please try again."
)
def post_link() -> int:
"""Handles the link posting process"""
print("\nPost a link by entering the details below.")
print(
"Enter {} for a list of categories. Enter an empty field to cancel.\n".format(
style_text("?", False, "underline")
)
)
try:
url = get_input("URL")
category = get_input("Category")
while not is_correct_category(category):
category = get_input("Category")
title = get_input("Title")
except ValueError:
print("Post cancelled")
return -1
record = data.LinkDataRecord(
username=getpass.getuser(),
timestamp=str(time()),
category=category,
link_URL=url,
link_title_or_comment=title,
)
return LinkData.add(record)
class Output:
class Content:
"""Menu content that is output to the curses screen, plus a method to
calculate how it is displayed"""
def __init__(self):
self.thead = None
self.content = None
self.subtitle = None
self.head = None
self.body = None
self.length = None
self.miny = None
self.maxy = None
self.scrollminy = 0
self.viewslice = None
def reset(self):
def reset_dimensions(self):
self.scrollminy = 0
def calc_dimensions(self):
if not self.content:
raise ValueError("Can't calculate nonexistant data")
self.length = len(self.content)
if self.thead:
if not self.body:
raise ValueError("Cannot calculate - no content set")
self.length = len(self.body)
if self.head and self.subtitle:
self.miny = 3
else:
self.miny = 2
self.maxy = curses.LINES - 3
self.viewslice = slice(self.scrollminy, self.scrollminy + self.maxy)
def scroll_down(self):
if self.scrollminy <= (self.length - self.maxy):
self.scrollminy += 1
def scroll_up(self):
if self.scrollminy > 0:
self.scrollminy -= 1
class Menu:
"""Class describing the data contained in each menu level. A level is like a
@ -453,7 +409,7 @@ class Menu:
def __init__(self, name):
self.name = name
self.data = None
self.output = Output()
self.content = Content()
self.selected_key = None
@ -473,11 +429,11 @@ class MenuHierarchy:
found_post = Menu("found_post")
self.search_hierarchy = [search_results, found_post]
# help page
# help page - single page
self.user_help_page = Menu("user_help_page")
# hierarchy settings
self.is_viewing_primary_hierarchy = True
self.primary_index = 0
self.search_index = 0
self.active = self.primary_hierarchy[self.primary_index]
@ -509,14 +465,15 @@ class MenuHierarchy:
self.search_index += 1
self.active = self.search_hierarchy[self.search_index]
def set_active_menu_data(self, LinkData):
def get_active_menu_data(self, LinkData):
"""get active menu data based on menu hierarchy settings"""
if self.active.name == "categories":
self.active.data = LinkData.categories
(
self.active.output.thead,
self.active.output.content,
self.active.content.head,
self.active.content.body,
) = view_categories(self.active.data, curses.COLS)
self.active.content.subtitle = "All Categories"
elif self.active.name == "links":
# reinitialise lower-level menu
@ -526,31 +483,143 @@ class MenuHierarchy:
self.active.selected_key
)
(
self.active.output.thead,
self.active.output.content,
self.active.content.subtitle,
self.active.content.head,
self.active.content.body,
) = view_links(self.active.data, self.active.selected_key, curses.COLS)
elif self.active.name == "post":
self.active.data = LinkData.get_post(self.active.selected_key)
self.active.output.content = view_post(self.active.data, curses.COLS)
self.active.content.subtitle = "Viewing post:"
self.active.content.body = view_post(self.active.data, curses.COLS)
elif self.active.name == "search_results":
self.is_viewing_primary_hierarchy = False
pass
elif self.active.name == "search_results_thread_details":
self.active.data = LinkData.get_post(self.active.selected_key)
self.active.output.content = view_post(self.active.data, curses.COLS)
self.active.content.body = view_post(self.active.data, curses.COLS)
elif self.active.name == "user_help_page":
self.is_viewing_primary_hierarchy = False
self.active.output.content = user_help_text(curses.COLS)
self.active.content.subtitle = "Quick Reference Guide"
self.active.content.body = user_help_text(curses.COLS)
else:
raise ValueError("This shouldn't happen?")
def navigate(self, int_action, linkdata, screen):
"""Navigate to an entry by index"""
if int_action:
if self.active.name == "categories":
try:
key = self.active.data[int_action - 1]["name"]
except IndexError:
screen.status = "Sorry, that category doesn't exist"
return
elif self.active.name == "links":
try:
key = self.active.data[int_action - 1]["post_id"]
except IndexError:
screen.status = "Sorry, that link doesn't exist"
return
elif self.active.name == "search_results":
# TODO
pass
else:
# Not a navigable screen
return
self.primary_index += 1
self.active = self.primary_hierarchy[self.primary_index]
self.active.selected_key = key
self.get_active_menu_data(linkdata)
self.active.content.reset_dimensions()
self.active.content.calc_dimensions()
class Status:
def go_to_post(self, post_category, post_id, linkdata):
"""Navigates directly to the specified post while also reloading
category and links pages"""
# set categories screen
self.primary_index = 0
self.active = self.primary_hierarchy[self.primary_index]
self.get_active_menu_data(linkdata)
self.active.content.calc_dimensions()
# set links screen
self.primary_index = 1
self.active = self.primary_hierarchy[self.primary_index]
self.active.selected_key = post_category
self.get_active_menu_data(linkdata)
self.active.content.calc_dimensions()
# set post screen
self.primary_index = 2
self.active = self.primary_hierarchy[self.primary_index]
self.active.selected_key = post_id
self.get_active_menu_data(linkdata)
self.active.content.calc_dimensions()
def reload_current(self, linkdata):
"""Reloads data for primary heirarchy"""
for i in range(len(self.primary_hierarchy)):
self.primary_index = i
self.active = self.primary_hierarchy[self.primary_index]
self.get_active_menu_data(linkdata)
self.active.content.calc_dimensions()
class Screen:
def __init__(self):
self.message: str
self.action: str
self.title: str = "Linkulator"
self.content = None
self.status: str = "Welcome! Press ? for keys and commands"
def set_content(self, content):
self.content = content
def print(self, stdscr):
# print title
stdscr.addnstr(0, 0, self.title, curses.COLS)
stdscr.clrtoeol()
# print subtitle
if self.content.subtitle:
stdscr.addnstr(1, 0, self.content.subtitle.encode(CODE), curses.COLS)
stdscr.clrtoeol()
# print content header
if self.content.head:
stdscr.addnstr(
self.content.miny - 1,
0,
self.content.head.encode(CODE),
curses.COLS,
)
stdscr.clrtoeol()
# print body
self.content.calc_dimensions()
count = 0
for i, line in enumerate(self.content.body[self.content.viewslice]):
# don't print beyond the screen
if i >= curses.LINES:
break
count += count
stdscr.addnstr(i + self.content.miny, 0, line.encode(CODE), curses.COLS)
stdscr.clrtoeol()
# this is meant to clear to the second last line, but...
# just erase to the bottom for now
stdscr.clrtobot()
# print status (footer)
if self.status:
stdscr.addnstr((curses.LINES - 1), 0, self.status.encode(CODE), curses.COLS)
stdscr.clrtoeol()
else:
stdscr.move(curses.LINES - 1, 0)
stdscr.clrtoeol()
# clear message for next loop
self.status = ""
def term_resize(stdscr):
@ -563,85 +632,31 @@ def term_resize(stdscr):
curses.update_lines_cols()
def menu_system(stdscr):
def control_loop(stdscr):
"""main loop of program, prints data and takes action based on user input"""
locale.setlocale(locale.LC_ALL, "")
code = locale.getpreferredencoding()
curses.use_default_colors()
curses.curs_set(0)
curses.set_escdelay(25)
title = "Linkulator".encode(code)
linkdata = data.LinkData()
menus = MenuHierarchy()
menus.set_active_menu_data(LinkData)
menus.active.output.calc_dimensions()
menus.get_active_menu_data(linkdata)
menus.active.content.calc_dimensions()
status = Status()
status.message = ""
status.action = ""
screen = Screen()
while True:
curses.set_escdelay(25)
curses.update_lines_cols()
# TODO: consider stdscr.erase()
# print title
stdscr.addnstr(0, 1, title, curses.COLS)
stdscr.clrtoeol()
# TODO: print subtitle?
# stdscr.addnstr(1, 4, "All categories".encode(code), curses.COLS)
# print header
if menus.active.output.thead:
stdscr.addnstr(
menus.active.output.miny - 1,
0,
menus.active.output.thead.encode(code),
curses.COLS,
)
stdscr.clrtoeol()
# print body
menus.active.output.calc_dimensions()
count = 0
for i, line in enumerate(
menus.active.output.content[menus.active.output.viewslice]
):
# guard just in case we try to print beyond the window because of
# some resize stuff
if i >= curses.LINES:
break
count += count
stdscr.addnstr(
i + menus.active.output.miny, 0, line.encode(code), curses.COLS
)
stdscr.clrtoeol()
# this is meant to clear to the second last line, but...
# just erase to the bottom for now
stdscr.clrtobot()
# print status
if status.message:
status_text = "{} - {}".format(status.message, status.action)
stdscr.addnstr((curses.LINES - 1), 0, status_text.encode(code), curses.COLS)
stdscr.clrtoeol()
else:
stdscr.move(curses.LINES - 1, 0)
stdscr.clrtoeol()
# clear message for next loop
status.message = ""
# refresh screen output with all changes made
screen.set_content(menus.active.content)
screen.print(stdscr)
stdscr.refresh()
try:
k = stdscr.getkey()
except (curses.error):
status.message = str(curses.error)
status.action = str(k)
screen.status = "Error {} on action {}".format(str(curses.error), str(k))
k = None
continue
@ -649,34 +664,14 @@ def menu_system(stdscr):
term_resize(stdscr)
elif k in [":", " "]:
action = command_bar()
handle_action(menus, action, status)
handle_command(action, screen, menus, linkdata)
else:
stdscr.erase()
stdscr.refresh()
handle_action(menus, k, status)
handle_hotkey(k, screen, menus, linkdata)
# 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):
def handle_command(action, screen, menus, linkdata):
int_action = None
try:
int_action = int(action)
@ -685,63 +680,22 @@ def handle_action(menus, action, status):
pass
if int_action:
navigate(menus, int_action)
menus.navigate(int_action, linkdata, screen)
else:
do_command(menus, action)
do_command(action, screen, menus, linkdata)
def navigate(menus, int_action):
if int_action:
if menus.active.name == "categories":
try:
key = menus.active.data[int_action - 1]["name"]
except IndexError:
raise IndexError("Sorry, that category doesn't exist")
return
elif menus.active.name == "links":
try:
key = menus.active.data[int_action - 1]["post_id"]
except IndexError:
raise IndexError("Sorry, that link doesn't exist")
return
elif menus.active.name == "search_results":
# TODO
pass
else:
# no action because it's not a valid menu
return
menus.primary_index += 1
menus.active = menus.primary_hierarchy[menus.primary_index]
menus.active.selected_key = key
menus.set_active_menu_data(LinkData)
menus.active.output.reset()
menus.active.output.calc_dimensions()
def do_command(menus, action):
"""???"""
if action in ["j", "KEY_DOWN"]:
# TODO: put up/down controls in output class?
if menus.active.output.scrollminy <= (
menus.active.output.length - menus.active.output.maxy
):
menus.active.output.scrollminy += 1
elif action in ["k", "KEY_UP"]:
if menus.active.output.scrollminy > 0:
menus.active.output.scrollminy -= 1
elif action in ["b", "back", "KEY_LEFT"]:
menus.back()
elif action in ["f", "forward", "KEY_RIGHT"]:
menus.forward()
elif action in ["?", "help"]:
def do_command(action, screen, menus, linkdata):
"""Executes the specified action, updating screen status if required"""
if action in ["?", "help"]:
menus.active = menus.user_help_page
menus.set_active_menu_data(None)
menus.active.output.reset()
menus.active.output.calc_dimensions()
menus.get_active_menu_data(None)
menus.active.content.reset_dimensions()
menus.active.content.calc_dimensions()
elif action in ["q", "quit", "exit", "\x04"]:
graceful_exit()
elif action in ["c", "create"]:
new_post_data = new_post()
new_post_data = get_new_post(linkdata.categories)
if new_post_data:
new_post_record = data.LinkDataRecord(
username=getpass.getuser(),
@ -751,57 +705,142 @@ def do_command(menus, action):
link_title_or_comment=new_post_data["title"],
)
new_post_id = LinkData.add(new_post_record)
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()
menus.go_to_post(new_post_data["category"], new_post_id, linkdata)
else:
screen.status = "Error: failed to create post" # this is unlikely
else:
screen.status = "Create post cancelled"
elif action in ["s", "search"]:
pass
screen.status = "Search not yet implemented"
# search()
# TODO: open search screen
# level.prior_to_search = level.current
# results are returned to a search list, displayed by setting menu level to search
elif action in ["r", "reply"] and menus.active.name in [
"search_result_thread_details",
"post",
]:
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 ["r", "reply"]:
if menus.active.name in [
"search_result_thread_details",
"post",
]:
reply_text = get_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.reload_current(linkdata)
else:
screen.status = "Reply cancelled"
else:
screen.status = "View a post to make a reply"
elif action in ["o", "open"] and menus.active.name in [
"search_result_thread_details",
"thread_details",
]:
screen.status = "Open in browser not yet implemented"
# open_link_in_browser(thread_details["url"])
# open link in external program
# is o the right command?
elif action == "":
return
else:
screen.status = "Invalid command. Press ? for help"
def handle_hotkey(action, screen, menus, linkdata):
int_action = None
try:
int_action = int(action)
except ValueError:
# it's not a number but that's ok
pass
if int_action:
menus.navigate(int_action, linkdata, screen)
else:
do_hotkey(action, screen, menus, linkdata)
def do_hotkey(action, screen, menus, linkdata):
"""Executes the provided hotkey action"""
if action in ["j", "KEY_DOWN"]:
screen.content.scroll_down()
elif action in ["k", "KEY_UP"]:
screen.content.scroll_up()
elif action in ["b", "KEY_LEFT"]:
menus.back()
elif action in ["f", "KEY_RIGHT"]:
menus.forward()
elif action == "?":
menus.active = menus.user_help_page
menus.get_active_menu_data(None)
menus.active.content.reset_dimensions()
menus.active.content.calc_dimensions()
elif action in ["q", "\x04"]:
# \x04 is CTRL+D
graceful_exit()
elif action == "c":
new_post_data = get_new_post(linkdata.categories)
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:
menus.go_to_post(new_post_data["category"], new_post_id, linkdata)
else:
screen.status = "Error: failed to create post" # this is unlikely
else:
screen.status = "Create post cancelled"
elif action == "/":
screen.status = "Search not yet implemented"
# search()
# TODO: open search screen
# level.prior_to_search = level.current
# results are returned to a search list, displayed by setting menu level to search
elif action == "r":
if menus.active.name in [
"search_result_thread_details",
"post",
]:
reply_text = get_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.reload_current(linkdata)
else:
screen.status = "Reply cancelled"
else:
screen.status = "View a post to make a reply"
elif action == "o" and menus.active.name in [
"search_result_thread_details",
"thread_details",
]:
screen.status = "Open in browser not yet implemented"
# open_link_in_browser(thread_details["url"])
# open link in external program
# is o the right command?
@ -853,7 +892,7 @@ def main(stdscr):
args = sys.argv[1:]
config.init()
if not args:
menu_system(stdscr)
control_loop(stdscr)
elif args[0] in ["-h", "--help", "help"]:
curses.endwin()
print(help_text())
@ -890,31 +929,32 @@ A few important points about Linkulator:
def user_help_text(cols):
user_help_text = """Welcome to Linkulator!
Press Q to quit
Press space or : to enter a command
user_help_text = """- press q to quit -
- press space or : to enter a command -
- scroll the page with j/k or up and down arrows -
keys
j/k or up/down arrow to scroll a page
b and f to go back and forward
c to create a new post
r to reply to a post
o to open a link
-keys-
Number keys - navigate to the numbered item
space or : - open the command prompt
j or down arrow - scroll down the page
k or up arrow - scroll up the page
b or left arrow - go back to the previous page
f or right arrow - go forward to a previously visited page
c - create a new post
r - reply to a post
o - open a link
/ - search all links using a keyword
q - quit the application
commands
q, quit, exit: exit the application
long page
long page
here's a really really long line as the final paragraph that is likely to exceed the terminal width
-commands-
q, quit, exit - quit the application
r, reply - reply to the post on screen
o, open - open the link from the post on screen in your browser
s, search - search all links using a keyword
b, back - go back to the previous page
f, forward - go forward to a previously visited page
c, create - create a new post
?, help - show this help page
"""
return user_help_text.splitlines()