thread composure in external editors
parent
7ee64d0e6d
commit
be41b2bba6
|
@ -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)
|
network = BBJ(host="127.0.0.1", port=7099)
|
||||||
|
except URLError as e:
|
||||||
|
exit("\033[0;31m%s\033[0m" % repr(e))
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
Loading…
Reference in New Issue