diff --git a/Makefile b/Makefile index a327413a..8c0935f0 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,8 @@ LOCALEDIR=$(DATADIR)/locale MANDIR=$(DATADIR)/man all: Makefile + python3 setup.py build + cp build/lib.linux-x86_64-3.2/poopt.cpython-32mu.so src/ clean: find ./ -name \*.pyc -delete diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..50d787da --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +from distutils.core import setup, Extension + +module_poopt = Extension('poopt', + sources = ['src/pooptmodule.c']) + +setup (name = 'BuildLines', + version = '0.0.1', + description = 'Poezio Optimizations', + ext_modules = [module_poopt], + author = 'Florent Le Coz', + author_email = 'louiz@louiz.org', + long_description = """ + a python3 module for poezio, used to replace some time-critical + python functions that are too slow. If compiled, poezio will use this module, + otherwise it will just use the equivalent python functions. + """) diff --git a/src/core.py b/src/core.py index a8fa65a5..8d1fde3a 100644 --- a/src/core.py +++ b/src/core.py @@ -355,7 +355,8 @@ class Core(object): return log.debug('on_got_offline: %s' % presence) resource = contact.get_resource_by_fulljid(jid.full) - assert resource + if not resource: + return # If a resource got offline, display the message in the conversation with this # precise resource. self.add_information_message_to_conversation_tab(jid.full, '\x195%s is \x191offline' % (resource.get_jid().full)) @@ -674,7 +675,7 @@ class Core(object): roster.add_contact(contact, jid) roster.edit_groups_of_contact(contact, []) contact.set_ask('asked') - self.tabs[0].set_color_state(theme.COLOR_TAB_HIGHLIGHT) + self.get_tab_by_number(0).set_color_state(theme.COLOR_TAB_HIGHLIGHT) self.information('%s wants to subscribe to your presence'%jid, 'Roster') if isinstance(self.current_tab(), tabs.RosterInfoTab): self.refresh_window() @@ -777,6 +778,12 @@ class Core(object): return tab return None + def get_tab_by_number(self, number): + for tab in self.tabs: + if tab.nb == number: + return tab + return None + def get_room_by_name(self, name): """ returns the room that has this name diff --git a/src/pooptmodule.c b/src/pooptmodule.c new file mode 100644 index 00000000..289313f3 --- /dev/null +++ b/src/pooptmodule.c @@ -0,0 +1,279 @@ +/* Copyright 2010-2011 Florent Le Coz */ + +/* This file is part of Poezio. */ + +/* Poezio is free software: you can redistribute it and/or modify */ +/* it under the terms of the MIT license. See the COPYING file. */ + +/** The poopt python3 module +**/ + +/* This file is a python3 module for poezio, used to replace some time-critical +python functions that are too slow. If compiled, poezio will use this module, +otherwise it will just use the equivalent python functions. */ + +#define PY_SSIZE_T_CLEAN + +#include "Python.h" + +PyObject *ErrorObject; + +/*** + The module functions + ***/ + +/* cut_text: takes a string and returns a tuple of int. + Each two int tuple is a line, represented by the ending position it (where it should be cut). + Not that this position is calculed using the position of the python string characters, + not just the individual bytes. + For example, poopt_cut_text("vivent les frigidaires", 6); + will return [(0, 6), (7, 10), (11, 17), (17, 22)], meaning that the lines are + "vivent", "les", "frigid" and "aires" +*/ +PyDoc_STRVAR(poopt_cut_text_doc, "cut_text(width, text)\n\n\nReturn the list of strings, cut according to the given size."); + +static PyObject *poopt_cut_text(PyObject *self, PyObject *args) +{ + /* int length; */ + unsigned char *buffer; + int width; + + if (PyArg_ParseTuple(args, "si", &buffer, &width) == 0) + return NULL; + + int bpos = 0; /* the real position in the char* */ + int spos = 0; /* the position, considering UTF-8 chars */ + int last_space = -1; + int start_pos = 0; + + PyObject* retlist = PyList_New(0); + + while (buffer[bpos]) + { + if (buffer[bpos] == ' ') + last_space = spos; + else if (buffer[bpos] == '\n') + { + if (PyList_Append(retlist, Py_BuildValue("ii", start_pos, spos)) == -1) + return NULL; + start_pos = spos + 1; + last_space = -1; + } + else if ((spos - start_pos) >= width) + { + if (last_space == -1) + { + if (PyList_Append(retlist, Py_BuildValue("ii", start_pos, spos)) == -1) + return NULL; + start_pos = spos; + } + else + { + if (PyList_Append(retlist, Py_BuildValue("ii", start_pos, last_space)) == -1) + return NULL; + start_pos = last_space + 1; + last_space = -1; + } + } + if (buffer[bpos] == 25) /* \x19 */ + { + spos++; + bpos += 2; + } + else + if (buffer[bpos] <= 127) /* ASCII char on one byte */ + bpos += 1; + else if (buffer[bpos] >= 194 && buffer[bpos] <= 223) + bpos += 2; + else if (buffer[bpos] >= 224 && buffer[bpos] <= 239) + bpos += 3; + else if (buffer[bpos] >= 240 && buffer[bpos] <= 244) + bpos += 4; + else + return NULL; + spos++; + } + if (PyList_Append(retlist, Py_BuildValue("(i,i)", start_pos, spos)) == -1) + return NULL; + return retlist; +} + +/*** + Module initialization. Just taken from the xxmodule.c template from the python sources. + ***/ +static PyTypeObject Str_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "pooptmodule.Str", /*tp_name*/ + 0, /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + 0, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_reserved*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + 0, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ + 0, /* see PyInit_xx */ /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + 0, /*tp_init*/ + 0, /*tp_alloc*/ + 0, /*tp_new*/ + 0, /*tp_free*/ + 0, /*tp_is_gc*/ +}; + +/* ---------- */ + +static PyObject * +null_richcompare(PyObject *self, PyObject *other, int op) +{ + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; +} + +static PyTypeObject Null_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "pooptmodule.Null", /*tp_name*/ + 0, /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + 0, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_reserved*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + null_richcompare, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + 0, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ + 0, /* see PyInit_xx */ /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + 0, /*tp_init*/ + 0, /*tp_alloc*/ + 0, /* see PyInit_xx */ /*tp_new*/ + 0, /*tp_free*/ + 0, /*tp_is_gc*/ +}; + + +/* List of functions defined in the module */ + +static PyMethodDef poopt_methods[] = { + {"cut_text", poopt_cut_text, METH_VARARGS, + poopt_cut_text_doc}, + {NULL, NULL} /* sentinel */ +}; + +PyDoc_STRVAR(module_doc, + "This is a template module just for instruction. And poopt."); + +/* Initialization function for the module (*must* be called PyInit_xx) */ + +static struct PyModuleDef pooptmodule = { + PyModuleDef_HEAD_INIT, + "poopt", + module_doc, + -1, + poopt_methods, + NULL, + NULL, + NULL, + NULL +}; + +PyMODINIT_FUNC +PyInit_poopt(void) +{ + PyObject *m = NULL; + + /* Due to cross platform compiler issues the slots must be filled + * here. It's required for portability to Windows without requiring + * C++. */ + Null_Type.tp_base = &PyBaseObject_Type; + Null_Type.tp_new = PyType_GenericNew; + Str_Type.tp_base = &PyUnicode_Type; + + /* Finalize the type object including setting type of the new type + * object; doing it here is required for portability, too. */ + /* if (PyType_Ready(&Xxo_Type) < 0) */ + /* goto fail; */ + + /* Create the module and add the functions */ + m = PyModule_Create(&pooptmodule); + if (m == NULL) + goto fail; + + /* Add some symbolic constants to the module */ + if (ErrorObject == NULL) { + ErrorObject = PyErr_NewException("poopt.error", NULL, NULL); + if (ErrorObject == NULL) + goto fail; + } + Py_INCREF(ErrorObject); + PyModule_AddObject(m, "error", ErrorObject); + + /* Add Str */ + if (PyType_Ready(&Str_Type) < 0) + goto fail; + PyModule_AddObject(m, "Str", (PyObject *)&Str_Type); + + /* Add Null */ + if (PyType_Ready(&Null_Type) < 0) + goto fail; + PyModule_AddObject(m, "Null", (PyObject *)&Null_Type); + return m; + fail: + Py_XDECREF(m); + return NULL; +} + +/* /\* test function *\/ */ +/* int main(void) */ +/* { */ +/* char coucou[] = "vive le foutre, le beurre et le caca boudin"; */ + +/* cut_text(coucou, 8); */ +/* } */ diff --git a/src/room.py b/src/room.py index 7719dd67..1ca3f599 100644 --- a/src/room.py +++ b/src/room.py @@ -120,7 +120,9 @@ class Room(TextBuffer): nick_color = highlight time = time or datetime.now() message = Message(txt='%s\x19o'%(txt,), nick_color=nick_color, - time=time, nickname=nickname, user=user) + time=time, str_time=time.strftime("%Y-%m-%d %H:%M:%S")\ + if history else time.strftime("%H:%M:%S"),\ + nickname=nickname, user=user) while len(self.messages) > self.messages_nb_limit: self.messages.pop(0) self.messages.append(message) diff --git a/src/tabs.py b/src/tabs.py index 9e55deff..c336adcd 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -14,7 +14,7 @@ Windows are displayed, resized, etc """ MIN_WIDTH = 50 -MIN_HEIGHT = 16 +MIN_HEIGHT = 22 import logging log = logging.getLogger(__name__) @@ -89,6 +89,10 @@ class Tab(object): @staticmethod def resize(scr): Tab.size = (Tab.height, Tab.width) = scr.getmaxyx() + if Tab.height < MIN_HEIGHT or Tab.width < MIN_WIDTH: + Tab.visible = False + else: + Tab.visible = True def complete_commands(self, the_input): """ @@ -671,6 +675,8 @@ class MucTab(ChatTab): """ Resize the whole window. i.e. all its sub-windows """ + if not self.visible: + return text_width = (self.width//10)*9 self.topic_win.resize(1, self.width, 0, 0) self.v_separator.resize(self.height-3, 1, 1, 9*(self.width//10)) @@ -1018,7 +1024,7 @@ class PrivateTab(ChatTab): self.core.close_tab() def resize(self): - if self.core.information_win_size >= self.height-3: + if self.core.information_win_size >= self.height-3 or not self.visible: return self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0) self.text_win.rebuild_everything(self._room) @@ -1165,6 +1171,8 @@ class RosterInfoTab(Tab): self.resize() def resize(self): + if not self.visible: + return roster_width = self.width//2 info_width = self.width-roster_width-1 self.v_separator.resize(self.height-2, 1, 0, roster_width) @@ -1518,7 +1526,7 @@ class ConversationTab(ChatTab): self.core.close_tab() def resize(self): - if self.core.information_win_size >= self.height-3: + if self.core.information_win_size >= self.height-3 or not self.visible: return self.text_win.resize(self.height-4-self.core.information_win_size, self.width, 1, 0) self.text_win.rebuild_everything(self._room) @@ -1635,6 +1643,8 @@ class MucListTab(Tab): self.input.refresh() def resize(self): + if not self.visible: + return self.upper_message.resize(1, self.width, 0, 0) column_size = {'node-part': (self.width-5)//4, 'name': (self.width-5)//4*3, @@ -1761,6 +1771,8 @@ class SimpleTextTab(Tab): self.core.close_tab() def resize(self): + if not self.visible: + return self.text_win.resize(self.height-2, self.width, 0, 0) self.input.resize(1, self.width, self.height-1, 0) diff --git a/src/text_buffer.py b/src/text_buffer.py index a3b5b1fb..a6465da3 100644 --- a/src/text_buffer.py +++ b/src/text_buffer.py @@ -27,7 +27,7 @@ from datetime import datetime import theme from config import config -Message = collections.namedtuple('Message', 'txt nick_color time nickname user') +Message = collections.namedtuple('Message', 'txt nick_color time str_time nickname user') class TextBuffer(object): """ @@ -45,8 +45,11 @@ class TextBuffer(object): self.windows.append(win) def add_message(self, txt, time=None, nickname=None, nick_color=None, history=None): + time = time or datetime.now() msg = Message(txt='%s\x19o'%(txt,), nick_color=nick_color, - time=time or datetime.now(), nickname=nickname, user=None) + time=time, str_time=time.strftime("%Y-%m-%d %H:%M:%S")\ + if history else time.strftime("%H:%M:%S"),\ + nickname=nickname, user=None) self.messages.append(msg) while len(self.messages) > self.messages_nb_limit: self.messages.pop(0) diff --git a/src/windows.py b/src/windows.py index 8e21d0f8..5b673c21 100644 --- a/src/windows.py +++ b/src/windows.py @@ -28,6 +28,7 @@ from threading import Lock from contact import Contact, Resource from roster import RosterGroup, roster +from poopt import cut_text # from message import Line from tabs import MIN_WIDTH, MIN_HEIGHT @@ -41,7 +42,10 @@ import wcwidth import singleton import collections -Line = collections.namedtuple('Line', 'text text_offset nickname_color time nickname') +# msg is a reference to the corresponding Message tuple. text_start and text_end are the position +# delimiting the text in this line. +# first is a bool telling if this is the first line of the message. +Line = collections.namedtuple('Line', 'msg start_pos end_pos first') g_lock = Lock() @@ -89,6 +93,12 @@ class Win(object): 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 @@ -101,7 +111,7 @@ class Win(object): one of 'u', 'b', 'c[0-9]' """ if y is not None and x is not None: - self._win.move(y, x) + self.move(y, x) next_attr_char = text.find('\x19') while next_attr_char != -1: if next_attr_char + 1 < len(text): @@ -529,30 +539,14 @@ class TextWin(Win): if None not in self.built_lines: self.built_lines.append(None) - def build_new_message(self, message, history=None): + def build_new_message(self, message, history=None, clean=True): """ Take one message, build it and add it to the list Return the number of lines that are built for the given message. """ - def cut_text(text, width): - """ - returns the text that should be displayed on the line, and the rest - of the text, in a tuple - """ - cutted = wcwidth.widthcut(text, width) or text[:width] - limit = cutted.find('\n') - if limit >= 0: - return (text[limit+1:], text[:limit]) - if not wcwidth.wcsislonger(text, width): - return ('', text) - limit = cutted.rfind(' ') - if limit <= 0: - return (text[len(cutted):], cutted) - else: - return (text[limit+1:], text[:limit]) - if message is None: # line separator + log.debug('je build NON, cool non ? +++++++++++++++++++++++++++') self.built_lines.append(None) return 0 txt = message.txt @@ -560,53 +554,32 @@ class TextWin(Win): return 0 else: txt = txt.replace('\t', ' ') - # length of the time - if history: - offset = 20 - else: - offset = 9 - if theme.CHAR_TIME_RIGHT: - offset += 1 - if theme.CHAR_TIME_RIGHT: - offset += 1 - nickname = message.nickname - if nickname and len(nickname) >= 25: - nick = nickname[:25]+'…' - else: - nick = nickname + nick = message.nickname + if nick and len(nick) >= 25: + nick = nick[:25]+'…' + offset = 1 + len(message.str_time) if nick: offset += wcwidth.wcswidth(nick) + 2 # + nick + spaces length + if nick: + offset += wcwidth.wcswidth(nick) + 2 # + nick + spaces length + if theme.CHAR_TIME_LEFT: + offset += 1 + if theme.CHAR_TIME_RIGHT: + offset += 1 + + lines = cut_text(txt, self.width-offset-1) + first = True - nb = 0 - while txt != '': - (txt, cutted_txt) = cut_text(txt, self.width-offset-1) - if first: - if message.nick_color: - color = message.nick_color - elif message.user: - color = message.user.color - else: - color = None - else: - color = None - if first: - if history: - time = message.time.strftime("%Y-%m-%d %H:%M:%S") - else: - time = message.time.strftime("%H:%M:%S") - nickname = nick - else: - time = None - nickname = None - self.built_lines.append(Line(text=cutted_txt, - text_offset=offset, - nickname_color=color, time=time, - nickname=nickname)) - nb += 1 + for line in lines: + self.built_lines.append(Line(msg=message, + start_pos=line[0], + end_pos=line[1], + first=first)) first = False - while len(self.built_lines) > self.lines_nb_limit: - self.built_lines.pop(0) - return nb + if clean: + while len(self.built_lines) > self.lines_nb_limit: + self.built_lines.pop(0) + return len(lines) def refresh(self, room): log.debug('Refresh: %s'%self.__class__.__name__) @@ -621,17 +594,26 @@ class TextWin(Win): self._win.erase() for y, line in enumerate(lines): if line is None: + log.debug('COUCOU JE SUIS NONE\n\n-----------------') self.write_line_separator() else: - self.write_time(line.time) - self.write_nickname(line.nickname, line.nickname_color) + msg = line.msg + if line.first: + if msg.nick_color: + color = msg.nick_color + elif msg.user: + color = msg.user.color + else: + color = None + self.write_time(msg.str_time) + self.write_nickname(msg.nickname, color) if y != self.height-1: self.addstr('\n') self._win.attrset(0) for y, line in enumerate(lines): if not line: continue - self.write_text(y, line.text_offset, line.text) + self.write_text(y, (3 if line.msg.nickname else 1) + len(line.msg.str_time)+len(line.msg.nickname or ''), line.msg.txt[line.start_pos:line.end_pos]) if y != self.height-1: self.addstr('\n') self._win.attrset(0) @@ -676,7 +658,9 @@ class TextWin(Win): def rebuild_everything(self, room): self.built_lines = [] for message in room.messages: - self.build_new_message(message) + self.build_new_message(message, clean=False) + 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)))