This is a huge configuration refactoring. The configuration system now uses Pydantic settings, with better validation, better support for environment variables and other goodies. The main config files now use TOML instead of INI, this allow for better handling of complex types, notably dict. Plugins, components and frontends can declare their config settings in a class which will be imported dynamically.
1041 lines
40 KiB
Python
1041 lines
40 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.util 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_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 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.common.bridges)).type
|
|
if "dbus" not in bridge_name:
|
|
print("only D-Bus bridge is currently supported")
|
|
sys.exit(3)
|
|
|
|
|
|
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.bridge(bridge_name, "libervia.frontends.bridge")
|
|
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,
|
|
bridge_factory=bridge_module.bridge,
|
|
xmlui=xmlui,
|
|
check_options=quick_utils.check_options,
|
|
connect_bridge=False,
|
|
)
|
|
self.tui_settings = main_config.get_settings(TUISettings)
|
|
## main loop setup ##
|
|
event_loop = (
|
|
urwid.GLibEventLoop if "dbus" in bridge_name else 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._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()
|
|
|
|
@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.bridge.version_get()
|
|
): # 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
|
|
@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)
|
|
|
|
for id_, type_, path, path_i18n, extra in self.bridge.menus_get(
|
|
"", C.NO_SECURITY_LIMIT
|
|
): # 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 _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"""
|
|
pop_up_widget = sat_widgets.InputDialog(
|
|
_("Entering a MUC room"),
|
|
_("Please enter MUC's JID"),
|
|
default_txt=self.bridge.muc_get_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)
|
|
|
|
def on_about_request(self, menu):
|
|
self.alert(_("About"), C.APP_NAME + " v" + self.bridge.version_get())
|
|
|
|
# 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()
|