736 lines
24 KiB
Python
Executable File
736 lines
24 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Linkulator"""
|
|
|
|
## If this script contains bugs, blame cmccabe.
|
|
|
|
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, get_terminal_size
|
|
from math import ceil
|
|
|
|
import data
|
|
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
|
|
|
|
|
|
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[pages.current_slice]):
|
|
out += "{:4d} {} {} ({})\n".format(
|
|
i + 1,
|
|
"x" if record["last_updated"] >= config.USER.lastlogin else " ",
|
|
record["name"],
|
|
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(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
|
|
desclen = (
|
|
columns - 18 - namelen - 9 - 1
|
|
) # length of available space for the description field.
|
|
# 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
|
|
|
|
header = "\n{}\n\n {:>3s} {:>10s} {:<{namelen}s} {:<5} {:<s}".format(
|
|
style_text(view_cat.upper(), False, "bold"),
|
|
"ID#",
|
|
"DATE",
|
|
"AUTHOR",
|
|
"#RESP",
|
|
"DESC",
|
|
namelen=namelen,
|
|
)
|
|
out = ""
|
|
|
|
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(
|
|
i + 1,
|
|
_dt,
|
|
link["link_author"],
|
|
link["reply_count"],
|
|
desc,
|
|
newmarker,
|
|
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")
|
|
|
|
|
|
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()
|
|
line_wrapper = textwrap.TextWrapper(
|
|
initial_indent=" " * 2, subsequent_indent=" " * 21, width=columns
|
|
)
|
|
|
|
# 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 reply view
|
|
if thread_details["replies"]:
|
|
print("\n{}:\n".format(style_text("Replies", False, "underline")))
|
|
for line in thread_details["replies"]:
|
|
comment_author = line[1]
|
|
comment_date = datetime.fromtimestamp(float(line[2])).isoformat(
|
|
sep=" ", timespec="minutes"
|
|
)
|
|
comment = line[6]
|
|
print(
|
|
line_wrapper.fill(
|
|
"{} {}: {}".format(comment_date, comment_author, comment)
|
|
)
|
|
)
|
|
else:
|
|
print("\nNo replies yet. Be the first!")
|
|
|
|
print("")
|
|
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(
|
|
keyword, "ID#", "DATE", "AUTHOR", "DESC"
|
|
)
|
|
)
|
|
|
|
for display_index, record in enumerate(search_results, start=1):
|
|
date = datetime.fromtimestamp(float(record[2])).strftime("%Y-%m-%d")
|
|
author = record[1]
|
|
desc = record[6]
|
|
print("{:4d} {:<15s}{:<12s}{:<13s}".format(display_index, date, author, desc))
|
|
print("")
|
|
|
|
|
|
## CONTROLS
|
|
|
|
|
|
def search():
|
|
"""Control for the search function"""
|
|
while True:
|
|
print("")
|
|
keyword = input("Enter your search (or leave empty to cancel): ")
|
|
if keyword == "":
|
|
print("Search cancelled\n")
|
|
return
|
|
search_results = LinkData.search(keyword)
|
|
if not search_results:
|
|
print("No results found\n")
|
|
return
|
|
while True:
|
|
print_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"),
|
|
style_text("m", False, "underline"),
|
|
style_text("q", False, "underline"),
|
|
)
|
|
).lower()
|
|
if option == "q":
|
|
graceful_exit()
|
|
if option == "m":
|
|
return
|
|
if option == "s":
|
|
break
|
|
try:
|
|
if 1 <= int(option) <= len(search_results):
|
|
menu_view_thread_details(search_results[int(option) - 1][0])
|
|
else:
|
|
raise IndexError("Invalid post ID")
|
|
except (KeyError, ValueError, IndexError):
|
|
# Catch a Post ID that is not in the thread list or is not a number
|
|
print("{}".format(style_text("Invalid entry", False, "bold")))
|
|
|
|
|
|
def open_link_in_browser(url):
|
|
"""Attempts to view the specified URL in the configured browser"""
|
|
if which(config.USER.browser) is None:
|
|
print(
|
|
"Sorry, "
|
|
+ config.USER.browser
|
|
+ " is not installed on your system. Ask your sysadmin to install it."
|
|
)
|
|
return
|
|
|
|
url_scheme = urlparse(url).scheme
|
|
if url_scheme in ["gopher", "https", "http"]:
|
|
subprocess.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])
|
|
|
|
|
|
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:
|
|
"""Determine validity of an input string
|
|
|
|
>>> is_valid_input("valid")
|
|
True
|
|
>>> is_valid_input("Not|valid")
|
|
False
|
|
>>> is_valid_input(" ")
|
|
False
|
|
"""
|
|
if "|" in entry:
|
|
return False
|
|
if entry.strip() == "":
|
|
return False
|
|
return True
|
|
|
|
|
|
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 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 categories
|
|
else ", ".join(sorted(record["name"] for record in 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 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 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
|
|
)
|
|
print_categories(menu.current_level.data, menu.current_level.pages)
|
|
change_level(menu)
|
|
|
|
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:
|
|
# 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 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:
|
|
boilerplate_len = 10
|
|
pages = calculate_pages(len(category_details), boilerplate_len, pages)
|
|
pages = get_pages_current_slice(pages)
|
|
|
|
print_category_details(
|
|
category_details[current_page],
|
|
pages.current,
|
|
pages.count,
|
|
)
|
|
|
|
pages = interpret_commands(pages, "category_details")
|
|
|
|
if option == "q":
|
|
graceful_exit()
|
|
if option == "m":
|
|
return
|
|
if option == "s":
|
|
search()
|
|
continue
|
|
if option == "p":
|
|
post_id = post_link()
|
|
if post_id >= 0:
|
|
menu_view_thread_details(post_id)
|
|
continue
|
|
try:
|
|
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
|
|
print(
|
|
"{}\n\n".format(style_text("Invalid category ID/entry", False, "bold"))
|
|
)
|
|
|
|
|
|
def menu_view_thread_details(post_id):
|
|
"""Displays thread details, handles related navigation menu"""
|
|
option_text = "Type {} to reply, {} to view in {}, {} to search, {} to post a new link, {} to go back, or {} to quit: ".format(
|
|
style_text("r", True, "underline"),
|
|
style_text("b", True, "underline"),
|
|
config.USER.browser,
|
|
style_text("s", True, "underline"),
|
|
style_text("p", True, "underline"),
|
|
style_text("m", True, "underline"),
|
|
style_text("q", True, "underline"),
|
|
)
|
|
|
|
while True:
|
|
parent_id, post_url = print_thread_details(post_id)
|
|
option = input(option_text).lower()
|
|
if option == "m":
|
|
return
|
|
if option == "b":
|
|
view_link_in_browser(post_url)
|
|
continue
|
|
if option == "r":
|
|
reply(parent_id)
|
|
continue
|
|
if option == "s":
|
|
search()
|
|
continue
|
|
if option == "p":
|
|
post_id = post_link()
|
|
if post_id >= 0:
|
|
continue
|
|
break
|
|
if option == "q":
|
|
graceful_exit()
|
|
print("{}\n\n".format(style_text("Invalid entry", False, "bold")))
|
|
|
|
|
|
## GENERAL
|
|
|
|
|
|
def style_text(text: str, is_input: bool, *args) -> str:
|
|
"""Style input strings as specified using terminal escape sequences. Returns a styled string"""
|
|
styles = {
|
|
"bold": "\033[1m",
|
|
"dim": "\033[2m",
|
|
"underline": "\033[4m",
|
|
"blink": "\033[5m", # This is here if you REALLY need it...dont use it.
|
|
# (ctrl+shift+esc+E to enable evil mode.)
|
|
"inverse": "\033[7m", # Make fg and bg color swap
|
|
"rl_left_fix": "\001" if is_input else "",
|
|
"rl_right_fix": "\002" if is_input else "",
|
|
}
|
|
out = ""
|
|
for arg in args:
|
|
if arg in styles:
|
|
out += styles["rl_left_fix"]
|
|
out += styles[arg]
|
|
out += styles["rl_right_fix"]
|
|
out += text
|
|
out += "{}\033[0m{}".format(styles["rl_left_fix"], styles["rl_right_fix"])
|
|
return out
|
|
|
|
|
|
def print_banner():
|
|
"""prints a banner"""
|
|
print(" ----------")
|
|
print(" LINKULATOR")
|
|
print(" ----------")
|
|
|
|
|
|
def graceful_exit():
|
|
"""Prints a nice message, performs cleanup and then exits"""
|
|
print("\n\nThank you for linkulating. Goodbye.\n")
|
|
config.USER.save()
|
|
sys.exit(0)
|
|
|
|
|
|
def signal_handler(sig, frame):
|
|
"""handle signals, exiting on SIGINT"""
|
|
graceful_exit()
|
|
|
|
|
|
def main():
|
|
"""main function - handles argument parsing and calls menu system"""
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
args = sys.argv[1:]
|
|
config.init()
|
|
if not args:
|
|
menu_view_categories()
|
|
elif args[0] in ["-h", "--help", "help"]:
|
|
print(HELP_TEXT)
|
|
else:
|
|
print("Unknown command: {}".format(args[0]))
|
|
graceful_exit()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
HELP_TEXT = """
|
|
options: -h or --help; or no option to browse links.
|
|
|
|
Linkulator is a minimalist, commandline link aggregator for small, trusting shell communities.
|
|
|
|
A few important points about Linkulator:
|
|
* Your username is associated with everything you post. No real anonymity.
|
|
* You may ignore other users by adding their username to your ~/.linkulator/ignore file,
|
|
followed by an optional description of why you're ignoring them.
|
|
* Link post categories are created dynamically when you submit a link. Think before you
|
|
create a new category and don't litter the board.
|
|
* No files are stored centrally. Each users' contributions are stored in ~/.linkulator/,
|
|
meaning that you may always edit or delete your own files. Please don't use this ability
|
|
to deceive others.
|
|
* Link/reply threads disappear if the original link poster deletes their post; or if you
|
|
put their username in your ignore file.
|
|
* Your ~/.linkulator/linkulator.data file must be readable by others, or nobody else will
|
|
see your contributions.
|
|
* Linkulator may not work outside of Linux systems.
|
|
"""
|
|
main()
|