Add help page and status messages
This commit is contained in:
parent
d34aecb24d
commit
9ac11fa06e
672
linkulator.py
672
linkulator.py
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue