initial work on pager - major changes to menu functionality

This commit is contained in:
asdf 2021-08-03 18:25:52 +10:00
parent ecbf2b66d2
commit e66d793646
2 changed files with 311 additions and 85 deletions

28
data.py
View File

@ -6,6 +6,7 @@ from pathlib import PurePath
from glob import glob from glob import glob
import re import re
import os import os
from datetime import datetime
import config import config
@ -239,7 +240,9 @@ class LinkData:
return sorted(search_results, key=lambda x: x[0], reverse=True) return sorted(search_results, key=lambda x: x[0], reverse=True)
def list_category_details(self, selected_category: str) -> list: def list_category_details(self, selected_category: str) -> list:
"""returns a sorted list of posts belonging to the specified category""" """accepts a category name. returns a sorted list of posts belonging to
the specified category"""
links = [] links = []
for record in self.link_data: for record in self.link_data:
@ -278,5 +281,26 @@ class LinkData:
"last_modified_timestamp": last_modified_timestamp, "last_modified_timestamp": last_modified_timestamp,
} }
) )
return sorted(links, key=lambda x: x["last_modified_timestamp"], reverse=True) return sorted(links, key=lambda x: x["last_modified_timestamp"], reverse=True)
def get_thread_details(self, selected_thread) -> dict:
output = {}
for line in self.link_data:
if line[0] == selected_thread:
output["parent_id"] = "{}+{}".format(line[1], str(line[2]))
output["author"] = line[1]
output["date"] = datetime.fromtimestamp(float(line[2])).strftime("%c")
output["category"] = line[4]
output["url"] = line[5]
output["title"] = line[6]
break
if not output["parent_id"]:
raise ValueError("Sorry, no thread found with that ID.")
output["replies"] = sorted(
[line for line in self.link_data if line[3] == output["parent_id"]],
key=lambda x: x[2],
)
return output

View File

