forked from cmccabe/linkulator2
Fixed forward button, minor refactor
This commit is contained in:
parent
9aed41c8f7
commit
33e8eca10c
7
data.py
7
data.py
|
@ -6,7 +6,6 @@ from pathlib import PurePath
|
|||
from glob import glob
|
||||
import re
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import config
|
||||
|
||||
|
@ -30,7 +29,7 @@ class LinkDataRecord(NamedTuple):
|
|||
def is_well_formed_line(line: str) -> bool:
|
||||
"""Checks if current line is valid or not, returns true or false respectively."""
|
||||
pipe_count = (
|
||||
4 ## A PROPERLY FORMATED LINE IN linkulator.data HAS EXACTLY FOUR PIPES.
|
||||
4 # A PROPERLY FORMATED LINE IN linkulator.data HAS EXACTLY FOUR PIPES.
|
||||
)
|
||||
return line.count("|") == pipe_count
|
||||
|
||||
|
@ -282,7 +281,8 @@ class LinkData:
|
|||
"last_modified_timestamp": last_modified_timestamp,
|
||||
}
|
||||
)
|
||||
return sorted(links, key=lambda x: x["last_modified_timestamp"], reverse=True)
|
||||
links.sort(key=lambda x: x["last_modified_timestamp"], reverse=True)
|
||||
return links
|
||||
|
||||
def get_post(self, post_id) -> dict:
|
||||
output = {}
|
||||
|
@ -299,6 +299,7 @@ class LinkData:
|
|||
if not output["parent_id"]:
|
||||
raise ValueError("Sorry, no thread found with that ID.")
|
||||
|
||||
# TODO: this should return just the required fields
|
||||
output["replies"] = sorted(
|
||||
[record for record in self.link_data if record[3] == output["parent_id"]],
|
||||
key=lambda x: x[2],
|
||||
|
|
384
linkulator.py
384
linkulator.py
|
@ -1,44 +1,36 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Linkulator"""
|
||||
|
||||
## If this script contains bugs, blame cmccabe.
|
||||
# If this script contains bugs, blame cmccabe and asdf.
|
||||
|
||||
import getpass
|
||||
|
||||
# import readline
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from time import time
|
||||
from urllib.parse import urlparse
|
||||
from datetime import datetime
|
||||
from shutil import which
|
||||
from typing import Tuple
|
||||
import curses
|
||||
import curses.textpad as textpad
|
||||
|
||||
import getpass
|
||||
import locale
|
||||
import signal
|
||||
from subprocess import call
|
||||
import sys
|
||||
import textwrap
|
||||
from datetime import datetime
|
||||
from shutil import which
|
||||
from time import time
|
||||
from typing import Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import data
|
||||
import config
|
||||
import data
|
||||
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
code = locale.getpreferredencoding()
|
||||
|
||||
## id (if parent), username, datestamp, parent-id, category, link-url, link-title
|
||||
# linkdata columns:
|
||||
# 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
|
||||
|
||||
|
||||
def print_page_count(pages):
|
||||
if pages.count > 1:
|
||||
print("Page {} of {}".format(pages.current, pages.count))
|
||||
# VIEWS
|
||||
|
||||
|
||||
def print_categories(categories, cols) -> Tuple[str, list[str]]:
|
||||
def view_categories(categories, cols) -> Tuple[str, list[str]]:
|
||||
"""Produces categories screen display data. Returns as tuple of header and
|
||||
content."""
|
||||
|
||||
|
@ -67,18 +59,20 @@ def print_categories(categories, cols) -> Tuple[str, list[str]]:
|
|||
return thead, content
|
||||
|
||||
|
||||
def print_links(links, cols) -> Tuple[str, list[str]]:
|
||||
"""Produces links screen display data. Accepts links list and max column
|
||||
width. Returns a tuple of header and content."""
|
||||
def view_links(links, category_name, cols) -> Tuple[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_author_cols = max([len(link["link_author"]) for link in links])
|
||||
author_cols = max(max_author_cols, 6) # minimum field width is 6
|
||||
author_cols = max(max_author_cols, 6)
|
||||
|
||||
description_cols = cols - 18 - author_cols - 9 - 1
|
||||
# description_cols calulated as: the terminal width, minus the width of ID
|
||||
# and Date fields and padding, minus the max name length, minus Resp field
|
||||
# and padding width, minus the unread mark width
|
||||
|
||||
thead = " {:>3s} {:>10s} {:<{author_cols}s} {:<5} {:<s}".format(
|
||||
thead = " {}\n {:>3s} {:>10s} {:<{author_cols}s} {:<5} {:<s}".format(
|
||||
category_name.title(),
|
||||
"ID#",
|
||||
"Date",
|
||||
"Author",
|
||||
|
@ -113,7 +107,7 @@ def print_links(links, cols) -> Tuple[str, list[str]]:
|
|||
return thead, content
|
||||
|
||||
|
||||
def print_post(post, cols) -> list[str]:
|
||||
def view_post(post, cols) -> list[str]:
|
||||
"""Produces post screen display data. Accepts post id and max column
|
||||
width. Returns content list."""
|
||||
|
||||
|
@ -151,9 +145,10 @@ def print_post(post, cols) -> list[str]:
|
|||
return output
|
||||
|
||||
|
||||
def print_search_results(keyword: str, search_results: list):
|
||||
print("\033c", end="")
|
||||
"""a view for the search results - prints results to screen"""
|
||||
def view_search_results(keyword: str, search_results: list):
|
||||
"""Produces search results display data. Accepts search keyword, search
|
||||
results and max column width. Returns list of strings."""
|
||||
# TODO: update to produce list of strings
|
||||
print(
|
||||
"\nShowing results for {}\n\n{:>4s} {:<15s}{:<12s}{:<13s}".format(
|
||||
keyword, "ID#", "DATE", "AUTHOR", "DESC"
|
||||
|
@ -168,7 +163,7 @@ def print_search_results(keyword: str, search_results: list):
|
|||
print("")
|
||||
|
||||
|
||||
## CONTROLS
|
||||
# CONTROLS
|
||||
|
||||
|
||||
def search():
|
||||
|
@ -184,7 +179,7 @@ def search():
|
|||
print("No results found\n")
|
||||
return
|
||||
while True:
|
||||
print_search_results(keyword, search_results)
|
||||
view_search_results(keyword, search_results)
|
||||
option = input(
|
||||
"Enter a post ID to see its thread, {} to start a new search, {} to go back, or {} to quit: ".format(
|
||||
style_text("s", False, "underline"),
|
||||
|
@ -220,39 +215,44 @@ def open_link_in_browser(url):
|
|||
|
||||
url_scheme = urlparse(url).scheme
|
||||
if url_scheme in ["gopher", "https", "http"]:
|
||||
subprocess.call([config.USER.browser, url])
|
||||
call([config.USER.browser, url])
|
||||
else:
|
||||
print("Sorry, that url doesn't start with gopher://, http:// or https://")
|
||||
try_anyway = input(
|
||||
"Do you want to try it in {} anyway? Y/[N]".format(config.USER.browser)
|
||||
).lower()
|
||||
if try_anyway == "y":
|
||||
subprocess.call([config.USER.browser, url])
|
||||
call([config.USER.browser, url])
|
||||
|
||||
|
||||
def reply(parent_id):
|
||||
"""Prompt for reply, validate input, save validated input to disk and update
|
||||
link_data. Calls view_thread when complete."""
|
||||
while True:
|
||||
comment = input("Enter your comment (or leave empty to abort): ")
|
||||
if comment == "":
|
||||
input("Reply aborted. Hit [Enter] to continue.")
|
||||
break
|
||||
if not is_valid_input(comment):
|
||||
print(
|
||||
"Entries consisting of whitespace, or containing pipes, '|', are "
|
||||
"not valid.Please try again."
|
||||
)
|
||||
else:
|
||||
record = data.LinkDataRecord(
|
||||
username=getpass.getuser(),
|
||||
timestamp=str(time()),
|
||||
parent_id=parent_id,
|
||||
link_title_or_comment=comment,
|
||||
)
|
||||
LinkData.add(record)
|
||||
input("Reply added. Hit [Enter] to return to thread.")
|
||||
break
|
||||
def new_reply(stdscr, post_id):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
|
||||
# def reply(parent_id):
|
||||
# """Prompt for reply, validate input, save validated input to disk and update
|
||||
# link_data. Calls view_thread when complete."""
|
||||
# while True:
|
||||
# comment = input("Enter your comment (or leave empty to abort): ")
|
||||
# if comment == "":
|
||||
# input("Reply aborted. Hit [Enter] to continue.")
|
||||
# break
|
||||
# if not is_valid_input(comment):
|
||||
# print(
|
||||
# "Entries consisting of whitespace, or containing pipes, '|', are "
|
||||
# "not valid.Please try again."
|
||||
# )
|
||||
# else:
|
||||
# record = data.LinkDataRecord(
|
||||
# username=getpass.getuser(),
|
||||
# timestamp=str(time()),
|
||||
# parent_id=parent_id,
|
||||
# link_title_or_comment=comment,
|
||||
# )
|
||||
# LinkData.add(record)
|
||||
# input("Reply added. Hit [Enter] to return to thread.")
|
||||
# break
|
||||
|
||||
|
||||
def is_valid_input(entry: str) -> bool:
|
||||
|
@ -339,6 +339,9 @@ def post_link() -> int:
|
|||
|
||||
|
||||
class Output:
|
||||
"""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
|
||||
|
@ -348,10 +351,9 @@ class Output:
|
|||
self.scrollminy = 0
|
||||
self.viewslice = None
|
||||
|
||||
def Calc(self):
|
||||
def Calc_Dimensions(self):
|
||||
if not self.content:
|
||||
raise ValueError
|
||||
# TODO: ???
|
||||
raise ValueError("Can't calculate nonexistant data")
|
||||
self.length = len(self.content)
|
||||
if self.thead:
|
||||
self.miny = 3
|
||||
|
@ -361,7 +363,10 @@ class Output:
|
|||
self.viewslice = slice(self.scrollminy, self.scrollminy + self.maxy)
|
||||
|
||||
|
||||
class Level:
|
||||
class Menu:
|
||||
"""Class describing the data contained in each menu level. A level is like a
|
||||
menu screen in a hierarchy."""
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.data = None
|
||||
|
@ -369,80 +374,83 @@ class Level:
|
|||
self.selected_key = None
|
||||
|
||||
|
||||
class Menu:
|
||||
class MenuHierarchy:
|
||||
"""Class containing all menus, describing their hierarchy and methods to
|
||||
handle movement between levels"""
|
||||
|
||||
def __init__(self):
|
||||
# main levels: categories -> links -> post
|
||||
categories = Level("categories")
|
||||
links = Level("links")
|
||||
post = Level("post")
|
||||
self.main_levels = [categories, links, post]
|
||||
# primary menu hierarchy: categories -> links -> post
|
||||
categories = Menu("categories")
|
||||
links = Menu("links")
|
||||
post = Menu("post")
|
||||
self.primary_hierarchy = [categories, links, post]
|
||||
|
||||
# search levels: search_results -> found_post
|
||||
search_results = Level("search_results")
|
||||
found_post = Level("found_post")
|
||||
self.search_levels = [search_results, found_post]
|
||||
# search menu hierarchy: search_results -> found_post
|
||||
search_results = Menu("search_results")
|
||||
found_post = Menu("found_post")
|
||||
self.search_hierarchy = [search_results, found_post]
|
||||
|
||||
self.is_main_level = True
|
||||
self.is_viewing_primary_hierarchy = True
|
||||
|
||||
self.main_level_index = 0
|
||||
self.search_level_index = 0
|
||||
self.current_level = self.main_levels[self.main_level_index]
|
||||
self.primary_index = 0
|
||||
self.search_index = 0
|
||||
self.active = self.primary_hierarchy[self.primary_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]
|
||||
"""goes back up the menu hierarchy"""
|
||||
if self.is_viewing_primary_hierarchy:
|
||||
if self.primary_index > 0:
|
||||
self.primary_index -= 1
|
||||
self.active = self.primary_hierarchy[self.primary_index]
|
||||
else:
|
||||
if self.search_level_index > 0:
|
||||
self.search_level_index -= 1
|
||||
self.current_level = self.search_levels[self.search_level_index]
|
||||
if self.search_index > 0:
|
||||
self.search_index -= 1
|
||||
self.active = self.search_hierarchy[self.search_index]
|
||||
else:
|
||||
self.current_level = self.main_levels[self.main_level_index]
|
||||
self.active = self.primary_hierarchy[self.primary_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]
|
||||
"""returns forward through previously visited menu hierarchy"""
|
||||
if self.is_viewing_primary_hierarchy:
|
||||
if self.primary_index < len(self.primary_hierarchy) - 1:
|
||||
if self.primary_hierarchy[self.primary_index + 1].data:
|
||||
self.primary_index += 1
|
||||
self.active = self.primary_hierarchy[self.primary_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]
|
||||
if self.search_index < len(self.search_hierarchy) - 1:
|
||||
if self.search_hierarchy[self.search_index + 1].data:
|
||||
self.search_index += 1
|
||||
self.active = self.search_hierarchy[self.search_index]
|
||||
|
||||
def get_data(self, LinkData):
|
||||
"""fill out current level data based on level settings"""
|
||||
if self.current_level.name == "categories":
|
||||
self.current_level.data = LinkData.categories
|
||||
def set_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.current_level.output.thead,
|
||||
self.current_level.output.content,
|
||||
) = print_categories(self.current_level.data, curses.COLS)
|
||||
self.active.output.thead,
|
||||
self.active.output.content,
|
||||
) = view_categories(self.active.data, curses.COLS)
|
||||
|
||||
elif self.current_level.name == "links":
|
||||
self.current_level.data = LinkData.get_links_by_category_name(
|
||||
self.current_level.selected_key
|
||||
elif self.active.name == "links":
|
||||
self.primary_hierarchy[self.primary_index + 1].data = None
|
||||
self.active.data = LinkData.get_links_by_category_name(
|
||||
self.active.selected_key
|
||||
)
|
||||
(
|
||||
self.current_level.output.thead,
|
||||
self.current_level.output.content,
|
||||
) = print_links(self.current_level.data, curses.COLS)
|
||||
self.active.output.thead,
|
||||
self.active.output.content,
|
||||
) = view_links(self.active.data, self.active.selected_key, curses.COLS)
|
||||
|
||||
elif self.current_level.name == "post":
|
||||
self.current_level.data = LinkData.get_post(self.current_level.selected_key)
|
||||
self.current_level.output.content = print_post(
|
||||
self.current_level.data, 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)
|
||||
|
||||
elif self.current_level.name == "search_results":
|
||||
elif self.active.name == "search_results":
|
||||
pass
|
||||
elif self.current_level.name == "search_results_thread_details":
|
||||
elif self.active.name == "search_results_thread_details":
|
||||
pass
|
||||
else:
|
||||
raise ValueError
|
||||
# TODO: if this fails it's uninitialised or something
|
||||
raise ValueError("This shouldn't happen?")
|
||||
|
||||
|
||||
class Status:
|
||||
|
@ -461,15 +469,18 @@ def term_resize(stdscr):
|
|||
curses.update_lines_cols()
|
||||
|
||||
|
||||
def new_main_menu(stdscr):
|
||||
def menu_system(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)
|
||||
|
||||
title = "Linkulator".encode(code)
|
||||
|
||||
menu = Menu()
|
||||
menu.get_data(LinkData)
|
||||
menu.current_level.output.Calc()
|
||||
menus = MenuHierarchy()
|
||||
menus.set_active_menu_data(LinkData)
|
||||
menus.active.output.Calc_Dimensions()
|
||||
|
||||
status = Status()
|
||||
status.message = ""
|
||||
|
@ -487,27 +498,27 @@ def new_main_menu(stdscr):
|
|||
# stdscr.addstr(1, 4, "All categories".encode(code))
|
||||
|
||||
# print header
|
||||
if menu.current_level.output.thead:
|
||||
if menus.active.output.thead:
|
||||
stdscr.addstr(
|
||||
menu.current_level.output.miny - 1,
|
||||
menus.active.output.miny - 1,
|
||||
0,
|
||||
menu.current_level.output.thead.encode(code),
|
||||
menus.active.output.thead.encode(code),
|
||||
)
|
||||
stdscr.clrtoeol()
|
||||
|
||||
# print body
|
||||
menu.current_level.output.Calc()
|
||||
menus.active.output.Calc_Dimensions()
|
||||
|
||||
count = 0
|
||||
for i, line in enumerate(
|
||||
menu.current_level.output.content[menu.current_level.output.viewslice]
|
||||
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.addstr(i + menu.current_level.output.miny, 0, line.encode(code))
|
||||
stdscr.addstr(i + menus.active.output.miny, 0, line.encode(code))
|
||||
stdscr.clrtoeol()
|
||||
|
||||
# this is meant to clear to the second last line, but...
|
||||
|
@ -540,21 +551,22 @@ def new_main_menu(stdscr):
|
|||
term_resize(stdscr)
|
||||
elif k in [":", " "]:
|
||||
iwin = curses.newwin(1, curses.COLS, curses.LINES - 1, 0)
|
||||
iwin.addch(0,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)
|
||||
handle_action(menus, action[1:], status)
|
||||
curses.curs_set(0)
|
||||
del itxt
|
||||
del iwin
|
||||
else:
|
||||
doAction(menu, k, status)
|
||||
handle_action(menus, k, status)
|
||||
|
||||
def doAction(menu, action, status):
|
||||
|
||||
def handle_action(menus, action, status):
|
||||
int_action = None
|
||||
try:
|
||||
int_action = int(action)
|
||||
|
@ -564,69 +576,70 @@ def doAction(menu, action, status):
|
|||
|
||||
if int_action:
|
||||
try:
|
||||
navigate(menu, int_action)
|
||||
navigate(menus, int_action)
|
||||
except Exception as e:
|
||||
status.message = str(e)
|
||||
else:
|
||||
try:
|
||||
handle_command(menu, action)
|
||||
do_command(menus, action)
|
||||
except Exception as e:
|
||||
status.message = str(e)
|
||||
|
||||
|
||||
def navigate(menu, int_action):
|
||||
def navigate(menus, int_action):
|
||||
if int_action:
|
||||
if menu.current_level.name == "categories":
|
||||
if menus.active.name == "categories":
|
||||
try:
|
||||
key = menu.current_level.data[int_action - 1]["name"]
|
||||
key = menus.active.data[int_action - 1]["name"]
|
||||
except IndexError:
|
||||
raise IndexError("Sorry, that category doesn't exist")
|
||||
return
|
||||
elif menu.current_level.name == "links":
|
||||
elif menus.active.name == "links":
|
||||
try:
|
||||
key = menu.current_level.data[int_action - 1]["post_id"]
|
||||
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
|
||||
menu.forward()
|
||||
menu.current_level.selected_key = key
|
||||
menu.get_data(LinkData)
|
||||
menu.current_level.output.Calc()
|
||||
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.Calc_Dimensions()
|
||||
|
||||
|
||||
def handle_command(menu, action):
|
||||
def do_command(menus, 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
|
||||
if menus.active.output.scrollminy <= (
|
||||
menus.active.output.length - menus.active.output.maxy
|
||||
):
|
||||
menu.current_level.output.scrollminy += 1
|
||||
menus.active.output.scrollminy += 1
|
||||
elif action in ["k", "KEY_UP"]:
|
||||
if menu.current_level.output.scrollminy > 0:
|
||||
menu.current_level.output.scrollminy -= 1
|
||||
if menus.active.output.scrollminy > 0:
|
||||
menus.active.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()
|
||||
menus.back()
|
||||
elif action in ["f", "forward"]:
|
||||
menus.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
|
||||
# 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
|
||||
|
@ -635,13 +648,13 @@ def handle_command(menu, action):
|
|||
# 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 [
|
||||
elif action in ["r", "reply"] and menus.active.name in [
|
||||
"search_result_thread_details",
|
||||
"thread_details",
|
||||
]:
|
||||
# reply(thread_details["postid"])
|
||||
pass
|
||||
elif action in ["o", "open"] and menu.current_level.name in [
|
||||
# TODO: need stdscr here
|
||||
new_reply(menus.active.selected_key)
|
||||
elif action in ["o", "open"] and menus.active.name in [
|
||||
"search_result_thread_details",
|
||||
"thread_details",
|
||||
]:
|
||||
|
@ -651,46 +664,12 @@ def handle_command(menu, action):
|
|||
# is o the right command?
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
## GENERAL
|
||||
|
||||
|
||||
def style_text(text: str, is_input: bool, *args) -> str:
|
||||
"""Style input strings as specified using terminal escape sequences. Returns a styled string"""
|
||||
# TODO: not sure if this function will work with curses
|
||||
styles = {
|
||||
"bold": "\033[1m",
|
||||
"dim": "\033[2m",
|
||||
|
@ -712,11 +691,6 @@ def style_text(text: str, is_input: bool, *args) -> str:
|
|||
return out
|
||||
|
||||
|
||||
def print_banner():
|
||||
"""prints a banner"""
|
||||
return " LINKULATOR ".encode(code)
|
||||
|
||||
|
||||
def graceful_exit():
|
||||
"""Prints a nice message, performs cleanup and then exits"""
|
||||
config.USER.save()
|
||||
|
@ -736,7 +710,7 @@ def main(stdscr):
|
|||
args = sys.argv[1:]
|
||||
config.init()
|
||||
if not args:
|
||||
new_main_menu(stdscr)
|
||||
menu_system(stdscr)
|
||||
elif args[0] in ["-h", "--help", "help"]:
|
||||
curses.endwin()
|
||||
print(HELP_TEXT)
|
||||
|
|
|
@ -25,7 +25,7 @@ class TestDataHelperFunctions(unittest.TestCase):
|
|||
self.assertEqual(data.wash_line(line["Test"]), line["Result"])
|
||||
|
||||
def test_is_well_formed_line(self):
|
||||
""" tests the data.is_well_formed_line function"""
|
||||
"""tests the data.is_well_formed_line function"""
|
||||
teststrings = [
|
||||
{"Test": "A line of text", "Result": False},
|
||||
{"Test": "1 Pipe |", "Result": False},
|
||||
|
|
|
@ -2,16 +2,15 @@
|
|||
"""Tests for Linkulator views"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch, call
|
||||
import linkulator
|
||||
|
||||
|
||||
# class TestPrintSearchResults(unittest.TestCase):
|
||||
# """Tests covering the print_search_results function"""
|
||||
# TODO: update to support list of strings output
|
||||
# class TestViewSearchResults(unittest.TestCase):
|
||||
# """Tests covering the view_search_results function"""
|
||||
|
||||
# @patch("builtins.print")
|
||||
# def test_print_search_results(self, mock_print):
|
||||
# """tests that the search results are printed correctly"""
|
||||
# """tests that the search results are produced correctly"""
|
||||
# test_keyword = "keyword"
|
||||
# test_search_results = [
|
||||
# (66, "keyword", "1576461366.5580268", "", "c", "c", "c"),
|
||||
|
@ -19,7 +18,7 @@ import linkulator
|
|||
# (64, "poster7", "1576461368.5580268", "", "c", "keyword", "c"),
|
||||
# (63, "poster8", "1576461369.5580268", "", "c", "c", "keyword"),
|
||||
# ]
|
||||
# test_print_calls = [
|
||||
# expected_output = [
|
||||
# call(
|
||||
# "\nShowing results for keyword\n\n ID# DATE AUTHOR DESC "
|
||||
# ),
|
||||
|
@ -30,18 +29,18 @@ import linkulator
|
|||
# call(""),
|
||||
# ]
|
||||
#
|
||||
# linkulator.print_search_results(test_keyword, test_search_results)
|
||||
# linkulator.view_search_results(test_keyword, test_search_results)
|
||||
#
|
||||
# self.assertEqual(
|
||||
# mock_print.call_count, 6
|
||||
# ) # one count for title, 4 for the items and a blank line for formatting
|
||||
#
|
||||
# self.assertListEqual(test_print_calls, mock_print.call_args_list)
|
||||
# self.assertListEqual(test_view_calls, mock_print.call_args_list)
|
||||
|
||||
|
||||
class TestPrintCategories(unittest.TestCase):
|
||||
def test_print_categories(self):
|
||||
"""Test general output of print_categories"""
|
||||
class TestViewCategories(unittest.TestCase):
|
||||
def test_view_categories(self):
|
||||
"""Test general output of view_categories"""
|
||||
|
||||
categories = [
|
||||
{
|
||||
|
@ -70,7 +69,7 @@ class TestPrintCategories(unittest.TestCase):
|
|||
" 3 * long category name that will be truncated because it's a long... (20)",
|
||||
]
|
||||
|
||||
actual_header, actual_content = linkulator.print_categories(categories, cols)
|
||||
actual_header, actual_content = linkulator.view_categories(categories, cols)
|
||||
|
||||
# confirm expected is equal to actual
|
||||
self.assertEqual(expected_header, actual_header)
|
||||
|
@ -91,7 +90,7 @@ class TestPrintCategories(unittest.TestCase):
|
|||
"",
|
||||
"There are no posts yet - enter p to post a new link",
|
||||
]
|
||||
actual_header, actual_content = linkulator.print_categories(
|
||||
actual_header, actual_content = linkulator.view_categories(
|
||||
empty_categories, cols
|
||||
)
|
||||
|
||||
|
@ -104,9 +103,9 @@ class TestPrintCategories(unittest.TestCase):
|
|||
self.assertTrue(content_max_cols <= cols)
|
||||
|
||||
|
||||
class TestPrintLinks(unittest.TestCase):
|
||||
def test_print_links(self):
|
||||
"""Test general output of print_links"""
|
||||
class TestViewLinks(unittest.TestCase):
|
||||
def test_view_links(self):
|
||||
"""Test general output of view_links"""
|
||||
|
||||
links = [
|
||||
{
|
||||
|
@ -130,15 +129,16 @@ class TestPrintLinks(unittest.TestCase):
|
|||
]
|
||||
|
||||
cols = 80
|
||||
expected_header = (
|
||||
" ID# Date Author #Repl Description"
|
||||
)
|
||||
category_name = "Test Name"
|
||||
expected_header = " Test Name\n 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...*",
|
||||
]
|
||||
|
||||
actual_header, actual_content = linkulator.print_links(links, cols)
|
||||
actual_header, actual_content = linkulator.view_links(
|
||||
links, category_name, cols
|
||||
)
|
||||
|
||||
# confirm expected is equal to actual
|
||||
self.assertEqual(expected_header, actual_header)
|
||||
|
@ -151,9 +151,9 @@ class TestPrintLinks(unittest.TestCase):
|
|||
self.assertTrue(content_max_cols <= cols)
|
||||
|
||||
|
||||
class TestPrintPost(unittest.TestCase):
|
||||
class TestViewPost(unittest.TestCase):
|
||||
def test_post_without_reply(self):
|
||||
"""Test print_post where the post has no reply"""
|
||||
"""Test view_post where the post has no reply"""
|
||||
|
||||
post = {
|
||||
"author": "post author 1",
|
||||
|
@ -175,7 +175,7 @@ class TestPrintPost(unittest.TestCase):
|
|||
"\n No replies yet. Be the first!",
|
||||
]
|
||||
|
||||
actual_content = linkulator.print_post(post, cols)
|
||||
actual_content = linkulator.view_post(post, cols)
|
||||
|
||||
# confirm expected is equal to actual
|
||||
self.assertListEqual(actual_content, expected_content)
|
||||
|
@ -185,7 +185,7 @@ class TestPrintPost(unittest.TestCase):
|
|||
self.assertTrue(content_max_cols <= cols)
|
||||
|
||||
def test_post_with_reply(self):
|
||||
"""Test print_post where the post has a reply"""
|
||||
"""Test view_post where the post has a reply"""
|
||||
|
||||
post = {
|
||||
"author": "author2",
|
||||
|
@ -228,7 +228,7 @@ 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",
|
||||
]
|
||||
|
||||
actual_content = linkulator.print_post(post, cols)
|
||||
actual_content = linkulator.view_post(post, cols)
|
||||
|
||||
# confirm expected is equal to actual
|
||||
self.assertListEqual(actual_content, expected_content)
|
||||
|
|
Loading…
Reference in New Issue