forked from cmccabe/linkulator2
Added more navigation and command paths
This commit is contained in:
parent
1656c0fc7b
commit
9aed41c8f7
262
linkulator.py
262
linkulator.py
|
@ -13,9 +13,11 @@ import textwrap
|
|||
from time import time
|
||||
from urllib.parse import urlparse
|
||||
from datetime import datetime
|
||||
from shutil import which, get_terminal_size
|
||||
from shutil import which
|
||||
from typing import Tuple
|
||||
import curses
|
||||
import curses.textpad as textpad
|
||||
|
||||
import locale
|
||||
|
||||
import data
|
||||
|
@ -443,6 +445,12 @@ class Menu:
|
|||
# TODO: if this fails it's uninitialised or something
|
||||
|
||||
|
||||
class Status:
|
||||
def __init__(self):
|
||||
self.message: str
|
||||
self.action: str
|
||||
|
||||
|
||||
def term_resize(stdscr):
|
||||
"""resizes the provided stdscr to the current size"""
|
||||
# TODO: ensure resize can decrement bodyscrollmin by the appropriate amount
|
||||
|
@ -463,15 +471,19 @@ def new_main_menu(stdscr):
|
|||
menu.get_data(LinkData)
|
||||
menu.current_level.output.Calc()
|
||||
|
||||
status = "".encode(code)
|
||||
action = "waiting".encode(code)
|
||||
status = Status()
|
||||
status.message = ""
|
||||
status.action = ""
|
||||
|
||||
while True:
|
||||
# stdscr.erase()
|
||||
curses.update_lines_cols()
|
||||
# TODO: consider stdscr.erase()
|
||||
|
||||
# print title
|
||||
stdscr.addstr(0, 1, title)
|
||||
stdscr.clrtoeol()
|
||||
# TODO: subtitle?
|
||||
|
||||
# TODO: print subtitle?
|
||||
# stdscr.addstr(1, 4, "All categories".encode(code))
|
||||
|
||||
# print header
|
||||
|
@ -490,6 +502,10 @@ def new_main_menu(stdscr):
|
|||
for i, line in enumerate(
|
||||
menu.current_level.output.content[menu.current_level.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.addstr(i + menu.current_level.output.miny, 0, line.encode(code))
|
||||
stdscr.clrtoeol()
|
||||
|
@ -497,134 +513,142 @@ def new_main_menu(stdscr):
|
|||
# this is meant to clear to the second last line, but...
|
||||
# just erase to the bottom for now
|
||||
stdscr.clrtobot()
|
||||
# leftover = menu.current_level.output.maxy - count
|
||||
# if leftover > 0:
|
||||
# for i in range(
|
||||
# (menu.current_level.output.maxy - menu.current_level.output.miny)
|
||||
# - count
|
||||
# ):
|
||||
# stdscr.move(count + i + 1, 0)
|
||||
# stdscr.clrtoeol()
|
||||
|
||||
stdscr.refresh()
|
||||
|
||||
# print status
|
||||
stdscr.addstr((curses.LINES - 1), 0, status)
|
||||
stdscr.clrtoeol()
|
||||
if status.message:
|
||||
status_text = "{} - {}".format(status.message, status.action)
|
||||
stdscr.addstr((curses.LINES - 1), 0, status_text.encode(code))
|
||||
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
|
||||
stdscr.refresh()
|
||||
|
||||
try:
|
||||
k = stdscr.getkey()
|
||||
except (curses.error):
|
||||
status = str(curses.error)
|
||||
status.message = str(curses.error)
|
||||
status.action = str(k)
|
||||
k = None
|
||||
continue
|
||||
|
||||
if k == "q":
|
||||
graceful_exit()
|
||||
elif k == "KEY_RESIZE":
|
||||
if k == "KEY_RESIZE":
|
||||
term_resize(stdscr)
|
||||
action = "resize"
|
||||
elif k == "j":
|
||||
# TODO: put controls in output class
|
||||
if menu.current_level.output.scrollminy <= (
|
||||
menu.current_level.output.length - menu.current_level.output.maxy
|
||||
):
|
||||
menu.current_level.output.scrollminy += 1
|
||||
action = "down"
|
||||
else:
|
||||
action = "tried down"
|
||||
elif k == "k":
|
||||
if menu.current_level.output.scrollminy > 0:
|
||||
menu.current_level.output.scrollminy -= 1
|
||||
action = "up"
|
||||
else:
|
||||
action = "tried up"
|
||||
elif k == "b":
|
||||
menu.back()
|
||||
elif k in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]:
|
||||
action = int(k)
|
||||
if menu.current_level.name == "categories":
|
||||
key = menu.current_level.data[action - 1]["name"]
|
||||
elif menu.current_level.name == "links":
|
||||
key = menu.current_level.data[action - 1]["post_id"]
|
||||
menu.forward()
|
||||
menu.current_level.selected_key = key
|
||||
menu.get_data(LinkData)
|
||||
menu.current_level.output.Calc()
|
||||
|
||||
status = "{}".format(action)
|
||||
status.encode(code)
|
||||
|
||||
stdscr.refresh()
|
||||
|
||||
|
||||
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:
|
||||
# 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?
|
||||
|
||||
elif k in [":", " "]:
|
||||
iwin = curses.newwin(1, curses.COLS, curses.LINES - 1, 0)
|
||||
iwin.addch(0,0,":")
|
||||
itxt = textpad.Textbox(iwin)
|
||||
curses.curs_set(1)
|
||||
itxt.edit()
|
||||
action = itxt.gather()
|
||||
# action slice to remove : at the start
|
||||
# TODO: can this be avoided?
|
||||
doAction(menu, action[1:], status)
|
||||
curses.curs_set(0)
|
||||
del itxt
|
||||
del iwin
|
||||
else:
|
||||
doAction(menu, k, status)
|
||||
|
||||
def doAction(menu, action, status):
|
||||
int_action = None
|
||||
try:
|
||||
int_action = int(action)
|
||||
except ValueError:
|
||||
# it's not a number but that's ok
|
||||
pass
|
||||
|
||||
if int_action:
|
||||
try:
|
||||
navigate(menu, int_action)
|
||||
except Exception as e:
|
||||
status.message = str(e)
|
||||
else:
|
||||
try:
|
||||
handle_command(menu, action)
|
||||
except Exception as e:
|
||||
status.message = str(e)
|
||||
|
||||
|
||||
def navigate(menu, int_action):
|
||||
if int_action:
|
||||
if menu.current_level.name == "categories":
|
||||
try:
|
||||
# numeric action
|
||||
action = int(action)
|
||||
except (ValueError):
|
||||
print("invalid input")
|
||||
continue
|
||||
key = menu.current_level.data[int_action - 1]["name"]
|
||||
except IndexError:
|
||||
raise IndexError("Sorry, that category doesn't exist")
|
||||
return
|
||||
elif menu.current_level.name == "links":
|
||||
try:
|
||||
key = menu.current_level.data[int_action - 1]["post_id"]
|
||||
except IndexError:
|
||||
raise IndexError("Sorry, that link doesn't exist")
|
||||
return
|
||||
else:
|
||||
# no action because it's not a valid menu
|
||||
return
|
||||
menu.forward()
|
||||
menu.current_level.selected_key = key
|
||||
menu.get_data(LinkData)
|
||||
menu.current_level.output.Calc()
|
||||
|
||||
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 handle_command(menu, action):
|
||||
"""???"""
|
||||
if action in ["j", "KEY_DOWN"]:
|
||||
# TODO: put up/down controls in output class?
|
||||
if menu.current_level.output.scrollminy <= (
|
||||
menu.current_level.output.length - menu.current_level.output.maxy
|
||||
):
|
||||
menu.current_level.output.scrollminy += 1
|
||||
elif action in ["k", "KEY_UP"]:
|
||||
if menu.current_level.output.scrollminy > 0:
|
||||
menu.current_level.output.scrollminy -= 1
|
||||
elif action in ["b", "back"]:
|
||||
menu.back()
|
||||
# TODO: add forward as a command?
|
||||
# it could work like a browser's forward but the command needs a guard to
|
||||
# not navigate forward unless you have already been forward
|
||||
# elif action in ["f", "forward"]:
|
||||
# menu.forward()
|
||||
elif action in ["?", "help"]:
|
||||
pass
|
||||
# print(HELP_TEXT)
|
||||
elif action in ["q", "quit", "exit"]:
|
||||
graceful_exit()
|
||||
elif action in ["c", "create"]:
|
||||
#post_id = post_link()
|
||||
#if post_id >= 0:
|
||||
# TODO: create new post
|
||||
# set category_details to the relevant category
|
||||
# set the selected index to the index of the new post in category_details
|
||||
# set menu level to thread index
|
||||
pass
|
||||
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?
|
||||
|
||||
|
||||
def parse_input() -> str:
|
||||
|
|
|
@ -62,28 +62,46 @@ class TestPrintCategories(unittest.TestCase):
|
|||
]
|
||||
cols = 80
|
||||
|
||||
test_results = (
|
||||
" ID# New Category",
|
||||
[
|
||||
" 1 * category 1 (1)",
|
||||
" 2 * category 2 (2)",
|
||||
" 3 * long category name that will be truncated because it's a long... (20)",
|
||||
],
|
||||
)
|
||||
expected_header = " ID# New Category"
|
||||
|
||||
test_output = linkulator.print_categories(categories, cols)
|
||||
self.assertTupleEqual(test_output, test_results)
|
||||
expected_content = [
|
||||
" 1 * category 1 (1)",
|
||||
" 2 * category 2 (2)",
|
||||
" 3 * long category name that will be truncated because it's a long... (20)",
|
||||
]
|
||||
|
||||
actual_header, actual_content = linkulator.print_categories(categories, cols)
|
||||
|
||||
# confirm expected is equal to actual
|
||||
self.assertEqual(expected_header, actual_header)
|
||||
self.assertListEqual(expected_content, actual_content)
|
||||
|
||||
# confirm actual does not exceed cols
|
||||
header_max_cols = max([len(line) for line in actual_header])
|
||||
self.assertTrue(header_max_cols <= cols)
|
||||
content_max_cols = max([len(line) for line in actual_content])
|
||||
self.assertTrue(content_max_cols <= cols)
|
||||
|
||||
def test_empty_categories(self):
|
||||
"""Test output when no categories data"""
|
||||
empty_categories = []
|
||||
cols = 80
|
||||
test_results = (
|
||||
expected_header = ""
|
||||
expected_content = [
|
||||
"",
|
||||
["", "There are no posts yet - enter p to post a new link"],
|
||||
"There are no posts yet - enter p to post a new link",
|
||||
]
|
||||
actual_header, actual_content = linkulator.print_categories(
|
||||
empty_categories, cols
|
||||
)
|
||||
test_output = linkulator.print_categories(empty_categories, cols)
|
||||
self.assertTupleEqual(test_output, test_results)
|
||||
|
||||
# confirm expected is equal to actual
|
||||
self.assertEqual(expected_header, actual_header)
|
||||
self.assertListEqual(expected_content, actual_content)
|
||||
|
||||
# confirm actual does not exceed cols
|
||||
content_max_cols = max([len(line) for line in actual_content])
|
||||
self.assertTrue(content_max_cols <= cols)
|
||||
|
||||
|
||||
class TestPrintLinks(unittest.TestCase):
|
||||
|
@ -103,8 +121,8 @@ class TestPrintLinks(unittest.TestCase):
|
|||
{
|
||||
"post_id": 2,
|
||||
"link_timestamp": "1627549445.044661",
|
||||
"link_author": "author 2",
|
||||
"reply_count": 25,
|
||||
"link_author": "author 2 with a long name",
|
||||
"reply_count": 250,
|
||||
"description": "a long description for the second post that should wrap i guess",
|
||||
"has_new_replies": True,
|
||||
"last_modified_timestamp": "1627549445.044661",
|
||||
|
@ -112,16 +130,25 @@ class TestPrintLinks(unittest.TestCase):
|
|||
]
|
||||
|
||||
cols = 80
|
||||
test_results = (
|
||||
" ID# Date Author #Repl Description",
|
||||
[
|
||||
" 1 2021-07-29 auth 1 [ 0] description 1",
|
||||
" 2 2021-07-29 author 2 [ 25] a long description for the second post...*",
|
||||
],
|
||||
expected_header = (
|
||||
" ID# Date Author #Repl Description"
|
||||
)
|
||||
expected_content = [
|
||||
" 1 2021-07-29 auth 1 [ 0] description 1",
|
||||
" 2 2021-07-29 author 2 [ 25] a long description for the second post...*",
|
||||
]
|
||||
|
||||
test_output = linkulator.print_links(links, cols)
|
||||
self.assertTupleEqual(test_output, test_results)
|
||||
actual_header, actual_content = linkulator.print_links(links, cols)
|
||||
|
||||
# confirm expected is equal to actual
|
||||
self.assertEqual(expected_header, actual_header)
|
||||
self.assertListEqual(expected_content, actual_content)
|
||||
|
||||
# confirm actual does not exceed cols
|
||||
header_max_cols = max([len(line) for line in actual_header])
|
||||
self.assertTrue(header_max_cols <= cols)
|
||||
content_max_cols = max([len(line) for line in actual_content])
|
||||
self.assertTrue(content_max_cols <= cols)
|
||||
|
||||
|
||||
class TestPrintPost(unittest.TestCase):
|
||||
|
@ -139,7 +166,7 @@ class TestPrintPost(unittest.TestCase):
|
|||
}
|
||||
|
||||
cols = 80
|
||||
test_results = [
|
||||
expected_content = [
|
||||
" Title : A cool website",
|
||||
" Link : http://asdflkjasdf",
|
||||
" Category : test category 1",
|
||||
|
@ -148,8 +175,14 @@ class TestPrintPost(unittest.TestCase):
|
|||
"\n No replies yet. Be the first!",
|
||||
]
|
||||
|
||||
test_output = linkulator.print_post(post, cols)
|
||||
self.assertListEqual(test_output, test_results)
|
||||
actual_content = linkulator.print_post(post, cols)
|
||||
|
||||
# confirm expected is equal to actual
|
||||
self.assertListEqual(actual_content, expected_content)
|
||||
|
||||
# confirm actual does not exceed cols
|
||||
content_max_cols = max([len(line) for line in actual_content])
|
||||
self.assertTrue(content_max_cols <= cols)
|
||||
|
||||
def test_post_with_reply(self):
|
||||
"""Test print_post where the post has a reply"""
|
||||
|
@ -184,7 +217,7 @@ class TestPrintPost(unittest.TestCase):
|
|||
}
|
||||
|
||||
cols = 80
|
||||
test_results = [
|
||||
expected_content = [
|
||||
" Title : Website 2",
|
||||
" Link : asdflkjasdf",
|
||||
" Category : category 2",
|
||||
|
@ -195,5 +228,11 @@ class TestPrintPost(unittest.TestCase):
|
|||
" 1970-01-01 10:16 reply author 2 with a long long long name, a very long name: a reply with a lot of words in it, too many to read, not going to read this",
|
||||
]
|
||||
|
||||
test_output = linkulator.print_post(post, cols)
|
||||
self.assertListEqual(test_output, test_results)
|
||||
actual_content = linkulator.print_post(post, cols)
|
||||
|
||||
# confirm expected is equal to actual
|
||||
self.assertListEqual(actual_content, expected_content)
|
||||
|
||||
# confirm actual does not exceed cols
|
||||
content_max_cols = max([len(line) for line in actual_content])
|
||||
self.assertTrue(content_max_cols <= cols)
|
||||
|
|
Loading…
Reference in New Issue