mirror of
https://github.com/ihabunek/toot
synced 2025-01-22 23:40:18 +01:00
Implement posting statuses
This commit is contained in:
parent
b95aca964f
commit
366e9382d3
@ -1,16 +1,24 @@
|
|||||||
maybe ???
|
Interesting urwid implementations:
|
||||||
https://github.com/CanonicalLtd/subiquity/blob/master/subiquitycore/core.py#L280
|
* https://github.com/CanonicalLtd/subiquity/blob/master/subiquitycore/core.py#L280
|
||||||
|
* https://github.com/TomasTomecek/sen/blob/master/sen/tui/ui.py
|
||||||
educational:
|
* https://github.com/rndusr/stig/tree/master/stig/tui
|
||||||
https://github.com/TomasTomecek/sen/blob/master/sen/tui/ui.py
|
|
||||||
|
|
||||||
check out:
|
|
||||||
https://github.com/rndusr/stig/tree/master/stig/tui
|
|
||||||
|
|
||||||
TODO/Ideas:
|
TODO/Ideas:
|
||||||
* pack left column in timeline view
|
* pack left column in timeline view
|
||||||
* when an error happens, show it in the status bar and have "press E to view exception" to show it in an overlay.
|
* allow scrolling of toot contents if they don't fit the screen, perhaps using
|
||||||
* maybe even have error reporting? e.g. button to open an issue on github?
|
pageup/pagedown
|
||||||
|
* consider adding semi-automated error reporting when viewing an exception,
|
||||||
|
something along the lines of "press T to submit a ticket", which would link
|
||||||
|
to a pre-filled issue submit page.
|
||||||
|
* show new toots, some ideas:
|
||||||
|
* R to reload/refresh timeline
|
||||||
|
* streaming new toots? not sold on the idea
|
||||||
|
* go up on first toot to fetch any newer ones, and prepend them?
|
||||||
|
* Switch timeline to top/bottom layout for narrow views.
|
||||||
|
* Think about how to show media
|
||||||
|
* download media and use local image viewer?
|
||||||
|
* convert to ascii art?
|
||||||
|
* use signals to avoid tightly coupling components
|
||||||
|
|
||||||
Questions:
|
Questions:
|
||||||
* is it possible to make a span a urwid.Text selectable? e.g. for urls and hashtags
|
* is it possible to make a span a urwid.Text selectable? e.g. for urls and hashtags
|
||||||
|
@ -7,6 +7,7 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
|
|
||||||
from toot import api, __version__
|
from toot import api, __version__
|
||||||
|
|
||||||
|
from .compose import StatusComposer
|
||||||
from .constants import PALETTE
|
from .constants import PALETTE
|
||||||
from .entities import Status
|
from .entities import Status
|
||||||
from .timeline import Timeline
|
from .timeline import Timeline
|
||||||
@ -225,6 +226,30 @@ class TUI(urwid.Frame):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def show_compose(self):
|
||||||
|
composer = StatusComposer()
|
||||||
|
urwid.connect_signal(composer, "close",
|
||||||
|
lambda *args: self.close_overlay())
|
||||||
|
urwid.connect_signal(composer, "post",
|
||||||
|
lambda _, content, warning: self.post_status(content, warning))
|
||||||
|
self.open_overlay(
|
||||||
|
widget=composer,
|
||||||
|
title="Compose status",
|
||||||
|
options={
|
||||||
|
"align": 'center',
|
||||||
|
"width": ('relative', 80),
|
||||||
|
"valign": 'middle',
|
||||||
|
"height": ('relative', 80),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def post_status(self, content, warning):
|
||||||
|
data = api.post_status(self.app, self.user, content, spoiler_text=warning)
|
||||||
|
status = Status(data, self.app.instance)
|
||||||
|
self.timeline.prepend_status(status)
|
||||||
|
self.footer.set_message("Status posted {} \\o/".format(status.id))
|
||||||
|
self.close_overlay()
|
||||||
|
|
||||||
def async_toggle_favourite(self, status):
|
def async_toggle_favourite(self, status):
|
||||||
def _favourite():
|
def _favourite():
|
||||||
logger.info("Favouriting {}".format(status))
|
logger.info("Favouriting {}".format(status))
|
||||||
@ -285,11 +310,16 @@ class TUI(urwid.Frame):
|
|||||||
# --- Keys -----------------------------------------------------------------
|
# --- Keys -----------------------------------------------------------------
|
||||||
|
|
||||||
def unhandled_input(self, key):
|
def unhandled_input(self, key):
|
||||||
|
# TODO: this should not be in unhandled input
|
||||||
if key in ('e', 'E'):
|
if key in ('e', 'E'):
|
||||||
if self.exception:
|
if self.exception:
|
||||||
self.show_exception(self.exception)
|
self.show_exception(self.exception)
|
||||||
|
|
||||||
if key in ('q', 'Q'):
|
elif key == 'esc':
|
||||||
|
if self.overlay:
|
||||||
|
self.close_overlay()
|
||||||
|
|
||||||
|
elif key in ('q', 'Q'):
|
||||||
if self.overlay:
|
if self.overlay:
|
||||||
self.close_overlay()
|
self.close_overlay()
|
||||||
else:
|
else:
|
||||||
|
87
toot/tui/compose.py
Normal file
87
toot/tui/compose.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import urwid
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class EditBox(urwid.AttrWrap):
|
||||||
|
def __init__(self):
|
||||||
|
edit = urwid.Edit(multiline=True, allow_tab=True)
|
||||||
|
return super().__init__(edit, "editbox", "editbox_focused")
|
||||||
|
|
||||||
|
|
||||||
|
class Button(urwid.AttrWrap):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
button = urwid.Button(*args, **kwargs)
|
||||||
|
padding = urwid.Padding(button, width=len(args[0]) + 4)
|
||||||
|
return super().__init__(padding, "button", "button_focused")
|
||||||
|
|
||||||
|
def set_label(self, *args, **kwargs):
|
||||||
|
self.original_widget.original_widget.set_label(*args, **kwargs)
|
||||||
|
self.original_widget.width = len(args[0]) + 4
|
||||||
|
|
||||||
|
|
||||||
|
class StatusComposer(urwid.Frame):
|
||||||
|
signals = ["close", "post"]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# This can be added by button press
|
||||||
|
self.content = EditBox()
|
||||||
|
self.content_warning = None
|
||||||
|
self.cw_button = Button("Add content warning", on_press=self.toggle_cw)
|
||||||
|
|
||||||
|
contents = [
|
||||||
|
urwid.Text("Status message"),
|
||||||
|
self.content,
|
||||||
|
urwid.Divider(),
|
||||||
|
self.cw_button,
|
||||||
|
Button("Post", on_press=self.post),
|
||||||
|
Button("Cancel", on_press=self.close),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.walker = urwid.SimpleListWalker(contents)
|
||||||
|
self.listbox = urwid.ListBox(self.walker)
|
||||||
|
return super().__init__(self.listbox)
|
||||||
|
|
||||||
|
def toggle_cw(self, button):
|
||||||
|
if self.content_warning:
|
||||||
|
self.cw_button.set_label("Add content warning")
|
||||||
|
self.walker.pop(2)
|
||||||
|
self.walker.pop(2)
|
||||||
|
self.walker.pop(2)
|
||||||
|
self.walker.set_focus(3)
|
||||||
|
self.content_warning = None
|
||||||
|
else:
|
||||||
|
self.cw_button.set_label("Remove content warning")
|
||||||
|
self.content_warning = EditBox()
|
||||||
|
self.walker.insert(2, self.content_warning)
|
||||||
|
self.walker.insert(2, urwid.Text("Content warning"))
|
||||||
|
self.walker.insert(2, urwid.Divider())
|
||||||
|
self.walker.set_focus(4)
|
||||||
|
|
||||||
|
def clear_error_message(self):
|
||||||
|
self.footer = None
|
||||||
|
|
||||||
|
def set_error_message(self, msg):
|
||||||
|
self.footer = urwid.Text(("footer_message_error", msg))
|
||||||
|
|
||||||
|
def post(self, button):
|
||||||
|
self.clear_error_message()
|
||||||
|
|
||||||
|
# Don't lstrip content to avoid removing intentional leading whitespace
|
||||||
|
# However, do strip both sides to check if there is any content there
|
||||||
|
content = self.content.edit_text.rstrip()
|
||||||
|
content = None if not content.strip() else content
|
||||||
|
|
||||||
|
warning = (self.content_warning.edit_text.rstrip()
|
||||||
|
if self.content_warning else "")
|
||||||
|
warning = None if not warning.strip() else warning
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
self.set_error_message("Cannot post an empty message")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._emit("post", content, warning)
|
||||||
|
|
||||||
|
def close(self, button):
|
||||||
|
self._emit("close")
|
@ -1,8 +1,12 @@
|
|||||||
# name, fg, bg, mono, fg_h, bg_h
|
# name, fg, bg, mono, fg_h, bg_h
|
||||||
PALETTE = [
|
PALETTE = [
|
||||||
# Components
|
# Components
|
||||||
|
('button', 'white', 'black'),
|
||||||
|
('button_focused', 'light gray', 'dark magenta'),
|
||||||
|
('editbox', 'white', 'black'),
|
||||||
|
('editbox_focused', '', 'dark magenta'),
|
||||||
('footer_message', 'dark green', ''),
|
('footer_message', 'dark green', ''),
|
||||||
('footer_message_error', 'white', 'dark red'),
|
('footer_message_error', 'light red', ''),
|
||||||
('footer_status', 'white', 'dark blue'),
|
('footer_status', 'white', 'dark blue'),
|
||||||
('footer_status_bold', 'white, bold', 'dark blue'),
|
('footer_status_bold', 'white, bold', 'dark blue'),
|
||||||
('header', 'white', 'dark blue'),
|
('header', 'white', 'dark blue'),
|
||||||
|
@ -2,7 +2,6 @@ import logging
|
|||||||
import urwid
|
import urwid
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
from toot import api
|
|
||||||
from toot.utils import format_content
|
from toot.utils import format_content
|
||||||
|
|
||||||
from .utils import highlight_hashtags
|
from .utils import highlight_hashtags
|
||||||
@ -14,9 +13,6 @@ logger = logging.getLogger("toot")
|
|||||||
class Timeline(urwid.Columns):
|
class Timeline(urwid.Columns):
|
||||||
"""
|
"""
|
||||||
Displays a list of statuses to the left, and status details on the right.
|
Displays a list of statuses to the left, and status details on the right.
|
||||||
|
|
||||||
TODO: Switch to top/bottom for narrow views.
|
|
||||||
TODO: Cache rendered statuses?
|
|
||||||
"""
|
"""
|
||||||
signals = [
|
signals = [
|
||||||
"status_focused",
|
"status_focused",
|
||||||
@ -68,13 +64,16 @@ class Timeline(urwid.Columns):
|
|||||||
def status_focused(self):
|
def status_focused(self):
|
||||||
"""Called when the list focus switches to a new status"""
|
"""Called when the list focus switches to a new status"""
|
||||||
status = self.get_focused_status()
|
status = self.get_focused_status()
|
||||||
self.status_details = StatusDetails(status)
|
self.draw_status_details(status)
|
||||||
self.contents[1] = self.status_details, ("weight", 50, False)
|
|
||||||
|
|
||||||
index = self.status_list.body.focus
|
index = self.status_list.body.focus
|
||||||
count = len(self.statuses)
|
count = len(self.statuses)
|
||||||
self._emit("status_focused", [status, index, count])
|
self._emit("status_focused", [status, index, count])
|
||||||
|
|
||||||
|
def draw_status_details(self, status):
|
||||||
|
self.status_details = StatusDetails(status)
|
||||||
|
self.contents[1] = self.status_details, ("weight", 50, False)
|
||||||
|
|
||||||
def keypress(self, size, key):
|
def keypress(self, size, key):
|
||||||
# If down is pressed on last status in list emit a signal to load more.
|
# If down is pressed on last status in list emit a signal to load more.
|
||||||
# TODO: Consider pre-loading statuses earlier
|
# TODO: Consider pre-loading statuses earlier
|
||||||
@ -90,6 +89,10 @@ class Timeline(urwid.Columns):
|
|||||||
self.tui.async_toggle_reblog(status)
|
self.tui.async_toggle_reblog(status)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if key in ('c', 'C'):
|
||||||
|
self.tui.show_compose()
|
||||||
|
return
|
||||||
|
|
||||||
if key in ("f", "F"):
|
if key in ("f", "F"):
|
||||||
status = self.get_focused_status()
|
status = self.get_focused_status()
|
||||||
self.tui.async_toggle_favourite(status)
|
self.tui.async_toggle_favourite(status)
|
||||||
@ -112,6 +115,17 @@ class Timeline(urwid.Columns):
|
|||||||
self.status_index_map[status.id] = len(self.statuses) - 1
|
self.status_index_map[status.id] = len(self.statuses) - 1
|
||||||
self.status_list.body.append(self.build_list_item(status))
|
self.status_list.body.append(self.build_list_item(status))
|
||||||
|
|
||||||
|
def prepend_status(self, status):
|
||||||
|
self.statuses.insert(0, status)
|
||||||
|
# Need to rebuild the map, there has to be a better way
|
||||||
|
self.status_index_map = {
|
||||||
|
status.id: n for n, status in enumerate(self.statuses)
|
||||||
|
}
|
||||||
|
self.status_list.body.insert(0, self.build_list_item(status))
|
||||||
|
|
||||||
|
if self.status_list.body.focus == 0:
|
||||||
|
self.draw_status_details(status)
|
||||||
|
|
||||||
def add_statuses(self, statuses):
|
def add_statuses(self, statuses):
|
||||||
for status in statuses:
|
for status in statuses:
|
||||||
self.add_status(status)
|
self.add_status(status)
|
||||||
@ -129,8 +143,8 @@ class Timeline(urwid.Columns):
|
|||||||
|
|
||||||
# Redraw status details if status is focused
|
# Redraw status details if status is focused
|
||||||
if index == self.status_list.body.focus:
|
if index == self.status_list.body.focus:
|
||||||
self.status_details = StatusDetails(status)
|
self.draw_status_details(status)
|
||||||
self.contents[1] = self.status_details, ("weight", 50, False)
|
|
||||||
|
|
||||||
class StatusDetails(urwid.Pile):
|
class StatusDetails(urwid.Pile):
|
||||||
def __init__(self, status):
|
def __init__(self, status):
|
||||||
@ -161,7 +175,8 @@ class StatusDetails(urwid.Pile):
|
|||||||
yield ("pack", urwid.Text([
|
yield ("pack", urwid.Text([
|
||||||
("cyan_bold", "B"), ("cyan", "oost"), " | ",
|
("cyan_bold", "B"), ("cyan", "oost"), " | ",
|
||||||
("cyan_bold", "F"), ("cyan", "avourite"), " | ",
|
("cyan_bold", "F"), ("cyan", "avourite"), " | ",
|
||||||
("cyan_bold", "V"), ("cyan", "iew in browser"), " | ",
|
("cyan_bold", "V"), ("cyan", "iew"), " | ",
|
||||||
|
("cyan", "So"), ("cyan_bold", "u"), ("cyan", "rce"), " | ",
|
||||||
("cyan_bold", "H"), ("cyan", "elp"), " ",
|
("cyan_bold", "H"), ("cyan", "elp"), " ",
|
||||||
]))
|
]))
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user