bbj/clients/urwid/main.py

1052 lines
34 KiB
Python
Raw Normal View History

2017-04-07 19:13:12 +00:00
# -*- fill-column: 72 -*-
2017-04-10 14:42:37 +00:00
"""
If you're looking for help on how to use the program, just press
? while its running. This mess will not help you.
Urwid aint my speed. Hell, making complex, UI-oriented programs
aint my speed. So some of this code is pretty messy. I stand by
it though, and it seems to be working rather well.
Most of the functionality is crammed in the App() class. Key
handling is found in the other subclasses for urwid widgets.
An instantiation of App() is casted as `app` globally and
the keypress methods will call into this global `app` object.
There are few additional functions that are defined outside
of the App class. They are delegated to the very bottom of
this file.
Please mail me (~desvox) for feedback and for any of your
"OH MY GOD WHY WOULD YOU DO THIS"'s or "PEP8 IS A THING"'s.
"""
2017-04-07 19:13:12 +00:00
from network import BBJ, URLError
2017-04-05 18:09:38 +00:00
from string import punctuation
2017-04-09 12:45:51 +00:00
from datetime import datetime
from time import time, sleep
2017-04-05 18:09:38 +00:00
from subprocess import run
2017-04-05 22:09:13 +00:00
from random import choice
2017-04-07 19:13:12 +00:00
import tempfile
2017-04-05 18:09:38 +00:00
import urwid
2017-04-07 19:13:12 +00:00
import json
import os
try:
network = BBJ(host="127.0.0.1", port=7099)
except URLError as e:
2017-04-09 12:45:51 +00:00
# print the connection error in red
2017-04-07 19:13:12 +00:00
exit("\033[0;31m%s\033[0m" % repr(e))
2017-04-05 18:09:38 +00:00
obnoxious_logo = """
2017-04-10 14:42:37 +00:00
% _ * ! *
% 8 888888888o % 8 888888888o . 8 8888
8 8888 `88. 8 8888 `88. _ ! 8 8888 &
^ 8 8888 `88 8 8888 `88 * 8 8888 _
8 8888 ,88 8 8888 ,88 8 8888
* 8 8888. ,88' 8 8888. ,88' ! 8 8888 "
8 8888888888 8 8888888888 8 8888 =
! 8 8888 `88. 8 8888 `88. 88. 8 8888
8 8888 88 8 8888 88 `88. | 8 888' '
> 8 8888. ,88' 8 8888. ,88' `88o. .8 88' .
8 888888888P 8 888888888P `Y888888 ' .
% %"""
2017-04-05 18:09:38 +00:00
welcome = """>>> Welcome to Bulletin Butter & Jelly! ------------------@
2017-04-10 14:42:37 +00:00
| BBJ is a persistent, chronologically ordered text |
| discussion board for tilde.town. You may log in, |
| register as a new user, or participate anonymously. |
|---------------------------------------------------------|
| \033[1;31mTo go anon, just press enter. Otherwise, give me a name\033[0m |
| \033[1;31m(registered or not)\033[0m |
2017-04-10 14:42:37 +00:00
@_________________________________________________________@
2017-04-05 18:09:38 +00:00
"""
colors = [
"\033[1;31m", "\033[1;33m", "\033[1;33m",
"\033[1;32m", "\033[1;34m", "\033[1;35m"
]
2017-04-10 14:02:08 +00:00
editors = ["nano", "vim", "emacs", "vim -u NONE", "emacs -Q", "micro", "ed", "joe"]
2017-04-07 19:13:12 +00:00
default_prefs = {
"editor": os.getenv("EDITOR", default="nano"),
"integrate_external_editor": True,
2017-04-08 10:09:50 +00:00
"dramatic_exit": True,
"date": "%Y/%m/%d",
2017-04-09 12:45:51 +00:00
"time": "%H:%M",
"frame_title": "> > T I L D E T O W N < <",
"max_text_width": 80
2017-04-07 19:13:12 +00:00
}
class App(object):
2017-04-05 18:09:38 +00:00
def __init__(self):
2017-04-08 10:09:50 +00:00
self.bars = {
2017-04-10 14:02:08 +00:00
"index": "[C]ompose [R]efresh [O]ptions [?]Help/More [Q]uit",
"thread": "[C]ompose [Q]Back [R]efresh [Enter]Post Options [/]Search [B/T]End [?]Help/More"
2017-04-08 10:09:50 +00:00
}
2017-04-05 18:09:38 +00:00
colors = [
2017-04-10 11:05:20 +00:00
("default", "default", "default"),
2017-04-05 21:33:25 +00:00
("bar", "light magenta", "default"),
("button", "light red", "default"),
2017-04-10 17:41:32 +00:00
("opt_prompt", "black", "light gray"),
("opt_header", "light cyan", "default"),
2017-04-10 14:02:08 +00:00
("hover", "light cyan", "default"),
2017-04-05 21:33:25 +00:00
("dim", "dark gray", "default"),
# map the bbj api color values for display
("0", "default", "default"),
2017-04-10 14:02:08 +00:00
("1", "dark red", "default"),
# sounds ugly but brown is as close as we can get to yellow without being bold
("2", "brown", "default"),
("3", "dark green", "default"),
("4", "dark blue", "default"),
("5", "dark cyan", "default"),
("6", "dark magenta", "default")
2017-04-05 18:09:38 +00:00
]
2017-04-05 21:33:25 +00:00
2017-04-08 10:09:50 +00:00
self.mode = None
self.thread = None
self.usermap = {}
self.prefs = bbjrc("load")
self.window_split = False
2017-04-07 20:40:39 +00:00
self.last_pos = 0
2017-04-08 10:09:50 +00:00
self.walker = urwid.SimpleFocusListWalker([])
self.loop = urwid.MainLoop(urwid.Frame(
2017-04-10 11:05:20 +00:00
urwid.LineBox(
ActionBox(self.walker),
2017-04-09 12:45:51 +00:00
title=self.prefs["frame_title"],
2017-04-10 11:05:20 +00:00
**frame_theme()
2017-04-08 10:09:50 +00:00
)), colors)
2017-04-05 18:09:38 +00:00
self.index()
def set_header(self, text, *format_specs):
2017-04-07 19:13:12 +00:00
"""
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.
"""
2017-04-09 12:45:51 +00:00
self.loop.widget.header = \
urwid.AttrMap(
urwid.Text(
("{}@bbj | " + text).format(
(network.user_name or "anonymous"),
*format_specs)),
"bar")
2017-04-05 18:09:38 +00:00
2017-04-08 10:09:50 +00:00
def set_footer(self, string):
2017-04-07 19:13:12 +00:00
"""
2017-04-08 10:09:50 +00:00
Sets the footer to display `string`, applying bar formatting.
Other than setting the color, `string` is shown verbatim.
2017-04-07 19:13:12 +00:00
"""
2017-04-09 12:45:51 +00:00
self.loop.widget.footer = urwid.AttrMap(urwid.Text(string), "bar")
2017-04-05 18:09:38 +00:00
2017-04-10 14:02:08 +00:00
def set_default_header(self):
"""
Sets the header to the default for the current screen.
"""
if self.mode == "thread":
name = self.usermap[self.thread["author"]]["user_name"]
self.set_header("~{}: {}", name, self.thread["title"])
else:
self.set_header("{} threads", len(self.walker))
def set_default_footer(self):
"""
Sets the footer to the default for the current screen.
"""
self.set_footer(self.bars[self.mode])
def set_bars(self):
"""
Sets both the footer and header to their default values
for the current mode.
"""
self.set_default_header()
self.set_default_footer()
2017-04-07 21:51:28 +00:00
def close_editor(self):
2017-04-08 10:09:50 +00:00
"""
Close whatever editing widget is open and restore proper
state back the walker.
"""
2017-04-07 21:51:28 +00:00
if self.window_split:
self.window_split = False
self.loop.widget.focus_position = "body"
2017-04-08 10:09:50 +00:00
self.set_footer(self.bars["thread"])
2017-04-07 21:51:28 +00:00
else:
self.loop.widget = self.loop.widget[0]
2017-04-10 14:02:08 +00:00
self.set_default_header()
2017-04-07 21:51:28 +00:00
def switch_editor(self):
2017-04-08 10:09:50 +00:00
"""
Switch focus between the thread viewer and the open editor
"""
2017-04-07 21:51:28 +00:00
if not self.window_split:
return
2017-04-08 10:09:50 +00:00
2017-04-07 21:51:28 +00:00
elif self.loop.widget.focus_position == "body":
self.loop.widget.focus_position = "footer"
2017-04-07 22:40:30 +00:00
focus = "[focused on editor]"
2017-04-09 12:45:51 +00:00
attr = ("bar", "dim")
2017-04-08 10:09:50 +00:00
2017-04-07 21:51:28 +00:00
else:
self.loop.widget.focus_position = "body"
2017-04-07 22:40:30 +00:00
focus = "[focused on thread]"
2017-04-09 12:45:51 +00:00
attr = ("dim", "bar")
2017-04-08 10:09:50 +00:00
control = "[save/quit to send]" if self.prefs["editor"] else "[F3]Send"
self.loop.widget.footer[0].set_text(
"[F1]Abort [F2]Swap %s %s" % (control, focus))
2017-04-09 12:45:51 +00:00
# this hideous and awful sinful horrid unspeakable shithack changes
# the color of the help/title lines and editor border to reflect which
# object is currently in focus.
2017-04-09 12:45:51 +00:00
self.loop.widget.footer.contents[1][0].original_widget.attr_map = \
self.loop.widget.footer.contents[0][0].attr_map = {None: attr[0]}
self.loop.widget.header.attr_map = {None: attr[1]}
2017-04-07 21:51:28 +00:00
2017-04-05 21:33:25 +00:00
def readable_delta(self, modified):
2017-04-07 19:13:12 +00:00
"""
Return a human-readable string representing the difference
between a given epoch time and the current time.
"""
2017-04-05 21:33:25 +00:00
delta = time() - modified
hours, remainder = divmod(delta, 3600)
if hours > 48:
2017-04-08 10:09:50 +00:00
return self.timestring(modified)
2017-04-05 21:33:25 +00:00
elif hours > 1:
2017-04-05 18:09:38 +00:00
return "%d hours ago" % hours
2017-04-05 21:33:25 +00:00
elif hours == 1:
return "about an hour ago"
minutes, remainder = divmod(remainder, 60)
if minutes > 1:
2017-04-07 19:13:12 +00:00
return "%d minutes ago" % minutes
2017-04-05 21:33:25 +00:00
return "less than a minute ago"
2017-04-05 18:09:38 +00:00
2017-04-07 19:13:12 +00:00
def make_message_body(self, message):
2017-04-10 14:02:08 +00:00
"""
Returns the widgets that comprise a message in a thread, including the
text body, author info and the action button
"""
2017-04-08 10:09:50 +00:00
info = "@ " + self.timestring(message["created"])
2017-04-07 19:13:12 +00:00
if message["edited"]:
info += " [edited]"
2017-04-09 12:45:51 +00:00
name = urwid.Text("~{}".format(self.usermap[message["author"]]["user_name"]))
2017-04-07 19:13:12 +00:00
post = str(message["post_id"])
2017-04-09 12:45:51 +00:00
head = urwid.Columns([
2017-04-10 14:02:08 +00:00
(2 + len(post), urwid.AttrMap(cute_button(">" + post), "button", "hover")),
2017-04-07 19:13:12 +00:00
(len(name._text) + 1,
2017-04-09 12:45:51 +00:00
urwid.AttrMap(name, str(self.usermap[message["author"]]["color"]))),
2017-04-07 19:13:12 +00:00
urwid.AttrMap(urwid.Text(info), "dim")
2017-04-09 12:45:51 +00:00
])
head.message = message
return [
head,
2017-04-07 19:13:12 +00:00
urwid.Divider(),
MessageBody(message["body"]),
urwid.Divider(),
urwid.AttrMap(urwid.Divider("-"), "dim")
2017-04-09 12:45:51 +00:00
]
2017-04-07 19:13:12 +00:00
2017-04-08 10:09:50 +00:00
def timestring(self, epoch, mode="both"):
2017-04-10 14:02:08 +00:00
"""
Returns a string of time representing a given epoch and mode.
"""
2017-04-08 10:09:50 +00:00
if mode == "delta":
return self.readable_delta(epoch)
date = datetime.fromtimestamp(epoch)
if mode == "time":
directive = self.prefs["time"]
elif mode == "date":
directive = self.prefs["date"]
else:
2017-04-09 12:45:51 +00:00
directive = "%s %s" % ( self.prefs["time"], self.prefs["date"])
2017-04-08 10:09:50 +00:00
return date.strftime(directive)
2017-04-07 19:13:12 +00:00
def make_thread_body(self, thread):
2017-04-10 14:02:08 +00:00
"""
Returns the pile widget that comprises a thread in the index.
"""
2017-04-07 19:13:12 +00:00
button = cute_button(">>", self.thread_load, thread["thread_id"])
title = urwid.Text(thread["title"])
2017-04-10 14:02:08 +00:00
user = self.usermap[thread["author"]]
dateline = [
("default", "by "),
(str(user["color"]), "~%s " % user["user_name"]),
("dim", "@ %s" % self.timestring(thread["created"]))
]
2017-04-07 19:13:12 +00:00
2017-04-08 10:09:50 +00:00
infoline = "%d replies; active %s" % (
thread["reply_count"], self.timestring(thread["last_mod"], "delta"))
2017-04-07 19:13:12 +00:00
pile = urwid.Pile([
2017-04-10 14:02:08 +00:00
urwid.Columns([(3, urwid.AttrMap(button, "button", "hover")), title]),
urwid.Text(dateline),
2017-04-07 19:13:12 +00:00
urwid.AttrMap(urwid.Text(infoline), "dim"),
urwid.AttrMap(urwid.Divider("-"), "dim")
])
pile.thread = thread
return pile
2017-04-05 18:09:38 +00:00
def index(self):
2017-04-07 19:13:12 +00:00
"""
Browse the index.
"""
2017-04-05 21:33:25 +00:00
self.mode = "index"
2017-04-05 22:09:13 +00:00
self.thread = None
2017-04-07 19:13:12 +00:00
self.window_split = False
2017-04-05 18:09:38 +00:00
threads, usermap = network.thread_index()
2017-04-05 22:09:13 +00:00
self.usermap.update(usermap)
2017-04-07 19:13:12 +00:00
self.walker.clear()
2017-04-05 18:09:38 +00:00
for thread in threads:
2017-04-07 19:13:12 +00:00
self.walker.append(self.make_thread_body(thread))
2017-04-10 14:02:08 +00:00
self.set_bars()
2017-04-08 10:09:50 +00:00
try: self.loop.widget.body.base_widget.set_focus(self.last_pos)
except IndexError:
pass
2017-04-05 18:09:38 +00:00
def thread_load(self, button, thread_id):
2017-04-07 19:13:12 +00:00
"""
2017-04-09 12:45:51 +00:00
Open a thread.
2017-04-07 19:13:12 +00:00
"""
2017-04-07 20:40:39 +00:00
if self.mode == "index":
self.last_pos = self.loop.widget.body.base_widget.get_focus()[1]
2017-04-05 21:33:25 +00:00
self.mode = "thread"
2017-04-05 18:09:38 +00:00
thread, usermap = network.thread_load(thread_id)
2017-04-05 22:09:13 +00:00
self.usermap.update(usermap)
self.thread = thread
2017-04-07 19:13:12 +00:00
self.walker.clear()
2017-04-09 12:45:51 +00:00
for message in thread["messages"]:
self.walker += self.make_message_body(message)
2017-04-10 14:02:08 +00:00
self.set_bars()
2017-04-07 19:13:12 +00:00
2017-04-10 11:05:20 +00:00
def refresh(self, bottom=False):
2017-04-07 19:13:12 +00:00
if self.mode == "index":
2017-04-08 10:09:50 +00:00
return self.index()
self.thread_load(None, self.thread["thread_id"])
2017-04-10 11:05:20 +00:00
if bottom:
self.loop.widget.body.base_widget.set_focus(len(self.walker) - 5)
2017-04-07 19:13:12 +00:00
2017-04-07 20:40:39 +00:00
def back(self):
2017-04-10 11:05:20 +00:00
if app.mode == "index":
frilly_exit()
elif app.mode == "thread":
2017-04-07 20:40:39 +00:00
self.index()
def set_new_editor(self, button, value, arg):
2017-04-10 14:02:08 +00:00
"""
Callback for the option radio buttons to set the the text editor.
"""
2017-04-10 11:05:20 +00:00
if value == False:
return
elif isinstance(value, str):
[button.set_state(False) for button in arg]
self.prefs["editor"] = value
bbjrc("update", **self.prefs)
return
key, widget = arg
widget.set_edit_text(key)
2017-04-10 11:05:20 +00:00
self.prefs.update({"editor": key})
bbjrc("update", **self.prefs)
def set_editor_mode(self, button, value):
2017-04-10 14:02:08 +00:00
"""
Callback for the editor mode radio buttons in the options.
"""
2017-04-10 11:05:20 +00:00
self.prefs["integrate_external_editor"] = value
bbjrc("update", **self.prefs)
2017-04-10 14:02:08 +00:00
def relog(self, *_, **__):
"""
Options menu callback to log the user in again.
Drops back to text mode because im too lazy to
write a responsive urwid thing for this.
"""
self.loop.widget = self.loop.widget[0]
self.loop.stop()
run("clear", shell=True)
print(welcome)
2017-04-10 16:13:41 +00:00
try: log_in()
except (KeyboardInterrupt, InterruptedError): pass
2017-04-10 14:02:08 +00:00
self.loop.start()
self.set_default_header()
self.options_menu()
def unlog(self, *_, **__):
"""
Options menu callback to anonymize the user and
then redisplay the options menu.
"""
network.user_name = network.user_auth = None
self.loop.widget = self.loop.widget[0]
self.set_default_header()
self.options_menu()
def set_color(self, button, value, color):
if value == False:
return
network.user_update(color=color)
2017-04-10 16:13:41 +00:00
def toggle_exit(self, button, value):
self.prefs["dramatic_exit"] = value
bbjrc("update", **self.prefs)
def change_username(self, *_):
self.loop.stop()
run("clear", shell=True)
def __loop(prompt, positive):
new_name = sane_value("user_name", prompt, positive)
if network.user_is_registered(new_name):
return __loop("%s is already registered" % new_name, False)
return new_name
try:
name = __loop("Choose a new username", True)
network.user_update(user_name=name)
motherfucking_rainbows("~~hello there %s~~" % name)
sleep(0.8)
self.loop.start()
self.loop.widget = self.loop.widget[0]
self.index()
self.options_menu()
except (KeyboardInterrupt, InterruptedError):
self.loop.start()
def change_password(self, *_):
self.loop.stop()
run("clear", shell=True)
def __loop(prompt, positive):
first = paren_prompt(prompt, positive)
if first == "":
confprompt = "Confirm empty password"
else:
confprompt = "Confirm it"
second = paren_prompt(confprompt)
if second != first:
return __loop("Those didnt match. Try again", False)
return first
try:
password = __loop("Choose a new password. Can be empty", True)
network.user_update(auth_hash=network._hash(password))
motherfucking_rainbows("SET NEW PASSWORD")
sleep(0.8)
self.loop.start()
self.loop.widget = self.loop.widget[0]
self.index()
self.options_menu()
except (KeyboardInterrupt, InterruptedError):
self.loop.start()
2017-04-10 17:41:32 +00:00
def live_time_render(self, editor, text, args):
widget, key = args
try:
rendered = datetime.fromtimestamp(time()).strftime(text)
self.prefs[key] = text
bbjrc("update", **self.prefs)
except:
rendered = ("1", "Invalid Input")
widget.set_text(rendered)
2017-04-10 11:05:20 +00:00
def options_menu(self):
2017-04-10 14:02:08 +00:00
"""
Create a popup for the user to configure their account and
display settings.
"""
2017-04-10 11:05:20 +00:00
editor_buttons = []
edit_mode = []
2017-04-10 14:02:08 +00:00
user_colors = []
if network.user_auth:
account_message = "Logged in as %s." % network.user_name
colors = ["None", "Red", "Yellow", "Green", "Blue", "Cyan", "Magenta"]
for index, color in enumerate(colors):
urwid.RadioButton(
user_colors, color,
network.user["color"] == index,
self.set_color, index)
account_stuff = [
2017-04-10 16:13:41 +00:00
urwid.Button("Relog", on_press=self.relog),
urwid.Button("Go anonymous", on_press=self.unlog),
urwid.Button("Change username", on_press=self.change_username),
urwid.Button("Change password", on_press=self.change_password),
2017-04-10 14:02:08 +00:00
urwid.Divider(),
urwid.Text(("button", "Your color:")),
*user_colors
]
else:
account_message = "You're browsing anonymously, and cannot set account preferences."
2017-04-10 16:13:41 +00:00
account_stuff = [urwid.Button("Login/Register", on_press=self.relog)]
2017-04-10 14:02:08 +00:00
2017-04-10 17:41:32 +00:00
time_box = urwid.Text(self.timestring(time(), "time"))
time_edit = urwid.Edit(edit_text=self.prefs["time"])
urwid.connect_signal(time_edit, "change", self.live_time_render, (time_box, "time"))
date_box = urwid.Text(self.timestring(time(), "date"))
2017-04-10 17:41:32 +00:00
date_edit = urwid.Edit(edit_text=self.prefs["date"])
urwid.connect_signal(date_edit, "change", self.live_time_render, (date_box, "date"))
time_stuff = [
urwid.Text(("button", "Time Format")),
time_box, urwid.AttrMap(time_edit, "opt_prompt"),
urwid.Divider(),
urwid.Text(("button", "Date Format")),
date_box, urwid.AttrMap(date_edit, "opt_prompt"),
]
2017-04-10 11:05:20 +00:00
editor_display = urwid.Edit(edit_text=self.prefs["editor"])
urwid.connect_signal(editor_display, "change", self.set_new_editor, editor_buttons)
2017-04-10 11:05:20 +00:00
for editor in editors:
urwid.RadioButton(
2017-04-10 14:02:08 +00:00
editor_buttons, editor,
2017-04-10 11:05:20 +00:00
state=self.prefs["editor"] == editor,
on_state_change=self.set_new_editor,
user_data=(editor, editor_display))
2017-04-10 11:05:20 +00:00
urwid.RadioButton(
edit_mode, "Integrate",
state=self.prefs["integrate_external_editor"],
on_state_change=self.set_editor_mode)
urwid.RadioButton(
edit_mode, "Overthrow",
state=not self.prefs["integrate_external_editor"])
widget = OptionsMenu(
urwid.ListBox(
urwid.SimpleFocusListWalker([
2017-04-10 16:13:41 +00:00
urwid.Text(("opt_header", "Account"), 'center'),
2017-04-10 14:02:08 +00:00
urwid.Text(account_message),
urwid.Divider(),
*account_stuff,
urwid.Divider("-"),
2017-04-10 16:13:41 +00:00
urwid.Text(("opt_header", "App"), 'center'),
2017-04-10 11:05:20 +00:00
urwid.Divider(),
2017-04-10 16:13:41 +00:00
urwid.CheckBox(
"Rainbow Vomit on Exit",
state=self.prefs["dramatic_exit"],
on_state_change=self.toggle_exit
),
2017-04-10 17:41:32 +00:00
urwid.Divider(),
*time_stuff,
urwid.Divider(),
2017-04-10 16:13:41 +00:00
urwid.Text(("button", "Text editor:")),
urwid.Text("You can type in your own command or use one of these presets."),
urwid.Divider(),
urwid.AttrMap(editor_display, "opt_prompt"),
2017-04-10 11:05:20 +00:00
*editor_buttons,
2017-04-10 16:13:41 +00:00
urwid.Divider(),
2017-04-10 14:02:08 +00:00
urwid.Text(("button", "External text editor mode:")),
2017-04-10 11:05:20 +00:00
urwid.Text("If you have problems using an external text editor, "
"set this to Overthrow."),
urwid.Divider(),
*edit_mode,
urwid.Divider("-"),
])
),
title="Options",
2017-04-10 11:05:20 +00:00
**frame_theme()
)
self.loop.widget = urwid.Overlay(
widget, self.loop.widget,
align="center",
valign="middle",
width=30,
height=(self.loop.screen_size[1] - 10)
)
2017-04-07 19:13:12 +00:00
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
2017-04-05 21:33:25 +00:00
])
2017-04-07 19:13:12 +00:00
self.loop.widget.footer = widget
2017-04-10 11:05:20 +00:00
self.loop.widget.focus_position = "footer"
2017-04-10 16:13:41 +00:00
def reset_footer(self, _, from_temp):
if from_temp and self.window_split:
return
2017-04-10 11:05:20 +00:00
try:
2017-04-10 16:13:41 +00:00
self.set_default_footer()
2017-04-10 11:05:20 +00:00
self.loop.widget.focus_position = "body"
except:
2017-04-10 16:13:41 +00:00
# just keep trying until the focus widget can handle it
2017-04-10 11:05:20 +00:00
self.loop.set_alarm_in(0.5, self.reset_footer)
2017-04-07 19:13:12 +00:00
def temp_footer_message(self, string, duration=3):
2017-04-10 16:13:41 +00:00
self.loop.set_alarm_in(duration, self.reset_footer, True)
self.set_footer(string)
def overthrow_ext_edit(self):
"""
Opens the external editor, but instead of integreating it into the app,
stops the mainloop and blocks until the editor is killed. Returns the
body of text the user composed.
"""
self.loop.stop()
descriptor, path = tempfile.mkstemp()
run("%s %s" % (self.prefs["editor"], path), shell=True)
with open(descriptor) as _:
body = _.read()
os.remove(path)
self.loop.start()
return body.strip()
2017-04-07 19:13:12 +00:00
def compose(self, title=None):
2017-04-10 16:13:41 +00:00
"""
Dispatches the appropriate composure mode and widget based on application
context and user preferences.
"""
if self.mode == "index" and not title:
return self.footer_prompt("Title", self.compose)
2017-04-05 21:33:25 +00:00
elif title:
2017-04-07 19:13:12 +00:00
try: network.validate("title", title)
except AssertionError as e:
return self.footer_prompt(
"Title", self.compose, extra_text=e.description)
2017-04-05 21:33:25 +00:00
if self.prefs["editor"] and not self.prefs["integrate_external_editor"]:
body = self.overthrow_ext_edit()
if not body:
return self.temp_footer_message("EMPTY POST DISCARDED")
params = {"body": body}
if self.mode == "thread":
2017-04-10 11:05:20 +00:00
endpoint = "reply"
params.update({"thread_id": self.thread["thread_id"]})
else:
2017-04-10 11:05:20 +00:00
endpoint = "create"
params.update({"title": title})
2017-04-10 11:05:20 +00:00
network.request("thread_" + endpoint, **params)
return self.refresh(True)
if self.mode == "index":
2017-04-07 19:13:12 +00:00
self.set_header('Composing "{}"', title)
self.set_footer("[F1]Abort [Save and quit to submit your thread]")
2017-04-07 19:13:12 +00:00
self.loop.widget = urwid.Overlay(
2017-04-07 21:51:28 +00:00
urwid.LineBox(
ExternalEditor("thread_create", title=title),
2017-04-10 11:05:20 +00:00
title=self.prefs["editor"] or "",
**frame_theme()),
self.loop.widget,
align="center",
valign="middle",
2017-04-07 19:13:12 +00:00
width=self.loop.screen_size[0] - 2,
height=(self.loop.screen_size[1] - 4))
2017-04-07 21:51:28 +00:00
elif self.mode == "thread":
self.window_split=True
self.set_header('Replying to "{}"', self.thread["title"])
2017-04-07 22:40:30 +00:00
self.loop.widget.footer = urwid.Pile([
urwid.AttrMap(urwid.Text(""), "bar"),
urwid.BoxAdapter(
urwid.AttrMap(
urwid.LineBox(
ExternalEditor("thread_reply", thread_id=self.thread["thread_id"]),
**frame_theme()
),
"bar"),
self.loop.screen_size[1] // 2),])
2017-04-07 21:51:28 +00:00
self.switch_editor()
2017-04-07 19:13:12 +00:00
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):
2017-04-10 16:13:41 +00:00
super(FootPrompt, self).keypress(size, key)
if key == "enter":
app.loop.widget.focus_position = "body"
app.set_default_footer()
self.callback(self.get_edit_text(), *self.args)
elif key.lower() in ["esc", "ctrl g", "ctrl c"]:
app.loop.widget.focus_position = "body"
app.set_default_footer()
2017-04-07 19:13:12 +00:00
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)]
2017-04-07 22:40:30 +00:00
super(ExternalEditor, self).__init__(command, env, app.loop, "f1")
2017-04-07 19:13:12 +00:00
def keypress(self, size, key):
if self.terminated:
2017-04-07 21:51:28 +00:00
app.close_editor()
2017-04-07 19:13:12 +00:00
with open(self.file_descriptor) as _:
self.params.update({"body": _.read().strip()})
2017-04-07 19:13:12 +00:00
os.remove(self.path)
if self.params["body"]:
network.request(self.endpoint, **self.params)
2017-04-10 11:05:20 +00:00
return app.refresh(True)
else:
return app.temp_footer_message("EMPTY POST DISCARDED")
2017-04-07 19:13:12 +00:00
2017-04-07 21:51:28 +00:00
elif key not in ["f1", "f2"]:
2017-04-07 19:13:12 +00:00
return super(ExternalEditor, self).keypress(size, key)
2017-04-07 22:40:30 +00:00
2017-04-07 21:51:28 +00:00
elif key == "f1":
self.terminate()
app.close_editor()
app.refresh()
2017-04-09 12:45:51 +00:00
# key == "f2"
2017-04-07 21:51:28 +00:00
app.switch_editor()
2017-04-05 18:09:38 +00:00
2017-04-10 11:05:20 +00:00
class OptionsMenu(urwid.LineBox):
def keypress(self, size, key):
2017-04-10 17:41:32 +00:00
if key == "esc":
app.loop.widget = app.loop.widget[0]
# try to let the base class handle the key, if not, we'll take over
elif not super(OptionsMenu, self).keypress(size, key):
return
elif key.lower() == "q":
2017-04-10 11:05:20 +00:00
app.loop.widget = app.loop.widget[0]
elif key in ["ctrl n", "j", "n"]:
return self.keypress(size, "down")
elif key in ["ctrl p", "k", "p"]:
return self.keypress(size, "up")
2017-04-05 18:09:38 +00:00
class ActionBox(urwid.ListBox):
2017-04-07 19:13:12 +00:00
"""
2017-04-10 11:05:20 +00:00
The listwalker used by all the browsing pages. Most of the application
takes place in an instance of this box. Handles many keybinds.
2017-04-07 19:13:12 +00:00
"""
2017-04-05 21:33:25 +00:00
def keypress(self, size, key):
super(ActionBox, self).keypress(size, key)
2017-04-07 20:40:39 +00:00
2017-04-07 21:51:28 +00:00
if key == "f2":
app.switch_editor()
2017-04-07 20:40:39 +00:00
elif key in ["j", "n", "ctrl n"]:
2017-04-05 21:33:25 +00:00
self._keypress_down(size)
2017-04-07 19:13:12 +00:00
elif key in ["k", "p", "ctrl p"]:
2017-04-05 21:33:25 +00:00
self._keypress_up(size)
2017-04-07 19:13:12 +00:00
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)
2017-04-07 20:40:39 +00:00
elif key in ["h", "left"]:
app.back()
elif key in ["l", "right"]:
self.keypress(size, "enter")
2017-04-10 11:05:20 +00:00
elif key == "b":
2017-04-07 19:13:12 +00:00
self.change_focus(size, len(app.walker) - 1)
2017-04-10 11:05:20 +00:00
elif key == "t":
2017-04-07 19:13:12 +00:00
self.change_focus(size, 0)
2017-04-10 11:05:20 +00:00
elif key in ["c", "R", "+"]:
2017-04-07 19:13:12 +00:00
app.compose()
elif key == "r":
app.refresh()
2017-04-10 11:05:20 +00:00
elif key == "o":
app.options_menu()
2017-04-05 21:33:25 +00:00
elif key.lower() == "q":
2017-04-10 11:05:20 +00:00
app.back()
2017-04-05 21:33:25 +00:00
2017-04-07 21:51:28 +00:00
def frilly_exit():
"""
Exit with some flair. Will fill the screen with rainbows
and shit, or just say bye, depending on the user's bbjrc
setting, `dramatic_exit`
"""
app.loop.stop()
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()
2017-04-05 21:33:25 +00:00
def cute_button(label, callback=None, data=None):
"""
Urwid's default buttons are shit, and they have ugly borders.
This function returns buttons that are a bit easier to love.
"""
button = urwid.Button("", callback, data)
super(urwid.Button, button).__init__(
urwid.SelectableIcon(label))
return button
2017-04-05 18:09:38 +00:00
2017-04-10 14:02:08 +00:00
def urwid_rainbows(string):
"""
Same as below, but instead of printing rainbow text, returns
a markup list suitable for urwid's Text contructor.
"""
colors = [str(x) for x in range(1, 7)]
return urwid.Text([(choice(colors), char) for char in string])
2017-04-05 18:09:38 +00:00
def motherfucking_rainbows(string, inputmode=False, end="\n"):
"""
2017-04-05 21:33:25 +00:00
I cANtT FeELLE MyYE FACECsEE ANYrrMOROeeee
2017-04-05 18:09:38 +00:00
"""
for character in string:
print(choice(colors) + character, end="")
print('\033[0m', end="")
if inputmode:
return input("")
return print(end, end="")
def paren_prompt(text, positive=True, choices=[]):
"""
input(), but riced the fuck out. Changes color depending on
the value of positive (blue/green for good stuff, red/yellow
for bad stuff like invalid input), and has a multiple choice
system capable of rejecting unavailable choices and highlighting
their first characters.
"""
end = text[-1]
if end != "?" and end in punctuation:
text = text[0:-1]
mood = ("\033[1;36m", "\033[1;32m") if positive \
else ("\033[1;31m", "\033[1;33m")
if choices:
prompt = "%s{" % mood[0]
for choice in choices:
prompt += "{0}[{1}{0}]{2}{3} ".format(
"\033[1;35m", choice[0], mood[1], choice[1:])
formatted_choices = prompt[:-1] + ("%s}" % mood[0])
else:
formatted_choices = ""
try:
response = input("{0}({1}{2}{0}){3}> \033[0m".format(
*mood, text, formatted_choices))
if not choices:
return response
elif response == "":
response = " "
char = response.lower()[0]
if char in [c[0] for c in choices]:
return char
return paren_prompt("Invalid choice", False, choices)
except EOFError:
print("")
return ""
2017-04-10 16:13:41 +00:00
# except KeyboardInterrupt:
# exit("\nNevermind then!")
2017-04-05 18:09:38 +00:00
def sane_value(key, prompt, positive=True, return_empty=False):
response = paren_prompt(prompt, positive)
if return_empty and response == "":
return response
try: network.validate(key, response)
except AssertionError as e:
return sane_value(key, e.description, False)
return response
def log_in():
"""
2017-04-09 12:45:51 +00:00
Handles login or registration using an oldschool input()
chain. The user is run through this before starting the
2017-04-05 18:09:38 +00:00
curses app.
"""
name = sane_value("user_name", "Username", return_empty=True)
if name == "":
motherfucking_rainbows("~~W3 4R3 4n0nYm0u5~~")
else:
# ConnectionRefusedError means registered but needs a
# password, ValueError means we need to register the user.
try:
network.set_credentials(name, "")
# make it easy for people who use an empty password =)
2017-04-05 21:33:25 +00:00
motherfucking_rainbows("~~welcome back {}~~".format(network.user_name))
2017-04-05 18:09:38 +00:00
except ConnectionRefusedError:
def login_loop(prompt, positive):
try:
password = paren_prompt(prompt, positive)
network.set_credentials(name, password)
except ConnectionRefusedError:
login_loop("// R E J E C T E D //.", False)
login_loop("Enter your password", True)
2017-04-05 21:33:25 +00:00
motherfucking_rainbows("~~welcome back {}~~".format(network.user_name))
2017-04-05 18:09:38 +00:00
except ValueError:
motherfucking_rainbows("Nice to meet'cha, %s!" % name)
response = paren_prompt(
"Register as %s?" % name,
2017-04-10 16:13:41 +00:00
choices=["yes!", "change name", "nevermind!"]
2017-04-05 18:09:38 +00:00
)
if response == "c":
2017-04-05 21:33:25 +00:00
def nameloop(prompt, positive):
name = sane_value("user_name", prompt, positive)
if network.user_is_registered(name):
return nameloop("%s is already registered" % name, False)
return name
name = nameloop("Pick a new name", True)
2017-04-05 18:09:38 +00:00
2017-04-10 16:13:41 +00:00
elif response == "n":
raise InterruptedError
2017-04-05 18:09:38 +00:00
def password_loop(prompt, positive=True):
response1 = paren_prompt(prompt, positive)
if response1 == "":
confprompt = "Confirm empty password"
else:
confprompt = "Confirm it"
response2 = paren_prompt(confprompt)
if response1 != response2:
return password_loop("Those didnt match. Try again", False)
return response1
password = password_loop("Enter a password. It can be empty if you want")
network.user_register(name, password)
motherfucking_rainbows("~~welcome to the party, %s!~~" % network.user_name)
2017-04-10 14:02:08 +00:00
sleep(0.8) # let that confirmation message shine
2017-04-05 18:09:38 +00:00
2017-04-10 11:05:20 +00:00
def frame_theme(mode="default"):
"""
Return the kwargs for a frame theme.
"""
if mode == "default":
return dict(
tlcorner="@", trcorner="@", blcorner="@", brcorner="@",
tline="=", bline="=", lline="|", rline="|"
)
2017-04-07 19:13:12 +00:00
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:
2017-04-08 10:09:50 +00:00
# load it up
2017-04-07 19:13:12 +00:00
with open(path, "r") as _in:
values = json.load(_in)
2017-04-08 10:09:50 +00:00
# update it with new keys if necessary
for key, default_value in default_prefs.items():
if key not in values:
values[key] = default_value
# else make one
2017-04-07 19:13:12 +00:00
except FileNotFoundError:
values = default_prefs
2017-04-08 10:09:50 +00:00
if mode == "update":
values.update(params)
2017-04-07 19:13:12 +00:00
with open(path, "w") as _out:
json.dump(values, _out)
2017-04-08 10:09:50 +00:00
2017-04-07 19:13:12 +00:00
return values
2017-04-05 18:09:38 +00:00
def main():
run("clear", shell=True)
motherfucking_rainbows(obnoxious_logo)
print(welcome)
2017-04-10 16:13:41 +00:00
try: log_in()
except (InterruptedError, KeyboardInterrupt):
exit("\nwell alrighty then")
2017-04-05 18:09:38 +00:00
if __name__ == "__main__":
main()
2017-04-10 16:13:41 +00:00
# is global
2017-04-05 18:09:38 +00:00
app = App()
2017-04-10 16:13:41 +00:00
try:
app.loop.run()
except KeyboardInterrupt:
frilly_exit()