use the new `quick_bridge` adapter and do few changes to make TUI work with new backend. TUI will probably be rewritten at some point, this is just an update to keep it working. `urwid` has been updated to have a version working with Python 3.14.
1079 lines
41 KiB
Python
1079 lines
41 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# Libervia TUI
|
|
# Copyright (C) 2009-2016 Jérôme Poisson (goffi@goffi.org)
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
from pydantic import Field
|
|
from libervia.backend.core.i18n import _, D_
|
|
from libervia.tui.constants import Const as C
|
|
from libervia.backend.core import log_config
|
|
|
|
log_config.libervia_configure(C.LOG_BACKEND_STANDARD, C)
|
|
from libervia.backend.core import log as logging
|
|
|
|
log = logging.getLogger(__name__)
|
|
from libervia.backend.tools import config as tools_config
|
|
import urwid
|
|
from urwid import is_wide_char
|
|
from urwid_satext import sat_widgets
|
|
from libervia.frontends.quick_frontend.quick_app import QuickApp
|
|
from libervia.frontends.quick_frontend import quick_bridge
|
|
from libervia.frontends.quick_frontend import quick_utils
|
|
from libervia.frontends.quick_frontend import quick_chat
|
|
from libervia.tui.profile_manager import ProfileManager
|
|
from libervia.tui.contact_list import ContactList
|
|
from libervia.tui.chat import Chat
|
|
from libervia.tui import xmlui
|
|
from libervia.tui.progress import Progress
|
|
from libervia.tui.notify import Notify
|
|
from libervia.tui.keys import action_key_map as a_key
|
|
|
|
# from libervia.tui import config
|
|
from libervia.frontends.tools.misc import InputHistory
|
|
from libervia.backend.tools.common import dynamic_import
|
|
from libervia.frontends.tools import jid
|
|
import asyncio
|
|
import signal
|
|
import sys
|
|
|
|
|
|
class TUISettings(tools_config.FrontendSettings, key="tui"):
|
|
bracketed_paste: bool = Field(
|
|
default=False,
|
|
description="""Enable or disable bracketed paste mode.
|
|
|
|
When enabled, this allows the terminal to distinguish between typed text and
|
|
pasted text, which is particularly useful for multi-line pastes in the TUI. This
|
|
prevents pasted content from being interpreted as individual keystrokes.
|
|
|
|
Note: This feature requires terminal support for bracketed paste mode (ANSI escape
|
|
sequences).
|
|
""",
|
|
)
|
|
|
|
|
|
## bridge handling
|
|
# we get bridge name from conf and initialise the right class accordingly
|
|
main_config = tools_config.parse_main_conf(ignore_cache=True)
|
|
bridge_name = next(iter(main_config.bridges)).type
|
|
if "dbus" not in bridge_name:
|
|
print("only D-Bus bridge is currently supported")
|
|
sys.exit(3)
|
|
# install the asyncio twisted reactor
|
|
try:
|
|
asyncio.get_event_loop()
|
|
except RuntimeError:
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
from twisted.internet import asyncioreactor
|
|
|
|
asyncioreactor.install()
|
|
|
|
|
|
class EditBar(sat_widgets.ModalEdit):
|
|
"""
|
|
The modal edit bar where you would enter messages and commands.
|
|
"""
|
|
|
|
def __init__(self, host):
|
|
modes = {
|
|
None: (C.MODE_NORMAL, ""),
|
|
a_key["MODE_INSERTION"]: (C.MODE_INSERTION, "> "),
|
|
a_key["MODE_COMMAND"]: (C.MODE_COMMAND, ":"),
|
|
} # XXX: captions *MUST* be unicode
|
|
super(EditBar, self).__init__(modes)
|
|
self.host = host
|
|
self.set_completion_method(self._text_completion)
|
|
urwid.connect_signal(self, "click", self.on_text_entered)
|
|
|
|
def _text_completion(self, text, completion_data, mode):
|
|
if mode == C.MODE_INSERTION:
|
|
if self.host.selected_widget is not None:
|
|
try:
|
|
completion = self.host.selected_widget.completion
|
|
except AttributeError:
|
|
return text
|
|
else:
|
|
return completion(text, completion_data)
|
|
else:
|
|
return text
|
|
|
|
def on_text_entered(self, editBar):
|
|
"""Called when text is entered in the main edit bar"""
|
|
if self.mode == C.MODE_INSERTION:
|
|
if isinstance(self.host.selected_widget, quick_chat.QuickChat):
|
|
chat_widget = self.host.selected_widget
|
|
self.host.message_send(
|
|
chat_widget.target,
|
|
{"": editBar.get_edit_text()}, # TODO: handle language
|
|
mess_type=(
|
|
C.MESS_TYPE_GROUPCHAT
|
|
if chat_widget.type == C.CHAT_GROUP
|
|
else C.MESS_TYPE_CHAT
|
|
), # TODO: put this in QuickChat
|
|
errback=lambda failure: self.host.show_dialog(
|
|
_("Error while sending message ({})").format(failure),
|
|
type="error",
|
|
),
|
|
profile_key=chat_widget.profile,
|
|
)
|
|
editBar.set_edit_text("")
|
|
elif self.mode == C.MODE_COMMAND:
|
|
self.command_handler()
|
|
|
|
def command_handler(self):
|
|
# TODO: separate class with auto documentation (with introspection)
|
|
# and completion method
|
|
tokens = self.get_edit_text().split(" ")
|
|
command, args = tokens[0], tokens[1:]
|
|
if command == "quit":
|
|
self.host.on_exit()
|
|
raise urwid.ExitMainLoop()
|
|
elif command == "messages":
|
|
wid = sat_widgets.GenericList(logging.memory_get())
|
|
self.host.select_widget(wid)
|
|
# FIXME: reactivate the command
|
|
# elif command == 'presence':
|
|
# values = [value for value in commonConst.PRESENCE.keys()]
|
|
# values = [value if value else 'online' for value in values] # the empty value actually means 'online'
|
|
# if args and args[0] in values:
|
|
# presence = '' if args[0] == 'online' else args[0]
|
|
# self.host.status_bar.on_change(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence]))
|
|
# else:
|
|
# self.host.status_bar.on_presence_click()
|
|
# elif command == 'status':
|
|
# if args:
|
|
# self.host.status_bar.on_change(user_data=sat_widgets.AdvancedEdit(args[0]))
|
|
# else:
|
|
# self.host.status_bar.on_status_click()
|
|
elif command == "history":
|
|
widget = self.host.selected_widget
|
|
if isinstance(widget, quick_chat.QuickChat):
|
|
try:
|
|
limit = int(args[0])
|
|
except (IndexError, ValueError):
|
|
limit = 50
|
|
widget.update_history(size=limit, profile=widget.profile)
|
|
elif command == "search":
|
|
widget = self.host.selected_widget
|
|
if isinstance(widget, quick_chat.QuickChat):
|
|
pattern = " ".join(args)
|
|
if not pattern:
|
|
self.host.notif_bar.add_message(
|
|
D_("Please specify the globbing pattern to search for")
|
|
)
|
|
else:
|
|
widget.update_history(
|
|
size=C.HISTORY_LIMIT_NONE,
|
|
filters={"search": pattern},
|
|
profile=widget.profile,
|
|
)
|
|
elif command == "filter":
|
|
# FIXME: filter is now only for current widget,
|
|
# need to be able to set it globally or per widget
|
|
widget = self.host.selected_widget
|
|
# FIXME: Q&D way, need to be more generic
|
|
if isinstance(widget, quick_chat.QuickChat):
|
|
widget.set_filter(args)
|
|
elif command in ("topic", "suject", "title"):
|
|
try:
|
|
new_title = args[0].strip()
|
|
except IndexError:
|
|
new_title = None
|
|
widget = self.host.selected_widget
|
|
if isinstance(widget, quick_chat.QuickChat) and widget.type == C.CHAT_GROUP:
|
|
widget.on_subject_dialog(new_title)
|
|
else:
|
|
return
|
|
self.set_edit_text("")
|
|
|
|
def _history_cb(self, text):
|
|
self.set_edit_text(text)
|
|
self.set_edit_pos(len(text))
|
|
|
|
def keypress(self, size, key):
|
|
"""Callback when a key is pressed. Send "composing" states
|
|
and move the index of the temporary history stack."""
|
|
if key == a_key["MODAL_ESCAPE"]:
|
|
# first save the text to the current mode, then change to NORMAL
|
|
self.host._update_input_history(self.get_edit_text(), mode=self.mode)
|
|
self.host._update_input_history(mode=C.MODE_NORMAL)
|
|
if self._mode == C.MODE_NORMAL and key in self._modes:
|
|
self.host._update_input_history(mode=self._modes[key][0])
|
|
if key == a_key["HISTORY_PREV"]:
|
|
self.host._update_input_history(
|
|
self.get_edit_text(), -1, self._history_cb, self.mode
|
|
)
|
|
return
|
|
elif key == a_key["HISTORY_NEXT"]:
|
|
self.host._update_input_history(
|
|
self.get_edit_text(), +1, self._history_cb, self.mode
|
|
)
|
|
return
|
|
elif key == a_key["EDIT_ENTER"]:
|
|
self.host._update_input_history(self.get_edit_text(), mode=self.mode)
|
|
else:
|
|
if (
|
|
self._mode == C.MODE_INSERTION
|
|
and isinstance(self.host.selected_widget, quick_chat.QuickChat)
|
|
and key not in sat_widgets.FOCUS_KEYS
|
|
and key not in (a_key["HISTORY_PREV"], a_key["HISTORY_NEXT"])
|
|
and self.host.sync
|
|
):
|
|
self.host.bridge.chat_state_composing(
|
|
self.host.selected_widget.target, self.host.selected_widget.profile
|
|
)
|
|
|
|
return super(EditBar, self).keypress(size, key)
|
|
|
|
|
|
class LiberviaTUITopWidget(sat_widgets.FocusPile):
|
|
"""Top most widget used in LiberviaTUI"""
|
|
|
|
_focus_inversed = True
|
|
positions = ("menu", "body", "notif_bar", "edit_bar")
|
|
can_hide = ("menu", "notif_bar")
|
|
|
|
def __init__(self, body, menu, notif_bar, edit_bar):
|
|
self._body = body
|
|
self._menu = menu
|
|
self._notif_bar = notif_bar
|
|
self._edit_bar = edit_bar
|
|
self._hidden = {"notif_bar"}
|
|
self._focus_extra = False
|
|
super(LiberviaTUITopWidget, self).__init__(
|
|
[("pack", self._menu), self._body, ("pack", self._edit_bar)]
|
|
)
|
|
for position in self.positions:
|
|
setattr(
|
|
self,
|
|
position,
|
|
property(
|
|
lambda: self,
|
|
self.widget_get(position=position),
|
|
lambda pos, new_wid: self.widget_set(new_wid, position=pos),
|
|
),
|
|
)
|
|
self.focus_position = len(self.contents) - 1
|
|
|
|
def get_visible_positions(self, keep=None):
|
|
"""Return positions that are not hidden in the right order
|
|
|
|
@param keep: if not None, this position will be keep in the right order, even if it's hidden
|
|
(can be useful to find its index)
|
|
@return (list): list of visible positions
|
|
"""
|
|
return [
|
|
pos
|
|
for pos in self.positions
|
|
if (keep and pos == keep) or pos not in self._hidden
|
|
]
|
|
|
|
def keypress(self, size, key):
|
|
"""Manage FOCUS keys that focus directly a main part (one of self.positions)
|
|
|
|
To avoid key conflicts, a combinaison must be made with FOCUS_EXTRA then an other key
|
|
"""
|
|
if key == a_key["FOCUS_EXTRA"]:
|
|
self._focus_extra = True
|
|
return
|
|
if self._focus_extra:
|
|
self._focus_extra = False
|
|
if key in ("m", "1"):
|
|
focus = "menu"
|
|
elif key in ("b", "2"):
|
|
focus = "body"
|
|
elif key in ("n", "3"):
|
|
focus = "notif_bar"
|
|
elif key in ("e", "4"):
|
|
focus = "edit_bar"
|
|
else:
|
|
return super(LiberviaTUITopWidget, self).keypress(size, key)
|
|
|
|
if focus in self._hidden:
|
|
return
|
|
|
|
self.focus_position = self.get_visible_positions().index(focus)
|
|
return
|
|
|
|
return super(LiberviaTUITopWidget, self).keypress(size, key)
|
|
|
|
def widget_get(self, position):
|
|
if not position in self.positions:
|
|
raise ValueError("Unknown position {}".format(position))
|
|
return getattr(self, "_{}".format(position))
|
|
|
|
def widget_set(self, widget, position):
|
|
if not position in self.positions:
|
|
raise ValueError("Unknown position {}".format(position))
|
|
return setattr(self, "_{}".format(position), widget)
|
|
|
|
def hide_switch(self, position):
|
|
if not position in self.can_hide:
|
|
raise ValueError("Can't switch position {}".format(position))
|
|
hide = not position in self._hidden
|
|
widget = self.widget_get(position)
|
|
idx = self.get_visible_positions(position).index(position)
|
|
if hide:
|
|
del self.contents[idx]
|
|
self._hidden.add(position)
|
|
else:
|
|
self.contents.insert(idx, (widget, ("pack", None)))
|
|
self._hidden.remove(position)
|
|
|
|
def show(self, position):
|
|
if position in self._hidden:
|
|
self.hide_switch(position)
|
|
|
|
def hide(self, position):
|
|
if not position in self._hidden:
|
|
self.hide_switch(position)
|
|
|
|
|
|
class LiberviaTUIApp(QuickApp, InputHistory):
|
|
MB_HANDLER = False
|
|
AVATARS_HANDLER = False
|
|
|
|
def __init__(self):
|
|
bridge_module = dynamic_import.import_bridge(bridge_name)
|
|
if bridge_module is None:
|
|
log.error("Can't import {} bridge".format(bridge_name))
|
|
sys.exit(3)
|
|
else:
|
|
log.debug("Loading {} bridge".format(bridge_name))
|
|
QuickApp.__init__(
|
|
self,
|
|
# AIOBridge is the only bridge compatible with the asyncio loop
|
|
bridge_factory=lambda: quick_bridge.QuickBridgeAdapter(
|
|
bridge_module.AIOBridge()
|
|
),
|
|
xmlui=xmlui,
|
|
check_options=quick_utils.check_options,
|
|
connect_bridge=False,
|
|
)
|
|
self.tui_settings = main_config.get_settings(TUISettings)
|
|
## main loop setup ##
|
|
# the new D-Bus bridge is twisted based (txdbus)
|
|
event_loop = urwid.TwistedEventLoop
|
|
self.loop = urwid.MainLoop(
|
|
urwid.SolidFill(),
|
|
C.PALETTE,
|
|
event_loop=event_loop(),
|
|
input_filter=self.input_filter,
|
|
unhandled_input=self.key_handler,
|
|
)
|
|
|
|
@classmethod
|
|
def run(cls):
|
|
cls().start()
|
|
|
|
def on_bridge_connected(self):
|
|
|
|
##misc setup##
|
|
self._version = None
|
|
self.bridge.version_get(
|
|
callback=self._version_get_cb, errback=lambda __: None
|
|
)
|
|
self._visible_widgets = set()
|
|
self.notif_bar = sat_widgets.NotificationBar()
|
|
urwid.connect_signal(self.notif_bar, "change", self.on_notification)
|
|
|
|
self.progress_wid = self.widgets.get_or_create_widget(
|
|
Progress, None, on_new_widget=None
|
|
)
|
|
urwid.connect_signal(
|
|
self.notif_bar.progress,
|
|
"click",
|
|
lambda x: self.select_widget(self.progress_wid),
|
|
)
|
|
self.__saved_overlay = None
|
|
|
|
self.x_notify = Notify()
|
|
|
|
# we already manage exit with a_key['APP_QUIT'], so we don't want C-c
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
self._bracketed_paste = self.tui_settings.bracketed_paste
|
|
if self._bracketed_paste:
|
|
log.debug("setting bracketed paste mode as requested")
|
|
sys.stdout.write("\033[?2004h")
|
|
self._bracketed_mode_set = True
|
|
|
|
self.loop.widget = self.main_widget = ProfileManager(self)
|
|
self.post_init()
|
|
|
|
def _version_get_cb(self, version):
|
|
self._version = version
|
|
|
|
@property
|
|
def visible_widgets(self):
|
|
return self._visible_widgets
|
|
|
|
@property
|
|
def mode(self):
|
|
return self.editBar.mode
|
|
|
|
@mode.setter
|
|
def mode(self, value):
|
|
self.editBar.mode = value
|
|
|
|
def mode_hint(self, value):
|
|
"""Change mode if make sens (i.e.: if there is nothing in the editBar)"""
|
|
if not self.editBar.get_edit_text():
|
|
self.mode = value
|
|
|
|
def debug(self):
|
|
"""convenient method to reset screen and launch (i)p(u)db"""
|
|
log.info("Entered debug mode")
|
|
try:
|
|
import pudb
|
|
|
|
pudb.set_trace()
|
|
except ImportError:
|
|
import os
|
|
|
|
os.system("reset")
|
|
try:
|
|
import ipdb
|
|
|
|
ipdb.set_trace()
|
|
except ImportError:
|
|
import pdb
|
|
|
|
pdb.set_trace()
|
|
|
|
def redraw(self):
|
|
"""redraw the screen"""
|
|
try:
|
|
self.loop.draw_screen()
|
|
except AttributeError:
|
|
pass
|
|
|
|
def start(self):
|
|
self.connect_bridge()
|
|
self.loop.run()
|
|
|
|
def post_init(self):
|
|
# FIXME: legacy code to handle config. Options there (such as the ones prefixed
|
|
# with "KEY_", or "disable_mouse") are not yet managed. This code can be deleted
|
|
# once everything is managed again.
|
|
# try:
|
|
# config.apply_config(self)
|
|
# except Exception as e:
|
|
# log.error("configuration error: {}".format(e))
|
|
# popup = self.alert(
|
|
# _("Configuration Error"),
|
|
# _(
|
|
# "Something went wrong while reading the configuration, please check :messages"
|
|
# ),
|
|
# )
|
|
# if self.options.profile:
|
|
# self._early_popup = popup
|
|
# else:
|
|
# self.show_pop_up(popup)
|
|
super(LiberviaTUIApp, self).post_init(self.main_widget)
|
|
|
|
def keys_to_text(self, keys):
|
|
"""Generator return normal text from urwid keys"""
|
|
for k in keys:
|
|
if k == "tab":
|
|
yield "\t"
|
|
elif k == "enter":
|
|
yield "\n"
|
|
elif is_wide_char(k, 0) or (len(k) == 1 and ord(k) >= 32):
|
|
yield k
|
|
|
|
def input_filter(self, input_, raw):
|
|
if self.__saved_overlay and input_ != a_key["OVERLAY_HIDE"]:
|
|
return
|
|
|
|
## paste detection/handling
|
|
if (
|
|
len(input_) > 1 # XXX: it may be needed to increase this value if buffer
|
|
and not isinstance(
|
|
input_[0], tuple
|
|
) # or other things result in several chars at once
|
|
and not "window resize" in input_
|
|
): # (e.g. using LiberviaTUI through ssh). Need some testing
|
|
# and experience to adjust value.
|
|
if input_[0] == "begin paste" and not self._bracketed_paste:
|
|
log.info("Bracketed paste mode detected")
|
|
self._bracketed_paste = True
|
|
|
|
if self._bracketed_paste:
|
|
# after this block, extra will contain non pasted keys
|
|
# and input_ will contain pasted keys
|
|
try:
|
|
begin_idx = input_.index("begin paste")
|
|
except ValueError:
|
|
# this is not a paste, maybe we have something buffering
|
|
# or bracketed mode is set in conf but not enabled in term
|
|
extra = input_
|
|
input_ = []
|
|
else:
|
|
try:
|
|
end_idx = input_.index("end paste")
|
|
except ValueError:
|
|
log.warning("missing end paste sequence, discarding paste")
|
|
extra = input_[:begin_idx]
|
|
del input_[begin_idx:]
|
|
else:
|
|
extra = input_[:begin_idx] + input_[end_idx + 1 :]
|
|
input_ = input_[begin_idx + 1 : end_idx]
|
|
else:
|
|
extra = None
|
|
|
|
log.debug("Paste detected (len {})".format(len(input_)))
|
|
try:
|
|
edit_bar = self.editBar
|
|
except AttributeError:
|
|
log.warning("Paste treated as normal text: there is no edit bar yet")
|
|
if extra is None:
|
|
extra = []
|
|
extra.extend(input_)
|
|
else:
|
|
if self.main_widget.focus == edit_bar:
|
|
# XXX: if a paste is detected, we append it directly to the edit bar text
|
|
# so the user can check it and press [enter] if it's OK
|
|
buf_paste = "".join(self.keys_to_text(input_))
|
|
pos = edit_bar.edit_pos
|
|
edit_bar.set_edit_text(
|
|
"{}{}{}".format(
|
|
edit_bar.edit_text[:pos], buf_paste, edit_bar.edit_text[pos:]
|
|
)
|
|
)
|
|
edit_bar.edit_pos += len(buf_paste)
|
|
else:
|
|
# we are not on the edit_bar,
|
|
# so we treat pasted text as normal text
|
|
if extra is None:
|
|
extra = []
|
|
extra.extend(input_)
|
|
if not extra:
|
|
return
|
|
input_ = extra
|
|
## end of paste detection/handling
|
|
|
|
for i in input_:
|
|
if isinstance(i, tuple):
|
|
if i[0] == "mouse press":
|
|
if i[1] == 4: # Mouse wheel up
|
|
input_[input_.index(i)] = a_key["HISTORY_PREV"]
|
|
if i[1] == 5: # Mouse wheel down
|
|
input_[input_.index(i)] = a_key["HISTORY_NEXT"]
|
|
return input_
|
|
|
|
def key_handler(self, input_):
|
|
if input_ == a_key["MENU_HIDE"]:
|
|
"""User want to (un)hide the menu roller"""
|
|
try:
|
|
self.main_widget.hide_switch("menu")
|
|
except AttributeError:
|
|
pass
|
|
elif input_ == a_key["NOTIFICATION_NEXT"]:
|
|
"""User wants to see next notification"""
|
|
self.notif_bar.show_next()
|
|
elif input_ == a_key["OVERLAY_HIDE"]:
|
|
"""User wants to (un)hide overlay window"""
|
|
if isinstance(self.loop.widget, urwid.Overlay):
|
|
self.__saved_overlay = self.loop.widget
|
|
self.loop.widget = self.main_widget
|
|
else:
|
|
if self.__saved_overlay:
|
|
self.loop.widget = self.__saved_overlay
|
|
self.__saved_overlay = None
|
|
|
|
elif (
|
|
input_ == a_key["DEBUG"] and ".dev0" in (self._version or "")
|
|
): # Debug only for dev versions
|
|
self.debug()
|
|
elif input_ == a_key["CONTACTS_HIDE"]: # user wants to (un)hide the contact lists
|
|
try:
|
|
for wid, options in self.center_part.contents:
|
|
if self.contact_lists_pile is wid:
|
|
self.center_part.contents.remove((wid, options))
|
|
break
|
|
else:
|
|
self.center_part.contents.insert(
|
|
0, (self.contact_lists_pile, ("weight", 2, False))
|
|
)
|
|
except AttributeError:
|
|
# The main widget is not built (probably in Profile Manager)
|
|
pass
|
|
elif input_ == "window resize":
|
|
width, height = self.loop.screen_size
|
|
if height <= 5 and width <= 35:
|
|
if not "save_main_widget" in dir(self):
|
|
self.save_main_widget = self.loop.widget
|
|
self.loop.widget = urwid.Filler(
|
|
urwid.Text(_("Pleeeeasse, I can't even breathe !"))
|
|
)
|
|
else:
|
|
if "save_main_widget" in dir(self):
|
|
self.loop.widget = self.save_main_widget
|
|
del self.save_main_widget
|
|
try:
|
|
return self.menu_roller.check_shortcuts(input_)
|
|
except AttributeError:
|
|
return input_
|
|
|
|
def add_menus(self, menu, type_filter, menu_data=None):
|
|
"""Add cached menus to instance
|
|
|
|
The menus are retrieved asynchronously from the bridge.
|
|
|
|
@param menu: sat_widgets.Menu instance
|
|
@param type_filter: menu type like is sat.core.sat_main.import_menu
|
|
@param menu_data: data to send with these menus
|
|
|
|
"""
|
|
|
|
def add_menu_cb(callback_id):
|
|
self.action_launch(callback_id, menu_data, profile=self.current_profile)
|
|
|
|
def menus_get_cb(menus):
|
|
for id_, type_, path, path_i18n, extra in menus: # TODO: manage extra
|
|
if type_ != type_filter:
|
|
continue
|
|
if len(path) != 2:
|
|
raise NotImplementedError(
|
|
"Menu with a path != 2 are not implemented yet"
|
|
)
|
|
menu.add_menu(
|
|
path_i18n[0], path_i18n[1], lambda dummy, id_=id_: add_menu_cb(id_)
|
|
)
|
|
|
|
def menus_get_eb(failure):
|
|
log.error("Can't get menus: {}".format(failure))
|
|
|
|
self.bridge.menus_get(
|
|
"", C.NO_SECURITY_LIMIT, callback=menus_get_cb, errback=menus_get_eb
|
|
)
|
|
|
|
def _build_menu_roller(self):
|
|
menu = sat_widgets.Menu(self.loop)
|
|
general = _("General")
|
|
menu.add_menu(general, _("Connect"), self.on_connect_request)
|
|
menu.add_menu(general, _("Disconnect"), self.on_disconnect_request)
|
|
menu.add_menu(general, _("Parameters"), self.on_param)
|
|
menu.add_menu(general, _("About"), self.on_about_request)
|
|
menu.add_menu(general, _("Exit"), self.on_exit_request, a_key["APP_QUIT"])
|
|
menu.add_menu(_("Contacts")) # add empty menu to save the place in the menu order
|
|
groups = _("Groups")
|
|
menu.add_menu(groups)
|
|
menu.add_menu(
|
|
groups, _("Join room"), self.on_join_room_request, a_key["ROOM_JOIN"]
|
|
)
|
|
# additionals menus
|
|
# FIXME: do this in a more generic way (in quickapp)
|
|
self.add_menus(menu, C.MENU_GLOBAL)
|
|
|
|
menu_roller = sat_widgets.MenuRoller([(_("Main menu"), menu, C.MENU_ID_MAIN)])
|
|
return menu_roller
|
|
|
|
def _build_main_widget(self):
|
|
self.contact_lists_pile = urwid.Pile([])
|
|
# self.center_part = urwid.Columns([('weight',2,self.contact_lists[profile]),('weight',8,Chat('',self))])
|
|
self.center_part = urwid.Columns(
|
|
[
|
|
("weight", 2, self.contact_lists_pile),
|
|
("weight", 8, urwid.Filler(urwid.Text(""))),
|
|
]
|
|
)
|
|
|
|
self.editBar = EditBar(self)
|
|
self.menu_roller = self._build_menu_roller()
|
|
self.main_widget = LiberviaTUITopWidget(
|
|
self.center_part, self.menu_roller, self.notif_bar, self.editBar
|
|
)
|
|
return self.main_widget
|
|
|
|
def plugging_profiles(self):
|
|
self.loop.widget = self._build_main_widget()
|
|
self.redraw()
|
|
try:
|
|
# if a popup arrived before main widget is build, we need to show it now
|
|
self.show_pop_up(self._early_popup)
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
del self._early_popup
|
|
|
|
def profile_plugged(self, profile):
|
|
QuickApp.profile_plugged(self, profile)
|
|
contact_list = self.widgets.get_or_create_widget(
|
|
ContactList,
|
|
None,
|
|
on_new_widget=None,
|
|
on_click=self.contact_selected,
|
|
on_change=lambda w: self.redraw(),
|
|
profile=profile,
|
|
)
|
|
self.contact_lists_pile.contents.append((contact_list, ("weight", 1)))
|
|
return contact_list
|
|
|
|
def is_hidden(self):
|
|
"""Tells if the frontend window is hidden.
|
|
|
|
@return bool
|
|
"""
|
|
return False # FIXME: implement when necessary
|
|
|
|
def alert(self, title, message):
|
|
"""Shortcut method to create an alert message
|
|
|
|
Alert will have an "OK" button, which remove it if pressed
|
|
@param title(unicode): title of the dialog
|
|
@param message(unicode): body of the dialog
|
|
@return (urwid_satext.Alert): the created Alert instance
|
|
"""
|
|
popup = sat_widgets.Alert(title, message)
|
|
popup.set_callback("ok", lambda dummy: self.remove_pop_up(popup))
|
|
self.show_pop_up(popup, width=75, height=20)
|
|
return popup
|
|
|
|
def remove_pop_up(self, widget=None):
|
|
"""Remove current pop-up, and if there is other in queue, show it
|
|
|
|
@param widget(None, urwid.Widget): if not None remove this popup from front or queue
|
|
"""
|
|
# TODO: refactor popup management in a cleaner way
|
|
# buttons' callback use themselve as first argument, and we never use
|
|
# a Button directly in a popup, so we consider urwid.Button as None
|
|
if widget is not None and not isinstance(widget, urwid.Button):
|
|
if isinstance(self.loop.widget, urwid.Overlay):
|
|
current_popup = self.loop.widget.top_w
|
|
if not current_popup == widget:
|
|
try:
|
|
self.notif_bar.remove_pop_up(widget)
|
|
except ValueError:
|
|
log.warning(
|
|
"Trying to remove an unknown widget {}".format(widget)
|
|
)
|
|
return
|
|
self.loop.widget = self.main_widget
|
|
next_popup = self.notif_bar.get_next_popup()
|
|
if next_popup:
|
|
# we still have popup to show, we display it
|
|
self.show_pop_up(next_popup)
|
|
else:
|
|
self.redraw()
|
|
|
|
def show_pop_up(
|
|
self, pop_up_widget, width=None, height=None, align="center", valign="middle"
|
|
):
|
|
"""Show a pop-up window if possible, else put it in queue
|
|
|
|
@param pop_up_widget: pop up to show
|
|
@param width(int, None): width of the popup
|
|
None to use default
|
|
@param height(int, None): height of the popup
|
|
None to use default
|
|
@param align: same as for [urwid.Overlay]
|
|
"""
|
|
if width == None:
|
|
width = 75 if isinstance(pop_up_widget, xmlui.LiberviaTUINoteDialog) else 135
|
|
if height == None:
|
|
height = 20 if isinstance(pop_up_widget, xmlui.LiberviaTUINoteDialog) else 40
|
|
if not isinstance(self.loop.widget, urwid.Overlay):
|
|
display_widget = urwid.Overlay(
|
|
pop_up_widget, self.main_widget, align, width, valign, height
|
|
)
|
|
self.loop.widget = display_widget
|
|
self.redraw()
|
|
else:
|
|
self.notif_bar.add_pop_up(pop_up_widget)
|
|
|
|
def bar_notify(self, message):
|
|
""" "Notify message to user via notification bar"""
|
|
self.notif_bar.add_message(message)
|
|
self.redraw()
|
|
|
|
def notify(
|
|
self,
|
|
type_,
|
|
entity=None,
|
|
message=None,
|
|
subject=None,
|
|
callback=None,
|
|
cb_args=None,
|
|
widget=None,
|
|
profile=C.PROF_KEY_NONE,
|
|
):
|
|
if widget is None or widget is not None and widget != self.selected_widget:
|
|
# we ignore notification if the widget is selected but we can
|
|
# still do a desktop notification is the X window has not the focus
|
|
super(LiberviaTUIApp, self).notify(
|
|
type_, entity, message, subject, callback, cb_args, widget, profile
|
|
)
|
|
# we don't want notifications without message on desktop
|
|
if message is not None and not self.x_notify.has_focus():
|
|
if message is None:
|
|
message = _("{app}: a new event has just happened{entity}").format(
|
|
app=C.APP_NAME, entity=" ({})".format(entity) if entity else ""
|
|
)
|
|
self.x_notify.send_notification(message)
|
|
|
|
def new_widget(self, widget, user_action=False):
|
|
"""Method called when a new widget is created
|
|
|
|
if suitable, the widget will be displayed
|
|
@param widget(widget.LiberviaTUIWidget): created widget
|
|
@param user_action(bool): if True, the widget has been created following an
|
|
explicit user action. In this case, the widget may get focus immediately
|
|
"""
|
|
# FIXME: when several widgets are possible (e.g. with :split)
|
|
# do not replace current widget when self.selected_widget != None
|
|
if user_action or self.selected_widget is None:
|
|
self.select_widget(widget)
|
|
|
|
def select_widget(self, widget):
|
|
"""Display a widget if possible,
|
|
|
|
else add it in the notification bar queue
|
|
@param widget: BoxWidget
|
|
"""
|
|
assert len(self.center_part.widget_list) <= 2
|
|
wid_idx = len(self.center_part.widget_list) - 1
|
|
self.center_part.widget_list[wid_idx] = widget
|
|
try:
|
|
self.menu_roller.remove_menu(C.MENU_ID_WIDGET)
|
|
except KeyError:
|
|
log.debug("No menu to delete")
|
|
self.selected_widget = widget
|
|
try:
|
|
on_selected = self.selected_widget.on_selected
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
on_selected()
|
|
self._visible_widgets = set(
|
|
[widget]
|
|
) # XXX: we can only have one widget visible at the time for now
|
|
self.contact_lists.select(None)
|
|
|
|
for (
|
|
wid
|
|
) in (
|
|
self.visible_widgets
|
|
): # FIXME: check if widgets.get_widgets is not more appropriate
|
|
if isinstance(wid, Chat):
|
|
contact_list = self.contact_lists[wid.profile]
|
|
contact_list.select(wid.target)
|
|
|
|
self.redraw()
|
|
|
|
def remove_window(self):
|
|
"""Remove window showed on the right column"""
|
|
# TODO: better Window management than this hack
|
|
assert len(self.center_part.widget_list) <= 2
|
|
wid_idx = len(self.center_part.widget_list) - 1
|
|
self.center_part.widget_list[wid_idx] = urwid.Filler(urwid.Text(""))
|
|
self.center_part.focus_position = 0
|
|
self.redraw()
|
|
|
|
def add_progress(self, pid, message, profile):
|
|
"""Follow a SàT progression
|
|
|
|
@param pid: progression id
|
|
@param message: message to show to identify the progression
|
|
"""
|
|
self.progress_wid.add(pid, message, profile)
|
|
|
|
def set_progress(self, percentage):
|
|
"""Set the progression shown in notification bar"""
|
|
self.notif_bar.set_progress(percentage)
|
|
|
|
def contact_selected(self, contact_list, entity):
|
|
self.clear_notifs(entity, profile=contact_list.profile)
|
|
if entity.resource:
|
|
# we have clicked on a private MUC conversation
|
|
chat_widget = self.widgets.get_or_create_widget(
|
|
Chat,
|
|
entity,
|
|
on_new_widget=None,
|
|
force_hash=Chat.get_private_hash(contact_list.profile, entity),
|
|
profile=contact_list.profile,
|
|
)
|
|
else:
|
|
chat_widget = self.widgets.get_or_create_widget(
|
|
Chat, entity, on_new_widget=None, profile=contact_list.profile
|
|
)
|
|
self.select_widget(chat_widget)
|
|
self.menu_roller.add_menu(
|
|
_("Chat menu"), chat_widget.get_menu(), C.MENU_ID_WIDGET
|
|
)
|
|
|
|
def _dialog_ok_cb(self, widget, data):
|
|
popup, answer_cb, answer_data = data
|
|
self.remove_pop_up(popup)
|
|
if answer_cb is not None:
|
|
answer_cb(True, answer_data)
|
|
|
|
def _dialog_cancel_cb(self, widget, data):
|
|
popup, answer_cb, answer_data = data
|
|
self.remove_pop_up(popup)
|
|
if answer_cb is not None:
|
|
answer_cb(False, answer_data)
|
|
|
|
def show_dialog(
|
|
self, message, title="", type="info", answer_cb=None, answer_data=None
|
|
):
|
|
if type == "info":
|
|
popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
|
|
if answer_cb is None:
|
|
popup.set_callback("ok", lambda dummy: self.remove_pop_up(popup))
|
|
elif type == "error":
|
|
popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
|
|
if answer_cb is None:
|
|
popup.set_callback("ok", lambda dummy: self.remove_pop_up(popup))
|
|
elif type == "yes/no":
|
|
popup = sat_widgets.ConfirmDialog(message)
|
|
popup.set_callback("yes", self._dialog_ok_cb, (popup, answer_cb, answer_data))
|
|
popup.set_callback(
|
|
"no", self._dialog_cancel_cb, (popup, answer_cb, answer_data)
|
|
)
|
|
else:
|
|
popup = sat_widgets.Alert(title, message, ok_cb=answer_cb)
|
|
if answer_cb is None:
|
|
popup.set_callback("ok", lambda dummy: self.remove_pop_up(popup))
|
|
log.error("unmanaged dialog type: {}".format(type))
|
|
self.show_pop_up(popup)
|
|
|
|
def dialog_failure(self, failure):
|
|
"""Show a failure that has been returned by an asynchronous bridge method.
|
|
|
|
@param failure (defer.Failure): Failure instance
|
|
"""
|
|
self.alert(failure.classname, failure.message)
|
|
|
|
def on_notification(self, notif_bar):
|
|
"""Called when a new notification has been received"""
|
|
if not isinstance(self.main_widget, LiberviaTUITopWidget):
|
|
# if we are not in the main configuration, we ignore the notifications bar
|
|
return
|
|
if self.notif_bar.can_hide():
|
|
# No notification left, we can hide the bar
|
|
self.main_widget.hide("notif_bar")
|
|
else:
|
|
self.main_widget.show("notif_bar")
|
|
self.redraw() # FIXME: invalidate cache in a more efficient way
|
|
|
|
def _action_manager_unknown_error(self):
|
|
self.alert(_("Error"), _("Unmanaged action"))
|
|
|
|
def room_joined_handler(self, room_jid_s, room_nicks, user_nick, subject, profile):
|
|
super(LiberviaTUIApp, self).room_joined_handler(
|
|
room_jid_s, room_nicks, user_nick, subject, profile
|
|
)
|
|
# if self.selected_widget is None:
|
|
# for contact_list in self.widgets.get_widgets(ContactList):
|
|
# if profile in contact_list.profiles:
|
|
# contact_list.set_focus(jid.JID(room_jid_s), True)
|
|
|
|
def progress_started_handler(self, pid, metadata, profile):
|
|
super(LiberviaTUIApp, self).progress_started_handler(pid, metadata, profile)
|
|
self.add_progress(pid, metadata.get("name", _("unkown")), profile)
|
|
|
|
def progress_finished_handler(self, pid, metadata, profile):
|
|
log.info("Progress {} finished".format(pid))
|
|
super(LiberviaTUIApp, self).progress_finished_handler(pid, metadata, profile)
|
|
|
|
def progress_error_handler(self, pid, err_msg, profile):
|
|
log.warning("Progress {pid} error: {err_msg}".format(pid=pid, err_msg=err_msg))
|
|
super(LiberviaTUIApp, self).progress_error_handler(pid, err_msg, profile)
|
|
|
|
##DIALOGS CALLBACKS##
|
|
def on_join_room(self, button, edit):
|
|
self.remove_pop_up()
|
|
room_jid = jid.JID(edit.get_edit_text())
|
|
self.bridge.muc_join(
|
|
room_jid,
|
|
self.profiles[self.current_profile].whoami.node,
|
|
{},
|
|
self.current_profile,
|
|
callback=lambda dummy: None,
|
|
errback=self.dialog_failure,
|
|
)
|
|
|
|
# MENU EVENTS#
|
|
def on_connect_request(self, menu):
|
|
QuickApp.connect(self, self.current_profile)
|
|
|
|
def on_disconnect_request(self, menu):
|
|
self.disconnect(self.current_profile)
|
|
|
|
def on_param(self, menu):
|
|
def success(params):
|
|
ui = xmlui.create(self, xml_data=params, profile=self.current_profile)
|
|
ui.show()
|
|
|
|
def failure(error):
|
|
self.alert(_("Error"), _("Can't get parameters (%s)") % error)
|
|
|
|
self.bridge.param_ui_get(
|
|
app=C.APP_NAME,
|
|
profile_key=self.current_profile,
|
|
callback=success,
|
|
errback=failure,
|
|
)
|
|
|
|
def on_exit_request(self, menu):
|
|
QuickApp.on_exit(self)
|
|
try:
|
|
if (
|
|
self._bracketed_mode_set
|
|
): # we don't unset if bracketed paste mode was detected automatically (i.e. not in conf)
|
|
log.debug("unsetting bracketed paste mode")
|
|
sys.stdout.write("\033[?2004l")
|
|
except AttributeError:
|
|
pass
|
|
raise urwid.ExitMainLoop()
|
|
|
|
def on_join_room_request(self, menu):
|
|
"""User wants to join a MUC room"""
|
|
|
|
def show_join_dialog(default_service):
|
|
pop_up_widget = sat_widgets.InputDialog(
|
|
_("Entering a MUC room"),
|
|
_("Please enter MUC's JID"),
|
|
default_txt=default_service,
|
|
ok_cb=self.on_join_room,
|
|
)
|
|
pop_up_widget.set_callback(
|
|
"cancel", lambda dummy: self.remove_pop_up(pop_up_widget)
|
|
)
|
|
self.show_pop_up(pop_up_widget)
|
|
|
|
self.bridge.muc_get_default_service(
|
|
callback=show_join_dialog, errback=lambda __: show_join_dialog("")
|
|
)
|
|
|
|
def on_about_request(self, menu):
|
|
self.alert(_("About"), C.APP_NAME + " v" + (self._version or ""))
|
|
|
|
# MISC CALLBACKS#
|
|
|
|
def set_presence_status(self, show="", status=None, profile=C.PROF_KEY_NONE):
|
|
contact_list_wid = self.widgets.get_widget(ContactList, profiles=profile)
|
|
if contact_list_wid is not None:
|
|
contact_list_wid.status_bar.set_presence_status(show, status)
|
|
else:
|
|
log.warning("No ContactList widget found for profile {}".format(profile))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
LiberviaTUIApp().start()
|