diff options
-rw-r--r-- | data/default_config.cfg | 4 | ||||
-rw-r--r-- | data/themes/dark.py | 8 | ||||
-rw-r--r-- | doc/en/configure.txt | 9 | ||||
-rw-r--r-- | plugins/reminder.py | 29 | ||||
-rw-r--r-- | plugins/send_delayed.py | 43 | ||||
-rw-r--r-- | src/common.py | 33 | ||||
-rw-r--r-- | src/connection.py | 2 | ||||
-rw-r--r-- | src/core.py | 30 | ||||
-rw-r--r-- | src/data_forms.py | 6 | ||||
-rw-r--r-- | src/tabs.py | 59 | ||||
-rw-r--r-- | src/theming.py | 10 | ||||
-rw-r--r-- | src/windows.py | 57 |
12 files changed, 231 insertions, 59 deletions
diff --git a/data/default_config.cfg b/data/default_config.cfg index 0ad9e328..dcc12cb7 100644 --- a/data/default_config.cfg +++ b/data/default_config.cfg @@ -177,6 +177,10 @@ themes_dir = # theme will be used instead theme = +enable_vertical_tab_list = false + +vertical_tab_list_size = 20 + # The nick of people who join, part, change their status, etc. in a MUC will # be displayed using their nick color if true. display_user_color_in_join_part = false diff --git a/data/themes/dark.py b/data/themes/dark.py index 015e2988..272c4d91 100644 --- a/data/themes/dark.py +++ b/data/themes/dark.py @@ -31,6 +31,14 @@ class DarkTheme(theming.Theme): COLOR_GROUPCHAT_NAME = (106, 236) COLOR_COLUMN_HEADER = (36, 236) + COLOR_VERTICAL_TAB_NORMAL = (240, -1) + COLOR_VERTICAL_TAB_CURRENT = (-1, 236) + COLOR_VERTICAL_TAB_NEW_MESSAGE = (3, -1) + COLOR_VERTICAL_TAB_HIGHLIGHT = (1, -1) + COLOR_VERTICAL_TAB_PRIVATE = (2, -1) + COLOR_VERTICAL_TAB_DISCONNECTED = (13, -1) + + theme = DarkTheme() diff --git a/doc/en/configure.txt b/doc/en/configure.txt index 5928ef3e..3c03b23d 100644 --- a/doc/en/configure.txt +++ b/doc/en/configure.txt @@ -216,6 +216,15 @@ Configuration options If the file is not found (or no filename is specified) the default theme will be used instead +*enable_vertical_tab_list*:: false + + If true, a vertical list of tabs, with their name, is displayed on the left of + the screen. + +*vertical_tab_list_size*:: 20 + + Define the width of the vertical tabs. Does nothing if it is not enabled. + *send_chat_states*:: true if true, chat states will be sent to the people you are talking to. diff --git a/plugins/reminder.py b/plugins/reminder.py index ed45985c..49062b2c 100644 --- a/plugins/reminder.py +++ b/plugins/reminder.py @@ -29,33 +29,15 @@ class Plugin(BasePlugin): args = common.shell_split(arg) if len(args) < 2: return - if args[0].endswith('d'): - modifier = 'd' - elif args[0].endswith('h'): - modifier = 'h' - elif args[0].endswith('m'): - modifier = 'm' - else: - modifier = None - try: - if modifier: - time = int(args[0][:-1]) - else: - time = int(args[0]) - except: + time = common.parse_str_to_secs(args[0]) + if not time: return - if modifier == 'd': - time = time * 86400 - elif modifier == 'h': - time = time * 3600 - elif modifier == 'm': - time = time * 60 - self.tasks[self.count] = (time, args[1]) timed_event = timed_events.DelayedEvent(time, self.remind, self.count) self.core.add_timed_event(timed_event) - self.core.information('Task %s added: %s every %s seconds.' % (self.count, args[1], time), 'Info') + self.core.information('Task %s added: %s every %s.' % (self.count, args[1], + common.parse_secs_to_str(time)), 'Info') self.count += 1 def completion_remind(self, the_input): @@ -87,7 +69,8 @@ class Plugin(BasePlugin): else: s = 'The following tasks are active:\n' for key in self.tasks: - s += 'Task %s: %s every %s seconds.\n' % (key, repr(self.tasks[key][1]), self.tasks[key][0]) + s += 'Task %s: %s every %s.\n' % (key, repr(self.tasks[key][1]), + common.parse_secs_to_str(self.tasks[key][0])) if s: self.core.information(s, 'Info') diff --git a/plugins/send_delayed.py b/plugins/send_delayed.py new file mode 100644 index 00000000..61d2d397 --- /dev/null +++ b/plugins/send_delayed.py @@ -0,0 +1,43 @@ +from plugin import BasePlugin +import tabs +import common +import timed_events + +class Plugin(BasePlugin): + + def init(self): + self.add_tab_command(tabs.PrivateTab, 'send_delayed', self.command_delayed, "Usage: /send_delayed <delay> <message>\nSend Delayed: Send <message> with a delay of <delay> seconds.", self.completion_delay) + self.add_tab_command(tabs.MucTab, 'send_delayed', self.command_delayed, "Usage: /send_delayed <delay> <message>\nSend Delayed: Send <message> with a delay of <delay> seconds.", self.completion_delay) + self.add_tab_command(tabs.ConversationTab, 'send_delayed', self.command_delayed, "Usage: /send_delayed <delay> <message>\nSend Delayed: Send <message> with a delay of <delay> seconds.", self.completion_delay) + + def command_delayed(self, arg): + args = common.shell_split(arg) + if len(args) != 2: + return + delay = common.parse_str_to_secs(args[0]) + if not delay: + return + + tab = self.core.current_tab() + timed_event = timed_events.DelayedEvent(delay, self.say, (tab, args[1])) + self.core.add_timed_event(timed_event) + + def completion_delay(self, the_input): + txt = the_input.get_text() + args = common.shell_split(txt) + n = len(args) + if txt.endswith(' '): + n += 1 + if n == 2: + return the_input.auto_completion(["60", "5m", "15m", "30m", "1h", "10h", "1d"], '') + + def say(self, args=None): + if not args: + return + + tab = args[0] + # anything could happen to the tab during the interval + try: + tab.command_say(args[1]) + except: + pass diff --git a/src/common.py b/src/common.py index 35fd08da..fdad77a9 100644 --- a/src/common.py +++ b/src/common.py @@ -16,6 +16,7 @@ import mimetypes import hashlib import subprocess import time +import string import shlex from config import config @@ -191,3 +192,35 @@ def replace_key_with_bound(key): return config.get(key, key, 'bindings') else: return key + +def parse_str_to_secs(duration=''): + values = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400} + result = 0 + tmp = '0' + for char in duration: + if char in string.digits: + tmp += char + elif char in values: + tmp_i = int(tmp) + result += tmp_i * values[char] + tmp = '0' + else: + result += int(tmp) + if tmp != '0': + result += int(tmp) + return result + +def parse_secs_to_str(duration=0): + secs, mins, hours, days = 0, 0, 0, 0 + result = '' + secs = duration % 60 + mins = (duration % 3600) // 60 + hours = (duration % 86400) // 3600 + days = duration // 86400 + + result += '%sd' % days if days else '' + result += '%sh' % hours if hours else '' + result += '%sm' % mins if mins else '' + result += '%ss' % secs if secs else '' + return result + diff --git a/src/connection.py b/src/connection.py index 0be94097..2d4b7f41 100644 --- a/src/connection.py +++ b/src/connection.py @@ -66,7 +66,7 @@ class Connection(sleekxmpp.ClientXMPP): custom_port = config.get('custom_port', 5222) if custom_host: res = self.connect((custom_host, custom_port), reattempt=False) - elif custom_port != 5222: + elif custom_port != 5222 and custom_port != -1: res = self.connect((self.boundjid.host, custom_port), reattempt=False) else: res = self.connect(reattempt=False) diff --git a/src/core.py b/src/core.py index ce675f04..8e2cccea 100644 --- a/src/core.py +++ b/src/core.py @@ -193,12 +193,7 @@ class Core(object): """ self.stdscr = curses.initscr() self.init_curses(self.stdscr) - # Init the tab's size. - tabs.Tab.resize(self.stdscr) - # resize the information_win to its initial size - self.resize_global_information_win() - # resize the global_info_bar to its initial size - self.resize_global_info_bar() + self.call_for_resize() default_tab = tabs.RosterInfoTab() default_tab.on_gain_focus() self.tabs.append(default_tab) @@ -226,6 +221,12 @@ class Core(object): """ with g_lock: self.tab_win.resize(1, tabs.Tab.width, tabs.Tab.height - 2, 0) + if config.get('enable_vertical_tab_list', 'false') == 'true': + height, width = self.stdscr.getmaxyx() + truncated_win = self.stdscr.subwin(height, config.get('vertical_tab_list_size', 20), 0, 0) + self.left_tab_win = windows.VerticalGlobalInfoBar(truncated_win) + else: + self.left_tab_win = None def on_exception(self, typ, value, trace): """ @@ -749,16 +750,25 @@ class Core(object): """ Called when we want to resize the screen """ - tabs.Tab.resize(self.stdscr) - self.resize_global_information_win() + # If we have the tabs list on the left, we just give a truncated + # window to each Tab class, so the draw themself in the portion + # of the screen that the can occupy, and we draw the tab list + # on the left remaining space + if config.get('enable_vertical_tab_list', 'false') == 'true': + scr = self.stdscr.subwin(0, config.get('vertical_tab_list_size', 20)) + else: + scr = self.stdscr + tabs.Tab.resize(scr) self.resize_global_info_bar() + self.resize_global_information_win() with g_lock: for tab in self.tabs: if config.get('lazy_resize', 'true') == 'true': tab.need_resize = True else: tab.resize() - self.full_screen_redraw() + if self.tabs: + self.full_screen_redraw() def read_keyboard(self): """ @@ -878,7 +888,7 @@ class Core(object): self.doupdate() def refresh_tab_win(self): - self.current_tab().tab_win.refresh() + self.current_tab().refresh_tab_win() if self.current_tab().input: self.current_tab().input.refresh() self.doupdate() diff --git a/src/data_forms.py b/src/data_forms.py index 8f19e41b..0ad62f73 100644 --- a/src/data_forms.py +++ b/src/data_forms.py @@ -157,7 +157,7 @@ class DummyInput(FieldInput, windows.Win): class ColoredLabel(windows.Win): def __init__(self, text): self.text = text - self.color = 14 + self.color = (14, -1) windows.Win.__init__(self) def resize(self, height, width, y, x): @@ -169,9 +169,9 @@ class ColoredLabel(windows.Win): def refresh(self): with g_lock: - self._win.attron(curses.color_pair(self.color)) + self._win.attron(to_curses_attr(self.color)) self.addstr(0, 0, self.text) - self._win.attroff(curses.color_pair(self.color)) + self._win.attroff(to_curses_attr(self.color)) self._refresh() class BooleanWin(FieldInput, windows.Win): diff --git a/src/tabs.py b/src/tabs.py index 430ee7f8..4ed8a335 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -69,6 +69,17 @@ STATE_COLORS = { # 'attention': lambda: get_theme().COLOR_TAB_ATTENTION, } +VERTICAL_STATE_COLORS = { + 'disconnected': lambda: get_theme().COLOR_VERTICAL_TAB_DISCONNECTED, + 'message': lambda: get_theme().COLOR_VERTICAL_TAB_NEW_MESSAGE, + 'highlight': lambda: get_theme().COLOR_VERTICAL_TAB_HIGHLIGHT, + 'private': lambda: get_theme().COLOR_VERTICAL_TAB_PRIVATE, + 'normal': lambda: get_theme().COLOR_VERTICAL_TAB_NORMAL, + 'current': lambda: get_theme().COLOR_VERTICAL_TAB_CURRENT, +# 'attention': lambda: get_theme().COLOR_VERTICAL_TAB_ATTENTION, + } + + STATE_PRIORITY = { 'normal': -1, 'current': -1, @@ -107,6 +118,13 @@ class Tab(object): return Tab.tab_core.tab_win @property + def left_tab_win(self): + if not Tab.tab_core: + Tab.tab_core = singleton.Singleton(core.Core) + return Tab.tab_core.left_tab_win + + + @property def info_win(self): return self.core.information_win @@ -115,6 +133,10 @@ class Tab(object): return STATE_COLORS[self._state]() @property + def vertical_color(self): + return VERTICAL_STATE_COLORS[self._state]() + + @property def state(self): return self._state @@ -135,6 +157,7 @@ class Tab(object): Tab.visible = False else: Tab.visible = True + windows.Win._tab_win = scr def complete_commands(self, the_input): """ @@ -202,6 +225,11 @@ class Tab(object): else: return False + def refresh_tab_win(self): + self.tab_win.refresh() + if self.left_tab_win: + self.left_tab_win.refresh() + def refresh(self): """ Called on each screen refresh (when something has changed) @@ -665,6 +693,7 @@ class MucTab(ChatTab): else: arg = None if self.joined: + self.disconnect() muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, arg) self.add_message(_("\x195}You left the chatroom\x193}")) if self == self.core.current_tab(): @@ -891,7 +920,7 @@ class MucTab(ChatTab): self.v_separator.refresh() self.user_win.refresh(self.users) self.info_header.refresh(self, self.text_win) - self.tab_win.refresh() + self.refresh_tab_win() self.info_win.refresh() self.input.refresh() @@ -1063,7 +1092,7 @@ class MucTab(ChatTab): if from_nick == self.own_nick: # we are banned self.disconnect() self.core.disable_private_tabs(self.name) - self.tab_win.refresh() + self.refresh_tab_win() self.core.doupdate() if by: kick_msg = _('\x191}%(spec)s \x193}You\x195} have been banned by \x194}%(by)s') % {'spec': get_theme().CHAR_KICK, 'by':by} @@ -1090,7 +1119,7 @@ class MucTab(ChatTab): if from_nick == self.own_nick: # we are kicked self.disconnect() self.core.disable_private_tabs(self.name) - self.tab_win.refresh() + self.refresh_tab_win() self.core.doupdate() if by: kick_msg = _('\x191}%(spec)s \x193}You\x195} have been kicked by \x193}%(by)s') % {'spec': get_theme().CHAR_KICK, 'by':by} @@ -1118,7 +1147,7 @@ class MucTab(ChatTab): # We are now out of the room. Happens with some buggy (? not sure) servers self.disconnect() self.core.disable_private_tabs(from_room) - self.tab_win.refresh() + self.refresh_tab_win() self.core.doupdate() hide_exit_join = config.get('hide_exit_join', -1) if config.get('hide_exit_join', -1) >= -1 else -1 if hide_exit_join == -1 or user.has_talked_since(hide_exit_join): @@ -1376,7 +1405,7 @@ class PrivateTab(ChatTab): self.text_win.refresh() self.info_header.refresh(self.name, self.text_win, self.chatstate) self.info_win.refresh() - self.tab_win.refresh() + self.refresh_tab_win() self.input.refresh() def refresh_info_header(self): @@ -1584,9 +1613,17 @@ class RosterInfoTab(Tab): return else: jid = JID(args[0]).bare + if not jid in [contact.bare_jid for contact in roster.get_contacts()]: + self.core.information('No subscription to deny') + return + self.core.xmpp.sendPresence(pto=jid, ptype='unsubscribed') - if self.core.xmpp.update_roster(jid, subscription='remove'): - roster.remove_contact(jid) + try: + if self.core.xmpp.update_roster(jid, subscription='remove'): + roster.remove_contact(jid) + except Exception as e: + import traceback + log.debug(_('Traceback when removing %s from the roster:\n')+traceback.format_exc()) def command_add(self, args): """ @@ -1844,7 +1881,7 @@ class RosterInfoTab(Tab): self.roster_win.refresh(roster) self.contact_info_win.refresh(self.roster_win.get_selected_row()) self.information_win.refresh() - self.tab_win.refresh() + self.refresh_tab_win() self.input.refresh() def get_name(self): @@ -2109,7 +2146,7 @@ class ConversationTab(ChatTab): self.upper_bar.refresh(self.get_name(), roster.get_contact_by_jid(self.get_name())) self.info_header.refresh(self.get_name(), roster.get_contact_by_jid(self.get_name()), self.text_win, self.chatstate, ConversationTab.additional_informations) self.info_win.refresh() - self.tab_win.refresh() + self.refresh_tab_win() self.input.refresh() def refresh_info_header(self): @@ -2222,7 +2259,7 @@ class MucListTab(Tab): self.upper_message.refresh() self.list_header.refresh() self.listview.refresh() - self.tab_win.refresh() + self.refresh_tab_win() self.input.refresh() self.update_commands() @@ -2370,7 +2407,7 @@ class SimpleTextTab(Tab): self.resize() log.debug(' TAB Refresh: %s'%self.__class__.__name__) self.text_win.refresh() - self.tab_win.refresh() + self.refresh_tab_win() self.input.refresh() def on_lose_focus(self): diff --git a/src/theming.py b/src/theming.py index 0a7ab22d..0fe45d59 100644 --- a/src/theming.py +++ b/src/theming.py @@ -114,6 +114,13 @@ class Theme(object): COLOR_TAB_PRIVATE = (7, 2) COLOR_TAB_DISCONNECTED = (7, 8) + COLOR_VERTICAL_TAB_NORMAL = (4, -1) + COLOR_VERTICAL_TAB_CURRENT = (7, 4) + COLOR_VERTICAL_TAB_NEW_MESSAGE = (5, -1) + COLOR_VERTICAL_TAB_HIGHLIGHT = (1, -1) + COLOR_VERTICAL_TAB_PRIVATE = (2, -1) + COLOR_VERTICAL_TAB_DISCONNECTED = (8, -1) + # Nickname colors # A list of colors randomly attributed to nicks in MUCs # Setting more colors makes it harder to have two nicks with the same color, @@ -151,6 +158,9 @@ class Theme(object): COLOR_QUIT_CHAR = (1, -1) COLOR_KICK_CHAR = (1, -1) + # Vertical tab list color + COLOR_VERTICAL_TAB_NUMBER = (34, -1) + # This is the default theme object, used if no theme is defined in the conf theme = Theme() diff --git a/src/windows.py b/src/windows.py index bbae1ab7..c31954d9 100644 --- a/src/windows.py +++ b/src/windows.py @@ -55,6 +55,7 @@ def truncate_nick(nick, size=25): class Win(object): _win_core = None + _tab_win = None def __init__(self): self._win = None @@ -63,14 +64,12 @@ class Win(object): self.height, self.width = height, width return self.height, self.width, self.x, self.y = height, width, x, y - if not self._win: - self._win = curses.newwin(height, width, y, x) - else: - try: - self._win.resize(height, width) - self._win.mvwin(y, x) - except: - log.debug('DEBUG: mvwin returned ERR. Please investigate') + # try: + self._win = Win._tab_win.derwin(height, width, y, x) + # except: + # log.debug('DEBUG: mvwin returned ERR. Please investigate') + + # If this ever fail, uncomment that ^ def resize(self, height, width, y, x): """ @@ -315,6 +314,42 @@ class GlobalInfoBar(Win): to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) self._refresh() +class VerticalGlobalInfoBar(Win): + def __init__(self, scr): + Win.__init__(self) + self._win = scr + + def refresh(self): + def compare_room(a): + return a.nb + comp = lambda x: x.nb + with g_lock: + height, width = self._win.getmaxyx() + self._win.erase() + sorted_tabs = sorted(self.core.tabs, key=comp) + if config.get('show_inactive_tabs', 'true') == 'false': + sorted_tabs = [tab for tab in sorted_tabs if\ + tab.vertical_color != get_theme().COLOR_VERTICAL_TAB_NORMAL] + nb_tabs = len(sorted_tabs) + if nb_tabs >= height: + for y, tab in enumerate(sorted_tabs): + if tab.vertical_color == get_theme().COLOR_VERTICAL_TAB_CURRENT: + pos = y + break + # center the current tab as much as possible + if pos < height//2: + sorted_tabs = sorted_tabs[:height] + elif nb_tabs - pos <= height//2: + sorted_tabs = sorted_tabs[-height:] + else: + sorted_tabs = sorted_tabs[pos-height//2 : pos+height//2] + for y, tab in enumerate(sorted_tabs): + color = tab.vertical_color + self.addstr(y, 0, "%2d" % tab.nb, to_curses_attr(get_theme().COLOR_VERTICAL_TAB_NUMBER)) + self.addstr('.') + self._win.addnstr("%s" % tab.get_name(), width - 4, to_curses_attr(color)) + self._refresh() + class InfoWin(Win): """ Base class for all the *InfoWin, used in various tabs. For example @@ -426,7 +461,7 @@ class ConversationInfoWin(InfoWin): presence = resource.presence color = RosterWin.color_show[presence]() self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.addstr(" ", to_curses_attr(color)) + self.addstr(get_theme().CHAR_STATUS, to_curses_attr(color)) self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_contact_informations(self, contact): @@ -1494,7 +1529,7 @@ class RosterWin(Win): else: display_name = '%s%s' % (contact.bare_jid, nb,) self.addstr(y, 0, ' ') - self.addstr(" ", to_curses_attr(color)) + self.addstr(get_theme().CHAR_STATUS, to_curses_attr(color)) if resource: self.addstr(' [+]' if contact._folded else ' [-]') self.addstr(' ') @@ -1511,7 +1546,7 @@ class RosterWin(Win): Draw a specific resource line """ color = RosterWin.color_show[resource.presence]() - self.addstr(y, 4, " ", to_curses_attr(color)) + self.addstr(y, 4, get_theme().CHAR_STATUS, to_curses_attr(color)) if colored: self.addstr(y, 6, resource.jid.full, to_curses_attr(get_theme().COLOR_SELECTED_ROW)) else: |