thread composure in external editors

pull/4/head
Blake DeMarcy 2017-04-07 14:13:12 -05:00
parent 7ee64d0e6d
commit be41b2bba6
1 changed files with 269 additions and 55 deletions

View File

@ -1,11 +1,21 @@
# -*- fill-column: 72 -*-
from time import time, sleep, localtime from time import time, sleep, localtime
from network import BBJ, URLError
from string import punctuation from string import punctuation
from subprocess import run from subprocess import run
from random import choice from random import choice
from network import BBJ import tempfile
import urwid import urwid
import json
import os
try:
network = BBJ(host="127.0.0.1", port=7099)
except URLError as e:
exit("\033[0;31m%s\033[0m" % repr(e))
network = BBJ(host="127.0.0.1", port=7099)
obnoxious_logo = """ obnoxious_logo = """
OwO 8 888888888o 8 888888888o 8 8888 1337 OwO 8 888888888o 8 888888888o 8 8888 1337
@ -34,11 +44,21 @@ colors = [
] ]
class App: editors = ["nano", "emacs", "vim", "micro", "ed", "joe"]
# defaults to internal editor, integrates the above as well
default_prefs = {
"editor": False,
"dramatic_exit": True
}
class App(object):
def __init__(self): def __init__(self):
self.mode = None self.mode = None
self.thread = None self.thread = None
self.usermap = {} self.usermap = {}
self.prefs = bbjrc("load")
self.window_split = False
colors = [ colors = [
("bar", "light magenta", "default"), ("bar", "light magenta", "default"),
("button", "light red", "default"), ("button", "light red", "default"),
@ -57,28 +77,44 @@ class App:
urwid.LineBox(ActionBox(urwid.SimpleFocusListWalker([])), urwid.LineBox(ActionBox(urwid.SimpleFocusListWalker([])),
title="> > T I L D E T O W N < <", title="> > T I L D E T O W N < <",
tlcorner="@", tline="=", lline="|", rline="|", tlcorner="@", tline="=", lline="|", rline="|",
bline="_", trcorner="@", brcorner="@", blcorner="@" bline="=", trcorner="@", brcorner="@", blcorner="@"
)), colors) )), colors)
self.walker = self.loop.widget.body.base_widget.body
self.date_format = "{1}/{2}/{0}" self.date_format = "{1}/{2}/{0}"
self.index() self.index()
def set_header(self, text, *format_specs): def set_header(self, text, *format_specs):
"""
Update the header line with the logged in user, a seperator,
then concat text with format_specs applied to it. Applies
bar formatting to it.
"""
self.loop.widget.header = urwid.AttrMap(urwid.Text( self.loop.widget.header = urwid.AttrMap(urwid.Text(
("%s@bbj | " % (network.user_name or "anonymous")) ("%s@bbj | " % (network.user_name or "anonymous"))
+ text.format(*format_specs) + text.format(*format_specs)
), "bar") ), "bar")
def set_footer(self, *controls): def set_footer(self, *controls, static_string=""):
"""
Sets the footer, emphasizing the first character of each string
argument passed to it. Used to show controls to the user. Applies
bar formatting.
"""
text = str() text = str()
for control in controls: for control in controls:
text += "[{}]{} ".format(control[0], control[1:]) text += "[{}]{} ".format(control[0], control[1:])
text += static_string
self.loop.widget.footer = urwid.AttrMap(urwid.Text(text), "bar") self.loop.widget.footer = urwid.AttrMap(urwid.Text(text), "bar")
def readable_delta(self, modified): def readable_delta(self, modified):
"""
Return a human-readable string representing the difference
between a given epoch time and the current time.
"""
delta = time() - modified delta = time() - modified
hours, remainder = divmod(delta, 3600) hours, remainder = divmod(delta, 3600)
if hours > 48: if hours > 48:
@ -89,87 +125,238 @@ class App:
return "about an hour ago" return "about an hour ago"
minutes, remainder = divmod(remainder, 60) minutes, remainder = divmod(remainder, 60)
if minutes > 1: if minutes > 1:
return "%d minutes ago" return "%d minutes ago" % minutes
return "less than a minute ago" return "less than a minute ago"
def make_message_body(self, message):
name = urwid.Text("~{}".format(self.usermap[message["author"]]["user_name"]))
info = "@ " + self.date_format.format(*localtime(message["created"]))
if message["edited"]:
info += " [edited]"
post = str(message["post_id"])
pile = urwid.Pile([
urwid.Columns([
(2 + len(post), urwid.AttrMap(urwid.Text(">" + post), "button")),
(len(name._text) + 1,
urwid.AttrMap(name, str(self.usermap[message["author"]]["color"]))),
urwid.AttrMap(urwid.Text(info), "dim")
]),
urwid.Divider(),
MessageBody(message["body"]),
urwid.Divider(),
urwid.AttrMap(urwid.Divider("-"), "dim")
])
pile.message = message
return pile
def make_thread_body(self, thread):
button = cute_button(">>", self.thread_load, thread["thread_id"])
title = urwid.Text(thread["title"])
infoline = "by ~{} @ {} | last active {}".format(
self.usermap[thread["author"]]["user_name"],
self.date_format.format(*localtime(thread["created"])),
self.readable_delta(thread["last_mod"])
)
pile = urwid.Pile([
urwid.Columns([(3, urwid.AttrMap(button, "button")), title]),
urwid.AttrMap(urwid.Text(infoline), "dim"),
urwid.AttrMap(urwid.Divider("-"), "dim")
])
pile.thread = thread
return pile
def index(self): def index(self):
"""
Browse the index.
"""
self.mode = "index" self.mode = "index"
self.thread = None self.thread = None
self.window_split = False
threads, usermap = network.thread_index() threads, usermap = network.thread_index()
self.usermap.update(usermap) self.usermap.update(usermap)
self.set_header("{} threads", len(threads)) self.set_header("{} threads", len(threads))
self.set_footer("Refresh", "Compose", "Quit", "/Search", "?Help") self.set_footer("Refresh", "Compose", "Quit", "/Search", "?Help")
walker = self.loop.widget.body.base_widget.body self.walker.clear()
walker.clear()
for thread in threads: for thread in threads:
button = cute_button(">>", self.thread_load, thread["thread_id"]) self.walker.append(self.make_thread_body(thread))
title = urwid.Text(thread["title"])
last_mod = thread["last_mod"]
created = thread["created"]
infoline = "by ~{} @ {} | last active {}".format(
usermap[thread["author"]]["user_name"],
self.date_format.format(*localtime(created)),
self.readable_delta(last_mod)
)
[walker.append(element)
for element in [
urwid.Columns([(3, urwid.AttrMap(button, "button")), title]),
urwid.AttrMap(urwid.Text(infoline), "dim"),
urwid.AttrMap(urwid.Divider("-"), "dim")
]]
def thread_load(self, button, thread_id): def thread_load(self, button, thread_id):
"""
Open a thread
"""
self.mode = "thread" self.mode = "thread"
thread, usermap = network.thread_load(thread_id) thread, usermap = network.thread_load(thread_id)
self.usermap.update(usermap) self.usermap.update(usermap)
self.thread = thread self.thread = thread
walker = self.loop.widget.body.base_widget.body self.walker.clear()
walker.clear()
self.set_header("~{}: {}", self.set_header("~{}: {}",
usermap[thread["author"]]["user_name"], thread["title"]) usermap[thread["author"]]["user_name"], thread["title"])
self.set_footer("Compose", "Refresh", "\"Quote", "/Search", "<Top", ">End", "QBack") self.set_footer(
"Compose", "Refresh",
"\"Quote", "/Search",
"Top", "Bottom", "QBack"
)
for message in thread["messages"]: for message in thread["messages"]:
name = urwid.Text("~{}".format(usermap[message["author"]]["user_name"])) app.walker.append(self.make_message_body(message))
info = "@ " + self.date_format.format(*localtime(message["created"]))
if message["edited"]:
info += " [edited]" def refresh(self):
head = urwid.Columns([ if self.mode == "index":
(3, urwid.AttrMap(cute_button(">>"), "button")), self.index()
(len(name._text) + 1, urwid.AttrMap(name, str(usermap[message["author"]]["color"]))),
urwid.AttrMap(urwid.Text(info), "dim")
def footer_prompt(self, text, callback, *callback_args, extra_text=None):
text = "(%s)> " % text
widget = urwid.Columns([
(len(text), urwid.AttrMap(urwid.Text(text), "bar")),
FootPrompt(callback, *callback_args)
]) ])
[walker.append(element) if extra_text:
for element in [ widget = urwid.Pile([
head, urwid.Divider(), urwid.Text(message["body"]), urwid.AttrMap(urwid.Text(extra_text), "2"),
urwid.Divider(), urwid.AttrMap(urwid.Divider("-"), "dim") widget
]] ])
self.loop.widget.footer = widget
app.loop.widget.focus_position = "footer"
# def compose(self): def compose(self, title=None):
# if self.mode == "index": if self.mode == "index":
# feedback = "Starting a new thread..." if not title:
# elif self.mode == "thread": return self.footer_prompt("Title", self.compose)
# feedback = "Replying in "
try: network.validate("title", title)
except AssertionError as e:
return self.footer_prompt(
"Title", self.compose, extra_text=e.description)
if self.prefs["editor"]:
editor = ExternalEditor("thread_create", title=title)
footer = "[F1]Abort [Save and quit to submit your thread]"
else:
editor = urwid.Edit()
self.set_header('Composing "{}"', title)
self.set_footer(static_string=
"[F1]Abort [Save and quit to submit your thread]")
self.loop.widget = urwid.Overlay(
urwid.LineBox(editor, title=self.prefs["editor"]),
self.loop.widget,
align="center",
width=self.loop.screen_size[0] - 2,
valign="middle",
height=(self.loop.screen_size[1] - 4))
class MessageBody(urwid.Text):
pass
class FootPrompt(urwid.Edit):
def __init__(self, callback, *callback_args):
super(FootPrompt, self).__init__()
self.callback = callback
self.args = callback_args
def keypress(self, size, key):
if key != "enter":
return super(FootPrompt, self).keypress(size, key)
app.loop.widget.focus_position = "body"
app.set_footer()
self.callback(self.get_edit_text(), *self.args)
class ExternalEditor(urwid.Terminal):
def __init__(self, endpoint, **params):
self.file_descriptor, self.path = tempfile.mkstemp()
self.endpoint = endpoint
self.params = params
env = os.environ
env.update({"LANG": "POSIX"})
command = ["bash", "-c", "{} {}; echo Press any key to kill this window...".format(
app.prefs["editor"], self.path)]
super(ExternalEditor, self).__init__(command, env, app.loop)
def keypress(self, size, key):
if self.terminated:
if app.window_split:
app.window_split = False
app.loop.widget.focus_position = "body"
app.set_footer("this")
else:
app.loop.widget = app.loop.widget[0]
with open(self.file_descriptor) as _:
self.params.update({"body": _.read()})
network.request(self.endpoint, **self.params)
os.remove(self.path)
return app.refresh()
elif key != "f1":
return super(ExternalEditor, self).keypress(size, key)
app.loop.widget.focus_position = "body"
app.loop.widget.footer.set_title("press f1 to return to the editor")
class ActionBox(urwid.ListBox): class ActionBox(urwid.ListBox):
"""
The listwalker used by all the browsing pages. Handles keys.
"""
def keypress(self, size, key): def keypress(self, size, key):
super(ActionBox, self).keypress(size, key) super(ActionBox, self).keypress(size, key)
if key.lower() in ["j", "n"]: if key == "h":
app.submit_thread()
if key == "f1" and app.window_split:
app.loop.widget.focus_position = "footer"
app.loop.widget.footer.set_title("press F1 to focus the thread")
if key in ["j", "n", "ctrl n"]:
self._keypress_down(size) self._keypress_down(size)
elif key.lower() in ["k", "p"]:
elif key in ["k", "p", "ctrl p"]:
self._keypress_up(size) self._keypress_up(size)
elif key in ["J", "N"]:
for x in range(5):
self._keypress_down(size)
elif key in ["K", "P"]:
for x in range(5):
self._keypress_up(size)
elif key.lower() == "b":
self.change_focus(size, len(app.walker) - 1)
elif key.lower() == "t":
self.change_focus(size, 0)
elif key == "c":
app.compose()
elif key == "r":
app.refresh()
elif key.lower() == "q": elif key.lower() == "q":
if app.mode == "index": if app.mode == "index":
app.loop.stop() app.loop.stop()
# run("clear", shell=True) if app.prefs["dramatic_exit"]:
width, height = app.loop.screen_size width, height = app.loop.screen_size
for x in range(height - 1): for x in range(height - 1):
motherfucking_rainbows( motherfucking_rainbows(
@ -177,6 +364,9 @@ class ActionBox(urwid.ListBox):
) )
out = " ~~CoMeE BaCkK SooOn~~ 0000000" out = " ~~CoMeE BaCkK SooOn~~ 0000000"
motherfucking_rainbows(out.zfill(width)) motherfucking_rainbows(out.zfill(width))
else:
run("clear", shell=True)
motherfucking_rainbows("Come back soon! <3")
exit() exit()
else: app.index() else: app.index()
@ -318,6 +508,30 @@ def log_in():
motherfucking_rainbows("~~welcome to the party, %s!~~" % network.user_name) motherfucking_rainbows("~~welcome to the party, %s!~~" % network.user_name)
def bbjrc(mode, **params):
"""
Maintains a user a preferences file, setting or returning
values depending on `mode`.
"""
path = os.path.join(os.getenv("HOME"), ".bbjrc")
try:
with open(path, "r") as _in:
values = json.load(_in)
except FileNotFoundError:
values = default_prefs
with open(path, "w") as _out:
json.dump(values, _out)
if mode == "load":
return values
values.update(params)
with open(path, "w") as _out:
json.dump(values, _out)
return values
def main(): def main():
run("clear", shell=True) run("clear", shell=True)
motherfucking_rainbows(obnoxious_logo) motherfucking_rainbows(obnoxious_logo)