From 332a5c2553db41de777473a1e1be9cd1522c9496 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Thu, 31 Mar 2016 18:54:41 +0100 Subject: Move the src directory to poezio, for better cython compatibility. --- src/windows/__init__.py | 20 - src/windows/base_wins.py | 168 --------- src/windows/bookmark_forms.py | 278 -------------- src/windows/data_forms.py | 472 ----------------------- src/windows/funcs.py | 54 --- src/windows/info_bar.py | 106 ------ src/windows/info_wins.py | 311 --------------- src/windows/input_placeholders.py | 77 ---- src/windows/inputs.py | 768 -------------------------------------- src/windows/list.py | 236 ------------ src/windows/misc.py | 60 --- src/windows/muc.py | 143 ------- src/windows/roster_win.py | 387 ------------------- src/windows/text_win.py | 597 ----------------------------- 14 files changed, 3677 deletions(-) delete mode 100644 src/windows/__init__.py delete mode 100644 src/windows/base_wins.py delete mode 100644 src/windows/bookmark_forms.py delete mode 100644 src/windows/data_forms.py delete mode 100644 src/windows/funcs.py delete mode 100644 src/windows/info_bar.py delete mode 100644 src/windows/info_wins.py delete mode 100644 src/windows/input_placeholders.py delete mode 100644 src/windows/inputs.py delete mode 100644 src/windows/list.py delete mode 100644 src/windows/misc.py delete mode 100644 src/windows/muc.py delete mode 100644 src/windows/roster_win.py delete mode 100644 src/windows/text_win.py (limited to 'src/windows') diff --git a/src/windows/__init__.py b/src/windows/__init__.py deleted file mode 100644 index 5ec73961..00000000 --- a/src/windows/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Module exporting all the Windows, which are wrappers around curses wins -used to display information on the screen -""" - -from . base_wins import Win -from . data_forms import FormWin -from . bookmark_forms import BookmarksWin -from . info_bar import GlobalInfoBar, VerticalGlobalInfoBar -from . info_wins import InfoWin, XMLInfoWin, PrivateInfoWin, MucListInfoWin, \ - ConversationInfoWin, DynamicConversationInfoWin, MucInfoWin, \ - ConversationStatusMessageWin, BookmarksInfoWin -from . input_placeholders import HelpText, YesNoInput -from . inputs import Input, HistoryInput, MessageInput, CommandInput -from . list import ListWin, ColumnHeaderWin -from . misc import VerticalSeparator -from . muc import UserList, Topic -from . roster_win import RosterWin, ContactInfoWin -from . text_win import TextWin, XMLTextWin - diff --git a/src/windows/base_wins.py b/src/windows/base_wins.py deleted file mode 100644 index 464c6fa1..00000000 --- a/src/windows/base_wins.py +++ /dev/null @@ -1,168 +0,0 @@ -""" -Define the base window object and the constants/"globals" used -by the file of this module. - -A window is a little part of the screen, for example the input window, -the text window, the roster window, etc. -A Tab (see the src/tabs module) is composed of multiple Windows -""" - -import logging -log = logging.getLogger(__name__) - -import collections -import curses -import string - -import core -import singleton -from theming import to_curses_attr, read_tuple - -FORMAT_CHAR = '\x19' -# These are non-printable chars, so they should never appear in the input, -# I guess. But maybe we can find better chars that are even less risky. -format_chars = ['\x0E', '\x0F', '\x10', '\x11', '\x12', '\x13', - '\x14', '\x15', '\x16', '\x17', '\x18'] - -# different colors allowed in the input -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. -Line = collections.namedtuple('Line', 'msg start_pos end_pos prepend') - -LINES_NB_LIMIT = 4096 - -class DummyWin(object): - def __getattribute__(self, name): - if name != '__bool__': - return lambda *args, **kwargs: (0, 0) - else: - return object.__getattribute__(self, name) - - def __bool__(self): - return False - -class Win(object): - _win_core = None - _tab_win = None - def __init__(self): - self._win = None - self.height, self.width = 0, 0 - - def _resize(self, height, width, y, x): - if height == 0 or width == 0: - self.height, self.width = height, width - return - self.height, self.width, self.x, self.y = height, width, x, y - try: - self._win = Win._tab_win.derwin(height, width, y, x) - except: - log.debug('DEBUG: mvwin returned ERR. Please investigate') - if self._win is None: - self._win = DummyWin() - - def resize(self, height, width, y, x): - """ - Override if something has to be done on resize - """ - self._resize(height, width, y, x) - - def _refresh(self): - self._win.noutrefresh() - - def addnstr(self, *args): - """ - Safe call to addnstr - """ - try: - self._win.addnstr(*args) - except: - # this actually mostly returns ERR, but works. - # more specifically, when the added string reaches the end - # of the screen. - pass - - def addstr(self, *args): - """ - Safe call to addstr - """ - try: - self._win.addstr(*args) - except: - pass - - def move(self, y, x): - try: - self._win.move(y, x) - except: - self._win.move(0, 0) - - def addstr_colored(self, text, y=None, x=None): - """ - Write a string on the window, setting the - attributes as they are in the string. - For example: - \x19bhello → hello in bold - \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]' - """ - if y is not None and x is not None: - self.move(y, x) - next_attr_char = text.find(FORMAT_CHAR) - while next_attr_char != -1 and text: - 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 or attr_char == '-') and attr_char != '': - color_str = text[next_attr_char+1:text.find('}', next_attr_char)] - if ',' in color_str: - tup, char = read_tuple(color_str) - self._win.attron(to_curses_attr(tup)) - if char: - if char == 'o': - self._win.attrset(0) - elif char == 'u': - self._win.attron(curses.A_UNDERLINE) - elif char == 'b': - self._win.attron(curses.A_BOLD) - else: - # this will reset previous bold/uderline sequences if any was used - self._win.attroff(curses.A_UNDERLINE) - self._win.attroff(curses.A_BOLD) - elif color_str: - 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(FORMAT_CHAR) - self.addstr(text) - - def finish_line(self, color=None): - """ - Write colored spaces until the end of line - """ - (y, x) = self._win.getyx() - size = self.width - x - if color: - self.addnstr(' '*size, size, to_curses_attr(color)) - else: - self.addnstr(' '*size, size) - - @property - def core(self): - if not Win._win_core: - Win._win_core = singleton.Singleton(core.Core) - return Win._win_core - diff --git a/src/windows/bookmark_forms.py b/src/windows/bookmark_forms.py deleted file mode 100644 index de1043c9..00000000 --- a/src/windows/bookmark_forms.py +++ /dev/null @@ -1,278 +0,0 @@ -""" -Windows used inthe bookmarkstab -""" -import curses - -from . import Win -from . inputs import Input -from . data_forms import FieldInput -from theming import to_curses_attr, get_theme -from common import safeJID - -class BookmarkJIDInput(FieldInput, Input): - def __init__(self, field): - FieldInput.__init__(self, field) - Input.__init__(self) - jid = safeJID(field.jid) - jid.resource = field.nick or None - self.text = jid.full - self.pos = len(self.text) - self.color = get_theme().COLOR_NORMAL_TEXT - - def save(self): - jid = safeJID(self.get_text()) - self._field.jid = jid.bare - self._field.name = jid.bare - self._field.nick = jid.resource - - def get_help_message(self): - return 'Edit the text' - -class BookmarkMethodInput(FieldInput, Win): - def __init__(self, field): - FieldInput.__init__(self, field) - Win.__init__(self) - self.options = ('local', 'remote') - # val_pos is the position of the currently selected option - self.val_pos = self.options.index(field.method) - - def do_command(self, key): - if key == 'KEY_LEFT': - if self.val_pos > 0: - self.val_pos -= 1 - elif key == 'KEY_RIGHT': - if self.val_pos < len(self.options)-1: - self.val_pos += 1 - else: - return - self.refresh() - - def refresh(self): - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) - if self.val_pos > 0: - self.addstr(0, 0, '←') - if self.val_pos < len(self.options)-1: - self.addstr(0, self.width-1, '→') - if self.options: - option = self.options[self.val_pos] - self.addstr(0, self.width//2-len(option)//2, option) - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - def save(self): - self._field.method = self.options[self.val_pos] - - def get_help_message(self): - return '←, →: Select a value amongst the others' - -class BookmarkPasswordInput(FieldInput, Input): - def __init__(self, field): - FieldInput.__init__(self, field) - Input.__init__(self) - self.text = field.password or '' - self.pos = len(self.text) - self.color = get_theme().COLOR_NORMAL_TEXT - - def rewrite_text(self): - self._win.erase() - if self.color: - self._win.attron(to_curses_attr(self.color)) - self.addstr('*'*len(self.text[self.view_pos:self.view_pos+self.width-1])) - if self.color: - (y, x) = self._win.getyx() - size = self.width-x - self.addnstr(' '*size, size, to_curses_attr(self.color)) - self.addstr(0, self.pos, '') - if self.color: - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - def save(self): - self._field.password = self.get_text() or None - - def get_help_message(self): - return 'Edit the secret text' - -class BookmarkAutojoinWin(FieldInput, Win): - def __init__(self, field): - FieldInput.__init__(self, field) - Win.__init__(self) - self.last_key = 'KEY_RIGHT' - self.value = field.autojoin - - def do_command(self, key): - if key == 'KEY_LEFT' or key == 'KEY_RIGHT': - self.value = not self.value - self.last_key = key - self.refresh() - - def refresh(self): - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - format_string = '←{:^%s}→' % 7 - inp = format_string.format(repr(self.value)) - self.addstr(0, 0, inp) - if self.last_key == 'KEY_RIGHT': - self.move(0, 8) - else: - self.move(0, 0) - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - def save(self): - self._field.autojoin = self.value - - def get_help_message(self): - return '← and →: change the value between True and False' - - -class BookmarksWin(Win): - def __init__(self, bookmarks, height, width, y, x): - self._win = Win._tab_win.derwin(height, width, y, x) - self.scroll_pos = 0 - self._current_input = 0 - self.current_horizontal_input = 0 - self._bookmarks = list(bookmarks) - self.lines = [] - for bookmark in sorted(self._bookmarks, key=lambda x: x.jid): - self.lines.append((BookmarkJIDInput(bookmark), - BookmarkPasswordInput(bookmark), - BookmarkAutojoinWin(bookmark), - BookmarkMethodInput(bookmark))) - - @property - def current_input(self): - return self._current_input - - @current_input.setter - def current_input(self, value): - if 0 <= self._current_input < len(self.lines): - if 0 <= value < len(self.lines): - self.lines[self._current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT) - self._current_input = value - else: - self._current_input = 0 - - def add_bookmark(self, bookmark): - self.lines.append((BookmarkJIDInput(bookmark), - BookmarkPasswordInput(bookmark), - BookmarkAutojoinWin(bookmark), - BookmarkMethodInput(bookmark))) - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT) - self.current_horizontal_input = 0 - self.current_input = len(self.lines) - 1 - if self.current_input - self.scroll_pos > self.height-1: - self.scroll_pos = self.current_input - self.height + 1 - self.refresh() - - def del_current_bookmark(self): - if self.lines: - bm = self.lines[self.current_input][0]._field - to_delete = self.current_input - self.current_input -= 1 - del self.lines[to_delete] - if self.scroll_pos: - self.scroll_pos -= 1 - self.refresh() - return bm - - def resize(self, height, width, y, x): - self.height = height - self.width = width - self._win = Win._tab_win.derwin(height, width, y, x) - # Adjust the scroll position, if resizing made the window too small - # for the cursor to be visible - while self.current_input - self.scroll_pos > self.height-1: - self.scroll_pos += 1 - - def go_to_next_line_input(self): - if not self.lines: - return - if self.current_input == len(self.lines) - 1: - return - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT) - # Adjust the scroll position if the current_input would be outside - # of the visible area - if self.current_input + 1 - self.scroll_pos > self.height-1: - self.current_input += 1 - self.scroll_pos += 1 - self.refresh() - else: - self.current_input += 1 - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW) - - def go_to_previous_line_input(self): - if not self.lines: - return - if self.current_input == 0: - return - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT) - self.current_input -= 1 - # Adjust the scroll position if the current_input would be outside - # of the visible area - if self.current_input < self.scroll_pos: - self.scroll_pos = self.current_input - self.refresh() - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW) - - def go_to_next_horizontal_input(self): - if not self.lines: - return - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT) - self.current_horizontal_input += 1 - if self.current_horizontal_input > 3: - self.current_horizontal_input = 0 - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW) - - def go_to_previous_horizontal_input(self): - if not self.lines: - return - if self.current_horizontal_input == 0: - return - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT) - self.current_horizontal_input -= 1 - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW) - - def on_input(self, key): - if not self.lines: - return - self.lines[self.current_input][self.current_horizontal_input].do_command(key) - - def refresh(self): - # store the cursor status - self._win.erase() - y = - self.scroll_pos - for i in range(len(self.lines)): - self.lines[i][0].resize(1, self.width//3, y + 1, 0) - self.lines[i][1].resize(1, self.width//3, y + 1, self.width//3) - self.lines[i][2].resize(1, self.width//6, y + 1, 2*self.width//3) - self.lines[i][3].resize(1, self.width//6, y + 1, 5*self.width//6) - y += 1 - self._refresh() - for i, inp in enumerate(self.lines): - if i < self.scroll_pos: - continue - if i >= self.height + self.scroll_pos: - break - for j in range(4): - inp[j].refresh() - - if self.lines and self.current_input < self.height-1: - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW) - self.lines[self.current_input][self.current_horizontal_input].refresh() - if not self.lines: - curses.curs_set(0) - else: - curses.curs_set(1) - - def refresh_current_input(self): - if self.lines: - self.lines[self.current_input][self.current_horizontal_input].refresh() - - def save(self): - for line in self.lines: - for item in line: - item.save() - diff --git a/src/windows/data_forms.py b/src/windows/data_forms.py deleted file mode 100644 index 410648ec..00000000 --- a/src/windows/data_forms.py +++ /dev/null @@ -1,472 +0,0 @@ -""" -Windows used by the DataFormsTab. - -We only need to export the FormWin (which is not a real Win, as it -does not inherit from the Win base class), as it will create the -others when needed. -""" - -from . import Win -from . inputs import Input - -from theming import to_curses_attr, get_theme - -class FieldInput(object): - """ - All input type in a data form should inherite this class, - in addition with windows.Input or any relevant class from the - 'windows' library. - """ - def __init__(self, field): - self._field = field - self.color = get_theme().COLOR_NORMAL_TEXT - - def set_color(self, color): - self.color = color - self.refresh() - - def update_field_value(self, value): - raise NotImplementedError - - def resize(self, height, width, y, x): - self._resize(height, width, y, x) - - def is_dummy(self): - return False - - def reply(self): - """ - Set the correct response value in the field - """ - raise NotImplementedError - - def get_help_message(self): - """ - Should return a string explaining the keys of the input. - Will be displayed at each refresh on a line at the bottom of the tab. - """ - return '' - -class ColoredLabel(Win): - def __init__(self, text): - self.text = text - self.color = get_theme().COLOR_NORMAL_TEXT - Win.__init__(self) - - def resize(self, height, width, y, x): - self._resize(height, width, y, x) - - def set_color(self, color): - self.color = color - self.refresh() - - def refresh(self): - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addstr(0, 0, self.text) - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - -class DummyInput(FieldInput, Win): - """ - Used for fields that do not require any input ('fixed') - """ - def __init__(self, field): - FieldInput.__init__(self, field) - Win.__init__(self) - - def do_command(self): - return - - def refresh(self): - return - - def is_dummy(self): - return True - -class BooleanWin(FieldInput, Win): - def __init__(self, field): - FieldInput.__init__(self, field) - Win.__init__(self) - self.last_key = 'KEY_RIGHT' - self.value = bool(field.getValue()) - - def do_command(self, key): - if key == 'KEY_LEFT' or key == 'KEY_RIGHT': - self.value = not self.value - self.last_key = key - self.refresh() - - def refresh(self): - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*(8), self.width) - self.addstr(0, 2, "%s"%self.value) - self.addstr(0, 8, '→') - self.addstr(0, 0, '←') - if self.last_key == 'KEY_RIGHT': - self.addstr(0, 8, '') - else: - self.addstr(0, 0, '') - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - def reply(self): - self._field['label'] = '' - self._field.setAnswer(self.value) - - def get_help_message(self): - return '← and →: change the value between True and False' - -class TextMultiWin(FieldInput, Win): - def __init__(self, field): - FieldInput.__init__(self, field) - Win.__init__(self) - self.options = field.getValue() - if not isinstance(self.options, list): - self.options = self.options.split('\n') if self.options else [] - self.val_pos = 0 - self.edition_input = None - if not isinstance(self.options, list): - if isinstance(self.options, str): - self.options = [self.options] - else: - self.options = [] - self.options.append('') - - def do_command(self, key): - if not self.edition_input: - if key == 'KEY_LEFT': - if self.val_pos > 0: - self.val_pos -= 1 - elif key == 'KEY_RIGHT': - if self.val_pos < len(self.options)-1: - self.val_pos += 1 - elif key == '^M': - self.edition_input = Input() - self.edition_input.color = self.color - self.edition_input.resize(self.height, self.width, self.y, self.x) - self.edition_input.text = self.options[self.val_pos] - self.edition_input.key_end() - else: - if key == '^M': - self.options[self.val_pos] = self.edition_input.get_text() - if not self.options[self.val_pos] and self.val_pos != len(self.options) -1: - del self.options[self.val_pos] - if self.val_pos == len(self.options) -1: - self.val_pos -= 1 - self.edition_input = None - if not self.options or self.options[-1] != '': - self.options.append('') - else: - self.edition_input.do_command(key) - self.refresh() - - def refresh(self): - if not self.edition_input: - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) - option = self.options[self.val_pos] - self.addstr(0, self.width//2-len(option)//2, option) - if self.val_pos > 0: - self.addstr(0, 0, '←') - if self.val_pos < len(self.options)-1: - self.addstr(0, self.width-1, '→') - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - else: - self.edition_input.refresh() - - def reply(self): - values = [val for val in self.options if val] - self._field.setAnswer(values) - - def get_help_message(self): - if not self.edition_input: - help_msg = '← and →: browse the available entries. ' - if self.val_pos == len(self.options)-1: - help_msg += 'Enter: add an entry' - else: - help_msg += 'Enter: edit this entry' - else: - help_msg = 'Enter: finish editing this entry.' - return help_msg - -class ListMultiWin(FieldInput, Win): - def __init__(self, field): - FieldInput.__init__(self, field) - Win.__init__(self) - values = field.getValue() or [] - self.options = [[option, True if option['value'] in values else False]\ - for option in field.get_options()] - self.val_pos = 0 - - def do_command(self, key): - if key == 'KEY_LEFT': - if self.val_pos > 0: - self.val_pos -= 1 - elif key == 'KEY_RIGHT': - if self.val_pos < len(self.options)-1: - self.val_pos += 1 - elif key == ' ': - self.options[self.val_pos][1] = not self.options[self.val_pos][1] - else: - return - self.refresh() - - def refresh(self): - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) - if self.val_pos > 0: - self.addstr(0, 0, '←') - if self.val_pos < len(self.options)-1: - self.addstr(0, self.width-1, '→') - if self.options: - option = self.options[self.val_pos] - self.addstr(0, self.width//2-len(option)//2, option[0]['label']) - self.addstr(0, 2, '✔' if option[1] else '☐') - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - def reply(self): - self._field['label'] = '' - self._field.delOptions() - values = [option[0]['value'] for option in self.options if option[1] is True] - self._field.setAnswer(values) - - def get_help_message(self): - return '←, →: Switch between the value. Space: select or unselect a value' - -class ListSingleWin(FieldInput, Win): - def __init__(self, field): - FieldInput.__init__(self, field) - Win.__init__(self) - # the option list never changes - self.options = field.getOptions() - # val_pos is the position of the currently selected option - self.val_pos = 0 - for i, option in enumerate(self.options): - if field.getValue() == option['value']: - self.val_pos = i - - def do_command(self, key): - if key == 'KEY_LEFT': - if self.val_pos > 0: - self.val_pos -= 1 - elif key == 'KEY_RIGHT': - if self.val_pos < len(self.options)-1: - self.val_pos += 1 - else: - return - self.refresh() - - def refresh(self): - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) - if self.val_pos > 0: - self.addstr(0, 0, '←') - if self.val_pos < len(self.options)-1: - self.addstr(0, self.width-1, '→') - if self.options: - option = self.options[self.val_pos]['label'] - self.addstr(0, self.width//2-len(option)//2, option) - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - def reply(self): - self._field['label'] = '' - self._field.delOptions() - self._field.setAnswer(self.options[self.val_pos]['value']) - - def get_help_message(self): - return '←, →: Select a value amongst the others' - -class TextSingleWin(FieldInput, Input): - def __init__(self, field): - FieldInput.__init__(self, field) - Input.__init__(self) - self.text = field.getValue() if isinstance(field.getValue(), str)\ - else "" - self.pos = len(self.text) - self.color = get_theme().COLOR_NORMAL_TEXT - - def reply(self): - self._field['label'] = '' - self._field.setAnswer(self.get_text()) - - def get_help_message(self): - return 'Edit the text' - -class TextPrivateWin(TextSingleWin): - def __init__(self, field): - TextSingleWin.__init__(self, field) - - def rewrite_text(self): - self._win.erase() - if self.color: - self._win.attron(to_curses_attr(self.color)) - self.addstr('*'*len(self.text[self.view_pos:self.view_pos+self.width-1])) - if self.color: - (y, x) = self._win.getyx() - size = self.width-x - self.addnstr(' '*size, size, to_curses_attr(self.color)) - self.addstr(0, self.pos, '') - if self.color: - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - def get_help_message(self): - return 'Edit the secret text' - -class FormWin(object): - """ - A window, with some subwins (the various inputs). - On init, create all the subwins. - On resize, move and resize all the subwin and define how the text will be written - On refresh, write all the text, and refresh all the subwins - """ - input_classes = {'boolean': BooleanWin, - 'fixed': DummyInput, - 'jid-multi': TextMultiWin, - 'jid-single': TextSingleWin, - 'list-multi': ListMultiWin, - 'list-single': ListSingleWin, - 'text-multi': TextMultiWin, - 'text-private': TextPrivateWin, - 'text-single': TextSingleWin, - } - def __init__(self, form, height, width, y, x): - self._form = form - self._win = Win._tab_win.derwin(height, width, y, x) - self.scroll_pos = 0 - self.current_input = 0 - self.inputs = [] # dict list - for (name, field) in self._form.getFields().items(): - if field['type'] == 'hidden': - continue - try: - input_class = self.input_classes[field['type']] - except IndexError: - continue - label = field['label'] - desc = field['desc'] - if field['type'] == 'fixed': - label = field.getValue() - inp = input_class(field) - self.inputs.append({'label':ColoredLabel(label), - 'description': desc, - 'input':inp}) - - def resize(self, height, width, y, x): - self.height = height - self.width = width - self._win = Win._tab_win.derwin(height, width, y, x) - # Adjust the scroll position, if resizing made the window too small - # for the cursor to be visible - while self.current_input - self.scroll_pos > self.height-1: - self.scroll_pos += 1 - - def reply(self): - """ - Set the response values in the form, for each field - from the corresponding input - """ - for inp in self.inputs: - if inp['input'].is_dummy(): - continue - else: - inp['input'].reply() - self._form['title'] = '' - self._form['instructions'] = '' - - def go_to_next_input(self): - if not self.inputs: - return - if self.current_input == len(self.inputs) - 1: - return - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_NORMAL_TEXT) - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_NORMAL_TEXT) - self.current_input += 1 - jump = 0 - while self.current_input+jump != len(self.inputs) - 1 and self.inputs[self.current_input+jump]['input'].is_dummy(): - jump += 1 - if self.inputs[self.current_input+jump]['input'].is_dummy(): - return - self.current_input += jump - # If moving made the current input out of the visible screen, we - # adjust the scroll position and we redraw the whole thing. We don’t - # call refresh() if this is not the case, because - # refresh_current_input() is always called anyway, so this is not - # needed - if self.current_input - self.scroll_pos > self.height-1: - self.scroll_pos += 1 - self.refresh() - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_SELECTED_ROW) - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_SELECTED_ROW) - - def go_to_previous_input(self): - if not self.inputs: - return - if self.current_input == 0: - return - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_NORMAL_TEXT) - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_NORMAL_TEXT) - self.current_input -= 1 - jump = 0 - while self.current_input-jump > 0 and self.inputs[self.current_input+jump]['input'].is_dummy(): - jump += 1 - if self.inputs[self.current_input+jump]['input'].is_dummy(): - return - # Adjust the scroll position if the current_input would be outside - # of the visible area - if self.current_input < self.scroll_pos: - self.scroll_pos = self.current_input - self.refresh() - self.current_input -= jump - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_SELECTED_ROW) - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_SELECTED_ROW) - - def on_input(self, key): - if not self.inputs: - return - self.inputs[self.current_input]['input'].do_command(key) - - def refresh(self): - self._win.erase() - y = -self.scroll_pos - i = 0 - for name, field in self._form.getFields().items(): - if field['type'] == 'hidden': - continue - self.inputs[i]['label'].resize(1, self.width//2, y + 1, 0) - self.inputs[i]['input'].resize(1, self.width//2, y+1, self.width//2) - # TODO: display the field description - y += 1 - i += 1 - self._win.refresh() - for i, inp in enumerate(self.inputs): - if i < self.scroll_pos: - continue - if i >= self.height + self.scroll_pos: - break - inp['label'].refresh() - inp['input'].refresh() - inp['label'].refresh() - if self.inputs and self.current_input < self.height-1: - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_SELECTED_ROW) - self.inputs[self.current_input]['input'].refresh() - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_SELECTED_ROW) - self.inputs[self.current_input]['label'].refresh() - - def refresh_current_input(self): - self.inputs[self.current_input]['input'].refresh() - - def get_help_message(self): - if self.inputs and self.current_input < self.height-1 and self.inputs[self.current_input]['input']: - return self.inputs[self.current_input]['input'].get_help_message() - return '' - diff --git a/src/windows/funcs.py b/src/windows/funcs.py deleted file mode 100644 index f1401628..00000000 --- a/src/windows/funcs.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -Standalone functions used by the modules -""" - -import string - -from . base_wins import FORMAT_CHAR, format_chars - -def find_first_format_char(text, chars=None): - if chars is None: - chars = format_chars - pos = -1 - for char in chars: - p = text.find(char) - if p == -1: - continue - if pos == -1 or p < pos: - pos = p - return pos - -def truncate_nick(nick, size=10): - if size < 1: - size = 1 - if nick and len(nick) > size: - return nick[:size]+'…' - return nick - -def parse_attrs(text, previous=None): - next_attr_char = text.find(FORMAT_CHAR) - if previous: - attrs = previous - else: - attrs = [] - while next_attr_char != -1 and text: - if next_attr_char + 1 < len(text): - attr_char = text[next_attr_char+1].lower() - else: - attr_char = str() - if attr_char == 'o': - attrs = [] - elif attr_char == 'u': - attrs.append('u') - elif attr_char == 'b': - attrs.append('b') - if attr_char in string.digits and attr_char != '': - color_str = text[next_attr_char+1:text.find('}', next_attr_char)] - if color_str: - attrs.append(color_str + '}') - text = text[next_attr_char+len(color_str)+2:] - else: - text = text[next_attr_char+2:] - next_attr_char = text.find(FORMAT_CHAR) - return attrs - diff --git a/src/windows/info_bar.py b/src/windows/info_bar.py deleted file mode 100644 index abd956cd..00000000 --- a/src/windows/info_bar.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -Module defining the global info bar - -This window is the one listing the current opened tabs in poezio. -The GlobalInfoBar can be either horizontal or vertical -(VerticalGlobalInfoBar). -""" -import logging -log = logging.getLogger(__name__) - -import curses - - -from config import config -from . import Win -from theming import get_theme, to_curses_attr - -class GlobalInfoBar(Win): - def __init__(self): - Win.__init__(self) - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - self.addstr(0, 0, "[", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - create_gaps = config.get('create_gaps') - show_names = config.get('show_tab_names') - show_nums = config.get('show_tab_numbers') - use_nicks = config.get('use_tab_nicks') - show_inactive = config.get('show_inactive_tabs') - # ignore any remaining gap tabs if the feature is not enabled - if create_gaps: - sorted_tabs = self.core.tabs[:] - else: - sorted_tabs = [tab for tab in self.core.tabs if tab] - - for nb, tab in enumerate(sorted_tabs): - if not tab: continue - color = tab.color - if not show_inactive and color is get_theme().COLOR_TAB_NORMAL: - continue - try: - if show_nums or not show_names: - self.addstr("%s" % str(nb), to_curses_attr(color)) - if show_names: - self.addstr(' ', to_curses_attr(color)) - if show_names: - if use_nicks: - self.addstr("%s" % str(tab.get_nick()), to_curses_attr(color)) - else: - self.addstr("%s" % tab.name, 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, '] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - (y, x) = self._win.getyx() - remaining_size = self.width - x - self.addnstr(' '*remaining_size, remaining_size, - 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): - height, width = self._win.getmaxyx() - self._win.erase() - sorted_tabs = [tab for tab in self.core.tabs if tab] - if not config.get('show_inactive_tabs'): - sorted_tabs = [tab for tab in sorted_tabs if\ - tab.vertical_color != get_theme().COLOR_VERTICAL_TAB_NORMAL] - nb_tabs = len(sorted_tabs) - use_nicks = config.get('use_tab_nicks') - 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] - asc_sort = (config.get('vertical_tab_list_sort') == 'asc') - for y, tab in enumerate(sorted_tabs): - color = tab.vertical_color - if asc_sort: - y = height - y - 1 - self.addstr(y, 0, "%2d" % tab.nb, - to_curses_attr(get_theme().COLOR_VERTICAL_TAB_NUMBER)) - self.addstr('.') - if use_nicks: - self.addnstr("%s" % tab.get_nick(), width - 4, to_curses_attr(color)) - else: - self.addnstr("%s" % tab.name, width - 4, to_curses_attr(color)) - separator = to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR) - self._win.attron(separator) - self._win.vline(0, width-1, curses.ACS_VLINE, height) - self._win.attroff(separator) - self._refresh() diff --git a/src/windows/info_wins.py b/src/windows/info_wins.py deleted file mode 100644 index f6aebd35..00000000 --- a/src/windows/info_wins.py +++ /dev/null @@ -1,311 +0,0 @@ -""" -Module defining all the "info wins", ie the bar which is on top of the -info buffer in normal tabs -""" - -import logging -log = logging.getLogger(__name__) - -from common import safeJID -from config import config - -from . import Win -from . funcs import truncate_nick -from theming import get_theme, to_curses_attr - -class InfoWin(Win): - """ - Base class for all the *InfoWin, used in various tabs. For example - MucInfoWin, etc. Provides some useful methods. - """ - def __init__(self): - Win.__init__(self) - - def print_scroll_position(self, window): - """ - Print, like in Weechat, a -MORE(n)- where n - is the number of available lines to scroll - down - """ - if window.pos > 0: - plus = ' -MORE(%s)-' % window.pos - self.addstr(plus, to_curses_attr(get_theme().COLOR_SCROLLABLE_NUMBER)) - -class XMLInfoWin(InfoWin): - """ - Info about the latest xml filter used and the state of the buffer. - """ - def __init__(self): - InfoWin.__init__(self) - - def refresh(self, filter_t='', filter='', window=None): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - bar = to_curses_attr(get_theme().COLOR_INFORMATION_BAR) - if not filter_t: - self.addstr('[No filter]', bar) - else: - info = '[%s] %s' % (filter_t, filter) - self.addstr(info, bar) - self.print_scroll_position(window) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - -class PrivateInfoWin(InfoWin): - """ - The line above the information window, displaying informations - about the MUC user we are talking to - """ - def __init__(self): - InfoWin.__init__(self) - - def refresh(self, name, window, chatstate, informations): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - self.write_room_name(name) - self.print_scroll_position(window) - self.write_chatstate(chatstate) - self.write_additional_informations(informations, name) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - - def write_additional_informations(self, informations, jid): - """ - Write all informations added by plugins by getting the - value returned by the callbacks. - """ - for key in informations: - self.addstr(informations[key](jid), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_room_name(self, name): - jid = safeJID(name) - room_name, nick = jid.bare, jid.resource - self.addstr(nick, to_curses_attr(get_theme().COLOR_PRIVATE_NAME)) - txt = ' from room %s' % room_name - self.addstr(txt, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_chatstate(self, state): - if state: - self.addstr(' %s' % (state,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - -class MucListInfoWin(InfoWin): - """ - The live above the information window, displaying informations - about the muc server being listed - """ - def __init__(self, message=''): - InfoWin.__init__(self) - self.message = message - - def refresh(self, name=None, window=None): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - if name: - self.addstr(name, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - else: - self.addstr(self.message, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - if window: - self.print_scroll_position(window) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - -class ConversationInfoWin(InfoWin): - """ - The line above the information window, displaying informations - about the user we are talking to - """ - - def __init__(self): - InfoWin.__init__(self) - - def refresh(self, jid, contact, window, chatstate, informations): - # contact can be None, if we receive a message - # from someone not in our roster. In this case, we display - # only the maximum information from the message we can get. - log.debug('Refresh: %s', self.__class__.__name__) - jid = safeJID(jid) - if contact: - if jid.resource: - resource = contact[jid.full] - else: - resource = contact.get_highest_priority_resource() - else: - resource = None - # if contact is None, then resource is None too: - # user is not in the roster so we know almost nothing about it - # If contact is a Contact, then - # resource can now be a Resource: user is in the roster and online - # or resource is None: user is in the roster but offline - self._win.erase() - if config.get('show_jid_in_conversations'): - self.write_contact_jid(jid) - self.write_contact_informations(contact) - self.write_resource_information(resource) - self.print_scroll_position(window) - self.write_chatstate(chatstate) - self.write_additional_informations(informations, jid) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - - def write_additional_informations(self, informations, jid): - """ - Write all informations added by plugins by getting the - value returned by the callbacks. - """ - for key in informations: - self.addstr(informations[key](jid), - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_resource_information(self, resource): - """ - Write the informations about the resource - """ - if not resource: - presence = "unavailable" - else: - presence = resource.presence - color = get_theme().color_show(presence) - self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - 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): - """ - Write the informations about the contact - """ - if not contact: - self.addstr("(contact not in roster)", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - return - display_name = contact.name - if display_name: - 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('[', 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,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - -class DynamicConversationInfoWin(ConversationInfoWin): - def write_contact_jid(self, jid): - """ - Just displays the resource in an other color - """ - log.debug("write_contact_jid DynamicConversationInfoWin, jid: %s", - jid.resource) - self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.addstr(jid.bare, to_curses_attr(get_theme().COLOR_CONVERSATION_NAME)) - if jid.resource: - self.addstr("/%s" % (jid.resource,), to_curses_attr(get_theme().COLOR_CONVERSATION_RESOURCE)) - self.addstr('] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - -class MucInfoWin(InfoWin): - """ - The line just above the information window, displaying informations - about the MUC we are viewing - """ - def __init__(self): - InfoWin.__init__(self) - - def refresh(self, room, window=None): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - self.write_room_name(room) - self.write_participants_number(room) - self.write_own_nick(room) - self.write_disconnected(room) - self.write_role(room) - if window: - self.print_scroll_position(window) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - - def write_room_name(self, room): - 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_participants_number(self, room): - self.addstr('{', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.addstr(str(len(room.users)), 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 ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_own_nick(self, room): - """ - Write our own nick in the info bar - """ - nick = room.own_nick - if not nick: - return - self.addstr(truncate_nick(nick, 13), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_role(self, room): - """ - Write our own role and affiliation - """ - own_user = None - for user in room.users: - if user.nick == room.own_nick: - own_user = user - break - if not own_user: - return - txt = ' (' - if own_user.affiliation != 'none': - txt += own_user.affiliation+', ' - txt += own_user.role+')' - self.addstr(txt, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - -class ConversationStatusMessageWin(InfoWin): - """ - The upper bar displaying the status message of the contact - """ - def __init__(self): - InfoWin.__init__(self) - - def refresh(self, jid, contact): - log.debug('Refresh: %s', self.__class__.__name__) - jid = safeJID(jid) - if contact: - if jid.resource: - resource = contact[jid.full] - else: - resource = contact.get_highest_priority_resource() - else: - resource = None - self._win.erase() - if resource: - self.write_status_message(resource) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - - def write_status_message(self, resource): - self.addstr(resource.status, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - -class BookmarksInfoWin(InfoWin): - def __init__(self): - InfoWin.__init__(self) - - def refresh(self, preferred): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - self.write_remote_status(preferred) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - - def write_remote_status(self, preferred): - self.addstr('Remote storage: %s' % preferred, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - diff --git a/src/windows/input_placeholders.py b/src/windows/input_placeholders.py deleted file mode 100644 index 496417d1..00000000 --- a/src/windows/input_placeholders.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -Classes used to replace the input in some tabs or special situations, -but which are not inputs. -""" - -import logging -log = logging.getLogger(__name__) - - -from . import Win -from theming import get_theme, to_curses_attr - - -class HelpText(Win): - """ - A Window just displaying a read-only message. - Usually used to replace an Input when the tab is in - command mode. - """ - def __init__(self, text=''): - Win.__init__(self) - self.txt = text - - def refresh(self, txt=None): - log.debug('Refresh: %s', self.__class__.__name__) - if txt: - self.txt = txt - self._win.erase() - 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, raw=False): - return False - - def on_delete(self): - return - -class YesNoInput(Win): - """ - A Window just displaying a Yes/No input - Used to ask a confirmation - """ - def __init__(self, text='', callback=None): - Win.__init__(self) - self.key_func = { - 'y' : self.on_yes, - 'n' : self.on_no, - } - self.txt = text - self.value = None - self.callback = callback - - def on_yes(self): - self.value = True - - def on_no(self): - self.value = False - - def refresh(self, txt=None): - log.debug('Refresh: %s', self.__class__.__name__) - if txt: - self.txt = txt - self._win.erase() - self.addstr(0, 0, self.txt[:self.width-1], to_curses_attr(get_theme().COLOR_WARNING_PROMPT)) - self.finish_line(get_theme().COLOR_WARNING_PROMPT) - self._refresh() - - def do_command(self, key, raw=False): - if key.lower() in self.key_func: - self.key_func[key]() - if self.value is not None and self.callback is not None: - return self.callback() - - def on_delete(self): - return - diff --git a/src/windows/inputs.py b/src/windows/inputs.py deleted file mode 100644 index 80f0c900..00000000 --- a/src/windows/inputs.py +++ /dev/null @@ -1,768 +0,0 @@ -""" -Text inputs. -""" - -import logging -log = logging.getLogger(__name__) - -import curses -import string - -import keyboard -import common -import poopt -from . import Win -from . base_wins import format_chars -from . funcs import find_first_format_char -from config import config -from theming import to_curses_attr - - -class Input(Win): - """ - The simplest Input possible, provides just a way to edit a single line - of text. It also has a clipboard, common to all Inputs. - Doesn't have any history. - It doesn't do anything when enter is pressed either. - This should be herited for all kinds of Inputs, for example MessageInput - or the little inputs in dataforms, etc, adding specific features (completion etc) - It features two kinds of completion, but they have to be called from outside (the Tab), - passing the list of items that can be used to complete. The completion can be used - in a very flexible way. - """ - text_attributes = ['b', 'o', 'u', '1', '2', '3', '4', '5', '6', '7', 't'] - clipboard = '' # A common clipboard for all the inputs, this makes - # it easy cut and paste text between various input - def __init__(self): - self.key_func = { - "KEY_LEFT": self.key_left, - "KEY_RIGHT": self.key_right, - "KEY_END": self.key_end, - "KEY_HOME": self.key_home, - "KEY_DC": self.key_dc, - '^D': self.key_dc, - 'M-b': self.jump_word_left, - "M-[1;5D": self.jump_word_left, - "kRIT5": self.jump_word_right, - "kLFT5": self.jump_word_left, - '^W': self.delete_word, - 'M-d': self.delete_next_word, - '^K': self.delete_end_of_line, - '^U': self.delete_beginning_of_line, - '^Y': self.paste_clipboard, - '^A': self.key_home, - '^E': self.key_end, - 'M-f': self.jump_word_right, - "M-[1;5C": self.jump_word_right, - "KEY_BACKSPACE": self.key_backspace, - "M-KEY_BACKSPACE": self.delete_word, - '^?': self.key_backspace, - "M-^?": self.delete_word, - # '^J': self.add_line_break, - } - Win.__init__(self) - self.text = '' - self.pos = 0 # The position of the “cursor” in the text - # (not only in the view) - self.view_pos = 0 # The position (in the text) of the - # first character displayed on the - # screen - self.on_input = None # callback called on any key pressed - self.color = None # use this color on addstr - - def on_delete(self): - """ - Remove all references kept to a tab, so that the tab - can be garbage collected - """ - del self.key_func - - def set_color(self, color): - self.color = color - self.rewrite_text() - - def is_empty(self): - if self.text: - return False - return True - - def is_cursor_at_end(self): - """ - Whether or not the cursor is at the end of the text. - """ - assert len(self.text) >= self.pos - if len(self.text) == self.pos: - return True - return False - - def jump_word_left(self): - """ - Move the cursor one word to the left - """ - if self.pos == 0: - return True - separators = string.punctuation+' ' - while self.pos > 0 and self.text[self.pos-1] in separators: - self.key_left() - while self.pos > 0 and self.text[self.pos-1] not in separators: - self.key_left() - return True - - def jump_word_right(self): - """ - Move the cursor one word to the right - """ - if self.is_cursor_at_end(): - return True - separators = string.punctuation+' ' - while not self.is_cursor_at_end() and self.text[self.pos] in separators: - self.key_right() - while not self.is_cursor_at_end() and self.text[self.pos] not in separators: - self.key_right() - return True - - def delete_word(self): - """ - Delete the word just before the cursor - """ - separators = string.punctuation+' ' - while self.pos > 0 and self.text[self.pos-1] in separators: - self.key_backspace() - while self.pos > 0 and self.text[self.pos-1] not in separators: - self.key_backspace() - return True - - def delete_next_word(self): - """ - Delete the word just after the cursor - """ - separators = string.punctuation+' ' - while not self.is_cursor_at_end() and self.text[self.pos] in separators: - self.key_dc() - while not self.is_cursor_at_end() and self.text[self.pos] not in separators: - self.key_dc() - return True - - def delete_end_of_line(self): - """ - Cut the text from cursor to the end of line - """ - if self.is_cursor_at_end(): - return False - Input.clipboard = self.text[self.pos:] - self.text = self.text[:self.pos] - self.key_end() - return True - - def delete_beginning_of_line(self): - """ - Cut the text from cursor to the beginning of line - """ - if self.pos == 0: - return True - Input.clipboard = self.text[:self.pos] - self.text = self.text[self.pos:] - self.key_home() - return True - - def paste_clipboard(self): - """ - Insert what is in the clipboard at the cursor position - """ - if not Input.clipboard: - return True - for letter in Input.clipboard: - self.do_command(letter, False) - self.rewrite_text() - return True - - def key_dc(self): - """ - delete char just after the cursor - """ - self.reset_completion() - if self.is_cursor_at_end(): - return True # end of line, nothing to delete - self.text = self.text[:self.pos]+self.text[self.pos+1:] - self.rewrite_text() - return True - - def key_home(self): - """ - Go to the beginning of line - """ - self.reset_completion() - self.pos = 0 - self.rewrite_text() - return True - - def key_end(self, reset=False): - """ - Go to the end of line - """ - if reset: - self.reset_completion() - self.pos = len(self.text) - assert self.is_cursor_at_end() - self.rewrite_text() - return True - - def key_left(self, jump=True, reset=True): - """ - Move the cursor one char to the left - """ - if reset: - self.reset_completion() - if self.pos == 0: - return True - self.pos -= 1 - if reset: - self.rewrite_text() - return True - - def key_right(self, jump=True, reset=True): - """ - Move the cursor one char to the right - """ - if reset: - self.reset_completion() - if self.is_cursor_at_end(): - return True - self.pos += 1 - if reset: - self.rewrite_text() - return True - - def key_backspace(self, reset=True): - """ - Delete the char just before the cursor - """ - self.reset_completion() - if self.pos == 0: - return - self.key_left() - self.key_dc() - return True - - def auto_completion(self, word_list, add_after='', quotify=True): - """ - Complete the input, from a list of words - if add_after is None, we use the value defined in completion - plus a space, after the completion. If it's a string, we use it after the - completion (with no additional space) - """ - if quotify: - for i, word in enumerate(word_list[:]): - word_list[i] = '"' + word + '"' - self.normal_completion(word_list, add_after) - return True - - def new_completion(self, word_list, argument_position=-1, add_after='', quotify=True, override=False): - """ - Complete the argument at position ``argument_postion`` in the input. - If ``quotify`` is ``True``, then the completion will operate on block of words - (e.g. "toto titi") whereas if it is ``False``, it will operate on words (e.g - "toto", "titi"). - - The completions may modify other parts of the input when completing an argument, - for example removing useless double quotes around single-words, or setting the - space between each argument to only one space. - - The case where we complete the first argument is special, because we complete - the command, and we do not want to modify anything else in the input. - - This method is the one that should be used if the command being completed - has several arguments. - """ - if argument_position == 0: - self._new_completion_first(word_list) - else: - self._new_completion_args(word_list, argument_position, add_after, quotify, override) - self.rewrite_text() - return True - - def _new_completion_args(self, word_list, argument_position=-1, add_after='', quoted=True, override=False): - """ - Case for completing arguments with position ≠ 0 - """ - if quoted: - words = common.shell_split(self.text) - else: - words = self.text.split() - if argument_position >= len(words): - current = '' - else: - current = words[argument_position] - - if quoted: - split_words = words[1:] - words = [words[0]] - for word in split_words: - if ' ' in word or '\\' in word: - words.append('"' + word + '"') - else: - words.append(word) - current_l = current.lower() - if self.last_completion is not None: - self.hit_list.append(self.hit_list.pop(0)) - else: - if override: - hit_list = word_list - else: - hit_list = [] - for word in word_list: - if word.lower().startswith(current_l): - hit_list.append(word) - if not hit_list: - return - self.hit_list = hit_list - - if argument_position >= len(words): - if quoted and ' ' in self.hit_list[0]: - words.append('"'+self.hit_list[0]+'"') - else: - words.append(self.hit_list[0]) - else: - if quoted and ' ' in self.hit_list[0]: - words[argument_position] = '"'+self.hit_list[0]+'"' - else: - words[argument_position] = self.hit_list[0] - - new_pos = -1 - for i, word in enumerate(words): - if argument_position >= i: - new_pos += len(word) + 1 - - self.last_completion = self.hit_list[0] - self.text = words[0] + ' ' + ' '.join(words[1:]) - self.pos = new_pos - - def _new_completion_first(self, word_list): - """ - Special case of completing the command itself: - we don’t want to change anything to the input doing that - """ - space_pos = self.text.find(' ') - if space_pos != -1: - current, follow = self.text[:space_pos], self.text[space_pos:] - else: - current, follow = self.text, '' - - if self.last_completion: - self.hit_list.append(self.hit_list.pop(0)) - else: - hit_list = [] - for word in word_list: - if word.lower().startswith(current): - hit_list.append(word) - if not hit_list: - return - self.hit_list = hit_list - - self.last_completion = self.hit_list[0] - self.text = self.hit_list[0] + follow - self.pos = len(self.hit_list[0]) - - def get_argument_position(self, quoted=True): - """ - Get the argument number at the current position - """ - command_stop = self.text.find(' ') - if command_stop == -1 or self.pos <= command_stop: - return 0 - text = self.text[command_stop+1:] - pos = self.pos - len(self.text) + len(text) - 1 - val = common.find_argument(pos, text, quoted=quoted) + 1 - return val - - def reset_completion(self): - """ - Reset the completion list (called on ALL keys except tab) - """ - self.hit_list = [] - self.last_completion = None - - def normal_completion(self, word_list, after): - """ - Normal completion - """ - pos = self.pos - if pos < len(self.text) and after.endswith(' ') and self.text[pos] == ' ': - after = after[:-1] # remove the last space if we are already on a space - if not self.last_completion: - space_before_cursor = self.text.rfind(' ', 0, pos) - if space_before_cursor != -1: - begin = self.text[space_before_cursor+1:pos] - else: - begin = self.text[:pos] - hit_list = [] # list of matching hits - for word in word_list: - if word.lower().startswith(begin.lower()): - hit_list.append(word) - elif word.startswith('"') and word.lower()[1:].startswith(begin.lower()): - hit_list.append(word) - if len(hit_list) == 0: - return - self.hit_list = hit_list - end = len(begin) - else: - begin = self.last_completion - end = len(begin) + len(after) - self.hit_list.append(self.hit_list.pop(0)) # rotate list - - self.text = self.text[:pos-end] + self.text[pos:] - pos -= end - hit = self.hit_list[0] # take the first hit - self.text = self.text[:pos] + hit + after + self.text[pos:] - for _ in range(end): - try: - self.key_left(reset=False) - except: - pass - for _ in range(len(hit) + len(after)): - self.key_right(reset=False) - - self.rewrite_text() - self.last_completion = hit - - def do_command(self, key, reset=True, raw=False): - if key in self.key_func: - res = self.key_func[key]() - if not raw and self.on_input: - self.on_input(self.get_text()) - return res - if not raw and (not key or len(key) > 1): - return False # ignore non-handled keyboard shortcuts - if reset: - self.reset_completion() - # Insert the char at the cursor position - self.text = self.text[:self.pos]+key+self.text[self.pos:] - self.pos += len(key) - if reset: - self.rewrite_text() - if self.on_input: - self.on_input(self.get_text()) - - return True - - def add_line_break(self): - """ - Add a (real) \n to the line - """ - self.do_command('\n') - - def get_text(self): - """ - Return the text entered so far - """ - return self.text - - def addstr_colored_lite(self, text, y=None, x=None): - """ - Just like addstr_colored, with the single-char attributes - (\x0E to \x19 instead of \x19 + attr). We do not use any } - char in this version - """ - chars = format_chars[:] - chars.append('\n') - if y is not None and x is not None: - self.move(y, x) - format_char = find_first_format_char(text, chars) - while format_char != -1: - if text[format_char] == '\n': - attr_char = '|' - else: - attr_char = self.text_attributes[ - format_chars.index(text[format_char])] - self.addstr(text[:format_char]) - self.addstr(attr_char, curses.A_REVERSE) - text = text[format_char+1:] - 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) - elif attr_char in string.digits and attr_char != '': - self._win.attron(to_curses_attr((int(attr_char), -1))) - format_char = find_first_format_char(text, chars) - self.addstr(text) - - def rewrite_text(self): - """ - Refresh the line onscreen, but first, always adjust the - view_pos. Also, each FORMAT_CHAR+attr_char count only take - one screen column (this is done in addstr_colored_lite), we - have to do some special calculations to find the correct - length of text to display, and the position of the cursor. - """ - self.adjust_view_pos() - text = self.text - self._win.erase() - if self.color: - self._win.attron(to_curses_attr(self.color)) - displayed_text = text[self.view_pos:self.view_pos+self.width-1].replace('\t', '\x18') - self._win.attrset(0) - self.addstr_colored_lite(displayed_text) - # Fill the rest of the line with the input color - if self.color: - (_, x) = self._win.getyx() - size = self.width - x - self.addnstr(' ' * size, size, to_curses_attr(self.color)) - self.addstr(0, - poopt.wcswidth(displayed_text[:self.pos-self.view_pos]), '') - if self.color: - self._win.attroff(to_curses_attr(self.color)) - curses.curs_set(1) - self._refresh() - - def adjust_view_pos(self): - """ - Adjust the position of the View, if needed (for example if the - cursor moved and would now be out of the view, we adapt the - view_pos so that we can always see our cursor) - """ - # start of the input - if self.pos == 0: - self.view_pos = 0 - return - # cursor outside of the screen (left) - if self.pos <= self.view_pos: - self.view_pos = self.pos - max(1 * self.width // 3, 1) - # cursor outside of the screen (right) - elif self.pos >= self.view_pos + self.width - 1: - self.view_pos = self.pos - max(2 * self.width // 3, 2) - - if self.view_pos < 0: - self.view_pos = 0 - - # text small enough to fit inside the window entirely: - # remove scrolling if present - if poopt.wcswidth(self.text) < self.width: - self.view_pos = 0 - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - self.rewrite_text() - - def clear_text(self): - self.text = '' - self.pos = 0 - self.rewrite_text() - - def key_enter(self): - txt = self.get_text() - self.clear_text() - return txt - -class HistoryInput(Input): - """ - An input with colors and stuff, plus an history - ^R allows to search inside the history (as in a shell) - """ - history = list() - - def __init__(self): - Input.__init__(self) - self.help_message = '' - self.current_completed = '' - self.key_func['^R'] = self.toggle_search - self.search = False - if config.get('separate_history'): - self.history = list() - - def toggle_search(self): - if self.help_message: - return - self.search = not self.search - self.refresh() - - def update_completed(self): - """ - Find a match for the current text - """ - if not self.text: - return - for i in self.history: - if self.text in i: - self.current_completed = i - return - self.current_completed = '' - - def history_enter(self): - """ - Enter was pressed, set the text to the - current completion and disable history - search - """ - if self.search: - self.search = False - if self.current_completed: - self.text = self.current_completed - self.current_completed = '' - self.refresh() - return True - self.refresh() - return False - - def key_up(self): - """ - Get the previous line in the history - """ - self.reset_completion() - if self.histo_pos == -1 and self.get_text(): - if not self.history or self.history[0] != self.get_text(): - # add the message to history, we do not want to lose it - self.history.insert(0, self.get_text()) - self.histo_pos += 1 - if self.histo_pos < len(self.history) - 1: - self.histo_pos += 1 - self.text = self.history[self.histo_pos] - self.key_end() - return True - - def key_down(self): - """ - Get the next line in the history - """ - self.reset_completion() - if self.histo_pos > 0: - self.histo_pos -= 1 - self.text = self.history[self.histo_pos] - elif self.histo_pos <= 0 and self.get_text(): - if not self.history or self.history[0] != self.get_text(): - # add the message to history, we do not want to lose it - self.history.insert(0, self.get_text()) - self.text = '' - self.histo_pos = -1 - self.key_end() - return True - -class MessageInput(HistoryInput): - """ - The input featuring history and that is being used in - Conversation, Muc and Private tabs - Also letting the user enter colors or other text markups - """ - history = list() # The history is common to all MessageInput - - def __init__(self): - HistoryInput.__init__(self) - self.last_completion = None - self.histo_pos = -1 - self.key_func["KEY_UP"] = self.key_up - self.key_func["M-A"] = self.key_up - self.key_func["KEY_DOWN"] = self.key_down - self.key_func["M-B"] = self.key_down - self.key_func['^C'] = self.enter_attrib - - def enter_attrib(self): - """ - Read one more char (c), add the corresponding char from formats_char to the text string - """ - def cb(attr_char): - if attr_char in self.text_attributes: - char = format_chars[self.text_attributes.index(attr_char)] - self.do_command(char, False) - self.rewrite_text() - keyboard.continuation_keys_callback = cb - - def key_enter(self): - if self.history_enter(): - return - - txt = self.get_text() - if len(txt) != 0: - if not self.history or self.history[0] != txt: - # add the message to history, but avoid duplicates - self.history.insert(0, txt) - self.histo_pos = -1 - self.clear_text() - return txt - -class CommandInput(HistoryInput): - """ - An input with an help message in the left, with three given callbacks: - one when when successfully 'execute' the command and when we abort it. - The last callback is optional and is called on any input key - This input is used, for example, in the RosterTab when, to replace the - HelpMessage when a command is started - The on_input callback - """ - history = list() - - def __init__(self, help_message, on_abort, on_success, on_input=None): - HistoryInput.__init__(self) - self.on_abort = on_abort - self.on_success = on_success - self.on_input = on_input - self.help_message = help_message - self.key_func['^M'] = self.success - self.key_func['^G'] = self.abort - self.key_func['^C'] = self.abort - self.key_func["KEY_UP"] = self.key_up - self.key_func["M-A"] = self.key_up - self.key_func["KEY_DOWN"] = self.key_down - self.key_func["M-B"] = self.key_down - self.histo_pos = -1 - - def do_command(self, key, refresh=True, raw=False): - res = Input.do_command(self, key, refresh, raw) - if self.on_input: - self.on_input(self.get_text()) - return res - - def disable_history(self): - """ - Disable the history (up/down) keys - """ - if 'KEY_UP' in self.key_func: - del self.key_func['KEY_UP'] - if 'KEY_DOWN' in self.key_func: - del self.key_func['KEY_DOWN'] - - @property - def history_disabled(self): - return 'KEY_UP' not in self.key_func and 'KEY_DOWN' not in self.key_func - - def success(self): - """ - call the success callback, passing the text as argument - """ - self.on_input = None - if self.search: - self.history_enter() - res = self.on_success(self.get_text()) - return res - - def abort(self): - """ - Call the abort callback, passing the text as argument - """ - self.on_input = None - return self.on_abort(self.get_text()) - - def on_delete(self): - """ - SERIOUSLY BIG WTF. - - I can do - self.key_func.clear() - - but not - del self.key_func - because that would raise an AttributeError exception. WTF. - """ - self.on_abort = None - self.on_success = None - self.on_input = None - self.key_func.clear() - - def key_enter(self): - txt = self.get_text() - if len(txt) != 0: - if not self.history or self.history[0] != txt: - # add the message to history, but avoid duplicates - self.history.insert(0, txt) - self.histo_pos = -1 - diff --git a/src/windows/list.py b/src/windows/list.py deleted file mode 100644 index 677df6ff..00000000 --- a/src/windows/list.py +++ /dev/null @@ -1,236 +0,0 @@ -""" -Windows relevant for the listing tabs, not much else -""" - -import logging -log = logging.getLogger(__name__) - -import curses - -from . import Win -from theming import to_curses_attr, get_theme - - -class ListWin(Win): - """ - A list (with no depth, so not for the roster) that can be - scrolled up and down, with one selected line at a time - """ - def __init__(self, columns, with_headers=True): - Win.__init__(self) - self._columns = columns # a dict {'column_name': tuple_index} - self._columns_sizes = {} # a dict {'column_name': size} - self.sorted_by = (None, None) # for example: ('name', '↑') - self.lines = [] # a list of dicts - self._selected_row = 0 - self._starting_pos = 0 # The column number from which we start the refresh - - @property - def pos(self): - if len(self.lines) > self.height: - return len(self.lines) - else: - return 0 - - def empty(self): - """ - emtpy the list and reset some important values as well - """ - self.lines = [] - self._selected_row = 0 - self._starting_pos = 0 - - def resize_columns(self, dic): - """ - Resize the width of the columns - """ - self._columns_sizes = dic - - def sort_by_column(self, col_name, asc=True): - """ - Sort the list by the given column, ascendant or descendant - """ - if not col_name: - return - elif asc: - self.lines.sort(key=lambda x: x[self._columns[col_name]]) - else: - self.lines.sort(key=lambda x: x[self._columns[col_name]], - reverse=True) - self.refresh() - curses.doupdate() - - def add_lines(self, lines): - """ - Append some lines at the end of the list - """ - if not lines: - return - self.lines.extend(lines) - - def set_lines(self, lines): - """ - Set the lines to another list - """ - if not lines: - return - self.lines = lines - - def get_selected_row(self): - """ - Return the tuple representing the selected row - """ - if self._selected_row is not None and self.lines: - return self.lines[self._selected_row] - return None - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - lines = self.lines[self._starting_pos:self._starting_pos+self.height] - for y, line in enumerate(lines): - x = 0 - for col in self._columns.items(): - try: - txt = line[col[1]] or '' - except KeyError: - txt = '' - size = self._columns_sizes[col[0]] - txt += ' ' * (size-len(txt)) - if not txt: - continue - if line is self.lines[self._selected_row]: - self.addstr(y, x, txt[:size], - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - else: - self.addstr(y, x, txt[:size]) - x += size - self._refresh() - - def move_cursor_down(self): - """ - Move the cursor Down - """ - if not self.lines: - return - if self._selected_row < len(self.lines) - 1: - self._selected_row += 1 - while self._selected_row >= self._starting_pos + self.height: - self._starting_pos += self.height // 2 - if self._starting_pos < 0: - self._starting_pos = 0 - return True - - def move_cursor_up(self): - """ - Move the cursor Up - """ - if not self.lines: - return - if self._selected_row > 0: - self._selected_row -= 1 - while self._selected_row < self._starting_pos: - self._starting_pos -= self.height // 2 - return True - - def scroll_down(self): - if not self.lines: - return - self._selected_row += self.height - if self._selected_row > len(self.lines) - 1: - self._selected_row = len(self.lines) -1 - while self._selected_row >= self._starting_pos + self.height: - self._starting_pos += self.height // 2 - if self._starting_pos < 0: - self._starting_pos = 0 - return True - - def scroll_up(self): - if not self.lines: - return - self._selected_row -= self.height + 1 - if self._selected_row < 0: - self._selected_row = 0 - while self._selected_row < self._starting_pos: - self._starting_pos -= self.height // 2 - return True - -class ColumnHeaderWin(Win): - """ - A class displaying the column's names - """ - def __init__(self, columns): - Win.__init__(self) - self._columns = columns - self._columns_sizes = {} - self._column_sel = '' - self._column_order = '' - self._column_order_asc = False - - def resize_columns(self, dic): - self._columns_sizes = dic - - def get_columns(self): - return self._columns - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - x = 0 - for col in self._columns: - txt = col - if col in self._column_order: - if self._column_order_asc: - txt += get_theme().CHAR_COLUMN_ASC - else: - txt += get_theme().CHAR_COLUMN_DESC - #⇓⇑↑↓⇧⇩▲▼ - size = self._columns_sizes[col] - txt += ' ' * (size-len(txt)) - if col in self._column_sel: - self.addstr(0, x, txt, to_curses_attr(get_theme().COLOR_COLUMN_HEADER_SEL)) - else: - self.addstr(0, x, txt, to_curses_attr(get_theme().COLOR_COLUMN_HEADER)) - x += size - self._refresh() - - def sel_column(self, dic): - self._column_sel = dic - - def get_sel_column(self): - return self._column_sel - - def set_order(self, order): - self._column_order = self._column_sel - self._column_order_asc = order - - def get_order(self): - if self._column_sel == self._column_order: - return self._column_order_asc - else: - return False - - def sel_column_left(self): - if self._column_sel in self._columns: - index = self._columns.index(self._column_sel) - if index > 1: - index = index -1 - else: - index = 0 - else: - index = 0 - self._column_sel = self._columns[index] - self.refresh() - - def sel_column_right(self): - if self._column_sel in self._columns: - index = self._columns.index(self._column_sel) - if index < len(self._columns)-2: - index = index +1 - else: - index = len(self._columns) -1 - else: - index = len(self._columns) - 1 - self._column_sel = self._columns[index] - self.refresh() - diff --git a/src/windows/misc.py b/src/windows/misc.py deleted file mode 100644 index 07c91bbd..00000000 --- a/src/windows/misc.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Wins that don’t fit any category -""" - -import logging -log = logging.getLogger(__name__) - -import curses - -from . import Win -from theming import get_theme, to_curses_attr - -class VerticalSeparator(Win): - """ - Just a one-column window, with just a line in it, that is - refreshed only on resize, but never on refresh, for efficiency - """ - def __init__(self): - Win.__init__(self) - - def rewrite_line(self): - self._win.vline(0, 0, curses.ACS_VLINE, self.height, - to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR)) - self._refresh() - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - self.rewrite_line() - - -class SimpleTextWin(Win): - def __init__(self, text): - Win.__init__(self) - self._text = text - self.built_lines = [] - - def rebuild_text(self): - """ - Transform the text in lines than can then be - displayed without any calculation or anything - at refresh() time - It is basically called on each resize - """ - self.built_lines = [] - for line in self._text.split('\n'): - while len(line) >= self.width: - limit = line[:self.width].rfind(' ') - if limit <= 0: - limit = self.width - self.built_lines.append(line[:limit]) - line = line[limit:] - self.built_lines.append(line) - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - for y, line in enumerate(self.built_lines): - self.addstr_colored(line, y, 0) - self._refresh() - diff --git a/src/windows/muc.py b/src/windows/muc.py deleted file mode 100644 index 84775787..00000000 --- a/src/windows/muc.py +++ /dev/null @@ -1,143 +0,0 @@ -""" -Windows specific to a MUC -""" - -import logging -log = logging.getLogger(__name__) - -import curses - -from . import Win - -import poopt -from config import config -from theming import to_curses_attr, get_theme - -def userlist_to_cache(userlist): - result = [] - for user in userlist: - result.append((user.nick, user.status, user.chatstate, user.affiliation, user.role)) - return result - -class UserList(Win): - def __init__(self): - Win.__init__(self) - self.pos = 0 - self.cache = [] - - def scroll_up(self): - self.pos += self.height-1 - return True - - def scroll_down(self): - pos = self.pos - self.pos -= self.height-1 - if self.pos < 0: - self.pos = 0 - return self.pos != pos - - def draw_plus(self, y): - self.addstr(y, self.width-2, '++', to_curses_attr(get_theme().COLOR_MORE_INDICATOR)) - - - def refresh_if_changed(self, users): - old = self.cache - new = userlist_to_cache(users[self.pos:self.pos+self.height]) - if len(old) != len(new): - self.cache = new - self.refresh(users) - return - for i in range(len(old)): - if old[i] != new[i]: - self.cache = new - self.refresh(users) - - def refresh(self, users): - log.debug('Refresh: %s', self.__class__.__name__) - if config.get('hide_user_list'): - return # do not refresh if this win is hidden. - if len(users) < self.height: - self.pos = 0 - elif self.pos >= len(users) - self.height and self.pos != 0: - self.pos = len(users) - self.height - self._win.erase() - asc_sort = (config.get('user_list_sort').lower() == 'asc') - if asc_sort: - y, x = self._win.getmaxyx() - y -= 1 - else: - y = 0 - - for user in users[self.pos:self.pos+self.height]: - self.draw_role_affiliation(y, user) - self.draw_status_chatstate(y, user) - self.addstr(y, 2, - poopt.cut_by_columns(user.nick, self.width - 2), - to_curses_attr(user.color)) - if asc_sort: - y -= 1 - else: - y += 1 - if y == self.height: - break - # draw indicators of position in the list - if self.pos > 0: - if asc_sort: - self.draw_plus(self.height-1) - else: - self.draw_plus(0) - if self.pos + self.height < len(users): - if asc_sort: - self.draw_plus(0) - else: - self.draw_plus(self.height-1) - self._refresh() - - def draw_role_affiliation(self, y, user): - theme = get_theme() - color = theme.color_role(user.role) - symbol = theme.char_affiliation(user.affiliation) - self.addstr(y, 1, symbol, to_curses_attr(color)) - - def draw_status_chatstate(self, y, user): - show_col = get_theme().color_show(user.show) - if user.chatstate == 'composing': - char = get_theme().CHAR_CHATSTATE_COMPOSING - elif user.chatstate == 'active': - char = get_theme().CHAR_CHATSTATE_ACTIVE - elif user.chatstate == 'paused': - char = get_theme().CHAR_CHATSTATE_PAUSED - else: - char = get_theme().CHAR_STATUS - self.addstr(y, 0, char, to_curses_attr(show_col)) - - def resize(self, height, width, y, x): - separator = to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR) - self._resize(height, width, y, x) - self._win.attron(separator) - self._win.vline(0, 0, curses.ACS_VLINE, self.height) - self._win.attroff(separator) - -class Topic(Win): - def __init__(self): - Win.__init__(self) - self._message = '' - - def refresh(self, topic=None): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - if topic: - msg = topic[:self.width-1] - else: - msg = self._message[:self.width-1] - 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, - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self._refresh() - - def set_message(self, message): - self._message = message - diff --git a/src/windows/roster_win.py b/src/windows/roster_win.py deleted file mode 100644 index a2e2badd..00000000 --- a/src/windows/roster_win.py +++ /dev/null @@ -1,387 +0,0 @@ -""" -Windows used with the roster (window displaying the contacts, and the -one showing detailed info on the current selection) -""" -import logging -log = logging.getLogger(__name__) - -from datetime import datetime - -from . import Win - -import common -from config import config -from contact import Contact, Resource -from roster import RosterGroup -from theming import get_theme, to_curses_attr - - -class RosterWin(Win): - - def __init__(self): - Win.__init__(self) - self.pos = 0 # cursor position in the contact list - self.start_pos = 1 # position of the start of the display - self.selected_row = None - self.roster_cache = [] - - @property - def roster_len(self): - return len(self.roster_cache) - - def move_cursor_down(self, number=1): - """ - Return True if we scrolled, False otherwise - """ - pos = self.pos - if self.pos < self.roster_len-number: - self.pos += number - else: - self.pos = self.roster_len - 1 - if self.pos >= self.start_pos-1 + self.height-1: - if number == 1: - self.scroll_down(8) - else: - self.scroll_down(self.pos-self.start_pos - self.height // 2) - self.update_pos() - return pos != self.pos - - def move_cursor_up(self, number=1): - """ - Return True if we scrolled, False otherwise - """ - pos = self.pos - if self.pos-number >= 0: - self.pos -= number - else: - self.pos = 0 - if self.pos <= self.start_pos: - if number == 1: - self.scroll_up(8) - else: - self.scroll_up(self.start_pos-self.pos + self.height // 2) - self.update_pos() - return pos != self.pos - - def update_pos(self): - if len(self.roster_cache) > self.pos and self.pos >= 0: - self.selected_row = self.roster_cache[self.pos] - elif self.roster_cache: - self.selected_row = self.roster_cache[-1] - - def scroll_down(self, number=8): - pos = self.start_pos - if self.start_pos + number <= self.roster_len-1: - self.start_pos += number - else: - self.start_pos = self.roster_len-1 - return self.start_pos != pos - - def scroll_up(self, number=8): - pos = self.start_pos - if self.start_pos - number > 0: - self.start_pos -= number - else: - self.start_pos = 1 - return self.start_pos != pos - - def build_roster_cache(self, roster): - """ - Regenerates the roster cache if needed - """ - if not roster.needs_rebuild: - return - log.debug('The roster has changed, rebuilding the cache…') - # This is a search - if roster.contact_filter: - self.roster_cache = [] - sort = config.get('roster_sort', 'jid:show') or 'jid:show' - for contact in roster.get_contacts_sorted_filtered(sort): - self.roster_cache.append(contact) - else: - show_offline = config.get('roster_show_offline') or roster.contact_filter - sort = config.get('roster_sort') or 'jid:show' - group_sort = config.get('roster_group_sort') or 'name' - self.roster_cache = [] - # build the cache - for group in roster.get_groups(group_sort): - contacts_filtered = group.get_contacts(roster.contact_filter) - if (not show_offline and group.get_nb_connected_contacts() == 0) or not contacts_filtered: - continue # Ignore empty groups - self.roster_cache.append(group) - if group.folded: - continue # ignore folded groups - for contact in group.get_contacts(roster.contact_filter, sort): - if not show_offline and len(contact) == 0: - continue # ignore offline contacts - self.roster_cache.append(contact) - if not contact.folded(group.name): - for resource in contact.get_resources(): - self.roster_cache.append(resource) - roster.last_built = datetime.now() - if self.selected_row in self.roster_cache: - if self.pos < self.roster_len and self.roster_cache[self.pos] != self.selected_row: - self.pos = self.roster_cache.index(self.selected_row) - - def refresh(self, roster): - """ - We display a number of lines from the roster cache - (and rebuild it if needed) - """ - log.debug('Refresh: %s', self.__class__.__name__) - self.build_roster_cache(roster) - # make sure we are within bounds - self.move_cursor_up((self.roster_len + self.pos) if self.pos >= self.roster_len else 0) - if not self.roster_cache: - self.selected_row = None - self._win.erase() - self._win.move(0, 0) - self.draw_roster_information(roster) - y = 1 - group = "none" - # scroll down if needed - if self.start_pos+self.height <= self.pos+2: - self.scroll_down(self.pos - self.start_pos - self.height + (self.height//2)) - # draw the roster from the cache - roster_view = self.roster_cache[self.start_pos-1:self.start_pos+self.height] - - options = { - 'show_roster_sub': config.get('show_roster_subscriptions'), - 'show_s2s_errors': config.get('show_s2s_errors'), - 'show_roster_jids': config.get('show_roster_jids') - } - - for item in roster_view: - draw_selected = False - if y -2 + self.start_pos == self.pos: - draw_selected = True - self.selected_row = item - - if isinstance(item, RosterGroup): - self.draw_group(y, item, draw_selected) - group = item.name - elif isinstance(item, Contact): - self.draw_contact_line(y, item, draw_selected, group, **options) - elif isinstance(item, Resource): - self.draw_resource_line(y, item, draw_selected) - - y += 1 - - if self.start_pos > 1: - self.draw_plus(1) - if self.start_pos + self.height-2 < self.roster_len: - self.draw_plus(self.height-1) - self._refresh() - - - def draw_plus(self, y): - """ - Draw the indicator that shows that - the list is longer than what is displayed - """ - 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(), - len(roster)), - 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(to_curses_attr(get_theme().COLOR_SELECTED_ROW)) - if group.folded: - self.addstr(y, 0, '[+] ') - else: - self.addstr(y, 0, '[-] ') - contacts = " (%s/%s)" % (group.get_nb_connected_contacts(), len(group)) - self.addstr(y, 4, self.truncate_name(group.name, len(contacts)+4) + contacts) - if colored: - self._win.attroff(to_curses_attr(get_theme().COLOR_SELECTED_ROW)) - self.finish_line() - - def truncate_name(self, name, added): - if len(name) + added <= self.width: - return name - return name[:self.width - added - 1] + '…' - - def draw_contact_line(self, y, contact, colored, group, show_roster_sub=False, - show_s2s_errors=True, show_roster_jids=False): - """ - Draw on a line all informations about one contact. - This is basically the highest priority resource's informations - Use 'color' to draw the jid/display_name to show what is - the currently selected contact in the list - """ - - theme = get_theme() - resource = contact.get_highest_priority_resource() - if not resource: - # There's no online resource - presence = 'unavailable' - nb = '' - else: - presence = resource.presence - nb = ' (%s)' % len(contact) - color = theme.color_show(presence) - added = 2 + len(theme.CHAR_STATUS) + len(nb) - - self.addstr(y, 0, ' ') - self.addstr(theme.CHAR_STATUS, to_curses_attr(color)) - - self.addstr(' ') - if resource: - self.addstr('[+] ' if contact.folded(group) else '[-] ') - added += 4 - if contact.ask: - added += len(get_theme().CHAR_ROSTER_ASKED) - if show_s2s_errors and contact.error: - added += len(get_theme().CHAR_ROSTER_ERROR) - if contact.tune: - added += len(get_theme().CHAR_ROSTER_TUNE) - if contact.mood: - added += len(get_theme().CHAR_ROSTER_MOOD) - if contact.activity: - added += len(get_theme().CHAR_ROSTER_ACTIVITY) - if contact.gaming: - added += len(get_theme().CHAR_ROSTER_GAMING) - if show_roster_sub in ('all', 'incomplete', 'to', 'from', 'both', 'none'): - added += len(theme.char_subscription(contact.subscription, keep=show_roster_sub)) - - if not show_roster_jids and contact.name: - display_name = '%s' % contact.name - elif contact.name and contact.name != contact.bare_jid: - display_name = '%s (%s)' % (contact.name, contact.bare_jid) - else: - display_name = '%s' % (contact.bare_jid,) - - display_name = self.truncate_name(display_name, added) + nb - - if colored: - self.addstr(display_name, to_curses_attr(get_theme().COLOR_SELECTED_ROW)) - else: - self.addstr(display_name) - - if show_roster_sub in ('all', 'incomplete', 'to', 'from', 'both', 'none'): - self.addstr(theme.char_subscription(contact.subscription, keep=show_roster_sub), to_curses_attr(theme.COLOR_ROSTER_SUBSCRIPTION)) - if contact.ask: - self.addstr(get_theme().CHAR_ROSTER_ASKED, to_curses_attr(get_theme().COLOR_IMPORTANT_TEXT)) - if show_s2s_errors and contact.error: - self.addstr(get_theme().CHAR_ROSTER_ERROR, to_curses_attr(get_theme().COLOR_ROSTER_ERROR)) - if contact.tune: - self.addstr(get_theme().CHAR_ROSTER_TUNE, to_curses_attr(get_theme().COLOR_ROSTER_TUNE)) - if contact.activity: - self.addstr(get_theme().CHAR_ROSTER_ACTIVITY, to_curses_attr(get_theme().COLOR_ROSTER_ACTIVITY)) - if contact.mood: - self.addstr(get_theme().CHAR_ROSTER_MOOD, to_curses_attr(get_theme().COLOR_ROSTER_MOOD)) - if contact.gaming: - self.addstr(get_theme().CHAR_ROSTER_GAMING, to_curses_attr(get_theme().COLOR_ROSTER_GAMING)) - self.finish_line() - - def draw_resource_line(self, y, resource, colored): - """ - Draw a specific resource line - """ - color = get_theme().color_show(resource.presence) - self.addstr(y, 4, get_theme().CHAR_STATUS, to_curses_attr(color)) - if colored: - self.addstr(y, 6, self.truncate_name(str(resource.jid), 6), to_curses_attr(get_theme().COLOR_SELECTED_ROW)) - else: - self.addstr(y, 6, self.truncate_name(str(resource.jid), 6)) - self.finish_line() - - def get_selected_row(self): - if self.pos >= len(self.roster_cache): - return self.selected_row - if len(self.roster_cache) > 0: - self.selected_row = self.roster_cache[self.pos] - return self.roster_cache[self.pos] - return None - -class ContactInfoWin(Win): - def __init__(self): - Win.__init__(self) - - def draw_contact_info(self, contact): - """ - draw the contact information - """ - resource = contact.get_highest_priority_resource() - if contact: - jid = contact.bare_jid - elif resource: - jid = resource.jid - else: - jid = 'example@example.com' # should never happen - if resource: - presence = resource.presence - else: - presence = 'unavailable' - i = 0 - self.addstr(0, 0, '%s (%s)'%(jid, presence,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - i += 1 - self.addstr(i, 0, 'Subscription: %s' % (contact.subscription,)) - self.finish_line() - i += 1 - if contact.ask: - if contact.ask == 'asked': - self.addstr(i, 0, 'Ask: %s' % (contact.ask,), to_curses_attr(get_theme().COLOR_IMPORTANT_TEXT)) - else: - self.addstr(i, 0, 'Ask: %s' % (contact.ask,)) - self.finish_line() - i += 1 - if resource: - self.addstr(i, 0, 'Status: %s' % (resource.status)) - self.finish_line() - i += 1 - - if contact.error: - self.addstr(i, 0, 'Error: %s' % contact.error, to_curses_attr(get_theme().COLOR_ROSTER_ERROR)) - self.finish_line() - i += 1 - - if contact.tune: - self.addstr(i, 0, 'Tune: %s' % common.format_tune_string(contact.tune), to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) - self.finish_line() - i += 1 - - if contact.mood: - self.addstr(i, 0, 'Mood: %s' % contact.mood, to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) - self.finish_line() - i += 1 - - if contact.activity: - self.addstr(i, 0, 'Activity: %s' % contact.activity, to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) - self.finish_line() - i += 1 - - if contact.gaming: - self.addstr(i, 0, 'Game: %s' % common.format_gaming_string(contact.gaming), to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) - self.finish_line() - i += 1 - - def draw_group_info(self, group): - """ - draw the group information - """ - 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__) - self._win.erase() - if isinstance(selected_row, RosterGroup): - self.draw_group_info(selected_row) - elif isinstance(selected_row, Contact): - self.draw_contact_info(selected_row) - # elif isinstance(selected_row, Resource): - # self.draw_contact_info(None, selected_row) - self._refresh() diff --git a/src/windows/text_win.py b/src/windows/text_win.py deleted file mode 100644 index fd1fe546..00000000 --- a/src/windows/text_win.py +++ /dev/null @@ -1,597 +0,0 @@ -""" -TextWin, the window showing the text messages and info messages in poezio. -Can be locked, scrolled, has a separator, etc… -""" - -import logging -log = logging.getLogger(__name__) - -import curses -from math import ceil, log10 - -from . import Win -from . base_wins import FORMAT_CHAR, Line -from . funcs import truncate_nick, parse_attrs - -import poopt -from config import config -from theming import to_curses_attr, get_theme, dump_tuple - - -class BaseTextWin(Win): - def __init__(self, lines_nb_limit=None): - if lines_nb_limit is None: - lines_nb_limit = config.get('max_lines_in_memory') - Win.__init__(self) - self.lines_nb_limit = lines_nb_limit - self.pos = 0 - self.built_lines = [] # Each new message is built and kept here. - # on resize, we rebuild all the messages - - self.lock = False - self.lock_buffer = [] - self.separator_after = None - - def toggle_lock(self): - if self.lock: - self.release_lock() - else: - self.acquire_lock() - return self.lock - - def acquire_lock(self): - self.lock = True - - def release_lock(self): - for line in self.lock_buffer: - self.built_lines.append(line) - self.lock = False - - def scroll_up(self, dist=14): - pos = self.pos - self.pos += dist - if self.pos + self.height > len(self.built_lines): - self.pos = len(self.built_lines) - self.height - if self.pos < 0: - self.pos = 0 - return self.pos != pos - - def scroll_down(self, dist=14): - pos = self.pos - self.pos -= dist - if self.pos <= 0: - self.pos = 0 - return self.pos != pos - - def build_new_message(self, message, history=None, clean=True, highlight=False, timestamp=False, nick_size=10): - """ - Take one message, build it and add it to the list - Return the number of lines that are built for the given - message. - """ - lines = self.build_message(message, timestamp=timestamp, nick_size=nick_size) - if self.lock: - self.lock_buffer.extend(lines) - else: - self.built_lines.extend(lines) - if not lines or not lines[0]: - return 0 - if clean: - while len(self.built_lines) > self.lines_nb_limit: - self.built_lines.pop(0) - return len(lines) - - def build_message(self, message, timestamp=False, nick_size=10): - """ - Build a list of lines from a message, without adding it - to a list - """ - pass - - def refresh(self): - pass - - def write_text(self, y, x, txt): - """ - write the text of a line. - """ - self.addstr_colored(txt, y, x) - - def write_time(self, time): - """ - Write the date on the yth line of the window - """ - if time: - color = get_theme().COLOR_TIME_STRING - curses_color = to_curses_attr(color) - self._win.attron(curses_color) - self.addstr(time) - self._win.attroff(curses_color) - self.addstr(' ') - return poopt.wcswidth(time) + 1 - return 0 - - def resize(self, height, width, y, x, room=None): - if hasattr(self, 'width'): - old_width = self.width - else: - old_width = None - self._resize(height, width, y, x) - if room and self.width != old_width: - self.rebuild_everything(room) - - # reposition the scrolling after resize - # (see #2450) - buf_size = len(self.built_lines) - if buf_size - self.pos < self.height: - self.pos = buf_size - self.height - if self.pos < 0: - self.pos = 0 - - def rebuild_everything(self, room): - self.built_lines = [] - with_timestamps = config.get('show_timestamps') - nick_size = config.get('max_nick_length') - for message in room.messages: - self.build_new_message(message, clean=False, timestamp=with_timestamps, nick_size=nick_size) - if self.separator_after is message: - self.build_new_message(None) - while len(self.built_lines) > self.lines_nb_limit: - self.built_lines.pop(0) - - def __del__(self): - log.debug('** TextWin: deleting %s built lines', (len(self.built_lines))) - del self.built_lines - -class TextWin(BaseTextWin): - def __init__(self, lines_nb_limit=None): - BaseTextWin.__init__(self, lines_nb_limit) - - # the Lines of the highlights in that buffer - self.highlights = [] - # the current HL position in that list NaN means that we’re not on - # an hl. -1 is a valid position (it's before the first hl of the - # list. i.e the separator, in the case where there’s no hl before - # it.) - self.hl_pos = float('nan') - - # Keep track of the number of hl after the separator. - # This is useful to make “go to next highlight“ work after a “move to separator”. - self.nb_of_highlights_after_separator = 0 - - self.separator_after = None - - def next_highlight(self): - """ - Go to the next highlight in the buffer. - (depending on which highlight was selected before) - if the buffer is already positionned on the last, of if there are no - highlights, scroll to the end of the buffer. - """ - log.debug('Going to the next highlight…') - if (not self.highlights or self.hl_pos != self.hl_pos or - self.hl_pos >= len(self.highlights) - 1): - self.hl_pos = float('nan') - self.pos = 0 - return - hl_size = len(self.highlights) - 1 - if self.hl_pos < hl_size: - self.hl_pos += 1 - else: - self.hl_pos = hl_size - log.debug("self.hl_pos = %s", self.hl_pos) - hl = self.highlights[self.hl_pos] - pos = None - while not pos: - try: - pos = self.built_lines.index(hl) - except ValueError: - self.highlights = self.highlights[self.hl_pos+1:] - if not self.highlights: - self.hl_pos = float('nan') - self.pos = 0 - return - self.hl_pos = 0 - hl = self.highlights[0] - self.pos = len(self.built_lines) - pos - self.height - if self.pos < 0 or self.pos >= len(self.built_lines): - self.pos = 0 - - def previous_highlight(self): - """ - Go to the previous highlight in the buffer. - (depending on which highlight was selected before) - if the buffer is already positionned on the first, or if there are no - highlights, scroll to the end of the buffer. - """ - log.debug('Going to the previous highlight…') - if not self.highlights or self.hl_pos <= 0: - self.hl_pos = float('nan') - self.pos = 0 - return - if self.hl_pos != self.hl_pos: - self.hl_pos = len(self.highlights) - 1 - else: - self.hl_pos -= 1 - log.debug("self.hl_pos = %s", self.hl_pos) - hl = self.highlights[self.hl_pos] - pos = None - while not pos: - try: - pos = self.built_lines.index(hl) - except ValueError: - self.highlights = self.highlights[self.hl_pos+1:] - if not self.highlights: - self.hl_pos = float('nan') - self.pos = 0 - return - self.hl_pos = 0 - hl = self.highlights[0] - self.pos = len(self.built_lines) - pos - self.height - if self.pos < 0 or self.pos >= len(self.built_lines): - self.pos = 0 - - def scroll_to_separator(self): - """ - Scroll until separator is centered. If no separator is - present, scroll at the top of the window - """ - if None in self.built_lines: - self.pos = len(self.built_lines) - self.built_lines.index(None) - self.height + 1 - if self.pos < 0: - self.pos = 0 - else: - self.pos = len(self.built_lines) - self.height + 1 - # Chose a proper position (not too high) - self.scroll_up(0) - # Make “next highlight” work afterwards. This makes it easy to - # review all the highlights since the separator was placed, in - # the correct order. - self.hl_pos = len(self.highlights) - self.nb_of_highlights_after_separator - 1 - log.debug("self.hl_pos = %s", self.hl_pos) - - def remove_line_separator(self): - """ - Remove the line separator - """ - log.debug('remove_line_separator') - if None in self.built_lines: - self.built_lines.remove(None) - self.separator_after = None - - def add_line_separator(self, room=None): - """ - add a line separator at the end of messages list - room is a textbuffer that is needed to get the previous message - (in case of resize) - """ - if None not in self.built_lines: - self.built_lines.append(None) - self.nb_of_highlights_after_separator = 0 - log.debug("Reseting number of highlights after separator") - if room and room.messages: - self.separator_after = room.messages[-1] - - def build_new_message(self, message, history=None, clean=True, highlight=False, timestamp=False, nick_size=10): - """ - Take one message, build it and add it to the list - Return the number of lines that are built for the given - message. - """ - lines = self.build_message(message, timestamp=timestamp, nick_size=nick_size) - if self.lock: - self.lock_buffer.extend(lines) - else: - self.built_lines.extend(lines) - if not lines or not lines[0]: - return 0 - if highlight: - self.highlights.append(lines[0]) - self.nb_of_highlights_after_separator += 1 - log.debug("Number of highlights after separator is now %s", - self.nb_of_highlights_after_separator) - if clean: - while len(self.built_lines) > self.lines_nb_limit: - self.built_lines.pop(0) - return len(lines) - - def build_message(self, message, timestamp=False, nick_size=10): - """ - Build a list of lines from a message, without adding it - to a list - """ - if message is None: # line separator - return [None] - txt = message.txt - if not txt: - return [] - if len(message.str_time) > 8: - default_color = (FORMAT_CHAR + dump_tuple(get_theme().COLOR_LOG_MSG) - + '}') - else: - default_color = None - ret = [] - nick = truncate_nick(message.nickname, nick_size) - offset = 0 - if message.ack: - if message.ack > 0: - offset += poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1 - else: - offset += poopt.wcswidth(get_theme().CHAR_NACK) + 1 - if nick: - offset += poopt.wcswidth(nick) + 2 # + nick + '> ' length - if message.revisions > 0: - offset += ceil(log10(message.revisions + 1)) - if message.me: - offset += 1 # '* ' before and ' ' after - if timestamp: - if message.str_time: - offset += 1 + len(message.str_time) - if get_theme().CHAR_TIME_LEFT and message.str_time: - offset += 1 - if get_theme().CHAR_TIME_RIGHT and message.str_time: - offset += 1 - lines = poopt.cut_text(txt, self.width-offset-1) - prepend = default_color if default_color else '' - attrs = [] - for line in lines: - saved = Line(msg=message, start_pos=line[0], end_pos=line[1], prepend=prepend) - attrs = parse_attrs(message.txt[line[0]:line[1]], attrs) - if attrs: - prepend = FORMAT_CHAR + FORMAT_CHAR.join(attrs) - else: - if default_color: - prepend = default_color - else: - prepend = '' - ret.append(saved) - return ret - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - if self.height <= 0: - return - if self.pos == 0: - lines = self.built_lines[-self.height:] - else: - lines = self.built_lines[-self.height-self.pos:-self.pos] - with_timestamps = config.get("show_timestamps") - nick_size = config.get("max_nick_length") - self._win.move(0, 0) - self._win.erase() - offset = 0 - for y, line in enumerate(lines): - if line: - msg = line.msg - if line.start_pos == 0: - offset = self.write_pre_msg(msg, with_timestamps, nick_size) - elif y == 0: - offset = self.compute_offset(msg, with_timestamps, nick_size) - self.write_text(y, offset, line.prepend - + line.msg.txt[line.start_pos:line.end_pos]) - else: - self.write_line_separator(y) - if y != self.height-1: - self.addstr('\n') - self._win.attrset(0) - self._refresh() - - def compute_offset(self, msg, with_timestamps, nick_size): - offset = 0 - if with_timestamps and msg.str_time: - offset += poopt.wcswidth(msg.str_time) + 1 - - if not msg.nickname: # not a message, nothing to do afterwards - return offset - - nick = truncate_nick(msg.nickname, nick_size) - offset += poopt.wcswidth(nick) - if msg.ack: - if msg.ack > 0: - offset += poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1 - else: - offset += poopt.wcswidth(get_theme().CHAR_NACK) + 1 - if msg.me: - offset += 3 - else: - offset += 2 - if msg.revisions: - offset += ceil(log10(msg.revisions + 1)) - offset += self.write_revisions(msg) - return offset - - - def write_pre_msg(self, msg, with_timestamps, nick_size): - offset = 0 - if with_timestamps: - offset += self.write_time(msg.str_time) - - if not msg.nickname: # not a message, nothing to do afterwards - return offset - - nick = truncate_nick(msg.nickname, nick_size) - offset += poopt.wcswidth(nick) - if msg.nick_color: - color = msg.nick_color - elif msg.user: - color = msg.user.color - else: - color = None - if msg.ack: - if msg.ack > 0: - offset += self.write_ack() - else: - offset += self.write_nack() - if msg.me: - self._win.attron(to_curses_attr(get_theme().COLOR_ME_MESSAGE)) - self.addstr('* ') - self.write_nickname(nick, color, msg.highlight) - offset += self.write_revisions(msg) - self.addstr(' ') - offset += 3 - else: - self.write_nickname(nick, color, msg.highlight) - offset += self.write_revisions(msg) - self.addstr('> ') - offset += 2 - return offset - - def write_revisions(self, msg): - if msg.revisions: - self._win.attron(to_curses_attr(get_theme().COLOR_REVISIONS_MESSAGE)) - self.addstr('%d' % msg.revisions) - self._win.attrset(0) - return ceil(log10(msg.revisions + 1)) - return 0 - - def write_line_separator(self, y): - char = get_theme().CHAR_NEW_TEXT_SEPARATOR - self.addnstr(y, 0, - char * (self.width // len(char) - 1), - self.width, - to_curses_attr(get_theme().COLOR_NEW_TEXT_SEPARATOR)) - - def write_ack(self): - color = get_theme().COLOR_CHAR_ACK - self._win.attron(to_curses_attr(color)) - self.addstr(get_theme().CHAR_ACK_RECEIVED) - self._win.attroff(to_curses_attr(color)) - self.addstr(' ') - return poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1 - - def write_nack(self): - color = get_theme().COLOR_CHAR_NACK - self._win.attron(to_curses_attr(color)) - self.addstr(get_theme().CHAR_NACK) - self._win.attroff(to_curses_attr(color)) - self.addstr(' ') - return poopt.wcswidth(get_theme().CHAR_NACK) + 1 - - def write_nickname(self, nickname, color, highlight=False): - """ - Write the nickname, using the user's color - and return the number of written characters - """ - if not nickname: - return - if highlight: - hl_color = get_theme().COLOR_HIGHLIGHT_NICK - if hl_color == "reverse": - self._win.attron(curses.A_REVERSE) - else: - color = hl_color - if color: - self._win.attron(to_curses_attr(color)) - self.addstr(nickname) - if color: - self._win.attroff(to_curses_attr(color)) - if highlight and hl_color == "reverse": - self._win.attroff(curses.A_REVERSE) - - def modify_message(self, old_id, message): - """ - Find a message, and replace it with a new one - (instead of rebuilding everything in order to correct a message) - """ - with_timestamps = config.get('show_timestamps') - nick_size = config.get('max_nick_length') - for i in range(len(self.built_lines)-1, -1, -1): - if self.built_lines[i] and self.built_lines[i].msg.identifier == old_id: - index = i - while index >= 0 and self.built_lines[index] and self.built_lines[index].msg.identifier == old_id: - self.built_lines.pop(index) - index -= 1 - index += 1 - lines = self.build_message(message, timestamp=with_timestamps, nick_size=nick_size) - for line in lines: - self.built_lines.insert(index, line) - index += 1 - break - - def __del__(self): - log.debug('** TextWin: deleting %s built lines', (len(self.built_lines))) - del self.built_lines - -class XMLTextWin(BaseTextWin): - def __init__(self): - BaseTextWin.__init__(self) - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - theme = get_theme() - if self.height <= 0: - return - if self.pos == 0: - lines = self.built_lines[-self.height:] - else: - lines = self.built_lines[-self.height-self.pos:-self.pos] - self._win.move(0, 0) - self._win.erase() - for y, line in enumerate(lines): - if line: - msg = line.msg - if line.start_pos == 0: - if msg.nickname == theme.CHAR_XML_OUT: - color = theme.COLOR_XML_OUT - elif msg.nickname == theme.CHAR_XML_IN: - color = theme.COLOR_XML_IN - self.write_time(msg.str_time) - self.write_prefix(msg.nickname, color) - self.addstr(' ') - if y != self.height-1: - self.addstr('\n') - self._win.attrset(0) - for y, line in enumerate(lines): - offset = 0 - # Offset for the timestamp (if any) plus a space after it - offset += len(line.msg.str_time) - # space - offset += 1 - - # Offset for the prefix - offset += poopt.wcswidth(truncate_nick(line.msg.nickname)) - # space - offset += 1 - - self.write_text(y, offset, line.prepend - + line.msg.txt[line.start_pos:line.end_pos]) - if y != self.height-1: - self.addstr('\n') - self._win.attrset(0) - self._refresh() - - def build_message(self, message, timestamp=False, nick_size=10): - txt = message.txt - ret = [] - default_color = None - nick = truncate_nick(message.nickname, nick_size) - offset = 0 - if nick: - offset += poopt.wcswidth(nick) + 1 # + nick + ' ' length - if message.str_time: - offset += 1 + len(message.str_time) - if get_theme().CHAR_TIME_LEFT and message.str_time: - offset += 1 - if get_theme().CHAR_TIME_RIGHT and message.str_time: - offset += 1 - lines = poopt.cut_text(txt, self.width-offset-1) - prepend = default_color if default_color else '' - attrs = [] - for line in lines: - saved = Line(msg=message, start_pos=line[0], end_pos=line[1], prepend=prepend) - attrs = parse_attrs(message.txt[line[0]:line[1]], attrs) - if attrs: - prepend = FORMAT_CHAR + FORMAT_CHAR.join(attrs) - else: - if default_color: - prepend = default_color - else: - prepend = '' - ret.append(saved) - return ret - - def write_prefix(self, nickname, color): - self._win.attron(to_curses_attr(color)) - self.addstr(truncate_nick(nickname)) - self._win.attroff(to_curses_attr(color)) - -- cgit v1.2.3