aboutsummaryrefslogtreecommitdiffstats
path: root/clients/urwid/main.py
diff options
context:
space:
mode:
authorBlake DeMarcy <ofunknowndescent@gmail.com>2017-04-07 14:13:12 -0500
committerBlake DeMarcy <ofunknowndescent@gmail.com>2017-04-07 14:13:12 -0500
commitbe41b2bba6fde46535804df1fd075aa5a1affc45 (patch)
treee84b212625aa2dd9aadd3d3b7c5017b7e4013623 /clients/urwid/main.py
parent7ee64d0e6d5dcc2d27c9c734e1afcc025a551480 (diff)
downloadbbj-be41b2bba6fde46535804df1fd075aa5a1affc45.tar.gz
thread composure in external editors
Diffstat (limited to 'clients/urwid/main.py')
-rw-r--r--clients/urwid/main.py324
1 files changed, 269 insertions, 55 deletions
diff --git a/clients/urwid/main.py b/clients/urwid/main.py
index 5c03a5a..bbf1164 100644
--- a/clients/urwid/main.py
+++ b/clients/urwid/main.py
@@ -1,11 +1,21 @@
+# -*- fill-column: 72 -*-
+
from time import time, sleep, localtime
+from network import BBJ, URLError
from string import punctuation
from subprocess import run
from random import choice
-from network import BBJ
+import tempfile
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 = """
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):
self.mode = None
self.thread = None
self.usermap = {}
+ self.prefs = bbjrc("load")
+ self.window_split = False
colors = [
("bar", "light magenta", "default"),
("button", "light red", "default"),
@@ -57,28 +77,44 @@ class App:
urwid.LineBox(ActionBox(urwid.SimpleFocusListWalker([])),
title="> > T I L D E T O W N < <",
tlcorner="@", tline="=", lline="|", rline="|",
- bline="_", trcorner="@", brcorner="@", blcorner="@"
+ bline="=", trcorner="@", brcorner="@", blcorner="@"
)), colors)
+ self.walker = self.loop.widget.body.base_widget.body
self.date_format = "{1}/{2}/{0}"
self.index()
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(
("%s@bbj | " % (network.user_name or "anonymous"))
+ text.format(*format_specs)
), "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()
for control in controls:
text += "[{}]{} ".format(control[0], control[1:])
+ text += static_string
self.loop.widget.footer = urwid.AttrMap(urwid.Text(text), "bar")
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
hours, remainder = divmod(delta, 3600)
if hours > 48:
@@ -89,94 +125,248 @@ class App:
return "about an hour ago"
minutes, remainder = divmod(remainder, 60)
if minutes > 1:
- return "%d minutes ago"
+ return "%d minutes ago" % minutes
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):
+ """
+ Browse the index.
+ """
self.mode = "index"
self.thread = None
+ self.window_split = False
threads, usermap = network.thread_index()
self.usermap.update(usermap)
self.set_header("{} threads", len(threads))
self.set_footer("Refresh", "Compose", "Quit", "/Search", "?Help")
- walker = self.loop.widget.body.base_widget.body
- walker.clear()
+ self.walker.clear()
for thread in threads:
- button = cute_button(">>", self.thread_load, thread["thread_id"])
- 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)
- )
+ self.walker.append(self.make_thread_body(thread))
- [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):
+ """
+ Open a thread
+ """
self.mode = "thread"
thread, usermap = network.thread_load(thread_id)
self.usermap.update(usermap)
self.thread = thread
- walker = self.loop.widget.body.base_widget.body
- walker.clear()
+ self.walker.clear()
self.set_header("~{}: {}",
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"]:
- name = urwid.Text("~{}".format(usermap[message["author"]]["user_name"]))
- info = "@ " + self.date_format.format(*localtime(message["created"]))
- if message["edited"]:
- info += " [edited]"
- head = urwid.Columns([
- (3, urwid.AttrMap(cute_button(">>"), "button")),
- (len(name._text) + 1, urwid.AttrMap(name, str(usermap[message["author"]]["color"]))),
- urwid.AttrMap(urwid.Text(info), "dim")
+ app.walker.append(self.make_message_body(message))
+
+
+ def refresh(self):
+ if self.mode == "index":
+ self.index()
+
+
+ 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)
+ ])
+
+ if extra_text:
+ widget = urwid.Pile([
+ urwid.AttrMap(urwid.Text(extra_text), "2"),
+ widget
])
- [walker.append(element)
- for element in [
- head, urwid.Divider(), urwid.Text(message["body"]),
- urwid.Divider(), urwid.AttrMap(urwid.Divider("-"), "dim")
- ]]
+ self.loop.widget.footer = widget
+ app.loop.widget.focus_position = "footer"
+
+
+ def compose(self, title=None):
+ if self.mode == "index":
+ if not title:
+ return self.footer_prompt("Title", self.compose)
+ try: network.validate("title", title)
+ except AssertionError as e:
+ return self.footer_prompt(
+ "Title", self.compose, extra_text=e.description)
- # def compose(self):
- # if self.mode == "index":
- # feedback = "Starting a new thread..."
- # elif self.mode == "thread":
- # feedback = "Replying in "
+ 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):
+ """
+ The listwalker used by all the browsing pages. Handles keys.
+ """
def keypress(self, 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)
- elif key.lower() in ["k", "p"]:
+
+ elif key in ["k", "p", "ctrl p"]:
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":
if app.mode == "index":
app.loop.stop()
- # run("clear", shell=True)
- width, height = app.loop.screen_size
- for x in range(height - 1):
- motherfucking_rainbows(
- "".join([choice([" ", choice(punctuation)]) for x in range(width)])
- )
- out = " ~~CoMeE BaCkK SooOn~~ 0000000"
- motherfucking_rainbows(out.zfill(width))
+ if app.prefs["dramatic_exit"]:
+ width, height = app.loop.screen_size
+ for x in range(height - 1):
+ motherfucking_rainbows(
+ "".join([choice([" ", choice(punctuation)]) for x in range(width)])
+ )
+ out = " ~~CoMeE BaCkK SooOn~~ 0000000"
+ motherfucking_rainbows(out.zfill(width))
+ else:
+ run("clear", shell=True)
+ motherfucking_rainbows("Come back soon! <3")
exit()
else: app.index()
@@ -318,6 +508,30 @@ def log_in():
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():
run("clear", shell=True)
motherfucking_rainbows(obnoxious_logo)
Un proyecto texto-plano.xyz