initial work on pager - major changes to menu functionality
This commit is contained in:
parent
ecbf2b66d2
commit
e66d793646
28
data.py
28
data.py
|
@ -6,6 +6,7 @@ from pathlib import PurePath
|
|||
from glob import glob
|
||||
import re
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import config
|
||||
|
||||
|
@ -239,7 +240,9 @@ class LinkData:
|
|||
return sorted(search_results, key=lambda x: x[0], reverse=True)
|
||||
|
||||
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 = []
|
||||
|
||||
for record in self.link_data:
|
||||
|
@ -278,5 +281,26 @@ class LinkData:
|
|||
"last_modified_timestamp": last_modified_timestamp,
|
||||
}
|
||||
)
|
||||
|
||||
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
|
||||
|
|
368
linkulator.py
368
linkulator.py
|
@ -4,7 +4,7 @@
|
|||
## If this script contains bugs, blame cmccabe.
|
||||
|
||||
import getpass
|
||||
import readline # pylint: disable=unused-import
|
||||
import readline
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
|
@ -13,6 +13,7 @@ from time import time
|
|||
from urllib.parse import urlparse
|
||||
from datetime import datetime
|
||||
from shutil import which, get_terminal_size
|
||||
from math import ceil
|
||||
|
||||
import data
|
||||
import config
|
||||
|
@ -21,15 +22,20 @@ import config
|
|||
## id (if parent), username, datestamp, parent-id, category, link-url, link-title
|
||||
LinkData = data.LinkData()
|
||||
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"""
|
||||
header = "\n{:>4s} New {:<25s}".format("ID#", "Category")
|
||||
|
||||
out = ""
|
||||
for i, record in enumerate(categories):
|
||||
|
||||
for i, record in enumerate(categories[pages.current_slice]):
|
||||
out += "{:4d} {} {} ({})\n".format(
|
||||
i + 1,
|
||||
"x" if record["last_updated"] >= config.USER.lastlogin else " ",
|
||||
|
@ -37,16 +43,20 @@ def print_categories():
|
|||
record["count"],
|
||||
)
|
||||
|
||||
print("\033c", end="")
|
||||
print_banner()
|
||||
if len(out) > 0:
|
||||
print(header)
|
||||
print(out)
|
||||
print_page_count(pages)
|
||||
else:
|
||||
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
|
||||
containing an index of threads"""
|
||||
view_cat = "category name"
|
||||
columns, _ = get_terminal_size()
|
||||
maxnamelen = len(max(link_data, key=lambda x: len(x[1]))[1])
|
||||
namelen = max(maxnamelen, 6) # minimum field width is 6
|
||||
|
@ -58,7 +68,7 @@ def print_category_details(view_cat):
|
|||
# unread mark width
|
||||
|
||||
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#",
|
||||
"DATE",
|
||||
"AUTHOR",
|
||||
|
@ -67,20 +77,15 @@ def print_category_details(view_cat):
|
|||
namelen=namelen,
|
||||
)
|
||||
out = ""
|
||||
link_count = 0
|
||||
thread_index = {}
|
||||
category_details = LinkData.list_category_details(view_cat["name"])
|
||||
|
||||
for link in category_details:
|
||||
link_count += 1
|
||||
thread_index[link_count] = link["postid"]
|
||||
for i, link in enumerate(category_details):
|
||||
desc = textwrap.shorten(link["description"], width=desclen, placeholder="...")
|
||||
newmarker = (
|
||||
"*" if link["last_modified_timestamp"] >= config.USER.lastlogin else ""
|
||||
)
|
||||
_dt = datetime.fromtimestamp(float(link["link_timestamp"])).strftime("%Y-%m-%d")
|
||||
out += " {:3d} {:>10s} {:<{namelen}s} [{:3d}] {:s}{}\n".format(
|
||||
link_count,
|
||||
i + 1,
|
||||
_dt,
|
||||
link["link_author"],
|
||||
link["reply_count"],
|
||||
|
@ -89,16 +94,17 @@ def print_category_details(view_cat):
|
|||
namelen=namelen,
|
||||
)
|
||||
|
||||
print("\033c", end="")
|
||||
if len(out) > 0:
|
||||
print(header)
|
||||
print("." * len(header))
|
||||
print(out)
|
||||
print_page_count(pages)
|
||||
else:
|
||||
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"""
|
||||
# set up line wrapping
|
||||
columns, _ = get_terminal_size()
|
||||
|
@ -106,38 +112,30 @@ def print_thread_details(post_id) -> tuple:
|
|||
initial_indent=" " * 2, subsequent_indent=" " * 21, width=columns
|
||||
)
|
||||
|
||||
# get post data
|
||||
parent_id: str = ""
|
||||
post_url: str = ""
|
||||
for line in link_data:
|
||||
if line[0] == post_id:
|
||||
parent_id = "{}+{}".format(line[1], str(line[2]))
|
||||
post_username = line[1]
|
||||
post_datetime = datetime.fromtimestamp(float(line[2])).strftime("%c")
|
||||
post_category = line[4]
|
||||
post_url = line[5]
|
||||
post_title = line[6]
|
||||
|
||||
# if post is not found, return empty string
|
||||
if parent_id == "":
|
||||
raise ValueError("Sorry, no thread found with that ID.")
|
||||
|
||||
# get replies data
|
||||
replies = sorted(
|
||||
[line for line in link_data if line[3] == parent_id], key=lambda x: x[2]
|
||||
# post detail view
|
||||
print("\033c", end="")
|
||||
print(
|
||||
"\n\n{:<17}: {}".format(
|
||||
style_text("Title", False, "bold"), thread_details["title"]
|
||||
)
|
||||
)
|
||||
print("{:<17}: {}".format(style_text("Link", False, "bold"), thread_details["url"]))
|
||||
print(
|
||||
"{:<17}: {}".format(
|
||||
style_text("Category", False, "bold"), thread_details["category"]
|
||||
)
|
||||
)
|
||||
print(
|
||||
"{:<17}: {}".format(style_text("User", False, "bold"), thread_details["author"])
|
||||
)
|
||||
print(
|
||||
"{:<17}: {}".format(style_text("Date", False, "bold"), thread_details["date"])
|
||||
)
|
||||
|
||||
# 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
|
||||
if replies:
|
||||
if thread_details["replies"]:
|
||||
print("\n{}:\n".format(style_text("Replies", False, "underline")))
|
||||
for line in replies:
|
||||
for line in thread_details["replies"]:
|
||||
comment_author = line[1]
|
||||
comment_date = datetime.fromtimestamp(float(line[2])).isoformat(
|
||||
sep=" ", timespec="minutes"
|
||||
|
@ -152,12 +150,11 @@ def print_thread_details(post_id) -> tuple:
|
|||
print("\nNo replies yet. Be the first!")
|
||||
|
||||
print("")
|
||||
|
||||
# return data used by menu control
|
||||
return parent_id, post_url
|
||||
print_page_count(pages)
|
||||
|
||||
|
||||
def print_search_results(keyword: str, search_results: list):
|
||||
print("\033c", end="")
|
||||
"""a view for the search results - prints results to screen"""
|
||||
print(
|
||||
"\nShowing results for {}\n\n{:>4s} {:<15s}{:<12s}{:<13s}".format(
|
||||
|
@ -213,7 +210,7 @@ def search():
|
|||
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"""
|
||||
if which(config.USER.browser) is None:
|
||||
print(
|
||||
|
@ -343,53 +340,259 @@ def post_link() -> int:
|
|||
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():
|
||||
"""Displays list of categories, takes keyboard input and
|
||||
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:
|
||||
print_categories()
|
||||
|
||||
option = input(
|
||||
"Enter a category ID, {} to post a link, {} to search, or {} to quit: ".format(
|
||||
style_text("p", True, "underline"),
|
||||
style_text("s", True, "underline"),
|
||||
style_text("q", True, "underline"),
|
||||
# print current menu level
|
||||
if menu.current_level.name == "categories":
|
||||
menu.current_level.data = LinkData.categories
|
||||
menu.current_level.pages.calculate_pages(
|
||||
len(menu.current_level.data), boilerplate_len
|
||||
)
|
||||
).lower()
|
||||
print_categories(menu.current_level.data, menu.current_level.pages)
|
||||
change_level(menu)
|
||||
|
||||
if option == "q":
|
||||
return
|
||||
if option == "p":
|
||||
elif menu.current_level.name == "category_details":
|
||||
menu.current_level.data = LinkData.list_category_details(
|
||||
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()
|
||||
if post_id >= 0:
|
||||
menu_view_thread_details(post_id)
|
||||
continue
|
||||
if option == "s":
|
||||
search()
|
||||
continue
|
||||
try:
|
||||
cat_index = categories[int(option) - 1]
|
||||
menu_view_category_details(cat_index)
|
||||
except (IndexError, ValueError):
|
||||
print("Sorry, that category does not exist. Try again.")
|
||||
# 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
|
||||
elif action in ["s", "search"]:
|
||||
pass
|
||||
# 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 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
|
||||
corresponding functions"""
|
||||
|
||||
pages = Pages()
|
||||
category_details = LinkData.list_category_details(selected_category)
|
||||
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(
|
||||
"Enter a post ID to see its thread, {} to go back, {} to "
|
||||
"search, {} to post a link, or {} to quit: ".format(
|
||||
style_text("m", True, "underline"),
|
||||
style_text("s", True, "underline"),
|
||||
style_text("p", True, "underline"),
|
||||
style_text("q", True, "underline"),
|
||||
)
|
||||
).lower()
|
||||
print_category_details(
|
||||
category_details[current_page],
|
||||
pages.current,
|
||||
pages.count,
|
||||
)
|
||||
|
||||
pages = interpret_commands(pages, "category_details")
|
||||
|
||||
if option == "q":
|
||||
graceful_exit()
|
||||
|
@ -404,7 +607,7 @@ def menu_view_category_details(cat_index):
|
|||
menu_view_thread_details(post_id)
|
||||
continue
|
||||
try:
|
||||
post_id = thread_index[int(option)]
|
||||
selected_category = category_details(option)
|
||||
menu_view_thread_details(post_id)
|
||||
except (KeyError, ValueError):
|
||||
# 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:]
|
||||
config.init()
|
||||
if not args:
|
||||
print_banner()
|
||||
menu_view_categories()
|
||||
elif args[0] in ["-h", "--help", "help"]:
|
||||
print(HELP_TEXT)
|
||||
|
|
Loading…
Reference in New Issue