From 2ed32d1a11670cd2593c77cc13826579cba597d8 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Tue, 21 Aug 2007 12:18:38 -0400 Subject: [PATCH] DevConsole: New IRC Client interface --- configure.ac | 5 +- services/console/console.py | 4 +- services/console/interface/Makefile.am | 2 +- .../console/interface/irc_client/Makefile.am | 5 + .../console/interface/irc_client/__init__.py | 2 + .../interface/irc_client/irc_client.py | 10 + services/console/interface/network/network.py | 2 +- services/console/interface/xo/cpu.py | 1 - services/console/lib/Makefile.am | 2 +- services/console/lib/gui/Makefile.am | 5 + services/console/lib/gui/__init__.py | 0 services/console/lib/gui/treeview.py | 73 ++ services/console/lib/purk/ABOUT | 7 + services/console/lib/purk/COPYING | 340 ++++++++ services/console/lib/purk/Makefile.am | 17 + services/console/lib/purk/README | 186 ++++ services/console/lib/purk/UrkLogQueryable.cs | 245 ++++++ services/console/lib/purk/__init__.py | 94 ++ services/console/lib/purk/conf.py | 86 ++ services/console/lib/purk/events.py | 298 +++++++ services/console/lib/purk/info.py | 8 + services/console/lib/purk/irc.py | 328 +++++++ services/console/lib/purk/parse_mirc.py | 457 ++++++++++ services/console/lib/purk/scripts/Makefile.am | 16 + services/console/lib/purk/scripts/alias.py | 60 ++ services/console/lib/purk/scripts/chaninfo.py | 320 +++++++ services/console/lib/purk/scripts/clicks.py | 146 ++++ .../console/lib/purk/scripts/completion.py | 135 +++ services/console/lib/purk/scripts/console.py | 68 ++ services/console/lib/purk/scripts/history.py | 45 + services/console/lib/purk/scripts/ignore.py | 43 + .../console/lib/purk/scripts/irc_script.py | 588 +++++++++++++ services/console/lib/purk/scripts/keys.py | 70 ++ services/console/lib/purk/scripts/theme.py | 366 ++++++++ services/console/lib/purk/scripts/timeout.py | 45 + .../console/lib/purk/scripts/ui_script.py | 132 +++ services/console/lib/purk/servers.py | 51 ++ services/console/lib/purk/ui.py | 105 +++ services/console/lib/purk/urk_trace.py | 70 ++ services/console/lib/purk/widgets.py | 811 ++++++++++++++++++ services/console/lib/purk/windows.py | 298 +++++++ 41 files changed, 5539 insertions(+), 7 deletions(-) create mode 100644 services/console/interface/irc_client/Makefile.am create mode 100644 services/console/interface/irc_client/__init__.py create mode 100644 services/console/interface/irc_client/irc_client.py create mode 100644 services/console/lib/gui/Makefile.am create mode 100644 services/console/lib/gui/__init__.py create mode 100644 services/console/lib/gui/treeview.py create mode 100644 services/console/lib/purk/ABOUT create mode 100644 services/console/lib/purk/COPYING create mode 100644 services/console/lib/purk/Makefile.am create mode 100644 services/console/lib/purk/README create mode 100644 services/console/lib/purk/UrkLogQueryable.cs create mode 100644 services/console/lib/purk/__init__.py create mode 100644 services/console/lib/purk/conf.py create mode 100644 services/console/lib/purk/events.py create mode 100644 services/console/lib/purk/info.py create mode 100644 services/console/lib/purk/irc.py create mode 100644 services/console/lib/purk/parse_mirc.py create mode 100644 services/console/lib/purk/scripts/Makefile.am create mode 100755 services/console/lib/purk/scripts/alias.py create mode 100644 services/console/lib/purk/scripts/chaninfo.py create mode 100644 services/console/lib/purk/scripts/clicks.py create mode 100644 services/console/lib/purk/scripts/completion.py create mode 100755 services/console/lib/purk/scripts/console.py create mode 100644 services/console/lib/purk/scripts/history.py create mode 100755 services/console/lib/purk/scripts/ignore.py create mode 100644 services/console/lib/purk/scripts/irc_script.py create mode 100644 services/console/lib/purk/scripts/keys.py create mode 100644 services/console/lib/purk/scripts/theme.py create mode 100755 services/console/lib/purk/scripts/timeout.py create mode 100644 services/console/lib/purk/scripts/ui_script.py create mode 100644 services/console/lib/purk/servers.py create mode 100644 services/console/lib/purk/ui.py create mode 100644 services/console/lib/purk/urk_trace.py create mode 100644 services/console/lib/purk/widgets.py create mode 100644 services/console/lib/purk/windows.py diff --git a/configure.ac b/configure.ac index b7b348cd..d61f24b4 100644 --- a/configure.ac +++ b/configure.ac @@ -69,7 +69,9 @@ services/console/lib/Makefile services/console/lib/graphics/Makefile services/console/lib/procmem/Makefile services/console/lib/net/Makefile -services/console/lib/ui/Makefile +services/console/lib/gui/Makefile +services/console/lib/purk/Makefile +services/console/lib/purk/scripts/Makefile services/console/Makefile services/console/interface/Makefile services/console/interface/xo/Makefile @@ -82,6 +84,7 @@ services/console/interface/memphis/Makefile services/console/interface/network/Makefile services/console/interface/logviewer/Makefile services/console/interface/terminal/Makefile +services/console/interface/irc_client/Makefile sugar/Makefile sugar/activity/Makefile sugar/clipboard/Makefile diff --git a/services/console/console.py b/services/console/console.py index 4fb36095..cf33a765 100755 --- a/services/console/console.py +++ b/services/console/console.py @@ -30,7 +30,7 @@ CONSOLE_BUS = 'org.laptop.sugar.Console' CONSOLE_PATH = '/org/laptop/sugar/Console' CONSOLE_IFACE = 'org.laptop.sugar.Console' -class Console: +class Console(object): def __init__(self): # Main Window @@ -54,6 +54,7 @@ class Console: self._load_interface('memphis', 'Memphis') self._load_interface('logviewer', 'Log Viewer') self._load_interface('terminal', 'Terminal') + self._load_interface('irc_client', 'IRC') self._load_interface('ps_watcher', 'Presence') main_hbox = gtk.HBox() @@ -90,5 +91,4 @@ bus = dbus.SessionBus() name = dbus.service.BusName(CONSOLE_BUS, bus) obj = Service(name) - gtk.main() diff --git a/services/console/interface/Makefile.am b/services/console/interface/Makefile.am index ef0f3e4f..628ab2cd 100644 --- a/services/console/interface/Makefile.am +++ b/services/console/interface/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = memphis network logviewer terminal xo +SUBDIRS = irc_client memphis network logviewer terminal xo sugardir = $(pkgdatadir)/services/console/interface sugar_PYTHON = \ diff --git a/services/console/interface/irc_client/Makefile.am b/services/console/interface/irc_client/Makefile.am new file mode 100644 index 00000000..934d9afd --- /dev/null +++ b/services/console/interface/irc_client/Makefile.am @@ -0,0 +1,5 @@ +sugardir = $(pkgdatadir)/services/console/interface/irc_client +sugar_PYTHON = \ + __init__.py \ + irc_client.py + diff --git a/services/console/interface/irc_client/__init__.py b/services/console/interface/irc_client/__init__.py new file mode 100644 index 00000000..5f8fa13e --- /dev/null +++ b/services/console/interface/irc_client/__init__.py @@ -0,0 +1,2 @@ +from irc_client import Interface + diff --git a/services/console/interface/irc_client/irc_client.py b/services/console/interface/irc_client/irc_client.py new file mode 100644 index 00000000..a7189594 --- /dev/null +++ b/services/console/interface/irc_client/irc_client.py @@ -0,0 +1,10 @@ +import purk + +class Interface(object): + def __init__(self): + client = purk.Client() + client.show() + client.join_server('irc.freenode.net') + self.widget = client.get_widget() + + diff --git a/services/console/interface/network/network.py b/services/console/interface/network/network.py index 1f226905..ecd09aa9 100644 --- a/services/console/interface/network/network.py +++ b/services/console/interface/network/network.py @@ -17,7 +17,7 @@ import gobject from net.device import Device -from ui.treeview import TreeView +from gui.treeview import TreeView class NetworkView(TreeView): def __init__(self): diff --git a/services/console/interface/xo/cpu.py b/services/console/interface/xo/cpu.py index 6f6a5a79..30a2ca34 100644 --- a/services/console/interface/xo/cpu.py +++ b/services/console/interface/xo/cpu.py @@ -101,7 +101,6 @@ class XO_CPU(gtk.Frame): gobject.timeout_add(self._DRW_CPU.frequency, self._update_cpu_usage) def _update_cpu_usage(self): - print "update XO CPU" self._cpu = self._DRW_CPU._get_CPU_usage() self.set_label('System CPU Usage: ' + str(self._cpu) + '%') diff --git a/services/console/lib/Makefile.am b/services/console/lib/Makefile.am index 0d4dcce2..02014465 100644 --- a/services/console/lib/Makefile.am +++ b/services/console/lib/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = procmem graphics net ui +SUBDIRS = procmem graphics net gui purk sugardir = $(pkgdatadir)/shell/console/lib sugar_PYTHON = diff --git a/services/console/lib/gui/Makefile.am b/services/console/lib/gui/Makefile.am new file mode 100644 index 00000000..c045c9eb --- /dev/null +++ b/services/console/lib/gui/Makefile.am @@ -0,0 +1,5 @@ +sugardir = $(pkgdatadir)/services/console/lib/gui + +sugar_PYTHON = \ + __init__.py \ + treeview.py diff --git a/services/console/lib/gui/__init__.py b/services/console/lib/gui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/console/lib/gui/treeview.py b/services/console/lib/gui/treeview.py new file mode 100644 index 00000000..5f5dc968 --- /dev/null +++ b/services/console/lib/gui/treeview.py @@ -0,0 +1,73 @@ +# Copyright (C) 2007, Eduardo Silva +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk + +class TreeView(gtk.ScrolledWindow): + iters = [] # Iters index + + # Create a window with a treeview object + # + # cols = List of dicts, ex: + # + # cols = [] + # cols.append({'index': integer_index_position, 'name': string_col_name}) + def __init__(self, cols_def, cols_name): + gtk.ScrolledWindow.__init__(self) + + self._iters = [] + self._treeview = gtk.TreeView() + + # Creating column data types + self._store = gtk.TreeStore(*cols_def) + + # Columns definition + cell = gtk.CellRendererText() + tv_cols = [] + + i=0 + for col in cols_name: + col_tv = gtk.TreeViewColumn(col['name'], cell, text=i) + col_tv.set_reorderable(True) + col_tv.set_resizable(True) + tv_cols.append(col_tv) + i+=1 + + # Setting treeview properties + self._treeview.set_model(self._store) + self._treeview.set_enable_search(True) + self._treeview.set_rules_hint(True) + + for col in tv_cols: + self._treeview.append_column(col) + self.add(self._treeview) + + def add_row(self, cols_data): + iter = self._store.insert_after(None, None) + for col in cols_data: + print col['index'],col['info'] + self._store.set_value(iter, int(col['index']) , col['info']) + + self.iters.append(iter) + return iter + + def update_row(self, iter, cols_data): + for col in cols_data: + self._store.set_value(iter, int(col['index']) , str(col['info'])) + + def remove_row(self, iter): + self._store.remove(iter) diff --git a/services/console/lib/purk/ABOUT b/services/console/lib/purk/ABOUT new file mode 100644 index 00000000..d1683db1 --- /dev/null +++ b/services/console/lib/purk/ABOUT @@ -0,0 +1,7 @@ +Urk is a PyGTK IRC Client written by Vincent Povirk and Marc Liddell. +This current version has been modified in order to have an PyGTK +IRC Client Widget called 'Purk'. + +Suggestions are welcome... + +Eduardo Silva diff --git a/services/console/lib/purk/COPYING b/services/console/lib/purk/COPYING new file mode 100644 index 00000000..3912109b --- /dev/null +++ b/services/console/lib/purk/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/services/console/lib/purk/Makefile.am b/services/console/lib/purk/Makefile.am new file mode 100644 index 00000000..33fe5957 --- /dev/null +++ b/services/console/lib/purk/Makefile.am @@ -0,0 +1,17 @@ +SUBDIRS = scripts +sugardir = $(pkgdatadir)/services/console/lib/purk + +sugar_PYTHON = \ + __init__.py \ + conf.py \ + events.py \ + info.py \ + irc.py \ + parse_mirc.py \ + servers.py \ + ui.py \ + urk_trace.py \ + widgets.py \ + windows.py \ + README \ + ABOUT diff --git a/services/console/lib/purk/README b/services/console/lib/purk/README new file mode 100644 index 00000000..9e55cca0 --- /dev/null +++ b/services/console/lib/purk/README @@ -0,0 +1,186 @@ +urk 0.-1.cvs +http://urk.sourceforge.net/ + + +Overview: + +urk is an IRC client written purely in python for linux/gnome. It has a powerful +built-in scripting system (also python), which is used to implement much of the +client. + + +Requirements/User installation: + +urk requires the following packages (and should run on any os that can provide +them): +-python version 2.4 or greater (www.python.org) +-pygtk 2.6 or greater (www.pygtk.org) + +Most Linux (or at least GNOME) users should have these things already or be able +to easily install them. + +Because urk is pure python, no compilation of urk is required. Just extract the +source to somewhere and run 'python urk.py'. + +Windows versions of the above should theoretically work but may not in practice. +I am hoping someone will come along and actually support urk on windows. + + +Optional requirements: + +urk can also make use of these packages if you have them: +-pygtksourceview, part of gnome-python-extras <=2.12 and gnome-python-desktop + >=2.14, for source highlighting and undo in the internal script editor +-python-dbus, for, well, not much (if dbus is available, urk only makes a single + instance per session, and commands can be executed remotely by calling urk.py) + + +Getting started: + +Make sure you have all the requirements, go to the directory where you have +extracted the source, and type 'python urk.py'. + +We don't have any preferences windows yet. You can change your nickname by +typing '/nick nickname' (replacing nickname with the nick you want to use) +or typing a new nick in the nick field on the lower right corner and pressing +enter. + +To connect, type '/server irc.gamesurge.net' (replacing irc.gamesurge.net with +the server you want to connect to). + +If you want to connect to another server (without disconnecting the current +one), use the -m switch as in '/server -m irc.gamesurge.net'. + +To join a channel when you're connected to a server, type '/join #channelname', +replacing #channelname with the channel you want to join. + +urk currently only supports the bare minimum commands and features you should +need to connect and chat normally. On channels, you can send messages by +typing them (if you want to send a message that starts with a /, use /say to +send your message). You can send actions to channels with /me and send messages +to arbitrary targets with /msg. If urk does not recognize a command, it will +send it to the server. This works to implement most commands you would expect +on an irc client. + + +Configuration: + +Most configuration has to be done manually. If urk is running, you can configure +it using commands. The settings are stored in urk.conf on your profile +directory, which you can locate by typing '/pyeval urk.userpath' in urk. + +To set a value in urk, type + +/pyexec conf.conf['setting'] = value + +To see the current value, type + +/pyeval conf.conf['setting'] + +To unset a value (meaning urk will use the default), type + +/pyexec del conf.conf['setting'] + +Setting: Description: + +'nick' The nickname urk should use. The default is to try to get + it from the os. Put this in quotes when you set it. + +'altnicks' A list of alternative nicknames to use. The default is + an empty list. + +'quitmsg' The message people see when you quit. The default is to + advertise urk with your current version; we have to promote it somehow. + This value needs to be in quotes. + +'autoreconnect' If True, urk will try to reconnect when you're + disconnected from a network. Defaults to True. Set this to True or False. + +'highlight_words' A list of words, in addition to your nick, that cause a + highlight event (normally the tab label turns blue and, if it's available, + the tray icon shows up). For example: ['python', 'whale', 'peanut butter'] + +'log_dir' The place where logs are written. The default is a + directory called "logs" on your profile directory. + +'ui-gtk/tab-pos' The side of the window where the tabs will reside + 2 for top + 0 for left + 1 for right + 3 for bottom (default) + +'ui-gtk/show-menubar' If True, the menubar is shown. The default is True. Set + this to True or False. + +'command_prefix' The prefix used to signal a command. Defaults to '/' + +'font' The font used for output. Defaults to "sans 8". + +'bg_color' The background color ("black" or "#000000"). + +'fg_color' The foreground color ("white" or "#ffffff"). + +'timestamp' A timestamp that will show up before messages. The + default is no timestamp. A simple timestamp with hours and minutes is + "[%H:%M] ". See http://docs.python.org/lib/module-time.html#l2h-1955 for + a key to format this string. + +'start-console' If True, urk will start up with a special console window + that shows debugging output (normally sent to a terminal) and accepts + python expressions. Defaults to False. + +'status' If True, urk will be in status window mode. Each network + will ALWAYS have a status window. When not in status window mode, networks + only have a status window when there are no channel windows. Defaults to + False. + +'open-file-command' The command used to open files and url's with your + preferred application (such as "gnome-open"). This is ignored on Windows, + and you should never need to mess with this, ever. + + +System-wide installation: + +Not yet implemented. + + +About scripting: + +urk scripts are python source files that contain definitions of functions with +certain "magic" names, like onText (for when someone sends a channel or private +message). See www.python.org for help on writing python code. The format for +onText is: + +def onText(e): + code + +e is an object used to pass on the various information relating to the event +that triggered the function. The name is a convention we use, much like self. + +e.source is the nickname of the person who sent the message. + +e.target is the nickname or channel that received the message. + +e.text is the text of the message. + +e.network is an object representing the network where the message was sent. + +e.window is a window that seems to be related to the event for some unspecified +reason. It could be the status window, a channel window, a query, anything. + +Complete documentation doesn't exist yet. Sorry. Ask us or look at the source. theme.py is good for finding event names. + + +Bugs/Feedback: + +Naturally, feedback of any sort is welcome. Of course we want to know about +bugs. In particular, we'd also like to hear about any features you want or +expect in an irc client that urk doesn't have. While we'd like to limit the +things that go in the default client (a notify list, for example, is something +we'd want to see as an external script, not as part of the default setup, and +something we're not likely to implement soon), there are probably a lot of +little things that we may have skipped over because we don't use them or have +become used to urk not having them. + +The best way to get in touch with us is by irc, at #urk on irc.gamesurge.net. +Or send a message to the mailing list, urk-discussion@lists.sourceforge.net. diff --git a/services/console/lib/purk/UrkLogQueryable.cs b/services/console/lib/purk/UrkLogQueryable.cs new file mode 100644 index 00000000..ee0611c2 --- /dev/null +++ b/services/console/lib/purk/UrkLogQueryable.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections; +using System.IO; +using System.Text; +using System.Threading; + +using Beagle.Daemon; +using Beagle.Util; + +namespace Beagle.Daemon.UrkLogQueryable { + + [QueryableFlavor (Name="UrkLog", Domain=QueryDomain.Local, RequireInotify=false)] + public class UrkLogQueryable : LuceneFileQueryable { + + private static Logger log = Logger.Get ("UrkLogQueryable"); + + private string config_dir, log_dir, icons_dir; + + private int polling_interval_in_seconds = 60; + + //private GaimBuddyListReader list = new GaimBuddyListReader (); + + public UrkLogQueryable () : base ("UrkLogIndex") + { + config_dir = Path.Combine (PathFinder.HomeDir, ".urk"); + log_dir = Path.Combine (config_dir, "logs"); + icons_dir = Path.Combine (config_dir, "icons"); + } + + ///////////////////////////////////////////////// + + private void StartWorker() + { + if (! Directory.Exists (log_dir)) { + GLib.Timeout.Add (60000, new GLib.TimeoutHandler (CheckForExistence)); + return; + } + + log.Info ("Starting urk log backend"); + + Stopwatch stopwatch = new Stopwatch (); + stopwatch.Start (); + + State = QueryableState.Crawling; + Crawl (); + State = QueryableState.Idle; + + if (!Inotify.Enabled) { + Scheduler.Task task = Scheduler.TaskFromHook (new Scheduler.TaskHook (CrawlHook)); + task.Tag = "Crawling ~/.urk/logs to find new logfiles"; + task.Source = this; + ThisScheduler.Add (task); + } + + stopwatch.Stop (); + + log.Info ("urk log backend worker thread done in {0}", stopwatch); + } + + public override void Start () + { + base.Start (); + + ExceptionHandlingThread.Start (new ThreadStart (StartWorker)); + } + + ///////////////////////////////////////////////// + + private void CrawlHook (Scheduler.Task task) + { + Crawl (); + task.Reschedule = true; + task.TriggerTime = DateTime.Now.AddSeconds (polling_interval_in_seconds); + } + + private void Crawl () + { + Inotify.Subscribe (log_dir, OnInotifyNewProtocol, Inotify.EventType.Create); + + // Walk through protocol subdirs + foreach (string proto_dir in DirectoryWalker.GetDirectories (log_dir)) + CrawlProtocolDirectory (proto_dir); + } + + private void CrawlNetworkDirectory (string proto_dir) + { + Inotify.Subscribe (proto_dir, OnInotifyNewTarget, Inotify.EventType.Create); + + // Walk through accounts + foreach (string account_dir in DirectoryWalker.GetDirectories (proto_dir)) + CrawlTargetDirectory (account_dir); + } + + private void CrawlTargetDirectory (string account_dir) + { + Inotify.Subscribe (account_dir, OnInotifyNewRemote, Inotify.EventType.Create); + + // Walk through remote user conversations + foreach (string remote_dir in DirectoryWalker.GetDirectories (account_dir)) + CrawlRemoteDirectory (remote_dir); + } + + private void CrawlRemoteDirectory (string remote_dir) + { + Inotify.Subscribe (remote_dir, OnInotifyNewConversation, Inotify.EventType.CloseWrite); + + foreach (FileInfo file in DirectoryWalker.GetFileInfos (remote_dir)) + if (FileIsInteresting (file.Name)) + IndexLog (file.FullName, Scheduler.Priority.Delayed); + } + + ///////////////////////////////////////////////// + + private bool CheckForExistence () + { + if (!Directory.Exists (log_dir)) + return true; + + this.Start (); + + return false; + } + + private bool FileIsInteresting (string filename) + { + // Filename must be fixed length, see below + if (filename.Length < 21 || filename.Length > 22) + return false; + + // Check match on regex: ^[0-9]{4}-[0-9]{2}-[0-9]{2}\\.[0-9]{6}\\.(txt|html)$ + // e.g. 2005-07-22.161521.txt + // We'd use System.Text.RegularExpressions if they werent so much more expensive + return Char.IsDigit (filename [0]) && Char.IsDigit (filename [1]) + && Char.IsDigit (filename [2]) && Char.IsDigit (filename [3]) + && filename [4] == '-' + && Char.IsDigit (filename [5]) && Char.IsDigit (filename [6]) + && filename [7] == '-' + && Char.IsDigit (filename [8]) && Char.IsDigit (filename [9]) + && filename [10] == '.' + && Char.IsDigit (filename [11]) && Char.IsDigit (filename [12]) + && Char.IsDigit (filename [13]) && Char.IsDigit (filename [14]) + && Char.IsDigit (filename [15]) && Char.IsDigit (filename [16]) + && filename [17] == '.' + && ( (filename [18] == 't' && filename [19] == 'x' && filename [20] == 't') + || (filename [18] == 'h' && filename [19] == 't' && filename [20] == 'm' && filename [21] == 'l') + ); + } + + ///////////////////////////////////////////////// + + private void OnInotifyNewNetwork (Inotify.Watch watch, + string path, string subitem, string srcpath, + Inotify.EventType type) + { + if (subitem.Length == 0 || (type & Inotify.EventType.IsDirectory) == 0) + return; + + CrawlNetworkDirectory (Path.Combine (path, subitem)); + } + + private void OnInotifyNewTarget (Inotify.Watch watch, + string path, string subitem, string srcpath, + Inotify.EventType type) + { + if (subitem.Length == 0 || (type & Inotify.EventType.IsDirectory) == 0) + return; + + CrawlTargetDirectory (Path.Combine (path, subitem)); + } + + private void OnInotifyNewRemote (Inotify.Watch watch, + string path, string subitem, string srcpath, + Inotify.EventType type) + { + if (subitem.Length == 0 || (type & Inotify.EventType.IsDirectory) == 0) + return; + + CrawlRemoteDirectory (Path.Combine (path, subitem)); + } + + private void OnInotifyNewConversation (Inotify.Watch watch, + string path, string subitem, string srcpath, + Inotify.EventType type) + { + if (subitem.Length == 0 || (type & Inotify.EventType.IsDirectory) != 0) + return; + + if (FileIsInteresting (subitem)) + IndexLog (Path.Combine (path, subitem), Scheduler.Priority.Immediate); + } + + ///////////////////////////////////////////////// + + private static Indexable IRCLogToIndexable (string filename) + { + Uri uri = UriFu.PathToFileUri (filename); + Indexable indexable = new Indexable (uri); + indexable.ContentUri = uri; + indexable.Timestamp = File.GetLastWriteTimeUtc (filename); + indexable.MimeType = GaimLog.MimeType; // XXX + indexable.HitType = "IRCLog"; + indexable.CacheContent = false; + + return indexable; + } + + private void IndexLog (string filename, Scheduler.Priority priority) + { + if (! File.Exists (filename)) + return; + + if (IsUpToDate (filename)) + return; + + Indexable indexable = IRCLogToIndexable (filename); + Scheduler.Task task = NewAddTask (indexable); + task.Priority = priority; + task.SubPriority = 0; + ThisScheduler.Add (task); + } + + override protected double RelevancyMultiplier (Hit hit) + { + return HalfLifeMultiplierFromProperty (hit, 0.25, + "fixme:endtime", "fixme:starttime"); + } + + override protected bool HitFilter (Hit hit) + { + /*ImBuddy buddy = list.Search (hit ["fixme:speakingto"]); + + if (buddy != null) { + if (buddy.Alias != "") + hit ["fixme:speakingto_alias"] = buddy.Alias; + + //if (buddy.BuddyIconLocation != "") + // hit ["fixme:speakingto_icon"] = Path.Combine (icons_dir, buddy.BuddyIconLocation); + }*/ + + return true; + } + + } +} + diff --git a/services/console/lib/purk/__init__.py b/services/console/lib/purk/__init__.py new file mode 100644 index 00000000..35d93ad6 --- /dev/null +++ b/services/console/lib/purk/__init__.py @@ -0,0 +1,94 @@ +import os +import sys +import traceback +import events +import windows + +urkpath = os.path.abspath(os.path.dirname(__file__)) + +if os.path.abspath(os.curdir) != os.path.join(urkpath): + sys.path[0] = os.path.join(urkpath) + +sys.path = [ + os.path.join(urkpath, "scripts"), + ] + sys.path + +script_path = urkpath+"/scripts" + +from ui import * + +# Here I'm trying to handle the original URL IRC Client, urk don't use +# normal classes . Let's try to get a urk widget: +class Trigger(object): + def __init__(self): + self._mods = [] + self.events = events + self._load_scripts() + + def _load_scripts(self): + script_path = urkpath + "/scripts" + try: + suffix = os.extsep+"py" + for script in os.listdir(script_path): + if script.endswith(suffix): + try: + mod = self.events.load(script) + self._mods.append(mod) + except: + traceback.print_exc() + print "Failed loading script %s." % script + except OSError: + pass + + def get_modules(self): + return self._mods + +class Core(object): + def __init__(self): + self.window = None + self.trigger = Trigger() + self.events = self.trigger.events + self.manager = widgets.UrkUITabs(self) + + mods = self.trigger.get_modules() + for m in mods: + m.core = self + m.manager = self.manager + + if not self.window: + self.window = windows.new(windows.StatusWindow, irc.Network(self), "status", self) + self.window.activate() + + register_idle(self.trigger_start) + gtk.gdk.threads_enter() + + def run_command(self, command): + offset = 0 + if command[0] == '/': + offset = 1 + + self.events.run(command[offset:], self.manager.get_active(), self.window.network) + + def trigger_start(self): + self.events.trigger("Start") + + def _add_script(self, module): + return + +class Client(object): + def __init__(self): + self.core = Core() + self.widget = self.core.manager.box + def run_command(self, command): + self.core.run_command(command) + + def join_server(self, network_name, port=6667): + command = 'server '+ network_name + ' ' + str(port) + self.run_command(command) + + def get_widget(self): + return self.widget + + def show(self): + self.widget.show_all() + diff --git a/services/console/lib/purk/conf.py b/services/console/lib/purk/conf.py new file mode 100644 index 00000000..9cae5127 --- /dev/null +++ b/services/console/lib/purk/conf.py @@ -0,0 +1,86 @@ +import os +urkpath = os.path.dirname(__file__) + +def path(filename=""): + if filename: + return os.path.join(urkpath, filename) + else: + return urkpath + +if os.access(path('profile'),os.F_OK) or os.path.expanduser("~") == "~": + userpath = path('profile') + if not os.access(userpath,os.F_OK): + os.mkdir(userpath) + if not os.access(os.path.join(userpath,'scripts'),os.F_OK): + os.mkdir(os.path.join(userpath,'scripts')) +else: + userpath = os.path.join(os.path.expanduser("~"), ".urk") + if not os.access(userpath,os.F_OK): + os.mkdir(userpath, 0700) + if not os.access(os.path.join(userpath,'scripts'),os.F_OK): + os.mkdir(os.path.join(userpath,'scripts'), 0700) + +CONF_FILE = os.path.join(userpath,'urk.conf') + + +def pprint(obj, depth=-2): + depth += 2 + + string = [] + + if isinstance(obj, dict): + if obj: + string.append('{\n') + + for key in obj: + string.append('%s%s%s' % (' '*depth, repr(key), ': ')) + string += pprint(obj[key], depth) + + string.append('%s%s' % (' '*depth, '},\n')) + + else: + string.append('{},\n') + + elif isinstance(obj, list): + if obj: + string.append('[\n') + + for item in obj: + string.append('%s' % (' '*depth)) + string += pprint(item, depth) + + string.append('%s%s' % (' '*depth, '],\n')) + + else: + string.append('[],\n') + + else: + string.append('%s,\n' % (repr(obj),)) + + if depth: + return string + else: + return ''.join(string)[:-2] + +def save(*args): + new_file = not os.access(CONF_FILE,os.F_OK) + fd = file(CONF_FILE, "wb") + try: + if new_file: + os.chmod(CONF_FILE,0600) + fd.write(pprint(conf)) + finally: + fd.close() + +#events.register('Exit', 'post', save) + +try: + conf = eval(file(CONF_FILE, "U").read().strip()) +except IOError, e: + if e.args[0] == 2: + conf = {} + else: + raise + +if __name__ == '__main__': + print pprint(conf) diff --git a/services/console/lib/purk/events.py b/services/console/lib/purk/events.py new file mode 100644 index 00000000..2a0d45e5 --- /dev/null +++ b/services/console/lib/purk/events.py @@ -0,0 +1,298 @@ +import sys +import os +import traceback + +pyending = os.extsep + 'py' + +class error(Exception): + pass + +class EventStopError(error): + pass + +class CommandError(error): + pass + +class data(object): + done = False + quiet = False + + def __init__(self, **kwargs): + for attr in kwargs.items(): + setattr(self, *attr) + +trigger_sequence = ("pre", "setup", "on", "setdown", "post") + +all_events = {} +loaded = {} + +# An event has occurred, the e_name event! +def trigger(e_name, e_data=None, **kwargs): + if e_data is None: + e_data = data(**kwargs) + + #print 'Event:', e_name, e_data + + failure = True + error = None + + if e_name in all_events: + for e_stage in trigger_sequence: + if e_stage in all_events[e_name]: + for f_ref, s_name in all_events[e_name][e_stage]: + try: + f_ref(e_data) + except EventStopError: + return + except CommandError, e: + error = e.args + continue + except: + traceback.print_exc() + failure = False + if failure: + print "Error handling: " + e_name + + return error + +# Stop all processing of the current event now! +def halt(): + raise EventStopError + +# Registers a specific function with an event at the given sequence stage. +def register(e_name, e_stage, f_ref, s_name=""): + global all_events + + if e_name not in all_events: + all_events[e_name] = {} + + if e_stage not in all_events[e_name]: + all_events[e_name][e_stage] = [] + + all_events[e_name][e_stage] += [(f_ref, s_name)] + +# turn a filename (or module name) and trim it to the name of the module +def get_scriptname(name): + s_name = os.path.basename(name) + if s_name.endswith(pyending): + s_name = s_name[:-len(pyending)] + return s_name + +#take a given script name and turn it into a filename +def get_filename(name): + # split the directory and filename + dirname = os.path.dirname(name) + s_name = get_scriptname(name) + + for path in dirname and (dirname,) or sys.path: + filename = os.path.join(path, s_name + pyending) + if os.access(filename, os.R_OK): + return filename + + raise ImportError("No urk script %s found" % name) + +# register the events defined by obj +def register_all(name, obj): + # we look through everything defined in the file + for f in dir(obj): + # for each bit of the event sequence + for e_stage in trigger_sequence: + + # if the function is for this bit + if f.startswith(e_stage): + # get a reference to a function + f_ref = getattr(obj, f) + #print f + # normalise to the event name + e_name = f.replace(e_stage, "", 1) + + # add our function to the right event section + register(e_name, e_stage, f_ref, name) + + break + +#load a .py file into a new module object without affecting sys.modules +def load_pyfile(filename): + s_name = get_scriptname(filename) + return __import__(s_name) + +# Load a python script and register +# the functions defined in it for events. +# Return True if we loaded the script, False if it was already loaded +def load(name): + s_name = get_scriptname(name) + filename = get_filename(name) + + if s_name in loaded: + return False + + loaded[s_name] = None + + try: + module = load_pyfile(filename) + loaded[s_name] = module + except: + del loaded[s_name] + raise + + register_all(s_name, loaded[s_name]) + + return module + +# Is the script with the given name loaded? +def is_loaded(name): + return get_scriptname(name) in loaded + +# Remove any function which was defined in the given script +def unload(name): + s_name = get_scriptname(name) + + del loaded[s_name] + + for e_name in list(all_events): + for e_stage in list(all_events[e_name]): + to_check = all_events[e_name][e_stage] + + all_events[e_name][e_stage] = [(f, m) for f, m in to_check if m != s_name] + + if not all_events[e_name][e_stage]: + del all_events[e_name][e_stage] + + if not all_events[e_name]: + del all_events[e_name] + +def reload(name): + s_name = get_scriptname(name) + + if s_name not in loaded: + return False + + temp = loaded[s_name] + + unload(s_name) + + try: + load(name) + return True + except: + loaded[s_name] = temp + register_all(s_name, temp) + raise + +def run(text, window, network): + split = text.split(' ') + + c_data = data(name=split.pop(0), text=text, window=window, network=network) + + if split and split[0].startswith('-'): + c_data.switches = set(split.pop(0)[1:]) + else: + c_data.switches = set() + + c_data.args = split + + event_name = "Command" + c_data.name.capitalize() + #print "searching: " + event_name + #for s in all_events: + # print "match: " + s + # if s == event_name: + # print "we got it!" + + if event_name in all_events: + result = trigger(event_name, c_data) + + if result: + print "* /%s: %s" % (c_data.name, result[0]) + c_data.window.write("* /%s: %s" % (c_data.name, result[0])) + else: + trigger("Command", c_data) + + if not c_data.done: + c_data.window.write("* /%s: No such command exists" % (c_data.name)) + +def onCommandPyeval(e): + loc = sys.modules.copy() + loc.update(e.__dict__) + import pydoc #fix nonresponsive help() command + old_pager, pydoc.pager = pydoc.pager, pydoc.plainpager + try: + result = repr(eval(' '.join(e.args), loc)) + if 's' in e.switches: + run( + 'say - %s => %s' % (' '.join(e.args),result), + e.window, + e.network + ) + else: + e.window.write(result) + except: + for line in traceback.format_exc().split('\n'): + e.window.write(line) + pydoc.pager = old_pager + +def onCommandPyexec(e): + loc = sys.modules.copy() + loc.update(e.__dict__) + import pydoc #fix nonresponsive help() command + old_pager, pydoc.pager = pydoc.pager, pydoc.plainpager + try: + exec ' '.join(e.args) in loc + except: + for line in traceback.format_exc().split('\n'): + e.window.write(line) + pydoc.pager = old_pager + +def onCommandLoad(e): + if e.args: + name = e.args[0] + else: + e.window.write('Usage: /load scriptname') + + try: + if load(name): + e.window.write("* The script '%s' has been loaded." % name) + else: + raise CommandError("The script is already loaded; use /reload instead") + except: + e.window.write(traceback.format_exc(), line_ending='') + raise CommandError("Error loading the script") + +def onCommandUnload(e): + if e.args: + name = e.args[0] + else: + e.window.write('Usage: /unload scriptname') + + if is_loaded(name): + unload(name) + e.window.write("* The script '%s' has been unloaded." % name) + else: + raise CommandError("No such script is loaded") + +def onCommandReload(e): + if e.args: + name = e.args[0] + else: + e.window.write('Usage: /reload scriptname') + + try: + if reload(name): + e.window.write("* The script '%s' has been reloaded." % name) + else: + raise CommandError("The script isn't loaded yet; use /load instead") + except: + e.window.write(traceback.format_exc(), line_ending='') + +def onCommandScripts(e): + e.window.write("Loaded scripts:") + for name in loaded: + e.window.write("* %s" % name) + +def onCommandEcho(e): + e.window.write(' '.join(e.args)) + +name = '' +for name in globals(): + if name.startswith('onCommand'): + register(name[2:], "on", globals()[name], '_all_events') +del name diff --git a/services/console/lib/purk/info.py b/services/console/lib/purk/info.py new file mode 100644 index 00000000..3765845d --- /dev/null +++ b/services/console/lib/purk/info.py @@ -0,0 +1,8 @@ +name = "PUrk" +long_name = "Purk IRC" +version = 0, 1, "git" +long_version = "%s v%s" % (long_name, ".".join(str(x) for x in version)) +website = "http://dev.laptop.org/" +authors = ["Vincent Povirk", "Marc Liddell"] +copyright = "2005 %s" % ', '.join(authors) + diff --git a/services/console/lib/purk/irc.py b/services/console/lib/purk/irc.py new file mode 100644 index 00000000..d5a01aad --- /dev/null +++ b/services/console/lib/purk/irc.py @@ -0,0 +1,328 @@ +import socket +import sys + +from conf import conf +import ui +import windows +import info + +DISCONNECTED = 0 +CONNECTING = 1 +INITIALIZING = 2 +CONNECTED = 3 + +def parse_irc(msg, server): + msg = msg.split(' ') + + # if our very first character is : + # then this is the source, + # otherwise insert the server as the source + if msg and msg[0].startswith(':'): + msg[0] = msg[0][1:] + else: + msg.insert(0, server) + + # loop through the msg until we find + # something beginning with : + for i, token in enumerate(msg): + if token.startswith(':'): + # remove the : + msg[i] = msg[i][1:] + + # join up the rest + msg[i:] = [' '.join(msg[i:])] + break + + # filter out the empty pre-":" tokens and add on the text to the end + return [m for m in msg[:-1] if m] + msg[-1:] + + # note: this sucks and makes very little sense, but it matches the BNF + # as far as we've tested, which seems to be the goal + +def default_nicks(): + try: + nicks = [conf.get('nick')] + conf.get('altnicks',[]) + if not nicks[0]: + import getpass + nicks = [getpass.getuser()] + except: + nicks = ["mrurk"] + return nicks + +class Network(object): + socket = None + + def __init__(self, core, server="irc.default.org", port=6667, nicks=[], + username="", fullname="", name=None, **kwargs): + self.manager = core.manager + self.server = server + self.port = port + self.events = core.events + + self.name = name or server + + self.nicks = nicks or default_nicks() + self.me = self.nicks[0] + + self.username = username or "urk" + self.fullname = fullname or conf.get("fullname", self.username) + self.password = '' + + self.isupport = { + 'NETWORK': server, + 'PREFIX': '(ohv)@%+', + 'CHANMODES': 'b,k,l,imnpstr', + } + self.prefixes = {'o':'@', 'h':'%', 'v':'+', '@':'o', '%':'h', '+':'v'} + + self.status = DISCONNECTED + self.failedhosts = [] #hosts we've tried and failed to connect to + self.channel_prefixes = '&#+$' # from rfc2812 + + self.on_channels = set() + self.requested_joins = set() + self.requested_parts = set() + + self.buffer = '' + + #called when we get a result from the dns lookup + def on_dns(self, result, error): + if error: + self.disconnect(error=error[1]) + else: + #import os + #import random + #random.seed() + #random.shuffle(result) + if socket.has_ipv6: #prefer ipv6 + result = [(f, t, p, c, a) for (f, t, p, c, a) in result if f == socket.AF_INET6]+result + elif hasattr(socket,"AF_INET6"): #ignore ipv6 + result = [(f, t, p, c, a) for (f, t, p, c, a) in result if f != socket.AF_INET6] + + self.failedlasthost = False + + for f, t, p, c, a in result: + if (f, t, p, c, a) not in self.failedhosts: + try: + self.socket = socket.socket(f, t, p) + except: + continue + self.source = ui.fork(self.on_connect, self.socket.connect, a) + self.failedhosts.append((f, t, p, c, a)) + if set(self.failedhosts) >= set(result): + self.failedlasthost = True + break + else: + self.failedlasthost = True + if len(result): + self.failedhosts[:] = (f, t, p, c, a), + f, t, p, c, a = result[0] + try: + self.socket = socket.socket(f, t, p) + self.source = ui.fork(self.on_connect, self.socket.connect, a) + except: + self.disconnect(error="Couldn't find a host we can connect to") + else: + self.disconnect(error="Couldn't find a host we can connect to") + + #called when socket.open() returns + def on_connect(self, result, error): + if error: + self.disconnect(error=error[1]) + #we should immediately retry if we failed to open the socket and there are hosts left + if self.status == DISCONNECTED and not self.failedlasthost: + windows.get_default(self).write("* Retrying with next available host") + self.connect() + else: + self.source = source = ui.Source() + self.status = INITIALIZING + self.failedhosts[:] = () + + self.events.trigger('SocketConnect', network=self) + + if source.enabled: + self.source = ui.fork(self.on_read, self.socket.recv, 8192) + + #called when we read data or failed to read data + def on_read(self, result, error): + if error: + self.disconnect(error=error[1]) + elif not result: + self.disconnect(error="Connection closed by remote host") + else: + self.source = source = ui.Source() + + self.buffer = (self.buffer + result).split("\r\n") + + for line in self.buffer[:-1]: + self.got_msg(line) + + if self.buffer: + self.buffer = self.buffer[-1] + else: + self.buffer = '' + + if source.enabled: + self.source = ui.fork(self.on_read, self.socket.recv, 8192) + + def raw(self, msg): + self.events.trigger("OwnRaw", network=self, raw=msg) + + if self.status >= INITIALIZING: + self.socket.send(msg + "\r\n") + + def got_msg(self, msg): + pmsg = parse_irc(msg, self.server) + + e_data = self.events.data( + raw=msg, + msg=pmsg, + text=pmsg[-1], + network=self, + window=windows.get_default(self, self.manager) + ) + + if "!" in pmsg[0]: + e_data.source, e_data.address = pmsg[0].split('!',1) + + else: + e_data.source, e_data.address = pmsg[0], '' + + if len(pmsg) > 2: + e_data.target = pmsg[2] + else: + e_data.target = pmsg[-1] + + self.events.trigger('Raw', e_data) + + def connect(self): + if not self.status: + self.status = CONNECTING + + self.source = ui.fork(self.on_dns, socket.getaddrinfo, self.server, self.port, 0, socket.SOCK_STREAM) + + self.events.trigger('Connecting', network=self) + + def disconnect(self, error=None): + if self.socket: + self.socket.close() + + if self.source: + self.source.unregister() + self.source = None + + self.socket = None + + self.status = DISCONNECTED + + #note: connecting from onDisconnect is probably a Bad Thing + self.events.trigger('Disconnect', network=self, error=error) + + #trigger a nick change if the nick we want is different from the one we + # had. + if self.me != self.nicks[0]: + self.events.trigger( + 'Nick', network=self, window=windows.get_default(self), + source=self.me, target=self.nicks[0], address='', + text=self.nicks[0] + ) + self.me = self.nicks[0] + + def norm_case(self, string): + return string.lower() + + def quit(self, msg=None): + if self.status: + try: + if msg == None: + msg = conf.get('quitmsg', "%s - %s" % (info.long_version, info.website)) + self.raw("QUIT :%s" % msg) + except: + pass + self.disconnect() + + def join(self, target, key='', requested=True): + if key: + key = ' '+key + self.raw("JOIN %s%s" % (target,key)) + if requested: + for chan in target.split(' ',1)[0].split(','): + if chan == '0': + self.requested_parts.update(self.on_channels) + else: + self.requested_joins.add(self.norm_case(chan)) + + def part(self, target, msg="", requested=True): + if msg: + msg = " :" + msg + + self.raw("PART %s%s" % (target, msg)) + if requested: + for chan in target.split(' ',1)[0].split(','): + self.requested_parts.add(self.norm_case(target)) + + def msg(self, target, msg): + self.raw("PRIVMSG %s :%s" % (target, msg)) + + self.events.trigger( + 'OwnText', source=self.me, target=str(target), text=msg, + network=self, window=windows.get_default(self, self.manager) + ) + + def notice(self, target, msg): + self.raw("NOTICE %s :%s" % (target, msg)) + + self.events.trigger( + 'OwnNotice', source=self.me, target=str(target), text=msg, + network=self, window=windows.get_default(self) + ) + +#a Network that is never connected +class DummyNetwork(Network): + def __nonzero__(self): + return False + + def __init__(self, core): + Network.__init__(self, core) + + self.name = self.server = self.isupport['NETWORK'] = "None" + + def connect(self): + raise NotImplementedError, "Cannot connect dummy network." + + def raw(self, msg): + raise NotImplementedError, "Cannot send %s over the dummy network." % repr(msg) + +#dummy_network = DummyNetwork() + +#this was ported from srvx's tools.c +def match_glob(text, glob, t=0, g=0): + while g < len(glob): + if glob[g] in '?*': + star_p = q_cnt = 0 + while g < len(glob): + if glob[g] == '*': + star_p = True + elif glob[g] == '?': + q_cnt += 1 + else: + break + g += 1 + t += q_cnt + if t > len(text): + return False + if star_p: + if g == len(glob): + return True + for i in xrange(t, len(text)): + if text[i] == glob[g] and match_glob(text, glob, i+1, g+1): + return True + return False + else: + if t == len(text) and g == len(glob): + return True + if t == len(text) or g == len(glob) or text[t] != glob[g]: + return False + t += 1 + g += 1 + return t == len(text) diff --git a/services/console/lib/purk/parse_mirc.py b/services/console/lib/purk/parse_mirc.py new file mode 100644 index 00000000..186ff9b2 --- /dev/null +++ b/services/console/lib/purk/parse_mirc.py @@ -0,0 +1,457 @@ +try: + from conf import conf +except ImportError: + conf = {} + +BOLD = '\x02' +UNDERLINE = '\x1F' +MIRC_COLOR = '\x03' +MIRC_COLOR_BG = MIRC_COLOR, MIRC_COLOR +BERS_COLOR = '\x04' +RESET = '\x0F' + +colors = ( + '#FFFFFF', '#000000', '#00007F', '#009300', + '#FF0000', '#7F0000', '#9C009C', '#FF7F00', + '#FFFF00', '#00FF00', '#009393', '#00FFFF', + '#0000FF', '#FF00FF', '#7F7F7F', '#D2D2D2' + ) + +def get_mirc_color(number): + if number == '99': + return None + + number = int(number) & 15 + + confcolors = conf.get('colors', colors) + try: + return confcolors[number] + except: + # someone edited their colors wrongly + return colors[number] + +DEC_DIGITS, HEX_DIGITS = set('0123456789'), set('0123456789abcdefABCDEF') + +def parse_mirc_color(string, pos, open_tags, tags): + color_chars = 1 + + if MIRC_COLOR in open_tags: + fgtag = open_tags.pop(MIRC_COLOR) + fgtag['to'] = pos + tags.append(fgtag) + + if MIRC_COLOR_BG in open_tags: + bgtag = open_tags.pop(MIRC_COLOR_BG) + bgtag['to'] = pos + tags.append(bgtag) + + bg = bgtag['data'][1] + else: + bg = None + + if string[0] in DEC_DIGITS: + if string[1] in DEC_DIGITS: + fg = get_mirc_color(string[:2]) + string = string[1:] + color_chars += 2 + + else: + fg = get_mirc_color(string[0]) + color_chars += 1 + + if string[1] == "," and string[2] in DEC_DIGITS: + if string[3] in DEC_DIGITS: + bg = get_mirc_color(string[2:4]) + color_chars += 3 + + else: + bg = get_mirc_color(string[2]) + color_chars += 2 + + else: + fg = bg = None + + if fg: + open_tags[MIRC_COLOR] = {'data': ("foreground",fg), 'from': pos} + else: + open_tags.pop(MIRC_COLOR,None) + + if bg: + open_tags[MIRC_COLOR_BG] = {'data': ("background",bg), 'from': pos} + else: + open_tags.pop(MIRC_COLOR_BG,None) + + return color_chars + +def parse_bersirc_color(string, pos, open_tags, tags): + bg = None + if MIRC_COLOR in open_tags: + tag = open_tags.pop(MIRC_COLOR) + tag['to'] = pos + tags.append(tag) + + if MIRC_COLOR_BG in open_tags: + bgtag = open_tags.pop(MIRC_COLOR_BG) + bgtag['to'] = pos + tags.append(bgtag) + + bg = bgtag['data'][1] + + for c in (0, 1, 2, 3, 4, 5): + if string[c] not in HEX_DIGITS: + return 1 + fg = '#' + string[:6].upper() + + color_chars = 7 + for c in (7, 8, 9, 10, 11, 12): + if string[c] not in HEX_DIGITS: + break + else: + if string[6] == ",": + bg = '#' + string[7:13].upper() + color_chars = 14 + + if fg: + open_tags[MIRC_COLOR] = {'data': ("foreground",fg), 'from': pos} + else: + open_tags.pop(MIRC_COLOR,None) + + if bg: + open_tags[MIRC_COLOR_BG] = {'data': ("background",bg), 'from': pos} + else: + open_tags.pop(MIRC_COLOR_BG,None) + + return color_chars + +def parse_bold(string, pos, open_tags, tags): + if BOLD in open_tags: + tag = open_tags.pop(BOLD) + tag['to'] = pos + tags.append(tag) + + else: + open_tags[BOLD] = {'data': ('weight', BOLD), 'from': pos} + + return 1 + +def parse_underline(string, pos, open_tags, tags): + if UNDERLINE in open_tags: + tag = open_tags.pop(UNDERLINE) + tag['to'] = pos + tags.append(tag) + + else: + open_tags[UNDERLINE] = {'data': ('underline', UNDERLINE), 'from': pos} + + return 1 + +def parse_reset(string, pos, open_tags, tags): + for t in open_tags: + tag = open_tags[t] + tag['to'] = pos + tags.append(tag) + + open_tags.clear() + + return 1 + +tag_parser = { + MIRC_COLOR: parse_mirc_color, + BERS_COLOR: parse_bersirc_color, + BOLD: parse_bold, + UNDERLINE: parse_underline, + RESET: parse_reset + } + +def parse_mirc(string): + string += RESET + + out = '' + open_tags = {} + tags = [] + text_i = outtext_i = 0 + + for tag_i, char in enumerate(string): + if char in tag_parser: + out += string[text_i:tag_i] + + outtext_i += tag_i - text_i + + text_i = tag_i + tag_parser[char]( + string[tag_i+1:], + outtext_i, + open_tags, + tags + ) + + return tags, out + +#transforms for unparse_mirc + +#^O +def transform_reset(start, end): + return RESET, '', {} + +#^K +def transform_color_reset(start, end): + if ('foreground' in start and 'foreground' not in end) or \ + ('background' in start and 'background' not in end): + result = start.copy() + result.pop("foreground",None) + result.pop("background",None) + return MIRC_COLOR, DEC_DIGITS, result + else: + return '','',start + +#^KXX +def transform_color(start, end): + if (start.get('foreground',99) != end.get('foreground',99)): + confcolors = conf.get('colors', colors) + result = start.copy() + if 'foreground' in end: + try: + index = list(confcolors).index(end['foreground'].upper()) + except ValueError: + return '','',start + result['foreground'] = end['foreground'] + else: + index = 99 + del result['foreground'] + return '\x03%02i' % index, ',', result + else: + return '','',start + +#^KXX,YY +def transform_bcolor(start, end): + if (start.get('background',99) != end.get('background',99)): + confcolors = conf.get('colors', colors) + result = start.copy() + if 'foreground' in end: + try: + fg_index = list(confcolors).index(end['foreground'].upper()) + except ValueError: + return '','',start + result['foreground'] = end['foreground'] + else: + fg_index = 99 + result.pop('foreground',None) + if 'background' in end: + try: + bg_index = list(confcolors).index(end['background'].upper()) + except ValueError: + return '','',start + result['background'] = end['background'] + else: + bg_index = 99 + del result['background'] + return '\x03%02i,%02i' % (fg_index, bg_index), ',', result + else: + return '','',start + +#^LXXXXXX +def transform_bersirc(start, end): + if 'foreground' in end and end['foreground'] != start.get('foreground'): + result = start.copy() + result['foreground'] = end['foreground'] + return "\x04%s" % end['foreground'][1:], ',', result + else: + return '','',start + +#^LXXXXXX,YYYYYY +def transform_bbersirc(start, end): + if 'foreground' in end and 'background' in end and ( + end['foreground'] != start.get('foreground') or + end['background'] != start.get('background')): + result = start.copy() + result['foreground'] = end['foreground'] + result['background'] = end['background'] + return "\x04%s,%s" % (end['foreground'][1:], end['background'][1:]), ',', result + else: + return '','',start + + +#^B +def transform_underline(start, end): + if ('underline' in start) != ('underline' in end): + result = start.copy() + if 'underline' in start: + del result['underline'] + else: + result['underline'] = UNDERLINE + return UNDERLINE, '', result + else: + return '','',start + +#^U +def transform_bold(start, end): + if ('weight' in start) != ('weight' in end): + result = start.copy() + if 'weight' in start: + del result['weight'] + else: + result['weight'] = BOLD + return BOLD, '', result + else: + return '','',start + +#^B^B +#In some rare circumstances, we HAVE to do this to generate a working string +def transform_dbold(start, end): + return BOLD*2, '', start + +#return the formatting needed to transform one set of format tags to another +def transform(start, end, nextchar=" "): + transform_functions = ( + transform_reset, transform_color_reset, transform_color, transform_bcolor, + transform_bersirc, transform_bbersirc, transform_underline, + transform_bold, transform_dbold, + ) + + candidates = [('','',start)] + result = None + + for f in transform_functions: + for string, badchars, s in candidates[:]: + newstring, badchars, s = f(s, end) + string += newstring + if newstring and (result == None or len(string) < len(result)): + if nextchar not in badchars and s == end: + result = string + else: + candidates.append((string, badchars, s)) + return result + +def unparse_mirc(tagsandtext): + lasttags, lastchar = {}, '' + + string = [] + for tags, char in tagsandtext: + if tags != lasttags: + string.append(transform(lasttags, tags, char[0])) + string.append(char) + lasttags, lastchar = tags, char + return ''.join(string) + +if __name__ == "__main__": + tests = [ + 'not\x02bold\x02not', + 'not\x1Funderline\x1Fnot', + + "\x02\x1FHi\x0F", + + 'not\x030,17white-on-black\x0304red-on-black\x03nothing', + + "\x040000CC<\x04nick\x040000CC>\x04 text", + + '\x04770077,FFFFFFbersirc color with background! \x04000077setting foreground! \x04reset!', + + '\x047700,FFFFbersirc', + + "\x03123Hello", + + "\x0312,Hello", + + "\x034Hello", + + "Bo\x02ld", + + "\x034,5Hello\x036Goodbye", + + "\x04ff0000,00ff00Hello\x040000ffGoodbye", + + "\x04777777(\x0400CCCCstuff\x04777777)\x04", + + '\x0307orange\x04CCCCCCgrey\x0307orange', + + '\x04CCCCCC,444444sdf\x0304jkl', + + '\x0403\x02\x02,trixy', + + '\x04FFFFFF\x02\x02,000000trixy for bersirc', + ] + + results = [ + ([{'from': 3, 'data': ('weight', '\x02'), 'to': 7}], 'notboldnot'), + + ([{'from': 3, 'data': ('underline', '\x1f'), 'to': 12}], 'notunderlinenot'), + + ([{'from': 0, 'data': ('weight', '\x02'), 'to': 2}, {'from': 0, 'data': ('underline', '\x1f'), 'to': 2}], 'Hi'), + + ([{'from': 3, 'data': ('foreground', '#FFFFFF'), 'to': 17}, {'from': 3, 'data': ('background', '#000000'), 'to': 17}, {'from': 17, 'data': ('foreground', '#FF0000'), 'to': 29}, {'from': 17, 'data': ('background', '#000000'), 'to': 29}], 'notwhite-on-blackred-on-blacknothing'), + + ([{'from': 0, 'data': ('foreground', '#0000CC'), 'to': 1}, {'from': 5, 'data': ('foreground', '#0000CC'), 'to': 6}], ' text'), + + ([{'from': 0, 'data': ('foreground', '#770077'), 'to': 31}, {'from': 0, 'data': ('background', '#FFFFFF'), 'to': 31}, {'from': 31, 'data': ('foreground', '#000077'), 'to': 51}, {'from': 31, 'data': ('background', '#FFFFFF'), 'to': 51}], 'bersirc color with background! setting foreground! reset!'), + + ([], '7700,FFFFbersirc'), + + ([{'from': 0, 'data': ('foreground', '#0000FF'), 'to': 6}], '3Hello'), + + ([{'from': 0, 'data': ('foreground', '#0000FF'), 'to': 6}], ',Hello'), + + ([{'from': 0, 'data': ('foreground', '#FF0000'), 'to': 5}], 'Hello'), + + ([{'from': 2, 'data': ('weight', '\x02'), 'to': 4}], 'Bold'), + + ([{'from': 0, 'data': ('foreground', '#FF0000'), 'to': 5}, {'from': 0, 'data': ('background', '#7F0000'), 'to': 5}, {'from': 5, 'data': ('foreground', '#9C009C'), 'to': 12}, {'from': 5, 'data': ('background', '#7F0000'), 'to': 12}], 'HelloGoodbye'), + + ([{'from': 0, 'data': ('foreground', '#FF0000'), 'to': 5}, {'from': 0, 'data': ('background', '#00FF00'), 'to': 5}, {'from': 5, 'data': ('foreground', '#0000FF'), 'to': 12}, {'from': 5, 'data': ('background', '#00FF00'), 'to': 12}], 'HelloGoodbye'), + + ([{'from': 0, 'data': ('foreground', '#777777'), 'to': 1}, {'from': 1, 'data': ('foreground', '#00CCCC'), 'to': 6}, {'from': 6, 'data': ('foreground', '#777777'), 'to': 7}], '(stuff)'), + + ([{'from': 0, 'data': ('foreground', '#FF7F00'), 'to': 6}, {'from': 6, 'data': ('foreground', '#CCCCCC'), 'to': 10}, {'from': 10, 'data': ('foreground', '#FF7F00'), 'to': 16}], 'orangegreyorange'), + + ([{'from': 0, 'data': ('foreground', '#CCCCCC'), 'to': 3}, {'from': 0, 'data': ('background', '#444444'), 'to': 3}, {'from': 3, 'data': ('foreground', '#FF0000'), 'to': 6}, {'from': 3, 'data': ('background', '#444444'), 'to': 6}], 'sdfjkl'), + + ([{'from': 2, 'data': ('weight', '\x02'), 'to': 2}], '03,trixy'), + + ([{'from': 0, 'data': ('weight', '\x02'), 'to': 0}, {'from': 0, 'data': ('foreground', '#FFFFFF'), 'to': 24}], ',000000trixy for bersirc'), + ] + + #""" + + #r = range(20000) + #for i in r: + # for test in tests: + # parse_mirc(test) + + """ + + lines = [eval(line.strip()) for line in file("parse_mirc_torture_test.txt")] + + for r in range(100): + for line in lines: + parse_mirc(line) + + #""" + + def setify_tags(tags): + return set(frozenset(tag.iteritems()) for tag in tags if tag['from'] != tag['to']) + + def parsed_eq((tags1, text1), (tags2, text2)): + return setify_tags(tags1) == setify_tags(tags2) and text1 == text2 + + def parsed_to_unparsed((tags, text)): + result = [] + for i, char in enumerate(text): + result.append(( + dict(tag['data'] for tag in tags if tag['from'] <= i < tag['to']), + char)) + return result + + for i, (test, result) in enumerate(zip(tests, results)): + if not parsed_eq(parse_mirc(test), result): + print "parse_mirc failed test %s:" % i + print repr(test) + print parse_mirc(test) + print result + print + + elif not parsed_eq(parse_mirc(unparse_mirc(parsed_to_unparsed(result))), result): + print "unparse_mirc failed test %s:" % i + print repr(test) + print unparse_mirc(test) + print + +#import dis +#dis.dis(parse_mirc) diff --git a/services/console/lib/purk/scripts/Makefile.am b/services/console/lib/purk/scripts/Makefile.am new file mode 100644 index 00000000..00ffbf8b --- /dev/null +++ b/services/console/lib/purk/scripts/Makefile.am @@ -0,0 +1,16 @@ +sugardir = $(pkgdatadir)/services/console/lib/purk/scripts + +sugar_PYTHON = \ + alias.py \ + chaninfo.py \ + clicks.py \ + completion.py \ + console.py \ + history.py \ + ignore.py \ + irc_script.py \ + keys.py \ + theme.py \ + timeout.py \ + ui_script.py + diff --git a/services/console/lib/purk/scripts/alias.py b/services/console/lib/purk/scripts/alias.py new file mode 100755 index 00000000..ffd213d8 --- /dev/null +++ b/services/console/lib/purk/scripts/alias.py @@ -0,0 +1,60 @@ +import sys +import os + +from conf import conf + +aliases = conf.get("aliases",{ + 'op':'"mode "+window.id+" +"+"o"*len(args)+" "+" ".join(args)', + 'deop':'"mode "+window.id+" -"+"o"*len(args)+" "+" ".join(args)', + 'voice':'"mode "+window.id+" +"+"v"*len(args)+" "+" ".join(args)', + 'devoice':'"mode "+window.id+" -"+"v"*len(args)+" "+" ".join(args)', + 'umode':'"mode "+network.me+" "+" ".join(args)', + 'clear':'window.output.clear()', + }) + +class CommandHandler: + __slots__ = ["command"] + def __init__(self, command): + self.command = command + def __call__(self, e): + loc = sys.modules.copy() + loc.update(e.__dict__) + result = eval(self.command,loc) + if isinstance(result,basestring): + core.events.run(result,e.window,e.network) + +for name in aliases: + globals()['onCommand'+name.capitalize()] = CommandHandler(aliases[name]) + +def onCommandAlias(e): + if e.args and 'r' in e.switches: + name = e.args[0].lower() + command = aliases[name] + del aliases[name] + conf['aliases'] = aliases + e.window.write("* Deleted alias %s%s (was %s)" % (conf.get('command-prefix','/'),name,command)) + core.events.load(__name__,reloading=True) + elif 'l' in e.switches: + e.window.write("* Current aliases:") + for i in aliases: + e.window.write("* %s%s: %s" % (conf.get('command-prefix','/'),i,aliases[i])) + elif len(e.args) >= 2: + name = e.args[0].lower() + command = ' '.join(e.args[1:]) + aliases[name] = command + conf['aliases'] = aliases + e.window.write("* Created an alias %s%s to %s" % (conf.get('command-prefix','/'),name,command)) + core.events.reload(__name__) + elif len(e.args) == 1: + name = e.args[0].lower() + if name in aliases: + e.window.write("* %s%s is an alias to %s" % (conf.get('command-prefix','/'),name,aliases[name])) + else: + e.window.write("* There is no alias %s%s" % (conf.get('command-prefix','/'),name)) + else: + e.window.write( +"""Usage: + /alias \x02name\x02 \x02expression\x02 to create or replace an alias + /alias \x02name\x02 to look at an alias + /alias -r \x02name\x02 to remove an alias + /alias -l to see a list of aliases""") diff --git a/services/console/lib/purk/scripts/chaninfo.py b/services/console/lib/purk/scripts/chaninfo.py new file mode 100644 index 00000000..e6ff3a0e --- /dev/null +++ b/services/console/lib/purk/scripts/chaninfo.py @@ -0,0 +1,320 @@ +import windows + +def _justprefix(network, channel, nick): + fr, to = network.isupport["PREFIX"][1:].split(")") + + for mode, prefix in zip(fr, to): + if mode in channel.nicks.get(nick, ''): + return prefix + + return '' + +def prefix(network, channelname, nick): + channel = getchan(network, channelname) + + if channel: + nick = '%s%s' % (_justprefix(network, channel, nick), nick) + + return nick + +def escape(string): + for escapes in (('&','&'), ('<','<'), ('>','>')): + string = string.replace(*escapes) + return string + +def sortkey(network, channelname, nick): + chanmodes, dummy = network.isupport["PREFIX"][1:].split(")") + nickmodes = mode(network, channelname, nick) + + return '%s%s' % (''.join(str(int(mode not in nickmodes)) for mode in chanmodes), network.norm_case(nick)) + +def nicklist_add(network, channel, nick): + window = windows.get(windows.ChannelWindow, network, channel.name, core) + #window = core.window + if window: + window.nicklist.append(nick, escape(prefix(network, channel.name, nick)), sortkey(network, channel.name, nick)) + +def nicklist_del(network, channel, nick): + window = windows.get(windows.ChannelWindow, network, channel.name, core) + #window = core.window + if window: + try: + window.nicklist.remove(nick) + except ValueError: + pass + +def setupListRightClick(e): + if isinstance(e.window, windows.ChannelWindow): + #if isinstance(core.window, windows.ChannelWindow): + #if e.data[0] in e.window.network.isupport["PREFIX"].split(")")[1]: + if e.data[0] in core.window.network.isupport["PREFIX"].split(")")[1]: + e.nick = e.data[1:] + else: + e.nick = e.data + +def setupSocketConnect(e): + e.network.channels = {} + +def setdownDisconnect(e): + e.network.channels = {} + +class Channel(object): + def __init__(self, name): + self.name = name + self.nicks = {} + self.normal_nicks = {} # mapping of normal nicks to actual nicks + self.getting_names = False #are we between lines in a /names reply? + self.mode = '' + self.special_mode = {} #for limits, keys, and anything similar + self.topic = '' + self.got_mode = False #did we get at least one mode reply? + self.got_names = False #did we get at least one names reply? + +def getchan(network, channel): + return hasattr(network, 'channels') and network.channels.get(network.norm_case(channel)) + +#return a list of channels you're on on the given network +def channels(network): + if not hasattr(network, 'channels'): + network.channels = {} + + return list(network.channels) + +#return True if you're on the channel +def ischan(network, channel): + return bool(getchan(network, channel)) + +#return True if the nick is on the channel +def ison(network, channel, nickname): + channel = getchan(network, channel) + return channel and network.norm_case(nickname) in channel.normal_nicks + +#return a list of nicks on the given channel +def nicks(network, channel): + channel = getchan(network, channel) + + if channel: + return channel.nicks + else: + return {} + +#return the mode on the given channel +def mode(network, channel, nickname=''): + channel = getchan(network, channel) + + if channel: + if nickname: + realnick = channel.normal_nicks.get(network.norm_case(nickname)) + if realnick: + return channel.nicks[realnick] + + else: + result = channel.mode + for m in channel.mode: + if m in channel.special_mode: + result += ' '+channel.special_mode[m] + return result + + return '' + +#return the topic on the given channel +def topic(network, channel): + channel = getchan(network, channel) + + if channel: + return channel.topic + else: + return '' + +def setupJoin(e): + print e + if e.source == e.network.me: + e.network.channels[e.network.norm_case(e.target)] = Channel(e.target) + e.network.raw('MODE '+e.target) + + #if we wanted to be paranoid, we'd account for not being on the channel + channel = getchan(e.network,e.target) + channel.nicks[e.source] = '' + channel.normal_nicks[e.network.norm_case(e.source)] = e.source + + if e.source == e.network.me: + #If the channel window already existed, and we're joining, then we + #didn't clear out the nicklist when we left. That means we have to clear + #it out now. + window = windows.get(windows.ChannelWindow, e.network, e.target, core) + #window = core.window + #print core + if window: + window.nicklist.clear() + + nicklist_add(e.network, channel, e.source) + +def setdownPart(e): + if e.source == e.network.me: + del e.network.channels[e.network.norm_case(e.target)] + else: + channel = getchan(e.network,e.target) + nicklist_del(e.network, channel, e.source) + del channel.nicks[e.source] + del channel.normal_nicks[e.network.norm_case(e.source)] + +def setdownKick(e): + if e.target == e.network.me: + del e.network.channels[e.network.norm_case(e.channel)] + else: + channel = getchan(e.network,e.channel) + nicklist_del(e.network, channel, e.target) + del channel.nicks[e.target] + del channel.normal_nicks[e.network.norm_case(e.target)] + +def setdownQuit(e): + #if paranoid: check if e.source is me + for channame in channels(e.network): + channel = getchan(e.network,channame) + if e.source in channel.nicks: + nicklist_del(e.network, channel, e.source) + del channel.nicks[e.source] + del channel.normal_nicks[e.network.norm_case(e.source)] + +def setupMode(e): + channel = getchan(e.network,e.channel) + if channel: + user_modes = e.network.isupport['PREFIX'].split(')')[0][1:] + + (list_modes, + always_parm_modes, + set_parm_modes, + normal_modes) = e.network.isupport['CHANMODES'].split(',') + + list_modes += user_modes + + mode_on = True #are we reading a + section or a - section? + params = e.text.split(' ') + + for char in params.pop(0): + if char == '+': + mode_on = True + + elif char == '-': + mode_on = False + + else: + if char in user_modes: + #these are modes like op and voice + nickname = params.pop(0) + nicklist_del(e.network, channel, nickname) + if mode_on: + channel.nicks[nickname] += char + else: + channel.nicks[nickname] = channel.nicks[nickname].replace(char, '') + nicklist_add(e.network, channel, nickname) + + elif char in list_modes: + #things like ban/unban + #FIXME: We don't keep track of those lists here, but we know + # when they're changed and how. Scriptors should be able to + # take advantage of this + params.pop(0) + + elif char in always_parm_modes: + #these always have a parameter + param = params.pop(0) + + if mode_on: + channel.special_mode[char] = param + else: + #account for unsetting modes that aren't set + channel.special_mode.pop(char, None) + + elif char in set_parm_modes: + #these have a parameter only if they're being set + if mode_on: + channel.special_mode[char] = params.pop(0) + else: + #account for unsetting modes that aren't set + channel.special_mode.pop(char, None) + + if char not in list_modes: + if mode_on: + channel.mode = channel.mode.replace(char, '')+char + else: + channel.mode = channel.mode.replace(char, '') + +def setdownNick(e): + for channame in channels(e.network): + channel = getchan(e.network,channame) + if e.source in channel.nicks: + nicklist_del(e.network, channel, e.source) + del channel.normal_nicks[e.network.norm_case(e.source)] + channel.nicks[e.target] = channel.nicks[e.source] + del channel.nicks[e.source] + channel.normal_nicks[e.network.norm_case(e.target)] = e.target + nicklist_add(e.network, channel, e.target) + +def setupTopic(e): + channel = getchan(e.network, e.target) + if channel: + channel.topic = e.text + +def setupRaw(e): + if e.msg[1] == '353': #names reply + channel = getchan(e.network,e.msg[4]) + if channel: + if not channel.getting_names: + channel.nicks.clear() + channel.normal_nicks.clear() + channel.getting_names = True + if not channel.got_names: + e.quiet = True + for nickname in e.msg[5].split(' '): + if nickname: + if not nickname[0].isalpha() and nickname[0] in e.network.prefixes: + n = nickname[1:] + channel.nicks[n] = e.network.prefixes[nickname[0]] + channel.normal_nicks[e.network.norm_case(n)] = n + else: + channel.nicks[nickname] = '' + channel.normal_nicks[e.network.norm_case(nickname)] = nickname + + elif e.msg[1] == '366': #end of names reply + channel = getchan(e.network,e.msg[3]) + if channel: + if not channel.got_names: + e.quiet = True + channel.got_names = True + channel.getting_names = False + + window = windows.get(windows.ChannelWindow, e.network, e.msg[3], core) + if window: + window.nicklist.replace( + (nick, escape(prefix(e.network, channel.name, nick)), sortkey(e.network, channel.name, nick)) for nick in channel.nicks + ) + + elif e.msg[1] == '324': #channel mode is + channel = getchan(e.network,e.msg[3]) + if channel: + if not channel.got_mode: + e.quiet = True + channel.got_mode = True + mode = e.msg[4] + params = e.msg[:4:-1] + list_modes, always_parm_modes, set_parm_modes, normal_modes = \ + e.network.isupport['CHANMODES'].split(',') + parm_modes = always_parm_modes + set_parm_modes + channel.mode = e.msg[4] + channel.special_mode.clear() + for char in channel.mode: + if char in parm_modes: + channel.special_mode[char] = params.pop() + + elif e.msg[1] == '331': #no topic + channel = getchan(e.network,e.msg[3]) + if channel: + channel.topic = '' + + elif e.msg[1] == '332': #channel topic is + channel = getchan(e.network,e.msg[3]) + if channel: + channel.topic = e.text + +#core.events.load(__name__) diff --git a/services/console/lib/purk/scripts/clicks.py b/services/console/lib/purk/scripts/clicks.py new file mode 100644 index 00000000..b2f3f829 --- /dev/null +++ b/services/console/lib/purk/scripts/clicks.py @@ -0,0 +1,146 @@ +import ui +import windows +import chaninfo +from conf import conf + +def set_target(e): + target_l = e.target.lstrip('@+%.(<') + e._target_fr = e.target_fr + len(e.target) - len(target_l) + + target_r = e.target.rstrip('>:,') + e._target_to = e.target_to - len(e.target) + len(target_r) + + if target_r.endswith(')'): + e._target = e.text[e._target_fr:e._target_to] + open_parens = e._target.count('(') - e._target.count(')') + while open_parens < 0 and e.text[e._target_to-1] == ')': + e._target_to -= 1 + open_parens += 1 + + e._target = e.text[e._target_fr:e._target_to] + +def is_nick(e): + return isinstance(e.window, windows.ChannelWindow) and \ + chaninfo.ison(e.window.network, e.window.id, e._target) + +def is_url(e): + def starts(prefix, mindots=1): + def prefix_url(target): + return target.startswith(prefix) and target.count('.') >= mindots + + return prefix_url + + to_check = [starts(*x) for x in [ + ('http://', 1), + ('https://', 1), + ('ftp://', 1), + ('www', 2), + ]] + + for check_url in to_check: + if check_url(e._target): + return True + + return False + +def is_chan(e): + # click on a #channel + return e.window.network and e._target and \ + e._target[0] in e.window.network.isupport.get('CHANTYPES', '&#$+') + +def get_autojoin_list(network): + perform = conf.get('networks',{}).get(network.name,{}).get('perform',()) + channels = set() + for line in perform: + if line.startswith('join ') and ' ' not in line[5:]: + channels.update(line[5:].split(',')) + return channels + +def add_autojoin(network, channel): + if 'networks' not in conf: + conf['networks'] = {} + if network.name not in conf['networks']: + conf['networks'][network.name] = {'server': network.server} + conf['start_networks'] = conf.get('start_networks',[]) + [network.name] + if 'perform' in conf['networks'][network.name]: + perform = conf['networks'][network.name]['perform'] + else: + perform = conf['networks'][network.name]['perform'] = [] + + for n, line in enumerate(perform): + if line.startswith('join ') and ' ' not in line[5:]: + perform[n] = "%s,%s" % (line, channel) + break + else: + perform.append('join %s' % channel) + +def make_nick_menu(e, target): + def query(): + core.events.run('query %s' % target, e.window, e.window.network) + + def whois(): + core.events.run('whois %s' % target, e.window, e.window.network) + + e.menu += [ + ('Query', query), + ('Whois', whois), + (), + ] + +def onHover(e): + set_target(e) + + for is_check in (is_nick, is_url, is_chan): + if is_check(e): + e.tolink.add((e._target_fr, e._target_to)) + break + +def onClick(e): + set_target(e) + + if is_nick(e): + core.events.run('query %s' % e._target, e.window, e.window.network) + + # url of the form http://xxx.xxx or www.xxx.xxx + elif is_url(e): + if e._target.startswith('www'): + e._target = 'http://%s' % e._target + ui.open_file(e._target) + + # click on a #channel + elif is_chan(e): + if not chaninfo.ischan(e.window.network, e._target): + e.window.network.join(e._target) + window = windows.get(windows.ChannelWindow, e.window.network, e._target) + if window: + window.activate() + +def onRightClick(e): + set_target(e) + + # nick on this channel + if is_nick(e): + make_nick_menu(e, e._target) + + elif is_url(e): + if e._target.startswith('www'): + e._target = 'http://%s' % e._target + + def copy_to(): + # copy to clipboard + ui.set_clipboard(e._target) + + e.menu += [('Copy', copy_to)] + + elif is_chan(e): + e.channel = e._target + e.network = e.window.network + core.events.trigger('ChannelMenu', e) + +def onListRightClick(e): + if isinstance(e.window, windows.ChannelWindow): + make_nick_menu(e, e.nick) + +def onListDoubleClick(e): + if isinstance(e.window, windows.ChannelWindow): + core.events.run("query %s" % e.target, e.window, e.window.network) diff --git a/services/console/lib/purk/scripts/completion.py b/services/console/lib/purk/scripts/completion.py new file mode 100644 index 00000000..1719702c --- /dev/null +++ b/services/console/lib/purk/scripts/completion.py @@ -0,0 +1,135 @@ +import windows +import chaninfo +from conf import conf + +def channel_completer(window, left, right, text): + if isinstance(window, windows.ChannelWindow): + yield window.id + + for w in windows.get_with(wclass=windows.ChannelWindow, network=window.network): + if w is not window: + yield w.id + + for w in windows.get_with(wclass=windows.ChannelWindow): + if w.network is not window.network: + yield w.id + +# normal server commands +srv_commands = ('ping', 'join', 'part', 'mode', 'server', 'kick', + 'quit', 'nick', 'privmsg', 'notice', 'topic') + +def command_completer(window, left, right, text): + for c in srv_commands: + yield '/%s' % c + + if 'CMDS' in window.network.isupport: + for c in window.network.isupport['CMDS'].split(','): + yield '/%s' % c.lower() + + for c in core.events.all_events: + if c.startswith('Command') and c != 'Command': + yield '/%s' % c[7:].lower() + +def nick_completer(window, left, right, text): + if type(window) == windows.QueryWindow: + yield window.id + + recent_speakers = getattr(window, 'recent_speakers', ()) + + for nick in recent_speakers: + if chaninfo.ison(window.network, window.id, nick): + yield nick + + for nick in chaninfo.nicks(window.network, window.id): + if nick not in recent_speakers: + yield nick + +def script_completer(window, left, right, text): + return core.events.loaded.iterkeys() + +def network_completer(window, left, right, text): + return conf.get('networks', {}).iterkeys() + +def get_completer_for(window): + input = window.input + + left, right = input.text[:input.cursor], input.text[input.cursor:] + + text = left.split(' ')[-1] + + while True: + suffix = '' + if text and text[0] in window.network.isupport.get('CHANTYPES', '#&+'): + candidates = channel_completer(window, left, right, text) + + elif input.text.startswith('/reload '): + candidates = script_completer(window, left, right, text) + + elif input.text.startswith('/edit '): + candidates = script_completer(window, left, right, text) + + elif input.text.startswith('/server '): + candidates = network_completer(window, left, right, text) + + elif text.startswith('/'): + candidates = command_completer(window, left, right, text) + suffix = ' ' + + else: + candidates = nick_completer(window, left, right, text) + + if left == text: + suffix = ': ' + else: + suffix = ' ' + + if text: + before = left[:-len(text)] + else: + before = left + + insert_text = '%s%s%s%s' % (before, '%s', suffix, right) + cursor_pos = len(before + suffix) + + original = window.input.text, window.input.cursor + + for cand in candidates: + if cand.lower().startswith(text.lower()): + window.input.text, window.input.cursor = insert_text % cand, cursor_pos + len(cand) + yield None + + window.input.text, window.input.cursor = original + yield None + +# generator--use recent_completer.next() to continue cycling through whatever +recent_completer = None + +def onKeyPress(e): + global recent_completer + + if e.key == 'Tab': + if not recent_completer: + recent_completer = get_completer_for(e.window) + + recent_completer.next() + + else: + recent_completer = None + +def onActive(e): + global recent_completer + + recent_completer = None + +def onText(e): + if chaninfo.ischan(e.network, e.target): + if not hasattr(e.window, 'recent_speakers'): + e.window.recent_speakers = [] + + for nick in e.window.recent_speakers: + if nick == e.source or not chaninfo.ison(e.network, e.target, nick): + e.window.recent_speakers.remove(nick) + + e.window.recent_speakers.insert(0, e.source) + +onAction = onText diff --git a/services/console/lib/purk/scripts/console.py b/services/console/lib/purk/scripts/console.py new file mode 100755 index 00000000..bef8e0e6 --- /dev/null +++ b/services/console/lib/purk/scripts/console.py @@ -0,0 +1,68 @@ +import sys +import traceback +import windows +from conf import conf + +class ConsoleWriter: + __slots__ = ['window'] + def __init__(self, window): + self.window = window + def write(self, text): + try: + self.window.write(text, line_ending='') + except: + self.window.write(traceback.format_exc()) + +class ConsoleWindow(windows.SimpleWindow): + def __init__(self, network, id): + windows.SimpleWindow.__init__(self, network, id) + + writer = ConsoleWriter(self) + + sys.stdout = writer + sys.stderr = writer + + self.globals = {'window': self} + self.locals = {} + +#this prevents problems (and updates an open console window) on reload +#window = None +#for window in manager: +# if type(window).__name__ == "ConsoleWindow": +# window.mutate(ConsoleWindow, window.network, window.id) +#del window + +def onClose(e): + if isinstance(e.window, ConsoleWindow): + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + +def onCommandConsole(e): + windows.new(ConsoleWindow, None, "console").activate() + +def onCommandSay(e): + if isinstance(e.window, ConsoleWindow): + import pydoc #fix nonresponsive help() command + old_pager, pydoc.pager = pydoc.pager, pydoc.plainpager + e.window.globals.update(sys.modules) + text = ' '.join(e.args) + try: + e.window.write(">>> %s" % text) + result = eval(text, e.window.globals, e.window.locals) + if result is not None: + e.window.write(repr(result)) + e.window.globals['_'] = result + except SyntaxError: + try: + exec text in e.window.globals, e.window.locals + except: + traceback.print_exc() + except: + traceback.print_exc() + pydoc.pager = old_pager + else: + raise core.events.CommandError("There's no one here to speak to.") + +def onStart(e): + if conf.get('start-console'): + windows.new(ConsoleWindow, None, "console") diff --git a/services/console/lib/purk/scripts/history.py b/services/console/lib/purk/scripts/history.py new file mode 100644 index 00000000..379ad5e3 --- /dev/null +++ b/services/console/lib/purk/scripts/history.py @@ -0,0 +1,45 @@ +def onKeyPress(e): + if not hasattr(e.window, 'history'): + e.window.history = [], -1 + + if e.key in ('Up', 'Down'): + h, i = e.window.history + + if i == -1 and e.window.input.text: + h.insert(0, e.window.input.text) + i = 0 + + if e.key == 'Up': + i += 1 + + if i < len(h): + e.window.history = h, i + + e.window.input.text = h[i] + e.window.input.cursor = -1 + + else: # e.key == 'Up' + i -= 1 + + if i > -1: + e.window.history = h, i + + e.window.input.text = h[i] + e.window.input.cursor = -1 + + elif i == -1: + e.window.history = h, i + + e.window.input.text = '' + e.window.input.cursor = -1 + +def onInput(e): + if not hasattr(e.window, 'history'): + e.window.history = [], -1 + + if e.text: + h, i = e.window.history + + h.insert(0, e.text) + + e.window.history = h, -1 diff --git a/services/console/lib/purk/scripts/ignore.py b/services/console/lib/purk/scripts/ignore.py new file mode 100755 index 00000000..98b4eed6 --- /dev/null +++ b/services/console/lib/purk/scripts/ignore.py @@ -0,0 +1,43 @@ +from conf import conf +import irc + +def preRaw(e): + if e.msg[1] in ('PRIVMSG','NOTICE'): + address = e.network.norm_case('%s!%s' % (e.source, e.address)) + for mask in conf.get('ignore_masks',()): + if irc.match_glob(address, e.network.norm_case(mask)): + core.events.halt() + +def onCommandIgnore(e): + if 'ignore_masks' not in conf: + conf['ignore_masks'] = [] + if 'l' in e.switches: + for i in conf['ignore_masks']: + e.window.write('* %s' % i) + elif 'c' in e.switches: + del conf['ignore_masks'] + e.window.write('* Cleared the ignore list.') + elif e.args: + if '!' in e.args[0] or '*' in e.args[0] or '?' in e.args[0]: + mask = e.args[0] + else: + mask = '%s!*' % e.args[0] + if 'r' in e.switches: + if mask in conf['ignore_masks']: + conf['ignore_masks'].remove(mask) + e.window.write('* Removed %s from the ignore list' % e.args[0]) + else: + raise core.events.CommandError("Couldn't find %s in the ignore list" % e.args[0]) + else: + if mask in conf['ignore_masks']: + e.window.write('* %s is already ignored' % e.args[0]) + else: + conf['ignore_masks'].append(mask) + e.window.write('* Ignoring messages from %s' % e.args[0]) + else: + e.window.write( +"""Usage: + /ignore \x02nick/mask\x02 to ignore a nickname or mask + /ignore -r \x02nick/mask\x02 to stop ignoring a nickname or mask + /ignore -l to view the ignore list + /ignore -c to clear the ignore list""") diff --git a/services/console/lib/purk/scripts/irc_script.py b/services/console/lib/purk/scripts/irc_script.py new file mode 100644 index 00000000..4582d725 --- /dev/null +++ b/services/console/lib/purk/scripts/irc_script.py @@ -0,0 +1,588 @@ +import time + +from conf import conf +import ui +import windows +import irc + +COMMAND_PREFIX = conf.get('command_prefix', '/') + +NICK_SUFFIX = r"`_-\|0123456789" + +_hextochr = dict(('%02x' % i, chr(i)) for i in range(256)) +def unquote(url, rurl=""): + + while '%' in url: + url, char = url.rsplit('%', 1) + + chars = char[:2].lower() + + if chars in _hextochr: + rurl = '%s%s%s' % (_hextochr[chars], char[2:], rurl) + else: + rurl = "%s%s%s" % ('%', char, rurl) + + return url + rurl + +#for getting a list of alternative nicks to try on a network +def _nick_generator(network): + for nick in network.nicks[1:]: + yield nick + if network._nick_error: + nick = 'ircperson' + else: + nick = network.nicks[0] + import itertools + for i in itertools.count(1): + for j in xrange(len(NICK_SUFFIX)**i): + suffix = ''.join(NICK_SUFFIX[(j/(len(NICK_SUFFIX)**x))%len(NICK_SUFFIX)] for x in xrange(i)) + if network._nick_max_length: + yield nick[0:network._nick_max_length-i]+suffix + else: + yield nick+suffix + +def setdownRaw(e): + if not e.done: + if not e.network.got_nick: + if e.msg[1] in ('432','433','436','437'): #nickname unavailable + failednick = e.msg[3] + nicks = list(e.network.nicks) + + if hasattr(e.network,'_nick_generator'): + if len(failednick) < len(e.network._next_nick): + e.network._nick_max_length = len(failednick) + e.network._next_nick = e.network._nick_generator.next() + e.network.raw('NICK %s' % e.network._next_nick) + e.network._nick_error |= (e.msg[1] == '432') + else: + e.network._nick_error = (e.msg[1] == '432') + if len(failednick) < len(e.network.nicks[0]): + e.network._nick_max_length = len(failednick) + else: + e.network._nick_max_length = 0 + e.network._nick_generator = _nick_generator(e.network) + e.network._next_nick = e.network._nick_generator.next() + e.network.raw('NICK %s' % e.network._next_nick) + + elif e.msg[1] == '431': #no nickname given--this shouldn't happen + pass + + elif e.msg[1] == '001': + e.network.got_nick = True + if e.network.me != e.msg[2]: + core.events.trigger( + 'Nick', network=e.network, window=e.window, + source=e.network.me, target=e.msg[2], address='', + text=e.msg[2] + ) + e.network.me = e.msg[2] + if hasattr(e.network,'_nick_generator'): + del e.network._nick_generator, e.network._nick_max_length, e.network._next_nick + + if e.msg[1] == "PING": + e.network.raw("PONG :%s" % e.msg[-1]) + e.done = True + + elif e.msg[1] == "JOIN": + e.channel = e.target + e.requested = e.network.norm_case(e.channel) in e.network.requested_joins + core.events.trigger("Join", e) + e.done = True + + elif e.msg[1] == "PART": + e.channel = e.target + e.requested = e.network.norm_case(e.channel) in e.network.requested_parts + e.text = ' '.join(e.msg[3:]) + core.events.trigger("Part", e) + e.done = True + + elif e.msg[1] in "MODE": + e.channel = e.target + e.text = ' '.join(e.msg[3:]) + core.events.trigger("Mode", e) + e.done = True + + elif e.msg[1] == "QUIT": + core.events.trigger('Quit', e) + e.done = True + + elif e.msg[1] == "KICK": + e.channel = e.msg[2] + e.target = e.msg[3] + core.events.trigger('Kick', e) + e.done = True + + elif e.msg[1] == "NICK": + core.events.trigger('Nick', e) + if e.network.me == e.source: + e.network.me = e.target + + e.done = True + + elif e.msg[1] == "PRIVMSG": + core.events.trigger('Text', e) + e.done = True + + elif e.msg[1] == "NOTICE": + core.events.trigger('Notice', e) + e.done = True + + elif e.msg[1] == "TOPIC": + core.events.trigger('Topic', e) + e.done = True + + elif e.msg[1] in ("376", "422"): #RPL_ENDOFMOTD + if e.network.status == irc.INITIALIZING: + e.network.status = irc.CONNECTED + core.events.trigger('Connect', e) + e.done = True + + elif e.msg[1] == "470": #forwarded from channel X to channel Y + if e.network.norm_case(e.msg[3]) in e.network.requested_joins: + e.network.requested_joins.discard(e.network.norm_case(e.msg[3])) + e.network.requested_joins.add(e.network.norm_case(e.msg[4])) + + elif e.msg[1] == "005": #RPL_ISUPPORT + for arg in e.msg[3:]: + if ' ' not in arg: #ignore "are supported by this server" + if '=' in arg: + name, value = arg.split('=', 1) + if value.isdigit(): + value = int(value) + else: + name, value = arg, '' + + #Workaround for broken servers (bahamut on EnterTheGame) + if name == 'PREFIX' and value[0] != '(': + continue + + #in theory, we're supposed to replace \xHH with the + # corresponding ascii character, but I don't think anyone + # really does this + e.network.isupport[name] = value + + if name == 'PREFIX': + new_prefixes = {} + modes, prefixes = value[1:].split(')') + for mode, prefix in zip(modes, prefixes): + new_prefixes[mode] = prefix + new_prefixes[prefix] = mode + e.network.prefixes = new_prefixes + +def setupSocketConnect(e): + e.network.got_nick = False + e.network.isupport = { + 'NETWORK': e.network.server, + 'PREFIX': '(ohv)@%+', + 'CHANMODES': 'b,k,l,imnpstr', + } + e.network.prefixes = {'o':'@', 'h':'%', 'v':'+', '@':'o', '%':'h', '+':'v'} + e.network.connect_timestamp = time.time() + e.network.requested_joins.clear() + e.network.requested_parts.clear() + e.network.on_channels.clear() + if hasattr(e.network,'_nick_generator'): + del e.network._nick_generator, e.network._nick_max_length, e.network._next_nick + if not e.done: + #this needs to be tested--anyone have a server that uses PASS? + if e.network.password: + e.network.raw("PASS :%s" % e.network.password) + e.network.raw("NICK %s" % e.network.nicks[0]) + e.network.raw("USER %s %s %s :%s" % + (e.network.username, "8", "*", e.network.fullname)) + #per rfc2812 these are username, user mode flags, unused, realname + + #e.network.me = None + e.done = True + +def onDisconnect(e): + if hasattr(e.network,'_reconnect_source'): + e.network._reconnect_source.unregister() + del e.network._reconnect_source + if hasattr(e.network,'connect_timestamp'): + if e.error and conf.get('autoreconnect',True): + delay = time.time() - e.network.connect_timestamp > 30 and 30 or 120 + def do_reconnect(): + if not e.network.status: + server(network=e.network) + def do_announce_reconnect(): + if not e.network.status: + windows.get_default(e.network).write("* Will reconnect in %s seconds.." % delay) + e.network._reconnect_source = ui.register_timer(delay*1000,do_reconnect) + e.network._reconnect_source = ui.register_idle(do_announce_reconnect) + +def onCloseNetwork(e): + e.network.quit() + if hasattr(e.network,'_reconnect_source'): + e.network._reconnect_source.unregister() + del e.network._reconnect_source + +def setdownDisconnect(e): + if hasattr(e.network,'connect_timestamp'): + del e.network.connect_timestamp + +def setupInput(e): + if not e.done: + if e.text.startswith(COMMAND_PREFIX) and not e.ctrl: + command = e.text[len(COMMAND_PREFIX):] + else: + command = 'say - %s' % e.text + + core.events.run(command, e.window, e.network) + + e.done = True + +def onCommandSay(e): + if isinstance(e.window, windows.ChannelWindow) or isinstance(e.window, windows.QueryWindow): + e.network.msg(e.window.id, ' '.join(e.args)) + else: + raise core.events.CommandError("There's no one here to speak to.") + +def onCommandMsg(e): + e.network.msg(e.args[0], ' '.join(e.args[1:])) + +def onCommandNotice(e): + e.network.notice(e.args[0], ' '.join(e.args[1:])) + +def onCommandQuery(e): + windows.new(windows.QueryWindow, e.network, e.args[0], core).activate() + if len(e.args) > 1: + message = ' '.join(e.args[1:]) + if message: #this is false if you do "/query nickname " + e.network.msg(e.args[0], ' '.join(e.args[1:])) + +def setupJoin(e): + if e.source == e.network.me: + chan = e.network.norm_case(e.channel) + e.network.on_channels.add(chan) + e.network.requested_joins.discard(chan) + +def setdownPart(e): + if e.source == e.network.me: + chan = e.network.norm_case(e.channel) + e.network.on_channels.discard(chan) + e.network.requested_parts.discard(chan) + +def setdownKick(e): + if e.target == e.network.me: + chan = e.network.norm_case(e.channel) + e.network.on_channels.discard(chan) + +def ischan(network, channel): + return network.norm_case(channel) in network.on_channels + +# make /nick work offline +def change_nick(network, nick): + if not network.status: + core.events.trigger( + 'Nick', + network=network, window=windows.get_default(network), + source=network.me, target=nick, address='', text=nick + ) + network.nicks[0] = nick + network.me = nick + else: + network.raw('NICK :%s' % nick) + +def onCommandNick(e): + default_nick = irc.default_nicks()[0] + if 't' not in e.switches and e.network.me == default_nick: + conf['nick'] = e.args[0] + import conf as _conf + _conf.save() + for network in set(w.network for w in core.manager): + if network.me == default_nick: + change_nick(network, e.args[0]) + else: + change_nick(e.network, e.args[0]) + +def setdownNick(e): + if e.source != e.network.me: + window = windows.get(windows.QueryWindow, e.network, e.source) + if window: + window.id = e.target + +# make /quit always disconnect us +def onCommandQuit(e): + if e.network.status: + e.network.quit(' '.join(e.args)) + else: + raise core.events.CommandError("We're not connected to a network.") + +def onCommandRaw(e): + if e.network.status >= irc.INITIALIZING: + e.network.raw(' '.join(e.args)) + else: + raise core.events.CommandError("We're not connected to a network.") + +onCommandQuote = onCommandRaw + +def onCommandJoin(e): + if e.args: + if e.network.status >= irc.INITIALIZING: + e.network.join(' '.join(e.args), requested = 'n' not in e.switches) + else: + raise core.events.CommandError("We're not connected.") + elif isinstance(e.window, windows.ChannelWindow): + e.window.network.join(e.window.id, requested = 'n' not in e.switches) + else: + raise core.events.CommandError("You must supply a channel.") + +def onCommandPart(e): + if e.args: + if e.network.status >= irc.INITIALIZING: + e.network.part(' '.join(e.args), requested = 'n' not in e.switches) + else: + raise core.events.CommandError("We're not connected.") + elif isinstance(e.window, windows.ChannelWindow): + e.window.network.part(e.window.id, requested = 'n' not in e.switches) + else: + raise core.events.CommandError("You must supply a channel.") + +def onCommandHop(e): + if e.args: + if e.network.status >= irc.INITIALIZING: + e.network.part(e.args[0], requested = False) + e.network.join(' '.join(e.args), requested = False) + else: + raise core.events.CommandError("We're not connected.") + elif isinstance(e.window, windows.ChannelWindow): + e.window.network.part(e.window.id, requested = False) + e.window.network.join(e.window.id, requested = False) + else: + raise core.events.CommandError("You must supply a channel.") + +#this should be used whereever a new irc.Network may need to be created +def server(server=None,port=6667,network=None,connect=True): + network_info = {} + + if server: + network_info["name"] = server + network_info["server"] = server + if port: + network_info["port"] = port + get_network_info(server, network_info) + + if not network: + network = irc.Network(**network_info) + windows.new(windows.StatusWindow, network, "status").activate() + else: + if "server" in network_info: + network.name = network_info['name'] + network.server = network_info['server'] + if not network.status: + #window = windows.get_default(network) + window = core.window + if window: + window.update() + if "port" in network_info: + network.port = network_info["port"] + + if network.status: + network.quit() + if connect: + network.connect() + core.window.write("* Connecting to %s on port %s" % (network.server, network.port)) + #windows.get_default(network).write( + # "* Connecting to %s on port %s" % (network.server, network.port) + # ) + + return network + +def onCommandServer(e): + host = port = None + + if e.args: + host = e.args[0] + + if ':' in host: + host, port = host.rsplit(':', 1) + port = int(port) + + elif len(e.args) > 1: + port = int(e.args[1]) + + else: + port = 6667 + + if 'm' in e.switches: + network = None + else: + network = e.network + + server(server=host, port=port, network=network, connect='o' not in e.switches) + +#see http://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt +def onCommandIrcurl(e): + url = e.args[0] + + if url.startswith('irc://'): + url = url[6:] + + if not url.startswith('/'): + host, target = url.rsplit('/',1) + if ':' in host: + host, port = host.rsplit(':',1) + else: + port = 6667 + else: + host = None + port = 6667 + target = url + + if host: + if e.network and e.network.server == host: + network = e.network + else: + for w in list(windows.manager): + if w.network and w.network.server == host: + network = w.network + break + else: + for w in list(windows.manager): + if w.network and w.network.server == 'irc.default.org': + network = server(host,port,w.network) + break + else: + network = server(host,port) + + if ',' in target: + target, modifiers = target.split(',',1) + action = '' + else: + target = unquote(target) + if target[0] not in '#&+': + target = '#'+target + action = 'join %s' % target + + if network.status == irc.CONNECTED: + core.events.run(action, windows.get_default(network), network) + else: + if not hasattr(network,'temp_perform'): + network.temp_perform = [action] + else: + network.temp_perform.append(action) + +#commands that we need to add a : to but otherwise can send unchanged +#the dictionary contains the number of arguments we take without adding the : +trailing = { + 'away':0, + 'cnotice':2, + 'cprivmsg':2, + 'kick':2, + 'kill':1, + 'part':1, + 'squery':1, + 'squit':1, + 'topic':1, + 'wallops':0, + } + +needschan = { + 'topic':0, + 'invite':1, + 'kick':0, +# 'mode':0, #this is commonly used for channels, but can apply to users +# 'names':0, #with no parameters, this is supposed to give a list of all users; we may be able to safely ignore that. + } + +def setupCommand(e): + if not e.done: + if e.name in needschan and isinstance(e.window, windows.ChannelWindow): + valid_chan_prefixes = e.network.isupport.get('CHANTYPES', '#&+') + chan_pos = needschan[e.name] + + if len(e.args) > chan_pos: + if not e.args[chan_pos] or e.args[chan_pos][0] not in valid_chan_prefixes: + e.args.insert(chan_pos, e.window.id) + else: + e.args.append(e.window.id) + + if e.name in trailing: + trailing_pos = trailing[e.name] + + if len(e.args) > trailing_pos: + e.args[trailing_pos] = ':%s' % e.args[trailing_pos] + + e.text = '%s %s' % (e.name, ' '.join(e.args)) + +def setdownCommand(e): + if not e.done and e.network.status >= irc.INITIALIZING: + e.network.raw(e.text) + e.done = True + +def get_network_info(name, network_info): + conf_info = conf.get('networks', {}).get(name) + + if conf_info: + network_info['server'] = name + network_info.update(conf_info) + +def onStart(e): + for network in conf.get('start_networks', []): + server(server=network) + +def onConnect(e): + network_info = conf.get('networks', {}).get(e.network.name, {}) + + for command in network_info.get('perform', []): + while command.startswith(COMMAND_PREFIX): + command = command[len(COMMAND_PREFIX):] + core.events.run(command, e.window, e.network) + + tojoin = ','.join(network_info.get('join', [])) + if tojoin: + core.events.run('join -n %s' % tojoin, e.window, e.network) + + if hasattr(e.network,'temp_perform'): + for command in e.network.temp_perform: + core.events.run(command, e.window, e.network) + del e.network.temp_perform + +def isautojoin(network, channel): + try: + joinlist = conf['networks'][network.name]['join'] + except KeyError: + return False + normchannel = network.norm_case(channel) + for chan in joinlist: + if normchannel == network.norm_case(chan): + return True + return False + +def setautojoin(network, channel): + if 'networks' not in conf: + conf['networks'] = networks = {} + else: + networks = conf['networks'] + if network.name not in networks: + networks[network.name] = network_settings = {} + if 'start_networks' not in conf: + conf['start_networks'] = [] + conf['start_networks'].append(network.name) + else: + network_settings = networks[network.name] + + if 'join' not in network_settings: + network_settings['join'] = [channel] + else: + network_settings['join'].append(channel) + +def unsetautojoin(network, channel): + try: + joinlist = conf['networks'][network.name]['join'] + except KeyError: + return False + normchannel = network.norm_case(channel) + for i, chan in enumerate(joinlist[:]): + if normchannel == network.norm_case(chan): + joinlist.pop(i) + +def onChannelMenu(e): + def toggle_join(): + if isautojoin(e.network, e.channel): + unsetautojoin(e.network, e.channel) + else: + setautojoin(e.network, e.channel) + + e.menu.append(('Autojoin', isautojoin(e.network, e.channel), toggle_join)) diff --git a/services/console/lib/purk/scripts/keys.py b/services/console/lib/purk/scripts/keys.py new file mode 100644 index 00000000..e26572cc --- /dev/null +++ b/services/console/lib/purk/scripts/keys.py @@ -0,0 +1,70 @@ +import windows +import widgets +import irc + +shortcuts = { + '^b': '\x02', + '^u': '\x1F', + '^r': '\x16', + '^k': '\x03', + '^l': '\x04', + '^o': '\x0F', + } + +def onKeyPress(e): + if e.key in shortcuts: + e.window.input.insert(shortcuts[e.key]) + + elif e.key == '!c': + e.window.output.copy() + + elif e.key == 'Page_Up': + e.window.output.y = e.window.output.y - e.window.output.height / 2 + + elif e.key == 'Page_Down': + e.window.output.y = e.window.output.y + e.window.output.height / 2 + + elif e.key == '^Home': + e.window.output.y = 0 + + elif e.key == '^End': + e.window.output.y = e.window.output.ymax + + elif e.key in ('^Page_Up', '^Page_Down'): + winlist = list(windows.manager) + index = winlist.index(e.window) + ((e.key == '^Page_Down') and 1 or -1) + if 0 <= index < len(winlist): + winlist[index].activate() + + elif e.key == '!a': + winlist = list(windows.manager) + winlist = winlist[winlist.index(e.window):]+winlist + w = [w for w in winlist if widgets.HILIT in w.activity] + + if not w: + w = [w for w in winlist if widgets.TEXT in w.activity] + + if w: + windows.manager.set_active(w[0]) + + # tabbed browsing + elif e.key == '^t': + windows.new(windows.StatusWindow, irc.Network(), 'status').activate() + + elif e.key == '^w': + windows.manager.get_active().close() + + elif e.key == '^f': + window = windows.manager.get_active() + + find = widgets.FindBox(window) + + window.pack_start(find, expand=False) + + find.textbox.grab_focus() + + elif len(e.key) == 2 and e.key.startswith('!') and e.key[1].isdigit(): + n = int(e.key[1]) + if n and n <= len(core.manager): + list(core.manager)[n-1].activate() + #else e.key == "!0" diff --git a/services/console/lib/purk/scripts/theme.py b/services/console/lib/purk/scripts/theme.py new file mode 100644 index 00000000..7fda4d2d --- /dev/null +++ b/services/console/lib/purk/scripts/theme.py @@ -0,0 +1,366 @@ +import time + +import windows +import widgets +import chaninfo + +from conf import conf + +textareas = {} +if 'font' in conf: + textareas['font'] = conf['font'] +if 'bg_color' in conf: + textareas['bg'] = conf['bg_color'] +if 'fg_color' in conf: + textareas['fg'] = conf['fg_color'] + +widgets.set_style("view", textareas) +widgets.set_style("nicklist", textareas) + +#copied pretty directly from something that was probably copied from wine sources +def RGBtoHSL(r, g, b): + maxval = max(r, g, b) + minval = min(r, g, b) + + luminosity = ((maxval + minval) * 240 + 255) // 510 + + if maxval == minval: + saturation = 0 + hue = 160 + else: + delta = maxval - minval + + if luminosity <= 120: + saturation = ((maxval+minval)//2 + delta*240) // (maxval + minval) + else: + saturation = ((150-maxval-minval)//2 + delta*240) // (150-maxval-minval) + + #sigh.. + rnorm = (delta//2 + maxval*40 - r*40)//delta + gnorm = (delta//2 + maxval*40 - g*40)//delta + bnorm = (delta//2 + maxval*40 - b*40)//delta + + if r == maxval: + hue = bnorm-gnorm + elif g == maxval: + hue = 80+rnorm-bnorm + else: + hue = 160+gnorm-rnorm + hue = hue % 240 + return hue, saturation, luminosity + +#copied from the same place +def huetoRGB(hue, mid1, mid2): + hue = hue % 240 + + if hue > 160: + return mid1 + elif hue > 120: + hue = 160 - hue + elif hue > 40: + return mid2 + return ((hue * (mid2 - mid1) + 20) // 40) + mid1 + +#this too +def HSLtoRGB(hue, saturation, luminosity): + if saturation != 0: + if luminosity > 120: + mid2 = saturation + luminosity - (saturation * luminosity + 120)//240 + else: + mid2 = ((saturation + 240) * luminosity + 120)//240 + + mid1 = luminosity * 2 - mid2 + + return tuple((huetoRGB(hue+x, mid1, mid2) * 255 + 120) // 240 for x in (80,0,-80)) + else: + value = luminosity * 255 // 240 + return value, value, value + +def gethashcolor(string): + h = hash(string) + rgb = HSLtoRGB(h%241, 100-h//241%61, 90) + return "%02x%02x%02x" % rgb + +#take an event e and trigger the highlight event if necessary +def hilight_text(e): + if not hasattr(e, 'Highlight'): + e.Highlight = [] + core.events.trigger('Highlight', e) + +#hilight own nick +def onHighlight(e): + lowertext = e.text.lower() + for word in conf.get('highlight_words', []) + [e.network.me] + e.network.nicks: + lowerword = word.lower() + pos = lowertext.find(lowerword, 0) + while pos != -1: + e.Highlight.append((pos, pos+len(word))) + pos = lowertext.find(lowerword, pos+1) + +def prefix(e): + return time.strftime(conf.get('timestamp', '')) + +def getsourcecolor(e): + address = getattr(e, "address", "") + if address: + if e.network.me == e.source: + e.network._my_address = address + elif e.network.me == e.source: + address = getattr(e.network, "_my_address", "") + if '@' in address: + address = address.split('@')[1] + if not address: + address = e.source + return "\x04%s" % gethashcolor(address) + +def format_source(e): + highlight = getattr(e, "Highlight", "") and '\x02' or '' + return "%s\x04%s%s" % (highlight, getsourcecolor(e), e.source) + +def format_info_source(e): + if e.source == e.network.me: + return "\x04%sYou" % (getsourcecolor(e)) + else: + return "\x04%s%s" % (getsourcecolor(e), e.source) + +def address(e): + #if e.source != e.network.me: + # return "%s " % info_in_brackets(e.address) + #else: + # return "" + return "" + +def text(e): + if e.text: + #return " %s" % info_in_brackets(e.text) + return ": \x0F%s" % e.text + else: + return "" + +def info_in_brackets(text): + return "(\x044881b6%s\x0F)" % text + +def pretty_time(secs): + times = ( + #("years", "year", 31556952), + ("weeks", "week", 604800), + ("days", "day", 86400), + ("hours", "hour", 3600), + ("minutes", "minute", 60), + ("seconds", "second", 1), + ) + if secs == 0: + return "0 seconds" + result = "" + for plural, singular, amount in times: + n, secs = divmod(secs, amount) + if n == 1: + result = result + " %s %s" % (n, singular) + elif n: + result = result + " %s %s" % (n, plural) + return result[1:] + +def onText(e): + hilight_text(e) + color = getsourcecolor(e) + to_write = prefix(e) + if e.network.me == e.target: # this is a pm + if e.window.id == e.network.norm_case(e.source): + to_write += "\x02<\x0F%s\x0F\x02>\x0F " % (format_source(e)) + else: + to_write += "\x02*\x0F%s\x0F\x02*\x0F " % (format_source(e)) + else: + if e.window.id == e.network.norm_case(e.target): + to_write += "\x02<\x0F%s\x0F\x02>\x0F " % (format_source(e)) + else: + to_write += "\x02<\x0F%s:%s\x0F\x02>\x0F " % (format_source(e), e.target) + to_write += e.text + + if e.Highlight: + e.window.write(to_write, widgets.HILIT) + else: + e.window.write(to_write, widgets.TEXT) + +def onOwnText(e): + color = getsourcecolor(e) + to_write = prefix(e) + if e.window.id == e.network.norm_case(e.target): + to_write += "\x02<\x0F%s\x0F\x02>\x0F %s" % (format_source(e), e.text) + else: + to_write += "%s->\x0F \x02*\x0F%s\x0F\x02*\x0F %s" % (color, e.target, e.text) + + e.window.write(to_write) + +def onAction(e): + hilight_text(e) + color = color = getsourcecolor(e) + to_write = "%s\x02*\x0F %s\x0F %s" % (prefix(e), format_source(e), e.text) + + if e.Highlight: + e.window.write(to_write, widgets.HILIT) + else: + e.window.write(to_write, widgets.TEXT) + +def onOwnAction(e): + color = getsourcecolor(e) + to_write = "%s\x02*\x0F %s\x0F %s" % (prefix(e), format_source(e), e.text) + + e.window.write(to_write) + +def onNotice(e): + hilight_text(e) + color = getsourcecolor(e) + to_write = prefix(e) + if e.network.me == e.target: # this is a pm + to_write += "\x02-\x0F%s\x0F\x02-\x0F " % (format_source(e)) + else: + to_write += "\x02-\x0F%s:%s\x0F\x02-\x0F " % (format_source(e), e.target) + to_write += e.text + + e.window.write(to_write, (e.Highlight and widgets.HILIT) or widgets.TEXT) + +def onOwnNotice(e): + color = getsourcecolor(e) + to_write = "%s-> \x02-\x02%s\x0F\x02-\x0F %s" % (prefix(e), e.target, e.text) + + e.window.write(to_write) + +def onCtcp(e): + color = getsourcecolor(e) + to_write = "%s\x02[\x02%s\x0F\x02]\x0F %s" % (prefix(e), format_source(e), e.text) + + if not e.quiet: + e.window.write(to_write) + +def onCtcpReply(e): + color = getsourcecolor(e) + to_write = "%s%s--- %s reply from %s:\x0F %s" % (prefix(e), color, e.name.capitalize(), format_source(e), ' '.join(e.args)) + + window = windows.manager.get_active() + if window.network != e.network: + window = windows.get_default(e.network) + window.write(to_write, widgets.TEXT) + +def onJoin(e): + if e.source == e.network.me: + to_write = "%s%s %sjoin %s" % (prefix(e), format_info_source(e), address(e), e.target) + else: + to_write = "%s%s %sjoins %s" % (prefix(e), format_info_source(e), address(e), e.target) + + e.window.write(to_write) + +def onPart(e): + if e.source == e.network.me: + to_write = "%s%s leave %s%s" % (prefix(e), format_info_source(e), e.target, text(e)) + else: + to_write = "%s%s leaves %s%s" % (prefix(e), format_info_source(e), e.target, text(e)) + + e.window.write(to_write) + +def onKick(e): + if e.source == e.network.me: + to_write = "%s%s kick %s%s" % (prefix(e), format_info_source(e), e.target, text(e)) + else: + to_write = "%s%s kicks %s%s" % (prefix(e), format_info_source(e), e.target, text(e)) + + e.window.write(to_write, (e.target == e.network.me and widgets.HILIT) or widgets.EVENT) + +def onMode(e): + if e.source == e.network.me: + to_write = "%s%s set mode:\x0F %s" % (prefix(e), format_info_source(e), e.text) + else: + to_write = "%s%s sets mode:\x0F %s" % (prefix(e), format_info_source(e), e.text) + + e.window.write(to_write) + +def onQuit(e): + to_write = "%s%s leaves%s" % (prefix(e), format_info_source(e), text(e)) + + for channame in chaninfo.channels(e.network): + if chaninfo.ison(e.network, channame, e.source): + window = windows.get(windows.ChannelWindow, e.network, channame, core) + if window: + window.write(to_write) + +def onNick(e): + color = getsourcecolor(e) + if e.source == e.network.me: + to_write = "%s%sYou are now known as %s" % (prefix(e), color, e.target) + else: + to_write = "%s%s%s is now known as %s" % (prefix(e), color, e.source, e.target) + + if e.source == e.network.me: + for window in windows.get_with(core.manager, network=e.network): + window.write(to_write) + else: + for channame in chaninfo.channels(e.network): + if chaninfo.ison(e.network,channame,e.source): + window = windows.get(windows.ChannelWindow, e.network, channame) + if window: + window.write(to_write) + +def onTopic(e): + if e.source == e.network.me: + to_write = "%s%s set topic:\x0F %s" % (prefix(e), format_info_source(e), e.text) + else: + to_write = "%s%s sets topic:\x0F %s" % (prefix(e), format_info_source(e), e.text) + + e.window.write(to_write) + +def onRaw(e): + if not e.quiet: + if e.msg[1].isdigit(): + if e.msg[1] == '332': + window = windows.get(windows.ChannelWindow, e.network, e.msg[3], core) or e.window + window.write( + "%sTopic on %s is: %s" % + (prefix(e), e.msg[3], e.text) + ) + + elif e.msg[1] == '333': + window = windows.get(windows.ChannelWindow, e.network, e.msg[3], core) or e.window + window.write( + "%sTopic on %s set by %s at time %s" % + (prefix(e), e.msg[3], e.msg[4], time.ctime(int(e.msg[5]))) + ) + + elif e.msg[1] == '329': #RPL_CREATIONTIME + pass + + elif e.msg[1] == '311': #RPL_WHOISUSER + e.window.write("* %s is %s@%s * %s" % (e.msg[3], e.msg[4], e.msg[5], e.msg[7])) + + elif e.msg[1] == '312': #RPL_WHOISSERVER + e.window.write("* %s on %s (%s)" % (e.msg[3], e.msg[4], e.msg[5])) + + elif e.msg[1] == '317': #RPL_WHOISIDLE + e.window.write("* %s has been idle for %s" % (e.msg[3], pretty_time(int(e.msg[4])))) + if e.msg[5].isdigit(): + e.window.write("* %s signed on %s" % (e.msg[3], time.ctime(int(e.msg[5])))) + + elif e.msg[1] == '319': #RPL_WHOISCHANNELS + e.window.write("* %s on channels: %s" % (e.msg[3], e.msg[4])) + + elif e.msg[1] == '330': #RPL_WHOISACCOUNT + #this appears to conflict with another raw, so if there's anything weird about it, + # we fall back on the default + if len(e.msg) == 6 and not e.msg[4].isdigit() and not e.msg[5].isdigit(): + e.window.write("* %s %s %s" % (e.msg[3], e.msg[5], e.msg[4])) + else: + e.window.write("* %s" % ' '.join(e.msg[3:])) + + else: + e.window.write("* %s" % ' '.join(e.msg[3:])) + elif e.msg[1] == 'ERROR': + e.window.write("Error: %s" % e.text) + +def onDisconnect(e): + to_write = '%s* Disconnected' % prefix(e) + if e.error: + to_write += ' (%s)' % e.error + + for window in windows.get_with(network=e.network): + if isinstance(window, windows.StatusWindow): + window.write(to_write, widgets.TEXT) + else: + window.write(to_write, widgets.EVENT) diff --git a/services/console/lib/purk/scripts/timeout.py b/services/console/lib/purk/scripts/timeout.py new file mode 100755 index 00000000..2f0f5852 --- /dev/null +++ b/services/console/lib/purk/scripts/timeout.py @@ -0,0 +1,45 @@ +import time + +import ui +from conf import conf + +def setupRaw(e): + e.network._message_timeout = False + +def onSocketConnect(e): + timeout = conf.get("server_traffic_timeout", 120)*1000 + e.network._message_timeout = False + if timeout: + e.network._message_timeout_source = ui.register_timer(timeout, check_timeout, e.network) + else: + e.network._message_timeout_source = None + +def check_timeout(network): + if network._message_timeout: + network.raw("PING %s" % network.me) + timeout = conf.get("server_death_timeout", 240)*1000 + network._message_timeout_source = ui.register_timer(timeout, check_death_timeout, network) + return False + else: + network._message_timeout = True + return True # call this function again + +def check_death_timeout(network): + if network._message_timeout: + network.raw("QUIT :Server missing, presumed dead") + network.disconnect(error="The server seems to have stopped talking to us") + else: + network._message_timeout = False + timeout = conf.get("server_traffic_timeout", 120)*1000 + if timeout: + network._message_timeout_source = ui.register_timer(timeout, check_timeout, network) + else: + network._message_timeout_source = None + +def onDisconnect(e): + try: + if e.network._message_timeout_source: + e.network._message_timeout_source.unregister() + e.network._message_timeout_source = None + except AttributeError: + pass diff --git a/services/console/lib/purk/scripts/ui_script.py b/services/console/lib/purk/scripts/ui_script.py new file mode 100644 index 00000000..459de96a --- /dev/null +++ b/services/console/lib/purk/scripts/ui_script.py @@ -0,0 +1,132 @@ +import irc +import ui +import windows +import irc_script +from conf import conf + +# FIXME: meh still might want rid of these, I'm not sure yet + +def onActive(e): + e.window.activity = None + + ui.register_idle(windows.manager.set_title) + +def setupNick(e): + if e.source == e.network.me: + for w in windows.get_with(core.manager, network=e.network): + try: + w.nick_label.update(e.target) + except AttributeError: + pass + +def onExit(e): + for n in set(w.network for w in windows.manager): + if n: + n.quit() + +def setupJoin(e): + if e.source == e.network.me: + window = windows.get(windows.StatusWindow, e.network, 'status', core) + + if window and not conf.get('status'): + window.mutate(windows.ChannelWindow, e.network, e.target) + else: + window = windows.new(windows.ChannelWindow, e.network, e.target, core) + + if e.requested: + window.activate() + + e.window = windows.get(windows.ChannelWindow, e.network, e.target, core) or e.window + +def setupText(e): + if e.target == e.network.me: + e.window = windows.new(windows.QueryWindow, e.network, e.source, core) + else: + e.window = \ + windows.get(windows.ChannelWindow, e.network, e.target, core) or \ + windows.get(windows.QueryWindow, e.network, e.source, core) or \ + e.window + +setupAction = setupText + +def setupNotice(e): + if e.target != e.network.me: + e.window = \ + windows.get(windows.ChannelWindow, e.network, e.target, core) or e.window + +def setupOwnText(e): + e.window = \ + windows.get(windows.ChannelWindow, e.network, e.target, core) or \ + windows.get(windows.QueryWindow, e.network, e.target, core) or \ + e.window + +setupOwnAction = setupOwnText + +def setdownPart(e): + if e.source == e.network.me: + window = windows.get(windows.ChannelWindow, e.network, e.target, core) + + if window: + cwindows = list(windows.get_with( + network=window.network, + wclass=windows.ChannelWindow + )) + + if len(cwindows) == 1 and not list(windows.get_with(network=window.network, wclass=windows.StatusWindow)): + window.mutate(windows.StatusWindow, e.network, 'status') + if e.requested: + window.activate() + elif e.requested: + window.close() + +def onClose(e): + nwindows = list(windows.get_with(core.manager, network=e.window.network)) + + if isinstance(e.window, windows.ChannelWindow): + cwindows = list(windows.get_with(core.manager, + network=e.window.network, + wclass=windows.ChannelWindow + )) + + #if we only have one window for the network, don't bother to part as + # we'll soon be quitting anyway + if len(nwindows) != 1 and irc_script.ischan(e.window.network, e.window.id): + e.window.network.part(e.window.id) + + if len(nwindows) == 1: + core.events.trigger("CloseNetwork", window=e.window, network=e.window.network) + + elif isinstance(e.window, windows.StatusWindow) and conf.get('status'): + core.events.trigger("CloseNetwork", window=e.window, network=e.window.network) + for window in nwindows: + if window != e.window: + window.close() + + if len(core.manager) == 1: + windows.new(windows.StatusWindow, irc.Network(), "status", core) + +def onConnecting(e): + return + window = windows.get_default(e.network) + if window: + window.update() + +onDisconnect = onConnecting + +def setupPart(e): + e.window = windows.get(windows.ChannelWindow, e.network, e.target, core) or e.window + +setupTopic = setupPart + +def setupKick(e): + e.window = windows.get(windows.ChannelWindow, e.network, e.channel, core) or e.window + +def setupMode(e): + if e.target != e.network.me: + e.window = windows.get(windows.ChannelWindow, e.network, e.target, core) or e.window + +def onWindowMenu(e): + if isinstance(e.window, windows.ChannelWindow): + e.channel = e.window.id + e.network = e.window.network + core.events.trigger('ChannelMenu', e) diff --git a/services/console/lib/purk/servers.py b/services/console/lib/purk/servers.py new file mode 100644 index 00000000..19ce23c1 --- /dev/null +++ b/services/console/lib/purk/servers.py @@ -0,0 +1,51 @@ +import gtk + +import windows +from conf import conf + +if 'networks' not in conf: + conf['networks'] = {} + +def server_get_data(network_info): + if 'port' in network_info: + return "%s:%s" % ( + network_info.get('server', '') , network_info.get('port') + ) + else: + return network_info.get('server', '') + +def server_set_data(text, network_info): + if ':' in text: + network_info['server'], port = text.rsplit(':',1) + network_info['port'] = int(port) + else: + network_info['server'] = text + network_info.pop('port', None) + +def channels_get_data(network_info): + return '\n'.join(network_info.get('join', ())) + +def channels_set_data(text, network_info): + network_info['join'] = [] + + for line in text.split('\n'): + for chan in line.split(','): + if chan: + network_info['join'].append(chan.strip()) + +def perform_get_data(network_info): + return '\n'.join(network_info.get('perform', ())) + +def perform_set_data(text, network_info): + network_info['perform'] = [line for line in text.split('\n') if line] + +def autoconnect_set_data(do_autoconnect, network): + if 'start_networks' not in conf: + conf['start_networks'] = [] + + # note (n in C) != w + if (network in conf.get('start_networks')) != do_autoconnect: + if do_autoconnect: + conf.get('start_networks').append(network) + else: + conf.get('start_networks').remove(network) diff --git a/services/console/lib/purk/ui.py b/services/console/lib/purk/ui.py new file mode 100644 index 00000000..6e1e28f1 --- /dev/null +++ b/services/console/lib/purk/ui.py @@ -0,0 +1,105 @@ +import sys +import os +import thread +import socket +import signal +import traceback + +import commands + +import gobject + +__sys_path = list(sys.path) +import gtk +sys.path = __sys_path + +import irc +from conf import conf + +import widgets +import windows + +# Running from same package dir +urkpath = os.path.dirname(__file__) + +def path(filename=""): + if filename: + return os.path.join(urkpath, filename) + else: + return urkpath + +# Priority Constants +PRIORITY_HIGH = gobject.PRIORITY_HIGH +PRIORITY_DEFAULT = gobject.PRIORITY_DEFAULT +PRIORITY_HIGH_IDLE = gobject.PRIORITY_HIGH_IDLE +PRIORITY_DEFAULT_IDLE = gobject.PRIORITY_DEFAULT_IDLE +PRIORITY_LOW = gobject.PRIORITY_LOW + + +if os.access(path('profile'),os.F_OK) or os.path.expanduser("~") == "~": + userpath = path('profile') + if not os.access(userpath,os.F_OK): + os.mkdir(userpath) + if not os.access(os.path.join(userpath,'scripts'),os.F_OK): + os.mkdir(os.path.join(userpath,'scripts')) +else: + userpath = os.path.join(os.path.expanduser("~"), ".urk") + if not os.access(userpath,os.F_OK): + os.mkdir(userpath, 0700) + if not os.access(os.path.join(userpath,'scripts'),os.F_OK): + os.mkdir(os.path.join(userpath,'scripts'), 0700) + + +def set_clipboard(text): + gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD).set_text(text) + gtk.clipboard_get(gtk.gdk.SELECTION_PRIMARY).set_text(text) + +class Source(object): + __slots__ = ['enabled'] + def __init__(self): + self.enabled = True + def unregister(self): + self.enabled = False + +class GtkSource(object): + __slots__ = ['tag'] + def __init__(self, tag): + self.tag = tag + def unregister(self): + gobject.source_remove(self.tag) + +def register_idle(f, *args, **kwargs): + priority = kwargs.pop("priority",PRIORITY_DEFAULT_IDLE) + def callback(): + return f(*args, **kwargs) + return GtkSource(gobject.idle_add(callback, priority=priority)) + +def register_timer(time, f, *args, **kwargs): + priority = kwargs.pop("priority",PRIORITY_DEFAULT_IDLE) + def callback(): + return f(*args, **kwargs) + return GtkSource(gobject.timeout_add(time, callback, priority=priority)) + +def fork(cb, f, *args, **kwargs): + is_stopped = Source() + def thread_func(): + try: + result, error = f(*args, **kwargs), None + except Exception, e: + result, error = None, e + + if is_stopped.enabled: + def callback(): + if is_stopped.enabled: + cb(result, error) + + gobject.idle_add(callback) + + thread.start_new_thread(thread_func, ()) + return is_stopped + +set_style = widgets.set_style + +def we_get_signal(*what): + gobject.idle_add(windows.manager.exit) + diff --git a/services/console/lib/purk/urk_trace.py b/services/console/lib/purk/urk_trace.py new file mode 100644 index 00000000..4b55b46d --- /dev/null +++ b/services/console/lib/purk/urk_trace.py @@ -0,0 +1,70 @@ +import sys +import commands +import linecache + +import time + +last_mem = [0] + +def traceit_memory(frame, event, arg): + if event == "line": + mem = int(" " + commands.getoutput( + "ps -eo cmd,rss | grep urk_trace.py | grep -v grep" + ).split(" ")[-1]) + + if mem > last_mem[0]: + last_mem[0] = mem + + mem = str(mem) + + filename = frame.f_globals["__file__"] + + if filename.endswith(".pyc") or filename.endswith(".pyo"): + filename = filename[:-1] + + name = frame.f_globals["__name__"] + + lineno = frame.f_lineno + line = linecache.getline(filename,lineno).rstrip() + + data = "%s:%i: %s" % (name, lineno, line) + + print "%s%s" % (data, mem.rjust(80 - len(data))) + + return traceit_memory + +lines = {} + +def traceit(frame, event, arg): + if event == "line": + try: + filename = frame.f_globals["__file__"] + + if filename.endswith(".pyc") or filename.endswith(".pyo"): + filename = filename[:-1] + + name = frame.f_globals["__name__"] + + lineno = frame.f_lineno + line = linecache.getline(filename,lineno).rstrip() + + data = "%s:%i: %s" % (name, lineno, line) + + print time.time(), data + + #if data in lines: + # lines[data] += 1 + #else: + # lines[data] = 1 + + except Exception, e: + print e + + return traceit + +def main(): + import urk + urk.main() + +sys.settrace(traceit) +main() diff --git a/services/console/lib/purk/widgets.py b/services/console/lib/purk/widgets.py new file mode 100644 index 00000000..1ad3cb81 --- /dev/null +++ b/services/console/lib/purk/widgets.py @@ -0,0 +1,811 @@ +import codecs + +import gobject +import gtk +import gtk.gdk +import pango + +from conf import conf +import parse_mirc +import windows + +import servers + +# Window activity Constants +HILIT = 'h' +TEXT ='t' +EVENT = 'e' + +ACTIVITY_MARKUP = { + HILIT: "%s", + TEXT: "%s", + EVENT: "%s", + } + +# This holds all tags for all windows ever +tag_table = gtk.TextTagTable() + +link_tag = gtk.TextTag('link') +link_tag.set_property('underline', pango.UNDERLINE_SINGLE) + +indent_tag = gtk.TextTag('indent') +indent_tag.set_property('indent', -20) + +tag_table.add(link_tag) +tag_table.add(indent_tag) + +#FIXME: MEH hates dictionaries, they remind him of the bad words +styles = {} + +def style_me(widget, style): + widget.set_style(styles.get(style)) + +def set_style(widget_name, style): + if style: + # FIXME: find a better way... + dummy = gtk.Label() + dummy.set_style(None) + + def apply_style_fg(value): + dummy.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse(value)) + + def apply_style_bg(value): + dummy.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse(value)) + + def apply_style_font(value): + dummy.modify_font(pango.FontDescription(value)) + + style_functions = ( + ('fg', apply_style_fg), + ('bg', apply_style_bg), + ('font', apply_style_font), + ) + + for name, f in style_functions: + if name in style: + f(style[name]) + + style = dummy.rc_get_style() + else: + style = None + + styles[widget_name] = style + +def menu_from_list(alist): + while alist and not alist[-1]: + alist.pop(-1) + + last = None + for item in alist: + if item != last: + if item: + if len(item) == 2: + name, function = item + + menuitem = gtk.ImageMenuItem(name) + + elif len(item) == 3: + name, stock_id, function = item + + if isinstance(stock_id, bool): + menuitem = gtk.CheckMenuItem(name) + menuitem.set_active(stock_id) + else: + menuitem = gtk.ImageMenuItem(stock_id) + + if isinstance(function, list): + submenu = gtk.Menu() + for subitem in menu_from_list(function): + submenu.append(subitem) + menuitem.set_submenu(submenu) + + else: + menuitem.connect("activate", lambda a, f: f(), function) + + yield menuitem + + else: + yield gtk.SeparatorMenuItem() + + last = item + +class Nicklist(gtk.TreeView): + def click(self, event): + if event.button == 3: + x, y = event.get_coords() + + (data,), path, x, y = self.get_path_at_pos(int(x), int(y)) + + c_data = self.events.data(window=self.win, data=self[data], menu=[]) + + self.events.trigger("ListRightClick", c_data) + + if c_data.menu: + menu = gtk.Menu() + for item in menu_from_list(c_data.menu): + menu.append(item) + menu.show_all() + menu.popup(None, None, None, event.button, event.time) + + elif event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS: + x, y = event.get_coords() + + (data,), path, x, y = self.get_path_at_pos(int(x), int(y)) + + self.events.trigger("ListDoubleClick", window=self.win, target=self[data]) + + def __getitem__(self, pos): + return self.get_model()[pos][0] + + def __setitem__(self, pos, name_markup): + realname, markedupname, sortkey = name_markup + + self.get_model()[pos] = realname, markedupname, sortkey + + def __len__(self): + return len(self.get_model()) + + def index(self, item): + for i, x in enumerate(self): + if x == item: + return i + + return -1 + + def append(self, realname, markedupname, sortkey): + self.get_model().append((realname, markedupname, sortkey)) + + def insert(self, pos, realname, markedupname, sortkey): + self.get_model().insert(pos, (realname, markedupname, sortkey)) + + def replace(self, names): + self.set_model(gtk.ListStore(str, str, str)) + + self.insert_column_with_attributes( + 0, '', gtk.CellRendererText(), markup=1 + ).set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + + for name in names: + self.append(*name) + + self.get_model().set_sort_column_id(2, gtk.SORT_ASCENDING) + + def remove(self, realname): + index = self.index(realname) + + if index == -1: + raise ValueError + + self.get_model().remove(self.get_model().iter_nth_child(None, index)) + + def clear(self): + self.get_model().clear() + + def __iter__(self): + return (r[0] for r in self.get_model()) + + def __init__(self, window, core): + self.win = window + self.core = core + self.events = core.events + + gtk.TreeView.__init__(self) + + self.replace(()) + + self.set_headers_visible(False) + self.set_property("fixed-height-mode", True) + self.connect("button-press-event", Nicklist.click) + self.connect_after("button-release-event", lambda *a: True) + + style_me(self, "nicklist") + +# Label used to display/edit your current nick on a network +class NickEditor(gtk.EventBox): + def nick_change(self, entry): + oldnick, newnick = self.label.get_text(), entry.get_text() + + if newnick and newnick != oldnick: + self.events.run('nick %s' % newnick, self.win, self.win.network) + + self.win.input.grab_focus() + + def update(self, nick=None): + self.label.set_text(nick or self.win.network.me) + + def to_edit_mode(self, widget, event): + if self.label not in self.get_children(): + return + + if getattr(event, 'button', None) == 3: + c_data = self.events.data(window=self.win, menu=[]) + self.events.trigger("NickEditMenu", c_data) + + if c_data.menu: + menu = gtk.Menu() + for item in menu_from_list(c_data.menu): + menu.append(item) + menu.show_all() + menu.popup(None, None, None, event.button, event.time) + + else: + entry = gtk.Entry() + entry.set_text(self.label.get_text()) + entry.connect('activate', self.nick_change) + entry.connect('focus-out-event', self.to_show_mode) + + self.remove(self.label) + self.add(entry) + self.window.set_cursor(None) + + entry.show() + entry.grab_focus() + + def to_show_mode(self, widget, event): + self.remove(widget) + self.add(self.label) + self.win.input.grab_focus() + self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM)) + + def __init__(self, window, core): + gtk.EventBox.__init__(self) + self.events = core.events + self.win = window + + self.label = gtk.Label() + self.label.set_padding(5, 0) + self.add(self.label) + + self.connect("button-press-event", self.to_edit_mode) + + self.update() + + self.connect( + "realize", + lambda *a: self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM)) + ) + +# The entry which you type in to send messages +class TextInput(gtk.Entry): + # Generates an input event + def entered_text(self, ctrl): + #for a in globals(): + # print a + #print events.__file__ + #self.core.run_command(self.text) + for line in self.text.splitlines(): + if line: + e_data = self.events.data( + window=self.win, network=self.win.network, + text=line, ctrl=ctrl + ) + self.events.trigger('Input', e_data) + if not e_data.done: + self.events.run(line, self.win, self.win.network) + + self.text = '' + + def _set_selection(self, s): + if s: + self.select_region(*s) + else: + self.select_region(self.cursor, self.cursor) + + #some nice toys for the scriptors + text = property(gtk.Entry.get_text, gtk.Entry.set_text) + cursor = property(gtk.Entry.get_position, gtk.Entry.set_position) + selection = property(gtk.Entry.get_selection_bounds, _set_selection) + + def insert(self, text): + self.do_insert_at_cursor(self, text) + + #hack to stop it selecting the text when we focus + def do_grab_focus(self): + temp = self.text, (self.selection or (self.cursor,)*2) + self.text = '' + gtk.Entry.do_grab_focus(self) + self.text, self.selection = temp + + def keypress(self, event): + keychar = ( + (gtk.gdk.CONTROL_MASK, '^'), + (gtk.gdk.SHIFT_MASK, '+'), + (gtk.gdk.MOD1_MASK, '!') + ) + + key = '' + for keymod, char in keychar: + # we make this an int, because otherwise it leaks + if int(event.state) & keymod: + key += char + key += gtk.gdk.keyval_name(event.keyval) + + self.events.trigger('KeyPress', key=key, string=event.string, window=self.win) + + if key == "^Return": + self.entered_text(True) + + up = gtk.gdk.keyval_from_name("Up") + down = gtk.gdk.keyval_from_name("Down") + tab = gtk.gdk.keyval_from_name("Tab") + + return event.keyval in (up, down, tab) + + def __init__(self, window, core): + gtk.Entry.__init__(self) + self.events = core.events + self.core = core + self.win = window + + # we don't want key events to propogate so we stop them in connect_after + self.connect('key-press-event', TextInput.keypress) + self.connect_after('key-press-event', lambda *a: True) + + self.connect('activate', TextInput.entered_text, False) + +gobject.type_register(TextInput) + +def prop_to_gtk(textview, (prop, val)): + if val == parse_mirc.BOLD: + val = pango.WEIGHT_BOLD + + elif val == parse_mirc.UNDERLINE: + val = pango.UNDERLINE_SINGLE + + return {prop: val} + +def word_from_pos(text, pos): + if text[pos] == ' ': + return ' ', pos, pos+1 + + else: + fr = text[:pos].split(" ")[-1] + to = text[pos:].split(" ")[0] + + return fr + to, pos - len(fr), pos + len(to) + +def get_iter_at_coords(view, x, y): + return view.get_iter_at_location( + *view.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, int(x), int(y)) + ) + +def get_event_at_iter(view, iter, core): + buffer = view.get_buffer() + + line_strt = buffer.get_iter_at_line(iter.get_line()) + line_end = line_strt.copy() + line_end.forward_lines(1) + + pos = iter.get_line_offset() + + #Caveat: text must be a unicode string, not utf-8 encoded; otherwise our + # offsets will be off when we use anything outside 7-bit ascii + #gtk.TextIter.get_text returns unicode but gtk.TextBuffer.get_text does not + text = line_strt.get_text(line_end).rstrip("\n") + + word, fr, to = word_from_pos(text, pos) + + return core.events.data( + window=view.win, pos=pos, text=text, + target=word, target_fr=fr, target_to=to, + ) + +class TextOutput(gtk.TextView): + def copy(self): + startend = self.get_buffer().get_selection_bounds() + + tagsandtext = [] + if startend: + start, end = startend + + while not start.equal(end): + tags_at_iter = {} + for tag in start.get_tags(): + try: + tagname, tagval = eval(tag.get_property('name')) + tags_at_iter[tagname] = tagval + except NameError: + continue + + tagsandtext.append((dict(tags_at_iter), start.get_char())) + start.forward_char() + + text = parse_mirc.unparse_mirc(tagsandtext) + + gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD).set_text(text) + gtk.clipboard_get(gtk.gdk.SELECTION_PRIMARY).set_text(text) + + return text + + def clear(self): + self.get_buffer().set_text('') + + def get_y(self): + rect = self.get_visible_rect() + return rect.y + + def set_y(self,y): + iter = self.get_iter_at_location(0, y) + if self.get_iter_location(iter).y < y: + self.forward_display_line(iter) + yalign = float(self.get_iter_location(iter).y-y)/self.height + self.scroll_to_iter(iter, 0, True, 0, yalign) + + self.check_autoscroll() + + def get_ymax(self): + buffer = self.get_buffer() + return sum(self.get_line_yrange(buffer.get_end_iter())) - self.height + + def get_height(self): + return self.get_visible_rect().height + + y = property(get_y, set_y) + ymax = property(get_ymax) + height = property(get_height) + + # the unknowing print weird things to our text widget function + def write(self, text, line_ending='\n', fg=None): + if not isinstance(text, unicode): + try: + text = codecs.utf_8_decode(text)[0] + except: + text = codecs.latin_1_decode(text)[0] + tags, text = parse_mirc.parse_mirc(text) + + if fg: + tags.append({'data': ("foreground", isinstance(fg, basestring) and ('#%s'%fg) or parse_mirc.get_mirc_color(fg)), 'from': 0, 'to': len(text)}) + + buffer = self.get_buffer() + + cc = buffer.get_char_count() + + buffer.insert_with_tags_by_name( + buffer.get_end_iter(), + text + line_ending, + 'indent' + ) + + for tag in tags: + tag_name = str(tag['data']) + + if not tag_table.lookup(tag_name): + buffer.create_tag(tag_name, **prop_to_gtk(self, tag['data'])) + + buffer.apply_tag_by_name( + tag_name, + buffer.get_iter_at_offset(tag['from'] + cc), + buffer.get_iter_at_offset(tag['to'] + cc) + ) + + def popup(self, menu): + hover_iter = get_iter_at_coords(self, *self.hover_coords) + + menuitems = [] + if not hover_iter.ends_line(): + c_data = get_event_at_iter(self, hover_iter) + c_data.menu = [] + + self.events.trigger("RightClick", c_data) + + menuitems = c_data.menu + + if not menuitems: + c_data = self.events.data(menu=[]) + self.events.trigger("MainMenu", c_data) + + menuitems = c_data.menu + + for child in menu.get_children(): + menu.remove(child) + + for item in menu_from_list(menuitems): + menu.append(item) + + menu.show_all() + + def mousedown(self, event): + if event.button == 3: + self.hover_coords = event.get_coords() + + def mouseup(self, event): + if not self.get_buffer().get_selection_bounds(): + if event.button == 1: + hover_iter = get_iter_at_coords(self, event.x, event.y) + + if not hover_iter.ends_line(): + c_data = get_event_at_iter(self, hover_iter, self.core) + + self.events.trigger("Click", c_data) + + if self.is_focus(): + self.win.focus() + + def clear_hover(self, _event=None): + buffer = self.get_buffer() + + for fr, to in self.linking: + buffer.remove_tag_by_name( + "link", + buffer.get_iter_at_mark(fr), + buffer.get_iter_at_mark(to) + ) + + self.linking = set() + self.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(None) + + def hover(self, event): + if self.linking: + self.clear_hover() + + hover_iter = get_iter_at_coords(self, event.x, event.y) + + if not hover_iter.ends_line(): + h_data = get_event_at_iter(self, hover_iter, self.core) + h_data.tolink = set() + + self.events.trigger("Hover", h_data) + + if h_data.tolink: + buffer = self.get_buffer() + + offset = buffer.get_iter_at_line( + hover_iter.get_line() + ).get_offset() + + for fr, to in h_data.tolink: + fr = buffer.get_iter_at_offset(offset + fr) + to = buffer.get_iter_at_offset(offset + to) + + buffer.apply_tag_by_name("link", fr, to) + + self.linking.add( + (buffer.create_mark(None, fr), + buffer.create_mark(None, to)) + ) + + self.get_window( + gtk.TEXT_WINDOW_TEXT + ).set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2)) + + self.get_pointer() + + def scroll(self, _allocation=None): + if self.autoscroll: + def do_scroll(): + self.scroller.value = self.scroller.upper - self.scroller.page_size + self._scrolling = False + + if not self._scrolling: + self._scrolling = gobject.idle_add(do_scroll) + + def check_autoscroll(self, *args): + def set_to_scroll(): + self.autoscroll = self.scroller.value + self.scroller.page_size >= self.scroller.upper + + gobject.idle_add(set_to_scroll) + + def __init__(self, core, window, buffer=None): + if not buffer: + buffer = gtk.TextBuffer(tag_table) + + gtk.TextView.__init__(self, buffer) + self.core = core + self.events = core.events + self.win = window + + self.set_size_request(0, -1) + + self.set_wrap_mode(gtk.WRAP_WORD_CHAR) + self.set_editable(False) + self.set_cursor_visible(False) + + self.set_property("left-margin", 3) + self.set_property("right-margin", 3) + + self.linking = set() + + self.add_events(gtk.gdk.POINTER_MOTION_HINT_MASK) + self.add_events(gtk.gdk.LEAVE_NOTIFY_MASK) + + self.connect('populate-popup', TextOutput.popup) + self.connect('motion-notify-event', TextOutput.hover) + self.connect('button-press-event', TextOutput.mousedown) + self.connect('button-release-event', TextOutput.mouseup) + self.connect_after('button-release-event', lambda *a: True) + self.connect('leave-notify-event', TextOutput.clear_hover) + + self.hover_coords = 0, 0 + + self.autoscroll = True + self._scrolling = False + self.scroller = gtk.Adjustment() + + def setup_scroll(self, _adj, vadj): + self.scroller = vadj + + if vadj: + def set_scroll(adj): + self.autoscroll = adj.value + adj.page_size >= adj.upper + + vadj.connect("value-changed", set_scroll) + + self.connect("set-scroll-adjustments", setup_scroll) + self.connect("size-allocate", TextOutput.scroll) + + def set_cursor(widget): + self.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(None) + + self.connect("realize", set_cursor) + + style_me(self, "view") + +class WindowLabel(gtk.EventBox): + def update(self): + title = self.win.get_title() + + for escapes in (('&','&'), ('<','<'), ('>','>')): + title = title.replace(*escapes) + + for a_type in (HILIT, TEXT, EVENT): + if a_type in self.win.activity: + title = ACTIVITY_MARKUP[a_type] % title + break + + self.label.set_markup(title) + + def tab_popup(self, event): + if event.button == 3: # right click + c_data = self.events.data(window=self.win, menu=[]) + self.events.trigger("WindowMenu", c_data) + + c_data.menu += [ + None, + ("Close", gtk.STOCK_CLOSE, self.win.close), + ] + + menu = gtk.Menu() + for item in menu_from_list(c_data.menu): + menu.append(item) + menu.show_all() + menu.popup(None, None, None, event.button, event.time) + + def __init__(self, window, core): + gtk.EventBox.__init__(self) + self.core = core + self.events = core.events + + self.win = window + self.connect("button-press-event", WindowLabel.tab_popup) + + self.label = gtk.Label() + self.add(self.label) + + self.update() + self.show_all() + +class FindBox(gtk.HBox): + def remove(self, *args): + self.parent.remove(self) + self.win.focus() + + def clicked(self, button, search_down=False): + text = self.textbox.get_text() + + if not text: + return + + buffer = self.win.output.get_buffer() + + if buffer.get_selection_bounds(): + if button == self.down: + _, cursor_iter = buffer.get_selection_bounds() + else: + cursor_iter, _ = buffer.get_selection_bounds() + else: + cursor_iter = buffer.get_end_iter() + + if search_down: + cursor = cursor_iter.forward_search( + text, gtk.TEXT_SEARCH_VISIBLE_ONLY + ) + else: + cursor = cursor_iter.backward_search( + text, gtk.TEXT_SEARCH_VISIBLE_ONLY + ) + + if not cursor: + return + + fr, to = cursor + + if button == self.up: + buffer.place_cursor(fr) + self.win.output.scroll_to_iter(fr, 0) + elif button == self.down: + buffer.place_cursor(to) + self.win.output.scroll_to_iter(to, 0) + + buffer.select_range(*cursor) + + cursor_iter = buffer.get_iter_at_mark(buffer.get_insert()) + + def __init__(self, window): + gtk.HBox.__init__(self) + + self.win = window + + self.up = gtk.Button(stock='gtk-go-up') + self.down = gtk.Button(stock='gtk-go-down') + + self.up.connect('clicked', self.clicked) + self.down.connect('clicked', self.clicked, True) + + self.up.set_property('can_focus', False) + self.down.set_property('can_focus', False) + + self.textbox = gtk.Entry() + + self.textbox.connect('focus-out-event', self.remove) + self.textbox.connect('activate', self.clicked) + + self.pack_start(gtk.Label('Find:'), expand=False) + self.pack_start(self.textbox) + + self.pack_start(self.up, expand=False) + self.pack_start(self.down, expand=False) + + self.show_all() + +#class UrkUITabs(gtk.Window): +class UrkUITabs(object): + def __init__(self, core): + # threading stuff + gtk.gdk.threads_init() + self.core = core + self.events = core.events + self.tabs = gtk.Notebook() + self.tabs.set_property( + "tab-pos", + conf.get("ui-gtk/tab-pos", gtk.POS_BOTTOM) + ) + + self.tabs.set_scrollable(True) + self.tabs.set_property("can-focus", False) + + self.box = gtk.VBox(False) + self.box.pack_end(self.tabs) + + def __iter__(self): + return iter(self.tabs.get_children()) + + def __len__(self): + return self.tabs.get_n_pages() + + def exit(self, *args): + self.events.trigger("Exit") + gtk.main_level() and gtk.main_quit() + + def get_active(self): + return self.tabs.get_nth_page(self.tabs.get_current_page()) + + def set_active(self, window): + self.tabs.set_current_page(self.tabs.page_num(window)) + + def add(self, window): + for pos in reversed(range(self.tabs.get_n_pages())): + if self.tabs.get_nth_page(pos).network == window.network: + break + else: + pos = self.tabs.get_n_pages() - 1 + + self.tabs.insert_page(window, WindowLabel(window, self.core), pos+1) + + def remove(self, window): + self.tabs.remove_page(self.tabs.page_num(window)) + + def update(self, window): + self.tabs.get_tab_label(window).update() + + def show_all(self): + self.box.show_all() diff --git a/services/console/lib/purk/windows.py b/services/console/lib/purk/windows.py new file mode 100644 index 00000000..22c783aa --- /dev/null +++ b/services/console/lib/purk/windows.py @@ -0,0 +1,298 @@ +import gtk + +import irc +from conf import conf +import widgets + +#manager = widgets.UrkUITabs() + +def append(window, manager): + manager.add(window) + +def remove(window, manager): + manager.remove(window) + + # i don't want to have to call this + window.destroy() + +def new(wclass, network, id, core): + if network is None: + network = irc.dummy_network + + w = get(wclass, network, id, core) + if not w: + w = wclass(network, id, core) + append(w, core.manager) + + return w + +def get(windowclass, network, id, core): + if network: + id = network.norm_case(id) + + for w in core.manager: + if (type(w), w.network, w.id) == (windowclass, network, id): + return w + +def get_with(manager, wclass=None, network=None, id=None): + if network and id: + id = network.norm_case(id) + + for w in list(manager): + for to_find, found in ((wclass, type(w)), (network, w.network), (id, w.id)): + # to_find might be False but not None (if it's a DummyNetwork) + if to_find is not None and to_find != found: + break + else: + yield w + +def get_default(network, manager): + + window = manager.get_active() + if window.network == network: + return window + + # There can be only one... + for window in get_with(network=network): + return window + +class Window(gtk.VBox): + need_vbox_init = True + + def mutate(self, newclass, network, id): + isactive = self == manager.get_active() + self.hide() + + for child in self.get_children(): + self.remove(child) + + self.__class__ = newclass + self.__init__(network, id) + self.update() + if isactive: + self.activate() + + def transfer_text(self, _widget, event): + if event.string and not self.input.is_focus(): + self.input.grab_focus() + self.input.set_position(-1) + self.input.event(event) + + def write(self, text, activity_type=widgets.EVENT, line_ending='\n', fg=None): + if self.manager.get_active() != self: + self.activity = activity_type + self.output.write(text, line_ending, fg) + + def get_id(self): + if self.network: + return self.network.norm_case(self.rawid) + else: + return self.rawid + + def set_id(self, id): + self.rawid = id + self.update() + + id = property(get_id, set_id) + + def get_toplevel_title(self): + return self.rawid + + def get_title(self): + return self.rawid + + def get_activity(self): + return self.__activity + + def set_activity(self, value): + if value: + self.__activity.add(value) + else: + self.__activity = set() + self.update() + + activity = property(get_activity, set_activity) + + def focus(self): + pass + + def activate(self): + self.manager.set_active(self) + self.focus() + + def close(self): + self.events.trigger("Close", window=self) + remove(self, self.manager) + + def update(self): + self.manager.update(self) + + def __init__(self, network, id, core): + self.manager = core.manager + self.events = core.events + + if self.need_vbox_init: + #make sure we don't call this an extra time when mutating + gtk.VBox.__init__(self, False) + self.need_vbox_init = False + + if hasattr(self, "buffer"): + self.output = widgets.TextOutput(core, self, self.buffer) + else: + self.output = widgets.TextOutput(core, self) + self.buffer = self.output.get_buffer() + + if hasattr(self, "input"): + if self.input.parent: + self.input.parent.remove(self.input) + else: + self.input = widgets.TextInput(self, core) + + self.network = network + self.rawid = id + + self.__activity = set() + +class SimpleWindow(Window): + def __init__(self, network, id, core): + Window.__init__(self, network, id) + + self.focus = self.input.grab_focus + self.connect("key-press-event", self.transfer_text) + + self.pack_end(self.input, expand=False) + + topbox = gtk.ScrolledWindow() + topbox.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + topbox.add(self.output) + + self.pack_end(topbox) + + self.show_all() + +class StatusWindow(Window): + def get_toplevel_title(self): + return '%s - %s' % (self.network.me, self.get_title()) + + def get_title(self): + # Something about self.network.isupport + if self.network.status: + return "%s" % self.network.server + else: + return "[%s]" % self.network.server + + def __init__(self, network, id, core): + Window.__init__(self, network, id, core) + + self.nick_label = widgets.NickEditor(self, core) + + self.focus = self.input.grab_focus + self.connect("key-press-event", self.transfer_text) + self.manager = core.manager + botbox = gtk.HBox() + botbox.pack_start(self.input) + botbox.pack_end(self.nick_label, expand=False) + + self.pack_end(botbox, expand=False) + + topbox = gtk.ScrolledWindow() + topbox.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + topbox.add(self.output) + + self.pack_end(topbox) + + self.show_all() + +class QueryWindow(Window): + def __init__(self, network, id, core): + Window.__init__(self, network, id, core) + + self.nick_label = widgets.NickEditor(self, core) + + self.focus = self.input.grab_focus + self.connect("key-press-event", self.transfer_text) + + botbox = gtk.HBox() + botbox.pack_start(self.input) + botbox.pack_end(self.nick_label, expand=False) + + self.pack_end(botbox, expand=False) + + topbox = gtk.ScrolledWindow() + topbox.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + topbox.add(self.output) + + self.pack_end(topbox) + + self.show_all() + +def move_nicklist(paned, event): + paned._moving = ( + event.type == gtk.gdk._2BUTTON_PRESS, + paned.get_position() + ) + +def drop_nicklist(paned, event): + width = paned.allocation.width + pos = paned.get_position() + + double_click, nicklist_pos = paned._moving + + if double_click: + # if we're "hidden", then we want to unhide + if width - pos <= 10: + # get the normal nicklist width + conf_nicklist = conf.get("ui-gtk/nicklist-width", 200) + + # if the normal nicklist width is "hidden", then ignore it + if conf_nicklist <= 10: + paned.set_position(width - 200) + else: + paned.set_position(width - conf_nicklist) + + # else we hide + else: + paned.set_position(width) + + else: + if pos != nicklist_pos: + conf["ui-gtk/nicklist-width"] = width - pos - 6 + +class ChannelWindow(Window): + def __init__(self, network, id, core): + Window.__init__(self, network, id, core) + + self.nicklist = widgets.Nicklist(self, core) + self.nick_label = widgets.NickEditor(self, core) + + self.focus = self.input.grab_focus + self.connect("key-press-event", self.transfer_text) + + topbox = gtk.ScrolledWindow() + topbox.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + topbox.add(self.output) + + nlbox = gtk.ScrolledWindow() + nlbox.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + nlbox.add(self.nicklist) + + nlbox.set_size_request(conf.get("ui-gtk/nicklist-width", 112), -1) + + botbox = gtk.HBox() + botbox.pack_start(self.input) + botbox.pack_end(self.nick_label, expand=False) + + self.pack_end(botbox, expand=False) + + pane = gtk.HPaned() + pane.pack1(topbox, resize=True, shrink=False) + pane.pack2(nlbox, resize=False, shrink=True) + + self.nicklist.pos = None + + pane.connect("button-press-event", move_nicklist) + pane.connect("button-release-event", drop_nicklist) + + self.pack_end(pane) + + self.show_all()