summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/default_config.cfg4
-rw-r--r--data/themes/dark.py8
-rw-r--r--doc/en/configure.txt9
-rw-r--r--plugins/reminder.py29
-rw-r--r--plugins/send_delayed.py43
-rw-r--r--src/common.py33
-rw-r--r--src/connection.py2
-rw-r--r--src/core.py30
-rw-r--r--src/data_forms.py6
-rw-r--r--src/tabs.py59
-rw-r--r--src/theming.py10
-rw-r--r--src/windows.py57
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: