diff options
-rw-r--r-- | data/themes/dark | 81 | ||||
-rw-r--r-- | data/themes/dark.py | 35 | ||||
-rw-r--r-- | data/themes/poezio | 83 | ||||
-rw-r--r-- | src/common.py | 5 | ||||
-rw-r--r-- | src/core.py | 39 | ||||
-rw-r--r-- | src/room.py | 30 | ||||
-rw-r--r-- | src/tabs.py | 156 | ||||
-rw-r--r-- | src/text_buffer.py | 1 | ||||
-rw-r--r-- | src/theme.py | 173 | ||||
-rw-r--r-- | src/theming.py | 265 | ||||
-rw-r--r-- | src/user.py | 8 | ||||
-rw-r--r-- | src/windows.py | 241 | ||||
-rw-r--r-- | src/xhtml.py | 216 |
13 files changed, 727 insertions, 606 deletions
diff --git a/data/themes/dark b/data/themes/dark deleted file mode 100644 index 77093577..00000000 --- a/data/themes/dark +++ /dev/null @@ -1,81 +0,0 @@ -# A dark theme file. -# For more informations, see http://dev.louiz.org/project/poezio/doc/TheThemes - -# Message text color -COLOR_NORMAL_TEXT = 0 -COLOR_INFORMATION_TEXT = 5 -COLOR_HIGHLIGHT_NICK = 10 - -# User list color -COLOR_USER_VISITOR = 7 -COLOR_USER_PARTICIPANT = 4 -COLOR_USER_NONE = 0 -COLOR_USER_MODERATOR = 1 - -# The character printed in color (COLOR_STATUS_*) before the nickname -# in the user list -CHAR_STATUS = '|' - -# Separators -COLOR_VERTICAL_SEPARATOR = 0 -COLOR_NEW_TEXT_SEPARATOR = 2 -COLOR_MORE_INDICATOR = 3 - -# Time -COLOR_TIME_SEPARATOR = 6 -COLOR_TIME_LIMITER = 0 -CHAR_TIME_LEFT = '' -CHAR_TIME_RIGHT = '' -COLOR_TIME_NUMBERS = 0 - -# Tabs -COLOR_TAB_NORMAL = 57 -COLOR_TAB_CURRENT = 7 -COLOR_TAB_NEW_MESSAGE = 10 -COLOR_TAB_HIGHLIGHT = 8 -COLOR_TAB_PRIVATE = 9 -COLOR_TAB_DISCONNECTED = 30 - -# Nickname colors -LIST_COLOR_NICKNAMES = [ - 1, 2, 3, 4, 5, 6, -2, -4, -5, -6 - ] -COLOR_OWN_NICK = 7 - -# Status color -COLOR_STATUS_XA = 5 -COLOR_STATUS_NONE = 4 -COLOR_STATUS_DND = 1 -COLOR_STATUS_AWAY = 3 -COLOR_STATUS_CHAT = 2 -COLOR_STATUS_UNAVAILABLE = 57 -COLOR_STATUS_ONLINE = 41 - -# Bars -COLOR_INFORMATION_BAR = 57 -COLOR_TOPIC_BAR = 14 -COLOR_PRIVATE_ROOM_BAR = 9 -COLOR_SCROLLABLE_NUMBER = 10 -COLOR_SELECTED_ROW = 14 -COLOR_PRIVATE_NAME = 13 -COLOR_CONVERSATION_NAME = 10 -COLOR_GROUPCHAT_NAME = 13 -COLOR_COLUMN_HEADER = 13 - -# Strings for special messages (like join, quit, nick change, etc) - -# Special messages -CHAR_JOIN = '---->' -CHAR_QUIT = '<----' -CHAR_KICK = '-!-' - -COLOR_JOIN_CHAR = 4 -COLOR_QUIT_CHAR = 1 -COLOR_KICK_CHAR = 1 - -# words between () -COLOR_CURLYBRACKETED_WORD = 4 -# words between {} -COLOR_ACCOLADE_WORD = 6 -# words between [] -COLOR_BRACKETED_WORD = 3 diff --git a/data/themes/dark.py b/data/themes/dark.py new file mode 100644 index 00000000..bbe226f8 --- /dev/null +++ b/data/themes/dark.py @@ -0,0 +1,35 @@ +import theming + +class DarkTheme(theming.Theme): + COLOR_INFORMATION_BAR = (-1, 236) + COLOR_STATUS_XA = (53, -1) + COLOR_STATUS_AWAY = (214, -1) + COLOR_STATUS_DND = (160, -1) + COLOR_STATUS_CHAT = (34 , -1) + COLOR_STATUS_UNAVAILABLE = (242 , -1) + COLOR_STATUS_ONLINE = (27 , -1) + + COLOR_VERTICAL_SEPARATOR = (236, -1) + COLOR_NEW_TEXT_SEPARATOR = (213, -1) + COLOR_MORE_INDICATOR = (6, 4) + + COLOR_HIGHLIGHT_NICK = (236, 202, 'b') + + COLOR_TAB_NORMAL = (-1, 236) + COLOR_TAB_CURRENT = (-1, 16) + COLOR_TAB_NEW_MESSAGE = (3, 236) + COLOR_TAB_HIGHLIGHT = (1, 236) + COLOR_TAB_PRIVATE = (2, 236) + COLOR_TAB_DISCONNECTED = (13, 236) + + COLOR_TOPIC_BAR = (-1, 236) + COLOR_SCROLLABLE_NUMBER = (220, 236, 'b') + COLOR_SELECTED_ROW = (-1, 238) + COLOR_PRIVATE_NAME = (173, 236) + COLOR_CONVERSATION_NAME = (2, 236) + COLOR_GROUPCHAT_NAME = (106, 236) + COLOR_COLUMN_HEADER = (36, 236) + +theme = DarkTheme() + + diff --git a/data/themes/poezio b/data/themes/poezio deleted file mode 100644 index b5bb6a66..00000000 --- a/data/themes/poezio +++ /dev/null @@ -1,83 +0,0 @@ -# A theme file. (the Default one) -# For more informations, see http://dev.louiz.org/project/poezio/doc/TheThemes - -# Message text color -COLOR_NORMAL_TEXT = 0 -COLOR_INFORMATION_TEXT = 5 -COLOR_HIGHLIGHT_NICK = -46 - -# User list color -COLOR_USER_VISITOR = 7 -COLOR_USER_PARTICIPANT = 4 -COLOR_USER_NONE = 0 -COLOR_USER_MODERATOR = 1 - -# nickname colors -COLOR_REMOTE_USER = 5 - -# The character printed in color (COLOR_STATUS_*) before the nickname -# in the user list -CHAR_STATUS = ' ' - -# Separators -COLOR_VERTICAL_SEPARATOR = 4 -COLOR_NEW_TEXT_SEPARATOR = 2 -COLOR_MORE_INDICATOR = 6 - -# Time -COLOR_TIME_SEPARATOR = 6 -COLOR_TIME_LIMITER = 0 -CHAR_TIME_LEFT = '' -CHAR_TIME_RIGHT = '' -COLOR_TIME_NUMBERS = 0 - -# Tabs -COLOR_TAB_NORMAL = 42 -COLOR_TAB_CURRENT = 56 -COLOR_TAB_NEW_MESSAGE = 49 -COLOR_TAB_HIGHLIGHT = 21 -COLOR_TAB_PRIVATE = 28 -COLOR_TAB_DISCONNECTED = 30 - -# Nickname colors -LIST_COLOR_NICKNAMES = [ - 1, 2, 3, 4, 5, 6, -2, -4, -5, -6 - ] -COLOR_OWN_NICK = 7 - -# Status color -COLOR_STATUS_XA = 49 -COLOR_STATUS_NONE = 0 -COLOR_STATUS_DND = 21 -COLOR_STATUS_AWAY = 35 -COLOR_STATUS_CHAT = 28 -COLOR_STATUS_UNAVAILABLE = 57 -COLOR_STATUS_ONLINE = 41 - -# Bars -COLOR_INFORMATION_BAR = 42 -COLOR_TOPIC_BAR = 42 -COLOR_PRIVATE_ROOM_BAR = 28 -COLOR_SCROLLABLE_NUMBER = 39 -COLOR_SELECTED_ROW = 42 -COLOR_PRIVATE_NAME = 42 -COLOR_CONVERSATION_NAME = 42 -COLOR_GROUPCHAT_NAME = 42 -COLOR_COLUMN_HEADER = 36 - -# Strings for special messages (like join, quit, nick change, etc) -# Special messages -CHAR_JOIN = '---->' -CHAR_QUIT = '<----' -CHAR_KICK = '-!-' - -COLOR_JOIN_CHAR = 4 -COLOR_QUIT_CHAR = 1 -COLOR_KICK_CHAR = 1 - -# words between () -COLOR_CURLYBRACKETED_WORD = 4 -# words between {} -COLOR_ACCOLADE_WORD = 6 -# words between [] -COLOR_BRACKETED_WORD = 3 diff --git a/src/common.py b/src/common.py index db750b30..11a09b93 100644 --- a/src/common.py +++ b/src/common.py @@ -186,11 +186,6 @@ def shell_split(st): except ValueError: return st.split(" ") -def curses_color_pair(color): - if color < 0: - return curses.color_pair(-color) | curses.A_BOLD - return curses.color_pair(color) - def replace_key_with_bound(key): if config.has_option('bindings', key): return config.get(key, key, 'bindings') diff --git a/src/core.py b/src/core.py index 0b8442ff..88c926a9 100644 --- a/src/core.py +++ b/src/core.py @@ -18,7 +18,7 @@ import traceback from datetime import datetime import common -import theme +import theming import logging import singleton import collections @@ -47,6 +47,7 @@ from roster import Roster, RosterGroup, roster from contact import Contact, Resource from text_buffer import TextBuffer from keyboard import read_char +from theming import get_theme # http://xmpp.org/extensions/xep-0045.html#errorstatus ERROR_AND_STATUS_CODES = { @@ -370,13 +371,13 @@ class Core(object): return # If a resource got offline, display the message in the conversation with this # precise resource. - self.add_information_message_to_conversation_tab(jid.full, '\x195%s is \x191offline' % (resource.get_jid().full)) + self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x191}offline' % (resource.get_jid().full)) contact.remove_resource(resource) # Display the message in the conversation with the bare JID only if that was # the only resource online (i.e. now the contact is completely disconnected) if not contact.get_highest_priority_resource(): # No resource left: that was the last one - self.add_information_message_to_conversation_tab(jid.bare, '\x195%s is \x191offline' % (jid.bare)) - self.information('\x193%s \x195is \x191offline' % (resource.get_jid().bare), "Roster") + self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x191}offline' % (jid.bare)) + self.information('\x193}%s \x195}is \x191}offline' % (resource.get_jid().bare), "Roster") def on_got_online(self, presence): jid = presence['from'] @@ -394,13 +395,13 @@ class Core(object): resource.set_status(status_message) resource.set_presence(status) resource.set_priority(priority) - self.add_information_message_to_conversation_tab(jid.full, '\x195%s is \x194online' % (jid.full)) + self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x194}online' % (jid.full)) if not contact.get_highest_priority_resource(): # No connected resource yet: the user's just connecting if time.time() - self.connection_time > 12: # We do not display messages if we recently logged in - self.information("\x193%s \x195is \x194online\x195 (\x190%s\x195)" % (resource.get_jid().bare, status), "Roster") - self.add_information_message_to_conversation_tab(jid.bare, '\x195%s is \x194online' % (jid.bare)) + self.information("\x193}%s \x195}is \x194}online\x195} (\x190}%s\x195})" % (resource.get_jid().bare, status), "Roster") + self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x194}online' % (jid.bare)) contact.add_resource(resource) def add_information_message_to_conversation_tab(self, jid, msg): @@ -614,7 +615,7 @@ class Core(object): remote_nick = roster.get_contact_by_jid(jid.bare).get_name() or jid.user else: remote_nick = jid.user - conversation.get_room().add_message(body, nickname=remote_nick, nick_color=theme.COLOR_REMOTE_USER) + conversation.get_room().add_message(body, nickname=remote_nick, nick_color=get_theme().COLOR_REMOTE_USER) if conversation.remote_wants_chatstates is None: if message['chat_state']: conversation.remote_wants_chatstates = True @@ -624,7 +625,7 @@ class Core(object): if 'private' in config.get('beep_on', 'highlight private').split(): curses.beep() if self.current_tab() is not conversation: - conversation.set_color_state(theme.COLOR_TAB_PRIVATE) + conversation.set_color_state(get_theme().COLOR_TAB_PRIVATE) self.refresh_tab_win() else: self.refresh_window() @@ -686,7 +687,7 @@ class Core(object): roster.add_contact(contact, jid) roster.edit_groups_of_contact(contact, []) contact.set_ask('asked') - self.get_tab_by_number(0).set_color_state(theme.COLOR_TAB_HIGHLIGHT) + self.get_tab_by_number(0).set_color_state(get_theme().COLOR_TAB_HIGHLIGHT) self.information('%s wants to subscribe to your presence'%jid, 'Roster') if isinstance(self.current_tab(), tabs.RosterInfoTab): self.refresh_window() @@ -815,9 +816,11 @@ class Core(object): curses.noecho() curses.nonl() curses.raw() - theme.init_colors() stdscr.idlok(True) stdscr.keypad(True) + curses.start_color() + curses.use_default_colors() + theming.reload_theme() curses.ungetch(" ") # H4X: without this, the screen is stdscr.getkey() # erased on the first "getkey()" @@ -835,7 +838,7 @@ class Core(object): """ Refresh everything """ - self.current_tab().set_color_state(theme.COLOR_TAB_CURRENT) + self.current_tab().set_color_state(get_theme().COLOR_TAB_CURRENT) self.current_tab().refresh() self.doupdate() @@ -883,19 +886,19 @@ class Core(object): - A Muc with any new message """ for tab in self.tabs: - if tab.get_color_state() == theme.COLOR_TAB_PRIVATE: + if tab.get_color_state() == get_theme().COLOR_TAB_PRIVATE: self.command_win('%s' % tab.nb) return for tab in self.tabs: - if tab.get_color_state() == theme.COLOR_TAB_HIGHLIGHT: + if tab.get_color_state() == get_theme().COLOR_TAB_HIGHLIGHT: self.command_win('%s' % tab.nb) return for tab in self.tabs: - if tab.get_color_state() == theme.COLOR_TAB_NEW_MESSAGE: + if tab.get_color_state() == get_theme().COLOR_TAB_NEW_MESSAGE: self.command_win('%s' % tab.nb) return for tab in self.tabs: - if tab.get_color_state() == theme.COLOR_TAB_DISCONNECTED: + if tab.get_color_state() == get_theme().COLOR_TAB_DISCONNECTED: self.command_win('%s' % tab.nb) return for tab in self.tabs: @@ -1210,9 +1213,7 @@ class Core(object): self.xmpp.plugin['xep_0030'].get_items(jid=server, block=False, callback=list_tab.on_muc_list_item_received) def command_theme(self, arg): - """ - """ - theme.reload_theme() + theming.reload_theme() self.refresh_window() def command_win(self, arg): diff --git a/src/room.py b/src/room.py index 83b00e62..b97dd0b6 100644 --- a/src/room.py +++ b/src/room.py @@ -12,7 +12,7 @@ from config import config from logger import logger import common -import theme +from theming import get_theme import logging import curses @@ -24,7 +24,7 @@ class Room(TextBuffer): TextBuffer.__init__(self, messages_nb_limit) self.name = name self.own_nick = nick - self.color_state = theme.COLOR_TAB_NORMAL # color used in RoomInfo + self.color_state = get_theme().COLOR_TAB_NORMAL # color used in RoomInfo self.joined = False # false until self presence is receied self.users = [] # User objects self.topic = '' @@ -35,7 +35,7 @@ class Room(TextBuffer): we can know if we can join it, send messages to it, etc """ self.users = [] - self.color_state = theme.COLOR_TAB_DISCONNECTED + self.color_state = get_theme().COLOR_TAB_DISCONNECTED self.joined = False def get_single_line_topic(self): @@ -59,16 +59,16 @@ class Room(TextBuffer): color = None if not time and nickname and nickname != self.own_nick and self.joined: if self.own_nick.lower() in txt.lower(): - if self.color_state != theme.COLOR_TAB_CURRENT: - self.set_color_state(theme.COLOR_TAB_HIGHLIGHT) - color = theme.COLOR_HIGHLIGHT_NICK + if self.color_state != get_theme().COLOR_TAB_CURRENT: + self.set_color_state(get_theme().COLOR_TAB_HIGHLIGHT) + color = get_theme().COLOR_HIGHLIGHT_NICK else: highlight_words = config.get('highlight_on', '').split(':') for word in highlight_words: if word and word.lower() in txt.lower(): - if self.color_state != theme.COLOR_TAB_CURRENT: - self.set_color_state(theme.COLOR_TAB_HIGHLIGHT) - color = theme.COLOR_HIGHLIGHT_NICK + if self.color_state != get_theme().COLOR_TAB_CURRENT: + self.set_color_state(get_theme().COLOR_TAB_HIGHLIGHT) + color = get_theme().COLOR_HIGHLIGHT_NICK break if color: beep_on = config.get('beep_on', 'highlight private').split() @@ -101,7 +101,7 @@ class Room(TextBuffer): self.log_message(txt, time, nickname) special_message = False if txt.startswith('/me '): - txt = "\x192* \x195" + nickname + ' ' + txt[4:] + txt = "\x192}* \x195}" + nickname + ' ' + txt[4:] special_message = True user = self.get_user_by_name(nickname) if nickname is not None else None if user: @@ -110,18 +110,18 @@ class Room(TextBuffer): user = forced_user if not time and nickname and\ nickname != self.own_nick and\ - self.color_state != theme.COLOR_TAB_CURRENT: - if self.color_state != theme.COLOR_TAB_HIGHLIGHT: - self.set_color_state(theme.COLOR_TAB_NEW_MESSAGE) + self.color_state != get_theme().COLOR_TAB_CURRENT: + if self.color_state != get_theme().COLOR_TAB_HIGHLIGHT: + self.set_color_state(get_theme().COLOR_TAB_NEW_MESSAGE) nick_color = nick_color or None if not nickname or time: - txt = '\x195%s' % (txt,) + txt = '\x195}%s' % (txt,) else: # TODO highlight = self.do_highlight(txt, time, nickname) if highlight: nick_color = highlight if special_message: - txt = '\x195%s' % (txt,) + txt = '\x195}%s' % (txt,) nickname = None time = time or datetime.now() message = Message(txt='%s\x19o'%(txt.replace('\t', ' '),), nick_color=nick_color, diff --git a/src/tabs.py b/src/tabs.py index 24f347ba..88d8ad7f 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -13,13 +13,15 @@ Each Tab object has different refresh() and resize() methods, defining how its Windows are displayed, resized, etc """ +MIN_WIDTH = 50 +MIN_HEIGHT = 22 + import logging log = logging.getLogger(__name__) from gettext import gettext as _ import windows -import theme import curses import difflib import text_buffer @@ -33,6 +35,8 @@ import timed_events import multiuserchat as muc +from theming import get_theme + from sleekxmpp.xmlstream.stanzabase import JID from config import config from roster import RosterGroup, roster @@ -58,7 +62,7 @@ class Tab(object): tab_core = None def __init__(self): self.input = None - self._color_state = theme.COLOR_TAB_NORMAL + self._color_state = get_theme().COLOR_TAB_NORMAL self.need_resize = False self.nb = Tab.number Tab.number += 1 @@ -86,7 +90,10 @@ class Tab(object): @staticmethod def resize(scr): Tab.size = (Tab.height, Tab.width) = scr.getmaxyx() - Tab.visible = True + if Tab.height < MIN_HEIGHT or Tab.width < MIN_WIDTH: + Tab.visible = False + else: + Tab.visible = True def complete_commands(self, the_input): """ @@ -185,13 +192,13 @@ class Tab(object): """ called when this tab loses the focus. """ - self._color_state = theme.COLOR_TAB_NORMAL + self._color_state = get_theme().COLOR_TAB_NORMAL def on_gain_focus(self): """ called when this tab gains the focus. """ - self._color_state = theme.COLOR_TAB_CURRENT + self._color_state = get_theme().COLOR_TAB_CURRENT def add_message(self): """ @@ -467,9 +474,9 @@ class MucTab(ChatTab): for user in users: if user.nick == room.own_nick: users.remove(user) - nb_color = len(theme.LIST_COLOR_NICKNAMES) + nb_color = len(get_theme().LIST_COLOR_NICKNAMES) for i, user in enumerate(sorted(users, key=compare_users, reverse=True)): - user.color = theme.LIST_COLOR_NICKNAMES[i % nb_color] + user.color = get_theme().LIST_COLOR_NICKNAMES[i % nb_color] self.text_win.rebuild_everything(self._room) self.text_win.refresh(self._room) self.input.refresh() @@ -674,7 +681,6 @@ class MucTab(ChatTab): """ if not self.visible: return - self.need_resize = False text_width = (self.width//10)*9 self.topic_win.resize(1, self.width, 0, 0) self.v_separator.resize(self.height-3, 1, 1, 9*(self.width//10)) @@ -744,14 +750,14 @@ class MucTab(ChatTab): return self._room def on_lose_focus(self): - self._room.set_color_state(theme.COLOR_TAB_NORMAL) + self._room.set_color_state(get_theme().COLOR_TAB_NORMAL) self.text_win.remove_line_separator() self.text_win.add_line_separator() if config.get('send_chat_states', 'true') == 'true' and not self.input.get_text(): self.send_chat_state('inactive') def on_gain_focus(self): - self._room.set_color_state(theme.COLOR_TAB_CURRENT) + self._room.set_color_state(get_theme().COLOR_TAB_CURRENT) if self.text_win.built_lines and self.text_win.built_lines[-1] is None: self.text_win.remove_line_separator() curses.curs_set(1) @@ -794,10 +800,10 @@ class MucTab(ChatTab): if from_nick == room.own_nick: room.joined = True self.send_chat_state('active') - new_user.color = theme.COLOR_OWN_NICK - room.add_message(_("\x195Your nickname is \x193%s") % (from_nick)) + new_user.color = get_theme().COLOR_OWN_NICK + room.add_message(_("\x195}Your nickname is \x193}%s") % (from_nick)) if '170' in status_codes: - room.add_message('\x191Warning: \x195this room is publicly logged') + room.add_message('\x191}Warning: \x195}this room is publicly logged') else: change_nick = '303' in status_codes kick = '307' in status_codes and typ == 'unavailable' @@ -836,9 +842,9 @@ class MucTab(ChatTab): hide_exit_join = config.get('hide_exit_join', -1) if hide_exit_join != 0: if not jid.full: - room.add_message('\x194%(spec)s \x193%(nick)s\x195 joined the room' % {'nick':from_nick, 'spec':theme.CHAR_JOIN}) + room.add_message('\x194}%(spec)s \x193}%(nick)s\x195} joined the room' % {'nick':from_nick, 'spec':get_theme().CHAR_JOIN}) else: - room.add_message('\x194%(spec)s \x193%(nick)s \x195(\x194%(jid)s\x195) joined the room' % {'spec':theme.CHAR_JOIN, 'nick':from_nick, 'jid':jid.full}) + room.add_message('\x194}%(spec)s \x193}%(nick)s \x195}(\x194}%(jid)s\x195}) joined the room' % {'spec':get_theme().CHAR_JOIN, 'nick':from_nick, 'jid':jid.full}) self.core.on_user_rejoined_private_conversation(room.name, from_nick) @@ -851,7 +857,7 @@ class MucTab(ChatTab): if isinstance(_tab, PrivateTab) and JID(_tab.get_name()).bare == room.name: _tab.get_room().own_nick = new_nick user.change_nick(new_nick) - room.add_message('\x193%(old)s\x195 is now known as \x193%(new)s' % {'old':from_nick, 'new':new_nick}) + room.add_message('\x193}%(old)s\x195} is now known as \x193}%(new)s' % {'old':from_nick, 'new':new_nick}) # rename the private tabs if needed self.core.rename_private_tabs(room.name, from_nick, new_nick) @@ -869,16 +875,16 @@ class MucTab(ChatTab): self.tab_win.refresh() self.core.doupdate() if by: - kick_msg = _('\x191%(spec)s \x193You\x195 have been banned by \x194%(by)s') % {'spec': theme.CHAR_KICK, 'by':by} + kick_msg = _('\x191}%(spec)s \x193}You\x195} have been banned by \x194}%(by)s') % {'spec': get_theme().CHAR_KICK, 'by':by} else: - kick_msg = _('\x191%(spec)s \x193You\x195 have been banned.') % {'spec':theme.CHAR_KICK} + kick_msg = _('\x191}%(spec)s \x193}You\x195} have been banned.') % {'spec':get_theme().CHAR_KICK} else: if by: - kick_msg = _('\x191%(spec)s \x193%(nick)s\x195 has been banned by \x194%(by)s') % {'spec':theme.CHAR_KICK, 'nick':from_nick, 'by':by} + kick_msg = _('\x191}%(spec)s \x193}%(nick)s\x195} has been banned by \x194}%(by)s') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'by':by} else: - kick_msg = _('\x191%(spec)s \x193%(nick)s\x195 has been banned') % {'spec':theme.CHAR_KICK, 'nick':from_nick.replace('"', '\\"')} + kick_msg = _('\x191}%(spec)s \x193}%(nick)s\x195 has been banned') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick.replace('"', '\\"')} if reason is not None and reason.text: - kick_msg += _('\x195 Reason: \x196%(reason)s\x195') % {'reason': reason.text} + kick_msg += _('\x195} Reason: \x196}%(reason)s\x195}') % {'reason': reason.text} room.add_message(kick_msg) def on_user_kicked(self, room, presence, user, from_nick): @@ -895,19 +901,19 @@ class MucTab(ChatTab): self.tab_win.refresh() self.core.doupdate() if by: - kick_msg = _('\x191%(spec)s \x193You\x195 have been kicked by \x193%(by)s') % {'spec': theme.CHAR_KICK, 'by':by} + kick_msg = _('\x191}%(spec)s \x193}You\x195} have been kicked by \x193}%(by)s') % {'spec': get_theme().CHAR_KICK, 'by':by} else: - kick_msg = _('\x191%(spec)s \x193You\x195 have been kicked.') % {'spec':theme.CHAR_KICK} + kick_msg = _('\x191}%(spec)s \x193}You\x195} have been kicked.') % {'spec':get_theme().CHAR_KICK} # try to auto-rejoin if config.get('autorejoin', 'false') == 'true': muc.join_groupchat(self.core.xmpp, room.name, room.own_nick) else: if by: - kick_msg = _('\x191%(spec)s \x193%(nick)s\x195 has been kicked by \x193%(by)s') % {'spec':theme.CHAR_KICK.replace('"', '\\"'), 'nick':from_nick.replace('"', '\\"'), 'by':by.replace('"', '\\"')} + kick_msg = _('\x191}%(spec)s \x193}%(nick)s\x195} has been kicked by \x193}%(by)s') % {'spec':get_theme().CHAR_KICK.replace('"', '\\"'), 'nick':from_nick.replace('"', '\\"'), 'by':by.replace('"', '\\"')} else: - kick_msg = _('\x191%(spec)s \x193%(nick)s\x195 has been kicked') % {'spec':theme.CHAR_KICK, 'nick':from_nick.replace('"', '\\"')} + kick_msg = _('\x191}%(spec)s \x193}%(nick)s\x195} has been kicked') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick.replace('"', '\\"')} if reason is not None and reason.text: - kick_msg += _('\x195 Reason: \x196%(reason)s') % {'reason': reason.text} + kick_msg += _('\x195} Reason: \x196}%(reason)s') % {'reason': reason.text} room.add_message(kick_msg) def on_user_leave_groupchat(self, room, user, jid, status, from_nick, from_room): @@ -924,9 +930,9 @@ class MucTab(ChatTab): 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): if not jid.full: - leave_msg = _('\x191%(spec)s \x193%(nick)s\x195 has left the room') % {'nick':from_nick, 'spec':theme.CHAR_QUIT} + leave_msg = _('\x191}%(spec)s \x193}%(nick)s\x195} has left the room') % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT} else: - leave_msg = _('\x191%(spec)s \x193%(nick)s\x195 (\x194%(jid)s\x195) has left the room') % {'spec':theme.CHAR_QUIT, 'nick':from_nick, 'jid':jid.full} + leave_msg = _('\x191}%(spec)s \x193}%(nick)s\x195} (\x194}%(jid)s\x195}) has left the room') % {'spec':get_theme().CHAR_QUIT, 'nick':from_nick, 'jid':jid.full} if status: leave_msg += ' (%s)' % status room.add_message(leave_msg) @@ -940,7 +946,7 @@ class MucTab(ChatTab): # build the message display_message = False # flag to know if something significant enough # to be displayed has changed - msg = _('\x193%s\x195 changed: ')% from_nick.replace('"', '\\"') + msg = _('\x193}%s\x195} changed: ')% from_nick.replace('"', '\\"') if show not in SHOW_NAME: self.core.information("%s from room %s sent an invalid show: %s" %\ (from_nick, from_room, show), "warning") @@ -993,12 +999,10 @@ class PrivateTab(ChatTab): # keys self.key_func['^I'] = self.completion # commands - self.commands['info'] = (self.command_info, _('Usage: /info\nInfo: Display some information about the user in the MUC: '), None) + #self.commands['info'] = (self.command_info, _('Usage: /info\nInfo: Display some information about the user in the MUC: '), None) self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: close the tab"), None) self.commands['part'] = (self.command_unquery, _("Usage: /part\nPart: close the tab"), None) - self.commands['version'] = (self.command_version, _('Usage: /version\nVersion: get the software version of the current interlocutor (usually its XMPP client and Operating System)'), None) self.resize() - self.parent_muc = self.core.get_tab_by_name(JID(room.name).bare, MucTab) self.on = True def completion(self): @@ -1029,35 +1033,9 @@ class PrivateTab(ChatTab): """ self.core.close_tab() - def command_version(self, arg): - """ - /version - """ - def callback(res): - if not res: - return self.core.information('Could not get the software version from %s' % (jid,), 'Warning') - version = '%s is running %s version %s on %s' % (jid, - res.get('name') or _('an unknown software'), - res.get('version') or _('unknown'), - res.get('os') or _('on an unknown platform')) - self.core.information(version, 'Info') - jid = self.get_room().name - self.core.xmpp.plugin['xep_0092'].get_version(jid, callback=callback) - - def command_info(self, arg): - """ - /info - """ - if arg: - self.parent_muc.command_info(arg) - else: - user = JID(self.get_room().name).resource - self.parent_muc.command_info(user) - def resize(self): if self.core.information_win_size >= self.height-3 or not self.visible: return - self.need_resize = False self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0) self.text_win.rebuild_everything(self._room) self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0) @@ -1078,10 +1056,10 @@ class PrivateTab(ChatTab): self.input.refresh() def get_color_state(self): - if self._room.color_state == theme.COLOR_TAB_NORMAL or\ - self._room.color_state == theme.COLOR_TAB_CURRENT: + if self._room.color_state == get_theme().COLOR_TAB_NORMAL or\ + self._room.color_state == get_theme().COLOR_TAB_CURRENT: return self._room.color_state - return theme.COLOR_TAB_PRIVATE + return get_theme().COLOR_TAB_PRIVATE def set_color_state(self, color): self._room.color_state = color @@ -1103,14 +1081,14 @@ class PrivateTab(ChatTab): return False def on_lose_focus(self): - self._room.set_color_state(theme.COLOR_TAB_NORMAL) + self._room.set_color_state(get_theme().COLOR_TAB_NORMAL) self.text_win.remove_line_separator() self.text_win.add_line_separator() if self.get_room().joined and config.get('send_chat_states', 'true') == 'true' and not self.input.get_text(): self.send_chat_state('inactive') def on_gain_focus(self): - self._room.set_color_state(theme.COLOR_TAB_CURRENT) + self._room.set_color_state(get_theme().COLOR_TAB_CURRENT) curses.curs_set(1) if self.get_room().joined and config.get('send_chat_states', 'true') == 'true' and not self.input.get_text(): self.send_chat_state('active') @@ -1146,24 +1124,22 @@ class PrivateTab(ChatTab): """ The user left the associated MUC """ - self.deactivate() if not status_message: - self.get_room().add_message(_('\x191%(spec)s \x193%(nick)s\x195 has left the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT.replace('"', '\\"')}) + self.get_room().add_message(_('\x191}%(spec)s \x193}%(nick)s\x195} has left the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':get_theme().CHAR_QUIT.replace('"', '\\"')}) else: - self.get_room().add_message(_('\x191%(spec)s \x193%(nick)s\x195 has left the room (%(status)s)"') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT, 'status': status_message.replace('"', '\\"')}) - if self.core.current_tab() is self: - self.refresh() - self.core.doupdate() + self.get_room().add_message(_('\x191}%(spec)s \x193}%(nick)s\x195} has left the room (%(status)s)"') % {'nick':from_nick.replace('"', '\\"'), 'spec':get_theme().CHAR_QUIT, 'status': status_message.replace('"', '\\"')}) + self.deactivate() + self.refresh() + self.core.doupdate() def user_rejoined(self, nick): """ The user (or at least someone with the same nick) came back in the MUC """ + self.get_room().add_message('\x194}%(spec)s \x193}%(nick)s\x195} joined the room' % {'nick':nick, 'spec':get_theme().CHAR_JOIN}) self.activate() - self.get_room().add_message('\x194%(spec)s \x193%(nick)s\x195 joined the room' % {'nick':nick, 'spec':theme.CHAR_JOIN}) - if self.core.current_tab() is self: - self.refresh() - self.core.doupdate() + self.refresh() + self.core.doupdate() def activate(self): self.on = True @@ -1185,7 +1161,7 @@ class RosterInfoTab(Tab): self.contact_info_win = windows.ContactInfoWin() self.default_help_message = windows.HelpText("Enter commands with “/”. “o”: toggle offline show") self.input = self.default_help_message - self.set_color_state(theme.COLOR_TAB_NORMAL) + self.set_color_state(get_theme().COLOR_TAB_NORMAL) self.key_func['^I'] = self.completion self.key_func[' '] = self.on_space self.key_func["/"] = self.on_slash @@ -1209,7 +1185,6 @@ class RosterInfoTab(Tab): def resize(self): if not self.visible: return - self.need_resize = False roster_width = self.width//2 info_width = self.width-roster_width-1 self.v_separator.resize(self.height-2, 1, 0, roster_width) @@ -1420,10 +1395,10 @@ class RosterInfoTab(Tab): return self.reset_help_message() def on_lose_focus(self): - self._color_state = theme.COLOR_TAB_NORMAL + self._color_state = get_theme().COLOR_TAB_NORMAL def on_gain_focus(self): - self._color_state = theme.COLOR_TAB_CURRENT + self._color_state = get_theme().COLOR_TAB_CURRENT if isinstance(self.input, windows.HelpText): curses.curs_set(0) else: @@ -1526,7 +1501,7 @@ class ConversationTab(ChatTab): def __init__(self, jid): txt_buff = text_buffer.TextBuffer() ChatTab.__init__(self, txt_buff) - self.color_state = theme.COLOR_TAB_NORMAL + self.color_state = get_theme().COLOR_TAB_NORMAL self._name = jid # a conversation tab is linked to one specific full jid OR bare jid self.text_win = windows.TextWin() txt_buff.add_window(self.text_win) @@ -1566,7 +1541,6 @@ class ConversationTab(ChatTab): def resize(self): if self.core.information_win_size >= self.height-3 or not self.visible: return - self.need_resize = False self.text_win.resize(self.height-4-self.core.information_win_size, self.width, 1, 0) self.text_win.rebuild_everything(self._room) self.upper_bar.resize(1, self.width, 0, 0) @@ -1589,10 +1563,10 @@ class ConversationTab(ChatTab): self.input.refresh() def get_color_state(self): - if self.color_state == theme.COLOR_TAB_NORMAL or\ - self.color_state == theme.COLOR_TAB_CURRENT: + if self.color_state == get_theme().COLOR_TAB_NORMAL or\ + self.color_state == get_theme().COLOR_TAB_CURRENT: return self.color_state - return theme.COLOR_TAB_PRIVATE + return get_theme().COLOR_TAB_PRIVATE def set_color_state(self, color): self.color_state = color @@ -1610,14 +1584,14 @@ class ConversationTab(ChatTab): return False def on_lose_focus(self): - self.set_color_state(theme.COLOR_TAB_NORMAL) + self.set_color_state(get_theme().COLOR_TAB_NORMAL) self.text_win.remove_line_separator() self.text_win.add_line_separator() if config.get('send_chat_states', 'true') == 'true' and not self.input.get_text() or not self.input.get_text().startswith('//'): self.send_chat_state('inactive') def on_gain_focus(self): - self.set_color_state(theme.COLOR_TAB_CURRENT) + self.set_color_state(get_theme().COLOR_TAB_CURRENT) curses.curs_set(1) if config.get('send_chat_states', 'true') == 'true' and not self.input.get_text() or not self.input.get_text().startswith('//'): self.send_chat_state('active') @@ -1652,7 +1626,7 @@ class MucListTab(Tab): """ def __init__(self, server): Tab.__init__(self) - self._color_state = theme.COLOR_TAB_NORMAL + self._color_state = get_theme().COLOR_TAB_NORMAL self.name = server self.upper_message = windows.Topic() self.upper_message.set_message('Chatroom list on server %s (Loading)' % self.name) @@ -1684,7 +1658,6 @@ class MucListTab(Tab): def resize(self): if not self.visible: return - self.need_resize = False self.upper_message.resize(1, self.width, 0, 0) column_size = {'node-part': (self.width-5)//4, 'name': (self.width-5)//4*3, @@ -1766,10 +1739,10 @@ class MucListTab(Tab): return self.key_func[key]() def on_lose_focus(self): - self._color_state = theme.COLOR_TAB_NORMAL + self._color_state = get_theme().COLOR_TAB_NORMAL def on_gain_focus(self): - self._color_state = theme.COLOR_TAB_CURRENT + self._color_state = get_theme().COLOR_TAB_CURRENT curses.curs_set(0) def get_color_state(self): @@ -1789,7 +1762,7 @@ class SimpleTextTab(Tab): """ def __init__(self, text): Tab.__init__(self) - self._color_state = theme.COLOR_TAB_NORMAL + self._color_state = get_theme().COLOR_TAB_NORMAL self.text_win = windows.SimpleTextWin(text) self.default_help_message = windows.HelpText("“Ctrl+q”: close") self.input = self.default_help_message @@ -1819,7 +1792,6 @@ class SimpleTextTab(Tab): def resize(self): if not self.visible: return - self.need_resize = False self.text_win.resize(self.height-2, self.width, 0, 0) self.input.resize(1, self.width, self.height-1, 0) @@ -1832,10 +1804,10 @@ class SimpleTextTab(Tab): self.input.refresh() def on_lose_focus(self): - self._color_state = theme.COLOR_TAB_NORMAL + self._color_state = get_theme().COLOR_TAB_NORMAL def on_gain_focus(self): - self._color_state = theme.COLOR_TAB_CURRENT + self._color_state = get_theme().COLOR_TAB_CURRENT curses.curs_set(0) def get_color_state(self): diff --git a/src/text_buffer.py b/src/text_buffer.py index d89dbfb9..f39f147a 100644 --- a/src/text_buffer.py +++ b/src/text_buffer.py @@ -15,7 +15,6 @@ log = logging.getLogger(__name__) import collections from datetime import datetime -import theme from config import config Message = collections.namedtuple('Message', 'txt nick_color time str_time nickname user') diff --git a/src/theme.py b/src/theme.py deleted file mode 100644 index 2502e5c6..00000000 --- a/src/theme.py +++ /dev/null @@ -1,173 +0,0 @@ -# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> -# -# This file is part of Poezio. -# -# Poezio is free software: you can redistribute it and/or modify -# it under the terms of the zlib license. See the COPYING file. - -""" -Define the variables (colors and some other stuff) that are -used when drawing the interface (mainly colors) -""" - -import curses -import glob -import imp -import os -from config import config - -import logging -log = logging.getLogger(__name__) - -## Define the default colors -## Do not change these colors, create a theme file instead. - -# Message text color -COLOR_NORMAL_TEXT = 0 -COLOR_INFORMATION_TEXT = 5 -COLOR_HIGHLIGHT_NICK = -46 - -# User list color -COLOR_USER_VISITOR = 7 -COLOR_USER_PARTICIPANT = 4 -COLOR_USER_NONE = 0 -COLOR_USER_MODERATOR = 1 - -# nickname colors -COLOR_REMOTE_USER = 5 - -# The character printed in color (COLOR_STATUS_*) before the nickname -# in the user list -CHAR_STATUS = ' ' - -# Separators -COLOR_VERTICAL_SEPARATOR = 4 -COLOR_NEW_TEXT_SEPARATOR = 2 -COLOR_MORE_INDICATOR = 6 - -# Time -COLOR_TIME_SEPARATOR = 6 -COLOR_TIME_LIMITER = 0 -CHAR_TIME_LEFT = '' -CHAR_TIME_RIGHT = '' -COLOR_TIME_NUMBERS = 0 - -# Tabs -COLOR_TAB_NORMAL = 42 -COLOR_TAB_CURRENT = 56 -COLOR_TAB_NEW_MESSAGE = 49 -COLOR_TAB_HIGHLIGHT = 21 -COLOR_TAB_PRIVATE = 28 -COLOR_TAB_DISCONNECTED = 30 - -# Nickname colors -LIST_COLOR_NICKNAMES = [ - 1, 2, 3, 4, 5, 6, -2, -4, -5, -6 - ] -COLOR_OWN_NICK = 7 - -# Status color -COLOR_STATUS_XA = 49 -COLOR_STATUS_NONE = 0 -COLOR_STATUS_DND = 21 -COLOR_STATUS_AWAY = 35 -COLOR_STATUS_CHAT = 28 -COLOR_STATUS_UNAVAILABLE = 57 -COLOR_STATUS_ONLINE = 41 - -# Bars -COLOR_INFORMATION_BAR = 42 -COLOR_TOPIC_BAR = 42 -COLOR_PRIVATE_ROOM_BAR = 28 -COLOR_SCROLLABLE_NUMBER = -39 -COLOR_SELECTED_ROW = 42 -COLOR_PRIVATE_NAME = 42 -COLOR_CONVERSATION_NAME = 42 -COLOR_GROUPCHAT_NAME = 42 -COLOR_COLUMN_HEADER = 36 - -# Strings for special messages (like join, quit, nick change, etc) -# Special messages -CHAR_JOIN = '---->' -CHAR_QUIT = '<----' -CHAR_KICK = '-!-' - -COLOR_JOIN_CHAR = 4 -COLOR_QUIT_CHAR = 1 -COLOR_KICK_CHAR = 1 - -# words between () -COLOR_CURLYBRACKETED_WORD = 4 -# words between {} -COLOR_ACCOLADE_WORD = 6 -# words between [] -COLOR_BRACKETED_WORD = 3 - -def init_colors(): - """ - Initilization of all the available ncurses colors - limit the number of colors to 64 (because some terminals - don't handle more than that), by removing some useless colors - like 'black on black', etc. - """ - curses.start_color() - curses.use_default_colors() - cpt = 0 - for i in range(-1, 7): - for y in range(0, 8): - if y == i: - continue - curses.init_pair(cpt, y, i) - cpt += 1 - for y in range(0, 7): - # init the default fg on others bg at last - curses.init_pair(cpt, -1, y) - cpt += 1 - # Have the default color be default fg on default bg - reload_theme() - -def reload_theme(): - themes_dir = config.get('themes_dir', '') - themes_dir = themes_dir or\ - os.path.join(os.environ.get('XDG_DATA_HOME') or\ - os.path.join(os.environ.get('HOME'), '.local', 'share'), - 'poezio', 'themes') - try: - os.makedirs(themes_dir) - except OSError: - pass - theme_name = config.get('theme', '') - if not theme_name: - return - try: - theme = imp.load_source('theme', os.path.join(themes_dir, theme_name)) - except: # TODO warning: theme not found - return - for var in dir(theme): - if var.startswith('COLOR_') or var.startswith('CHAR_') or var.startswith('LIST_'): - globals()[var] = getattr(theme, var) - -if __name__ == '__main__': - """ - Launch 'python theme.py' to see the list of all the available colors - in your terminal - """ - s = curses.initscr() - curses.start_color() - curses.use_default_colors() - init_colors() - for i in range(64): - s.attron(curses.color_pair(i) | curses.A_BOLD) - s.addstr(str(curses.color_pair(i) | curses.A_BOLD)) - s.attroff(curses.color_pair(i) | curses.A_BOLD) - s.addstr(' ') - s.addstr('\n') - for i in range(64): - s.attron(curses.color_pair(i)) - s.addstr(str(i)) - s.attroff(curses.color_pair(i)) - s.addstr(' ') - - s.refresh() - s.getch() - s.endwin() diff --git a/src/theming.py b/src/theming.py new file mode 100644 index 00000000..382a3146 --- /dev/null +++ b/src/theming.py @@ -0,0 +1,265 @@ +# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the zlib license. See the COPYING file. + +""" +Define the variables (colors and some other stuff) that are +used when drawing the interface. + +Colors are numbers from -1 to 7 (if only 8 colors are supported) or -1 to 255 +if 256 colors are available. +We check the number of available colors at startup, and we load a theme accordingly. +A 8 color theme should NEVER use colors not in the -1 -> 7 range. We won't check that +at run time. If the case occurs, the THEME should be fixed. +XHTML-IM colors are converted to -1 -> 255 colors if available, or directly to +-1 -> 8 if we are in 8-color-mode. + +A pair_color is a background-foreground pair. All possible pairs are not created +at startup, because that would create 256*256 pairs, and almost all of them +would never be used. +So, a theme should define color tuples, like (200, -1), and when they are to +be used by poezio's interface, they will be created once, and kept in a list for +later usage. +A color tuple is of the form (foreground, background, optional) +A color of -1 means the default color. So if you do not want to have +a background color, use (x, -1). +The optional third value of the tuple defines additional information. It +is a string and can contain one or more of these characteres: +'b': bold +'u': underlined +'x': blink + +For example, (200, 208, 'bu') is bold, underlined and pink foreground on orange background. + +A theme file is a python file containing one object named 'theme', which is an +instance of a class (derived from the Theme class) defined in that same file. +For example, in pinkytheme.py: + +import theming +class PinkyTheme(theming.Theme): + COLOR_NORMAL_TEXT = (200, -1) + +theme = PinkyTheme() + +if the command '/theme pinkytheme' is issued, we import the pinkytheme.py file +and set the global variable 'theme' to pinkytheme.theme. + +And in poezio's code we just use theme.COLOR_NORMAL_TEXT etc + +Since a theme inherites from the Theme class (defined here), if a color is not defined in a +theme file, the color is the default one. + +Some values in that class are a list of color tuple. +For example [(1, -1), (2, -1), (3, -1)] +Such a list SHOULD contain at list one color tuple. +It is used for example to define color gradient, etc. +""" + +import logging +log = logging.getLogger(__name__) + +from config import config + +import curses +import imp +import os + +class Theme(object): + """ + The theme class, from which all theme should inherit. + All of the following value can be replaced in subclasses, in + order to create a new theme. + Do not edit this file if you want to change the theme to suit your + needs. Create a new theme and share it if you think it can be useful + for others. + """ + # Message text color + COLOR_NORMAL_TEXT = (-1, -1) + COLOR_INFORMATION_TEXT = (137, -1) # TODO + COLOR_HIGHLIGHT_NICK = (3, 5, 'b') + + # User list color + COLOR_USER_VISITOR = (0, -1) + COLOR_USER_PARTICIPANT = (4, -1) + COLOR_USER_NONE = (0, -1) + COLOR_USER_MODERATOR = (1, -1) + + # nickname colors + COLOR_REMOTE_USER = (5, -1) + + # The character printed in color (COLOR_STATUS_*) before the nickname + # in the user list + CHAR_STATUS = '|' + + # Separators + COLOR_VERTICAL_SEPARATOR = (4, -1) + COLOR_NEW_TEXT_SEPARATOR = (2, -1) + COLOR_MORE_INDICATOR = (6, 4) + + # Time + COLOR_TIME_SEPARATOR = (106, -1) + COLOR_TIME_LIMITER = (0, -1) + CHAR_TIME_LEFT = '' + CHAR_TIME_RIGHT = '' + COLOR_TIME_NUMBERS = (0, -1) + + # Tabs + COLOR_TAB_NORMAL = (7, 4) + COLOR_TAB_CURRENT = (7, 6) + COLOR_TAB_NEW_MESSAGE = (7, 5) + COLOR_TAB_HIGHLIGHT = (7, 1) + COLOR_TAB_PRIVATE = (7, 2) + COLOR_TAB_DISCONNECTED = (7, 8) + + # 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, + # avoiding confusions. + LIST_COLOR_NICKNAMES = [(1, -1), (2, -1), (3, -1), (4, -1), (5, -1), (6, -1), (7, -1), (8, -1), (9, -1), (10, -1), (11, -1), (12, -1), (13, -1), (14, -1), (23, -1), (23, -1), (88, -1), (99, -1), (100, -1), (154, -1), (213, -1), (216, -1), (227, -1)] + # This is your own nickname + COLOR_OWN_NICK = (254, -1) + + # Status color + COLOR_STATUS_XA = (16, 90) + COLOR_STATUS_NONE = (16, 4) + COLOR_STATUS_DND = (16, 1) + COLOR_STATUS_AWAY = (16, 3) + COLOR_STATUS_CHAT = (16, 2) + COLOR_STATUS_UNAVAILABLE = (-1, 247) + COLOR_STATUS_ONLINE = (16, 4) + + # Bars + COLOR_INFORMATION_BAR = (7, 4) + COLOR_TOPIC_BAR = (7, 4) + COLOR_SCROLLABLE_NUMBER = (220, 4, 'b') + COLOR_SELECTED_ROW = (-1, 33) + COLOR_PRIVATE_NAME = (-1, 4) + COLOR_CONVERSATION_NAME = (2, 4) + COLOR_GROUPCHAT_NAME = (7, 4) + COLOR_COLUMN_HEADER = (36, 4) + + # Strings for special messages (like join, quit, nick change, etc) + # Special messages + CHAR_JOIN = '--->' + CHAR_QUIT = '<---' + CHAR_KICK = '-!-' + + COLOR_JOIN_CHAR = (4, -1) + COLOR_QUIT_CHAR = (1, -1) + COLOR_KICK_CHAR = (1, -1) + +# This is the default theme object, used if no theme is defined in the conf +theme = Theme() + +# a dict "color tuple -> color_pair" +# Each time we use a color tuple, we check if it has already been used. +# If not we create a new color_pair and keep it in that dict, to use it +# the next time. +curses_colors_dict = {} + +table_256_to_16 = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 0, 4, 4, 4, 12, 12, 2, 6, 4, 4, 12, 12, 2, 2, 6, 4, + 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, + 10, 10, 10, 14, 1, 5, 4, 4, 12, 12, 3, 8, 4, 4, 12, 12, + 2, 2, 6, 4, 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, + 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 5, 4, 12, 12, 1, 1, + 5, 4, 12, 12, 3, 3, 8, 4, 12, 12, 2, 2, 2, 6, 12, 12, + 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 1, 5, + 12, 12, 1, 1, 1, 5, 12, 12, 1, 1, 1, 5, 12, 12, 3, 3, + 3, 7, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, + 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, + 13, 12, 9, 9, 9, 9, 13, 12, 11, 11, 11, 11, 7, 12, 10, 10, + 10, 10, 10, 14, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, + 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, + 9, 13, 11, 11, 11, 11, 11, 15, 0, 0, 0, 0, 0, 0, 8, 8, + 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 15, 15, 15, 15, 15, 15 +] + +def color_256_to_16(color): + if color == -1: + return color + return table_256_to_16[color] + +def to_curses_attr(color_tuple): + """ + Takes a color tuple (as defined at the top of this file) and + returns a valid curses attr that can be passed directly to attron() or attroff() + """ + # extract the color from that tuple + if len(color_tuple) == 3: + colors = (color_tuple[0], color_tuple[1]) + else: + colors = color_tuple + + bold = False + if curses.COLORS != 256: + # We are not in a term supporting 256 colors, so we convert + # colors to numbers between -1 and 8 + colors = (color_256_to_16(colors[0]), color_256_to_16(colors[1])) + if colors[0] >= 8: + colors = (colors[0] - 8, colors[1]) + bold = True + if colors[1] >= 8: + colors = (colors[0], colors[1] - 8) + + # check if we already used these colors + try: + pair = curses_colors_dict[colors] + except KeyError: + pair = len(curses_colors_dict) + 1 + curses.init_pair(pair, colors[0], colors[1]) + curses_colors_dict[colors] = pair + curses_pair = curses.color_pair(pair) + if len(color_tuple) == 3: + additional_val = color_tuple[2] + if 'b' in additional_val or bold is True: + curses_pair = curses_pair | curses.A_BOLD + if 'u' in additional_val: + curses_pair = curses_pair | curses.A_UNDERLINE + if 'a' in additional_val: + curses_pair = curses_pair | curses.A_BLINK + return curses_pair + +def get_theme(): + """ + Returns the current theme + """ + return theme + +def reload_theme(): + themes_dir = config.get('themes_dir', '') + themes_dir = themes_dir or\ + os.path.join(os.environ.get('XDG_DATA_HOME') or\ + os.path.join(os.environ.get('HOME'), '.local', 'share'), + 'poezio', 'themes') + try: + os.makedirs(themes_dir) + except OSError: + pass + theme_name = config.get('theme', '') + if not theme_name: + return + try: + file_path = os.path.join(themes_dir, theme_name)+'.py' + log.debug('Theme file to load: %s' %(file_path,)) + new_theme = imp.load_source('theme', os.path.join(themes_dir, theme_name)+'.py') + except: # TODO warning: theme not found + return + global theme + theme = new_theme.theme + +if __name__ == '__main__': + """ + Display some nice text with nice colors + """ + s = curses.initscr() + curses.start_color() + curses.use_default_colors() + s.addstr('%s' % curses.COLORS, to_curses_attr((3, -1, 'a'))) + s.refresh() + s.getkey() + curses.endwin() diff --git a/src/user.py b/src/user.py index 0fe0bad4..5867e1f3 100644 --- a/src/user.py +++ b/src/user.py @@ -10,11 +10,13 @@ Define the user class. An user is a MUC participant, not a roster contact (see contact.py) """ +import curses + from random import randrange, choice from config import config from datetime import timedelta, datetime -import curses -import theme + +from theming import get_theme ROLE_DICT = { '':0, @@ -32,7 +34,7 @@ class User(object): self.last_talked = datetime(1, 1, 1) # The oldest possible time self.update(affiliation, show, status, role) self.change_nick(nick) - self.color = choice(theme.LIST_COLOR_NICKNAMES) + self.color = choice(get_theme().LIST_COLOR_NICKNAMES) self.jid = jid self.chatstate = None diff --git a/src/windows.py b/src/windows.py index 3e607541..2352a82a 100644 --- a/src/windows.py +++ b/src/windows.py @@ -33,12 +33,14 @@ from poopt import cut_text from sleekxmpp.xmlstream.stanzabase import JID import core -import theme import common import wcwidth import singleton import collections +from theming import get_theme, to_curses_attr + +allowed_color_digits = ('0', '1', '2', '3', '4', '5', '6', '7') # msg is a reference to the corresponding Message tuple. text_start and text_end are the position # delimiting the text in this line. # first is a bool telling if this is the first line of the message. @@ -111,7 +113,7 @@ class Win(object): attributes as they are in the string. For example: \x19bhello → hello in bold - \xc1Bonj\xc2our → 'Bonj' in red and 'our' in green + \x191}Bonj\x192}our → 'Bonj' in red and 'our' in green next_attr_char is the \x19 delimiter attr_char is the char following it, it can be one of 'u', 'b', 'c[0-9]' @@ -119,6 +121,37 @@ class Win(object): if y is not None and x is not None: self.move(y, x) next_attr_char = text.find('\x19') + while next_attr_char != -1 and text: + log.debug('Addstr_Colored: [%s]' % text.replace('\x19', '\\x19')) + if next_attr_char + 1 < len(text): + attr_char = text[next_attr_char+1].lower() + else: + attr_char = str() + if next_attr_char != 0: + self.addstr(text[:next_attr_char]) + if attr_char == 'o': + self._win.attrset(0) + elif attr_char == 'u': + self._win.attron(curses.A_UNDERLINE) + elif attr_char == 'b': + self._win.attron(curses.A_BOLD) + if attr_char in string.digits and attr_char != '': + color_str = text[next_attr_char+1:text.find('}', next_attr_char)] + self._win.attron(to_curses_attr((int(color_str), -1))) + text = text[next_attr_char+len(color_str)+2:] + else: + text = text[next_attr_char+2:] + next_attr_char = text.find('\x19') + self.addstr(text) + + def addstr_colored_lite(self, text, y=None, x=None): + """ + Just like addstr_colored, but only handles colors with one digit. + \x193 is the 3rd color. We do not use any } char in this version + """ + if y is not None and x is not None: + self.move(y, x) + next_attr_char = text.find('\x19') while next_attr_char != -1: if next_attr_char + 1 < len(text): attr_char = text[next_attr_char+1].lower() @@ -134,7 +167,7 @@ class Win(object): elif attr_char == 'b': self._win.attron(curses.A_BOLD) elif attr_char in string.digits and attr_char != '': - self._win.attron(common.curses_color_pair(int(attr_char))) + self._win.attron(to_curses_attr((int(attr_char), -1))) next_attr_char = text.find('\x19') self.addstr(text) @@ -145,7 +178,7 @@ class Win(object): (y, x) = self._win.getyx() size = self.width-x if color: - self.addnstr(' '*size, size, common.curses_color_pair(color)) + self.addnstr(' '*size, size, to_curses_attr(color)) else: self.addnstr(' '*size, size) @@ -159,18 +192,18 @@ class UserList(Win): def __init__(self): Win.__init__(self) self.pos = 0 - self.color_role = {'moderator': theme.COLOR_USER_MODERATOR, - 'participant':theme.COLOR_USER_PARTICIPANT, - 'visitor':theme.COLOR_USER_VISITOR, - 'none':theme.COLOR_USER_NONE, - '':theme.COLOR_USER_NONE + self.color_role = {'moderator': get_theme().COLOR_USER_MODERATOR, + 'participant':get_theme().COLOR_USER_PARTICIPANT, + 'visitor':get_theme().COLOR_USER_VISITOR, + 'none':get_theme().COLOR_USER_NONE, + '':get_theme().COLOR_USER_NONE } - self.color_show = {'xa':theme.COLOR_STATUS_XA, - 'none':theme.COLOR_STATUS_NONE, - '':theme.COLOR_STATUS_NONE, - 'dnd':theme.COLOR_STATUS_DND, - 'away':theme.COLOR_STATUS_AWAY, - 'chat':theme.COLOR_STATUS_CHAT + self.color_show = {'xa':get_theme().COLOR_STATUS_XA, + 'none':get_theme().COLOR_STATUS_NONE, + '':get_theme().COLOR_STATUS_NONE, + 'dnd':get_theme().COLOR_STATUS_DND, + 'away':get_theme().COLOR_STATUS_AWAY, + 'chat':get_theme().COLOR_STATUS_CHAT } def scroll_up(self): @@ -182,7 +215,7 @@ class UserList(Win): self.pos = 0 def draw_plus(self, y): - self.addstr(y, self.width-2, '++', common.curses_color_pair(theme.COLOR_MORE_INDICATOR)) + self.addstr(y, self.width-2, '++', to_curses_attr(get_theme().COLOR_MORE_INDICATOR)) def refresh(self, users): log.debug('Refresh: %s'%self.__class__.__name__) @@ -194,11 +227,11 @@ class UserList(Win): self.pos = len(users)-1 for user in users[self.pos:]: if not user.role in self.color_role: - role_col = theme.COLOR_USER_NONE + role_col = get_theme().COLOR_USER_NONE else: role_col = self.color_role[user.role] if not user.show in self.color_show: - show_col = theme.COLOR_STATUS_NONE + show_col = get_theme().COLOR_STATUS_NONE else: show_col = self.color_show[user.show] if user.chatstate == 'composing': @@ -208,9 +241,9 @@ class UserList(Win): elif user.chatstate == 'paused': char = 'p' else: - char = theme.CHAR_STATUS - self.addstr(y, 0, char, common.curses_color_pair(show_col)) - self.addstr(y, 1, user.nick[:self.width-2], common.curses_color_pair(role_col)) + char = get_theme().CHAR_STATUS + self.addstr(y, 0, char, to_curses_attr(show_col)) + self.addstr(y, 1, user.nick[:self.width-2], to_curses_attr(role_col)) y += 1 if y == self.height: break @@ -223,9 +256,9 @@ class UserList(Win): def resize(self, height, width, y, x): self._resize(height, width, y, x) - self._win.attron(common.curses_color_pair(theme.COLOR_VERTICAL_SEPARATOR)) + self._win.attron(to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR)) self._win.vline(0, 0, curses.ACS_VLINE, self.height) - self._win.attroff(common.curses_color_pair(theme.COLOR_VERTICAL_SEPARATOR)) + self._win.attroff(to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR)) class Topic(Win): def __init__(self): @@ -240,12 +273,12 @@ class Topic(Win): msg = topic[:self.width-1] else: msg = self._message[:self.width-1] - self.addstr(0, 0, msg, common.curses_color_pair(theme.COLOR_TOPIC_BAR)) + self.addstr(0, 0, msg, to_curses_attr(get_theme().COLOR_TOPIC_BAR)) (y, x) = self._win.getyx() remaining_size = self.width - x if remaining_size: self.addnstr(' '*remaining_size, remaining_size, - common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) self._refresh() def set_message(self, message): @@ -262,24 +295,24 @@ class GlobalInfoBar(Win): comp = lambda x: x.nb with g_lock: self._win.erase() - self.addstr(0, 0, "[", common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr(0, 0, "[", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) sorted_tabs = sorted(self.core.tabs, key=comp) for tab in sorted_tabs: color = tab.get_color_state() if config.get('show_inactive_tabs', 'true') == 'false' and\ - color == theme.COLOR_TAB_NORMAL: + color == get_theme().COLOR_TAB_NORMAL: continue try: - self.addstr("%s" % str(tab.nb), common.curses_color_pair(color)) - self.addstr("|", common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr("%s" % str(tab.nb), to_curses_attr(color)) + self.addstr("|", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) except: # end of line break (y, x) = self._win.getyx() - self.addstr(y, x-1, '] ', common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr(y, x-1, '] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) (y, x) = self._win.getyx() remaining_size = self.width - x self.addnstr(' '*remaining_size, remaining_size, - common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) self._refresh() class InfoWin(Win): @@ -298,7 +331,7 @@ class InfoWin(Win): """ if window.pos > 0: plus = ' -PLUS(%s)-' % window.pos - self.addstr(plus, common.curses_color_pair(theme.COLOR_SCROLLABLE_NUMBER)) + self.addstr(plus, to_curses_attr(get_theme().COLOR_SCROLLABLE_NUMBER)) class PrivateInfoWin(InfoWin): """ @@ -315,33 +348,33 @@ class PrivateInfoWin(InfoWin): self.write_room_name(room) self.print_scroll_position(window) self.write_chatstate(chatstate) - self.finish_line(theme.COLOR_INFORMATION_BAR) + self.finish_line(get_theme().COLOR_INFORMATION_BAR) self._refresh() def write_room_name(self, room): jid = JID(room.name) room_name, nick = jid.bare, jid.resource - self.addstr(nick, common.curses_color_pair(theme.COLOR_PRIVATE_NAME)) + self.addstr(nick, to_curses_attr(get_theme().COLOR_PRIVATE_NAME)) txt = ' from room %s' % room_name - self.addstr(txt, common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr(txt, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_chatstate(self, state): if state: - self.addstr(' %s' % (state,), common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr(' %s' % (state,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) class ConversationInfoWin(InfoWin): """ The line above the information window, displaying informations about the user we are talking to """ - color_show = {'xa':theme.COLOR_STATUS_XA, - 'none':theme.COLOR_STATUS_ONLINE, - '':theme.COLOR_STATUS_ONLINE, - 'available':theme.COLOR_STATUS_ONLINE, - 'dnd':theme.COLOR_STATUS_DND, - 'away':theme.COLOR_STATUS_AWAY, - 'chat':theme.COLOR_STATUS_CHAT, - 'unavailable':theme.COLOR_STATUS_UNAVAILABLE + color_show = {'xa':get_theme().COLOR_STATUS_XA, + 'none':get_theme().COLOR_STATUS_ONLINE, + '':get_theme().COLOR_STATUS_ONLINE, + 'available':get_theme().COLOR_STATUS_ONLINE, + 'dnd':get_theme().COLOR_STATUS_DND, + 'away':get_theme().COLOR_STATUS_AWAY, + 'chat':get_theme().COLOR_STATUS_CHAT, + 'unavailable':get_theme().COLOR_STATUS_UNAVAILABLE } def __init__(self): @@ -372,7 +405,7 @@ class ConversationInfoWin(InfoWin): self.write_resource_information(resource) self.print_scroll_position(window) self.write_chatstate(chatstate) - self.finish_line(theme.COLOR_INFORMATION_BAR) + self.finish_line(get_theme().COLOR_INFORMATION_BAR) self._refresh() def write_resource_information(self, resource): @@ -384,31 +417,31 @@ class ConversationInfoWin(InfoWin): else: presence = resource.get_presence() color = RosterWin.color_show[presence] - self.addstr('[', common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) - self.addstr(" ", common.curses_color_pair(color)) - self.addstr(']', common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(" ", to_curses_attr(color)) + self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_contact_informations(self, contact): """ Write the informations about the contact """ if not contact: - self.addstr("(contact not in roster)", common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr("(contact not in roster)", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) return display_name = contact.get_name() or contact.get_bare_jid() - self.addstr('%s '%(display_name), common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr('%s '%(display_name), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_contact_jid(self, jid): """ Just write the jid that we are talking to """ - self.addstr('[', common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) - self.addstr(jid.full, common.curses_color_pair(theme.COLOR_CONVERSATION_NAME)) - self.addstr('] ', common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(jid.full, to_curses_attr(get_theme().COLOR_CONVERSATION_NAME)) + self.addstr('] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_chatstate(self, state): if state: - self.addstr(' %s' % (state,), common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr(' %s' % (state,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) class ConversationStatusMessageWin(InfoWin): """ @@ -431,11 +464,11 @@ class ConversationStatusMessageWin(InfoWin): self._win.erase() if resource: self.write_status_message(resource) - self.finish_line(theme.COLOR_INFORMATION_BAR) + self.finish_line(get_theme().COLOR_INFORMATION_BAR) self._refresh() def write_status_message(self, resource): - self.addstr(resource.get_status(), common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr(resource.get_status(), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) class MucInfoWin(InfoWin): """ @@ -455,20 +488,20 @@ class MucInfoWin(InfoWin): self.write_role(room) if window: self.print_scroll_position(window) - self.finish_line(theme.COLOR_INFORMATION_BAR) + self.finish_line(get_theme().COLOR_INFORMATION_BAR) self._refresh() def write_room_name(self, room): - self.addstr('[', common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) - self.addstr(room.name, common.curses_color_pair(theme.COLOR_GROUPCHAT_NAME)) - self.addstr('] ', common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(room.name, to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME)) + self.addstr('] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_disconnected(self, room): """ Shows a message if the room is not joined """ if not room.joined: - self.addstr(' -!- Not connected ', common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr(' -!- Not connected ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_own_nick(self, room): """ @@ -477,7 +510,7 @@ class MucInfoWin(InfoWin): nick = room.own_nick if not nick: return - self.addstr(truncate_nick(nick, 13), common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr(truncate_nick(nick, 13), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_role(self, room): """ @@ -494,7 +527,7 @@ class MucInfoWin(InfoWin): if own_user.affiliation != 'none': txt += own_user.affiliation+', ' txt += own_user.role+')' - self.addstr(txt, common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr(txt, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) class TextWin(Win): def __init__(self, lines_nb_limit=config.get('max_lines_in_memory', 2048)): @@ -559,9 +592,9 @@ class TextWin(Win): offset = 1 + len(message.str_time) if nick: offset += wcwidth.wcswidth(nick) + 2 # + nick + spaces length - if theme.CHAR_TIME_LEFT: + if get_theme().CHAR_TIME_LEFT: offset += 1 - if theme.CHAR_TIME_RIGHT: + if get_theme().CHAR_TIME_RIGHT: offset += 1 lines = cut_text(txt, self.width-offset) for line in lines: @@ -610,7 +643,7 @@ class TextWin(Win): self._refresh() def write_line_separator(self): - self.addnstr('- '*(self.width//2-1)+'-', self.width, common.curses_color_pair(theme.COLOR_NEW_TEXT_SEPARATOR)) + self.addnstr('- '*(self.width//2-1)+'-', self.width, to_curses_attr(get_theme().COLOR_NEW_TEXT_SEPARATOR)) def write_text(self, y, x, txt): """ @@ -626,10 +659,10 @@ class TextWin(Win): if not nickname: return if color: - self._win.attron(common.curses_color_pair(color)) + self._win.attron(to_curses_attr(color)) self.addstr(truncate_nick(nickname)) if color: - self._win.attroff(common.curses_color_pair(color)) + self._win.attroff(to_curses_attr(color)) self.addstr("> ") def write_time(self, time): @@ -672,8 +705,8 @@ class HelpText(Win): self.txt = txt with g_lock: self._win.erase() - self.addstr(0, 0, self.txt[:self.width-1], common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) - self.finish_line(theme.COLOR_INFORMATION_BAR) + self.addstr(0, 0, self.txt[:self.width-1], to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.finish_line(get_theme().COLOR_INFORMATION_BAR) self._refresh() def do_command(self, key): @@ -1034,16 +1067,16 @@ class Input(Win): text = self.text.replace('\n', '|') self._win.erase() if self.color: - self._win.attron(common.curses_color_pair(self.color)) + self._win.attron(to_curses_attr(self.color)) displayed_text = text[self.line_pos:self.line_pos+self.width-1] self.addstr(displayed_text) if self.color: (y, x) = self._win.getyx() size = self.width-x - self.addnstr(' '*size, size, common.curses_color_pair(self.color)) + self.addnstr(' '*size, size, to_curses_attr(self.color)) self.addstr(0, wcwidth.wcswidth(displayed_text[:self.pos]), '') if self.color: - self._win.attroff(common.curses_color_pair(self.color)) + self._win.attroff(to_curses_attr(self.color)) self._refresh() def refresh(self): @@ -1100,7 +1133,7 @@ class MessageInput(Input): Read one more char (c) and add \x19c to the string """ attr_char = self.core.read_keyboard()[0] - if attr_char in self.text_attributes or (attr_char in string.digits and int(attr_char) < 7): + if attr_char in self.text_attributes or attr_char in allowed_color_digits: self.do_command('\x19', False) self.do_command(attr_char) @@ -1138,13 +1171,13 @@ class MessageInput(Input): text = self.text.replace('\n', '|') self._win.erase() if self.color: - self._win.attron(common.curses_color_pair(self.color)) + self._win.attron(to_curses_attr(self.color)) displayed_text = text[self.line_pos:self.line_pos+self.width-1] self._win.attrset(0) - self.addstr_colored(displayed_text) + self.addstr_colored_lite(displayed_text) self.addstr(0, wcwidth.wcswidth(displayed_text[:self.pos]), '') if self.color: - self._win.attroff(common.curses_color_pair(self.color)) + self._win.attroff(to_curses_attr(self.color)) self._refresh() class CommandInput(Input): @@ -1201,7 +1234,7 @@ class CommandInput(Input): """ with g_lock: self._win.erase() - self.addstr(self.help_message, common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr(self.help_message, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) cursor_pos = self.pos + len(self.help_message) if len(self.help_message): self.addstr(' ') @@ -1276,7 +1309,7 @@ class VerticalSeparator(Win): def rewrite_line(self): with g_lock: - self._win.vline(0, 0, curses.ACS_VLINE, self.height, common.curses_color_pair(theme.COLOR_VERTICAL_SEPARATOR)) + self._win.vline(0, 0, curses.ACS_VLINE, self.height, to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR)) self._refresh() def refresh(self): @@ -1284,14 +1317,14 @@ class VerticalSeparator(Win): self.rewrite_line() class RosterWin(Win): - color_show = {'xa':theme.COLOR_STATUS_XA, - 'none':theme.COLOR_STATUS_ONLINE, - '':theme.COLOR_STATUS_ONLINE, - 'available':theme.COLOR_STATUS_ONLINE, - 'dnd':theme.COLOR_STATUS_DND, - 'away':theme.COLOR_STATUS_AWAY, - 'chat':theme.COLOR_STATUS_CHAT, - 'unavailable':theme.COLOR_STATUS_UNAVAILABLE + color_show = {'xa':get_theme().COLOR_STATUS_XA, + 'none':get_theme().COLOR_STATUS_ONLINE, + '':get_theme().COLOR_STATUS_ONLINE, + 'available':get_theme().COLOR_STATUS_ONLINE, + 'dnd':get_theme().COLOR_STATUS_DND, + 'away':get_theme().COLOR_STATUS_AWAY, + 'chat':get_theme().COLOR_STATUS_CHAT, + 'unavailable':get_theme().COLOR_STATUS_UNAVAILABLE } def __init__(self): @@ -1388,22 +1421,22 @@ class RosterWin(Win): Draw the indicator that shows that the list is longer than what is displayed """ - self.addstr(y, self.width-5, '++++', common.curses_color_pair(theme.COLOR_MORE_INDICATOR)) + self.addstr(y, self.width-5, '++++', to_curses_attr(get_theme().COLOR_MORE_INDICATOR)) def draw_roster_information(self, roster): """ The header at the top """ self.addstr('Roster: %s/%s contacts' % (roster.get_nb_connected_contacts(), roster.get_contact_len())\ - , common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) - self.finish_line(theme.COLOR_INFORMATION_BAR) + , to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.finish_line(get_theme().COLOR_INFORMATION_BAR) def draw_group(self, y, group, colored): """ Draw a groupname on a line """ if colored: - self._win.attron(common.curses_color_pair(theme.COLOR_SELECTED_ROW)) + self._win.attron(to_curses_attr(get_theme().COLOR_SELECTED_ROW)) if group.folded: self.addstr(y, 0, '[+] ') else: @@ -1411,7 +1444,7 @@ class RosterWin(Win): contacts = " (%s/%s)" % (group.get_nb_connected_contacts(), len(group)) self.addstr(y, 4, group.name + contacts) if colored: - self._win.attroff(common.curses_color_pair(theme.COLOR_SELECTED_ROW)) + self._win.attroff(to_curses_attr(get_theme().COLOR_SELECTED_ROW)) self.finish_line() def draw_contact_line(self, y, contact, colored): @@ -1438,16 +1471,16 @@ class RosterWin(Win): else: display_name = '%s%s' % (contact.get_bare_jid(), nb,) self.addstr(y, 0, ' ') - self.addstr(" ", common.curses_color_pair(color)) + self.addstr(" ", to_curses_attr(color)) if resource: self.addstr(' [+]' if contact._folded else ' [-]') self.addstr(' ') if colored: - self.addstr(display_name, common.curses_color_pair(theme.COLOR_SELECTED_ROW)) + self.addstr(display_name, to_curses_attr(get_theme().COLOR_SELECTED_ROW)) else: self.addstr(display_name) if contact.get_ask() == 'asked': - self.addstr('?', common.curses_color_pair(theme.COLOR_HIGHLIGHT_NICK)) + self.addstr('?', to_curses_attr(get_theme().COLOR_HIGHLIGHT_NICK)) self.finish_line() def draw_resource_line(self, y, resource, colored): @@ -1455,9 +1488,9 @@ class RosterWin(Win): Draw a specific resource line """ color = RosterWin.color_show[resource.get_presence()] - self.addstr(y, 4, " ", common.curses_color_pair(color)) + self.addstr(y, 4, " ", to_curses_attr(color)) if colored: - self.addstr(y, 6, resource.get_jid().full, common.curses_color_pair(theme.COLOR_SELECTED_ROW)) + self.addstr(y, 6, resource.get_jid().full, to_curses_attr(get_theme().COLOR_SELECTED_ROW)) else: self.addstr(y, 6, resource.get_jid().full) self.finish_line() @@ -1482,12 +1515,12 @@ class ContactInfoWin(Win): presence = resource.get_presence() else: presence = 'unavailable' - self.addstr(0, 0, '%s (%s)'%(jid, presence,), common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) - self.finish_line(theme.COLOR_INFORMATION_BAR) + self.addstr(0, 0, '%s (%s)'%(jid, presence,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.finish_line(get_theme().COLOR_INFORMATION_BAR) self.addstr(1, 0, 'Subscription: %s' % (contact.get_subscription(),)) if contact.get_ask(): if contact.get_ask() == 'asked': - self.addstr(' Ask: %s' % (contact.get_ask(),), common.curses_color_pair(theme.COLOR_HIGHLIGHT_NICK)) + self.addstr(' Ask: %s' % (contact.get_ask(),), to_curses_attr(get_theme().COLOR_HIGHLIGHT_NICK)) else: self.addstr(' Ask: %s' % (contact.get_ask(),)) self.finish_line() @@ -1497,8 +1530,8 @@ class ContactInfoWin(Win): """ draw the group information """ - self.addstr(0, 0, group.name, common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) - self.finish_line(theme.COLOR_INFORMATION_BAR) + self.addstr(0, 0, group.name, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.finish_line(get_theme().COLOR_INFORMATION_BAR) def refresh(self, selected_row): log.debug('Refresh: %s'%self.__class__.__name__) @@ -1581,7 +1614,7 @@ class ListWin(Win): if not txt: continue if line is self.lines[self._selected_row]: - self.addstr(y, x, txt[:size], common.curses_color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr(y, x, txt[:size], to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) else: self.addstr(y, x, txt[:size]) x += size @@ -1659,7 +1692,7 @@ class ColumnHeaderWin(Win): txt = col size = self._columns_sizes[col] txt += ' ' * (size-len(txt)) - self.addstr(0, x, txt, common.curses_color_pair(theme.COLOR_COLUMN_HEADER)) + self.addstr(0, x, txt, to_curses_attr(get_theme().COLOR_COLUMN_HEADER)) x += size self._refresh() diff --git a/src/xhtml.py b/src/xhtml.py index 8f629e3b..9bb2705d 100644 --- a/src/xhtml.py +++ b/src/xhtml.py @@ -22,12 +22,159 @@ from config import config import logging digits = '0123456789' # never trust the modules +colors = { + 'aliceblue': 231, + 'antiquewhite': 231, + 'aqua': 51, + 'aquamarine': 122, + 'azure': 231, + 'beige': 231, + 'bisque': 230, + 'black': 232, + 'blanchedalmond': 230, + 'blue': 21, + 'blueviolet': 135, + 'brown': 124, + 'burlywood': 223, + 'cadetblue': 109, + 'chartreuse': 118, + 'chocolate': 172, + 'coral': 209, + 'cornflowerblue': 111, + 'cornsilk': 231, + 'crimson': 197, + 'cyan': 51, + 'darkblue': 19, + 'darkcyan': 37, + 'darkgoldenrod': 178, + 'darkgray': 247, + 'darkgreen': 28, + 'darkgrey': 247, + 'darkkhaki': 186, + 'darkmagenta': 127, + 'darkolivegreen': 65, + 'darkorange': 214, + 'darkorchid': 134, + 'darkred': 124, + 'darksalmon': 216, + 'darkseagreen': 151, + 'darkslateblue': 61, + 'darkslategray': 59, + 'darkslategrey': 59, + 'darkturquoise': 44, + 'darkviolet': 128, + 'deeppink': 199, + 'deepskyblue': 45, + 'dimgray': 241, + 'dimgrey': 241, + 'dodgerblue': 39, + 'firebrick': 160, + 'floralwhite': 231, + 'forestgreen': 34, + 'fuchsia': 201, + 'gainsboro': 252, + 'ghostwhite': 231, + 'gold': 226, + 'goldenrod': 214, + 'gray': 244, + 'green': 34, + 'greenyellow': 191, + 'grey': 244, + 'honeydew': 231, + 'hotpink': 212, + 'indianred': 174, + 'indigo': 55, + 'ivory': 231, + 'khaki': 229, + 'lavender': 231, + 'lavenderblush': 231, + 'lawngreen': 118, + 'lemonchiffon': 230, + 'lightblue': 195, + 'lightcoral': 217, + 'lightcyan': 231, + 'lightgoldenrodyellow': 230, + 'lightgray': 251, + 'lightgreen': 157, + 'lightgrey': 251, + 'lightpink': 224, + 'lightsalmon': 216, + 'lightseagreen': 43, + 'lightskyblue': 153, + 'lightslategray': 109, + 'lightslategrey': 109, + 'lightsteelblue': 189, + 'lightyellow': 231, + 'lime': 46, + 'limegreen': 77, + 'linen': 231, + 'magenta': 201, + 'maroon': 124, + 'mediumaquamarine': 115, + 'mediumblue': 20, + 'mediumorchid': 170, + 'mediumpurple': 141, + 'mediumseagreen': 78, + 'mediumslateblue': 105, + 'mediumspringgreen': 49, + 'mediumturquoise': 80, + 'mediumvioletred': 163, + 'midnightblue': 18, + 'mintcream': 231, + 'mistyrose': 231, + 'moccasin': 230, + 'navajowhite': 230, + 'navy': 19, + 'oldlace': 231, + 'olive': 142, + 'olivedrab': 106, + 'orange': 214, + 'orangered': 202, + 'orchid': 213, + 'palegoldenrod': 229, + 'palegreen': 157, + 'paleturquoise': 195, + 'palevioletred': 211, + 'papayawhip': 231, + 'peachpuff': 230, + 'peru': 179, + 'pink': 224, + 'plum': 219, + 'powderblue': 195, + 'purple': 127, + 'red': 196, + 'rosybrown': 181, + 'royalblue': 69, + 'saddlebrown': 130, + 'salmon': 216, + 'sandybrown': 216, + 'seagreen': 72, + 'seashell': 231, + 'sienna': 131, + 'silver': 250, + 'skyblue': 153, + 'slateblue': 104, + 'slategray': 109, + 'slategrey': 109, + 'snow': 231, + 'springgreen': 48, + 'steelblue': 74, + 'tan': 187, + 'teal': 37, + 'thistle': 225, + 'tomato': 209, + 'turquoise': 86, + 'violet': 219, + 'wheat': 230, + 'white': 255, + 'whitesmoke': 255, + 'yellow': 226, + 'yellowgreen': 149 +} log = logging.getLogger(__name__) -shell_colors_re = re.compile(r'(\[(?:\d+;)*(?:\d+m))') -start_indent_re = re.compile(r'\[0;30m\[0;37m ') -newline_indent_re = re.compile('\n\[0;37m ') +whitespace_re = re.compile(r'\s+') def get_body_from_message_stanza(message): """ @@ -41,31 +188,38 @@ def get_body_from_message_stanza(message): return xhtml_to_poezio_colors(xhtml_body) return message['body'] - def xhtml_to_poezio_colors(text): def parse_css(css): - def get_color(string): - if value == 'black': - return 0 - if value == 'red': - return 1 - if value == 'green': - return 2 - if value == 'yellow': - return 3 - if value == 'blue': - return 4 - if value == 'magenta': - return 5 - if value == 'cyan': - return 6 - if value == 'white': - return 7 - if value == 'default': - return 8 + def get_color(value): + if value[0] == '#': + value = value[1:] + length = len(value) + if length != 3 and length != 6: + return -1 + value = int(value, 16) + if length == 6: + r = int(value >> 16) + g = int((value >> 8) & 0xff) + b = int(value & 0xff) + if r == g == b: + return 232 + int(r/10.6251) + div = 42.51 + else: + r = int(value >> 8) + g = int((value >> 4) & 0xf) + b = int(value & 0xf) + if r == g == b: + return 232 + int(1.54*r) + div = 2.51 + return 6*6*int(r/div) + 6*int(g/div) + int(b/div) + 16 + if value in colors: + return colors[value] + return -1 shell = '' rules = css.split(';') for rule in rules: + if ':' not in rule: + continue key, value = rule.split(':', 1) key = key.strip() value = value.strip() @@ -73,7 +227,7 @@ def xhtml_to_poezio_colors(text): if key == 'background-color': pass#shell += '\x191' elif key == 'color': - shell += '\x19%d' % get_color(value) + shell += '\x19%d}' % get_color(value) elif key == 'font-style': shell += '\x19i' elif key == 'font-weight': @@ -89,13 +243,16 @@ def xhtml_to_poezio_colors(text): shell += '\x19a' return shell + def trim(string): + return re.sub(whitespace_re, ' ', string) + log.debug(text) xml = ET.fromstring(text) message = '' for elem in xml.iter(): if elem.tag == '{http://www.w3.org/1999/xhtml}a': if 'href' in elem.attrib and elem.attrib['href'] != elem.text: - message += '\x19u%s\x19o (%s)' % (elem.attrib['href'], elem.text) + message += '\x19u%s\x19o (%s)' % (trim(elem.attrib['href']), trim(elem.text)) else: message += '\x19u' + elem.text + '\x19o' elif elem.tag == '{http://www.w3.org/1999/xhtml}blockquote': @@ -109,8 +266,8 @@ def xhtml_to_poezio_colors(text): elif elem.tag == '{http://www.w3.org/1999/xhtml}em': message += '\x19i' elif elem.tag == '{http://www.w3.org/1999/xhtml}img' and 'src' in elem.attrib: - if elem.attrib['alt']: - message += '%s (%s)' % (elem.attrib['src'], elem.attrib['alt']) + if 'alt' in elem.attrib: + message += '%s (%s)' % (trim(elem.attrib['src']), trim(elem.attrib['alt'])) else: message += elem.attrib['src'] elif elem.tag == '{http://www.w3.org/1999/xhtml}li': @@ -134,7 +291,7 @@ def xhtml_to_poezio_colors(text): if (elem.text and elem.tag != '{http://www.w3.org/1999/xhtml}a' and elem.tag != '{http://www.w3.org/1999/xhtml}br' and elem.tag != '{http://www.w3.org/1999/xhtml}img'): - message += elem.text + message += trim(elem.text) if ('style' in elem.attrib and elem.tag != '{http://www.w3.org/1999/xhtml}br' and elem.tag != '{http://www.w3.org/1999/xhtml}em' @@ -156,7 +313,7 @@ def xhtml_to_poezio_colors(text): message += ' [' + elem.attrib['title'] + ']' if elem.tail: - message += elem.tail + message += trim(elem.tail) return message @@ -223,7 +380,6 @@ def poezio_colors_to_html(string): res += "</p></body>" return res.replace('\n', '<br />') - def poezio_colors_to_xhtml(string): """ Generate a valid xhtml string from |