@ -4,7 +4,7 @@
## If this script contains bugs, blame cmccabe. ## If this script contains bugs, blame cmccabe.
import getpass import getpass
import readline # pylint: disable=unused-import import readline
import signal import signal
import subprocess import subprocess
import sys import sys
@ -13,6 +13,7 @@ from time import time
from urllib.parse import urlparse from urllib.parse import urlparse
from datetime import datetime from datetime import datetime
from shutil import which, get_terminal_size from shutil import which, get_terminal_size
from math import ceil
import data import data
import config import config
@ -21,15 +22,20 @@ import config
## id (if parent), username, datestamp, parent-id, category, link-url, link-title ## id (if parent), username, datestamp, parent-id, category, link-url, link-title
LinkData = data.LinkData() LinkData = data.LinkData()
link_data: list = LinkData.link_data link_data: list = LinkData.link_data
categories: list = LinkData.categories # categories: list = LinkData.categories
def print_categories(): def print_page_count(pages):
if pages.count > 1:
print("Page {} of {}".format(pages.current, pages.count))
def print_categories(categories, pages):
"""Prints the list of categories with an indicator for new activity""" """Prints the list of categories with an indicator for new activity"""
header = "\n{:>4s} New {:<25s}".format("ID#", "Category") header = "\n{:>4s} New {:<25s}".format("ID#", "Category")
out = "" out = ""
for i, record in enumerate(categories):
for i, record in enumerate(categories[pages.current_slice]):
out += "{:4d} {} {} ({})\n".format( out += "{:4d} {} {} ({})\n".format(
i + 1, i + 1,
"x" if record["last_updated"] >= config.USER.lastlogin else " ", "x" if record["last_updated"] >= config.USER.lastlogin else " ",
@ -37,16 +43,20 @@ def print_categories():
record["count"], record["count"],
) )
print("\033c", end="")
print_banner()
if len(out) > 0: if len(out) > 0:
print(header) print(header)
print(out) print(out)
print_page_count(pages)
else: else:
print("\n There are no posts yet - enter p to post a new link\n") print("\n There are no posts yet - enter p to post a new link\n")
def print_category_details(view_cat): def print_category_details(category_details, pages):
"""produces category detail data, prints it to the console. returns dict """produces category detail data, prints it to the console. returns dict
containing an index of threads""" containing an index of threads"""
view_cat = "category name"
columns, _ = get_terminal_size() columns, _ = get_terminal_size()
maxnamelen = len(max(link_data, key=lambda x: len(x[1]))[1]) maxnamelen = len(max(link_data, key=lambda x: len(x[1]))[1])
namelen = max(maxnamelen, 6) # minimum field width is 6 namelen = max(maxnamelen, 6) # minimum field width is 6
@ -58,7 +68,7 @@ def print_category_details(view_cat):
# unread mark width # unread mark width
header = "\n{}\n\n {:>3s} {:>10s} {:<{namelen}s} {:<5} {:<s}".format( header = "\n{}\n\n {:>3s} {:>10s} {:<{namelen}s} {:<5} {:<s}".format(
style_text(view_cat["name"].upper(), False, "bold"), style_text(view_cat.upper(), False, "bold"),
"ID#", "ID#",
"DATE", "DATE",
"AUTHOR", "AUTHOR",
@ -67,20 +77,15 @@ def print_category_details(view_cat):
namelen=namelen, namelen=namelen,
) )
out = "" out = ""
link_count = 0
thread_index = {}
category_details = LinkData.list_category_details(view_cat["name"])
for link in category_details: for i, link in enumerate(category_details):
link_count += 1
thread_index[link_count] = link["postid"]
desc = textwrap.shorten(link["description"], width=desclen, placeholder="...") desc = textwrap.shorten(link["description"], width=desclen, placeholder="...")
newmarker = ( newmarker = (
"*" if link["last_modified_timestamp"] >= config.USER.lastlogin else "" "*" if link["last_modified_timestamp"] >= config.USER.lastlogin else ""
) )
_dt = datetime.fromtimestamp(float(link["link_timestamp"])).strftime("%Y-%m-%d") _dt = datetime.fromtimestamp(float(link["link_timestamp"])).strftime("%Y-%m-%d")
out += " {:3d} {:>10s} {:<{namelen}s} [{:3d}] {:s}{}\n".format( out += " {:3d} {:>10s} {:<{namelen}s} [{:3d}] {:s}{}\n".format(
link_count, i + 1,
_dt, _dt,
link["link_author"], link["link_author"],
link["reply_count"], link["reply_count"],
@ -89,16 +94,17 @@ def print_category_details(view_cat):
namelen=namelen, namelen=namelen,
) )
print("\033c", end="")
if len(out) > 0: if len(out) > 0:
print(header) print(header)
print("." * len(header)) print("." * len(header))
print(out) print(out)
print_page_count(pages)
else: else:
print("\n\nThere are no posts for this category\n") print("\n\nThere are no posts for this category\n")
return thread_index
def print_thread_details(post_id) -> tuple: def print_thread_details(thread_details, pages) -> tuple:
"""produces thread detail data, prints it to the console""" """produces thread detail data, prints it to the console"""
# set up line wrapping # set up line wrapping
columns, _ = get_terminal_size() columns, _ = get_terminal_size()
@ -106,38 +112,30 @@ def print_thread_details(post_id) -> tuple:
initial_indent=" " * 2, subsequent_indent=" " * 21, width=columns initial_indent=" " * 2, subsequent_indent=" " * 21, width=columns
) )
# get post data # post detail view
parent_id: str = "" print("\033c", end="")
post_url: str = "" print(
for line in link_data: "\n\n{:<17}: {}".format(
if line[0] == post_id: style_text("Title", False, "bold"), thread_details["title"]
parent_id = "{}+{}".format(line[1], str(line[2])) )
post_username = line[1] )
post_datetime = datetime.fromtimestamp(float(line[2])).strftime("%c") print("{:<17}: {}".format(style_text("Link", False, "bold"), thread_details["url"]))
post_category = line[4] print(
post_url = line[5] "{:<17}: {}".format(
post_title = line[6] style_text("Category", False, "bold"), thread_details["category"]
)
# if post is not found, return empty string )
if parent_id == "": print(
raise ValueError("Sorry, no thread found with that ID.") "{:<17}: {}".format(style_text("User", False, "bold"), thread_details["author"])
)
# get replies data print(
replies = sorted( "{:<17}: {}".format(style_text("Date", False, "bold"), thread_details["date"])
[line for line in link_data if line[3] == parent_id], key=lambda x: x[2]
) )
# post detail view
print("\n\n{:<17}: {}".format(style_text("Title", False, "bold"), post_title))
print("{:<17}: {}".format(style_text("Link", False, "bold"), post_url))
print("{:<17}: {}".format(style_text("Category", False, "bold"), post_category))
print("{:<17}: {}".format(style_text("User", False, "bold"), post_username))
print("{:<17}: {}".format(style_text("Date", False, "bold"), post_datetime))
# post reply view # post reply view
if replies: if thread_details["replies"]:
print("\n{}:\n".format(style_text("Replies", False, "underline"))) print("\n{}:\n".format(style_text("Replies", False, "underline")))
for line in replies: for line in thread_details["replies"]:
comment_author = line[1] comment_author = line[1]
comment_date = datetime.fromtimestamp(float(line[2])).isoformat( comment_date = datetime.fromtimestamp(float(line[2])).isoformat(
sep=" ", timespec="minutes" sep=" ", timespec="minutes"
@ -152,12 +150,11 @@ def print_thread_details(post_id) -> tuple:
print("\nNo replies yet. Be the first!") print("\nNo replies yet. Be the first!")
print("") print("")
print_page_count(pages)
# return data used by menu control
return parent_id, post_url
def print_search_results(keyword: str, search_results: list): def print_search_results(keyword: str, search_results: list):
print("\033c", end="")
"""a view for the search results - prints results to screen""" """a view for the search results - prints results to screen"""
print( print(
"\nShowing results for {}\n\n{:>4s} {:<15s}{:<12s}{:<13s}".format( "\nShowing results for {}\n\n{:>4s} {:<15s}{:<12s}{:<13s}".format(
@ -213,7 +210,7 @@ def search():
print("{}".format(style_text("Invalid entry", False, "bold"))) print("{}".format(style_text("Invalid entry", False, "bold")))
def view_link_in_browser(url): def open_link_in_browser(url):
"""Attempts to view the specified URL in the configured browser""" """Attempts to view the specified URL in the configured browser"""
if which(config.USER.browser) is None: if which(config.USER.browser) is None:
print( print(
@ -343,53 +340,259 @@ def post_link() -> int:
return LinkData.add(record) return LinkData.add(record)
class Pages:
def __init__(self):
self.current: int = 1 # the current page
self.count: int = 0 # the total number of pages
self.length: int = None # the number of lines on a page
self.current_slice: slice = None # a slice to display the current page data
def calculate_pages(self, content_len: int, boilerplate_len: int):
_, lines = get_terminal_size()
self.length = lines - boilerplate_len
self.count = ceil(content_len / self.length)
self.get_pages_current_slice()
def next(self):
if self.current < self.count:
self.current += 1
def prev(self):
if self.current > 1:
self.current -= 1
def get_pages_current_slice(self):
self.current_slice = slice(
(self.length * (self.current - 1)), (self.length * self.current)
)
class Level:
def __init__(self, name):
self.name = name
self.data = None
self.pages = Pages()
self.selected_index = None
class Menu:
def __init__(self):
# main levels: categories -> category_details -> thread_details
categories = Level("categories")
category_details = Level("category_details")
thread_details = Level("thread_details")
self.main_levels = [categories, category_details, thread_details]
# search levels: search_results -> search_results_thread_details
search_results = Level("search_results")
search_results_thread_details = Level("search_results_thread_details")
self.search_levels = [search_results, search_results_thread_details]
self.is_main_level = True
self.main_level_index = 0
self.search_level_index = 0
self.current_level = self.main_levels[self.main_level_index]
def back(self):
if self.is_main_level:
if self.main_level_index > 0:
self.main_level_index -= 1
self.current_level = self.main_levels[self.main_level_index]
else:
if self.search_level_index > 0:
self.search_level_index -= 1
self.current_level = self.search_levels[self.search_level_index]
else:
self.current_level = self.main_levels[self.main_level_index]
def forward(self):
if self.is_main_level:
if self.main_level_index < len(self.main_levels) - 1:
self.main_level_index += 1
self.current_level = self.main_levels[self.main_level_index]
else:
if self.search_level_index < len(self.search_levels) - 1:
self.search_level_index += 1
self.current_level = self.search_levels[self.search_level_index]
else:
self.current_level = self.main_levels[self.main_level_index]
def menu_view_categories(): def menu_view_categories():
"""Displays list of categories, takes keyboard input and """Displays list of categories, takes keyboard input and
executes corresponding functions.""" executes corresponding functions."""
boilerplate_len = 10
menu = Menu()
# each menu level consists of:
# data (a list or dictionary) that gets printed
# this data may be based on an index chosen by the previous level
# page settings which are used to calculate the index
# input which is handled
while True: while True:
print_categories() # print current menu level
if menu.current_level.name == "categories":
option = input( menu.current_level.data = LinkData.categories
"Enter a category ID, {} to post a link, {} to search, or {} to quit: ".format( menu.current_level.pages.calculate_pages(
style_text("p", True, "underline"), len(menu.current_level.data), boilerplate_len
style_text("s", True, "underline"),
style_text("q", True, "underline"),
) )
).lower() print_categories(menu.current_level.data, menu.current_level.pages)
change_level(menu)
if option == "q": elif menu.current_level.name == "category_details":
return menu.current_level.data = LinkData.list_category_details(
if option == "p": menu.main_levels[0].data[menu.current_level.selected_index]["name"]
)
menu.current_level.pages.calculate_pages(
len(menu.current_level.data), boilerplate_len
)
print_category_details(
menu.current_level.data[menu.current_level.pages.current_slice],
menu.current_level.pages,
)
change_level(menu)
elif menu.current_level.name == "thread_details":
menu.current_level.data = LinkData.get_thread_details(
menu.main_levels[1].data[menu.current_level.selected_index]["postid"]
)
menu.current_level.pages.calculate_pages(
len(menu.current_level.data), boilerplate_len
)
print_thread_details(menu.current_level.data, menu.current_level.pages)
change_level(menu)
elif menu.current_level.name == "search_results":
pass
elif menu.current_level.name == "search_results_thread_details":
pass
def change_level(menu) -> int:
"""???"""
while True:
# get input
action = parse_input()
# handle input
if action in ["n", "next"]:
menu.current_level.pages.next()
elif action in ["p", "prev"]:
menu.current_level.pages.prev()
elif action in ["b", "back"]:
menu.back()
elif action in ["f", "forward"]:
menu.forward()
elif action in ["?", "help"]:
print(HELP_TEXT)
elif action in ["q", "quit", "exit"]:
graceful_exit()
elif action in ["c", "create"]:
post_id = post_link() post_id = post_link()
if post_id >= 0: if post_id >= 0:
menu_view_thread_details(post_id) # TODO: create new post
continue # set category_details to the relevant category
if option == "s": # set the selected index to the index of the new post in category_details
search() # set menu level to thread index
continue pass
try: elif action in ["s", "search"]:
cat_index = categories[int(option) - 1] pass
menu_view_category_details(cat_index) # search()
except (IndexError, ValueError): # TODO: open search screen
print("Sorry, that category does not exist. Try again.") # 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 menu.current_level.name in [
"search_result_thread_details",
"thread_details",
]:
# reply(thread_details["postid"])
pass
elif action in ["o", "open"] and menu.current_level.name in [
"search_result_thread_details",
"thread_details",
]:
pass
# open_link_in_browser(thread_details["url"])
# open link in external program
# is o the right command?
else:
try:
# numeric action
action = int(action)
except (ValueError):
print("invalid input")
continue
selected_index = (action - 1) + (
menu.current_level.pages.length * (menu.current_level.pages.current - 1)
)
menu.forward()
menu.current_level.pages.current_page = 0
menu.current_level.selected_index = selected_index
break
# except (IndexError, ValueError):
# print("Sorry, that category does not exist. Try again.")
def menu_view_category_details(cat_index): def parse_input() -> str:
output = input("input: ").lower()
return output
"""Gets user input and processes it. Accepts a menu level of categories,
category_details or thread_details. Returns a menu page action or
nothing"""
# def parse_input(level.current: str) -> str:
# if level.current == "categories":
# input_text = (
# "Enter an ID, {}dd a link, {}earch, {}ext or {}prev page, {} or {}uit: ".format(
# style_text("a", True, "underline"),
# style_text("s", True, "underline"),
# style_text("n", True, "underline"),
# style_text("p", True, "underline"),
# style_text("?", True, "underline"),
# style_text("q", True, "underline"),
# )
# )
# elif level.current = "category_details"
# input_text = (
# "Enter an ID, go {}ack, {}dd a link, {}earch, page {}p or {}own".format(
# style_text("b", True, "underline"),
# style_text("a", True, "underline"),
# style_text("s", True, "underline"),
# style_text("n", True, "underline"),
# style_text("?", True, "underline"),
# )
# )
# return input(input_text).lower()
def menu_view_category_details(selected_category):
"""Displays category details, takes keyboard input and executes """Displays category details, takes keyboard input and executes
corresponding functions""" corresponding functions"""
pages = Pages()
category_details = LinkData.list_category_details(selected_category)
while True: while True:
thread_index = print_category_details(cat_index) boilerplate_len = 10
pages = calculate_pages(len(category_details), boilerplate_len, pages)
pages = get_pages_current_slice(pages)
option = input( print_category_details(
"Enter a post ID to see its thread, {} to go back, {} to " category_details[current_page],
"search, {} to post a link, or {} to quit: ".format( pages.current,
style_text("m", True, "underline"), pages.count,
style_text("s", True, "underline"), )
style_text("p", True, "underline"),
style_text("q", True, "underline"), pages = interpret_commands(pages, "category_details")
)
).lower()
if option == "q": if option == "q":
graceful_exit() graceful_exit()
@ -404,7 +607,7 @@ def menu_view_category_details(cat_index):
menu_view_thread_details(post_id) menu_view_thread_details(post_id)
continue continue
try: try:
post_id = thread_index[int(option)] selected_category = category_details(option)
menu_view_thread_details(post_id) menu_view_thread_details(post_id)
except (KeyError, ValueError): except (KeyError, ValueError):
# Catch a Post ID that is not in the thread list or is not a number # Catch a Post ID that is not in the thread list or is not a number
@ -500,7 +703,6 @@ def main():
args = sys.argv[1:] args = sys.argv[1:]
config.init() config.init()
if not args: if not args:
print_banner()
menu_view_categories() menu_view_categories()
elif args[0] in ["-h", "--help", "help"]: elif args[0] in ["-h", "--help", "help"]:
print(HELP_TEXT) print(HELP_TEXT)