Mercurial > hg > octave-nkf
changeset 13357:f9fbf8954d7d
Added qtermwidget files.
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/gui//AUTHORS @@ -0,0 +1,1 @@ +e_k@users.sourceforge.net \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/gui//BlockArray.cpp @@ -0,0 +1,337 @@ +/* + This file is part of Konsole, an X terminal. + Copyright (C) 2000 by Stephan Kulow <coolo@kde.org> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. + +*/ + +// Own +#include "BlockArray.h" + +#include <QtCore> + +// System +#include <assert.h> +#include <sys/mman.h> +#include <sys/param.h> +#include <unistd.h> +#include <stdio.h> + + +using namespace Konsole; + +static int blocksize = 0; + +BlockArray::BlockArray() + : size(0), + current(size_t(-1)), + index(size_t(-1)), + lastmap(0), + lastmap_index(size_t(-1)), + lastblock(0), ion(-1), + length(0) +{ + // lastmap_index = index = current = size_t(-1); + if (blocksize == 0) + blocksize = ((sizeof(Block) / getpagesize()) + 1) * getpagesize(); + +} + +BlockArray::~BlockArray() +{ + setHistorySize(0); + assert(!lastblock); +} + +size_t BlockArray::append(Block *block) +{ + if (!size) + return size_t(-1); + + ++current; + if (current >= size) current = 0; + + int rc; + rc = lseek(ion, current * blocksize, SEEK_SET); if (rc < 0) { perror("HistoryBuffer::add.seek"); setHistorySize(0); return size_t(-1); } + rc = write(ion, block, blocksize); if (rc < 0) { perror("HistoryBuffer::add.write"); setHistorySize(0); return size_t(-1); } + + length++; + if (length > size) length = size; + + ++index; + + delete block; + return current; +} + +size_t BlockArray::newBlock() +{ + if (!size) + return size_t(-1); + append(lastblock); + + lastblock = new Block(); + return index + 1; +} + +Block *BlockArray::lastBlock() const +{ + return lastblock; +} + +bool BlockArray::has(size_t i) const +{ + if (i == index + 1) + return true; + + if (i > index) + return false; + if (index - i >= length) + return false; + return true; +} + +const Block* BlockArray::at(size_t i) +{ + if (i == index + 1) + return lastblock; + + if (i == lastmap_index) + return lastmap; + + if (i > index) { + qDebug() << "BlockArray::at() i > index\n"; + return 0; + } + +// if (index - i >= length) { +// kDebug(1211) << "BlockArray::at() index - i >= length\n"; +// return 0; +// } + + size_t j = i; // (current - (index - i) + (index/size+1)*size) % size ; + + assert(j < size); + unmap(); + + Block *block = (Block*)mmap(0, blocksize, PROT_READ, MAP_PRIVATE, ion, j * blocksize); + + if (block == (Block*)-1) { perror("mmap"); return 0; } + + lastmap = block; + lastmap_index = i; + + return block; +} + +void BlockArray::unmap() +{ + if (lastmap) { + int res = munmap((char*)lastmap, blocksize); + if (res < 0) perror("munmap"); + } + lastmap = 0; + lastmap_index = size_t(-1); +} + +bool BlockArray::setSize(size_t newsize) +{ + return setHistorySize(newsize * 1024 / blocksize); +} + +bool BlockArray::setHistorySize(size_t newsize) +{ +// kDebug(1211) << "setHistorySize " << size << " " << newsize; + + if (size == newsize) + return false; + + unmap(); + + if (!newsize) { + delete lastblock; + lastblock = 0; + if (ion >= 0) close(ion); + ion = -1; + current = size_t(-1); + return true; + } + + if (!size) { + FILE* tmp = tmpfile(); + if (!tmp) { + perror("konsole: cannot open temp file.\n"); + } else { + ion = dup(fileno(tmp)); + if (ion<0) { + perror("konsole: cannot dup temp file.\n"); + fclose(tmp); + } + } + if (ion < 0) + return false; + + assert(!lastblock); + + lastblock = new Block(); + size = newsize; + return false; + } + + if (newsize > size) { + increaseBuffer(); + size = newsize; + return false; + } else { + decreaseBuffer(newsize); + ftruncate(ion, length*blocksize); + size = newsize; + + return true; + } +} + +void moveBlock(FILE *fion, int cursor, int newpos, char *buffer2) +{ + int res = fseek(fion, cursor * blocksize, SEEK_SET); + if (res) + perror("fseek"); + res = fread(buffer2, blocksize, 1, fion); + if (res != 1) + perror("fread"); + + res = fseek(fion, newpos * blocksize, SEEK_SET); + if (res) + perror("fseek"); + res = fwrite(buffer2, blocksize, 1, fion); + if (res != 1) + perror("fwrite"); + // printf("moving block %d to %d\n", cursor, newpos); +} + +void BlockArray::decreaseBuffer(size_t newsize) +{ + if (index < newsize) // still fits in whole + return; + + int offset = (current - (newsize - 1) + size) % size; + + if (!offset) + return; + + // The Block constructor could do somthing in future... + char *buffer1 = new char[blocksize]; + + FILE *fion = fdopen(dup(ion), "w+b"); + if (!fion) { + delete [] buffer1; + perror("fdopen/dup"); + return; + } + + int firstblock; + if (current <= newsize) { + firstblock = current + 1; + } else { + firstblock = 0; + } + + size_t oldpos; + for (size_t i = 0, cursor=firstblock; i < newsize; i++) { + oldpos = (size + cursor + offset) % size; + moveBlock(fion, oldpos, cursor, buffer1); + if (oldpos < newsize) { + cursor = oldpos; + } else + cursor++; + } + + current = newsize - 1; + length = newsize; + + delete [] buffer1; + + fclose(fion); + +} + +void BlockArray::increaseBuffer() +{ + if (index < size) // not even wrapped once + return; + + int offset = (current + size + 1) % size; + if (!offset) // no moving needed + return; + + // The Block constructor could do somthing in future... + char *buffer1 = new char[blocksize]; + char *buffer2 = new char[blocksize]; + + int runs = 1; + int bpr = size; // blocks per run + + if (size % offset == 0) { + bpr = size / offset; + runs = offset; + } + + FILE *fion = fdopen(dup(ion), "w+b"); + if (!fion) { + perror("fdopen/dup"); + delete [] buffer1; + delete [] buffer2; + return; + } + + int res; + for (int i = 0; i < runs; i++) + { + // free one block in chain + int firstblock = (offset + i) % size; + res = fseek(fion, firstblock * blocksize, SEEK_SET); + if (res) + perror("fseek"); + res = fread(buffer1, blocksize, 1, fion); + if (res != 1) + perror("fread"); + int newpos = 0; + for (int j = 1, cursor=firstblock; j < bpr; j++) + { + cursor = (cursor + offset) % size; + newpos = (cursor - offset + size) % size; + moveBlock(fion, cursor, newpos, buffer2); + } + res = fseek(fion, i * blocksize, SEEK_SET); + if (res) + perror("fseek"); + res = fwrite(buffer1, blocksize, 1, fion); + if (res != 1) + perror("fwrite"); + } + current = size - 1; + length = size; + + delete [] buffer1; + delete [] buffer2; + + fclose(fion); + +} +
new file mode 100644 --- /dev/null +++ b/gui//BlockArray.h @@ -0,0 +1,125 @@ +/* + This file is part of Konsole, an X terminal. + Copyright (C) 2000 by Stephan Kulow <coolo@kde.org> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef BLOCKARRAY_H +#define BLOCKARRAY_H + +#include <unistd.h> + +//#error Do not use in KDE 2.1 + +#define BlockSize (1 << 12) +#define ENTRIES ((BlockSize - sizeof(size_t) ) / sizeof(unsigned char)) + +namespace Konsole +{ + +struct Block { + Block() { size = 0; } + unsigned char data[ENTRIES]; + size_t size; +}; + +// /////////////////////////////////////////////////////// + +class BlockArray { +public: + /** + * Creates a history file for holding + * maximal size blocks. If more blocks + * are requested, then it drops earlier + * added ones. + */ + BlockArray(); + + /// destructor + ~BlockArray(); + + /** + * adds the Block at the end of history. + * This may drop other blocks. + * + * The ownership on the block is transfered. + * An unique index number is returned for accessing + * it later (if not yet dropped then) + * + * Note, that the block may be dropped completely + * if history is turned off. + */ + size_t append(Block *block); + + /** + * gets the block at the index. Function may return + * 0 if the block isn't available any more. + * + * The returned block is strictly readonly as only + * maped in memory - and will be invalid on the next + * operation on this class. + */ + const Block *at(size_t index); + + /** + * reorders blocks as needed. If newsize is null, + * the history is emptied completely. The indices + * returned on append won't change their semantic, + * but they may not be valid after this call. + */ + bool setHistorySize(size_t newsize); + + size_t newBlock(); + + Block *lastBlock() const; + + /** + * Convenient function to set the size in KBytes + * instead of blocks + */ + bool setSize(size_t newsize); + + size_t len() const { return length; } + + bool has(size_t index) const; + + size_t getCurrent() const { return current; } + +private: + void unmap(); + void increaseBuffer(); + void decreaseBuffer(size_t newsize); + + size_t size; + // current always shows to the last inserted block + size_t current; + size_t index; + + Block *lastmap; + size_t lastmap_index; + Block *lastblock; + + int ion; + size_t length; + +}; + +} + +#endif
new file mode 100644 --- /dev/null +++ b/gui//COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 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. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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. + + <signature of Ty Coon>, 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.
new file mode 100644 --- /dev/null +++ b/gui//Changelog @@ -0,0 +1,19 @@ +31.07.2008 +Interface class from c-style conversions rewritten with pimpl support. + + +16.07.2008 +Added optional scrollbar + + +06.06.2008 +Some artefacts were removed, some added... +Also added support for color schemes, and 3 color schemes provided (classical - white on black, green on black, black on light yellow). Is it enough or not? + + +26.05.2008 +Added file release as an archive with source code. But preferrable way is still getting code from CVS, cause file release can be outdated. + + +11.05.2008 +Initial CVS import - first version comes with number 0.0.1 \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/gui//Character.h @@ -0,0 +1,210 @@ +/* + This file is part of Konsole, KDE's terminal. + + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef CHARACTER_H +#define CHARACTER_H + +// Qt +#include <QtCore/QHash> + +// Local +#include "CharacterColor.h" + +namespace Konsole +{ + +typedef unsigned char LineProperty; + +static const int LINE_DEFAULT = 0; +static const int LINE_WRAPPED = (1 << 0); +static const int LINE_DOUBLEWIDTH = (1 << 1); +static const int LINE_DOUBLEHEIGHT = (1 << 2); + +#define DEFAULT_RENDITION 0 +#define RE_BOLD (1 << 0) +#define RE_BLINK (1 << 1) +#define RE_UNDERLINE (1 << 2) +#define RE_REVERSE (1 << 3) // Screen only +#define RE_INTENSIVE (1 << 3) // Widget only +#define RE_CURSOR (1 << 4) +#define RE_EXTENDED_CHAR (1 << 5) + +/** + * A single character in the terminal which consists of a unicode character + * value, foreground and background colors and a set of rendition attributes + * which specify how it should be drawn. + */ +class Character +{ +public: + /** + * Constructs a new character. + * + * @param _c The unicode character value of this character. + * @param _f The foreground color used to draw the character. + * @param _b The color used to draw the character's background. + * @param _r A set of rendition flags which specify how this character is to be drawn. + */ + inline Character(quint16 _c = ' ', + CharacterColor _f = CharacterColor(COLOR_SPACE_DEFAULT,DEFAULT_FORE_COLOR), + CharacterColor _b = CharacterColor(COLOR_SPACE_DEFAULT,DEFAULT_BACK_COLOR), + quint8 _r = DEFAULT_RENDITION) + : character(_c), rendition(_r), foregroundColor(_f), backgroundColor(_b) {} + + union + { + /** The unicode character value for this character. */ + quint16 character; + /** + * Experimental addition which allows a single Character instance to contain more than + * one unicode character. + * + * charSequence is a hash code which can be used to look up the unicode + * character sequence in the ExtendedCharTable used to create the sequence. + */ + quint16 charSequence; + }; + + /** A combination of RENDITION flags which specify options for drawing the character. */ + quint8 rendition; + + /** The foreground color used to draw this character. */ + CharacterColor foregroundColor; + /** The color used to draw this character's background. */ + CharacterColor backgroundColor; + + /** + * Returns true if this character has a transparent background when + * it is drawn with the specified @p palette. + */ + bool isTransparent(const ColorEntry* palette) const; + /** + * Returns true if this character should always be drawn in bold when + * it is drawn with the specified @p palette, independent of whether + * or not the character has the RE_BOLD rendition flag. + */ + bool isBold(const ColorEntry* base) const; + + /** + * Compares two characters and returns true if they have the same unicode character value, + * rendition and colors. + */ + friend bool operator == (const Character& a, const Character& b); + /** + * Compares two characters and returns true if they have different unicode character values, + * renditions or colors. + */ + friend bool operator != (const Character& a, const Character& b); +}; + +inline bool operator == (const Character& a, const Character& b) +{ + return a.character == b.character && + a.rendition == b.rendition && + a.foregroundColor == b.foregroundColor && + a.backgroundColor == b.backgroundColor; +} + +inline bool operator != (const Character& a, const Character& b) +{ + return a.character != b.character || + a.rendition != b.rendition || + a.foregroundColor != b.foregroundColor || + a.backgroundColor != b.backgroundColor; +} + +inline bool Character::isTransparent(const ColorEntry* base) const +{ + return ((backgroundColor._colorSpace == COLOR_SPACE_DEFAULT) && + base[backgroundColor._u+0+(backgroundColor._v?BASE_COLORS:0)].transparent) + || ((backgroundColor._colorSpace == COLOR_SPACE_SYSTEM) && + base[backgroundColor._u+2+(backgroundColor._v?BASE_COLORS:0)].transparent); +} + +inline bool Character::isBold(const ColorEntry* base) const +{ + return ((backgroundColor._colorSpace == COLOR_SPACE_DEFAULT) && + base[backgroundColor._u+0+(backgroundColor._v?BASE_COLORS:0)].bold) + || ((backgroundColor._colorSpace == COLOR_SPACE_SYSTEM) && + base[backgroundColor._u+2+(backgroundColor._v?BASE_COLORS:0)].bold); +} + +extern unsigned short vt100_graphics[32]; + + +/** + * A table which stores sequences of unicode characters, referenced + * by hash keys. The hash key itself is the same size as a unicode + * character ( ushort ) so that it can occupy the same space in + * a structure. + */ +class ExtendedCharTable +{ +public: + /** Constructs a new character table. */ + ExtendedCharTable(); + ~ExtendedCharTable(); + + /** + * Adds a sequences of unicode characters to the table and returns + * a hash code which can be used later to look up the sequence + * using lookupExtendedChar() + * + * If the same sequence already exists in the table, the hash + * of the existing sequence will be returned. + * + * @param unicodePoints An array of unicode character points + * @param length Length of @p unicodePoints + */ + ushort createExtendedChar(ushort* unicodePoints , ushort length); + /** + * Looks up and returns a pointer to a sequence of unicode characters + * which was added to the table using createExtendedChar(). + * + * @param hash The hash key returned by createExtendedChar() + * @param length This variable is set to the length of the + * character sequence. + * + * @return A unicode character sequence of size @p length. + */ + ushort* lookupExtendedChar(ushort hash , ushort& length) const; + + /** The global ExtendedCharTable instance. */ + static ExtendedCharTable instance; +private: + // calculates the hash key of a sequence of unicode points of size 'length' + ushort extendedCharHash(ushort* unicodePoints , ushort length) const; + // tests whether the entry in the table specified by 'hash' matches the + // character sequence 'unicodePoints' of size 'length' + bool extendedCharMatch(ushort hash , ushort* unicodePoints , ushort length) const; + // internal, maps hash keys to character sequence buffers. The first ushort + // in each value is the length of the buffer, followed by the ushorts in the buffer + // themselves. + QHash<ushort,ushort*> extendedCharTable; +}; + +} + +#endif // CHARACTER_H +
new file mode 100644 --- /dev/null +++ b/gui//CharacterColor.h @@ -0,0 +1,301 @@ +/* + This file is part of Konsole, KDE's terminal. + + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef CHARACTERCOLOR_H +#define CHARACTERCOLOR_H + +// Qt +#include <QtGui/QColor> + +namespace Konsole +{ + +/** + * An entry in a terminal display's color palette. + * + * A color palette is an array of 16 ColorEntry instances which map + * system color indexes (from 0 to 15) into actual colors. + * + * Each entry can be set as bold, in which case any text + * drawn using the color should be drawn in bold. + * + * Each entry can also be transparent, in which case the terminal + * display should avoid drawing the background for any characters + * using the entry as a background. + */ +class ColorEntry +{ +public: + /** + * Constructs a new color palette entry. + * + * @param c The color value for this entry. + * @param tr Specifies that the color should be transparent when used as a background color. + * @param b Specifies that text drawn with this color should be bold. + */ + ColorEntry(QColor c, bool tr, bool b) : color(c), transparent(tr), bold(b) {} + + /** + * Constructs a new color palette entry with an undefined color, and + * with the transparent and bold flags set to false. + */ + ColorEntry() : transparent(false), bold(false) {} + + /** + * Sets the color, transparency and boldness of this color to those of @p rhs. + */ + void operator=(const ColorEntry& rhs) + { + color = rhs.color; + transparent = rhs.transparent; + bold = rhs.bold; + } + + /** The color value of this entry for display. */ + QColor color; + + /** + * If true character backgrounds using this color should be transparent. + * This is not applicable when the color is used to render text. + */ + bool transparent; + /** + * If true characters drawn using this color should be bold. + * This is not applicable when the color is used to draw a character's background. + */ + bool bold; +}; + + +// Attributed Character Representations /////////////////////////////// + +// Colors + +#define BASE_COLORS (2+8) +#define INTENSITIES 2 +#define TABLE_COLORS (INTENSITIES*BASE_COLORS) + +#define DEFAULT_FORE_COLOR 0 +#define DEFAULT_BACK_COLOR 1 + +//a standard set of colors using black text on a white background. +//defined in TerminalDisplay.cpp + +static const ColorEntry base_color_table[TABLE_COLORS] = +// The following are almost IBM standard color codes, with some slight +// gamma correction for the dim colors to compensate for bright X screens. +// It contains the 8 ansiterm/xterm colors in 2 intensities. +{ + // Fixme: could add faint colors here, also. + // normal + ColorEntry(QColor(0x00,0x00,0x00), 0, 0 ), ColorEntry( QColor(0xB2,0xB2,0xB2), 1, 0 ), // Dfore, Dback + ColorEntry(QColor(0x00,0x00,0x00), 0, 0 ), ColorEntry( QColor(0xB2,0x18,0x18), 0, 0 ), // Black, Red + ColorEntry(QColor(0x18,0xB2,0x18), 0, 0 ), ColorEntry( QColor(0xB2,0x68,0x18), 0, 0 ), // Green, Yellow + ColorEntry(QColor(0x18,0x18,0xB2), 0, 0 ), ColorEntry( QColor(0xB2,0x18,0xB2), 0, 0 ), // Blue, Magenta + ColorEntry(QColor(0x18,0xB2,0xB2), 0, 0 ), ColorEntry( QColor(0xB2,0xB2,0xB2), 0, 0 ), // Cyan, White + // intensiv + ColorEntry(QColor(0x00,0x00,0x00), 0, 1 ), ColorEntry( QColor(0xFF,0xFF,0xFF), 1, 0 ), + ColorEntry(QColor(0x68,0x68,0x68), 0, 0 ), ColorEntry( QColor(0xFF,0x54,0x54), 0, 0 ), + ColorEntry(QColor(0x54,0xFF,0x54), 0, 0 ), ColorEntry( QColor(0xFF,0xFF,0x54), 0, 0 ), + ColorEntry(QColor(0x54,0x54,0xFF), 0, 0 ), ColorEntry( QColor(0xFF,0x54,0xFF), 0, 0 ), + ColorEntry(QColor(0x54,0xFF,0xFF), 0, 0 ), ColorEntry( QColor(0xFF,0xFF,0xFF), 0, 0 ) +}; + +/* CharacterColor is a union of the various color spaces. + + Assignment is as follows: + + Type - Space - Values + + 0 - Undefined - u: 0, v:0 w:0 + 1 - Default - u: 0..1 v:intense w:0 + 2 - System - u: 0..7 v:intense w:0 + 3 - Index(256) - u: 16..255 v:0 w:0 + 4 - RGB - u: 0..255 v:0..256 w:0..256 + + Default colour space has two separate colours, namely + default foreground and default background colour. +*/ + +#define COLOR_SPACE_UNDEFINED 0 +#define COLOR_SPACE_DEFAULT 1 +#define COLOR_SPACE_SYSTEM 2 +#define COLOR_SPACE_256 3 +#define COLOR_SPACE_RGB 4 + +/** + * Describes the color of a single character in the terminal. + */ +class CharacterColor +{ + friend class Character; + +public: + /** Constructs a new CharacterColor whoose color and color space are undefined. */ + CharacterColor() + : _colorSpace(COLOR_SPACE_UNDEFINED), + _u(0), + _v(0), + _w(0) + {} + + /** + * Constructs a new CharacterColor using the specified @p colorSpace and with + * color value @p co + * + * The meaning of @p co depends on the @p colorSpace used. + * + * TODO : Document how @p co relates to @p colorSpace + * + * TODO : Add documentation about available color spaces. + */ + CharacterColor(quint8 colorSpace, int co) + : _colorSpace(colorSpace), + _u(0), + _v(0), + _w(0) + { + switch (colorSpace) + { + case COLOR_SPACE_DEFAULT: + _u = co & 1; + break; + case COLOR_SPACE_SYSTEM: + _u = co & 7; + _v = (co >> 3) & 1; + break; + case COLOR_SPACE_256: + _u = co & 255; + break; + case COLOR_SPACE_RGB: + _u = co >> 16; + _v = co >> 8; + _w = co; + break; + default: + _colorSpace = COLOR_SPACE_UNDEFINED; + } + } + + /** + * Returns true if this character color entry is valid. + */ + bool isValid() + { + return _colorSpace != COLOR_SPACE_UNDEFINED; + } + + /** + * Toggles the value of this color between a normal system color and the corresponding intensive + * system color. + * + * This is only applicable if the color is using the COLOR_SPACE_DEFAULT or COLOR_SPACE_SYSTEM + * color spaces. + */ + void toggleIntensive(); + + /** + * Returns the color within the specified color @palette + * + * The @p palette is only used if this color is one of the 16 system colors, otherwise + * it is ignored. + */ + QColor color(const ColorEntry* palette) const; + + /** + * Compares two colors and returns true if they represent the same color value and + * use the same color space. + */ + friend bool operator == (const CharacterColor& a, const CharacterColor& b); + /** + * Compares two colors and returns true if they represent different color values + * or use different color spaces. + */ + friend bool operator != (const CharacterColor& a, const CharacterColor& b); + +private: + quint8 _colorSpace; + + // bytes storing the character color + quint8 _u; + quint8 _v; + quint8 _w; +}; + +inline bool operator == (const CharacterColor& a, const CharacterColor& b) +{ + return *reinterpret_cast<const quint32*>(&a._colorSpace) == + *reinterpret_cast<const quint32*>(&b._colorSpace); +} + +inline bool operator != (const CharacterColor& a, const CharacterColor& b) +{ + return *reinterpret_cast<const quint32*>(&a._colorSpace) != + *reinterpret_cast<const quint32*>(&b._colorSpace); +} + +inline const QColor color256(quint8 u, const ColorEntry* base) +{ + // 0.. 16: system colors + if (u < 8) return base[u+2 ].color; u -= 8; + if (u < 8) return base[u+2+BASE_COLORS].color; u -= 8; + + // 16..231: 6x6x6 rgb color cube + if (u < 216) return QColor(255*((u/36)%6)/5, + 255*((u/ 6)%6)/5, + 255*((u/ 1)%6)/5); u -= 216; + + // 232..255: gray, leaving out black and white + int gray = u*10+8; return QColor(gray,gray,gray); +} + +inline QColor CharacterColor::color(const ColorEntry* base) const +{ + switch (_colorSpace) + { + case COLOR_SPACE_DEFAULT: return base[_u+0+(_v?BASE_COLORS:0)].color; + case COLOR_SPACE_SYSTEM: return base[_u+2+(_v?BASE_COLORS:0)].color; + case COLOR_SPACE_256: return color256(_u,base); + case COLOR_SPACE_RGB: return QColor(_u,_v,_w); + case COLOR_SPACE_UNDEFINED: return QColor(); + } + + Q_ASSERT(false); // invalid color space + + return QColor(); +} + +inline void CharacterColor::toggleIntensive() +{ + if (_colorSpace == COLOR_SPACE_SYSTEM || _colorSpace == COLOR_SPACE_DEFAULT) + { + _v = !_v; + } +} + + +} + +#endif // CHARACTERCOLOR_H +
new file mode 100644 --- /dev/null +++ b/gui//ColorTables.h @@ -0,0 +1,58 @@ +#ifndef _COLOR_TABLE_H +#define _COLOR_TABLE_H + +#include "CharacterColor.h" + +using namespace Konsole; + +static const ColorEntry whiteonblack_color_table[TABLE_COLORS] = +{ + // normal + ColorEntry(QColor(0xFF,0xFF,0xFF), 0, 0 ), ColorEntry( QColor(0x00,0x00,0x00), 1, 0 ), // Dfore, Dback + ColorEntry(QColor(0x00,0x00,0x00), 0, 0 ), ColorEntry( QColor(0xB2,0x18,0x18), 0, 0 ), // Black, Red + ColorEntry(QColor(0x18,0xB2,0x18), 0, 0 ), ColorEntry( QColor(0xB2,0x68,0x18), 0, 0 ), // Green, Yellow + ColorEntry(QColor(0x18,0x18,0xB2), 0, 0 ), ColorEntry( QColor(0xB2,0x18,0xB2), 0, 0 ), // Blue, Magenta + ColorEntry(QColor(0x18,0xB2,0xB2), 0, 0 ), ColorEntry( QColor(0xB2,0xB2,0xB2), 0, 0 ), // Cyan, White + // intensiv + ColorEntry(QColor(0x00,0x00,0x00), 0, 1 ), ColorEntry( QColor(0xFF,0xFF,0xFF), 1, 0 ), + ColorEntry(QColor(0x68,0x68,0x68), 0, 0 ), ColorEntry( QColor(0xFF,0x54,0x54), 0, 0 ), + ColorEntry(QColor(0x54,0xFF,0x54), 0, 0 ), ColorEntry( QColor(0xFF,0xFF,0x54), 0, 0 ), + ColorEntry(QColor(0x54,0x54,0xFF), 0, 0 ), ColorEntry( QColor(0xFF,0x54,0xFF), 0, 0 ), + ColorEntry(QColor(0x54,0xFF,0xFF), 0, 0 ), ColorEntry( QColor(0xFF,0xFF,0xFF), 0, 0 ) +}; + +static const ColorEntry greenonblack_color_table[TABLE_COLORS] = +{ + ColorEntry(QColor( 24, 240, 24), 0, 0), ColorEntry(QColor( 0, 0, 0), 1, 0), + ColorEntry(QColor( 0, 0, 0), 0, 0), ColorEntry(QColor( 178, 24, 24), 0, 0), + ColorEntry(QColor( 24, 178, 24), 0, 0), ColorEntry(QColor( 178, 104, 24), 0, 0), + ColorEntry(QColor( 24, 24, 178), 0, 0), ColorEntry(QColor( 178, 24, 178), 0, 0), + ColorEntry(QColor( 24, 178, 178), 0, 0), ColorEntry(QColor( 178, 178, 178), 0, 0), + // intensive colors + ColorEntry(QColor( 24, 240, 24), 0, 1 ), ColorEntry(QColor( 0, 0, 0), 1, 0 ), + ColorEntry(QColor( 104, 104, 104), 0, 0 ), ColorEntry(QColor( 255, 84, 84), 0, 0 ), + ColorEntry(QColor( 84, 255, 84), 0, 0 ), ColorEntry(QColor( 255, 255, 84), 0, 0 ), + ColorEntry(QColor( 84, 84, 255), 0, 0 ), ColorEntry(QColor( 255, 84, 255), 0, 0 ), + ColorEntry(QColor( 84, 255, 255), 0, 0 ), ColorEntry(QColor( 255, 255, 255), 0, 0 ) +}; + +static const ColorEntry blackonlightyellow_color_table[TABLE_COLORS] = +{ + ColorEntry(QColor( 0, 0, 0), 0, 0), ColorEntry(QColor( 255, 255, 221), 1, 0), + ColorEntry(QColor( 0, 0, 0), 0, 0), ColorEntry(QColor( 178, 24, 24), 0, 0), + ColorEntry(QColor( 24, 178, 24), 0, 0), ColorEntry(QColor( 178, 104, 24), 0, 0), + ColorEntry(QColor( 24, 24, 178), 0, 0), ColorEntry(QColor( 178, 24, 178), 0, 0), + ColorEntry(QColor( 24, 178, 178), 0, 0), ColorEntry(QColor( 178, 178, 178), 0, 0), + ColorEntry(QColor( 0, 0, 0), 0, 1), ColorEntry(QColor( 255, 255, 221), 1, 0), + ColorEntry(QColor(104, 104, 104), 0, 0), ColorEntry(QColor( 255, 84, 84), 0, 0), + ColorEntry(QColor( 84, 255, 84), 0, 0), ColorEntry(QColor( 255, 255, 84), 0, 0), + ColorEntry(QColor( 84, 84, 255), 0, 0), ColorEntry(QColor( 255, 84, 255), 0, 0), + ColorEntry(QColor( 84, 255, 255), 0, 0), ColorEntry(QColor( 255, 255, 255), 0, 0) +}; + + + + + +#endif +
new file mode 100644 --- /dev/null +++ b/gui//DefaultTranslatorText.h @@ -0,0 +1,2 @@ +"keyboard \"Fallback Key Translator\"\n" +"key Tab : \"\\t\" \0"
new file mode 100644 --- /dev/null +++ b/gui//Emulation.cpp @@ -0,0 +1,543 @@ +/* + This file is part of Konsole, an X terminal. + + Copyright (C) 2007 Robert Knight <robertknight@gmail.com> + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + Copyright (C) 1996 by Matthias Ettrich <ettrich@kde.org> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +// Own +#include "Emulation.h" + +// System +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +// Qt +#include <QtGui/QApplication> +#include <QtGui/QClipboard> +#include <QtCore/QHash> +#include <QtGui/QKeyEvent> +#include <QtCore/QRegExp> +#include <QtCore/QTextStream> +#include <QtCore/QThread> + +#include <QtCore/QTime> + +// Konsole +#include "KeyboardTranslator.h" +#include "Screen.h" +#include "TerminalCharacterDecoder.h" +#include "ScreenWindow.h" + +using namespace Konsole; + +/* ------------------------------------------------------------------------- */ +/* */ +/* Emulation */ +/* */ +/* ------------------------------------------------------------------------- */ + +//#define CNTL(c) ((c)-'@') + +/*! +*/ + +Emulation::Emulation() : + _currentScreen(0), + _codec(0), + _decoder(0), + _keyTranslator(0), + _usesMouse(false) +{ + + // create screens with a default size + _screen[0] = new Screen(40,80); + _screen[1] = new Screen(40,80); + _currentScreen = _screen[0]; + + QObject::connect(&_bulkTimer1, SIGNAL(timeout()), this, SLOT(showBulk()) ); + QObject::connect(&_bulkTimer2, SIGNAL(timeout()), this, SLOT(showBulk()) ); + + // listen for mouse status changes + connect( this , SIGNAL(programUsesMouseChanged(bool)) , + SLOT(usesMouseChanged(bool)) ); +} + +bool Emulation::programUsesMouse() const +{ + return _usesMouse; +} + +void Emulation::usesMouseChanged(bool usesMouse) +{ + _usesMouse = usesMouse; +} + +ScreenWindow* Emulation::createWindow() +{ + ScreenWindow* window = new ScreenWindow(); + window->setScreen(_currentScreen); + _windows << window; + + connect(window , SIGNAL(selectionChanged()), + this , SLOT(bufferedUpdate())); + + connect(this , SIGNAL(outputChanged()), + window , SLOT(notifyOutputChanged()) ); + return window; +} + +/*! +*/ + +Emulation::~Emulation() +{ + QListIterator<ScreenWindow*> windowIter(_windows); + + while (windowIter.hasNext()) + { + delete windowIter.next(); + } + + delete _screen[0]; + delete _screen[1]; + delete _decoder; +} + +/*! change between primary and alternate _screen +*/ + +void Emulation::setScreen(int n) +{ + Screen *old = _currentScreen; + _currentScreen = _screen[n&1]; + if (_currentScreen != old) + { + old->setBusySelecting(false); + + // tell all windows onto this emulation to switch to the newly active _screen + QListIterator<ScreenWindow*> windowIter(_windows); + while ( windowIter.hasNext() ) + { + windowIter.next()->setScreen(_currentScreen); + } + } +} + +void Emulation::clearHistory() +{ + _screen[0]->setScroll( _screen[0]->getScroll() , false ); +} +void Emulation::setHistory(const HistoryType& t) +{ + _screen[0]->setScroll(t); + + showBulk(); +} + +const HistoryType& Emulation::history() +{ + return _screen[0]->getScroll(); +} + +void Emulation::setCodec(const QTextCodec * qtc) +{ + Q_ASSERT( qtc ); + + _codec = qtc; + delete _decoder; + _decoder = _codec->makeDecoder(); + + emit useUtf8Request(utf8()); +} + +void Emulation::setCodec(EmulationCodec codec) +{ + if ( codec == Utf8Codec ) + setCodec( QTextCodec::codecForName("utf8") ); + else if ( codec == LocaleCodec ) + setCodec( QTextCodec::codecForLocale() ); +} + +void Emulation::setKeyBindings(const QString& name) +{ + _keyTranslator = KeyboardTranslatorManager::instance()->findTranslator(name); +} + +QString Emulation::keyBindings() +{ + return _keyTranslator->name(); +} + + +// Interpreting Codes --------------------------------------------------------- + +/* + This section deals with decoding the incoming character stream. + Decoding means here, that the stream is first separated into `tokens' + which are then mapped to a `meaning' provided as operations by the + `Screen' class. +*/ + +/*! +*/ + +void Emulation::receiveChar(int c) +// process application unicode input to terminal +// this is a trivial scanner +{ + c &= 0xff; + switch (c) + { + case '\b' : _currentScreen->BackSpace(); break; + case '\t' : _currentScreen->Tabulate(); break; + case '\n' : _currentScreen->NewLine(); break; + case '\r' : _currentScreen->Return(); break; + case 0x07 : emit stateSet(NOTIFYBELL); + break; + default : _currentScreen->ShowCharacter(c); break; + }; +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Keyboard Handling */ +/* */ +/* ------------------------------------------------------------------------- */ + +/*! +*/ + +void Emulation::sendKeyEvent( QKeyEvent* ev ) +{ + emit stateSet(NOTIFYNORMAL); + + if (!ev->text().isEmpty()) + { // A block of text + // Note that the text is proper unicode. + // We should do a conversion here, but since this + // routine will never be used, we simply emit plain ascii. + //emit sendBlock(ev->text().toAscii(),ev->text().length()); + emit sendData(ev->text().toUtf8(),ev->text().length()); + } +} + +void Emulation::sendString(const char*,int) +{ + // default implementation does nothing +} + +void Emulation::sendMouseEvent(int /*buttons*/, int /*column*/, int /*row*/, int /*eventType*/) +{ + // default implementation does nothing +} + +// Unblocking, Byte to Unicode translation --------------------------------- -- + +/* + We are doing code conversion from locale to unicode first. +TODO: Character composition from the old code. See #96536 +*/ + +void Emulation::receiveData(const char* text, int length) +{ + emit stateSet(NOTIFYACTIVITY); + + bufferedUpdate(); + + QString unicodeText = _decoder->toUnicode(text,length); + + //send characters to terminal emulator + for (int i=0;i<unicodeText.length();i++) + { + receiveChar(unicodeText[i].unicode()); + } + + //look for z-modem indicator + //-- someone who understands more about z-modems that I do may be able to move + //this check into the above for loop? + for (int i=0;i<length;i++) + { + if (text[i] == '\030') + { + if ((length-i-1 > 3) && (strncmp(text+i+1, "B00", 3) == 0)) + emit zmodemDetected(); + } + } +} + +//OLDER VERSION +//This version of onRcvBlock was commented out because +// a) It decoded incoming characters one-by-one, which is slow in the current version of Qt (4.2 tech preview) +// b) It messed up decoding of non-ASCII characters, with the result that (for example) chinese characters +// were not printed properly. +// +//There is something about stopping the _decoder if "we get a control code halfway a multi-byte sequence" (see below) +//which hasn't been ported into the newer function (above). Hopefully someone who understands this better +//can find an alternative way of handling the check. + + +/*void Emulation::onRcvBlock(const char *s, int len) +{ + emit notifySessionState(NOTIFYACTIVITY); + + bufferedUpdate(); + for (int i = 0; i < len; i++) + { + + QString result = _decoder->toUnicode(&s[i],1); + int reslen = result.length(); + + // If we get a control code halfway a multi-byte sequence + // we flush the _decoder and continue with the control code. + if ((s[i] < 32) && (s[i] > 0)) + { + // Flush _decoder + while(!result.length()) + result = _decoder->toUnicode(&s[i],1); + reslen = 1; + result.resize(reslen); + result[0] = QChar(s[i]); + } + + for (int j = 0; j < reslen; j++) + { + if (result[j].characterategory() == QChar::Mark_NonSpacing) + _currentScreen->compose(result.mid(j,1)); + else + onRcvChar(result[j].unicode()); + } + if (s[i] == '\030') + { + if ((len-i-1 > 3) && (strncmp(s+i+1, "B00", 3) == 0)) + emit zmodemDetected(); + } + } +}*/ + +// Selection --------------------------------------------------------------- -- + +#if 0 +void Emulation::onSelectionBegin(const int x, const int y, const bool columnmode) { + if (!connected) return; + _currentScreen->setSelectionStart( x,y,columnmode); + showBulk(); +} + +void Emulation::onSelectionExtend(const int x, const int y) { + if (!connected) return; + _currentScreen->setSelectionEnd(x,y); + showBulk(); +} + +void Emulation::setSelection(const bool preserve_line_breaks) { + if (!connected) return; + QString t = _currentScreen->selectedText(preserve_line_breaks); + if (!t.isNull()) + { + QListIterator< TerminalDisplay* > viewIter(_views); + + while (viewIter.hasNext()) + viewIter.next()->setSelection(t); + } +} + +void Emulation::testIsSelected(const int x, const int y, bool &selected) +{ + if (!connected) return; + selected=_currentScreen->isSelected(x,y); +} + +void Emulation::clearSelection() { + if (!connected) return; + _currentScreen->clearSelection(); + showBulk(); +} + +#endif + +void Emulation::writeToStream( TerminalCharacterDecoder* _decoder , + int startLine , + int endLine) +{ + _currentScreen->writeToStream(_decoder,startLine,endLine); +} + +int Emulation::lineCount() +{ + // sum number of lines currently on _screen plus number of lines in history + return _currentScreen->getLines() + _currentScreen->getHistLines(); +} + +// Refreshing -------------------------------------------------------------- -- + +#define BULK_TIMEOUT1 10 +#define BULK_TIMEOUT2 40 + +/*! +*/ +void Emulation::showBulk() +{ + _bulkTimer1.stop(); + _bulkTimer2.stop(); + + emit outputChanged(); + + _currentScreen->resetScrolledLines(); + _currentScreen->resetDroppedLines(); +} + +void Emulation::bufferedUpdate() +{ + _bulkTimer1.setSingleShot(true); + _bulkTimer1.start(BULK_TIMEOUT1); + if (!_bulkTimer2.isActive()) + { + _bulkTimer2.setSingleShot(true); + _bulkTimer2.start(BULK_TIMEOUT2); + } +} + +char Emulation::getErase() const +{ + return '\b'; +} + +void Emulation::setImageSize(int lines, int columns) +{ + //kDebug() << "Resizing image to: " << lines << "by" << columns << QTime::currentTime().msec(); + Q_ASSERT( lines > 0 ); + Q_ASSERT( columns > 0 ); + + _screen[0]->resizeImage(lines,columns); + _screen[1]->resizeImage(lines,columns); + + emit imageSizeChanged(lines,columns); + + bufferedUpdate(); +} + +QSize Emulation::imageSize() +{ + return QSize(_currentScreen->getColumns(), _currentScreen->getLines()); +} + +ushort ExtendedCharTable::extendedCharHash(ushort* unicodePoints , ushort length) const +{ + ushort hash = 0; + for ( ushort i = 0 ; i < length ; i++ ) + { + hash = 31*hash + unicodePoints[i]; + } + return hash; +} +bool ExtendedCharTable::extendedCharMatch(ushort hash , ushort* unicodePoints , ushort length) const +{ + ushort* entry = extendedCharTable[hash]; + + // compare given length with stored sequence length ( given as the first ushort in the + // stored buffer ) + if ( entry == 0 || entry[0] != length ) + return false; + // if the lengths match, each character must be checked. the stored buffer starts at + // entry[1] + for ( int i = 0 ; i < length ; i++ ) + { + if ( entry[i+1] != unicodePoints[i] ) + return false; + } + return true; +} +ushort ExtendedCharTable::createExtendedChar(ushort* unicodePoints , ushort length) +{ + // look for this sequence of points in the table + ushort hash = extendedCharHash(unicodePoints,length); + + // check existing entry for match + while ( extendedCharTable.contains(hash) ) + { + if ( extendedCharMatch(hash,unicodePoints,length) ) + { + // this sequence already has an entry in the table, + // return its hash + return hash; + } + else + { + // if hash is already used by another, different sequence of unicode character + // points then try next hash + hash++; + } + } + + + // add the new sequence to the table and + // return that index + ushort* buffer = new ushort[length+1]; + buffer[0] = length; + for ( int i = 0 ; i < length ; i++ ) + buffer[i+1] = unicodePoints[i]; + + extendedCharTable.insert(hash,buffer); + + return hash; +} + +ushort* ExtendedCharTable::lookupExtendedChar(ushort hash , ushort& length) const +{ + // lookup index in table and if found, set the length + // argument and return a pointer to the character sequence + + ushort* buffer = extendedCharTable[hash]; + if ( buffer ) + { + length = buffer[0]; + return buffer+1; + } + else + { + length = 0; + return 0; + } +} + +ExtendedCharTable::ExtendedCharTable() +{ +} +ExtendedCharTable::~ExtendedCharTable() +{ + // free all allocated character buffers + QHashIterator<ushort,ushort*> iter(extendedCharTable); + while ( iter.hasNext() ) + { + iter.next(); + delete[] iter.value(); + } +} + +// global instance +ExtendedCharTable ExtendedCharTable::instance; + + +//#include "moc_Emulation.cpp" +
new file mode 100644 --- /dev/null +++ b/gui//Emulation.h @@ -0,0 +1,465 @@ +/* + This file is part of Konsole, an X terminal. + + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef EMULATION_H +#define EMULATION_H + +// System +#include <stdio.h> + +// Qt +#include <QtGui/QKeyEvent> +//#include <QPointer> +#include <QtCore/QTextCodec> +#include <QtCore/QTextStream> +#include <QtCore/QTimer> + + +namespace Konsole +{ + +class KeyboardTranslator; +class HistoryType; +class Screen; +class ScreenWindow; +class TerminalCharacterDecoder; + +/** + * This enum describes the available states which + * the terminal emulation may be set to. + * + * These are the values used by Emulation::stateChanged() + */ +enum +{ + /** The emulation is currently receiving user input. */ + NOTIFYNORMAL=0, + /** + * The terminal program has triggered a bell event + * to get the user's attention. + */ + NOTIFYBELL=1, + /** + * The emulation is currently receiving data from its + * terminal input. + */ + NOTIFYACTIVITY=2, + + // unused here? + NOTIFYSILENCE=3 +}; + +/** + * Base class for terminal emulation back-ends. + * + * The back-end is responsible for decoding an incoming character stream and + * producing an output image of characters. + * + * When input from the terminal is received, the receiveData() slot should be called with + * the data which has arrived. The emulation will process the data and update the + * screen image accordingly. The codec used to decode the incoming character stream + * into the unicode characters used internally can be specified using setCodec() + * + * The size of the screen image can be specified by calling setImageSize() with the + * desired number of lines and columns. When new lines are added, old content + * is moved into a history store, which can be set by calling setHistory(). + * + * The screen image can be accessed by creating a ScreenWindow onto this emulation + * by calling createWindow(). Screen windows provide access to a section of the + * output. Each screen window covers the same number of lines and columns as the + * image size returned by imageSize(). The screen window can be moved up and down + * and provides transparent access to both the current on-screen image and the + * previous output. The screen windows emit an outputChanged signal + * when the section of the image they are looking at changes. + * Graphical views can then render the contents of a screen window, listening for notifications + * of output changes from the screen window which they are associated with and updating + * accordingly. + * + * The emulation also is also responsible for converting input from the connected views such + * as keypresses and mouse activity into a character string which can be sent + * to the terminal program. Key presses can be processed by calling the sendKeyEvent() slot, + * while mouse events can be processed using the sendMouseEvent() slot. When the character + * stream has been produced, the emulation will emit a sendData() signal with a pointer + * to the character buffer. This data should be fed to the standard input of the terminal + * process. The translation of key presses into an output character stream is performed + * using a lookup in a set of key bindings which map key sequences to output + * character sequences. The name of the key bindings set used can be specified using + * setKeyBindings() + * + * The emulation maintains certain state information which changes depending on the + * input received. The emulation can be reset back to its starting state by calling + * reset(). + * + * The emulation also maintains an activity state, which specifies whether + * terminal is currently active ( when data is received ), normal + * ( when the terminal is idle or receiving user input ) or trying + * to alert the user ( also known as a "Bell" event ). The stateSet() signal + * is emitted whenever the activity state is set. This can be used to determine + * how long the emulation has been active/idle for and also respond to + * a 'bell' event in different ways. + */ +class Emulation : public QObject +{ +Q_OBJECT + +public: + + /** Constructs a new terminal emulation */ + Emulation(); + ~Emulation(); + + /** + * Creates a new window onto the output from this emulation. The contents + * of the window are then rendered by views which are set to use this window using the + * TerminalDisplay::setScreenWindow() method. + */ + ScreenWindow* createWindow(); + + /** Returns the size of the screen image which the emulation produces */ + QSize imageSize(); + + /** + * Returns the total number of lines, including those stored in the history. + */ + int lineCount(); + + + /** + * Sets the history store used by this emulation. When new lines + * are added to the output, older lines at the top of the screen are transferred to a history + * store. + * + * The number of lines which are kept and the storage location depend on the + * type of store. + */ + void setHistory(const HistoryType&); + /** Returns the history store used by this emulation. See setHistory() */ + const HistoryType& history(); + /** Clears the history scroll. */ + void clearHistory(); + + /** + * Copies the output history from @p startLine to @p endLine + * into @p stream, using @p decoder to convert the terminal + * characters into text. + * + * @param decoder A decoder which converts lines of terminal characters with + * appearance attributes into output text. PlainTextDecoder is the most commonly + * used decoder. + * @param startLine The first + */ + virtual void writeToStream(TerminalCharacterDecoder* decoder,int startLine,int endLine); + + + /** Returns the codec used to decode incoming characters. See setCodec() */ + const QTextCodec* codec() { return _codec; } + /** Sets the codec used to decode incoming characters. */ + void setCodec(const QTextCodec*); + + /** + * Convenience method. + * Returns true if the current codec used to decode incoming + * characters is UTF-8 + */ + bool utf8() { Q_ASSERT(_codec); return _codec->mibEnum() == 106; } + + + /** TODO Document me */ + virtual char getErase() const; + + /** + * Sets the key bindings used to key events + * ( received through sendKeyEvent() ) into character + * streams to send to the terminal. + */ + void setKeyBindings(const QString& name); + /** + * Returns the name of the emulation's current key bindings. + * See setKeyBindings() + */ + QString keyBindings(); + + /** + * Copies the current image into the history and clears the screen. + */ + virtual void clearEntireScreen() =0; + + /** Resets the state of the terminal. */ + virtual void reset() =0; + + /** + * Returns true if the active terminal program wants + * mouse input events. + * + * The programUsesMouseChanged() signal is emitted when this + * changes. + */ + bool programUsesMouse() const; + +public slots: + + /** Change the size of the emulation's image */ + virtual void setImageSize(int lines, int columns); + + /** + * Interprets a sequence of characters and sends the result to the terminal. + * This is equivalent to calling sendKeyEvent() for each character in @p text in succession. + */ + virtual void sendText(const QString& text) = 0; + + /** + * Interprets a key press event and emits the sendData() signal with + * the resulting character stream. + */ + virtual void sendKeyEvent(QKeyEvent*); + + /** + * Converts information about a mouse event into an xterm-compatible escape + * sequence and emits the character sequence via sendData() + */ + virtual void sendMouseEvent(int buttons, int column, int line, int eventType); + + /** + * Sends a string of characters to the foreground terminal process. + * + * @param string The characters to send. + * @param length Length of @p string or if set to a negative value, @p string will + * be treated as a null-terminated string and its length will be determined automatically. + */ + virtual void sendString(const char* string, int length = -1) = 0; + + /** + * Processes an incoming stream of characters. receiveData() decodes the incoming + * character buffer using the current codec(), and then calls receiveChar() for + * each unicode character in the resulting buffer. + * + * receiveData() also starts a timer which causes the outputChanged() signal + * to be emitted when it expires. The timer allows multiple updates in quick + * succession to be buffered into a single outputChanged() signal emission. + * + * @param buffer A string of characters received from the terminal program. + * @param len The length of @p buffer + */ + void receiveData(const char* buffer,int len); + +signals: + + /** + * Emitted when a buffer of data is ready to send to the + * standard input of the terminal. + * + * @param data The buffer of data ready to be sent + * @paran len The length of @p data in bytes + */ + void sendData(const char* data,int len); + + /** + * Requests that sending of input to the emulation + * from the terminal process be suspended or resumed. + * + * @param suspend If true, requests that sending of + * input from the terminal process' stdout be + * suspended. Otherwise requests that sending of + * input be resumed. + */ + void lockPtyRequest(bool suspend); + + /** + * Requests that the pty used by the terminal process + * be set to UTF 8 mode. + * + * TODO: More documentation + */ + void useUtf8Request(bool); + + /** + * Emitted when the activity state of the emulation is set. + * + * @param state The new activity state, one of NOTIFYNORMAL, NOTIFYACTIVITY + * or NOTIFYBELL + */ + void stateSet(int state); + + /** TODO Document me */ + void zmodemDetected(); + + + /** + * Requests that the color of the text used + * to represent the tabs associated with this + * emulation be changed. This is a Konsole-specific + * extension from pre-KDE 4 times. + * + * TODO: Document how the parameter works. + */ + void changeTabTextColorRequest(int color); + + /** + * This is emitted when the program running in the shell indicates whether or + * not it is interested in mouse events. + * + * @param usesMouse This will be true if the program wants to be informed about + * mouse events or false otherwise. + */ + void programUsesMouseChanged(bool usesMouse); + + /** + * Emitted when the contents of the screen image change. + * The emulation buffers the updates from successive image changes, + * and only emits outputChanged() at sensible intervals when + * there is a lot of terminal activity. + * + * Normally there is no need for objects other than the screen windows + * created with createWindow() to listen for this signal. + * + * ScreenWindow objects created using createWindow() will emit their + * own outputChanged() signal in response to this signal. + */ + void outputChanged(); + + /** + * Emitted when the program running in the terminal wishes to update the + * session's title. This also allows terminal programs to customize other + * aspects of the terminal emulation display. + * + * This signal is emitted when the escape sequence "\033]ARG;VALUE\007" + * is received in the input string, where ARG is a number specifying what + * should change and VALUE is a string specifying the new value. + * + * TODO: The name of this method is not very accurate since this method + * is used to perform a whole range of tasks besides just setting + * the user-title of the session. + * + * @param title Specifies what to change. + * <ul> + * <li>0 - Set window icon text and session title to @p newTitle</li> + * <li>1 - Set window icon text to @p newTitle</li> + * <li>2 - Set session title to @p newTitle</li> + * <li>11 - Set the session's default background color to @p newTitle, + * where @p newTitle can be an HTML-style string (#RRGGBB) or a named + * color (eg 'red', 'blue'). + * See http://doc.trolltech.com/4.2/qcolor.html#setNamedColor for more + * details. + * </li> + * <li>31 - Supposedly treats @p newTitle as a URL and opens it (NOT IMPLEMENTED)</li> + * <li>32 - Sets the icon associated with the session. @p newTitle is the name + * of the icon to use, which can be the name of any icon in the current KDE icon + * theme (eg: 'konsole', 'kate', 'folder_home')</li> + * </ul> + * @param newTitle Specifies the new title + */ + + void titleChanged(int title,const QString& newTitle); + + /** + * Emitted when the program running in the terminal changes the + * screen size. + */ + void imageSizeChanged(int lineCount , int columnCount); + + /** + * Emitted when the terminal program requests to change various properties + * of the terminal display. + * + * A profile change command occurs when a special escape sequence, followed + * by a string containing a series of name and value pairs is received. + * This string can be parsed using a ProfileCommandParser instance. + * + * @param text A string expected to contain a series of key and value pairs in + * the form: name=value;name2=value2 ... + */ + void profileChangeCommandReceived(const QString& text); + +protected: + virtual void setMode (int mode) = 0; + virtual void resetMode(int mode) = 0; + + /** + * Processes an incoming character. See receiveData() + * @p ch A unicode character code. + */ + virtual void receiveChar(int ch); + + /** + * Sets the active screen. The terminal has two screens, primary and alternate. + * The primary screen is used by default. When certain interactive programs such + * as Vim are run, they trigger a switch to the alternate screen. + * + * @param index 0 to switch to the primary screen, or 1 to switch to the alternate screen + */ + void setScreen(int index); + + enum EmulationCodec + { + LocaleCodec = 0, + Utf8Codec = 1 + }; + void setCodec(EmulationCodec codec); // codec number, 0 = locale, 1=utf8 + + + QList<ScreenWindow*> _windows; + + Screen* _currentScreen; // pointer to the screen which is currently active, + // this is one of the elements in the screen[] array + + Screen* _screen[2]; // 0 = primary screen ( used by most programs, including the shell + // scrollbars are enabled in this mode ) + // 1 = alternate ( used by vi , emacs etc. + // scrollbars are not enabled in this mode ) + + + //decodes an incoming C-style character stream into a unicode QString using + //the current text codec. (this allows for rendering of non-ASCII characters in text files etc.) + const QTextCodec* _codec; + QTextDecoder* _decoder; + + const KeyboardTranslator* _keyTranslator; // the keyboard layout + +protected slots: + /** + * Schedules an update of attached views. + * Repeated calls to bufferedUpdate() in close succession will result in only a single update, + * much like the Qt buffered update of widgets. + */ + void bufferedUpdate(); + +private slots: + + // triggered by timer, causes the emulation to send an updated screen image to each + // view + void showBulk(); + + void usesMouseChanged(bool usesMouse); + +private: + + bool _usesMouse; + QTimer _bulkTimer1; + QTimer _bulkTimer2; + +}; + +} + +#endif // ifndef EMULATION_H
new file mode 100644 --- /dev/null +++ b/gui//ExtendedDefaultTranslator.h @@ -0,0 +1,74 @@ +"keyboard \"Default (XFree 4)\"" +"key Escape : \"\\E\"" +"key Tab -Shift : \"\\t\"\n" +"key Tab +Shift+Ansi : \"\\E[Z\"\n" +"key Tab +Shift-Ansi : \"\\t\"\n" +"key Backtab +Ansi : \"\\E[Z\"\n" +"key Backtab -Ansi : \"\\t\"\n" +"key Return-Shift-NewLine : \"\\r\"\n" +"key Return-Shift+NewLine : \"\\r\\n\"\n" +"key Return+Shift : \"\\EOM\"\n" +"key Backspace : \"\\x7f\"\n" +"key Up -Shift-Ansi : \"\\EA\"\n" +"key Down -Shift-Ansi : \"\\EB\"\n" +"key Right-Shift-Ansi : \"\\EC\"\n" +"key Left -Shift-Ansi : \"\\ED\"\n" +"key Up -Shift-AnyMod+Ansi+AppCuKeys : \"\\EOA\"\n" +"key Down -Shift-AnyMod+Ansi+AppCuKeys : \"\\EOB\"\n" +"key Right -Shift-AnyMod+Ansi+AppCuKeys : \"\\EOC\"\n" +"key Left -Shift-AnyMod+Ansi+AppCuKeys : \"\\EOD\"\n" +"key Up -Shift-AnyMod+Ansi-AppCuKeys : \"\\E[A\"\n" +"key Down -Shift-AnyMod+Ansi-AppCuKeys : \"\\E[B\"\n" +"key Right -Shift-AnyMod+Ansi-AppCuKeys : \"\\E[C\"\n" +"key Left -Shift-AnyMod+Ansi-AppCuKeys : \"\\E[D\"\n" +"key Up -Shift+AnyMod+Ansi : \"\\E[1;*A\"\n" +"key Down -Shift+AnyMod+Ansi : \"\\E[1;*B\"\n" +"key Right -Shift+AnyMod+Ansi : \"\\E[1;*C\"\n" +"key Left -Shift+AnyMod+Ansi : \"\\E[1;*D\"\n" +"key Enter+NewLine : \"\\r\\n\"\n" +"key Enter-NewLine : \"\\r\"\n" +"key Home -AnyMod -AppCuKeys : \"\\E[H\" \n" +"key End -AnyMod -AppCuKeys : \"\\E[F\" \n" +"key Home -AnyMod +AppCuKeys : \"\\EOH\" \n" +"key End -AnyMod +AppCuKeys : \"\\EOF\" \n" +"key Home +AnyMod : \"\\E[1;*H\"\n" +"key End +AnyMod : \"\\E[1;*F\"\n" +"key Insert -AnyMod : \"\\E[2~\"\n" +"key Delete -AnyMod : \"\\E[3~\"\n" +"key Insert +AnyMod : \"\\E[2;*~\"\n" +"key Delete +AnyMod : \"\\E[3;*~\"\n" +"key Prior -Shift-AnyMod : \"\\E[5~\"\n" +"key Next -Shift-AnyMod : \"\\E[6~\"\n" +"key Prior -Shift+AnyMod : \"\\E[5;*~\"\n" +"key Next -Shift+AnyMod : \"\\E[6;*~\"\n" +"key F1 -AnyMod : \"\\EOP\"\n" +"key F2 -AnyMod : \"\\EOQ\"\n" +"key F3 -AnyMod : \"\\EOR\"\n" +"key F4 -AnyMod : \"\\EOS\"\n" +"key F5 -AnyMod : \"\\E[15~\"\n" +"key F6 -AnyMod : \"\\E[17~\"\n" +"key F7 -AnyMod : \"\\E[18~\"\n" +"key F8 -AnyMod : \"\\E[19~\"\n" +"key F9 -AnyMod : \"\\E[20~\"\n" +"key F10 -AnyMod : \"\\E[21~\"\n" +"key F11 -AnyMod : \"\\E[23~\"\n" +"key F12 -AnyMod : \"\\E[24~\"\n" +"key F1 +AnyMod : \"\\EO*P\"\n" +"key F2 +AnyMod : \"\\EO*Q\"\n" +"key F3 +AnyMod : \"\\EO*R\"\n" +"key F4 +AnyMod : \"\\EO*S\"\n" +"key F5 +AnyMod : \"\\E[15;*~\"\n" +"key F6 +AnyMod : \"\\E[17;*~\"\n" +"key F7 +AnyMod : \"\\E[18;*~\"\n" +"key F8 +AnyMod : \"\\E[19;*~\"\n" +"key F9 +AnyMod : \"\\E[20;*~\"\n" +"key F10 +AnyMod : \"\\E[21;*~\"\n" +"key F11 +AnyMod : \"\\E[23;*~\"\n" +"key F12 +AnyMod : \"\\E[24;*~\"\n" +"key Space +Control : \"\\x00\"\n" +"key Up +Shift-AppScreen : scrollLineUp\n" +"key Prior +Shift-AppScreen : scrollPageUp\n" +"key Down +Shift-AppScreen : scrollLineDown\n" +"key Next +Shift-AppScreen : scrollPageDown\n" +"key ScrollLock : scrollLock\n" +"\0"
new file mode 100644 --- /dev/null +++ b/gui//Filter.cpp @@ -0,0 +1,562 @@ +/* + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +// Own +#include "Filter.h" + +// System +#include <iostream> + +// Qt +#include <QtGui/QAction> +#include <QtGui/QApplication> +#include <QtGui/QClipboard> +#include <QtCore/QString> + +#include <QtCore/QSharedData> +#include <QtCore> + +// KDE +//#include <KLocale> +//#include <KRun> + +// Konsole +#include "TerminalCharacterDecoder.h" + +using namespace Konsole; + +FilterChain::~FilterChain() +{ + QMutableListIterator<Filter*> iter(*this); + + while ( iter.hasNext() ) + { + Filter* filter = iter.next(); + iter.remove(); + delete filter; + } +} + +void FilterChain::addFilter(Filter* filter) +{ + append(filter); +} +void FilterChain::removeFilter(Filter* filter) +{ + removeAll(filter); +} +bool FilterChain::containsFilter(Filter* filter) +{ + return contains(filter); +} +void FilterChain::reset() +{ + QListIterator<Filter*> iter(*this); + while (iter.hasNext()) + iter.next()->reset(); +} +void FilterChain::setBuffer(const QString* buffer , const QList<int>* linePositions) +{ + QListIterator<Filter*> iter(*this); + while (iter.hasNext()) + iter.next()->setBuffer(buffer,linePositions); +} +void FilterChain::process() +{ + QListIterator<Filter*> iter(*this); + while (iter.hasNext()) + iter.next()->process(); +} +void FilterChain::clear() +{ + QList<Filter*>::clear(); +} +Filter::HotSpot* FilterChain::hotSpotAt(int line , int column) const +{ + QListIterator<Filter*> iter(*this); + while (iter.hasNext()) + { + Filter* filter = iter.next(); + Filter::HotSpot* spot = filter->hotSpotAt(line,column); + if ( spot != 0 ) + { + return spot; + } + } + + return 0; +} + +QList<Filter::HotSpot*> FilterChain::hotSpots() const +{ + QList<Filter::HotSpot*> list; + QListIterator<Filter*> iter(*this); + while (iter.hasNext()) + { + Filter* filter = iter.next(); + list << filter->hotSpots(); + } + return list; +} +//QList<Filter::HotSpot*> FilterChain::hotSpotsAtLine(int line) const; + +TerminalImageFilterChain::TerminalImageFilterChain() +: _buffer(0) +, _linePositions(0) +{ +} + +TerminalImageFilterChain::~TerminalImageFilterChain() +{ + delete _buffer; + delete _linePositions; +} + +void TerminalImageFilterChain::setImage(const Character* const image , int lines , int columns, const QVector<LineProperty>& lineProperties) +{ +//qDebug("%s %d", __FILE__, __LINE__); + if (empty()) + return; +//qDebug("%s %d", __FILE__, __LINE__); + + // reset all filters and hotspots + reset(); +//qDebug("%s %d", __FILE__, __LINE__); + + PlainTextDecoder decoder; + decoder.setTrailingWhitespace(false); + +//qDebug("%s %d", __FILE__, __LINE__); + // setup new shared buffers for the filters to process on + QString* newBuffer = new QString(); + QList<int>* newLinePositions = new QList<int>(); + setBuffer( newBuffer , newLinePositions ); + + // free the old buffers + delete _buffer; + delete _linePositions; + + _buffer = newBuffer; + _linePositions = newLinePositions; + + QTextStream lineStream(_buffer); + decoder.begin(&lineStream); + + for (int i=0 ; i < lines ; i++) + { + _linePositions->append(_buffer->length()); + decoder.decodeLine(image + i*columns,columns,LINE_DEFAULT); + + // pretend that each line ends with a newline character. + // this prevents a link that occurs at the end of one line + // being treated as part of a link that occurs at the start of the next line + // + // the downside is that links which are spread over more than one line are not + // highlighted. + // + // TODO - Use the "line wrapped" attribute associated with lines in a + // terminal image to avoid adding this imaginary character for wrapped + // lines + if ( !(lineProperties.value(i,LINE_DEFAULT) & LINE_WRAPPED) ) + lineStream << QChar('\n'); + } + decoder.end(); +// qDebug("%s %d", __FILE__, __LINE__); +} + +Filter::Filter() : +_linePositions(0), +_buffer(0) +{ +} + +Filter::~Filter() +{ + QListIterator<HotSpot*> iter(_hotspotList); + while (iter.hasNext()) + { + delete iter.next(); + } +} +void Filter::reset() +{ + _hotspots.clear(); + _hotspotList.clear(); +} + +void Filter::setBuffer(const QString* buffer , const QList<int>* linePositions) +{ + _buffer = buffer; + _linePositions = linePositions; +} + +void Filter::getLineColumn(int position , int& startLine , int& startColumn) +{ + Q_ASSERT( _linePositions ); + Q_ASSERT( _buffer ); + + + for (int i = 0 ; i < _linePositions->count() ; i++) + { + //kDebug() << "line position at " << i << " = " << _linePositions[i]; + int nextLine = 0; + + if ( i == _linePositions->count()-1 ) + { + nextLine = _buffer->length() + 1; + } + else + { + nextLine = _linePositions->value(i+1); + } + + // kDebug() << "pos - " << position << " line pos(" << i<< ") " << _linePositions->value(i) << + // " next = " << nextLine << " buffer len = " << _buffer->length(); + + if ( _linePositions->value(i) <= position && position < nextLine ) + { + startLine = i; + startColumn = position - _linePositions->value(i); + return; + } + } +} + + +/*void Filter::addLine(const QString& text) +{ + _linePositions << _buffer.length(); + _buffer.append(text); +}*/ + +const QString* Filter::buffer() +{ + return _buffer; +} +Filter::HotSpot::~HotSpot() +{ +} +void Filter::addHotSpot(HotSpot* spot) +{ + _hotspotList << spot; + + for (int line = spot->startLine() ; line <= spot->endLine() ; line++) + { + _hotspots.insert(line,spot); + } +} +QList<Filter::HotSpot*> Filter::hotSpots() const +{ + return _hotspotList; +} +QList<Filter::HotSpot*> Filter::hotSpotsAtLine(int line) const +{ + return _hotspots.values(line); +} + +Filter::HotSpot* Filter::hotSpotAt(int line , int column) const +{ + QListIterator<HotSpot*> spotIter(_hotspots.values(line)); + + while (spotIter.hasNext()) + { + HotSpot* spot = spotIter.next(); + + if ( spot->startLine() == line && spot->startColumn() > column ) + continue; + if ( spot->endLine() == line && spot->endColumn() < column ) + continue; + + return spot; + } + + return 0; +} + +Filter::HotSpot::HotSpot(int startLine , int startColumn , int endLine , int endColumn) + : _startLine(startLine) + , _startColumn(startColumn) + , _endLine(endLine) + , _endColumn(endColumn) + , _type(NotSpecified) +{ +} +QString Filter::HotSpot::tooltip() const +{ + return QString(); +} +QList<QAction*> Filter::HotSpot::actions() +{ + return QList<QAction*>(); +} +int Filter::HotSpot::startLine() const +{ + return _startLine; +} +int Filter::HotSpot::endLine() const +{ + return _endLine; +} +int Filter::HotSpot::startColumn() const +{ + return _startColumn; +} +int Filter::HotSpot::endColumn() const +{ + return _endColumn; +} +Filter::HotSpot::Type Filter::HotSpot::type() const +{ + return _type; +} +void Filter::HotSpot::setType(Type type) +{ + _type = type; +} + +RegExpFilter::RegExpFilter() +{ +} + +RegExpFilter::HotSpot::HotSpot(int startLine,int startColumn,int endLine,int endColumn) + : Filter::HotSpot(startLine,startColumn,endLine,endColumn) +{ + setType(Marker); +} + +void RegExpFilter::HotSpot::activate(QObject*) +{ +} + +void RegExpFilter::HotSpot::setCapturedTexts(const QStringList& texts) +{ + _capturedTexts = texts; +} +QStringList RegExpFilter::HotSpot::capturedTexts() const +{ + return _capturedTexts; +} + +void RegExpFilter::setRegExp(const QRegExp& regExp) +{ + _searchText = regExp; +} +QRegExp RegExpFilter::regExp() const +{ + return _searchText; +} +/*void RegExpFilter::reset(int) +{ + _buffer = QString(); +}*/ +void RegExpFilter::process() +{ + int pos = 0; + const QString* text = buffer(); + + Q_ASSERT( text ); + + // ignore any regular expressions which match an empty string. + // otherwise the while loop below will run indefinitely + static const QString emptyString(""); + if ( _searchText.exactMatch(emptyString) ) + return; + + while(pos >= 0) + { + pos = _searchText.indexIn(*text,pos); + + if ( pos >= 0 ) + { + + int startLine = 0; + int endLine = 0; + int startColumn = 0; + int endColumn = 0; + + + //kDebug() << "pos from " << pos << " to " << pos + _searchText.matchedLength(); + + getLineColumn(pos,startLine,startColumn); + getLineColumn(pos + _searchText.matchedLength(),endLine,endColumn); + + //kDebug() << "start " << startLine << " / " << startColumn; + //kDebug() << "end " << endLine << " / " << endColumn; + + RegExpFilter::HotSpot* spot = newHotSpot(startLine,startColumn, + endLine,endColumn); + spot->setCapturedTexts(_searchText.capturedTexts()); + + addHotSpot( spot ); + pos += _searchText.matchedLength(); + + // if matchedLength == 0, the program will get stuck in an infinite loop + Q_ASSERT( _searchText.matchedLength() > 0 ); + } + } +} + +RegExpFilter::HotSpot* RegExpFilter::newHotSpot(int startLine,int startColumn, + int endLine,int endColumn) +{ + return new RegExpFilter::HotSpot(startLine,startColumn, + endLine,endColumn); +} +RegExpFilter::HotSpot* UrlFilter::newHotSpot(int startLine,int startColumn,int endLine, + int endColumn) +{ + return new UrlFilter::HotSpot(startLine,startColumn, + endLine,endColumn); +} +UrlFilter::HotSpot::HotSpot(int startLine,int startColumn,int endLine,int endColumn) +: RegExpFilter::HotSpot(startLine,startColumn,endLine,endColumn) +, _urlObject(new FilterObject(this)) +{ + setType(Link); +} +QString UrlFilter::HotSpot::tooltip() const +{ + QString url = capturedTexts().first(); + + const UrlType kind = urlType(); + + if ( kind == StandardUrl ) + return QString(); + else if ( kind == Email ) + return QString(); + else + return QString(); +} +UrlFilter::HotSpot::UrlType UrlFilter::HotSpot::urlType() const +{ + QString url = capturedTexts().first(); + + if ( FullUrlRegExp.exactMatch(url) ) + return StandardUrl; + else if ( EmailAddressRegExp.exactMatch(url) ) + return Email; + else + return Unknown; +} + +void UrlFilter::HotSpot::activate(QObject* object) +{ + QString url = capturedTexts().first(); + + const UrlType kind = urlType(); + + const QString& actionName = object ? object->objectName() : QString(); + + if ( actionName == "copy-action" ) + { + //kDebug() << "Copying url to clipboard:" << url; + + QApplication::clipboard()->setText(url); + return; + } + + if ( !object || actionName == "open-action" ) + { + if ( kind == StandardUrl ) + { + // if the URL path does not include the protocol ( eg. "www.kde.org" ) then + // prepend http:// ( eg. "www.kde.org" --> "http://www.kde.org" ) + if (!url.contains("://")) + { + url.prepend("http://"); + } + } + else if ( kind == Email ) + { + url.prepend("mailto:"); + } + +// new KRun(url,QApplication::activeWindow()); + } +} + +// Note: Altering these regular expressions can have a major effect on the performance of the filters +// used for finding URLs in the text, especially if they are very general and could match very long +// pieces of text. +// Please be careful when altering them. + +//regexp matches: +// full url: +// protocolname:// or www. followed by anything other than whitespaces, <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, comma and dot +const QRegExp UrlFilter::FullUrlRegExp("(www\\.(?!\\.)|[a-z][a-z0-9+.-]*://)[^\\s<>'\"]+[^!,\\.\\s<>'\"\\]]"); +// email address: +// [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] +const QRegExp UrlFilter::EmailAddressRegExp("\\b(\\w|\\.|-)+@(\\w|\\.|-)+\\.\\w+\\b"); + +// matches full url or email address +const QRegExp UrlFilter::CompleteUrlRegExp('('+FullUrlRegExp.pattern()+'|'+ + EmailAddressRegExp.pattern()+')'); + +UrlFilter::UrlFilter() +{ + setRegExp( CompleteUrlRegExp ); +} +UrlFilter::HotSpot::~HotSpot() +{ + delete _urlObject; +} +void FilterObject::activated() +{ + _filter->activate(sender()); +} +QList<QAction*> UrlFilter::HotSpot::actions() +{ + QList<QAction*> list; + + const UrlType kind = urlType(); + + QAction* openAction = new QAction(_urlObject); + QAction* copyAction = new QAction(_urlObject);; + + Q_ASSERT( kind == StandardUrl || kind == Email ); + + if ( kind == StandardUrl ) + { + openAction->setText(("Open Link")); + copyAction->setText(("Copy Link Address")); + } + else if ( kind == Email ) + { + openAction->setText(("Send Email To...")); + copyAction->setText(("Copy Email Address")); + } + + // object names are set here so that the hotspot performs the + // correct action when activated() is called with the triggered + // action passed as a parameter. + openAction->setObjectName("open-action"); + copyAction->setObjectName("copy-action"); + + QObject::connect( openAction , SIGNAL(triggered()) , _urlObject , SLOT(activated()) ); + QObject::connect( copyAction , SIGNAL(triggered()) , _urlObject , SLOT(activated()) ); + + list << openAction; + list << copyAction; + + return list; +} + +//#include "moc_Filter.cpp"
new file mode 100644 --- /dev/null +++ b/gui//Filter.h @@ -0,0 +1,383 @@ +/* + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef FILTER_H +#define FILTER_H + +// Qt +#include <QtGui/QAction> +#include <QtCore/QList> +#include <QtCore/QObject> +#include <QtCore/QStringList> +#include <QtCore/QHash> +#include <QtCore/QRegExp> + +// Local +#include "Character.h" + +namespace Konsole +{ + +/** + * A filter processes blocks of text looking for certain patterns (such as URLs or keywords from a list) + * and marks the areas which match the filter's patterns as 'hotspots'. + * + * Each hotspot has a type identifier associated with it ( such as a link or a highlighted section ), + * and an action. When the user performs some activity such as a mouse-click in a hotspot area ( the exact + * action will depend on what is displaying the block of text which the filter is processing ), the hotspot's + * activate() method should be called. Depending on the type of hotspot this will trigger a suitable response. + * + * For example, if a hotspot represents a URL then a suitable action would be opening that URL in a web browser. + * Hotspots may have more than one action, in which case the list of actions can be obtained using the + * actions() method. + * + * Different subclasses of filter will return different types of hotspot. + * Subclasses must reimplement the process() method to examine a block of text and identify sections of interest. + * When processing the text they should create instances of Filter::HotSpot subclasses for sections of interest + * and add them to the filter's list of hotspots using addHotSpot() + */ +class Filter +{ +public: + /** + * Represents an area of text which matched the pattern a particular filter has been looking for. + * + * Each hotspot has a type identifier associated with it ( such as a link or a highlighted section ), + * and an action. When the user performs some activity such as a mouse-click in a hotspot area ( the exact + * action will depend on what is displaying the block of text which the filter is processing ), the hotspot's + * activate() method should be called. Depending on the type of hotspot this will trigger a suitable response. + * + * For example, if a hotspot represents a URL then a suitable action would be opening that URL in a web browser. + * Hotspots may have more than one action, in which case the list of actions can be obtained using the + * actions() method. These actions may then be displayed in a popup menu or toolbar for example. + */ + class HotSpot + { + public: + /** + * Constructs a new hotspot which covers the area from (@p startLine,@p startColumn) to (@p endLine,@p endColumn) + * in a block of text. + */ + HotSpot(int startLine , int startColumn , int endLine , int endColumn); + virtual ~HotSpot(); + + enum Type + { + // the type of the hotspot is not specified + NotSpecified, + // this hotspot represents a clickable link + Link, + // this hotspot represents a marker + Marker + }; + + /** Returns the line when the hotspot area starts */ + int startLine() const; + /** Returns the line where the hotspot area ends */ + int endLine() const; + /** Returns the column on startLine() where the hotspot area starts */ + int startColumn() const; + /** Returns the column on endLine() where the hotspot area ends */ + int endColumn() const; + /** + * Returns the type of the hotspot. This is usually used as a hint for views on how to represent + * the hotspot graphically. eg. Link hotspots are typically underlined when the user mouses over them + */ + Type type() const; + /** + * Causes the an action associated with a hotspot to be triggered. + * + * @param object The object which caused the hotspot to be triggered. This is + * typically null ( in which case the default action should be performed ) or + * one of the objects from the actions() list. In which case the associated + * action should be performed. + */ + virtual void activate(QObject* object = 0) = 0; + /** + * Returns a list of actions associated with the hotspot which can be used in a + * menu or toolbar + */ + virtual QList<QAction*> actions(); + + /** + * Returns the text of a tooltip to be shown when the mouse moves over the hotspot, or + * an empty string if there is no tooltip associated with this hotspot. + * + * The default implementation returns an empty string. + */ + virtual QString tooltip() const; + + protected: + /** Sets the type of a hotspot. This should only be set once */ + void setType(Type type); + + private: + int _startLine; + int _startColumn; + int _endLine; + int _endColumn; + Type _type; + + }; + + /** Constructs a new filter. */ + Filter(); + virtual ~Filter(); + + /** Causes the filter to process the block of text currently in its internal buffer */ + virtual void process() = 0; + + /** + * Empties the filters internal buffer and resets the line count back to 0. + * All hotspots are deleted. + */ + void reset(); + + /** Adds a new line of text to the filter and increments the line count */ + //void addLine(const QString& string); + + /** Returns the hotspot which covers the given @p line and @p column, or 0 if no hotspot covers that area */ + HotSpot* hotSpotAt(int line , int column) const; + + /** Returns the list of hotspots identified by the filter */ + QList<HotSpot*> hotSpots() const; + + /** Returns the list of hotspots identified by the filter which occur on a given line */ + QList<HotSpot*> hotSpotsAtLine(int line) const; + + /** + * TODO: Document me + */ + void setBuffer(const QString* buffer , const QList<int>* linePositions); + +protected: + /** Adds a new hotspot to the list */ + void addHotSpot(HotSpot*); + /** Returns the internal buffer */ + const QString* buffer(); + /** Converts a character position within buffer() to a line and column */ + void getLineColumn(int position , int& startLine , int& startColumn); + +private: + QMultiHash<int,HotSpot*> _hotspots; + QList<HotSpot*> _hotspotList; + + const QList<int>* _linePositions; + const QString* _buffer; +}; + +/** + * A filter which searches for sections of text matching a regular expression and creates a new RegExpFilter::HotSpot + * instance for them. + * + * Subclasses can reimplement newHotSpot() to return custom hotspot types when matches for the regular expression + * are found. + */ +class RegExpFilter : public Filter +{ +public: + /** + * Type of hotspot created by RegExpFilter. The capturedTexts() method can be used to find the text + * matched by the filter's regular expression. + */ + class HotSpot : public Filter::HotSpot + { + public: + HotSpot(int startLine, int startColumn, int endLine , int endColumn); + virtual void activate(QObject* object = 0); + + /** Sets the captured texts associated with this hotspot */ + void setCapturedTexts(const QStringList& texts); + /** Returns the texts found by the filter when matching the filter's regular expression */ + QStringList capturedTexts() const; + private: + QStringList _capturedTexts; + }; + + /** Constructs a new regular expression filter */ + RegExpFilter(); + + /** + * Sets the regular expression which the filter searches for in blocks of text. + * + * Regular expressions which match the empty string are treated as not matching + * anything. + */ + void setRegExp(const QRegExp& text); + /** Returns the regular expression which the filter searches for in blocks of text */ + QRegExp regExp() const; + + /** + * Reimplemented to search the filter's text buffer for text matching regExp() + * + * If regexp matches the empty string, then process() will return immediately + * without finding results. + */ + virtual void process(); + +protected: + /** + * Called when a match for the regular expression is encountered. Subclasses should reimplement this + * to return custom hotspot types + */ + virtual RegExpFilter::HotSpot* newHotSpot(int startLine,int startColumn, + int endLine,int endColumn); + +private: + QRegExp _searchText; +}; + +class FilterObject; + +/** A filter which matches URLs in blocks of text */ +class UrlFilter : public RegExpFilter +{ +public: + /** + * Hotspot type created by UrlFilter instances. The activate() method opens a web browser + * at the given URL when called. + */ + class HotSpot : public RegExpFilter::HotSpot + { + public: + HotSpot(int startLine,int startColumn,int endLine,int endColumn); + virtual ~HotSpot(); + + virtual QList<QAction*> actions(); + + /** + * Open a web browser at the current URL. The url itself can be determined using + * the capturedTexts() method. + */ + virtual void activate(QObject* object = 0); + + virtual QString tooltip() const; + private: + enum UrlType + { + StandardUrl, + Email, + Unknown + }; + UrlType urlType() const; + + FilterObject* _urlObject; + }; + + UrlFilter(); + +protected: + virtual RegExpFilter::HotSpot* newHotSpot(int,int,int,int); + +private: + + static const QRegExp FullUrlRegExp; + static const QRegExp EmailAddressRegExp; + + // combined OR of FullUrlRegExp and EmailAddressRegExp + static const QRegExp CompleteUrlRegExp; +}; + +class FilterObject : public QObject +{ +Q_OBJECT +public: + FilterObject(Filter::HotSpot* filter) : _filter(filter) {} +private slots: + void activated(); +private: + Filter::HotSpot* _filter; +}; + +/** + * A chain which allows a group of filters to be processed as one. + * The chain owns the filters added to it and deletes them when the chain itself is destroyed. + * + * Use addFilter() to add a new filter to the chain. + * When new text to be filtered arrives, use addLine() to add each additional + * line of text which needs to be processed and then after adding the last line, use + * process() to cause each filter in the chain to process the text. + * + * After processing a block of text, the reset() method can be used to set the filter chain's + * internal cursor back to the first line. + * + * The hotSpotAt() method will return the first hotspot which covers a given position. + * + * The hotSpots() and hotSpotsAtLine() method return all of the hotspots in the text and on + * a given line respectively. + */ +class FilterChain : protected QList<Filter*> +{ +public: + virtual ~FilterChain(); + + /** Adds a new filter to the chain. The chain will delete this filter when it is destroyed */ + void addFilter(Filter* filter); + /** Removes a filter from the chain. The chain will no longer delete the filter when destroyed */ + void removeFilter(Filter* filter); + /** Returns true if the chain contains @p filter */ + bool containsFilter(Filter* filter); + /** Removes all filters from the chain */ + void clear(); + + /** Resets each filter in the chain */ + void reset(); + /** + * Processes each filter in the chain + */ + void process(); + + /** Sets the buffer for each filter in the chain to process. */ + void setBuffer(const QString* buffer , const QList<int>* linePositions); + + /** Returns the first hotspot which occurs at @p line, @p column or 0 if no hotspot was found */ + Filter::HotSpot* hotSpotAt(int line , int column) const; + /** Returns a list of all the hotspots in all the chain's filters */ + QList<Filter::HotSpot*> hotSpots() const; + /** Returns a list of all hotspots at the given line in all the chain's filters */ + QList<Filter::HotSpot> hotSpotsAtLine(int line) const; + +}; + +/** A filter chain which processes character images from terminal displays */ +class TerminalImageFilterChain : public FilterChain +{ +public: + TerminalImageFilterChain(); + virtual ~TerminalImageFilterChain(); + + /** + * Set the current terminal image to @p image. + * + * @param image The terminal image + * @param lines The number of lines in the terminal image + * @param columns The number of columns in the terminal image + */ + void setImage(const Character* const image , int lines , int columns, + const QVector<LineProperty>& lineProperties); + +private: + QString* _buffer; + QList<int>* _linePositions; +}; + +} +#endif //FILTER_H
new file mode 100644 --- /dev/null +++ b/gui//History.cpp @@ -0,0 +1,698 @@ +/* + This file is part of Konsole, an X terminal. + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +// Own +#include "History.h" + +// System +#include <iostream> +#include <stdlib.h> +#include <assert.h> +#include <stdio.h> +#include <sys/types.h> +#include <sys/mman.h> +#include <unistd.h> +#include <errno.h> + + +// Reasonable line size +#define LINE_SIZE 1024 + +using namespace Konsole; + +/* + An arbitrary long scroll. + + One can modify the scroll only by adding either cells + or newlines, but access it randomly. + + The model is that of an arbitrary wide typewriter scroll + in that the scroll is a serie of lines and each line is + a serie of cells with no overwriting permitted. + + The implementation provides arbitrary length and numbers + of cells and line/column indexed read access to the scroll + at constant costs. + +KDE4: Can we use QTemporaryFile here, instead of KTempFile? + +FIXME: some complain about the history buffer comsuming the + memory of their machines. This problem is critical + since the history does not behave gracefully in cases + where the memory is used up completely. + + I put in a workaround that should handle it problem + now gracefully. I'm not satisfied with the solution. + +FIXME: Terminating the history is not properly indicated + in the menu. We should throw a signal. + +FIXME: There is noticeable decrease in speed, also. Perhaps, + there whole feature needs to be revisited therefore. + Disadvantage of a more elaborated, say block-oriented + scheme with wrap around would be it's complexity. +*/ + +//FIXME: tempory replacement for tmpfile +// this is here one for debugging purpose. + +//#define tmpfile xTmpFile + +// History File /////////////////////////////////////////// + +/* + A Row(X) data type which allows adding elements to the end. +*/ + +HistoryFile::HistoryFile() + : ion(-1), + length(0), + fileMap(0) +{ + if (tmpFile.open()) + { + tmpFile.setAutoRemove(true); + ion = tmpFile.handle(); + } +} + +HistoryFile::~HistoryFile() +{ + if (fileMap) + unmap(); +} + +//TODO: Mapping the entire file in will cause problems if the history file becomes exceedingly large, +//(ie. larger than available memory). HistoryFile::map() should only map in sections of the file at a time, +//to avoid this. +void HistoryFile::map() +{ + assert( fileMap == 0 ); + + fileMap = (char*)mmap( 0 , length , PROT_READ , MAP_PRIVATE , ion , 0 ); + + //if mmap'ing fails, fall back to the read-lseek combination + if ( fileMap == MAP_FAILED ) + { + readWriteBalance = 0; + fileMap = 0; + qDebug() << ": mmap'ing history failed. errno = " << errno; + } +} + +void HistoryFile::unmap() +{ + int result = munmap( fileMap , length ); + assert( result == 0 ); + + fileMap = 0; +} + +bool HistoryFile::isMapped() +{ + return (fileMap != 0); +} + +void HistoryFile::add(const unsigned char* bytes, int len) +{ + if ( fileMap ) + unmap(); + + readWriteBalance++; + + int rc = 0; + + rc = lseek(ion,length,SEEK_SET); if (rc < 0) { perror("HistoryFile::add.seek"); return; } + rc = write(ion,bytes,len); if (rc < 0) { perror("HistoryFile::add.write"); return; } + length += rc; +} + +void HistoryFile::get(unsigned char* bytes, int len, int loc) +{ + //count number of get() calls vs. number of add() calls. + //If there are many more get() calls compared with add() + //calls (decided by using MAP_THRESHOLD) then mmap the log + //file to improve performance. + readWriteBalance--; + if ( !fileMap && readWriteBalance < MAP_THRESHOLD ) + map(); + + if ( fileMap ) + { + for (int i=0;i<len;i++) + bytes[i]=fileMap[loc+i]; + } + else + { + int rc = 0; + + if (loc < 0 || len < 0 || loc + len > length) + fprintf(stderr,"getHist(...,%d,%d): invalid args.\n",len,loc); + rc = lseek(ion,loc,SEEK_SET); if (rc < 0) { perror("HistoryFile::get.seek"); return; } + rc = read(ion,bytes,len); if (rc < 0) { perror("HistoryFile::get.read"); return; } + } +} + +int HistoryFile::len() +{ + return length; +} + + +// History Scroll abstract base class ////////////////////////////////////// + + +HistoryScroll::HistoryScroll(HistoryType* t) + : m_histType(t) +{ +} + +HistoryScroll::~HistoryScroll() +{ + delete m_histType; +} + +bool HistoryScroll::hasScroll() +{ + return true; +} + +// History Scroll File ////////////////////////////////////// + +/* + The history scroll makes a Row(Row(Cell)) from + two history buffers. The index buffer contains + start of line positions which refere to the cells + buffer. + + Note that index[0] addresses the second line + (line #1), while the first line (line #0) starts + at 0 in cells. +*/ + +HistoryScrollFile::HistoryScrollFile(const QString &logFileName) + : HistoryScroll(new HistoryTypeFile(logFileName)), + m_logFileName(logFileName) +{ +} + +HistoryScrollFile::~HistoryScrollFile() +{ +} + +int HistoryScrollFile::getLines() +{ + return index.len() / sizeof(int); +} + +int HistoryScrollFile::getLineLen(int lineno) +{ + return (startOfLine(lineno+1) - startOfLine(lineno)) / sizeof(Character); +} + +bool HistoryScrollFile::isWrappedLine(int lineno) +{ + if (lineno>=0 && lineno <= getLines()) { + unsigned char flag; + lineflags.get((unsigned char*)&flag,sizeof(unsigned char),(lineno)*sizeof(unsigned char)); + return flag; + } + return false; +} + +int HistoryScrollFile::startOfLine(int lineno) +{ + if (lineno <= 0) return 0; + if (lineno <= getLines()) + { + + if (!index.isMapped()) + index.map(); + + int res; + index.get((unsigned char*)&res,sizeof(int),(lineno-1)*sizeof(int)); + return res; + } + return cells.len(); +} + +void HistoryScrollFile::getCells(int lineno, int colno, int count, Character res[]) +{ + cells.get((unsigned char*)res,count*sizeof(Character),startOfLine(lineno)+colno*sizeof(Character)); +} + +void HistoryScrollFile::addCells(const Character text[], int count) +{ + cells.add((unsigned char*)text,count*sizeof(Character)); +} + +void HistoryScrollFile::addLine(bool previousWrapped) +{ + if (index.isMapped()) + index.unmap(); + + int locn = cells.len(); + index.add((unsigned char*)&locn,sizeof(int)); + unsigned char flags = previousWrapped ? 0x01 : 0x00; + lineflags.add((unsigned char*)&flags,sizeof(unsigned char)); +} + + +// History Scroll Buffer ////////////////////////////////////// +HistoryScrollBuffer::HistoryScrollBuffer(unsigned int maxLineCount) + : HistoryScroll(new HistoryTypeBuffer(maxLineCount)) + ,_historyBuffer() + ,_maxLineCount(0) + ,_usedLines(0) + ,_head(0) +{ + setMaxNbLines(maxLineCount); +} + +HistoryScrollBuffer::~HistoryScrollBuffer() +{ + delete[] _historyBuffer; +} + +void HistoryScrollBuffer::addCellsVector(const QVector<Character>& cells) +{ + _head++; + if ( _usedLines < _maxLineCount ) + _usedLines++; + + if ( _head >= _maxLineCount ) + { + _head = 0; + } + + _historyBuffer[bufferIndex(_usedLines-1)] = cells; + _wrappedLine[bufferIndex(_usedLines-1)] = false; +} +void HistoryScrollBuffer::addCells(const Character a[], int count) +{ + HistoryLine newLine(count); + qCopy(a,a+count,newLine.begin()); + + addCellsVector(newLine); +} + +void HistoryScrollBuffer::addLine(bool previousWrapped) +{ + _wrappedLine[bufferIndex(_usedLines-1)] = previousWrapped; +} + +int HistoryScrollBuffer::getLines() +{ + return _usedLines; +} + +int HistoryScrollBuffer::getLineLen(int lineNumber) +{ + Q_ASSERT( lineNumber >= 0 && lineNumber < _maxLineCount ); + + if ( lineNumber < _usedLines ) + { + return _historyBuffer[bufferIndex(lineNumber)].size(); + } + else + { + return 0; + } +} + +bool HistoryScrollBuffer::isWrappedLine(int lineNumber) +{ + Q_ASSERT( lineNumber >= 0 && lineNumber < _maxLineCount ); + + if (lineNumber < _usedLines) + { + //kDebug() << "Line" << lineNumber << "wrapped is" << _wrappedLine[bufferIndex(lineNumber)]; + return _wrappedLine[bufferIndex(lineNumber)]; + } + else + return false; +} + +void HistoryScrollBuffer::getCells(int lineNumber, int startColumn, int count, Character* buffer) +{ + if ( count == 0 ) return; + + Q_ASSERT( lineNumber < _maxLineCount ); + + if (lineNumber >= _usedLines) + { + memset(buffer, 0, count * sizeof(Character)); + return; + } + + const HistoryLine& line = _historyBuffer[bufferIndex(lineNumber)]; + + //kDebug() << "startCol " << startColumn; + //kDebug() << "line.size() " << line.size(); + //kDebug() << "count " << count; + + Q_ASSERT( startColumn <= line.size() - count ); + + memcpy(buffer, line.constData() + startColumn , count * sizeof(Character)); +} + +void HistoryScrollBuffer::setMaxNbLines(unsigned int lineCount) +{ + HistoryLine* oldBuffer = _historyBuffer; + HistoryLine* newBuffer = new HistoryLine[lineCount]; + + for ( int i = 0 ; i < qMin(_usedLines,(int)lineCount) ; i++ ) + { + newBuffer[i] = oldBuffer[bufferIndex(i)]; + } + + _usedLines = qMin(_usedLines,(int)lineCount); + _maxLineCount = lineCount; + _head = ( _usedLines == _maxLineCount ) ? 0 : _usedLines-1; + + _historyBuffer = newBuffer; + delete[] oldBuffer; + + _wrappedLine.resize(lineCount); +} + +int HistoryScrollBuffer::bufferIndex(int lineNumber) +{ + Q_ASSERT( lineNumber >= 0 ); + Q_ASSERT( lineNumber < _maxLineCount ); + Q_ASSERT( (_usedLines == _maxLineCount) || lineNumber <= _head ); + + if ( _usedLines == _maxLineCount ) + { + return (_head+lineNumber+1) % _maxLineCount; + } + else + { + return lineNumber; + } +} + + +// History Scroll None ////////////////////////////////////// + +HistoryScrollNone::HistoryScrollNone() + : HistoryScroll(new HistoryTypeNone()) +{ +} + +HistoryScrollNone::~HistoryScrollNone() +{ +} + +bool HistoryScrollNone::hasScroll() +{ + return false; +} + +int HistoryScrollNone::getLines() +{ + return 0; +} + +int HistoryScrollNone::getLineLen(int) +{ + return 0; +} + +bool HistoryScrollNone::isWrappedLine(int /*lineno*/) +{ + return false; +} + +void HistoryScrollNone::getCells(int, int, int, Character []) +{ +} + +void HistoryScrollNone::addCells(const Character [], int) +{ +} + +void HistoryScrollNone::addLine(bool) +{ +} + +// History Scroll BlockArray ////////////////////////////////////// + +HistoryScrollBlockArray::HistoryScrollBlockArray(size_t size) + : HistoryScroll(new HistoryTypeBlockArray(size)) +{ + m_blockArray.setHistorySize(size); // nb. of lines. +} + +HistoryScrollBlockArray::~HistoryScrollBlockArray() +{ +} + +int HistoryScrollBlockArray::getLines() +{ + return m_lineLengths.count(); +} + +int HistoryScrollBlockArray::getLineLen(int lineno) +{ + if ( m_lineLengths.contains(lineno) ) + return m_lineLengths[lineno]; + else + return 0; +} + +bool HistoryScrollBlockArray::isWrappedLine(int /*lineno*/) +{ + return false; +} + +void HistoryScrollBlockArray::getCells(int lineno, int colno, + int count, Character res[]) +{ + if (!count) return; + + const Block *b = m_blockArray.at(lineno); + + if (!b) { + memset(res, 0, count * sizeof(Character)); // still better than random data + return; + } + + assert(((colno + count) * sizeof(Character)) < ENTRIES); + memcpy(res, b->data + (colno * sizeof(Character)), count * sizeof(Character)); +} + +void HistoryScrollBlockArray::addCells(const Character a[], int count) +{ + Block *b = m_blockArray.lastBlock(); + + if (!b) return; + + // put cells in block's data + assert((count * sizeof(Character)) < ENTRIES); + + memset(b->data, 0, ENTRIES); + + memcpy(b->data, a, count * sizeof(Character)); + b->size = count * sizeof(Character); + + size_t res = m_blockArray.newBlock(); + assert (res > 0); + Q_UNUSED( res ); + + m_lineLengths.insert(m_blockArray.getCurrent(), count); +} + +void HistoryScrollBlockArray::addLine(bool) +{ +} + +////////////////////////////////////////////////////////////////////// +// History Types +////////////////////////////////////////////////////////////////////// + +HistoryType::HistoryType() +{ +} + +HistoryType::~HistoryType() +{ +} + +////////////////////////////// + +HistoryTypeNone::HistoryTypeNone() +{ +} + +bool HistoryTypeNone::isEnabled() const +{ + return false; +} + +HistoryScroll* HistoryTypeNone::scroll(HistoryScroll *old) const +{ + delete old; + return new HistoryScrollNone(); +} + +int HistoryTypeNone::maximumLineCount() const +{ + return 0; +} + +////////////////////////////// + +HistoryTypeBlockArray::HistoryTypeBlockArray(size_t size) + : m_size(size) +{ +} + +bool HistoryTypeBlockArray::isEnabled() const +{ + return true; +} + +int HistoryTypeBlockArray::maximumLineCount() const +{ + return m_size; +} + +HistoryScroll* HistoryTypeBlockArray::scroll(HistoryScroll *old) const +{ + delete old; + return new HistoryScrollBlockArray(m_size); +} + + +////////////////////////////// + +HistoryTypeBuffer::HistoryTypeBuffer(unsigned int nbLines) + : m_nbLines(nbLines) +{ +} + +bool HistoryTypeBuffer::isEnabled() const +{ + return true; +} + +int HistoryTypeBuffer::maximumLineCount() const +{ + return m_nbLines; +} + +HistoryScroll* HistoryTypeBuffer::scroll(HistoryScroll *old) const +{ + if (old) + { + HistoryScrollBuffer *oldBuffer = dynamic_cast<HistoryScrollBuffer*>(old); + if (oldBuffer) + { + oldBuffer->setMaxNbLines(m_nbLines); + return oldBuffer; + } + + HistoryScroll *newScroll = new HistoryScrollBuffer(m_nbLines); + int lines = old->getLines(); + int startLine = 0; + if (lines > (int) m_nbLines) + startLine = lines - m_nbLines; + + Character line[LINE_SIZE]; + for(int i = startLine; i < lines; i++) + { + int size = old->getLineLen(i); + if (size > LINE_SIZE) + { + Character *tmp_line = new Character[size]; + old->getCells(i, 0, size, tmp_line); + newScroll->addCells(tmp_line, size); + newScroll->addLine(old->isWrappedLine(i)); + delete [] tmp_line; + } + else + { + old->getCells(i, 0, size, line); + newScroll->addCells(line, size); + newScroll->addLine(old->isWrappedLine(i)); + } + } + delete old; + return newScroll; + } + return new HistoryScrollBuffer(m_nbLines); +} + +////////////////////////////// + +HistoryTypeFile::HistoryTypeFile(const QString& fileName) + : m_fileName(fileName) +{ +} + +bool HistoryTypeFile::isEnabled() const +{ + return true; +} + +const QString& HistoryTypeFile::getFileName() const +{ + return m_fileName; +} + +HistoryScroll* HistoryTypeFile::scroll(HistoryScroll *old) const +{ + if (dynamic_cast<HistoryFile *>(old)) + return old; // Unchanged. + + HistoryScroll *newScroll = new HistoryScrollFile(m_fileName); + + Character line[LINE_SIZE]; + int lines = (old != 0) ? old->getLines() : 0; + for(int i = 0; i < lines; i++) + { + int size = old->getLineLen(i); + if (size > LINE_SIZE) + { + Character *tmp_line = new Character[size]; + old->getCells(i, 0, size, tmp_line); + newScroll->addCells(tmp_line, size); + newScroll->addLine(old->isWrappedLine(i)); + delete [] tmp_line; + } + else + { + old->getCells(i, 0, size, line); + newScroll->addCells(line, size); + newScroll->addLine(old->isWrappedLine(i)); + } + } + + delete old; + return newScroll; +} + +int HistoryTypeFile::maximumLineCount() const +{ + return 0; +}
new file mode 100644 --- /dev/null +++ b/gui//History.h @@ -0,0 +1,344 @@ +/* + This file is part of Konsole, an X terminal. + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef TEHISTORY_H +#define TEHISTORY_H + +// Qt +#include <QtCore/QBitRef> +#include <QtCore/QHash> +#include <QtCore> + +// Konsole +#include "BlockArray.h" +#include "Character.h" + +namespace Konsole +{ + +#if 1 +/* + An extendable tmpfile(1) based buffer. +*/ + +class HistoryFile +{ +public: + HistoryFile(); + virtual ~HistoryFile(); + + virtual void add(const unsigned char* bytes, int len); + virtual void get(unsigned char* bytes, int len, int loc); + virtual int len(); + + //mmaps the file in read-only mode + void map(); + //un-mmaps the file + void unmap(); + //returns true if the file is mmap'ed + bool isMapped(); + + +private: + int ion; + int length; + QTemporaryFile tmpFile; + + //pointer to start of mmap'ed file data, or 0 if the file is not mmap'ed + char* fileMap; + + //incremented whenver 'add' is called and decremented whenever + //'get' is called. + //this is used to detect when a large number of lines are being read and processed from the history + //and automatically mmap the file for better performance (saves the overhead of many lseek-read calls). + int readWriteBalance; + + //when readWriteBalance goes below this threshold, the file will be mmap'ed automatically + static const int MAP_THRESHOLD = -1000; +}; +#endif + +////////////////////////////////////////////////////////////////////// + +////////////////////////////////////////////////////////////////////// +// Abstract base class for file and buffer versions +////////////////////////////////////////////////////////////////////// +class HistoryType; + +class HistoryScroll +{ +public: + HistoryScroll(HistoryType*); + virtual ~HistoryScroll(); + + virtual bool hasScroll(); + + // access to history + virtual int getLines() = 0; + virtual int getLineLen(int lineno) = 0; + virtual void getCells(int lineno, int colno, int count, Character res[]) = 0; + virtual bool isWrappedLine(int lineno) = 0; + + // backward compatibility (obsolete) + Character getCell(int lineno, int colno) { Character res; getCells(lineno,colno,1,&res); return res; } + + // adding lines. + virtual void addCells(const Character a[], int count) = 0; + // convenience method - this is virtual so that subclasses can take advantage + // of QVector's implicit copying + virtual void addCellsVector(const QVector<Character>& cells) + { + addCells(cells.data(),cells.size()); + } + + virtual void addLine(bool previousWrapped=false) = 0; + + // + // FIXME: Passing around constant references to HistoryType instances + // is very unsafe, because those references will no longer + // be valid if the history scroll is deleted. + // + const HistoryType& getType() { return *m_histType; } + +protected: + HistoryType* m_histType; + +}; + +#if 1 + +////////////////////////////////////////////////////////////////////// +// File-based history (e.g. file log, no limitation in length) +////////////////////////////////////////////////////////////////////// + +class HistoryScrollFile : public HistoryScroll +{ +public: + HistoryScrollFile(const QString &logFileName); + virtual ~HistoryScrollFile(); + + virtual int getLines(); + virtual int getLineLen(int lineno); + virtual void getCells(int lineno, int colno, int count, Character res[]); + virtual bool isWrappedLine(int lineno); + + virtual void addCells(const Character a[], int count); + virtual void addLine(bool previousWrapped=false); + +private: + int startOfLine(int lineno); + + QString m_logFileName; + HistoryFile index; // lines Row(int) + HistoryFile cells; // text Row(Character) + HistoryFile lineflags; // flags Row(unsigned char) +}; + + +////////////////////////////////////////////////////////////////////// +// Buffer-based history (limited to a fixed nb of lines) +////////////////////////////////////////////////////////////////////// +class HistoryScrollBuffer : public HistoryScroll +{ +public: + typedef QVector<Character> HistoryLine; + + HistoryScrollBuffer(unsigned int maxNbLines = 1000); + virtual ~HistoryScrollBuffer(); + + virtual int getLines(); + virtual int getLineLen(int lineno); + virtual void getCells(int lineno, int colno, int count, Character res[]); + virtual bool isWrappedLine(int lineno); + + virtual void addCells(const Character a[], int count); + virtual void addCellsVector(const QVector<Character>& cells); + virtual void addLine(bool previousWrapped=false); + + void setMaxNbLines(unsigned int nbLines); + unsigned int maxNbLines() { return _maxLineCount; } + + +private: + int bufferIndex(int lineNumber); + + HistoryLine* _historyBuffer; + QBitArray _wrappedLine; + int _maxLineCount; + int _usedLines; + int _head; + + //QVector<histline*> m_histBuffer; + //QBitArray m_wrappedLine; + //unsigned int m_maxNbLines; + //unsigned int m_nbLines; + //unsigned int m_arrayIndex; + //bool m_buffFilled; +}; + +/*class HistoryScrollBufferV2 : public HistoryScroll +{ +public: + virtual int getLines(); + virtual int getLineLen(int lineno); + virtual void getCells(int lineno, int colno, int count, Character res[]); + virtual bool isWrappedLine(int lineno); + + virtual void addCells(const Character a[], int count); + virtual void addCells(const QVector<Character>& cells); + virtual void addLine(bool previousWrapped=false); + +};*/ + +#endif + +////////////////////////////////////////////////////////////////////// +// Nothing-based history (no history :-) +////////////////////////////////////////////////////////////////////// +class HistoryScrollNone : public HistoryScroll +{ +public: + HistoryScrollNone(); + virtual ~HistoryScrollNone(); + + virtual bool hasScroll(); + + virtual int getLines(); + virtual int getLineLen(int lineno); + virtual void getCells(int lineno, int colno, int count, Character res[]); + virtual bool isWrappedLine(int lineno); + + virtual void addCells(const Character a[], int count); + virtual void addLine(bool previousWrapped=false); +}; + +////////////////////////////////////////////////////////////////////// +// BlockArray-based history +////////////////////////////////////////////////////////////////////// +class HistoryScrollBlockArray : public HistoryScroll +{ +public: + HistoryScrollBlockArray(size_t size); + virtual ~HistoryScrollBlockArray(); + + virtual int getLines(); + virtual int getLineLen(int lineno); + virtual void getCells(int lineno, int colno, int count, Character res[]); + virtual bool isWrappedLine(int lineno); + + virtual void addCells(const Character a[], int count); + virtual void addLine(bool previousWrapped=false); + +protected: + BlockArray m_blockArray; + QHash<int,size_t> m_lineLengths; +}; + +////////////////////////////////////////////////////////////////////// +// History type +////////////////////////////////////////////////////////////////////// + +class HistoryType +{ +public: + HistoryType(); + virtual ~HistoryType(); + + /** + * Returns true if the history is enabled ( can store lines of output ) + * or false otherwise. + */ + virtual bool isEnabled() const = 0; + /** + * Returns true if the history size is unlimited. + */ + bool isUnlimited() const { return maximumLineCount() == 0; } + /** + * Returns the maximum number of lines which this history type + * can store or 0 if the history can store an unlimited number of lines. + */ + virtual int maximumLineCount() const = 0; + + virtual HistoryScroll* scroll(HistoryScroll *) const = 0; +}; + +class HistoryTypeNone : public HistoryType +{ +public: + HistoryTypeNone(); + + virtual bool isEnabled() const; + virtual int maximumLineCount() const; + + virtual HistoryScroll* scroll(HistoryScroll *) const; +}; + +class HistoryTypeBlockArray : public HistoryType +{ +public: + HistoryTypeBlockArray(size_t size); + + virtual bool isEnabled() const; + virtual int maximumLineCount() const; + + virtual HistoryScroll* scroll(HistoryScroll *) const; + +protected: + size_t m_size; +}; + +#if 1 +class HistoryTypeFile : public HistoryType +{ +public: + HistoryTypeFile(const QString& fileName=QString()); + + virtual bool isEnabled() const; + virtual const QString& getFileName() const; + virtual int maximumLineCount() const; + + virtual HistoryScroll* scroll(HistoryScroll *) const; + +protected: + QString m_fileName; +}; + + +class HistoryTypeBuffer : public HistoryType +{ +public: + HistoryTypeBuffer(unsigned int nbLines); + + virtual bool isEnabled() const; + virtual int maximumLineCount() const; + + virtual HistoryScroll* scroll(HistoryScroll *) const; + +protected: + unsigned int m_nbLines; +}; + +#endif + +} + +#endif // TEHISTORY_H
new file mode 100644 --- /dev/null +++ b/gui//KeyboardTranslator.cpp @@ -0,0 +1,903 @@ +/* + This source file was part of Konsole, a terminal emulator. + + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +// Own +#include "KeyboardTranslator.h" + +// System +#include <ctype.h> +#include <stdio.h> + +// Qt +#include <QtCore/QBuffer> +//#include <KDebug> +#include <QtCore/QFile> +#include <QtCore/QFileInfo> +#include <QtCore> +#include <QtGui> + +// KDE +//#include <KDebug> +//#include <KLocale> +//#include <KStandardDirs> + +using namespace Konsole; + +//this is for default REALLY fallback translator. + +//const char* KeyboardTranslatorManager::defaultTranslatorText = +//#include "DefaultTranslatorText.h" +//; + +//and this is default now translator - default.keytab from original Konsole +const char* KeyboardTranslatorManager::defaultTranslatorText = +#include "ExtendedDefaultTranslator.h" +; + +KeyboardTranslatorManager::KeyboardTranslatorManager() + : _haveLoadedAll(false) +{ +} +KeyboardTranslatorManager::~KeyboardTranslatorManager() +{ + qDeleteAll(_translators.values()); +} +QString KeyboardTranslatorManager::findTranslatorPath(const QString& name) +{ + return QString("kb-layouts/" + name + ".keytab"); +} +void KeyboardTranslatorManager::findTranslators() +{ + QDir dir("kb-layouts/"); + QStringList filters; + filters << "*.keytab"; + dir.setNameFilters(filters); + QStringList list = dir.entryList(filters); //(".keytab"); // = KGlobal::dirs()->findAllResources("data", + // "konsole/*.keytab", + // KStandardDirs::NoDuplicates); + list = dir.entryList(filters); + // add the name of each translator to the list and associated + // the name with a null pointer to indicate that the translator + // has not yet been loaded from disk + QStringListIterator listIter(list); + while (listIter.hasNext()) + { + QString translatorPath = listIter.next(); + + QString name = QFileInfo(translatorPath).baseName(); + + if ( !_translators.contains(name) ) { + _translators.insert(name,0); + } + } + _haveLoadedAll = true; +} + +const KeyboardTranslator* KeyboardTranslatorManager::findTranslator(const QString& name) +{ + if ( name.isEmpty() ) + return defaultTranslator(); + +//here was smth wrong in original Konsole source + findTranslators(); + + if ( _translators.contains(name) && _translators[name] != 0 ) { + return _translators[name]; + } + + KeyboardTranslator* translator = loadTranslator(name); + + if ( translator != 0 ) + _translators[name] = translator; + else if ( !name.isEmpty() ) + qWarning() << "Unable to load translator" << name; + + return translator; +} + +bool KeyboardTranslatorManager::saveTranslator(const KeyboardTranslator* translator) +{ + const QString path = ".keytab";// = KGlobal::dirs()->saveLocation("data","konsole/")+translator->name() +// +".keytab"; + + qDebug() << "Saving translator to" << path; + + QFile destination(path); + + if (!destination.open(QIODevice::WriteOnly | QIODevice::Text)) + { + qWarning() << "Unable to save keyboard translation:" + << destination.errorString(); + + return false; + } + + { + KeyboardTranslatorWriter writer(&destination); + writer.writeHeader(translator->description()); + + QListIterator<KeyboardTranslator::Entry> iter(translator->entries()); + while ( iter.hasNext() ) + writer.writeEntry(iter.next()); + } + + destination.close(); + + return true; +} + +KeyboardTranslator* KeyboardTranslatorManager::loadTranslator(const QString& name) +{ + const QString& path = findTranslatorPath(name); + + QFile source(path); + + if (name.isEmpty() || !source.open(QIODevice::ReadOnly | QIODevice::Text)) + return 0; + + return loadTranslator(&source,name); +} + +const KeyboardTranslator* KeyboardTranslatorManager::defaultTranslator() +{ + qDebug() << "Loading default translator from text"; + QBuffer textBuffer; + textBuffer.setData(defaultTranslatorText,strlen(defaultTranslatorText)); + + if (!textBuffer.open(QIODevice::ReadOnly)) + return 0; + + return loadTranslator(&textBuffer,"fallback"); +} + +KeyboardTranslator* KeyboardTranslatorManager::loadTranslator(QIODevice* source,const QString& name) +{ + KeyboardTranslator* translator = new KeyboardTranslator(name); + KeyboardTranslatorReader reader(source); + translator->setDescription( reader.description() ); + + while ( reader.hasNextEntry() ) { + translator->addEntry(reader.nextEntry()); + } + + source->close(); + + if ( !reader.parseError() ) + { + return translator; + } + else + { + delete translator; + return 0; + } +} + +KeyboardTranslatorWriter::KeyboardTranslatorWriter(QIODevice* destination) +: _destination(destination) +{ + Q_ASSERT( destination && destination->isWritable() ); + + _writer = new QTextStream(_destination); +} +KeyboardTranslatorWriter::~KeyboardTranslatorWriter() +{ + delete _writer; +} +void KeyboardTranslatorWriter::writeHeader( const QString& description ) +{ + *_writer << "keyboard \"" << description << '\"' << '\n'; +} +void KeyboardTranslatorWriter::writeEntry( const KeyboardTranslator::Entry& entry ) +{ + QString result; + + if ( entry.command() != KeyboardTranslator::NoCommand ) + result = entry.resultToString(); + else + result = '\"' + entry.resultToString() + '\"'; + + *_writer << "key " << entry.conditionToString() << " : " << result << '\n'; +} + + +// each line of the keyboard translation file is one of: +// +// - keyboard "name" +// - key KeySequence : "characters" +// - key KeySequence : CommandName +// +// KeySequence begins with the name of the key ( taken from the Qt::Key enum ) +// and is followed by the keyboard modifiers and state flags ( with + or - in front +// of each modifier or flag to indicate whether it is required ). All keyboard modifiers +// and flags are optional, if a particular modifier or state is not specified it is +// assumed not to be a part of the sequence. The key sequence may contain whitespace +// +// eg: "key Up+Shift : scrollLineUp" +// "key Next-Shift : "\E[6~" +// +// (lines containing only whitespace are ignored, parseLine assumes that comments have +// already been removed) +// + +KeyboardTranslatorReader::KeyboardTranslatorReader( QIODevice* source ) + : _source(source) + , _hasNext(false) +{ + // read input until we find the description + while ( _description.isEmpty() && !source->atEnd() ) + { + const QList<Token>& tokens = tokenize( QString(source->readLine()) ); + + if ( !tokens.isEmpty() && tokens.first().type == Token::TitleKeyword ) + { + _description = (tokens[1].text.toUtf8()); + } + } + + readNext(); +} +void KeyboardTranslatorReader::readNext() +{ + // find next entry + while ( !_source->atEnd() ) + { + const QList<Token>& tokens = tokenize( QString(_source->readLine()) ); + if ( !tokens.isEmpty() && tokens.first().type == Token::KeyKeyword ) + { + KeyboardTranslator::States flags = KeyboardTranslator::NoState; + KeyboardTranslator::States flagMask = KeyboardTranslator::NoState; + Qt::KeyboardModifiers modifiers = Qt::NoModifier; + Qt::KeyboardModifiers modifierMask = Qt::NoModifier; + + int keyCode = Qt::Key_unknown; + + decodeSequence(tokens[1].text.toLower(), + keyCode, + modifiers, + modifierMask, + flags, + flagMask); + + KeyboardTranslator::Command command = KeyboardTranslator::NoCommand; + QByteArray text; + + // get text or command + if ( tokens[2].type == Token::OutputText ) + { + text = tokens[2].text.toLocal8Bit(); + } + else if ( tokens[2].type == Token::Command ) + { + // identify command + if (!parseAsCommand(tokens[2].text,command)) + qWarning() << "Command" << tokens[2].text << "not understood."; + } + + KeyboardTranslator::Entry newEntry; + newEntry.setKeyCode( keyCode ); + newEntry.setState( flags ); + newEntry.setStateMask( flagMask ); + newEntry.setModifiers( modifiers ); + newEntry.setModifierMask( modifierMask ); + newEntry.setText( text ); + newEntry.setCommand( command ); + + _nextEntry = newEntry; + + _hasNext = true; + + return; + } + } + + _hasNext = false; +} + +bool KeyboardTranslatorReader::parseAsCommand(const QString& text,KeyboardTranslator::Command& command) +{ + if ( text.compare("erase",Qt::CaseInsensitive) == 0 ) + command = KeyboardTranslator::EraseCommand; + else if ( text.compare("scrollpageup",Qt::CaseInsensitive) == 0 ) + command = KeyboardTranslator::ScrollPageUpCommand; + else if ( text.compare("scrollpagedown",Qt::CaseInsensitive) == 0 ) + command = KeyboardTranslator::ScrollPageDownCommand; + else if ( text.compare("scrolllineup",Qt::CaseInsensitive) == 0 ) + command = KeyboardTranslator::ScrollLineUpCommand; + else if ( text.compare("scrolllinedown",Qt::CaseInsensitive) == 0 ) + command = KeyboardTranslator::ScrollLineDownCommand; + else if ( text.compare("scrolllock",Qt::CaseInsensitive) == 0 ) + command = KeyboardTranslator::ScrollLockCommand; + else + return false; + + return true; +} + +bool KeyboardTranslatorReader::decodeSequence(const QString& text, + int& keyCode, + Qt::KeyboardModifiers& modifiers, + Qt::KeyboardModifiers& modifierMask, + KeyboardTranslator::States& flags, + KeyboardTranslator::States& flagMask) +{ + bool isWanted = true; + bool endOfItem = false; + QString buffer; + + Qt::KeyboardModifiers tempModifiers = modifiers; + Qt::KeyboardModifiers tempModifierMask = modifierMask; + KeyboardTranslator::States tempFlags = flags; + KeyboardTranslator::States tempFlagMask = flagMask; + + for ( int i = 0 ; i < text.count() ; i++ ) + { + const QChar& ch = text[i]; + bool isLastLetter = ( i == text.count()-1 ); + + endOfItem = true; + if ( ch.isLetterOrNumber() ) + { + endOfItem = false; + buffer.append(ch); + } + + if ( (endOfItem || isLastLetter) && !buffer.isEmpty() ) + { + Qt::KeyboardModifier itemModifier = Qt::NoModifier; + int itemKeyCode = 0; + KeyboardTranslator::State itemFlag = KeyboardTranslator::NoState; + + if ( parseAsModifier(buffer,itemModifier) ) + { + tempModifierMask |= itemModifier; + + if ( isWanted ) + tempModifiers |= itemModifier; + } + else if ( parseAsStateFlag(buffer,itemFlag) ) + { + tempFlagMask |= itemFlag; + + if ( isWanted ) + tempFlags |= itemFlag; + } + else if ( parseAsKeyCode(buffer,itemKeyCode) ) + keyCode = itemKeyCode; + else + qDebug() << "Unable to parse key binding item:" << buffer; + + buffer.clear(); + } + + // check if this is a wanted / not-wanted flag and update the + // state ready for the next item + if ( ch == '+' ) + isWanted = true; + else if ( ch == '-' ) + isWanted = false; + } + + modifiers = tempModifiers; + modifierMask = tempModifierMask; + flags = tempFlags; + flagMask = tempFlagMask; + + return true; +} + +bool KeyboardTranslatorReader::parseAsModifier(const QString& item , Qt::KeyboardModifier& modifier) +{ + if ( item == "shift" ) + modifier = Qt::ShiftModifier; + else if ( item == "ctrl" || item == "control" ) + modifier = Qt::ControlModifier; + else if ( item == "alt" ) + modifier = Qt::AltModifier; + else if ( item == "meta" ) + modifier = Qt::MetaModifier; + else if ( item == "keypad" ) + modifier = Qt::KeypadModifier; + else + return false; + + return true; +} +bool KeyboardTranslatorReader::parseAsStateFlag(const QString& item , KeyboardTranslator::State& flag) +{ + if ( item == "appcukeys" ) + flag = KeyboardTranslator::CursorKeysState; + else if ( item == "ansi" ) + flag = KeyboardTranslator::AnsiState; + else if ( item == "newline" ) + flag = KeyboardTranslator::NewLineState; + else if ( item == "appscreen" ) + flag = KeyboardTranslator::AlternateScreenState; + else if ( item == "anymod" ) + flag = KeyboardTranslator::AnyModifierState; + else + return false; + + return true; +} +bool KeyboardTranslatorReader::parseAsKeyCode(const QString& item , int& keyCode) +{ + QKeySequence sequence = QKeySequence::fromString(item); + if ( !sequence.isEmpty() ) + { + keyCode = sequence[0]; + + if ( sequence.count() > 1 ) + { + qDebug() << "Unhandled key codes in sequence: " << item; + } + } + // additional cases implemented for backwards compatibility with KDE 3 + else if ( item == "prior" ) + keyCode = Qt::Key_PageUp; + else if ( item == "next" ) + keyCode = Qt::Key_PageDown; + else + return false; + + return true; +} + +QString KeyboardTranslatorReader::description() const +{ + return _description; +} +bool KeyboardTranslatorReader::hasNextEntry() +{ + return _hasNext; +} +KeyboardTranslator::Entry KeyboardTranslatorReader::createEntry( const QString& condition , + const QString& result ) +{ + QString entryString("keyboard \"temporary\"\nkey "); + entryString.append(condition); + entryString.append(" : "); + + // if 'result' is the name of a command then the entry result will be that command, + // otherwise the result will be treated as a string to echo when the key sequence + // specified by 'condition' is pressed + KeyboardTranslator::Command command; + if (parseAsCommand(result,command)) + entryString.append(result); + else + entryString.append('\"' + result + '\"'); + + QByteArray array = entryString.toUtf8(); + + KeyboardTranslator::Entry entry; + + QBuffer buffer(&array); + buffer.open(QIODevice::ReadOnly); + KeyboardTranslatorReader reader(&buffer); + + if ( reader.hasNextEntry() ) + entry = reader.nextEntry(); + + return entry; +} + +KeyboardTranslator::Entry KeyboardTranslatorReader::nextEntry() +{ + Q_ASSERT( _hasNext ); + + + KeyboardTranslator::Entry entry = _nextEntry; + + readNext(); + + return entry; +} +bool KeyboardTranslatorReader::parseError() +{ + return false; +} +QList<KeyboardTranslatorReader::Token> KeyboardTranslatorReader::tokenize(const QString& line) +{ + QString text = line.simplified(); + + // comment line: # comment + static QRegExp comment("\\#.*"); + // title line: keyboard "title" + static QRegExp title("keyboard\\s+\"(.*)\""); + // key line: key KeySequence : "output" + // key line: key KeySequence : command + static QRegExp key("key\\s+([\\w\\+\\s\\-]+)\\s*:\\s*(\"(.*)\"|\\w+)"); + + QList<Token> list; + + if ( text.isEmpty() || comment.exactMatch(text) ) + { + return list; + } + + if ( title.exactMatch(text) ) + { + Token titleToken = { Token::TitleKeyword , QString() }; + Token textToken = { Token::TitleText , title.capturedTexts()[1] }; + + list << titleToken << textToken; + } + else if ( key.exactMatch(text) ) + { + Token keyToken = { Token::KeyKeyword , QString() }; + Token sequenceToken = { Token::KeySequence , key.capturedTexts()[1].remove(' ') }; + + list << keyToken << sequenceToken; + + if ( key.capturedTexts()[3].isEmpty() ) + { + // capturedTexts()[2] is a command + Token commandToken = { Token::Command , key.capturedTexts()[2] }; + list << commandToken; + } + else + { + // capturedTexts()[3] is the output string + Token outputToken = { Token::OutputText , key.capturedTexts()[3] }; + list << outputToken; + } + } + else + { + qWarning() << "Line in keyboard translator file could not be understood:" << text; + } + + return list; +} + +QList<QString> KeyboardTranslatorManager::allTranslators() +{ + if ( !_haveLoadedAll ) + { + findTranslators(); + } + + return _translators.keys(); +} + +KeyboardTranslator::Entry::Entry() +: _keyCode(0) +, _modifiers(Qt::NoModifier) +, _modifierMask(Qt::NoModifier) +, _state(NoState) +, _stateMask(NoState) +, _command(NoCommand) +{ +} + +bool KeyboardTranslator::Entry::operator==(const Entry& rhs) const +{ + return _keyCode == rhs._keyCode && + _modifiers == rhs._modifiers && + _modifierMask == rhs._modifierMask && + _state == rhs._state && + _stateMask == rhs._stateMask && + _command == rhs._command && + _text == rhs._text; +} + +bool KeyboardTranslator::Entry::matches(int keyCode , + Qt::KeyboardModifiers modifiers, + States state) const +{ + if ( _keyCode != keyCode ) + return false; + + if ( (modifiers & _modifierMask) != (_modifiers & _modifierMask) ) + return false; + + // if modifiers is non-zero, the 'any modifier' state is implicit + if ( modifiers != 0 ) + state |= AnyModifierState; + + if ( (state & _stateMask) != (_state & _stateMask) ) + return false; + + // special handling for the 'Any Modifier' state, which checks for the presence of + // any or no modifiers. In this context, the 'keypad' modifier does not count. + bool anyModifiersSet = modifiers != 0 && modifiers != Qt::KeypadModifier; + if ( _stateMask & KeyboardTranslator::AnyModifierState ) + { + // test fails if any modifier is required but none are set + if ( (_state & KeyboardTranslator::AnyModifierState) && !anyModifiersSet ) + return false; + + // test fails if no modifier is allowed but one or more are set + if ( !(_state & KeyboardTranslator::AnyModifierState) && anyModifiersSet ) + return false; + } + + return true; +} +QByteArray KeyboardTranslator::Entry::escapedText(bool expandWildCards,Qt::KeyboardModifiers modifiers) const +{ + QByteArray result(text(expandWildCards,modifiers)); + + for ( int i = 0 ; i < result.count() ; i++ ) + { + char ch = result[i]; + char replacement = 0; + + switch ( ch ) + { + case 27 : replacement = 'E'; break; + case 8 : replacement = 'b'; break; + case 12 : replacement = 'f'; break; + case 9 : replacement = 't'; break; + case 13 : replacement = 'r'; break; + case 10 : replacement = 'n'; break; + default: + // any character which is not printable is replaced by an equivalent + // \xhh escape sequence (where 'hh' are the corresponding hex digits) + if ( !QChar(ch).isPrint() ) + replacement = 'x'; + } + + if ( replacement == 'x' ) + { + result.replace(i,1,"\\x"+QByteArray(1,ch).toInt(0, 16)); + } else if ( replacement != 0 ) + { + result.remove(i,1); + result.insert(i,'\\'); + result.insert(i+1,replacement); + } + } + + return result; +} +QByteArray KeyboardTranslator::Entry::unescape(const QByteArray& input) const +{ + QByteArray result(input); + + for ( int i = 0 ; i < result.count()-1 ; i++ ) + { + + QByteRef ch = result[i]; + if ( ch == '\\' ) + { + char replacement[2] = {0,0}; + int charsToRemove = 2; + bool escapedChar = true; + + switch ( result[i+1] ) + { + case 'E' : replacement[0] = 27; break; + case 'b' : replacement[0] = 8 ; break; + case 'f' : replacement[0] = 12; break; + case 't' : replacement[0] = 9 ; break; + case 'r' : replacement[0] = 13; break; + case 'n' : replacement[0] = 10; break; + case 'x' : + { + // format is \xh or \xhh where 'h' is a hexadecimal + // digit from 0-9 or A-F which should be replaced + // with the corresponding character value + char hexDigits[3] = {0}; + + if ( (i < result.count()-2) && isxdigit(result[i+2]) ) + hexDigits[0] = result[i+2]; + if ( (i < result.count()-3) && isxdigit(result[i+3]) ) + hexDigits[1] = result[i+3]; + + int charValue = 0; + sscanf(hexDigits,"%x",&charValue); + + replacement[0] = (char)charValue; + + charsToRemove = 2 + strlen(hexDigits); + } + break; + default: + escapedChar = false; + } + + if ( escapedChar ) + result.replace(i,charsToRemove,replacement); + } + } + + return result; +} + +void KeyboardTranslator::Entry::insertModifier( QString& item , int modifier ) const +{ + if ( !(modifier & _modifierMask) ) + return; + + if ( modifier & _modifiers ) + item += '+'; + else + item += '-'; + + if ( modifier == Qt::ShiftModifier ) + item += "Shift"; + else if ( modifier == Qt::ControlModifier ) + item += "Ctrl"; + else if ( modifier == Qt::AltModifier ) + item += "Alt"; + else if ( modifier == Qt::MetaModifier ) + item += "Meta"; + else if ( modifier == Qt::KeypadModifier ) + item += "KeyPad"; +} +void KeyboardTranslator::Entry::insertState( QString& item , int state ) const +{ + if ( !(state & _stateMask) ) + return; + + if ( state & _state ) + item += '+' ; + else + item += '-' ; + + if ( state == KeyboardTranslator::AlternateScreenState ) + item += "AppScreen"; + else if ( state == KeyboardTranslator::NewLineState ) + item += "NewLine"; + else if ( state == KeyboardTranslator::AnsiState ) + item += "Ansi"; + else if ( state == KeyboardTranslator::CursorKeysState ) + item += "AppCuKeys"; + else if ( state == KeyboardTranslator::AnyModifierState ) + item += "AnyMod"; +} +QString KeyboardTranslator::Entry::resultToString(bool expandWildCards,Qt::KeyboardModifiers modifiers) const +{ + if ( !_text.isEmpty() ) + return escapedText(expandWildCards,modifiers); + else if ( _command == EraseCommand ) + return "Erase"; + else if ( _command == ScrollPageUpCommand ) + return "ScrollPageUp"; + else if ( _command == ScrollPageDownCommand ) + return "ScrollPageDown"; + else if ( _command == ScrollLineUpCommand ) + return "ScrollLineUp"; + else if ( _command == ScrollLineDownCommand ) + return "ScrollLineDown"; + else if ( _command == ScrollLockCommand ) + return "ScrollLock"; + + return QString(); +} +QString KeyboardTranslator::Entry::conditionToString() const +{ + QString result = QKeySequence(_keyCode).toString(); + + // add modifiers + insertModifier( result , Qt::ShiftModifier ); + insertModifier( result , Qt::ControlModifier ); + insertModifier( result , Qt::AltModifier ); + insertModifier( result , Qt::MetaModifier ); + + // add states + insertState( result , KeyboardTranslator::AlternateScreenState ); + insertState( result , KeyboardTranslator::NewLineState ); + insertState( result , KeyboardTranslator::AnsiState ); + insertState( result , KeyboardTranslator::CursorKeysState ); + insertState( result , KeyboardTranslator::AnyModifierState ); + + return result; +} + +KeyboardTranslator::KeyboardTranslator(const QString& name) +: _name(name) +{ +} + +void KeyboardTranslator::setDescription(const QString& description) +{ + _description = description; +} +QString KeyboardTranslator::description() const +{ + return _description; +} +void KeyboardTranslator::setName(const QString& name) +{ + _name = name; +} +QString KeyboardTranslator::name() const +{ + return _name; +} + +QList<KeyboardTranslator::Entry> KeyboardTranslator::entries() const +{ + return _entries.values(); +} + +void KeyboardTranslator::addEntry(const Entry& entry) +{ + const int keyCode = entry.keyCode(); + _entries.insertMulti(keyCode,entry); +} +void KeyboardTranslator::replaceEntry(const Entry& existing , const Entry& replacement) +{ + if ( !existing.isNull() ) + _entries.remove(existing.keyCode()); + _entries.insertMulti(replacement.keyCode(),replacement); +} +void KeyboardTranslator::removeEntry(const Entry& entry) +{ + _entries.remove(entry.keyCode()); +} +KeyboardTranslator::Entry KeyboardTranslator::findEntry(int keyCode, Qt::KeyboardModifiers modifiers, States state) const +{ + if ( _entries.contains(keyCode) ) + { + QList<Entry> entriesForKey = _entries.values(keyCode); + + QListIterator<Entry> iter(entriesForKey); + + while (iter.hasNext()) + { + const Entry& next = iter.next(); + if ( next.matches(keyCode,modifiers,state) ) + return next; + } + + return Entry(); // entry not found + } + else + { + return Entry(); + } + +} +void KeyboardTranslatorManager::addTranslator(KeyboardTranslator* translator) +{ + _translators.insert(translator->name(),translator); + + if ( !saveTranslator(translator) ) + qWarning() << "Unable to save translator" << translator->name() + << "to disk."; +} +bool KeyboardTranslatorManager::deleteTranslator(const QString& name) +{ + Q_ASSERT( _translators.contains(name) ); + + // locate and delete + QString path = findTranslatorPath(name); + if ( QFile::remove(path) ) + { + _translators.remove(name); + return true; + } + else + { + qWarning() << "Failed to remove translator - " << path; + return false; + } +} +K_GLOBAL_STATIC( KeyboardTranslatorManager , theKeyboardTranslatorManager ) +KeyboardTranslatorManager* KeyboardTranslatorManager::instance() +{ + return theKeyboardTranslatorManager; +}
new file mode 100644 --- /dev/null +++ b/gui//KeyboardTranslator.h @@ -0,0 +1,657 @@ +/* + This source file is part of Konsole, a terminal emulator. + + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef KEYBOARDTRANSLATOR_H +#define KEYBOARDTRANSLATOR_H + +// Qt +#include <QtCore/QHash> +#include <QtCore/QList> +#include <QtGui/QKeySequence> +#include <QtCore/QMetaType> +#include <QtCore/QVarLengthArray> +#include <QtCore> + +typedef void (*CleanUpFunction)(); + +/** + * @internal + * + * Helper class for K_GLOBAL_STATIC to clean up the object on library unload or application + * shutdown. + */ +class CleanUpGlobalStatic +{ + public: + CleanUpFunction func; + + inline ~CleanUpGlobalStatic() { func(); } +}; + + +//these directives are taken from the heart of kdecore + +# define K_GLOBAL_STATIC_STRUCT_NAME(NAME) + +#if QT_VERSION < 0x040400 +# define Q_BASIC_ATOMIC_INITIALIZER Q_ATOMIC_INIT +# define testAndSetOrdered testAndSet +#endif + +#define K_GLOBAL_STATIC(TYPE, NAME) K_GLOBAL_STATIC_WITH_ARGS(TYPE, NAME, ()) + +#define K_GLOBAL_STATIC_WITH_ARGS(TYPE, NAME, ARGS) \ +static QBasicAtomicPointer<TYPE > _k_static_##NAME = Q_BASIC_ATOMIC_INITIALIZER(0); \ +static bool _k_static_##NAME##_destroyed; \ +static struct K_GLOBAL_STATIC_STRUCT_NAME(NAME) \ +{ \ + bool isDestroyed() \ + { \ + return _k_static_##NAME##_destroyed; \ + } \ + inline operator TYPE*() \ + { \ + return operator->(); \ + } \ + inline TYPE *operator->() \ + { \ + if (!_k_static_##NAME) { \ + if (isDestroyed()) { \ + qFatal("Fatal Error: Accessed global static '%s *%s()' after destruction. " \ + "Defined at %s:%d", #TYPE, #NAME, __FILE__, __LINE__); \ + } \ + TYPE *x = new TYPE ARGS; \ + if (!_k_static_##NAME.testAndSetOrdered(0, x) \ + && _k_static_##NAME != x ) { \ + delete x; \ + } else { \ + static CleanUpGlobalStatic cleanUpObject = { destroy }; \ + } \ + } \ + return _k_static_##NAME; \ + } \ + inline TYPE &operator*() \ + { \ + return *operator->(); \ + } \ + static void destroy() \ + { \ + _k_static_##NAME##_destroyed = true; \ + TYPE *x = _k_static_##NAME; \ + _k_static_##NAME = 0; \ + delete x; \ + } \ +} NAME; + + + + + +class QIODevice; +class QTextStream; + +namespace Konsole +{ + +/** + * A convertor which maps between key sequences pressed by the user and the + * character strings which should be sent to the terminal and commands + * which should be invoked when those character sequences are pressed. + * + * Konsole supports multiple keyboard translators, allowing the user to + * specify the character sequences which are sent to the terminal + * when particular key sequences are pressed. + * + * A key sequence is defined as a key code, associated keyboard modifiers + * (Shift,Ctrl,Alt,Meta etc.) and state flags which indicate the state + * which the terminal must be in for the key sequence to apply. + */ +class KeyboardTranslator +{ +public: + /** + * The meaning of a particular key sequence may depend upon the state which + * the terminal emulation is in. Therefore findEntry() may return a different + * Entry depending upon the state flags supplied. + * + * This enum describes the states which may be associated with with a particular + * entry in the keyboard translation entry. + */ + enum State + { + /** Indicates that no special state is active */ + NoState = 0, + /** + * TODO More documentation + */ + NewLineState = 1, + /** + * Indicates that the terminal is in 'Ansi' mode. + * TODO: More documentation + */ + AnsiState = 2, + /** + * TODO More documentation + */ + CursorKeysState = 4, + /** + * Indicates that the alternate screen ( typically used by interactive programs + * such as screen or vim ) is active + */ + AlternateScreenState = 8, + /** Indicates that any of the modifier keys is active. */ + AnyModifierState = 16 + }; + Q_DECLARE_FLAGS(States,State) + + /** + * This enum describes commands which are associated with particular key sequences. + */ + enum Command + { + /** Indicates that no command is associated with this command sequence */ + NoCommand = 0, + /** TODO Document me */ + SendCommand = 1, + /** Scroll the terminal display up one page */ + ScrollPageUpCommand = 2, + /** Scroll the terminal display down one page */ + ScrollPageDownCommand = 4, + /** Scroll the terminal display up one line */ + ScrollLineUpCommand = 8, + /** Scroll the terminal display down one line */ + ScrollLineDownCommand = 16, + /** Toggles scroll lock mode */ + ScrollLockCommand = 32, + /** Echos the operating system specific erase character. */ + EraseCommand = 64 + }; + Q_DECLARE_FLAGS(Commands,Command) + + /** + * Represents an association between a key sequence pressed by the user + * and the character sequence and commands associated with it for a particular + * KeyboardTranslator. + */ + class Entry + { + public: + /** + * Constructs a new entry for a keyboard translator. + */ + Entry(); + + /** + * Returns true if this entry is null. + * This is true for newly constructed entries which have no properties set. + */ + bool isNull() const; + + /** Returns the commands associated with this entry */ + Command command() const; + /** Sets the command associated with this entry. */ + void setCommand(Command command); + + /** + * Returns the character sequence associated with this entry, optionally replacing + * wildcard '*' characters with numbers to indicate the keyboard modifiers being pressed. + * + * TODO: The numbers used to replace '*' characters are taken from the Konsole/KDE 3 code. + * Document them. + * + * @param expandWildCards Specifies whether wild cards (occurrences of the '*' character) in + * the entry should be replaced with a number to indicate the modifier keys being pressed. + * + * @param modifiers The keyboard modifiers being pressed. + */ + QByteArray text(bool expandWildCards = false, + Qt::KeyboardModifiers modifiers = Qt::NoModifier) const; + + /** Sets the character sequence associated with this entry */ + void setText(const QByteArray& text); + + /** + * Returns the character sequence associated with this entry, + * with any non-printable characters replaced with escape sequences. + * + * eg. \\E for Escape, \\t for tab, \\n for new line. + * + * @param expandWildCards See text() + * @param modifiers See text() + */ + QByteArray escapedText(bool expandWildCards = false, + Qt::KeyboardModifiers modifiers = Qt::NoModifier) const; + + /** Returns the character code ( from the Qt::Key enum ) associated with this entry */ + int keyCode() const; + /** Sets the character code associated with this entry */ + void setKeyCode(int keyCode); + + /** + * Returns a bitwise-OR of the enabled keyboard modifiers associated with this entry. + * If a modifier is set in modifierMask() but not in modifiers(), this means that the entry + * only matches when that modifier is NOT pressed. + * + * If a modifier is not set in modifierMask() then the entry matches whether the modifier + * is pressed or not. + */ + Qt::KeyboardModifiers modifiers() const; + + /** Returns the keyboard modifiers which are valid in this entry. See modifiers() */ + Qt::KeyboardModifiers modifierMask() const; + + /** See modifiers() */ + void setModifiers( Qt::KeyboardModifiers modifiers ); + /** See modifierMask() and modifiers() */ + void setModifierMask( Qt::KeyboardModifiers modifiers ); + + /** + * Returns a bitwise-OR of the enabled state flags associated with this entry. + * If flag is set in stateMask() but not in state(), this means that the entry only + * matches when the terminal is NOT in that state. + * + * If a state is not set in stateMask() then the entry matches whether the terminal + * is in that state or not. + */ + States state() const; + + /** Returns the state flags which are valid in this entry. See state() */ + States stateMask() const; + + /** See state() */ + void setState( States state ); + /** See stateMask() */ + void setStateMask( States mask ); + + /** + * Returns the key code and modifiers associated with this entry + * as a QKeySequence + */ + //QKeySequence keySequence() const; + + /** + * Returns this entry's conditions ( ie. its key code, modifier and state criteria ) + * as a string. + */ + QString conditionToString() const; + + /** + * Returns this entry's result ( ie. its command or character sequence ) + * as a string. + * + * @param expandWildCards See text() + * @param modifiers See text() + */ + QString resultToString(bool expandWildCards = false, + Qt::KeyboardModifiers modifiers = Qt::NoModifier) const; + + /** + * Returns true if this entry matches the given key sequence, specified + * as a combination of @p keyCode , @p modifiers and @p state. + */ + bool matches( int keyCode , + Qt::KeyboardModifiers modifiers , + States flags ) const; + + bool operator==(const Entry& rhs) const; + + private: + void insertModifier( QString& item , int modifier ) const; + void insertState( QString& item , int state ) const; + QByteArray unescape(const QByteArray& text) const; + + int _keyCode; + Qt::KeyboardModifiers _modifiers; + Qt::KeyboardModifiers _modifierMask; + States _state; + States _stateMask; + + Command _command; + QByteArray _text; + }; + + /** Constructs a new keyboard translator with the given @p name */ + KeyboardTranslator(const QString& name); + + //KeyboardTranslator(const KeyboardTranslator& other); + + /** Returns the name of this keyboard translator */ + QString name() const; + + /** Sets the name of this keyboard translator */ + void setName(const QString& name); + + /** Returns the descriptive name of this keyboard translator */ + QString description() const; + + /** Sets the descriptive name of this keyboard translator */ + void setDescription(const QString& description); + + /** + * Looks for an entry in this keyboard translator which matches the given + * key code, keyboard modifiers and state flags. + * + * Returns the matching entry if found or a null Entry otherwise ( ie. + * entry.isNull() will return true ) + * + * @param keyCode A key code from the Qt::Key enum + * @param modifiers A combination of modifiers + * @param state Optional flags which specify the current state of the terminal + */ + Entry findEntry(int keyCode , + Qt::KeyboardModifiers modifiers , + States state = NoState) const; + + /** + * Adds an entry to this keyboard translator's table. Entries can be looked up according + * to their key sequence using findEntry() + */ + void addEntry(const Entry& entry); + + /** + * Replaces an entry in the translator. If the @p existing entry is null, + * then this is equivalent to calling addEntry(@p replacement) + */ + void replaceEntry(const Entry& existing , const Entry& replacement); + + /** + * Removes an entry from the table. + */ + void removeEntry(const Entry& entry); + + /** Returns a list of all entries in the translator. */ + QList<Entry> entries() const; + +private: + + QHash<int,Entry> _entries; // entries in this keyboard translation, + // entries are indexed according to + // their keycode + QString _name; + QString _description; +}; +Q_DECLARE_OPERATORS_FOR_FLAGS(KeyboardTranslator::States) +Q_DECLARE_OPERATORS_FOR_FLAGS(KeyboardTranslator::Commands) + +/** + * Parses the contents of a Keyboard Translator (.keytab) file and + * returns the entries found in it. + * + * Usage example: + * + * @code + * QFile source( "/path/to/keytab" ); + * source.open( QIODevice::ReadOnly ); + * + * KeyboardTranslator* translator = new KeyboardTranslator( "name-of-translator" ); + * + * KeyboardTranslatorReader reader(source); + * while ( reader.hasNextEntry() ) + * translator->addEntry(reader.nextEntry()); + * + * source.close(); + * + * if ( !reader.parseError() ) + * { + * // parsing succeeded, do something with the translator + * } + * else + * { + * // parsing failed + * } + * @endcode + */ +class KeyboardTranslatorReader +{ +public: + /** Constructs a new reader which parses the given @p source */ + KeyboardTranslatorReader( QIODevice* source ); + + /** + * Returns the description text. + * TODO: More documentation + */ + QString description() const; + + /** Returns true if there is another entry in the source stream */ + bool hasNextEntry(); + /** Returns the next entry found in the source stream */ + KeyboardTranslator::Entry nextEntry(); + + /** + * Returns true if an error occurred whilst parsing the input or + * false if no error occurred. + */ + bool parseError(); + + /** + * Parses a condition and result string for a translator entry + * and produces a keyboard translator entry. + * + * The condition and result strings are in the same format as in + */ + static KeyboardTranslator::Entry createEntry( const QString& condition , + const QString& result ); +private: + struct Token + { + enum Type + { + TitleKeyword, + TitleText, + KeyKeyword, + KeySequence, + Command, + OutputText + }; + Type type; + QString text; + }; + QList<Token> tokenize(const QString&); + void readNext(); + bool decodeSequence(const QString& , + int& keyCode, + Qt::KeyboardModifiers& modifiers, + Qt::KeyboardModifiers& modifierMask, + KeyboardTranslator::States& state, + KeyboardTranslator::States& stateFlags); + + static bool parseAsModifier(const QString& item , Qt::KeyboardModifier& modifier); + static bool parseAsStateFlag(const QString& item , KeyboardTranslator::State& state); + static bool parseAsKeyCode(const QString& item , int& keyCode); + static bool parseAsCommand(const QString& text , KeyboardTranslator::Command& command); + + QIODevice* _source; + QString _description; + KeyboardTranslator::Entry _nextEntry; + bool _hasNext; +}; + +/** Writes a keyboard translation to disk. */ +class KeyboardTranslatorWriter +{ +public: + /** + * Constructs a new writer which saves data into @p destination. + * The caller is responsible for closing the device when writing is complete. + */ + KeyboardTranslatorWriter(QIODevice* destination); + ~KeyboardTranslatorWriter(); + + /** + * Writes the header for the keyboard translator. + * @param description Description of the keyboard translator. + */ + void writeHeader( const QString& description ); + /** Writes a translator entry. */ + void writeEntry( const KeyboardTranslator::Entry& entry ); + +private: + QIODevice* _destination; + QTextStream* _writer; +}; + +/** + * Manages the keyboard translations available for use by terminal sessions, + * see KeyboardTranslator. + */ +class KeyboardTranslatorManager +{ +public: + /** + * Constructs a new KeyboardTranslatorManager and loads the list of + * available keyboard translations. + * + * The keyboard translations themselves are not loaded until they are + * first requested via a call to findTranslator() + */ + KeyboardTranslatorManager(); + ~KeyboardTranslatorManager(); + + /** + * Adds a new translator. If a translator with the same name + * already exists, it will be replaced by the new translator. + * + * TODO: More documentation. + */ + void addTranslator(KeyboardTranslator* translator); + + /** + * Deletes a translator. Returns true on successful deletion or false otherwise. + * + * TODO: More documentation + */ + bool deleteTranslator(const QString& name); + + /** Returns the default translator for Konsole. */ + const KeyboardTranslator* defaultTranslator(); + + /** + * Returns the keyboard translator with the given name or 0 if no translator + * with that name exists. + * + * The first time that a translator with a particular name is requested, + * the on-disk .keyboard file is loaded and parsed. + */ + const KeyboardTranslator* findTranslator(const QString& name); + /** + * Returns a list of the names of available keyboard translators. + * + * The first time this is called, a search for available + * translators is started. + */ + QList<QString> allTranslators(); + + /** Returns the global KeyboardTranslatorManager instance. */ + static KeyboardTranslatorManager* instance(); + +private: + static const char* defaultTranslatorText; + + void findTranslators(); // locate the available translators + KeyboardTranslator* loadTranslator(const QString& name); // loads the translator + // with the given name + KeyboardTranslator* loadTranslator(QIODevice* device,const QString& name); + + bool saveTranslator(const KeyboardTranslator* translator); + QString findTranslatorPath(const QString& name); + + QHash<QString,KeyboardTranslator*> _translators; // maps translator-name -> KeyboardTranslator + // instance + bool _haveLoadedAll; +}; + +inline int KeyboardTranslator::Entry::keyCode() const { return _keyCode; } +inline void KeyboardTranslator::Entry::setKeyCode(int keyCode) { _keyCode = keyCode; } + +inline void KeyboardTranslator::Entry::setModifiers( Qt::KeyboardModifiers modifier ) +{ + _modifiers = modifier; +} +inline Qt::KeyboardModifiers KeyboardTranslator::Entry::modifiers() const { return _modifiers; } + +inline void KeyboardTranslator::Entry::setModifierMask( Qt::KeyboardModifiers mask ) +{ + _modifierMask = mask; +} +inline Qt::KeyboardModifiers KeyboardTranslator::Entry::modifierMask() const { return _modifierMask; } + +inline bool KeyboardTranslator::Entry::isNull() const +{ + return ( *this == Entry() ); +} + +inline void KeyboardTranslator::Entry::setCommand( Command command ) +{ + _command = command; +} +inline KeyboardTranslator::Command KeyboardTranslator::Entry::command() const { return _command; } + +inline void KeyboardTranslator::Entry::setText( const QByteArray& text ) +{ + _text = unescape(text); +} +inline int oneOrZero(int value) +{ + return value ? 1 : 0; +} +inline QByteArray KeyboardTranslator::Entry::text(bool expandWildCards,Qt::KeyboardModifiers modifiers) const +{ + QByteArray expandedText = _text; + + if (expandWildCards) + { + int modifierValue = 1; + modifierValue += oneOrZero(modifiers & Qt::ShiftModifier); + modifierValue += oneOrZero(modifiers & Qt::AltModifier) << 1; + modifierValue += oneOrZero(modifiers & Qt::ControlModifier) << 2; + + for (int i=0;i<_text.length();i++) + { + if (expandedText[i] == '*') + expandedText[i] = '0' + modifierValue; + } + } + + return expandedText; +} + +inline void KeyboardTranslator::Entry::setState( States state ) +{ + _state = state; +} +inline KeyboardTranslator::States KeyboardTranslator::Entry::state() const { return _state; } + +inline void KeyboardTranslator::Entry::setStateMask( States stateMask ) +{ + _stateMask = stateMask; +} +inline KeyboardTranslator::States KeyboardTranslator::Entry::stateMask() const { return _stateMask; } + +} + +Q_DECLARE_METATYPE(Konsole::KeyboardTranslator::Entry) +Q_DECLARE_METATYPE(const Konsole::KeyboardTranslator*) + +#endif // KEYBOARDTRANSLATOR_H +
new file mode 100644 --- /dev/null +++ b/gui//LineFont.h @@ -0,0 +1,21 @@ +// WARNING: Autogenerated by "fontembedder ./linefont.src". +// You probably do not want to hand-edit this! + +static const quint32 LineChars[] = { + 0x00007c00, 0x000fffe0, 0x00421084, 0x00e739ce, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00427000, 0x004e7380, 0x00e77800, 0x00ef7bc0, + 0x00421c00, 0x00439ce0, 0x00e73c00, 0x00e7bde0, 0x00007084, 0x000e7384, 0x000079ce, 0x000f7bce, + 0x00001c84, 0x00039ce4, 0x00003dce, 0x0007bdee, 0x00427084, 0x004e7384, 0x004279ce, 0x00e77884, + 0x00e779ce, 0x004f7bce, 0x00ef7bc4, 0x00ef7bce, 0x00421c84, 0x00439ce4, 0x00423dce, 0x00e73c84, + 0x00e73dce, 0x0047bdee, 0x00e7bde4, 0x00e7bdee, 0x00427c00, 0x0043fce0, 0x004e7f80, 0x004fffe0, + 0x004fffe0, 0x00e7fde0, 0x006f7fc0, 0x00efffe0, 0x00007c84, 0x0003fce4, 0x000e7f84, 0x000fffe4, + 0x00007dce, 0x0007fdee, 0x000f7fce, 0x000fffee, 0x00427c84, 0x0043fce4, 0x004e7f84, 0x004fffe4, + 0x00427dce, 0x00e77c84, 0x00e77dce, 0x0047fdee, 0x004e7fce, 0x00e7fde4, 0x00ef7f84, 0x004fffee, + 0x00efffe4, 0x00e7fdee, 0x00ef7fce, 0x00efffee, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x000f83e0, 0x00a5294a, 0x004e1380, 0x00a57800, 0x00ad0bc0, 0x004390e0, 0x00a53c00, 0x00a5a1e0, + 0x000e1384, 0x0000794a, 0x000f0b4a, 0x000390e4, 0x00003d4a, 0x0007a16a, 0x004e1384, 0x00a5694a, + 0x00ad2b4a, 0x004390e4, 0x00a52d4a, 0x00a5a16a, 0x004f83e0, 0x00a57c00, 0x00ad83e0, 0x000f83e4, + 0x00007d4a, 0x000f836a, 0x004f93e4, 0x00a57d4a, 0x00ad836a, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00001c00, 0x00001084, 0x00007000, 0x00421000, + 0x00039ce0, 0x000039ce, 0x000e7380, 0x00e73800, 0x000e7f80, 0x00e73884, 0x0003fce0, 0x004239ce +};
new file mode 100644 --- /dev/null +++ b/gui//LineFont.src @@ -0,0 +1,786 @@ +#2500: single horizontal line +2500 + + +----- + + + +#2501: triple horizontal line +2501 + +----- +----- +----- + + +#2502: single vertical line +2502 + | + | + | + | + | + +#2503: triple vertical line +2503 + ||| + ||| + ||| + ||| + ||| + +#2504-250B are dashed - not handled + +#250C: top-left corner (lines on bottom + right) +250C + + + .-- + | + | + +#250D: as above, but top line triple-width +250D + + .-- + .-- + |-- + | + +#250E: now the vert line triple-width +250E + + + ..-- + ||| + ||| + +#250F: and now both lines triple-width +250F + + .___ + |.-- + ||._ + ||| + +#2510: top-right corner +2510 + + +--. + | + | + +2511 + +==. +==. +==| + | + +2512 + + +==.. + ||| + ||| + +2513 + +===. +==.| +=.|| + ||| + +#2514: bottom-left corner +2514 + | + | + .== + + + +2515 + | + |== + |== + === + + + +2516 + ||| + ||| + |.== + + + +2517 + ||| + ||.= + |.== + .=== + + +#2518: bottm-right corner +2518 + | + | +==. + + + +2519 + | +==| +==| +=== + + + +251A + ||| + ||| +==== + + + +251B + ||| +=.|| +==.| +===. + + +#251C: Join of vertical line and one from the right +251C + | + | + |== + | + | + +251D + | + |== + |== + |== + | + +251E + ||| + ||| + ||== + | + | + +251F + | + | + ||== + ||| + ||| + + +2520 + ||| + ||| + ||== + ||| + ||| + +2521 + ||| + |||= + ||== + .|== + | + +2522 + | + .|== + ||== + |||= + ||| + +2523 + ||| + ||.= + ||== + ||.= + ||| + +#2524: Join of vertical line and one from the left +2524 + | + | +==| + | + | + +2525 + | +==| +==| +==| + | + +2526 + ||| + ||| +==+| + | + | + +2527 + | + | +==+| + ||| + ||| + +2528 + ||| + ||| +==+| + ||| + ||| + +2529 + ||| +=+|| +==+| +===+ + | + +252A + | +=+|| +==+| +===+ + ||| + +252B + ||| +=+|| +==+| +=+|| + ||| + +#252C: horizontal line joined to from below +252C + + +===== + | + | + +252D + +=== +==|== +==| + | + +252E + + === +==|== + |== + | + +252F + +==+== +==|== +==|== + | + +2530 + +===== +===== +==|== + | + +2531 + +===| +==||= +=||| + ||| + +2532 + + |=== +=||== + ||== + || + +2533 + +===== +==|== +=+|+= + ||| + +#2534: bottom line, connected to from top +2534 + | + | +===== + + + +2535 + | +==| +===== +=== + + +2536 + | + |== +===== + === + + +2537 + | +==|== +===== +===== + + +2538 + ||| + ||| +===== + + + +2539 + ||| +==|| +===== +===| + + + +253A + ||| + ||== +=|=== + |=== + + +253B + ||| +==|== +===== +===== + + +#253C: vertical + horizontal lines intersecting +253C + | + | +===== + | + | + +253D + | +==| +===== +==| + | + +253E + | + |== +===== + |== + | + +253F + | +==|== +===== +==|== + | + +2540 + ||| + ||| +===== + | + | + +2541 + | + | +===== + ||| + ||| + +2542 + ||| + ||| +===== + ||| + ||| + +2543 + ||| +=||| +===== +==|+ + | + +2544 + ||| + ||== +===== + |== + | + +2545 + | +==|+ +===== +=||| + ||| + +2546 + | + |== +===== + ||== + ||| + +2547 + ||| +=|||= +===== +=|||= + | + +2548 + | +=|||= +===== +=|||= + ||| + +2549 + ||| +=||| +===== +=||| + ||| + +254A + ||| + |||= +===== + |||= + ||| + +254B + ||| +=|||= +===== +=|||= + ||| + +#254C-254F are dashed +2550 + +_____ + +_____ + + +2551 + | | + | | + | | + | | + | | + +2552 + + |-- + | + |-- + | + +2553 + + + ---- + | | + | | + +2554 + + +--- + | + + +- + | | + +2555 + +--+ + | +--+ + | + +2556 + + +-+-+ + | | + | | + +2557 + +---+ + | +-+ | + | | + +2558 + | + +-- + | + +-- + +2559 + | | + | | + +-+- + + + +255A + | | + | +- + | + +--- + + +255B + | +--+ + | +--+ + + +255C + | | + | | +-+-+ + + +255D + | | +-+ | + | +---+ + + +255E + | + +-- + | + +-- + | + +255F + | | + | | + | +- + | | + | | + +2560 + | | + | +- + | | + | +- + | | + +2561 + | +--+ + | +--+ + | + +2562 + | | + | | +-+ + + | | + | | + +2563 + | | +-+ | + | +-+ | + | | + +2564 + +----- + +--+-- + | + +2565 + + +-+-+- + | | + | | + +2566 + +----- + +-+ +- + | | + +2567 + | +--+-- + +----- + + +2568 + | | + | | +-+-+- + + + +2569 + | | +-+ +- + +----- + + +256A + | +--+-- + | +--+-- + | + +256B + | | + | | +-+-+- + | | + | | + +256C + | | +-+ +- + +-+ +- + | | + +#256F-2570 are curly, +#2571-2573 are slashes and X + +2574 + + +___ + + + +2575 + | + | + | + + + +2576 + + + ___ + + + +2577 + + + | + | + | + +2578 + +___ +___ +___ + + +2579 + ||| + ||| + ||| + + + +257A + + ___ + ___ + ___ + + +257B + + + ||| + ||| + ||| + +257C + + ___ +_____ + ___ + + +257D + | + | + ||| + ||| + ||| + +257E + +___ +_____ +___ + + +257F + ||| + ||| + ||| + | + |
new file mode 100644 --- /dev/null +++ b/gui//Makefile @@ -0,0 +1,494 @@ +############################################################################# +# Makefile for building: Quint +# Generated by qmake (2.01a) (Qt 4.7.2) on: Do. Apr 7 11:59:45 2011 +# Project: Quint.pro +# Template: app +# Command: /usr/bin/qmake -o Makefile Quint.pro +############################################################################# + +####### Compiler, tools and options + +CC = gcc +CXX = g++ +DEFINES = -DHAVE_POSIX_OPENPT -DQT_NO_DEBUG -DQT_WEBKIT_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_SHARED +CFLAGS = -pipe -O2 -Wall -W -D_REENTRANT $(DEFINES) +CXXFLAGS = -pipe -O2 -Wall -W -D_REENTRANT $(DEFINES) +INCPATH = -I/usr/share/qt4/mkspecs/linux-g++ -I. -I/usr/include/qt4/QtCore -I/usr/include/qt4/QtGui -I/usr/include/qt4/QtWebKit -I/usr/include/qt4 -I. +LINK = g++ +LFLAGS = -Wl,-O1 +LIBS = $(SUBLIBS) -L/usr/lib -lQtWebKit -lQtGui -lQtCore -lpthread +AR = ar cqs +RANLIB = +QMAKE = /usr/bin/qmake +TAR = tar -cf +COMPRESS = gzip -9f +COPY = cp -f +SED = sed +COPY_FILE = $(COPY) +COPY_DIR = $(COPY) -r +STRIP = strip +INSTALL_FILE = install -m 644 -p +INSTALL_DIR = $(COPY_DIR) +INSTALL_PROGRAM = install -m 755 -p +DEL_FILE = rm -f +SYMLINK = ln -f -s +DEL_DIR = rmdir +MOVE = mv -f +CHK_DIR_EXISTS= test -d +MKDIR = mkdir -p + +####### Output directory + +OBJECTS_DIR = ./ + +####### Files + +SOURCES = main.cpp \ + mainwindow.cpp \ + terminal.cpp \ + TerminalCharacterDecoder.cpp \ + KeyboardTranslator.cpp \ + Screen.cpp \ + History.cpp \ + BlockArray.cpp \ + konsole_wcwidth.cpp \ + ScreenWindow.cpp \ + Emulation.cpp \ + Vt102Emulation.cpp \ + TerminalDisplay.cpp \ + Filter.cpp \ + Pty.cpp \ + kpty.cpp \ + k3process.cpp \ + k3processcontroller.cpp \ + Session.cpp \ + ShellCommand.cpp \ + qtermwidget.cpp moc_mainwindow.cpp \ + moc_terminal.cpp \ + moc_ScreenWindow.cpp \ + moc_Emulation.cpp \ + moc_Vt102Emulation.cpp \ + moc_TerminalDisplay.cpp \ + moc_Filter.cpp \ + moc_Pty.cpp \ + moc_k3process.cpp \ + moc_k3processcontroller.cpp \ + moc_Session.cpp \ + moc_qtermwidget.cpp +OBJECTS = main.o \ + mainwindow.o \ + terminal.o \ + TerminalCharacterDecoder.o \ + KeyboardTranslator.o \ + Screen.o \ + History.o \ + BlockArray.o \ + konsole_wcwidth.o \ + ScreenWindow.o \ + Emulation.o \ + Vt102Emulation.o \ + TerminalDisplay.o \ + Filter.o \ + Pty.o \ + kpty.o \ + k3process.o \ + k3processcontroller.o \ + Session.o \ + ShellCommand.o \ + qtermwidget.o \ + moc_mainwindow.o \ + moc_terminal.o \ + moc_ScreenWindow.o \ + moc_Emulation.o \ + moc_Vt102Emulation.o \ + moc_TerminalDisplay.o \ + moc_Filter.o \ + moc_Pty.o \ + moc_k3process.o \ + moc_k3processcontroller.o \ + moc_Session.o \ + moc_qtermwidget.o +DIST = /usr/share/qt4/mkspecs/common/g++.conf \ + /usr/share/qt4/mkspecs/common/unix.conf \ + /usr/share/qt4/mkspecs/common/linux.conf \ + /usr/share/qt4/mkspecs/qconfig.pri \ + /usr/share/qt4/mkspecs/modules/qt_webkit_version.pri \ + /usr/share/qt4/mkspecs/features/qt_functions.prf \ + /usr/share/qt4/mkspecs/features/qt_config.prf \ + /usr/share/qt4/mkspecs/features/exclusive_builds.prf \ + /usr/share/qt4/mkspecs/features/default_pre.prf \ + /usr/share/qt4/mkspecs/features/release.prf \ + /usr/share/qt4/mkspecs/features/default_post.prf \ + /usr/share/qt4/mkspecs/features/warn_on.prf \ + /usr/share/qt4/mkspecs/features/qt.prf \ + /usr/share/qt4/mkspecs/features/unix/thread.prf \ + /usr/share/qt4/mkspecs/features/moc.prf \ + /usr/share/qt4/mkspecs/features/resources.prf \ + /usr/share/qt4/mkspecs/features/uic.prf \ + /usr/share/qt4/mkspecs/features/yacc.prf \ + /usr/share/qt4/mkspecs/features/lex.prf \ + /usr/share/qt4/mkspecs/features/include_source_dir.prf \ + Quint.pro +QMAKE_TARGET = Quint +DESTDIR = +TARGET = Quint + +first: all +####### Implicit rules + +.SUFFIXES: .o .c .cpp .cc .cxx .C + +.cpp.o: + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o "$@" "$<" + +.cc.o: + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o "$@" "$<" + +.cxx.o: + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o "$@" "$<" + +.C.o: + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o "$@" "$<" + +.c.o: + $(CC) -c $(CFLAGS) $(INCPATH) -o "$@" "$<" + +####### Build rules + +all: Makefile $(TARGET) + +$(TARGET): $(OBJECTS) + $(LINK) $(LFLAGS) -o $(TARGET) $(OBJECTS) $(OBJCOMP) $(LIBS) + +Makefile: Quint.pro /usr/share/qt4/mkspecs/linux-g++/qmake.conf /usr/share/qt4/mkspecs/common/g++.conf \ + /usr/share/qt4/mkspecs/common/unix.conf \ + /usr/share/qt4/mkspecs/common/linux.conf \ + /usr/share/qt4/mkspecs/qconfig.pri \ + /usr/share/qt4/mkspecs/modules/qt_webkit_version.pri \ + /usr/share/qt4/mkspecs/features/qt_functions.prf \ + /usr/share/qt4/mkspecs/features/qt_config.prf \ + /usr/share/qt4/mkspecs/features/exclusive_builds.prf \ + /usr/share/qt4/mkspecs/features/default_pre.prf \ + /usr/share/qt4/mkspecs/features/release.prf \ + /usr/share/qt4/mkspecs/features/default_post.prf \ + /usr/share/qt4/mkspecs/features/warn_on.prf \ + /usr/share/qt4/mkspecs/features/qt.prf \ + /usr/share/qt4/mkspecs/features/unix/thread.prf \ + /usr/share/qt4/mkspecs/features/moc.prf \ + /usr/share/qt4/mkspecs/features/resources.prf \ + /usr/share/qt4/mkspecs/features/uic.prf \ + /usr/share/qt4/mkspecs/features/yacc.prf \ + /usr/share/qt4/mkspecs/features/lex.prf \ + /usr/share/qt4/mkspecs/features/include_source_dir.prf \ + /usr/lib/libQtWebKit.prl \ + /usr/lib/libQtGui.prl \ + /usr/lib/libQtCore.prl + $(QMAKE) -o Makefile Quint.pro +/usr/share/qt4/mkspecs/common/g++.conf: +/usr/share/qt4/mkspecs/common/unix.conf: +/usr/share/qt4/mkspecs/common/linux.conf: +/usr/share/qt4/mkspecs/qconfig.pri: +/usr/share/qt4/mkspecs/modules/qt_webkit_version.pri: +/usr/share/qt4/mkspecs/features/qt_functions.prf: +/usr/share/qt4/mkspecs/features/qt_config.prf: +/usr/share/qt4/mkspecs/features/exclusive_builds.prf: +/usr/share/qt4/mkspecs/features/default_pre.prf: +/usr/share/qt4/mkspecs/features/release.prf: +/usr/share/qt4/mkspecs/features/default_post.prf: +/usr/share/qt4/mkspecs/features/warn_on.prf: +/usr/share/qt4/mkspecs/features/qt.prf: +/usr/share/qt4/mkspecs/features/unix/thread.prf: +/usr/share/qt4/mkspecs/features/moc.prf: +/usr/share/qt4/mkspecs/features/resources.prf: +/usr/share/qt4/mkspecs/features/uic.prf: +/usr/share/qt4/mkspecs/features/yacc.prf: +/usr/share/qt4/mkspecs/features/lex.prf: +/usr/share/qt4/mkspecs/features/include_source_dir.prf: +/usr/lib/libQtWebKit.prl: +/usr/lib/libQtGui.prl: +/usr/lib/libQtCore.prl: +qmake: FORCE + @$(QMAKE) -o Makefile Quint.pro + +dist: + @$(CHK_DIR_EXISTS) .tmp/Quint1.0.0 || $(MKDIR) .tmp/Quint1.0.0 + $(COPY_FILE) --parents $(SOURCES) $(DIST) .tmp/Quint1.0.0/ && $(COPY_FILE) --parents mainwindow.h terminal.h TerminalCharacterDecoder.h Character.h CharacterColor.h KeyboardTranslator.h ExtendedDefaultTranslator.h Screen.h History.h BlockArray.h konsole_wcwidth.h ScreenWindow.h Emulation.h Vt102Emulation.h TerminalDisplay.h Filter.h LineFont.h Pty.h kpty.h kpty_p.h k3process.h k3processcontroller.h Session.h ShellCommand.h qtermwidget.h .tmp/Quint1.0.0/ && $(COPY_FILE) --parents main.cpp mainwindow.cpp terminal.cpp TerminalCharacterDecoder.cpp KeyboardTranslator.cpp Screen.cpp History.cpp BlockArray.cpp konsole_wcwidth.cpp ScreenWindow.cpp Emulation.cpp Vt102Emulation.cpp TerminalDisplay.cpp Filter.cpp Pty.cpp kpty.cpp k3process.cpp k3processcontroller.cpp Session.cpp ShellCommand.cpp qtermwidget.cpp .tmp/Quint1.0.0/ && (cd `dirname .tmp/Quint1.0.0` && $(TAR) Quint1.0.0.tar Quint1.0.0 && $(COMPRESS) Quint1.0.0.tar) && $(MOVE) `dirname .tmp/Quint1.0.0`/Quint1.0.0.tar.gz . && $(DEL_FILE) -r .tmp/Quint1.0.0 + + +clean:compiler_clean + -$(DEL_FILE) $(OBJECTS) + -$(DEL_FILE) *~ core *.core + + +####### Sub-libraries + +distclean: clean + -$(DEL_FILE) $(TARGET) + -$(DEL_FILE) Makefile + + +check: first + +mocclean: compiler_moc_header_clean compiler_moc_source_clean + +mocables: compiler_moc_header_make_all compiler_moc_source_make_all + +compiler_moc_header_make_all: moc_mainwindow.cpp moc_terminal.cpp moc_ScreenWindow.cpp moc_Emulation.cpp moc_Vt102Emulation.cpp moc_TerminalDisplay.cpp moc_Filter.cpp moc_Pty.cpp moc_k3process.cpp moc_k3processcontroller.cpp moc_Session.cpp moc_qtermwidget.cpp +compiler_moc_header_clean: + -$(DEL_FILE) moc_mainwindow.cpp moc_terminal.cpp moc_ScreenWindow.cpp moc_Emulation.cpp moc_Vt102Emulation.cpp moc_TerminalDisplay.cpp moc_Filter.cpp moc_Pty.cpp moc_k3process.cpp moc_k3processcontroller.cpp moc_Session.cpp moc_qtermwidget.cpp +moc_mainwindow.cpp: mainwindow.h + /usr/bin/moc-qt4 $(DEFINES) $(INCPATH) mainwindow.h -o moc_mainwindow.cpp + +moc_terminal.cpp: qtermwidget.h \ + terminal.h + /usr/bin/moc-qt4 $(DEFINES) $(INCPATH) terminal.h -o moc_terminal.cpp + +moc_ScreenWindow.cpp: Character.h \ + CharacterColor.h \ + ScreenWindow.h + /usr/bin/moc-qt4 $(DEFINES) $(INCPATH) ScreenWindow.h -o moc_ScreenWindow.cpp + +moc_Emulation.cpp: Emulation.h + /usr/bin/moc-qt4 $(DEFINES) $(INCPATH) Emulation.h -o moc_Emulation.cpp + +moc_Vt102Emulation.cpp: Emulation.h \ + Screen.h \ + Character.h \ + CharacterColor.h \ + History.h \ + BlockArray.h \ + Vt102Emulation.h + /usr/bin/moc-qt4 $(DEFINES) $(INCPATH) Vt102Emulation.h -o moc_Vt102Emulation.cpp + +moc_TerminalDisplay.cpp: Filter.h \ + Character.h \ + CharacterColor.h \ + ColorTables.h \ + TerminalDisplay.h + /usr/bin/moc-qt4 $(DEFINES) $(INCPATH) TerminalDisplay.h -o moc_TerminalDisplay.cpp + +moc_Filter.cpp: Character.h \ + CharacterColor.h \ + Filter.h + /usr/bin/moc-qt4 $(DEFINES) $(INCPATH) Filter.h -o moc_Filter.cpp + +moc_Pty.cpp: k3process.h \ + Pty.h + /usr/bin/moc-qt4 $(DEFINES) $(INCPATH) Pty.h -o moc_Pty.cpp + +moc_k3process.cpp: k3process.h + /usr/bin/moc-qt4 $(DEFINES) $(INCPATH) k3process.h -o moc_k3process.cpp + +moc_k3processcontroller.cpp: k3process.h \ + k3processcontroller.h + /usr/bin/moc-qt4 $(DEFINES) $(INCPATH) k3processcontroller.h -o moc_k3processcontroller.cpp + +moc_Session.cpp: History.h \ + BlockArray.h \ + Character.h \ + CharacterColor.h \ + Session.h + /usr/bin/moc-qt4 $(DEFINES) $(INCPATH) Session.h -o moc_Session.cpp + +moc_qtermwidget.cpp: qtermwidget.h + /usr/bin/moc-qt4 $(DEFINES) $(INCPATH) qtermwidget.h -o moc_qtermwidget.cpp + +compiler_rcc_make_all: +compiler_rcc_clean: +compiler_image_collection_make_all: qmake_image_collection.cpp +compiler_image_collection_clean: + -$(DEL_FILE) qmake_image_collection.cpp +compiler_moc_source_make_all: +compiler_moc_source_clean: +compiler_uic_make_all: +compiler_uic_clean: +compiler_yacc_decl_make_all: +compiler_yacc_decl_clean: +compiler_yacc_impl_make_all: +compiler_yacc_impl_clean: +compiler_lex_make_all: +compiler_lex_clean: +compiler_clean: compiler_moc_header_clean + +####### Compile + +main.o: main.cpp mainwindow.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o main.o main.cpp + +mainwindow.o: mainwindow.cpp mainwindow.h \ + terminal.h \ + qtermwidget.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o mainwindow.o mainwindow.cpp + +terminal.o: terminal.cpp terminal.h \ + qtermwidget.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o terminal.o terminal.cpp + +TerminalCharacterDecoder.o: TerminalCharacterDecoder.cpp TerminalCharacterDecoder.h \ + Character.h \ + CharacterColor.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o TerminalCharacterDecoder.o TerminalCharacterDecoder.cpp + +KeyboardTranslator.o: KeyboardTranslator.cpp KeyboardTranslator.h \ + ExtendedDefaultTranslator.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o KeyboardTranslator.o KeyboardTranslator.cpp + +Screen.o: Screen.cpp Screen.h \ + Character.h \ + CharacterColor.h \ + History.h \ + BlockArray.h \ + konsole_wcwidth.h \ + TerminalCharacterDecoder.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o Screen.o Screen.cpp + +History.o: History.cpp History.h \ + BlockArray.h \ + Character.h \ + CharacterColor.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o History.o History.cpp + +BlockArray.o: BlockArray.cpp BlockArray.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o BlockArray.o BlockArray.cpp + +konsole_wcwidth.o: konsole_wcwidth.cpp konsole_wcwidth.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o konsole_wcwidth.o konsole_wcwidth.cpp + +ScreenWindow.o: ScreenWindow.cpp ScreenWindow.h \ + Character.h \ + CharacterColor.h \ + Screen.h \ + History.h \ + BlockArray.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o ScreenWindow.o ScreenWindow.cpp + +Emulation.o: Emulation.cpp Emulation.h \ + KeyboardTranslator.h \ + Screen.h \ + Character.h \ + CharacterColor.h \ + History.h \ + BlockArray.h \ + TerminalCharacterDecoder.h \ + ScreenWindow.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o Emulation.o Emulation.cpp + +Vt102Emulation.o: Vt102Emulation.cpp Vt102Emulation.h \ + Emulation.h \ + Screen.h \ + Character.h \ + CharacterColor.h \ + History.h \ + BlockArray.h \ + KeyboardTranslator.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o Vt102Emulation.o Vt102Emulation.cpp + +TerminalDisplay.o: TerminalDisplay.cpp TerminalDisplay.h \ + Filter.h \ + Character.h \ + CharacterColor.h \ + ColorTables.h \ + konsole_wcwidth.h \ + ScreenWindow.h \ + TerminalCharacterDecoder.h \ + LineFont.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o TerminalDisplay.o TerminalDisplay.cpp + +Filter.o: Filter.cpp Filter.h \ + Character.h \ + CharacterColor.h \ + TerminalCharacterDecoder.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o Filter.o Filter.cpp + +Pty.o: Pty.cpp Pty.h \ + k3process.h \ + kpty.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o Pty.o Pty.cpp + +kpty.o: kpty.cpp kpty_p.h \ + kpty.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o kpty.o kpty.cpp + +k3process.o: k3process.cpp k3process.h \ + k3processcontroller.h \ + kpty.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o k3process.o k3process.cpp + +k3processcontroller.o: k3processcontroller.cpp k3processcontroller.h \ + k3process.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o k3processcontroller.o k3processcontroller.cpp + +Session.o: Session.cpp Session.h \ + History.h \ + BlockArray.h \ + Character.h \ + CharacterColor.h \ + Pty.h \ + k3process.h \ + TerminalDisplay.h \ + Filter.h \ + ColorTables.h \ + ShellCommand.h \ + Vt102Emulation.h \ + Emulation.h \ + Screen.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o Session.o Session.cpp + +ShellCommand.o: ShellCommand.cpp ShellCommand.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o ShellCommand.o ShellCommand.cpp + +qtermwidget.o: qtermwidget.cpp qtermwidget.h \ + Session.h \ + History.h \ + BlockArray.h \ + Character.h \ + CharacterColor.h \ + TerminalDisplay.h \ + Filter.h \ + ColorTables.h + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o qtermwidget.o qtermwidget.cpp + +moc_mainwindow.o: moc_mainwindow.cpp + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_mainwindow.o moc_mainwindow.cpp + +moc_terminal.o: moc_terminal.cpp + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_terminal.o moc_terminal.cpp + +moc_ScreenWindow.o: moc_ScreenWindow.cpp + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_ScreenWindow.o moc_ScreenWindow.cpp + +moc_Emulation.o: moc_Emulation.cpp + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_Emulation.o moc_Emulation.cpp + +moc_Vt102Emulation.o: moc_Vt102Emulation.cpp + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_Vt102Emulation.o moc_Vt102Emulation.cpp + +moc_TerminalDisplay.o: moc_TerminalDisplay.cpp + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_TerminalDisplay.o moc_TerminalDisplay.cpp + +moc_Filter.o: moc_Filter.cpp + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_Filter.o moc_Filter.cpp + +moc_Pty.o: moc_Pty.cpp + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_Pty.o moc_Pty.cpp + +moc_k3process.o: moc_k3process.cpp + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_k3process.o moc_k3process.cpp + +moc_k3processcontroller.o: moc_k3processcontroller.cpp + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_k3processcontroller.o moc_k3processcontroller.cpp + +moc_Session.o: moc_Session.cpp + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_Session.o moc_Session.cpp + +moc_qtermwidget.o: moc_qtermwidget.cpp + $(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_qtermwidget.o moc_qtermwidget.cpp + +####### Install + +install: FORCE + +uninstall: FORCE + +FORCE: +
new file mode 100644 --- /dev/null +++ b/gui//Pty.cpp @@ -0,0 +1,320 @@ +/* + This file is part of Konsole, an X terminal. + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +// Own +#include "Pty.h" + +// System +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> +#include <termios.h> + +// Qt +#include <QtCore> + +// KDE +//#include <KStandardDirs> +//#include <KLocale> +//#include <KDebug> +#include "kpty.h" + +using namespace Konsole; + +void Pty::donePty() +{ + emit done(exitStatus()); +} + +void Pty::setWindowSize(int lines, int cols) +{ + _windowColumns = cols; + _windowLines = lines; + + if (pty()->masterFd() >= 0) + pty()->setWinSize(lines, cols); +} +QSize Pty::windowSize() const +{ + return QSize(_windowColumns,_windowLines); +} + +void Pty::setXonXoff(bool enable) +{ + _xonXoff = enable; + + if (pty()->masterFd() >= 0) + { + struct ::termios ttmode; + pty()->tcGetAttr(&ttmode); + if (!enable) + ttmode.c_iflag &= ~(IXOFF | IXON); + else + ttmode.c_iflag |= (IXOFF | IXON); + if (!pty()->tcSetAttr(&ttmode)) + qWarning("Unable to set terminal attributes."); + } +} + +void Pty::setUtf8Mode(bool enable) +{ +#ifdef IUTF8 // XXX not a reasonable place to check it. + _utf8 = enable; + + if (pty()->masterFd() >= 0) + { + struct ::termios ttmode; + pty()->tcGetAttr(&ttmode); + if (!enable) + ttmode.c_iflag &= ~IUTF8; + else + ttmode.c_iflag |= IUTF8; + if (!pty()->tcSetAttr(&ttmode)) + qWarning("Unable to set terminal attributes."); + } +#endif +} + +void Pty::setErase(char erase) +{ + _eraseChar = erase; + + if (pty()->masterFd() >= 0) + { + struct ::termios ttmode; + + pty()->tcGetAttr(&ttmode); + + ttmode.c_cc[VERASE] = erase; + + if (!pty()->tcSetAttr(&ttmode)) + qWarning("Unable to set terminal attributes."); + } +} + +char Pty::erase() const +{ + if (pty()->masterFd() >= 0) + { + qDebug() << "Getting erase char"; + struct ::termios ttyAttributes; + pty()->tcGetAttr(&ttyAttributes); + return ttyAttributes.c_cc[VERASE]; + } + + return _eraseChar; +} + +void Pty::addEnvironmentVariables(const QStringList& environment) +{ + QListIterator<QString> iter(environment); + while (iter.hasNext()) + { + QString pair = iter.next(); + + // split on the first '=' character + int pos = pair.indexOf('='); + + if ( pos >= 0 ) + { + QString variable = pair.left(pos); + QString value = pair.mid(pos+1); + + //kDebug() << "Setting environment pair" << variable << + // " set to " << value; + + setEnvironment(variable,value); + } + } +} + +int Pty::start(const QString& program, + const QStringList& programArguments, + const QStringList& environment, + ulong winid, + bool addToUtmp +// const QString& dbusService, +// const QString& dbusSession) + ) +{ + clearArguments(); + + setBinaryExecutable(program.toLatin1()); + + addEnvironmentVariables(environment); + + QStringListIterator it( programArguments ); + while (it.hasNext()) + arguments.append( it.next().toUtf8() ); + +// if ( !dbusService.isEmpty() ) +// setEnvironment("KONSOLE_DBUS_SERVICE",dbusService); +// if ( !dbusSession.isEmpty() ) +// setEnvironment("KONSOLE_DBUS_SESSION", dbusSession); + + setEnvironment("WINDOWID", QString::number(winid)); + + // unless the LANGUAGE environment variable has been set explicitly + // set it to a null string + // this fixes the problem where KCatalog sets the LANGUAGE environment + // variable during the application's startup to something which + // differs from LANG,LC_* etc. and causes programs run from + // the terminal to display mesages in the wrong language + // + // this can happen if LANG contains a language which KDE + // does not have a translation for + // + // BR:149300 + if (!environment.contains("LANGUAGE")) + setEnvironment("LANGUAGE",QString()); + + setUsePty(All, addToUtmp); + + pty()->open(); + + struct ::termios ttmode; + pty()->tcGetAttr(&ttmode); + if (!_xonXoff) + ttmode.c_iflag &= ~(IXOFF | IXON); + else + ttmode.c_iflag |= (IXOFF | IXON); +#ifdef IUTF8 // XXX not a reasonable place to check it. + if (!_utf8) + ttmode.c_iflag &= ~IUTF8; + else + ttmode.c_iflag |= IUTF8; +#endif + + if (_eraseChar != 0) + ttmode.c_cc[VERASE] = _eraseChar; + + if (!pty()->tcSetAttr(&ttmode)) + qWarning("Unable to set terminal attributes."); + + pty()->setWinSize(_windowLines, _windowColumns); + + if ( K3Process::start(NotifyOnExit, (Communication) (Stdin | Stdout)) == false ) + return -1; + + resume(); // Start... + return 0; + +} + +void Pty::setWriteable(bool writeable) +{ + struct stat sbuf; + stat(pty()->ttyName(), &sbuf); + if (writeable) + chmod(pty()->ttyName(), sbuf.st_mode | S_IWGRP); + else + chmod(pty()->ttyName(), sbuf.st_mode & ~(S_IWGRP|S_IWOTH)); +} + +Pty::Pty() + : _bufferFull(false), + _windowColumns(0), + _windowLines(0), + _eraseChar(0), + _xonXoff(true), + _utf8(true) +{ + connect(this, SIGNAL(receivedStdout(K3Process *, char *, int )), + this, SLOT(dataReceived(K3Process *,char *, int))); + connect(this, SIGNAL(processExited(K3Process *)), + this, SLOT(donePty())); + connect(this, SIGNAL(wroteStdin(K3Process *)), + this, SLOT(writeReady())); + _pty = new KPty; + + setUsePty(All, false); // utmp will be overridden later +} + +Pty::~Pty() +{ + delete _pty; +} + +void Pty::writeReady() +{ + _pendingSendJobs.erase(_pendingSendJobs.begin()); + _bufferFull = false; + doSendJobs(); +} + +void Pty::doSendJobs() { + if(_pendingSendJobs.isEmpty()) + { + emit bufferEmpty(); + return; + } + + SendJob& job = _pendingSendJobs.first(); + + + if (!writeStdin( job.data(), job.length() )) + { + qWarning("Pty::doSendJobs - Could not send input data to terminal process."); + return; + } + _bufferFull = true; +} + +void Pty::appendSendJob(const char* s, int len) +{ + _pendingSendJobs.append(SendJob(s,len)); +} + +void Pty::sendData(const char* s, int len) +{ + appendSendJob(s,len); + if (!_bufferFull) + doSendJobs(); +} + +void Pty::dataReceived(K3Process *,char *buf, int len) +{ + emit receivedData(buf,len); +} + +void Pty::lockPty(bool lock) +{ + if (lock) + suspend(); + else + resume(); +} + +int Pty::foregroundProcessGroup() const +{ + int pid = tcgetpgrp(pty()->masterFd()); + + if ( pid != -1 ) + { + return pid; + } + + return 0; +} + +//#include "moc_Pty.cpp"
new file mode 100644 --- /dev/null +++ b/gui//Pty.h @@ -0,0 +1,243 @@ +/* + This file is part of Konsole, KDE's terminal emulator. + + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef PTY_H +#define PTY_H + +// Qt +#include <QtCore/QStringList> +#include <QtCore/QVector> +#include <QtCore/QList> +#include <QtCore> + +#include "k3process.h" + + +namespace Konsole +{ + +/** + * The Pty class is used to start the terminal process, + * send data to it, receive data from it and manipulate + * various properties of the pseudo-teletype interface + * used to communicate with the process. + * + * To use this class, construct an instance and connect + * to the sendData slot and receivedData signal to + * send data to or receive data from the process. + * + * To start the terminal process, call the start() method + * with the program name and appropriate arguments. + */ +class Pty: public K3Process +{ +Q_OBJECT + + public: + + /** + * Constructs a new Pty. + * + * Connect to the sendData() slot and receivedData() signal to prepare + * for sending and receiving data from the terminal process. + * + * To start the terminal process, call the run() method with the + * name of the program to start and appropriate arguments. + */ + Pty(); + ~Pty(); + + /** + * Starts the terminal process. + * + * Returns 0 if the process was started successfully or non-zero + * otherwise. + * + * @param program Path to the program to start + * @param arguments Arguments to pass to the program being started + * @param environment A list of key=value pairs which will be added + * to the environment for the new process. At the very least this + * should include an assignment for the TERM environment variable. + * @param winid Specifies the value of the WINDOWID environment variable + * in the process's environment. + * @param addToUtmp Specifies whether a utmp entry should be created for + * the pty used. See K3Process::setUsePty() + * @param dbusService Specifies the value of the KONSOLE_DBUS_SERVICE + * environment variable in the process's environment. + * @param dbusSession Specifies the value of the KONSOLE_DBUS_SESSION + * environment variable in the process's environment. + */ + int start( const QString& program, + const QStringList& arguments, + const QStringList& environment, + ulong winid, + bool addToUtmp +// const QString& dbusService, +// const QString& dbusSession + ); + + /** TODO: Document me */ + void setWriteable(bool writeable); + + /** + * Enables or disables Xon/Xoff flow control. + */ + void setXonXoff(bool on); + + /** + * Sets the size of the window (in lines and columns of characters) + * used by this teletype. + */ + void setWindowSize(int lines, int cols); + + /** Returns the size of the window used by this teletype. See setWindowSize() */ + QSize windowSize() const; + + /** TODO Document me */ + void setErase(char erase); + + /** */ + char erase() const; + + /** + * Returns the process id of the teletype's current foreground + * process. This is the process which is currently reading + * input sent to the terminal via. sendData() + * + * If there is a problem reading the foreground process group, + * 0 will be returned. + */ + int foregroundProcessGroup() const; + + /** + * Returns whether the buffer used to send data to the + * terminal process is full. + */ + bool bufferFull() const { return _bufferFull; } + + + public slots: + + /** + * Put the pty into UTF-8 mode on systems which support it. + */ + void setUtf8Mode(bool on); + + /** + * Suspend or resume processing of data from the standard + * output of the terminal process. + * + * See K3Process::suspend() and K3Process::resume() + * + * @param lock If true, processing of output is suspended, + * otherwise processing is resumed. + */ + void lockPty(bool lock); + + /** + * Sends data to the process currently controlling the + * teletype ( whose id is returned by foregroundProcessGroup() ) + * + * @param buffer Pointer to the data to send. + * @param length Length of @p buffer. + */ + void sendData(const char* buffer, int length); + + signals: + + /** + * Emitted when the terminal process terminates. + * + * @param exitCode The status code which the process exited with. + */ + void done(int exitCode); + + /** + * Emitted when a new block of data is received from + * the teletype. + * + * @param buffer Pointer to the data received. + * @param length Length of @p buffer + */ + void receivedData(const char* buffer, int length); + + /** + * Emitted when the buffer used to send data to the terminal + * process becomes empty, i.e. all data has been sent. + */ + void bufferEmpty(); + + + private slots: + + // called when terminal process exits + void donePty(); + // called when data is received from the terminal process + void dataReceived(K3Process*, char* buffer, int length); + // sends the first enqueued buffer of data to the + // terminal process + void doSendJobs(); + // called when the terminal process is ready to + // receive more data + void writeReady(); + + private: + // takes a list of key=value pairs and adds them + // to the environment for the process + void addEnvironmentVariables(const QStringList& environment); + + // enqueues a buffer of data to be sent to the + // terminal process + void appendSendJob(const char* buffer, int length); + + // a buffer of data in the queue to be sent to the + // terminal process + class SendJob { + public: + SendJob() {} + SendJob(const char* b, int len) : buffer(len) + { + memcpy( buffer.data() , b , len ); + } + + const char* data() const { return buffer.constData(); } + int length() const { return buffer.size(); } + private: + QVector<char> buffer; + }; + + QList<SendJob> _pendingSendJobs; + bool _bufferFull; + + int _windowColumns; + int _windowLines; + char _eraseChar; + bool _xonXoff; + bool _utf8; + KPty *_pty; +}; + +} + +#endif // PTY_H
new file mode 100755 index 0000000000000000000000000000000000000000..07caf7b0e3d6c22453226397d57eed57d2879394 GIT binary patch literal 543012 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�yUhJU@s`OVRY%B)VoM!hycwa4zRj zOQpQsf=6X1iV0V(VWOK|NLS>Gy}>5`vNaX&(9z)chbsqwKi7%BA|1s9xZ8u>m6L)0 zpO_=scY)<?=17lW490_C7|zXB&nua;To*uZ(lcpNRnm}D%eeqb;{*J4vvx6}z!gSx z8}nGB+TnHi(0vdoH#IzIu9Ko)r#2DoyCWF<5?+E!`<)0|F_^Lb-PLc~ULxP=O?J5E zSV;WpYb0s9ISQ?OrQPzSXh=^t^Alb!V(cGzV^3}YFSl;v^&jgZf8LG`ES}sIJ#|4+ zE=uR+Wjy(do>bPi7R9&oUp>(`H=dvF0fxAkKAS&zCE9;1_{5%QXRnyuuf89`8)qx$ z6GcJ(G57mF?7VDmtrYT)c%bHqDA!X_ot}zJD4TH~rLUmO<^S|RBBJPAT)egMA?20U zot}zFD4Y3H-lMbjwAJZsjSA7$hrPD4BONV&LC$UO<)`<^mlMc>o1f(as_^m!{G{sf zJ?NuaMhEy7w2p?)Qrq|X&f5IH2TDIB4M~!IgxJiYbb}jZkCBF$&FOHolDwN$X;NjX zL667x2+qIo`1K*+dTDTFn4AZM`78C{rR7Tnn#Y<=Dbi@OvHad7dx<{HUTR3`onsQ* zGvM1?8R30}jS;<4T%DKb?oGv0Uyg{bba1B`<$CJ56zLD)^Y4Sw&E_O&S5on%2c$zu z`d!7BAJ894+JDELV)E^;O49!rG}dfPs@!dm!pw}Oa94`i=pJJ5F`6R;XBUvNR_-<m z&P=*82~JQjB;DAp(?P_+uN9EUz!?SPG0-RoUx*T%y@M#(2!5SoILHIAkOKpN?HnWm zxXghLf*ApS&az4roc-u3kL6>e2lVjtYz|Bq??z7`9DtdFC;(S=JW~i?^0opX>?_zb zCe8X?$7r;rKpjRZ6)w8;Sm*QRc?111;aII`=p~CDz+O2<<F=kir<&m4ggL;zn>F}J zXv##^rYZ6p-J$zYvs3(9a24~ZFR`atV<fs$_3S4_evCb@(CQfC^ZD2f&BK&6Jld2! z>aqC!J|SQ2qd)-UEqhpB^j!9mUqvms0in=eJ=?tGXy>avpA{B<Xb9wk&%wz>@N5iy zMhyFlcc*$;mx`nLD#hGKp4t(H0_$3iHB~+zt8ycI1m6lIBL@D9hZm%DbDW$?FPQ|# z)G8uMU$4R%b>tPu`m7_Jz3P6(z>19Ue-^$S@Cs(bitHmF=)hlv;L2^_)l@^>WX8b> zOIYMTmG{_Sa9qEGMfzJqDUJ~z;V*57xew+kzt91{GD>jlX79Pe%;0(Gb*BbW8m~|q z$Eb|K5~F#EFwPv{41t~t0hF>Pi(`L%phO_W3&qNRMAK_9A{}qQYY}GNzj&R-Vf_z* zmlz%X!sSi;O&Gq8By`6dVupGR*U}mjNGmp`Je|J^gWb_o@#L(NTJVTKVpI5gFfTad z+q}p?a#?Y#R>K3x=F~Qb##&7j99wSJ+M#j4`EV5d)D@r2WA%6&THII1Xj{{j$bnhN zF_mYluQS_8>~YWF+jt((baMtQ?6EPl%4A>=CfN~1>vRUpEsxyC@5yeq?0bIsn;&}v z3(~Y$TBRmF2<HM6NSi=oqw*`RIa;io518nvw6S${J7{o3>9CyA6sNCnB|bVVfQh&y zZ>2}h%Mbb=2|UkEx|t$unh#nZ_B;FDAn-pgMDNc{^PA((QqbMFHlnU5MG}qdzr+wJ zCR@riWlLhiY$+qG27OVkO|pSloA7Hw6Qfyf+sf;^CWLbz9Qj~`!Y&-`=Jh=aOtH1C z1>L^)G*fP!WVa+cW~A^7{J_GK@+$UJ`4RLNI|2*rDG_2&j5f>QFIIuhV^;G`an_~v zILprg<|s`%lqUU{DwT+?oT$ov4cPJnW@)9YNr86%p>-){a8vWKUr{+mkNi$72jtf} z5p|<12hth0iu@=YjAi9+8}hPy`6KdYU6h7j&98FgE$nXIqi20>;X7D$fZr^gWCdN^ zDhM0DDmOsj3@;;P#!MU!>3DRNe6ow5&y!cNtvSW5G-8dE$_tu#IjL|=<W+fmv{K~5 z_}=^GJgkZh=pK>oqVt>58D56JUz>roWeoj#Vvyl!U8<u}aOAQ|ND?+pH;1I6PZYam z+zI=Ch`C`BdOfAG$t~SI+8mWFO*gksmo8+(n!*;AbVy1EkD?*qN|a*mET^@m^Ppa2 zJst-OfRq>_Uis8NJJUlO7?0^O`PBG__L2xu`c)nh3HM7xX+KtC@s%jC*c0Wi%y77h zgdxj%K?=JT84>9oh&k^+0jo)8rEx+#(-^$U8QL7UeXY~~qwQV5o2s(K@uV#@VDSX0 zP?6E98K+fLs*FP{XhIV>l>k*h8O6DsDRvmana(xUDg}yZl;a^#MFmGt$0v$2s5pS4 z^Z{)F9~4wXQ9(r5Arw%gAd=?49{Ze}6ufhP^W8t+N3$Pi@4fbG?X}ikdo51<{1<Eo z<zq8vJLJTgjeYiW?-eHkO#o{W793EM2ve@A4@YN!E}|Lrk&`Zc5O2+E>X8t$MSN+9 z#s^jw=(MTBiOM2X`w+)K;FVJd@j$pCIFc-8klh0`cXGhSuY-r+8$%C1FI4e)VF_%Q ziA-Ow;QfC{{M}Mb<y)ANAVRuT^aJvn&|J6y$RU?9wheN%Hf>9zI6w5Y-QKq4#Fpds zZRz7(xyQD{fMyd1lD`2?h=DM+PAY3}Oowj4Fb^OKc|k4@-_(Ac34F4AGaB;H36@BR ze}c$b913p?DE?hA?ZSpE9D$4Gi7v)P*W$O?qXQBa`#vy^IEFCk_)Z<72tkHK(t8nZ zSdQXbSX<FO_*@cTrjN2+5X^(j3|u3z59^Zi$M$Rj`1sJUX9IM<#h}gX1Gr;q>QO@n zkjM|k+O}ft1HZOe38VV8S6S&vYw@|xv@f-@xrn715LF=3Y`2lm#tD5pjYrQd;<zFs zVhj+Sq2pI7Pd5yK;!5op=zC2yR7Bb5bcFWq3$=_AH}^(vGw4#dRNgt}UfCyh^v6h1 zrWzTS?ypU#im$0=NQUx2AG_o%g&W$&FMI!zoAn?JwfUV%yyg_m*&JoIQ(XNknzS(b z`zY4^HlOZs_`?k|`>DFesjRa7vIY?LFxxNjv_u6p;KBo16ft54wv?G8TM|nK8eRS7 zSDoZ5Xd*7uQe3E0aG_S9afYgZ^jZkW2S?iCZ-&NpX<Fs_rFI%&CHxFhs4K^1)}Uj@ zb~}16gMIKQ?SmP)YN&OHIQ9z~)|q!GD~otyqIXETR$1vxxIW44?Kk{?7@hWr@6M8A zfL#azp&7MX@QzV;;dwMPkHP##FHG7?F<P=kZ7KXk&Ga62W#uTxIa`ly4ejn38Q(Jv z4NXL{AE|lgvDc|&tq+=sZe+g_&|Uk`OyvPo#gCL6uQ>r>)EJFa9QM?7eyTihEAIS; z9GY5|Eh#GxM+UEq1g~~z-)Y~A`_AlS8y(cYnyNHtzr=n{!?Cu|uAUK}GYvhsp(tZ~ z7?1q>pT{azz5OEnys?yi9(sj-?nib?e*}*O#^;3HVgCS54<b$vNSq#B96x<~_O@6Z z`{h}wrbBuf!L6*Uu*-+J@Am>|f0y*r;nl}vc=f);+GnFRILdKDiz#Ro_EjFAF5_w6 zbjoOUke3d_?g=lx2jIwo9i}MZr_glG`zI~))}TgwH9eyT*#1HNGMEhQu=bs&W1n}m z8zFk@D*XF4{au59%NvD5Yiih^7Rk8L-jLU*wZX^A%8g!S`9{oZ5Rl`6oF?tCw_}&G zQtIA8{#LD}yz!vJ({M0dI}D|?*7C+f5IqDXu8RyEVo!MTI%$kN%E}HXa74TUa~U0K z^oLv^`W_h?9V(A9a-?{5zOl`!T~fZ(&tkpnwD!A__%4X_Qk+noM1IFm+F>BN&0-ZG zoSNzBfK~8$JfnzjdShB9q`hooa&t>>KC*iZ=t`L!%fh{nKDyxec8Kj4GIl}o!(Xtm zqbSV@TjvT}@Pa@j;JO0EU(R9;0Q+7pfQ82s37@hO@`^Qp`^RL?uoLXAGJG~;m#)Vk zVwZ_%`lS?k`*r1Yuun4+u4WYwfE>@Fbck0NaX&hW{yN#f;-j!$ZcbSDxdZp-9?Skj zr5<yChSF$%@{B`|yX?*{dctgOv*L|WJpr5bFt+kt4jjsr_|<DDPvG@F&SCDS|9tmm z;O^}X2O(}=2X5Y;xOuapBQ5r>y0nM*A~u)s(x~o`-i;<7z|4^QHwz*CA-R8Rrgy{g z8UF|HW+S|Pk-k8Ha}ePE2=IVt?a#Jnfww4QXy)A|#vI{O9yv#MkCOP)d0PB=O%LeN zg>iGgN&N7Tw4-IjFG>6izUYb_R$oPJY*dzSAViUhA!P1WWqBJR3kK=mU<oQ@q6#OX z3T63WQR6@pEGK^skG=bEaMuU_w~%T347fxlde8kyj-SL|`{SwcOVGjt^1rCwZ{>ee z<5qs}rzF3}_6MbzkOxV}6=>xvfn3S$@bsubf7g@H`QbD+!I6u|ArFzmAQXpN=#6l@ z-qWZ5Ap=SEyDIb>vx3b5J%mKbXsGZl_lD|!MMiYG+mtbFKKJhGrJ86we!f?0@fIBP z=C>3@vMzRSn)!$?f447khu!BsIP(F0T+pZO^5-A*Da*?y0Z)lo5gnt`_FvuvDOr-+ zL_R0sOD~(IEZ?Lqf+_g~_Krs_+gKbKS`*2>5n{!SyVJt$#jvGjj>V%7NV#Y?Ae)OO z>ElZEQB#h6sVpB?4NVCWI2UwiS-@}X92uEBFcBHw8xZu@n307<u+{`Eh--ibjebd% zeLAVeR<V$4gjx=djJPh2J%2j?s&f31I$lK7@vo6OuDtl?7)_w<77=YH{Y#JJnQ5!3 z$A=g;CHysh6sliMvD$AihK7`+{ieI#B%>tys0tWRTJ~!vImzSwIXT1?uTEoX&MESg zWP&~l*shy%_2Iy|T<Xn9_vWS#&(T^M51t>%=)UE6q#|1%J^+?Z1~3o!MRvM(K>F}( zt*LQ8e37>0IGR^^DcJ(cAN0&b$rMOIrS7$;4J5{&*~J{H5<b!?4J3(A2S{$DysqUz z>a9EiJhpnDQ~I?Qe}z-BK99!O=PH4&SKxq)6Y1z<dooHG0?2=U&bD&Ve$62#nakyl zTD@!6`t58pCPDS9k!e60^v*I}W`|Zw;fx0*A#f-U5$n>BgqS96dXV*Xyp0Q-Wp*8w z+PvNdVgIr0Z_-_MZ6l409QI7~)+-~8$8(|V?g1&&R0<s|#Wf}J;p}EuZP|XCxZ{kJ z?{bNC7VFRGO!e3u^s@os9cfkr!j};ZvJB@%8W2|X1z6Qv<8hn~Bf@=Qg+!k+{AK5g ztIq|FemUDaLn`WGh(`w{WPXY0i}6>bRV*mKS5jGPds!?~ZIOvD_v%biX0Jj7y<?+W zYq7Z}b7C*aFK6{4B8_G@w%5F)nj^DIIU5^%tS2Xp?$z1Fzp%Vlg^q?3!ER#OD6*;R zSeMG*{-j3gNu!!6KZDIpG1@v&%)c&?2sc!Y!)-@M3JziUn<G)w09qJi*oN;tCb{xd zHh?)9jqDNi6-4T*S_&Z;4xe5&$)`^(MX9TP*EB{@--=;a8>|nXJ{_|#)qWH{m?ADh zRH!UJ4q#?`Bhv;ZATVHLWbki%0JC#sB>3wDhH|2RNu=E8SmZIEbd|?E$V2cGP_}$Y zSBo#)#cG<^CS{f0U?VYT2rX2ojr((zRcqcM+GNFALe_xF;VU#};nNqe&5M9*03^L? z5G{R?Xvx7kn8aDbHaLQ={!j_hIe=8kQ7o2?H6b;G(kUlY_Y6V5UtNTCW)Tq_hzx`c zN~aH1-W@A<U~N6UWJ#5X)SW({>Bd9`E~R=ih?Ew4_5)#z)HJ%tg;n)6RLx(D;&y|Y zRwFu&2yj}-+!%SG!EF*krFjSfMH(V?_@5IqrTV*m5MNZ7_v<yQP-tS04(`)Q_|B)F zr;l*>^qWh4!0Gw)k&}GdXQ2i=9Cn+0`P<XSPJvL9Pum1PZRs~pi&Wba`iLy(n1H?? zhA&~b4*2pnKz>8Smxl8AA8<ViT#tv>N%`}jnpG%Ovx_fICXD;?CbU)7cp_KhJBS>- z8HN7?#1tvx&BYI_B3Y6r=83AFBsd>*EAS7V+74{OGiTL<P=641K|Q{7((iGGJE|^0 zo9P@M>kMzKIxI#b=d^|de3b)QLKI-XLu>K1YV;S5&P37=>2(SA{%sjgeIfqptMEjP z?(CUn`=9dbPtC_`>>nm*pRnKcdJ-KApm5REFrU&SPJXL2o6W4>fw~mGf^$XNf9bdi znOo2yCTEzUXLhW;z|Q&>QXKt5XWYp5!O2v4jzuE@RXFfkl^UTIqL1}3`Yp@*ddMb$ zw30RzIpbU)9?|@3_(sDS@&CpiHEn(iZkwWygavnPvGxJ%eI(a_0PxydyjwcOrg%I7 zq#I8>%z05H<JwD$rpQGwo^FSM95`|QvFW8=&2RI=;?EF|LMgRT<a+bh19f+4(KPwV zq=Kz5v}zN4=+$S~w6<`<s}w^767+FZ?g_T)o}vAAZ31LtN;PdL+FEi~84N27#MmY* zW^A-6(DORfQc<;SG@Wp$pt(QT<qo`p$YTHK`n$LRb+pDAgtYhWXIi+gsyj(wzB6%S zj!Mp}$!30Qy0rTC#;a`g&CDYI?Eiy3zR=7;DBunbIGxRDBCAneQ$K_j{~wcJs*$^M zgs;<_Vjp~;C9djbt{88q+3wZWi$OT`x<3#5QWOdN(nj|~`Hi6_yFc9EQJ!jng%xVa z5dAXKY|*<D?Ei(<I1wOuY$G&fKeRlEnhr_)1p7I7MnQuaFX)Ko!%{z;fB4XWJW>bQ zSFR0qiE*&tYa7-e4HE1RV`iH8LyPw7lJ;@6eJg%1Zt8;)kIL=Js=+Qb@@|!Uk|_@M zML}hPaJk~!9$QtX&Thrr==k1@FU|YDnoc_)9URPaPzmPPw$R=TF{YQP2~%`3PTDog z(v4d_sRr}6bS53nf&oVP$Ad$x_&EhX%sxPbdT172k)ydA5s|b^g;!bmF)|yAfr#{= zDd2QF6P*=4U3K`w8<kr0YQS{d3`8Q&+6}U9cTYdlv`Al;2+huvrsc})ExROpfe6?y z7G*H4SDK_PkJ?TlanX7c9&QzCWG*utNI&^;T*)s*D_a+n%ogFS>oP6MQ-8=n@AlI@ z;w^7N-ZyT>F~mNl`g^YseUYr3y&gxWi$C-vwY-*k{eB{zl&e>WUXo1qB-w&i$rgA- z1E5)HY~03l=07pzR|%_qWkdzyaZEpHy}X*-pM`Cc`Wu&gH}>awz9d~r{>VnENmL&B zDV~UZV$!89_&U}?`dmlEOJ@)bEiX}8Zw2lop7?k7TSI7_AOa}F9c@8FQ%)ojp&5C0 zrFI^!>y;r~aj^XRz+Of(@0W&-X9NcteBjUG4w}6Vf4{WK&rM7LoU7<zH!+3QOh&3R zq05LQypgPdfD+`TB4Y}1A}!4pTa?`2I7U~)+=r$kgh<4N*5FHc`^;Y}D;L~C$m1># z?wlHQw#mEe6Ph$Mp)40h4|2=Uc_v=WQ2$y(SPA}vo{lB3i&GHXjkY9$)7B%76JygD zZbELS0PY3AjmnFVFSJEhbsG9Wv~m;ICXD7`rg^)M%Hd^jk)8Aj2y5y0c+f8HU}fdP zTZ~(I@hlV;|L7riL;*4WU&#%SG?A+?I=kGu9-X55nS^BW*2(3C7NfWJ>omNow2BvO zghM?x)Y^|OL`?~X+i-<h`xEoO69SPDfL4#fzNKATuvKEBIdXI565DHV!^TdoOz$*F zHnGSCJ2_GbOK=U6vmz&phro6OH_QqmV|m{g7sC?OjCV~DQ*B8_jnofHFf>uz2Ordo zJ}ZMS(_TFuPsm#;JUtEG?=eh<CjSB3{}lS<bo*!afz>pcGd<M#Jp~<J?Gt3-&J-^X zBNrW7OL60YTyH3zargClkpm8DtnZibzd<W<v=p6D!{_sM6tocYpf8p;`OlN(KfKu| zgMPEuujV5U<=a~O@Pw;<ks<F!5le*>u{fV&3Lx^R#HT?-uD}mVR7#31w;qj4Z28@V z@*0w(Z@~Pcwygdb1MwJ+5pZy~2c^t7kml4URF5v(Gb%E8a&ctv8)E(ihNefjAJ<iZ zgE*u!E-y=w5j`C9*<k;5!OTiHO&Z>=5LJM_qDLeiMX`eHkLOw0A3SED-d*u{mvbfj zu0+PA;wr4<pXUAo;;{t&Pn1jJ5DLx4<DW^weZ)`+>B25}Fr*3%nu$&}^zrmcVk=O@ zk_(GAI!eL`@VwR?;)PPGKsbC}Hm}_Bcsz;f@0E!+V4^W1vdAE@22x{<i0cm-^qL`` zJ7SoG9#S5f>AVVvmtZc5N)Cz2AL4XhlB=>Hbh5;{bvzw60j)5*nuGw3ppVMz0}eh{ z41)~c87`L>&RiJ(&d`s2?s-*RO-sGa{;c}ZE}%8<{aekbvoeUNiT%yIKb@X)IFSj{ z;nV-<IGj+}vb575-cz|Hw!}Q|#4^B0v8YLZX{%~d<i9N=ReOqlIiZWlGTH!u>wv4C zPyD&E>Oth<l~wgt4xU+j!1L?*WNNl0S$X1~Eqpeh6K3&&-Bz`OKDs#p)_e}|>vMdr z-lDBNj_8-2X5wwpoRF**uOjDA<zg9qga+YPpcMqvADt6oopGpR4t*;6W^_tVNb9=r zZ{tGsH~{I;%V77KS*oqF8}W}Y4$x@l+p~b^s7F>(T#1KZ2tNjDe+Qb5-Uf$l<rZ8z z+h~z}^>zfmMk|=NM$^;jeDLW{(=}XpM^$M-L%~K>Ux@oQ)Mzhn%}C3&A={izNiD<E z?JdJI;I|w6@1ByA2{{h<-vh!uZL*FQ9~@w6s3FTeADfu*{1M=vCrI>;b0o$dMyx6M ztSS_^MDBnAYGjq3D2e_m@;?y$zKbUUI+4ue4iQcmZ=!CO&14_d$eV<rG)7d~t7IQm zEQhhzLue0%lx9i{rI}SW8l>s1Mbiv*vh{}$lr6~8VaxfAHEYm-icQn8!pb5&gf-IH zWiW%lyaB5>rp4W+6$OD;(>6i6R#dHR05%YE#vay-s<gW*+$|8)?y8b_80Hw}ueE53 z(RA@4%P33%<}km>U`9)|1=wtKoT)!Pgt8_VfB^Vs<PPP3LFq5WxC3kiFRbM-$B zxb#oi`TMyxvHhUQtK-12nf(;82aW==zEFg1-D;20XJQFZ713tPeW9v{7=WFsUXrU1 z9}sFtyL5QARy0NY^`Hels@g=C69GOxVgb5KZy1o@lzv0Dgzg#(bW|)N71I#8k`xCQ z$9`HI9*u410(dA&6P<8YM=!>E(^UKrI^YoZqjvJtpt5{wg|>{G=rr5avUWgtcfu-d zx2ui&(!`P@(0C}G2wsZ<LByw!Q{D<2p=Eg5DO=!_g&%6l!qSx+RgI)}g!%CSO7zi_ zQm@+ZF9+Co99!#ukiSH2rjZrUIelahZ)=4Y54aE@BAZ@Wq5Ffu8HIygK7F9er;Iu1 z54X=OEricdZ5r{nLmAUnutP1|8i)+N5pBw66hnssk-;|>H||Y~^)&qY_+bRr1+={Z z?L=sueYE`tyr3QGQS~rWTtin2k%l6dSoKGEGCG7Cb}2xh>NDu|jzcURoWKP4U{?^1 zyhAPF!6a(?`ddhTjKop@BKiT&nahsbZJcjose(nhp5cKm6#9det76-UUX1R**|XA{ zBXgDI_mnEjZ=R&91AMwC2N=#=z)zQ|+3pX6{j^nRNk&_UW?`ahyBT)iz3_!UztQL3 zRQcr*3;k;7r`FWd_O3i%4b@}EZNYA$v@;<erdZ@_H&^KQ1Vv@S@Og;$8j^)1a#1|A z57#RYXnK`XL=wq}yaXZtZjb^e1b%TVeol>;@)c9QkP`;(<R{>$ilrFCa@(XWt4OnM ze~G)qrEFNAeL|3T2z0>E3{-*S6Y5!l$F;;(pCTVjdW_NUVf8N}R#pgu&U*q5CvQ5N z_+f^=CL0A>KA?r({Z4)*FO2qLcSS#-hQC)XY4~3GUAL*%r`gJt%js7Mwc~-R<Zbv~ znbhk~kn+7Uuo1q*IQ&lRHk#AgA{l93qR}lp7fTq}%6*H^HPOD-ivw^6?0IFCNrqnf zeLSHxubB-X#uVg#*4fqBKQ%!#c(JnfDyHJEmSv>6+cD^cyE*s~RcrGii5PB}$#kpO ze=MHx!Y(WI>Ot3}fVK|-n=I0g+DWr-S)4Uc#+@|A>n*||?3864cB-nFI@J)(x9XDt z`&f_Y>e7_^3op{IL^;it53%m+X%NY!N0`Y_>6dhS>c*|+W8^0|HA4rS;f|IEabeq) zu^)!F;-NG)sEw@9XH~m@RK{)wyo!rm8&$23s&a2o#x}x^){3fv=K^tqS|K8EfZJFP zJUsN*K~5!hib@iruh^Al;56s0(TDfM@qX;VT~>KglK!lJ8i);p0tFuQ&wwkAtmW$? z6l#{1idl5>?d4P9kT>x$dhj@f$$kR%l&7{m*i$bWf`p}3G`P!cKC_~d|Lr09-|vwV zt7C2pnXqD%-O-&FFky5bWbqDk_T$p!#qT@>H^UhrnO-`<`Vk&W#2?DtkKnFLWtQ)& z-4VQ;OX1ud6l;lX$#4a<L#kfk6rD%xHfs-qLsITES=^K>Uh0gWR-4IkpGG6|G_2d$ zyE2X&7n>G;g3{6dPP%q{VlIn{3LHE#J&Zj|JOq~L2RS4_h<$4MJ#8I{h8b=3$ODMG zzl~ciS?3VhHgyQ+-9mYkp`3^(#4SI@x#bpqu5rh4Imw5_b?x!fNl{i4Up9RRj@aDe zGK`~)%nsG@_;+!GK9<uwIg>jxlwhGd(!aLz(#?mYSnL%2-jw@+K%_h;MZtFwp_TwL zeB)h{=q<rsXnqo|=~l!F2*&(<B(#?D^@)fN#MG%IUq?5uy;1TpeY~VL(M+@+LZ;ar zLTg|%gSvkpP@TW7Su=!H9FPmp$K{1kkrjnUp%ZDre)<9ohPMWLEzMABBlsl8kdsYO zSz&fED$Kr0lrC!Rvy#O-U6m~4`4!3_2+-n~GXFkW)hmwkHWp9A{%a-RsGIZ=hY)PK z@ulJzie5|dbrftV;Ss1@)MOEFO$zO{LqBN7&uI^Rn-0MH_b*LjWe&d}aaT!u+#&%J zH$dw+j3oWy`%LYiDg7!sWVc1TC+S|D;0&-2UsPcrgK6}ZX#0(n;fqw*eaiB?CW(EV z;DzaUU*@@w`;^h}fBE!DV$qE_?tsb<p>9*QORVCLNR0I)W(ElH1(4}mgYEdV6&X^B zRN~&CEuhgHvjJ731CeD2n6i9SsitEnQIboNYF%}NHwJ$xuT|gN9hYJzl)whGC@FH_ zv%6PB+|cGeB))8qTdpFl(kOWsd7zzT7)e2R_yy^Rt3><-_Fwezc#6NtaRYUKPUQNY z?hUgp3hnRdZmS-kj~k@>2XLA~X-b3@cs)`++V_y1C5%fhVOKogqT|0R_PrO7k8N?r zqLbs6w@jPmRl`dM;)%NdS$;hGz;-$X>rq!rJBoxQVD(IovhpaY)+~0heC$<MgN;R6 z8*D7nYU5?TotAlB{f2Bqf2JTBPwLN9v@&Nv`*zhX;2#0g(NzKS5~xK}tgXi<JTJqL z2DIneq!vw5#8i8T?l{(9j<`x<P12sf+K^E3W3Q3N<4H+aDWkoMVsU!NO&1&<6oZPx z$E8$9+_Z~Bx$Jp8^SCkVIFelcoBdp#7YymsLKXjzRg{Q6yJ$1DNi`Xdcy^zez&EtB zlV1?mY_{(dkDp8_>=ZAU)Jx&<WGQ-k-t!yFx6CcJvcr@t%s%62muC0xHWLi0Q`J}S z=uR&YQ+LT(vfR&zzB{pbDRzXfq?dTL)mAaN+}7&JBGQO%iCR6Zn&Qd62h&z_+5k>N zQ#rPRjjbLRAumrZQ>85`>~bE~N$qkP#UsZ|IHSlLmYe5jg}4~<qG{yyj*ig2`*4XI z5(jO(z+0kmLuls_#mD&Ec|B?o+6BLYLujYDGj7Ldn|T?ttwRd!XjNME>pZm`o;j<~ zs%J0*<s}6j(YH=o_*tFa46U=s$F|eR-a}ek(c2mlsq*J>sMW_;h-z!*YuBJN_L0sx zL*Z8+HtV5_Y+@6M0*NkU6{2FA!09Cz>rNAxYs`O2%Hfn0V0l6)eK$#)36Ox+TFr#X zkWgh!D1rn!ci~j*X{L@wLlDA6RE4B8Z~i=<q?=jBUkl4Fwgm5c*I=C@i1FnV$413u zjL)MuHX+t8NF-t}s1eHL3yTX9ad}}8p*lGA3`^=Fo00m$=f>p*S*gIJ`}Y_hGOBin z*FG~-TE%m)WMXS#&&58F*Ao2At5T5dh*rSTjIC2chn&%yJI(tMB)`N+64&j#HTo2^ z(M5kBzK?xw;TNPVArQIKGmZ#tpkJJZ0++ZGaX_oz#uM+SiHC*U>K{lO#y^O+ncqWS zC*VYG@vC-7DB=7`w7vYL{4<$<D)=YJKUMrwO+R6rBo5WeklIR;1#T#eLt6TKqPc%R zM~df{AW9BIUO=1Uv7UxMxpmpNsj9z*o#+0hrO0V(De7&j+}9c&B&ifBS0Loy5+?1_ zmN05p+Us8d622w;n=}Gss6o|)J!Z&fhEy{YFheC~XuR3hM6Un0{BsNc+{!If$gjjd zaqDPdTum?(X4zW)`T_oVkbmZI*}3LdB#?k1!WAmnl9?I}5otQMkc|fXV0D@~oDF8_ z4KlOMgnhf2_XRhFA`-%j;az4(@J9);;4?|auM-<oMD_3TyTK)gsWT>N7&ghput6HO zhD~xY>`0eGu?|ssYZzC)?9A&t-vu45;eqK!9eHMGuvy0-8KV1bppIJo(|C=M;DZhi zv1dDidr(5$frqWOgd`p0_wSHM-;t2!Lh_NN)!#L&qx9dwiW2wlHj~ka3@bqGX9cJZ z*mta6TAk9)hkL1=MY7BrPA<c0r8`BvS<IhOY#|qWMiy)6Qfv+slkb`ji8H=RB-TFj z_i~E-?yUqr%p^JiQ-*$L`BvG1gF{!tYZaNMABoE%XUY`h_c$d(rW`wLz`5j8E1c&O z2X$Ks6M=R-&-BU?(m#bd5cH2F=rJcN(thxb!I+r_WgzvXkNHG=8ip8%EC>d)Z^RZl z<liOq{lbqFCRF_dhY5{GI7}$J;FC@|bkBiuv8Az<gcoAVC0Dx+Fk9@ky4cgmwl3vX z#GiNKIsHxSc}&Edy4dTdv+rWaUuPdUPD6r{k3S%T4+KsF50a{}8#2+82*0`Hv<qpJ zFKkQ*wI#p9p!o045@>S?vjZ$lRhIjM>Fn)`Ucq=!ZNH`}JwsVp0sl+7DJ#|P$^&V{ zSxjP60dv2=*4PsqaNOb~t<q{1gJYuD|9SEYF$_gP`o9b#*3V#CekmT4(y5$Ax+m=S z8G`Ki`AH%f#se)|HXNOhHW7!R#<K9d#ANtXO?CK;@0|jA9{PGFK|D@OiJurb3{JqH zd;P2i-5-ohw92!T<?8`JmMya*HT!lR4fy|nvdUJjv41mR|13_=!ZaIkW8yn{j+KCF zmD&k*PHXp3J4!7&jEWT5^$C;o(WQQ%VeQD}Da(I^|GT}RR=cu%1L}tOG!z#q%eUd( z?ch+<j>>ldHqs$B`-oQ$UWrvV`Q+`;g=n}(`yq-nWL<cNG7mM3LoqvaEqYnPcvpz` zy3%nW$xA~Y@w8w9@ajXw*7jrqvb%3=jVF*JJrUz|R+C&m7!=PQj3<&;?x>Feyoi{) z2L~niYgD!O<$ps<VMJtTA(8EdISw@TGX#5@jeq$4^*4Kx&5Mg+Uqth;KiJa^Md-en z@1Zfiv<M+=jXr5SDHP?GTh`NWFDHfzcZx-RFCU<yuV%WJqt{@aA6Xa~$q4oT%3Q(Y zksAUwNo?C^a6$nn5uHa*dOi3Muc5b}b*jW}Xf}Ei5aynm&P?Tj7}OORaU9>?p`SWm zWW-@_&GAo_2hfRKs3nJX5Wi8vcSln@dct-JweXv+a%|-Vl3~JE_!w*DR(fD5U+L`> zKKvpCU(}qiV~+8D?XX(bR-bJ%cBr!aQ|QLo|F2Kk#^Z(ZO|a%l@HTlOUL)UvPuV6# zou@Pz@eN9gX^^l@4K=03?xz=LC0V53=!b|@lksT{1XB624hV*JXHMO8)!UfW@qNKo z_>9xJgP42bs<&M!xtRF<RoV&c{Rmi_(eO_e{jxW=7<|$#11)P|^gtf2<}X+Tz1Q3W zP^JMAW1BF!If=s7i6;*7Y49*)*CFmL)_yRqzpBKj&2UEOZ%RA-xFr5`D>LG(uaiVx zPGpvxjKMCJ+K1#`7oU<4=f^&k$D<E>bmv}Hl4W|d&l8VailVCVt{i`OYo(Lqu4*A< zR9=8`58MY8m(nd&m8uEjJi`xyu<LUEt&I?Tui>G=A*s$_Gwc%+&9Ev4dL+LNoFya# zM64~5lpT~LZ*UO-@Fnmdacr+4JOU{9w;LjWk|^^Mxjr;>g&vsw;f*9KQo;jKWH*f5 zu^$o(p<~extm=c|OPCVq73H}(`Z>>M%;Z>)2xfV2g)d_tW8Qake>vWe5PPJaD7}@? zI$0a#7o<;fmxr%e&Objv#`?tLIULZM#dH}&l(_j^Q=W>NoVi-mht@w@#N9XZ^ya3r zhJTPBixIc*VsYXT`7(*1#<OG|9~q0^MB|3yUiB`f_FU$5R(}~9B%H!>QDgF#2>9b+ z;LRQ}wYy$=-*{gA<vt#-=Fj4Wk|j|i&HIe%*C5}w)zrr^^mh#%yh{+6%<~Xp$zJXi zc{mwp<0+-Hy$mnk60V^xk*H0i>h_-bKFcGn{>;$VMEa&!>m(8f=^K?c3fWy$YOezP zAnqHGfDJd3l$EBa+M_IJV}&^O04shoAVK07jA$(Z?U)F+a3L{JmKJ^b8`a=)f1xkO zU?^TOf<D(^@ed=?EY9keNND@Hy7JAC99m0!p;Fr`6A$-e{(^Qf>_fg~Ay^ROgm)mC z)Y;RmS5d$8wmcAKVpxKYh_{7ZPvTiP(W{TBfOvtO)321jSIJ*=m1%RYgU|&s<wDZF z9<we~?HY{ni!km}il>p`sX<U*(CcpYDPu)(*_Uc$=(*yakIcKEBAAfliC_-WO-?l= zP+0)6NK+c7SGIf6%Ge^JuTfMzOi~GWtx*ib!01i!1hhNIrp!Zwdg|02Sx^G83X<ox z;)pNPjv`c*v?buY#!XSIKaL|;fc61K1o377ut`D^RgJsSRC^nOGlcETAUaqMb72r& z14H5%j4~<^+^A|%{4y_~9Y;fWC9&)ln;j4d{BO)6Z3g$@;6P>7HRW!1<sN0_Eono3 zn^w6)zc1Z=Z+hiNK7Dwm&+W~u+!)&=*J;5HZ_Qfb%at(tvx8=od94^I*c1Cpd7VF! z(cfRN(eG{!_AC!ocguoFeYDXheTPD2>HH{5$A<zs-n2<~v!dy}cw$6k@<o6#9;KAS ziJqiHwua`fb~yZ$*CEa4cYmwQ8;2uY_JdbhIWJ5{OUB)B7=&$Tgd(&%t;Z&>@))fo zZ_Uu(*n+KaqUV(R!#jfS(p0wiT<7E2-5>3e*?sQ)L4aiZ-RS`HW6;{8kgcqom^NpU z>sNY7CM-i-SGp&?kUJ90)MSseLqrA<kiPtVp@dzTcM)lJwAlSvw?<DQGVU&p4E_zy zo?(|H(9KpJa>Fog1e}Vu*rA3jEacufYd5{urSEiZ*?mK%65c>$)OgpguDKyi3BL`o znrmTTUWb5hNB{s1zli9$44;=_A45J@mL2*y;QH9pNqq}B+!q1kVGQQ{6{cQfvHk#} zEyR+ARSsqD_wa4)4y87P+f%=35<8-2PsMO5P4uRZnda5+E>)H{MKbJ=G6_=fjn`4B zS%h!63btx<iHe8>&P*6WW{o{%Lk@9+M|LvVY`7rC9VEWm%i9SPg_l?*R1yN{^6)b8 zd_ext$y~dSPDwZHW@Y}{XrxXbQAM%2f439d)kt4^-Jf{15mhs)z52{jEHcTb9rUM< zn1U$9m%ksk+azWAF{58Z3`!@}k->w2s0&8M_e}tWSAkYJiI_*lHt3$uRV@l+58A9f z7$AE<Eaa@OffkCa%rksP*pLzVb9KR1ar7$-b83{`^(~jhL{aoLf8iuf>6i$?kpNpU z%vX&I5t-;~QE(BO<+N4(_h{7Q0jwczeLqfGMowF>Co6Xszn{cW;sBnXv)AJ>Vj=!@ zVw<vxBWr1HZU)*yEo01t3ChYH%1U3FGqfwC$2#S)by1)$67=9r4BTP?D~vR)1uSLG z?Rb%sgG;aF0g{@L>KiPeNigVx06ZA@nXsU43SNrSxBNqxlbcE-Sqfyq%9=SvS>9qQ z1I{Haz{vP(5}GtGBstKC&z^uSOGtF9#tz}dBVY0W?y?T?49P`dqS%F8)MlbKc*)_( z|K3i^XszAJ<iU$ulEv_&7g2=qnK4(G@+hXX8Z(b6_hZVxkZ^bn=kKG>fPmY3yzBcV zQ=g5fuV9JsrtgQ%ai*|Z#1nmzujjtpY+kv-5xjE?e^k)nZuTgTwusBK`L!?#n{SIY z9OGDuz2sBh`{UCO?I=?_m%%FQ@VWYA1bfu1ArWNdog`1%=Y5B`zbyD*_E6+J(s&hO z`+QOKI>1@8hNPj%#}k!f{Iz?6XHXqG#acK*Nca9Us>+OybboH>_&B8&r!aK<hRXX{ zcGV08qj&K$LKM64;#&^qp79!UjC_q5n{U)JtS{=#)cy?;hxMc2OBn0}!Js<qY)lDX zn_%jdaC$+9m~j>miiJ|2yQW#v+*Uh<g$PMB<=qD9N<m@ISzH+$jpF&<99C8y7K_ee zexl(h|K@c-z3Yv4Lb5!MVH}FZ))?s|<fHU&G5xtZI1i<~t62Nk5P1<vs39A^dA&(( z0WnoiWK~&ZwLaP;1>syK6V;MT^y#CgXq#HuBe#tbCIA}ThBTl~5`W;!6F&X6N%?I) zc>=P!ENurA+8{^d1!5QWug{mifpdREY7^@WH)!k(T#ZO?;>uV;vPcv73e!|2(1!Y1 z8JA*<XSK2ya)Knk4f$)T&wK1AtZ<-FVV<neB`eftRD}-N0V!ZY;Wb934unbX-ztej zsx%4UTyngQ_2wG&4xj^`{k22ATd)lZ!^h54fU^)_pME7YnnkqV53TG4-hkZ#ceYW# zQ^K7k>jzLHL$Agvi$e$O`dQH7@nZi56tXtQdX=Mk9A*HekX9Tz3DjrdB-eOl{)BF@ zH-N1}qN)V;^8mD7b}I_P#={Y(j}8KS1EkCn@G08yE2*h=-72M)er46r8m0Dfs09e5 zgc5dj<EL1Y{O-L<BtIQrCTB=J6(FO4b!|cV6+aH1=hIG<`?VRi*}KtZ-0Tzhf`t|; z(on#;E+JLiMmrNo?$5l+*!R@3jcO#TxZE2WI>)A~>43?RCehVQ#99zLOoxmoZH~`% z1zBqKx?8FTdLvmV+f!xn;z-sUpMFESuaN0QrFJaZGi5rp&|f{{A{`&^FCQJWZ%E2# z%R@u#p^t0^{!>=^C&F0d+OVl$MvH$UJsG8df#Fe!z}1MTm9Dp7lm`@+IA+Yy;fn|} zc>&iDJTU>$ZWj41cv@Dw2FUMK4eIak4RNLg`;+;J?-ggSzy+u?=<J5S;+`*)mD9;f zC-o4qv$@kq4<Lpd-r>t}W+>t7P(f(|(qaREg!ZIYPJ=BCC44}IrO%iW$>{FW_A1N2 zQ<lG{F=vHT7}B5EJS3s`N@KO_Nbt-A1d+=UN(*zj6$NB?#?ur>i8p?n&ebHz8~BK9 zsMwp}i>3SpR=4E@ppuWV`o_lS`9KnGIc^BhA6}PA<2*xSBc<@`8=x&(rb}3sJeP(Z zMeHh>@qvBA1rGV_yVOMin9i}0jF32EMS|X6TtR)C<DV#sKS_>ei37GlAxzlJD<n;0 z+&+ZXGfgu<K4>xs8u%y#TPT4Nc7(vxW$T5cGtmiR8_+NIFK~CvZLyoz0l3p(24Kw< zMY1kX%NmU(*WpRaRcf11Lwpr=<XR|HU$eN}Rt`wwT0#;cl#AJ%7v37YFmgRkMUS+C zjcliC)nG@tR}1z6-m@`Hb^ln|j#43GTl=(5AFlM-dubPTlIg}r>9u<bDAB1Wjdx3O zA-;DWFT^$QD-Hr77kxHvd7rA^n_0-j5Uk=MO89vw4~JMME_nwe;Zt_iha4=Qf4EK^ zWPHLqWE5~ILHIt@YA=MZl(E&gccEKDXW`agZG+w33Y8@wH=u-zPoe;HRiRJ&arW2L zjUTxijn@QF<dqNkgmbdgvZhdz9nem---ZvTLpzXx8Xn2G2=Ih5*9IRBj|_FFWt&Yr zQFuc-fvkklP?ZmrFMWl-1*YmF)O<pHH((?Nppo(cZos-q1QldQhue{eJ|y}!o{maJ z99a_m^aMZf`uX*;T3(k`w8FYE&+8uyQcn%d%(Dmk0@0(Msz1+U;5Wt#T)O@r*y?lf z{XX#jw0O3yMN*5@4pCcgAW~9FZ3FA%c*Lp<>~dMqpcI$kmAOw_hWF)uW%;+_@n(F0 zLpmZpyJ!jFWw_mIKcI!k3AUa-x@-^hU{oY~a&aVkp<g?sthzE$*8t-&Dl&d@+^Ydq zPP!gm?SR_2n;w7U;5ze%_skjvlQy$L)phzHGBTm}2>obBy8_i@!<Urc9OAnMHn#6z z>ofGVx~RivciMwjCiz#L@B``U!=~s>Wf>Hhd@8ThK5plp!{Jo05$#n>=$=TBU8xm> z7K*BoK*~W2T=xBevJ;~sfm!Kl<JV~+VRttxbC1$TRS@&Jg)$%MMEHs|!aO+OpKQSa zTY;dAie$_}OUZ*^e?3s{b}F??&DQs82buIGHL%idh^mrLkV%I6Q(DqEi5SzANVS5b z)D}TyEg@75u)}!}r)OS{-hP|bDsn#{`KEQ%)Jc^aMs{+9N#q;=YJPq_jxdC9lvJI@ zqPVQN!`B^U?){1OU6C@pXPSl0A?C7>qJ(2;fVZd`_F|+W;XYd3AJsq$+KXYsqkxH? zTgr6|8L@pp$HID;*~d#n%>JzCSEgPTEYE5$5N((h`^Iv=2;2v1qz;ZJnI6%UqGuWf zDi?K3?U4y+5K0=65WCj{Vg8b7CzRyAm&3Ffk3gk<mGI^73O6WEHz+F`@|&YRDMcr_ z@XpV9<E~m~>N}eE%Z47cl#*XWq#R%m+@(2cs3k{u*Cg;r%OSQcgj)^|INnmiWltnp zLa1$G=k3OXeR8#6yMJl6+w=waeR%84!a(FX+7dj&kvkGR$qsy-@UM4qW?9>+ti8jR z=R`}oxDcqg9zrz~Qd?eQL*i4ZO~>S-3S97Q%KX`ON#E&r1rgrRLA!W8?Ud!}6akxQ zZ)c}?x<1jxC|d1eq~n$`*5KK?25@I&U>;+eq%Hw1*YY}s4-V<@R*izn{BzNgv<Yx{ zY|8ryw9%A;0|LCI(-+_X2g-IMjv5=8JUOmLvL7yP+>>TvDxJ`tS8GPR6mFk2)EwI1 z%AxZ`?n@5pF;V)CIjEz{62p;%vj=}=@OcZ8Fbs#Ca-p$7nw8p5&4DLwd?V10AKP)O zRB)=c40VxU=(Po?iUbodua?%`GRp)rAWpP+jo9&{HEc`3oaL<@KBv=Oc_vpGEn@yr z&NobKlbz;Pj_SONAV2ne9gBc221YqLp_A;NA%MWFUWEYCx2+*VQ;NZheiBc<2Mo8* z%nG&Jfepzb(-!*FV2Ebc4yiF=YCt~!DQ}(X2GVd#c`tt&`;KbTz~jW4dQyZ6_QZ3M z?SRlc-<~5n{}nf$HpqLloPf4Bd6niAL!0S7y+RF5hja5!$um>xX}`J(VD{(HDd|Js zFtjN;5JMf$2>umc)DAWj_`{oV8YW~PYqF7e1Lt@4ZU}b|puMfHsDF)i8`D#hP-}e} zoB8qJa}KigBj0qG{TFM1-iU26@sDgPc)sq>F4&5ub~6YO;GHN`KkRMf^KoAN*_fHM zl_7S&YZd+MsV8OoJZ~g$8Dc?0KEcZN+v))q{ucEEpgYka7JKW)cZ`c{5*LZ14R+%Q z^JyL8>P|FsHHT%5nq^cnDrZXD6{M&;CxqBSYeOprS;O#;@@NQ^=*%|}AKN5Na?#18 zuY-Q?qUe8Mo=;ohOEZul@r&9rm3C3cN7`M4MWc0=_o=lF!2C?eW;J1kgaK1(QLTf* zfSVekzfVZ{3OD)uJJ!*Aac!5;OS6vNA{eCTxrThkp9Agis1*&2W=J~#v@YPB%1xTK zLh^8+D6}?2)=b=HV`R-FY4k{3fepQpqTio--pF?>WSB+oZm5<3y@NVeU%z3tL@GR9 z)R`LZny6~c(s}_t$Zc%T6)&xcqZ!gEI%{O(Ett=}d&U_)?RiS@X}k5JQe}Bj^@xZo z*X#axmcz)>i>5&-()|?frWri|1dM1+`lw0D@=;axM)wwF-gy8|C~Ajb?Jw|oN|RDM zDxJAH6yzZf#9W@CCbP4ZrL_1Df`&q=h_4`}D=NhJ+Zaoqt3u}w5u21^zaT9r-g_JR zrmw(dliw^q=H&yQO-3W<V(Hst5v*p-L|&7nd><7=q%gS!-z53m`>W5!kPYx_z*XuF zxGI#z5U=yQ57Jtz?n_1yDit{XTyCGM6e$Q;2Unsc0F<`R!IM9N8DaDe48*5jJD}&0 z`jY)XRX<<hb0`^6=50c`OVXY^mZfrR4pi|q`t+&{pPmcE>Zd;Y?y6pe3%KCR88%xT zeXoQo=@H9hR|P5v1%U|1l~%Lnx5?W9=o@#?=Wh2Zi(8e*!+7|$EAzV1^%<QifwFIX zbD&0)GWqd=O*;|t^33r@?o3$Z6FK6#f0+j)oQ}CjM54bHER7YTt|(LyYe2fMpxs}v zM?%zs`W#C5PYLoTRUs#$vX=>Pcbv4KtD>OYjs7ZXSJKJ)gHn5YdeWRvCiFeW76f!n zYP!L`vN+ZwXZGrD2zAiKQgPq}_U9iI#V0xwh@=hMc1fBmpgo5R1?{qk&}x(=Q_Z*u zeA;ukq?FoagtQsX@KY4w#=;Lv)P_q5ffp4&l2s`j9xKg5^aJb;d4FMGq_b9=+c#2S z7kxGXmPpxbO8A|VmiMJx>WfvDQVH?pMmX3XJ8AK!feL35DEmS2<#9A2ZNdLOt=+H1 zeacFhbqmMyf}Kfne=R#;Qfl$OwP0f*(FTUK9@tSqoJ*K@bX%ZNCg5zM8ZIV#xW2-m zE2xC8OPYfd`2aFq&eiVhq(`U~(cGk676A|ejTZd_+mE&`TlS}i)?5&^R<PGLEdg7S zPO<7U&;$tXXILcK;exi>t+)w;XZkeZ)lPb`-pyJwa3E;zW$(;fIlYzr^ie@&`3FAx z*Hu}CI&ZE3)PeNuKoS8@!#(u_?yU+hjF#3}&`tnC|1b~!SHLJnA56{<TKhrcWzlrD z*KH1R^zjt`5s1gM#)5`IId<66JKV8$9@_8=c)wdngNlOPoam!g`wS7C>C<nCOG}|0 z<$d21ub;9@FCfOP3fZ+{bbH6MKCdUzi0?2zRyYmktq$EO46)FEO6^%`mN|goFW4Hr zFUjZO<tU;czjpFndRAD-{YS$EZ0Qt9=1A6!fb&r>qpT{haPY*wqhg8d7BH7EB6%cV z1uE{wbe1iAk{mw*eb+3nv38Ho>V?9O=$;dXoU7}A3=wl)!vR^q+;3}#FAd20O(_ID z(DRotM6gf?SN;Rwh?FGYjGU>EtI$w$RxB*-5VYD+_2Gc*|LC$k0h2vJUIc0*cBHr~ zJEBGwu)w61UjE7Q$d{n%#&`)JYK!P+g-+OmXVJAcJu+!s-kG3xDEtiyNw5#OM4$W| zU(y;p0}i3+lRwgYSm^kej{9sXu;1-O#K(BtrhDFE5^O&TP_mB#;%GTx&a=DP<?<+t zTcQhiesM*3wfAXHx;MkJQ07m@wX;pWJ4Bhie1wGaA1ns`Z15@?>Z|7vEuZCLNJGbS zX@8=o1N1P<rG)P~$#MpsYbX0J47%*vq3A?X`t%j2%N66-j>Ey_*S;k|)UGt%6=fgb zFRY_xpZ1+s+jh!iL+hx==`cZnc2PsX5iEp;ecHEhc>A>PVHbO~5B*vzgQCnMyAkGT zUAq;}Ykd$L*wOl*Xp8ZOGvDT|*qLs!cchnNw8E_1W6%_CNnBnAB1WsI(6A3X1Pvbm z2%!0BoNb_Z$p5YyxyNZG4<mdVE}Scw2iveTo-{x+QH@mQNsnga?}|C660voKML)Sz zT=ga^AD*J6+2P3o405-jplRx6MoOH9TBZZGX$A~S8kvsE<7&bU!84O`g7J2Fiy-c3 zh?6`v#~<Ed)!S6U^KFI_DB3qomhC~p3WJ~ADULs%NcL}tNmN1~2bnf$P|dR8g@i>j zd#7mI%vb$1SS!}WlaY&Gjwes<b*dY|sr@UF_D*Ss$D`^VvZkYo<1o&t4pH{Lgo8&K z(H^5kue9W_Qw=pbgzI5eYV=qEx?*?2Q1~2A3ZG7Yc&k+SMEhs_>YwB==y^Rl-Lyde z$t1`<Y#KXj!RQjJLMY$f;CP={Yw75)rMyHwdCg?3*9p}5j%_ugf5PYoX7m<}u0=hD z<@o2#RQ-kMt<ZE`@}H__;_UUYngqB`&xE9IN6=1EbPcq8>i0=C$?iwkPpf`i^zf}X z&xXE&p{xMZX=br@l68ZpwdIH>-Xz`4V$`^VL|x4S*xPbccRbjet`5g<$Wil8sG$>? zv(&?hGf2s-vb&xR?LuV(T*$^dkV15#t(A3#T_#G#d^0QBx!km)8QOiE>>B`U6?-M6 zKYB$*)Sr-}f^j;Fx<~D=bSCPKtgB1;XR#MU@<jmX^1}%~8?VSTM+H#Xr5*k(d!>Wq zlP{y1==M|_F%oY5I<fQ?IMzSp`iYy=K4G&k=nz{!UoUdF56?V9PbX0b%A56~3`9O` z;-C-NFZd(tx~;``yN&xYLak0w(8!Nu!#kAP`yqR@KG@Y-L=SrlI{XD2m6hYNTGROq znVJ#WWS=uG%d4y$lNH(>4}G21V-v(fWm)N?(FRGV#jZB)%~0)!{rYf+5A`v}H2d8f zW{xe648Bs;nta+uw%61G=m70j>N8=Su7MvIsrwwEMnIcQPOp1|Hp(&UGoQBU*wzt| zj5MFtG%^yf+f9t*bw_6uMkXYD`I|Uxywi#2CQSahtsdOKwpLontsZvU;UTXbxEWP< zTji$#_ea`PM^HKJmFkH>U$Hj8Wf1try-mcIm(YqFawNkmk>H|{3@<}+P9Fsmx4pRR zyJ#oN6^c>+SNSbW)a#AAGJM*>w`ipIW)NE43B48%KLg{9mMB_+d47G2W3)Ed70_TG z9Km|<jfME<JMn~g86^(d_UJS`BgB*M(97h|emfwhU^fwa6+XH6X;jM3aQWO*GOBaL zs@LKK63(Cm3ki1d^y@UkV&Q9yC6>RMvR94uT#}(zIOy6C;;oq#{rnbtF|C>SJW$n+ zA5L_eUxF_Ln^OFfwq~MzYR*?~JSD%Y|87}-bn?sbM5_I2HiyXO2VjAtAJAqL(DQr3 zM`7^f`*CfeLz|N2(@LE_ZA!M<Y8%uKAJ2MnrJ-!?27&^<CMgi@C)`=M?Q;TT2U?nd zMtM1+vYtoG{zhZV;#T`sr{uqvn(rO2Mn>kaji$1j0aFcVgI~a81KU)0EO?$8ib3n? zCi;X(q4*=RitaBfccY>W)-N^OeRy1<1*1vP$;)_ryyNlpPJw(0Ln`A$d}{kd0St*Q z{k3xTTW`RhxAD(f{L|8{{o#x7XCMA(-P+)X{DYQj-8RT@cL=YQVF!fYl;NHbenp0} zAp8u3Mg7xs$xn|ua;<Q+bXy4d_LgoBb4&NhlG#x5zh$^Dgr~@G4upRv!~G#VUWNxi z_<C$<0=JaQ)}**|IQehqBs({RJ9i~Fd9JL&1y%Hs;fo>MLx%GpoT%gQB@jL;!vi54 zgRt2B6gA2IRjf_Yc;--(pVd*5AJtKl@5m|!<G9K2)i`c4JOsy0hOdF}<1#!H!gI07 zzi^Z6yVly|lw>dO<0gaL<ZZGFA5<|xhDShnlnkp7_R8?}5dMt}2OxYIHhFRhHA%!{ zsdd%lBzt)-H+eQU**Tx97!Or+$nZ@NJ|M#rA-r3LZ-MZ38J+~;4<RfxZjvpDpVB1v z@}2qA<jVQf<kPZ>$xy{Y87_x#M24q8I3&Y&K=@u6o(AE+V3U2h$qIu`I%RusM{nU) z$8)R0WGz9c<<~NNH-s;h;VKAUAj9`TxStGHL%0{V`pja|x2cgdeBJ39=I$PyM-3mE zM-6YEhZV6P7V7#yMjzm4gN)AM=n5Hqh@&sc=v<CI0a1~|aMan7dOE3_^&xJ)mYcs* zR`DoT@dp`woTC$DbP-2Y8T}ha3uSaMM=!_bpLmj*e?~U{T5=fh&)fKCEyK~5;c&u_ zUF-H^gzI{q>pCc-FLLy28D)Ph-9DDlS2)@#qpx!GZHS8A499D>q_(z%V~V-QrVt#@ zMhJN=j?j37WnFAsw%dI&x{{-J%IIp2mdfbc933a4?{IVk!lC_*;AoI=v?bvfk8q4n zj>qK@8jn1NqqnSUJ=fJ;Mq4=g<6Mqz;OGGv-NezIGWtG8w?b6d84g3eeODJaXkNw> z9B<DhINqE~a4eB^eav+|BBR?l8kW)R9IcYk&pA3(M!(?b?+}iApCCAPNjMIl4h|lV z5e$ci;mDVD?cuu4lhM5#?Jc8%qZu-~pQFcgjvnCXeu#?i77-lAg9W^>vaZwd$?@2# z6CCgB1joN*T|aPLugGWzN0-RxagNr>=n0O7Wb`CQ??yQO#BkW`Nxk+{;NbE29m6q^ z;qc13GVD~>)iT<hqxmxG;OGDu?a5I^Mzc7YhH!knkl=7iICAZ$*|$6%`yV1W_B=#z zeDDz0mCbcE$!K4W{!2!4IJ#6u`*ZY386CjUd1&zFw+u(FgafsV2?7Sjx<2k^I4T&9 z-^#jNT-O*Gy_ln38O`J9U>UuHqrZ~TfgJ6RaBO*;;K26l2Q4j!hWc}R+1y@S<1&M} z%=a>SHAlad(IFh&E~D3Q^gS6J%F(qNjz5rmZ_g<$x2?t2>Kp1kHnHTTxOCD{b++Fo z4lZF8Ob-e3Nmy53^I|!vVPcwi_k}oVK4K(QlZ~<u{=;YjOVz*D0!>_FHnAF;&}9=P zmL}%NCNNFhBAX~Vr3uQ`%_ioUO?>*I6j{Irlq|weN+SuFg0P6Yp5$s@v$Iy|GgLhi z@@z9K@+u{XC!fNS9MPKCmdL`tbJ=HP*}7!eYAU;!lEi;Kg-`s~<EI$cox~b@Q6OIB zK&2<+!@kdABOD>0JYxMT+ys@Y0xBNZy0Xnsrrf8wuGb)?EK%N?aUTOvVFEznA-R$( z2>zG3q6!IXX%g0a2`eUv6-!K5OOvox+O4qChj;v)VJ$Ub&5^K{c7;{dzB&b5V~*=^ z5@tS^ESZ$oAxbqsPNu#sQ|nSv=gQP)EUCdejEOZ<R7|NkWd&i1C^q*&ow<S@d!7V( z4R*u9i}gJm)-3u!so2YP7^iiTj`0U6PE&dW28I0%Nx6!gj~$pkI3=BibseTB?Hj^4 z7FJ~#<E+em3B`Gp$$JydSLS|$-mPDcOP>J}wwvEfu77d;o6L+~xFl{^#{3N=#kR;2 z1nn;|WsgiDIQwAAPH9J_`<rY><!c%5Si$i(WPGoTHyXvs|KSQW$t7JKu7^nUR(cq} zE?SykL28No@}(6F;;h9Cw@0SWm+608oU|XPk93get_;O=__ROb+mI9MowSD~9BSd9 zprX2)D!(U3PBdO5OkG!Ar?!rCNWOiGm+eipW*+XxX3(gD){(^Z8+KJ&GhgFeN6jCo z*#2u}v5`($(Hx^9E(yQ6R0GQ!?#WfPn{(DB;m_?N&p`Qvc8^25IZGSs^l2lr4G6G2 zbT`GHo<7pqC7#tKp4}y$lM>hNNsY_4QrZWYSi~{X5OKWdp&r7=DB?(gKh&BR{q&`H z(q37pB@b<ug%*-j>DI3D7{mVVVHQ@7bfOO}9mZv%Q*2&_=wJr<B_r7+$|$9HGO>&A zFQgu2jyc!_YZv%Uz}v600*-^-*Ke!GlH!edaWV)+mTG?uG$PtXYa51kjBbOLr{=X5 z<&XqkIz~gSn4RL){x&j{@G%x(<D(9dxiD@RdTWk-_A~xo>e04Zs>dYls8{>gt9{^O z`*ZV9ss<q+**!3=F!i8aWAP}~#WRwG?Ec3IB5@5sc8Yy-r|i0_&&sn}?Tbv$Ra?_> z1jYSVcft^)<7zZ)3P?M4tIy$k6|!TuV^Q4v4DG2dO4ZjRdvy;cq`kU>k9D<I*Y*9G zb_6Fgpx={2>h{HYcfBM_%XaA{j(qrw;;NZ9!H>V79rYi5t_+`hnkyqX#ke<*RN7JZ z^5b*pYSgD4#8?Z&+}mee>2>d%d6{1u=#uIGJV)Lc=daWHcQHV3*okkp`w9oTW_(TU zR6echei(I|4-hWi-Nu%5wZ8|eIMnJOn^AZj<`6IU=S9Q&pH}%zfCFIF_dA?OQyyzn z^?n`0o%TwFhxguV3|oR&sIKdNvD$-I^~+WFUvkm;$N8p|e`L}Jt{2vAZinA#5B5vS zAtfxv{r~jbF1F!+baz_)i_OXv3uB2y?9Do9uc8zEXYwu5<%Y9Y<9?h{SG%RJV?xZj zw6KzE5F})XdXg(w<PPp6gFCsBhWtu=_#$dwR$c>PBKpL;kTBGK&W>Xrh1R8=))w}Y zKr)a^!a?Nsq+myEySY&R>mTMmY!z2NZn>g*;VL$a@r%d1_+!HRP*XKq+b06jv6ocq z#iY-MbW&1E?~EHx$S=N%?P+hpqOIxDy4^=^X+hkg?p!G~EIxgfaHC<t+5qyISNXWB zdQ6_w`$YSd4EODjB>e+SLzC5$@lxw3DaT5E*iP1moqYfqbiCg(uI%n3O?Va)aj$xr zd7Z!MH1jOoV_A+df5uzs#cVt3mMh6=u^Gp=+-kVptF<V#PubCO3#v*1_L3i<x_S9S z`0`-bwbxvP8X-@~yIlEj_0zNc$=Vm!@vSo2!x8`WDAJ_OsSfjMKZEc5=yDKEKtapF z8=&fM+*I|QX<T(KSN)B(YOkz17pu;A6x{-Jc{`z`p~2PmU>|?D1L$;QNJE`K1>c-z z>uL%(!!D^&TUVNr+B%ANFZyB@O6jjk|69+Ka0rbSuePzR?gq@Cy?%qu@W(}-08Nnp z3Dsa%YtJT%Nv1j$$IS@&X~V5_)%<u@k64pTk67hUPjociDO&w|4f+i)(a^9LhFl~k zP6_{^lQs5BaFXHcl9c%gJVsMOSINe^unIG`^J!@3TkCrX=JbwOt9W955`6N-MS3G_ z;H;GVeLWaQFPNzH`LxYOHX_?=8|KtfjFkfv|Ni7$Jj3fi2~U(a*zA2`?IGkZN6q7L zagyGwt`d79{`G9U%S(jtx#s2g5bQ-((hp;<3Ho`M^SSlCo3zwTBQ+>xxm`#rEc$BP ztfy3b1q@;I2CF|uhV&Q~wCt(JBXLGK#w=T5|MBnf6s!44xRkc|P?wmA_1_pb@9~>O zU}>D(QTXJ={m`XMC1gJwXRG|pA|H}sJIs9!i0Gcn8S5OXiyJnsn@!kG0I?T)Rla#i zCVzv;k0Ak6hkq7Gtkx_xK}Pi6gw#Jl`p_Y161H@L9wM(k8ynuvbVpZz=6HTO|A+H) z<F3TdpPv)PX&(`(6_jnMk$W8clBzM=CXGs+qOk%8Xt`-lj5SmY!(ln%w>uO3DheN( z=8$2UvU{^s?J#`&V~*-R9=t_G%ZRF@aDpRvdEo?S(2c}Rj<h$mldLKXA**7H{6%-@ zLTnz6PvLD)(}y!QsoI9vo3L&QZ_Azur_6^Kf1d0LK$`{au@&Z{qfKA2Re<61E$7|8 zWa4G><EPkvCVw|Xc}w2rG~HKf7tmfKWhgK9zL9&64wuUQitfn2%ZYUZeoAXo-5Y{= zsy>8lK>}@g0Ch6(sRbE<Ca3UNjn)=xF0V6RW8{e52xDZrQ#df77dZ|mR3{_SGgW=M zW9=YMkxktCa9sMwk#=x!ytYA!=<f{W7G4HTUmD8*>g1&5mLvx!%~dS4tOB?p_D$@= zy3^9jUHHE52X*E9>aIF1--iq(lG>43c_I>yC%RD6_jM+cL`o!b`uTARGFH)={u<Oo zatWNx`QInH);70~r9aon+Pc!?nTGx7-#v5+JuYpZ0<=!{Kb0h8J-ub!`b39&qEngs zI<1KrjzHvghara;>7-}Uh+|&M=4^dzu6t~Dbswevp7TO2b}?r$v2~ap-Z)zOHh5*h zR&D#S?@;Gezu7S+GH}1DPjI031l8RZ%*K7-p6CdkQH&=vrd1v$MG`SaCzS@+PxQ1& zD{*QQq1<R2+C$PD#hRZ02#w6P7vADj=Ft){$^3PMhoN9>qiN5?;6~WzQEtt!H-dN+ z>VL_tJga`lQXonHRvSm}=;1WfRF6z^<*oHBWox|7#YDZHU9h!aV*uH;Lt^wo{8(Fe zZ$DbIhLVTXKXAa6O%MOj^4UH(HP}?W)QW@OZ28LuyWjtw?H>D(?Sit-$lx3-o`VCv z{QCr}Wso&QZ6BKbgZ^a$UK)@nfvG%Lj1sziV&z;ifQKy5vLoaOMl8rfGOh!wIUeUW zb^-8Oll^l=8<>!uugnglp6B26gp+j(9Md#Js@5%h*F`o_GRF|4;Rw|~v&{qa0&HJ$ z5C>R^$8%FHpG1#5WKy}Ad3@f?dgzq(oAis;n&FZ=amr<qv>xD6-T&nX!bw=<WL7AN zLxQ7$k9Twwx$MEK@XN!AQBKy}IunmdMlPK=E|D2daeHr*(KqaE&2WgFkQ!@}blEJ4 zkTyY?*mDx{VP~)=Ub80Y^Q0|d;%uv=z6wD7hbyE%^<&~Cm^X5k1C*4wWG3#8yYkop zg;@5tIOideKSgAv=0^TTmfA3{RH;3LRfOVFPAjJdiJq$;h!ZU{MLxa6mu^@7tt-h> zJKvm8r16AJmMS&;b|s!jr8I(nRrPUsR52QVz7BCy0RD(=8h*%GrKwqKG^V<z;0o*P zElEC%e8*#MF|R4<B&S&Um=vDY5n1yRMrV`183s3j;NB3nbh3n%QsZ^;RCg%BF<hbt z!g7At=pegmMN5V6$r2Wi+$&6v+?Qj+&v$n5zg;5wLVD~)oNuoET&%sZ)&kEURw2Dp z);_2!JOi=B{8|e<M26&wLRliomV_^o@Z^f8fToOQcZFw=c;Hcl$0P}Eeu{~_Lk?b@ zzj`n(9q2;lHINzmTH2|+3?m;)uNIzKwm~H=e)h(^)sp^OC(<zba_l2i;(XR5G!86* z)CDQ~^%J@Fb4XUJ;qqbHBEyBGMI|xSKR6NgY!_LaCd<-jbvhD2^U{rUv{h!t&SUxl z=?XjsfGC;>{hN|g86{y6)-yNL+_=mihgu!cm!NV(-g>Hh=A7OZ`HXi$u@<N2a*_XX zkR2@qx;4|8<L5h&U>1&`---6p{0`k&Q|jzAlP@=t;qRReI0$|E?T#SnSoU#}a{u*_ zY2W71x=v5ppLI6}f5R570{ZPXF$bp{kGgbI>abIM1cb`y@Sb2xG5HJI2gFqD?eaQo z4YF%gr=3l6;l_zR&>bh5#DLSgvw-rduxS{6Cz9bS0G`-abzS92e{mwgAw}T8OZP=* zY*u;FA(4MSuE?o5n2pbih2t_lFP!(sPvdIvm6Z1?a2(sjJ~#woZ%P0k>g3Xoz-l9Z z^FN6{8uG%mv%AtKlz+47|KgN<)JY`@KaW@2sibVBlv3S)Cd>YuDMrq7Vv5A?m_yI+ zEq2a>Ow{T_aI)ag)jsF*6G)H`Eu?2=H-iXci4Mx88e(6Wn`B=G@584<r<`y98^|~G zIz8m)(YhL9*kI2Qn~x_Fr|(GRr(D%-8MO9O9JLo`SV+i|_ovEpNnWdcXet4lR!FK4 z>Hv8uEpfO@vXt6+cx^P$)HvV$qU1*kIz$JA>xqIJdJl?^ZLH(j49DWKi`k_G<(|j6 zcw!c7t&n?J9_0qmEHznxTl^F83Z&xyOI3H0(a*-(jrbB^2pwAY&6m70^ybgAFkF<Q zlBzZeO5lUyOA_V>qh}eIkzP1p8&iRjnvV9JOUReg^s0E$9<x(?!3VH14=)~z%UTNB z1CgN~bgde@jPMUK-fL00UwjSJI69RqWmCat)x;B_X8UOSNw2$oW(jpdpPqwt2LQe4 z9tYWfK4YL9Kzx4qeL&kQ?!~)Ee#)O#IK?qzv0C;CJpc(lV%9h7H3o0=-(-KpirK_@ zRCO`YPDjAP*-U~dvro$}ki6HVTVA<XQd=WVyGVnTr(2rWCHMazTAzG0;Uc}{u*HDM zm))eiPNcW}?Al$!efzuJ5E2Z!Rr1aMgn^D$Gxp%c{LW?zcXdTg+%V38dC!=6$;BU# z?@}DGcVeOr{e%Ad{WzrO>G$LLUG^j0u{eLGQ`@+%_id+c$kjH>hMYPJH>Bmg#K$S` zB~Ez{5bg*%_*!j*6Eep!dZ+um$gEtkcz}7!1A~~n$H|z*s@h$@pO*ZdFz|rtl<S`l zW_Ho=mhUlgOquAJ_!6W0N5;MODe&SW1%qTyJF*VGCNeu(Hk)cLXb5*yDXOmM_d4_& zvhq*3w^g5U#Bv{B)<GtseAlMwzDCEofBSx9dMd;}=Z_AJTI!jA^jL6&f67XL@txe3 z<J0zw+h-a_Hkr>2lzCtw4E_KiDh@TIT_34#a1^^w2J_WWXQrxsKQkA1^3y~n|6Gyr z4UW-e?ZuJ7g9G-ckDj3n-K4rZgZ)RBZM@#{3T0*wd_5v=fZ7fxM%$RHap0Ny)E0J2 ze7dVbQ;1cD5-SuyMKapfjU!>r5hgtPv&Bf*PqV}~M0EC1HL^Jl>+w9{nS-6qLFq%Q z=<}C&Li}?!EBwD$!w6+<bytP||8RV}&G=8o_qsX%m*e~0gQtw|@7n(V9N%3JSjKn7 zoiM&*YIuA{%;NF=-Sn>Gi}b5}okKTSNWq$@`%tb#-B;^9^_jWhM9^QbRoigv7$S3R zE3n0Fs{2TAP~j*?u$QWztGd4mUIdZKBWmO>6ex&cbcJxPT6mi+*ux8?p}WWt+>MlH zK--mc@`Ila-^3!vx9_vUcv^e0Fl-kCwCF`S%Ztu6-J60V>E)BdU6B=>&Z9Lse-jVY zY7%cxfW3Y_lJI~(Vcgt@_3N{Y2U?Jn-$W0z&X+gksSmF#_=4VDwTf&Q>)1!gUmO$; zST?bzq&+(KuHZX|y@?E0mlsrh1KLGsTndhX#n{bpo6sdd9ln=RKZM{e?yp6DXbJ(h z^X_2Zb<_7-POCrfx13h9?}dRoUHL_FcX?z{s%OF7@nk(0TkCP(Yng!4)u&qFiA62& zd=<6S^Ig@?z~hkcycV^<lWv9Qacey<S6Sdm;Xm?FsKd{BeuCxdIZrOvfiBg3MerA= zdB--0-m%gBHY~Z#t=P;YJZX(rHVD`14KN=&%aiZf-iLULl-NAk%FDD5Lv@<>bJuxS zBQeS^Bk(L*>p{8Cg|iKml$2A$rYU5p(3|&R?xkIB-T!m^SM?v^ztI0F{=46b|1x3o z!zp^j{~P+NBYyE7lq(qP@n`ViN2lV$w%HbZ_}mo0hu2PLe0cSJyaLDEeY*4J-@NyH z`M#fh@A-=^?>%Q`8Sgar%*Mgsx1VU1^)#<ONgjUscc0xi8AO|W-#Nj2-}yi6he7B5 zKkkQF9jDz7a}ufhp=zULKaA?&{g7TsLz$F@lMKg!@RAs$mpTj~3w9H3GSWU62H;YE zL3_ateP$LMC$auqf7yF#SsRRD?=-b=+BQe<ywEzkb|SyY{ZaKdcy9PC(mu4`@fE&5 zLvPS&v*J{(tnR(j;ENnjZ3nuxKZ^HFeUz2MouO~-p<SIl#GK*IQxsl!KJ<Qi8m_f1 zVm4Z<XiZ%%b#$)~-8RGYm-3&yugjeMZ{OEt1m*j>30yzRoN$0Gtpp;jwt)Ma;B{8{ z0Je82%3fS3nmL8IonaG0q;JHU3K&867nSdMd9U_KN{EvBKXh~-l5(Rz-*sAf%fdml zkhPXuqeDY>TUUJ~L}jU4Ok05Lu(pEyG@w$OsJ+@o48r0|(R^T49@#U!noLCjlbyPK zfG@{Q^7<t<Tit1PR?7XDzK|TMJxF@Ljk|Nz9&PKo%7Yk_PxQ=L^%$f~244S{h8t+L zpg}}`&7j#CG!MOJr?kD8yeXfRK>V|_T()c$+^kGy@5m=3Vm<3?`ulAnUe3~5b+URD zw^E6QStpwJ2(w+T1Zq>gCBC16@~%|)9zPv?#S*^XUy_9H*GT{_vBKwJ`1&TZ3sc~` z?SBJbJ;8V5sqn3M^mOpOJec8Y%qRG+KmZ;aYy$959>L_9GtJdU%t)p`n}>kG4oU^5 zzXV6_R|fmV+u_96ChGtAGxtHW4=hcEjNgj^a9I*K7XhctL5H>cqPJ;s+_0cf;bhfH zblyeg>mJ8pYAtd~#h8BCjCfIWAhkeSi|C%Nr6||lQgnfx)i%86`e{Q965mc`N&X;` zY>4u*u0s6U6@-l7+nVXQHYJSj1oWbukg$gvw4$8Kf|}_UKr$ZJYIsNRLKF`r3%|%G zi}>m<eC|CyjRpMYUB&k)CD`TOzap#Dt}=9pvxvs$s%#n`C5&%})Y<EC)L1^bHdj`8 z>mTEEW(=}GqtyNxkBlXT{)@P{T7E>p*A`*!yKHBAWf&I;+Juq#cDhp>gJU`xp!*tR zBv{hXG+o|5@p*C+5c{0*r$5LHRj(jH$soOIfCx?Na$Ob7CJ4yYja9ykRlN9TSTDO+ z9|`J+l)!1et#V21by0mgj#MkYXAkCx^Zv|a74&<-pLqz+fL}496#jhuSJ_FjPh<#h z+1BvwJvv#`jkm{dm=`SvYYlPRqx?Z^F<y@xkZl~ggWGuNPE$YH*k5dVMch`v%k}n) z@fUra)#GXtXTyNQCa)X>s4yq=Z9CRhAis6Px1<~_#{NETF+0NV2Jrh2oT$?v?NDsf zHbqy?04z>%I6*7Vh;{&K;dt~t#JVZ+XoqMW#4YXor48){FM{2HuP=aRODMi3JVMr4 z&jagy!g97?PckGgUn$U?f$KuPmiXdNIAUvXtq4!g|Btpefs3+Q<Hu)MWLyW!l*|Q{ zyttI0W|Eo^po5}PE?H_(no)X-awaJ^Y?ySKDDKV5b}LQW%*xDM5Jf@F5;1pkU(V~e zP%3T=^E=P-E;B&)-rw*4_w$i?zvn&gbDpz5=bYy}k0vh}&JP9+f;NpY;N0RiiX?gr zA_8Nu>2LCS4gRsGs^<qYpQ>>_5MQaPcZy|$K{+E7`6_in0=!NxZHoOpHNL0>O-`9g zotkk$X8}KzIH94NeqJI6s1hg4Eanm?4B%#)dw*eBOY+^jZ8AB=9f3o`{=!FZqwwV% zK8?fO9R9&%b!WVNCbd+V=bvt(ZY@g=Exdwm++k1&h=7Ps-DxtZs-vKQMTL2h&-xms z@%hgrgM#|REQ5mjuL1k}n(jjeT8OOuv~G7}&=cj;I(5U-;%x~2VPz;%4sZ{95^9gn z?a0y&o`I?g!HI?aFaivnt6rdO@<fE1F5qEtbQ<bXY|0sd{ms1r{ol&i`6JEPJAWoe zi-$z%B%ki_57j$QrqdRxK6(rY7?17$2so9sx_Yv%WSqE1u2z)W{Q-8MJHiU)S5@CJ zQZxs=l=N{=3tL1rY_(^g)JI+4=JtvmxJ*7RIz^Cn!zWMqbf1c-8uzWbKP?vJ;?b+D zoeRPG@~Q6a;KHvmY2*<1CaA7$tY5H`-4zE+gMy~PABWPudUm?IQrH}<8Vc<}j*sH* zm>_*tQcTVkk30q1f~TPU9QV;yR~;>4V?$Po&L2l=-}&}3(jD?PrgD<+JaFc!pL&+q zlWh3|EUcRd?Xt-;bhv_QpX(!7yHn$86=_Xw&VtgOsOf)Ypz$Q|vN7{9^)BJ3vOkIb zPf$C^;Pd?pbi}`R5D_-$5O3sd{|r4SHAGd0_zGxUoWSQkxJJbjayZamgB+7$YW#r! zt<*T1HFdvE53fOit(%un-I<)2C0SSOk$jbs;xKmS_t^F316^p)ZD1vQUl;mpfr-Mq zw>6n^*TT3X=W{eTPj}vPPk3|JzD>cuAzsJ65_xWxLRW-j69o|P7uq?HZiBW)fwAjY z95y^3!T^}!S%kWsKgmzr`%_4cgZNMaFzTqD$4g0t91qL}r31ayP?Girkf1vTiV7M{ z+c0P}^RSU>!ijsY)df;oO_##Zj&ippa&FMqa)FxZZO!nq9I0u{k;@q6j%u|B=S%@D zu>)4uE2xn#yFJTw{Ttvouh%l&OR#{n22VWZULaqN?)~B;TktT_vrgTNkSVO2K`J5_ zpN9pXNkMO<IA|P|AqC!b`T((PIt>t(P#Pd|pD?Ityg41!G*s=2yy-sn^7<0I*o+QE zHEUfZrfNK32OC>trL9<eZz8IlB~{@Vt0{+W61^n_B?n4#V!;0FWpzClE9V6FZpoT^ zr|5#Z?*SJg=7NfWr*8}#myX|Q1KUoUVCku85T6ov)u*p&ELZ!+2zLeTQbfYUk6TFU zRt0WIyl19~?D+|re%KridUQf9=ars0l;{mQhqe##SioCEb6fs8oESTUik1hbdk@?j zEJu+81|(0MXOsmDM2U$Vv75A~>EAp?-!C4cY_*#W_IAgtGs$+Ngg@j{^_ivo@e{aK zL}P^}d%ZDy?@x*&k760uxX)AjeqgMcll*UySGBw^h^z)$4%BV7$aGJpB|SgO0xBZ6 zrPXj2i`OTzQKGm21Lf^<*kaixK;jCtQKY&$NHLLpsl(@`od08O=X<r<TU#ch^QA7_ zRslU+DHMv{4$|7}VRNT~zfCzG<V2RD!EX0xOMIn!G+6s`9`nu3fStoA*{H^hQ=Weo z)}u5})O()g7w?*&{hQj)8{a-(*LfrA=b?%c@AgL2+<<kT5iVSl+zf7+8l|~6rAxUd zJ-?9;%8xcvQ#M!z<@dCJKh)%O_fc;{&Hu`J3oz8!=4onk%W)$l(fU>?_+pmxpW~=X zGnk+htYrG<Y-ZV~(>FvReOrFmRa?S(d)Fc6Z5sVF@XiOO5z|2MQWGel1lBu_R3vFT zwkc`jM>a~!PmO}jEBVaZODF9eU|r`tXECb!@K(~e$6yQTtm6o8Aw`qkQsCAus?);u zZqm47@OBgX7_8Q{^M;G$-<a*z-D%k4omEY`GmU7Y7q$6|RJX?Jdd?t(ir;WqjTu$- zo=e5M55QO^n`hY#IkI(mg0_S^OrWK4pUj~1UG=_HlKXeQDuo^ee5(Hd+3(sWxi5Q$ zhovMOv*z6lm)Oa)9h}1_!JtXX$Im^F%^1g|QfuB#C_*?)o&jM=2^-xTlB`322dimv zfN^eUDt?#*9$y@$B+pQbd$7g2B3UzTH$Kd{**rHHjN6TB<UHa7_p@aZgK@j0gkNV( zfr?3SPm(pd*M5^OQIKEtai+jMvUD0n?L3NV_afy~=G+j8X5pY*oDcX^Yj9K2-NaYA z`=T<Q?A)ig&%{@D+KT(%(;G^Wu3Y>nbD%uur+YqBB(7s%mu|2H_pez#maXQeGk(77 zULeN6dU_Ntt`92*wedtGKST3jE~*x**5&dak+C;tKrPEGCuD|AZz>*ooJtPT0nK4y z8&6f7_&8PZ=tuM_*6qpN1m^1s0{vZM?MA>o6It1<pqIA_@Cgr&Z5oOBj{x1U7`S}O zG1BowVEpbO`Ca!6Nw{DiKg3;yqo%h7kB{zFG0+!>$o|yd=G<hVMclXtXwrh6T3Gso zs<Lw^m_c=H0rt~i|6Kpvv0hJCjscbFa7Yo<=gfbl=l3lelHjwCL2(4Q$?sa>y{VuM zK6c&zpZQo{9mEIpG}VbRNcnrjPzAR7>jiMr%IK2?M}bGxE6DzbAXNdK^wLnDe@Mb< zYu+z7Xq~mBL8EmYu%E&G)N6F;@SFU=yVv`hWR=^k7?Vkr{KHV6nlui|=zA?Kzw1Uh zJlVU@@HGzi%Fe?W6)-0~it@ulFn@T<(meNySH{vflICtMu4ig{K*Mx?Q;1*t6GmQ9 z<E#Pm+8%g4C|1Mgo<5@Zw458?De!JU{f9?c|6vTZ)VCR*9|D=USJWFzI%#R{dg3M6 zYjR{##D8)`*BGB)8?OY)1#l??gaw0q5RvA-M5Ah!VN|UzB}}Y!IZ=s&!}P<gpjx+g zNW;P7Z|xHTGfS%ca5hf;cJM@>PJL$G)E%F~r~iac=d)FaiU(mec?630WIA<mXI3M# z$iYmPiZWy1_(!Qk#ymnLGUZ{ygBg#17~EPS-T6e<yV|?3Mt_e21ErHJV&fway_8x| zBSW8(JwGjB7j8<}ACXgTn)`QK{$y{IxB1`oYuC{K+@GGi5!9dLTx_KS!&`&znj7}p zHb$kp{rWp)|4F~vKEEBMwa<lvWu6lzNT!pFE%Yoz%i<%vTp8WF-&+R7)@&RV+vLI2 z0`p|+9pb)MRLO90WgM05g|QSV;vsE7<@3o!)qFl#8i7Vl2ZrHZOX)z1iMGr}RJR8) zqnPb#1e_m{eT4^P+b=bdNmTwiQT@6(el5FBsLefEPU}Yb_->nfW@NFx|A8rJPEBT? zU5WE<ThGm|@|;`cqH(1;XwgV=SSi_i;RF}?kll45ctM#=<OtI!++Yf|x+_EV;Pm`S zmQ~=cAZg7eDeC_WW*gQn2cf?+-}yBfJF?~-1N$t6L*&xFPzoySES49Xzujn02MJ`} zs_>j76JI2zLR)9kTi`fzD(*F?zPc|AkuuftK|Z{h9T||;Bg{c`Kxia~=IVBPTEk;D zdZlVGm?)uDAWu#t`l^vkw10qdpOXZ4@A$hr)bB2ca^xV{QdrMKABqKo@FrAV7l_nH z9F^U-G?a|pZZca}3^C<xvF5#R#$667ibTCAimz)$k%%R>#adWic!UaM;v-Zbaa<sH zej*Db&P>kBKTo4P#2NGL%ErYueI-7Vm8m>ZZM3Tcr6eYe*RxNVbdl;+4xbb3vAW8^ zxXuamKLOvN>+az|uONUPnzXuYJYNo0!wqyc$Y3lk;Yg!PW4l0pS&~lWR)*HxwX#kL z<+Ao`E%K2xDswu;kn#zg21HDNi&^nSE@%R{9-qg?j7a&}>bg`#MFF96VZ}6Wqz~Pk zT9kcN#`6+z-+<Kv1sDx82Of?sf`s8*4BVz-CE)oO*;T?LvMGZDj>s-u)v6N={OPA; z@S~oRIk)-?!&<>c_XlWhP86b~w}x%ck#AU$V51rWpf~ERsFhCN(UI9lVF5>GR@akZ z@CuI2?hWU?wh{Q1@#yT9aGKOU8$n~;7%3UW7b!oh_t#~H!3T}Iv@mW&Sgve~oo!Tg zFs^u(<4edTm|>!QN42u<pGb`sHu?&8!=B;CBYgq3wvL#LX1Ow%5`>C8nBF`l)ykPV zodo&>wdOsC1JX2$Y9UQF7V+)?O2udp&6;-?4+4Qkvl??~fPpR7R3EjLOyg+y_;A^< z<;36@S+c4<eV}B(Gi7{HVf=`&MA;Sqp^O$WkeVqs<U^(!4f(ABYD0zr2dQZ9z8Hc7 zPjZ-bZUT-ZB+JmSq{X@@DR`!Z)%tFae23G5@)x>ufbN$OX>~0H4-!S-{GpE)w~tL7 zrIeP}Dcfk@YKk2Gl%qU?pe4D-AmnEb!AcqiAv3CtNi+jO9sQaeu65U;;zwoR;>wzL z05X&nDQ+Lc*3`5@nx>DeIjMT#khhF_Ljnr4uO2tu5VsIbbv-OYlpk+!&(-pIJXP<( z1!@o8R7&>Jum^t!wN{#{_TcW9wH^#sDN0kR2d7dGPL(}4RXyjnVITe)O;?KR>C}hG zj^%RvSU6IsO)d*)z3<`7LEq)hfIbW%o>cC`sZoxE+!@e+VIKsJ3Oh>y%g}fCqCjv# z%j4k;I!}FPh|gHXqX(!}EIam*>&EH=JL*bNKn+fXpxIo_$h=LqdSwJ;G@23=18_(p z#_=Fq5}qRoud9aI&KgR!?SdtzSoJ8{d8l8leScWg!|!SEQfxOUEgb9)N|8&6!H^k| z;x|C(M>XFOsoKsJ^T$$Q0+21@jBF9dsEcV)?n>69(6Kog7KM}Zv?2OCy)0ih)CJCK zcv3$E82AH=Y)YhRwN|uX28whBCWUAWV3g(e=}}Y$&?mcOU~{_uPG{f`vp%3J!?-JA zW`rzHwh5<JQY3V<4!l)hAD~t>sv2JrdlVYWhYi)1avSl{>ph}-3g09}d+{|!l`E=a zus>cZ#wLRNd%vN2^L%8C4(;|zp}6G}G^|$`d)JSWj5iN#*G*^JbtJ0D^@-xsY}ScG za-voqCM$PdMIG!RT8yGk7NP}%fblm(1}bg%+oMMFctXg;VV&t+%NF`@GHnMRoU7JW z3$q7d5h(ks{RZi>SbQNUH57;X0*5QBRSso_B1wA;idJRiZb?ablgX5E4np9|Sv4aM zlFdz-$o&c1MihiJ<<GH!xOV}CXkf&B2CCHR>PdI{#4q>C3YnN;=r2948e75~FmU)D z894NAEI<bo)52W-oJJy3w!zY5Tw9FM?CS;Tf0CvUeALjjCFee|#!t2;Y*NZ6MU;`y ztYx1+)?b&s*1>fCbA9>c#m^n-Qr5kkq{+G-wp=6I54ynfETr~E+BLe~p{|GWk@s8e zJVCsFABh*F52OwGYq*0;MjkRSVsjrO5ismw`YhWI-XJE#M2q{z$SEcPr<f$`yp5^W z6(>BA;0_t-{Y$4;zrQ|(q_~g8SBiHA7^WNh!yNDEy@sg<Jm`rFG6Wq?81(P@#Fwip zhzv<sIjq{r(ei6`^?=@+<PVUt9q)R-3=o#VHiI&mPe=mw5ASAOU{|^QZky*m3l1Vu z&b2qSdHRe|<`4Ag{>(ftS;S&os>nAp2da9UV~SP%5Kmh%Ck17R!X_g?w?J|}02<xw zhcFbh90xq0em4@=dw7X?_SyO7pwsoaD9OOxX;-Dqef*;BE4vkr)w-^VMtK2BP)=h_ zZTZvESD}#WJ}(kvXuMzCCr8C~?c_`<>3}yQ5#LWTadse`0mjH7Nmx-vJ+-b|E3n7M zBkWvcLRFV*0Qx?-;w=^n<XARR<<sWrRvW8$XBk^WxjKc+u!zdOxbtEGtBdRU$<%%h z$IlL3{M_mSf2}rWad><Ym&IwBPspVOe2HU5C-&pKh{=+Y_y=cVdtY^j+}=9gUy|6> zRFar3=hwkFT)Ul^?R#Wj7q<_uEps+Ka|VA+h20>@WEivaGt5BWnz|pYzhJu?%}r{j z)zuoB6x=`+uKudMG}2+Vx&JCO%i8z!mbGu|i+yQc5o$8kxR0yG4^iVs{e65mL;}Ta zfD_$4V+xy%fQJz=A+q-(fjxb60zvw$27OCe^EGrW;r<P*{PN1}w}8@ToHIS9k;O*7 zi8K6kWzwddr8`>a%AG{tIU%Oe74L7telPn!k-UN<Do<i0pW8)vjQcl$6T|dFGu(8+ z`;a$m38jvP-T~@3D{P)DOL~50h0T*up<TnRFZ+0fs2mQvPIC3BE10BtWCId^K)ok; z9Ys*DVC4dKZj^uY)%~Ua-{@a2hVkt0^{?{=sQI6jV{?{9E1?fSngxk-r^XuXnOav2 z48Vc<ZMm~rnNR`U(IURcS$*@&#NZ1bo~hC9GqSgbC96wz$3Z!$=D4#W$)8tHeBr^y zwd$;_OGnH2qJ%Ru%W7z{r+@`8Nwj|w5CsdPf6t%e)*upw@%!0m(K{KJ2Xbgyiuw(q zCCTvF;nL)2Y66-*o+lTlIyAqk?ss7M#VN^De$Zdyiy&Ql8_$E$uJV~J++|nxI%h?N zT2sp0d&F&njh&}JmTxO#`GPrIV}F2I`v_#J4DM{me4%|qfn^6Lc5+&uj?+4<4Io)- z-Z%0oHNMhSnbQ`JPe{`Swx8ykOk7A5CholtbG{a-<;!e}Rs&3?{L%g8YGgx7eva9i zws-yYw(HNdTz{ipext~={FIm&+xP>{qy6z9y3-EZ&XYe!;`Aq1XH{}w1Wy-3z^l^v z6t~}2fAYadk<$z9*IDxxn?MQKF<6u&k*-af0W>Io5VM3uh-EB7kU_w&QCtGUfSd7` zrLWl=_nO!qu$acArliWwg2w4U%L<ptVmp;}-ot*f3zQ^Zq3wtQIQbWe{OSBuwug-V z;dr0_q7>Sky~C_|IEW5X?X}4|g+^>|3}x+zs2mtGOV*|CpU5)|-fywritlgs42*Fe zndK}pTMLVJT$Q|hTy!ze9`RI&Uupjb#&72t7USOPJT}X@L4Hxb<GO4=jb|SP2}&+a zA^l)~PO|;n9@u^$!Quja{F;>kM#v)KE0rT@K7S2=Q2gGCs!Ph+G@qaK1Xy11JeUbH z{Ef;DW_x$%v1<2&nerQ((P*yh&bHEI8<49sneEL=li>yi?MSF{hi6)EQFJ%Rr}{hZ zv55r+`XNByNAxpl($66JAwa)tE%dvJOr+nnF8X0W{|3<?U6cN3q8|qI`_@9guP`C~ zz5(=u^?e8C$h6|m*}(3*Oro9m>Y87+!JH^ar7|(h?6@7?VUH7!UXrt!2p*f%eN>C+ zb_Ip=9PhS*0D169UMsBQ-xjLAFQ};p+xz3$)H5~P^PpWyqj{ZV+MWJ_i{hbEvplnU z!`b#G>?t`-&B@t;_YZ_u%93!#7G^)?yxP!yl=eqmJ$*r!2R3P!2OjDbu&=WJczgZw zfVQauv;#L)vT;*oa!j09(#L1KJkY6!FZl966BO`djNDnUi*g#0y$^SW<oCw?luodp z;&>i}&^E@s6#Foc@IUTP`zH_frS-Gbhi1M;8|5w8*5wLe(E!}-&7#rs5JdiTxY?vd zHsSV#HJ!{Lp<bqg_5Ak__8Im3pVRdGXLtA2qUZm9Utf@(e-F%+(wpo(p}e`G7nhI{ z?$_;(MvK>FTFP<FaOcNT$N?Mlw03m3B%j`Yt7>ng$p67U9~;w<t611uXdvp}i^q}P z3LIbi#7XgVcM_Ma@;xn;q0}C`mr8A4Kc8w<qF9_A;o}<s|K5kM$;;(g{^s00^v$Av zs=gR@PSlWb&dM^$<pb)qX!2U+z)Z8<YjaP{DjkSEvvHhqKWK-F5lvEI$zsReD0^Em z+6Be^{xC2z3j6)ur3FFbU5@jV*<2|H&M@Z@U;PTXs+v16%pRV5kIB&#%(oZ5(>m3p zOc{jw84zG|?^Y%Z(*5Nb`}?pQPd(=`GYCOu>x!f>xP4bPHz_Q|Q!f?ug`6*1O!oWh zls{~qr!3Gqn?dPV*|=S40u=XPIVvStOf`ih;De4~tK)pCj{KX)X{@lhvn*h86pMRi z0u^_!zM7v+7@?%VtL_iLE&0NKm^4Ety-EN2E}N=I{IM(yQQ4a`Lh!s-w*&%e>d~Zn zj<THlm^hdS+Up}?qS2JRn9+}v*`omz+{t_Vp$0=DS~I`hXjzR_BMV{Q5KWx)#%`9V z-fxlN-tDXn`QihV$@}~Gbc3H09sqUGiG08IhI?vE$-q`7e*uU+DW1Vmsje-K&@pfy z_YjVp^M;yDnmv8@6)8}~h6OElqryN~sZ7qaIPOQy*j1)5uqS;lspZdXX&YbWUM~rj zq%?~(k^a48>m^AU4ydBSj7q-3(L;(ckOq$*Xp0qZHK5x}@tOQgJd+A%#yGD}j%nmw zDb~i<Vjois^uruur~JswEWYa3?$;obTBf4V^3th*d|5DG;`@G_j}}@!!kVFWq^#DF zoC!-%CSds;MVS!o;`x3&+PgRMGgap&X;gd>-Kk~ENwd%5uAk@GoFvak6P)PnsAn>D zXnv*Kd90q*HNBpCU+pGnJ^Ax!7f`>lM&~{_@f_(Ea3uHGAKHdK>|NG=Bu8`4d_I)J zP0LPq(T08^AIjk_|H7`cKnGx$hb;O@C{0C|WQcQMlpGLfOBCss42;%Xj}<P24z;PL zPdlpB4*n9m)^_msDyI>rrIBTU3X4CNgkzx>{dqe#nVr69YaV0{+$CIu(%gC6y|3Ml zgkKy-T?ZUt2|FG8@`|jk_<H)e+bWRV-G{_me7p`}>MkV}%>&j{+En1<=90u{lc*QO zK13I@yVSPLJvcm={n6nj%`x37^~%cf9)T3`Afj8B+D=g(szI?^IGySp35aKwp|V(T za)7dNoiLn_7lN}N@k}>8Rjxl-II_CTG|vw0rk}H`8|hRmmv2cmITN9tN&7%t>`tVj zIRguje&^bb9&+mG=kb*4(0Y2Si41Nvxmv4vgVTuQG~kVBz-i<Lq;Y}^DN;-0({OGz zD0_NuqW-A1n+Du&yh)yBF}1gwPr^)^Uq!XujN`LvvRytsCc9>%iBucSj(8*Y_Lyuj ztP{yP20QZGLWB9<9ZIA#wYD-hh)|}e$AmI<LdR<m`Z<(ZNkKqCc|wC)NhP<ENUfFF z?$FaCQWL|Btt9piT{ng9<6t37ZlX64JfJ9xGoJR_V=~0hSmN7o?Z(S|o3zB%%MdZ) z9{oh1mV2`2>A*kuo@_5Z>|fE7rpRaFiArScR2tQ*l}dFOQORa1u}mepb}F4~r&7vP zIy04HaYQAqb}B2wYNaxYshsFRR6bxTiGftCuJIwNGDNki43QBMtPGJ%)Z)*Hc}y=o z4vx5Gt*ZLvo*5ST+O^;B&nvP&2@+P4r)x}ySX`j}=Jop%D>s?#q3-0~@*xBsx;jsW z!EaxP?BRQ*(5<eNxrb*abqI@DVos96l-2cyWO_-69KU1@FA2kbX?1N4!5t2CCngMT zu~m7I50yUaA2f93ExFDouHqdzBq;A*SM_&bSQKw|htsj^>iz~rgz-t)a6Q0&DjxzT z!(ql-*Nyr<IBvp4Z5WwE_s+o#EWV~P&>ZN_zlQc|;T~P(y+J)FuXB|e_l)ej%57De z{xo}xI4&ERGQKL1-$IDnyDG@uYo>S`4rZEEVS3ziH9lYM6rA?mH`KJB0eZ1F0{j;Y z?ipW>6Du7wHB6>Gq@`w%9Wo(K{5fSzHS;T<hgAoa9|{biz|xT`f&5p;>LY*>TgBy* zA$IozXJ>#Z-fkHW&x9G5#OiraoHv5f!)f46AkiSGeqWE(Cne?mYg|4!HI0Y(FI)?Z z|46J}r^@B4TKWTkANLjh9)*<-@co3K{I!YIMJMHQCB)Ak+&7>+S0_<<-Wm#>LiPpO z{?EuJzr<|K`vA1*r}Y%?+z!jS-r|khwGE@v<esRr@}j1vG`MR3-SJEpJe~ku?@~!g z*On1KbU>O${TcDG41>7J)GGD-UoMmsvZ*Oie?V)lGm3{@dF7%C+ryfwK<T7LzKQTR zH&o>TSg8Eb$GG^n=FL~t2H<6G`+yAq5;I^A;GM2PdjQtFm=Mwyz?wO6T~8xXyvS)J zavB5mG!jKCP9rf`DWM0aVYmdk-E7cESb9xQBTn4HX~c0F4fHhP#L9O3QcWY0)7ajH zU+8JP#A#qlE-<7KExx;l_jx!CSW|Ku(P9Lr5v`^X&1p>PQe$^WF;N=W>YrEBkSoPu zm_k?#ClJpG$Tydx>5WP~k*PQCLg6~`%}gAd_@ZmjV7mr1szL3bp~VG+^=iirt@(BQ zK>XMqBZ+U?Lko|i7LGfYiX3<4tG<S&do>y&Ck9YFA;Xki*VN0FKg!xvEPi$4=W~F+ z`qcZN(4Lg`+I`Tu0H-39YwVW*bq>%rc^GsIp7a@mzJj2Y0YPwIqCnXipiCqn+U0;~ z37}szfVYichv@-L`Qq79pQbw^9xpX0j*Kj&>39Z|KYi!e1p!~T_W8xYZn^-X{ObyB zg8m!c+|f-JaFqBfnl8wXT}U0~U+QWQ@6X)l^Lt-Y?O$}Ey}*WDG##2;`be4RSLwhl z)u+4oG<6d3O^J_ASoxD7{K>6;Ex!)Dynm^c<nE0-4Qd@jP)og(v_Ec!C4U*jnRKTv z&?u=_{Hntl)EGIx2{lH%i}_kC2JWLk<zp&iyk%+hUh|L~8SwxEUgokI$^w4b7hX<M zH}!TwUvMM6!$9&;JsVcnqrBVP3E13thc3M-BP#vs4j^(gkU2`#?S^1@H)dbIl)E;O z3KOUNNcmd%gulUiydNck(@ieh?>bTqJ=F=@gtFZisF3rVjI4a4yZ%J6XU#^CM0`fw z8_6G#J!n*Nx(h<K8nuabZd9)?%cLIi6H{mhzCOg=1Htkk_&zJ255p;dC-%y&4$o$S zd}wvSQ9lTXRu_06mJ*`X)s95g(&R+y{&9wR&UrP|VfQ3wE-^b^gc<U7kQBolO~;HQ z_5ZP&eIT57D`k~<ru4PB_p$%0P2e0MvpuR`*xZvwOHmgFUibcV_h~S@aZ)ZP3)jJ# zvWqNDooa#es@d@#CTyQ0r^~`n8g3k|aQ4jIW+@Caq2u2o@t6#w<fmfty=C&NXdh7J z;7~i21DiE(2arMs8xLYHon`}0zHUthK#-Y_r2GJhhgkEP$Wrje7szo>%#}S`DJQ)e z7&LQQvtlaXSv^G-;ExFtL#J*7hPnooM~22thW$ZEd%GcR;HxbsZ?WdR!Hs>*2|olW zvsmsBo?dVpZ~SQGThxyi(+Jy?c68rw65$N*N4y7ny#ozp-^=Ff^~%wtW_#fcaM^4s zO|q@P;U?^RJD!iPTz9=S-U!HR2i`f|rAaa$q627HrnpaE*|q*$Tk%vYD9zzHs?LQc z1X|1iAd;LZpAxX`$n`sggwc74tH{v>_S$KUp`TyQ>KR`tUcnTQJa}13VZEYt2WXEO z&avzp)`hi^Rd$<fKkFLFCn(`2otPDHSo)GB-F-}~K7eN`YC5=VT71ZlnGtRDbd&@? ztyg3^bL4xR=wy@|?eS#lNg(S9QBt+@4jJ!Jh-apFJ29Sh?q@hZV_dkZBij$le?+IP z3?O@!Huuq0jZJ)@AV%B@ukG)`r}5|H#|O3cMiHiH-^bPVeKYz#B>tQ%;z(=Wi<ovC zrF}Q?=ANMINo(nVd$|ImeG~0!k8Txd>M>Aq8y=ks+oK=A=;T84L8Y{pljc4pCV&y7 zO|GJp{3rUSSksQIzuGo6-oIY7Oj=)6FF!pTd@neU_V2VlZRbhJVLfZ!Ao=Of<XGiO ziu)>@-Ft>-#``NbgpfTA=dmz*cTaNP{A^SQu%=b8DnK=;0@%h^0=*s7<98mj)o*qw zq12jwHFp{)A%GM3q4m3-NVqT~#C<__HfO0BR0F^mLRNFo2QXP*`-d5P6vB;2?DY0- zvd`v^>hG#_|7lIzz5ZI;^-9b2*X!BFU(avW#CfcLr>!<mqbi&AvmMr#Z^l*)pY+Wi z0H6G2O)FsrugXt0=Z7_S9_iny%;ssfO@6Y$`Z7!u#NBmS=tEsyPgwIR<P)fScr&$g z<ov1qU4HjfYuXiAh_VDRzY(0@^`y13^N6i}*d=A3cJBr0y1wR7qvKKfoyotu;a4?G z6=2T;)H2Wvfr(T!a``zupPrSC)DFe?J5V3Nu|Vk;u-*iFmV%Xo(I47jAx_hn1(v~Z zKfc8&U;}&)7qB;;6wjkkw$g}#)KpU{SO!7+L~^iNdk}i~xEwixp(%xCFf8Wm@uSbY zxVAog3EMJ*<v3sv>)*mAogAy3=1i4Hw=VRM>U5xCh2NG%VRzX)GjhbiHd2`Q2~4Pv z39qrY%hp{AtsC4B&1@wLO!5UvQRUHS|G+aXN=)4^=Y}F4m!?IFccjeZ{XoUBVzC@7 zm1#Vl$O+>ez~kur6qH!SD49(K&RO6FW&X6NboX)b*KM3={Z>>Kk44kKeJ$F@@uEfJ z-eCAhKE}du_vjjlz*P)d55@Gon}YN!N+ZfrQcX%8&mU}09>PSn>c(SFYlFXNqkoUW z*0tHAM1N>+LS>YOLJh*12V3rHTtfHydYiNZ4`|)2$yAz*6GTaJtVvbaP!%gw1qj~a zP|vzcle6i^4nN}cW<GyzW>{4z=PePBCrcCAQ+Q%j5golevytvImL{sEW_~YLLt^;~ zV*5>MNF0Yma)>)QM=X?0LVVnsPuokyrS`yuaiJ8rF`!9NsGh;b5j4kly;@RFdA$uS z7SRrKJ6VFk`Wx=caBg`+yoHt-&dCCXnquo{@Xy{+3b3jl8L{*znQp0RXLVgum>jFy z;bOx33yZKbtLT9D^ET>vtTy?}{uOzv&rdv`5*ye0{k0s3{@O-85Jlq;c>cNXXFmUI zW3*2#Uqnai^z60Mn^6lrH`6l%J+MC^*Tl*~o_AIMKx|w+%BJ3iABz`?Rnu0*_O$f; zCnG1)`RZwLR}|1Z1*6EBHMMebu{Bf;_}VTu+Q=$)tfec#Lbt9C1W&9aCdal8^v`0n zCvy?6iqqxs*=J#tP4_%0Ws_Z?iWxN|e{xKeCov1n%D~m4Kci4WMm8^q9ulSXk@4(Z zZTUH9^Y{qvZjFt}g4)i8>*6_}q6YOX2X~-fT!8i^@N`7Aj~WS#uD3arNP82s^8wnS zY^3DcRr^x3<nGS6O4dKT9|ij()NSMTws{`S!I9MchbJQov`Ij-I!fFPyn6mA-=)e5 z87}`nZ1HyH@vs(u%X3<RIx55;14d8OQm8M7zbJ90h5RaET$D8r7gqU`DrgCp@34qx zng?!Y!6h1PJL@|uzfAXJvZGL0XnmR+cXjN;v>lz7t=f)GP9z%@J0;dc1#joL68!2e z$M)NJtHP*1p=;DH^sX$Rv@@d85`MS39)taq=L6!Wc~WJzGon3DTU_TH4N0?r8diRG zG_<T&Z!v5V+3(>EqTidS8$>ygBBE>Hu8?CTHsLtQUI0PXyIJGi!maks71U}!Z)s4O zxpf8WEeI>Wp3~At_fbHfv@}vFZK3(^3YHJ&hLGB)+aA*lovc`0@8VYBz{nI&cB7Oo z*>W-SPACZ%N1&8xtX;USJ$p+BX<n|krr=G{#8`0Pvk+sZ=RX?>Zr)Q}ekpZEm`o&h zC$qD2>@%%f(2<I5{I6gVjw}i^4FT(S7O_l@Ke#Up-Tuvn=<X*e;r!Iqv`A~(4pvH& za$t4lNsZ+u)hQgCR9gOwSn+6MXs4;Mpq&kUI@(s6YU$Pn7Hz3?;A_@yc=xjK9l_Hg zwlN=O<6fiuv^13*lDbnPWz%W~{#T>aT*5hOb_fpV?&K&>Q?%!vnC+g2q5d+sWb$no zD<P;K2^S>6w+rJ!O#1y`04Vbhw+1L9{JtjO=KJ4S(T#0Q$_Pb}&<D+EN4<zy#eK6D zjVObG*WTMl@bQp#a(JztjNG{m?bH+nowFs->DW_^=$x%{;K|qya^$FG*d9eqvn%!v z=#1?f!^L4x3OQmnUcGJ_LTo_j+^`tYG74^9hs7vYWk3Pxatvrhx*VAyLAo57YVkE| z5>R|^t=5V!_dgWhX7z%#q$9@N%m*$Zd}0oH+1r3m%ma$=CFa`{q88sApx|xAdv+l~ z2aK?Ip(31W;RD8I=HLUyp=^cpt`<~$=WfQ^*5^#By;&-cUJa={GJ?w^LoJW6#sTHw zTBVgo=6@)Ut6Uxxko6%qQF&BwbApe>=K53~#!J!UyB%LAbkM(ng0bI3hduF4^l-89 zHY%oypmW$fa7r(miJTupYO_m`lt0s)8h_5V{&c7?fe0YwwOI80m?q00KbZtLZ=AM2 zA-lNRCKHkGR128P2T$N>?iOjzQcG&W`4=+vaq1>$uKGulZlXF$8~>|exawMl*MIV7 z(Cr|{e4uB`x5)u~tHT2c=)|XfhWeiAF|n!c%0Z=Je*$01XE$*DMJ?hJwMB%`^*V!F z_47VHFFFQ`k|RFDxhFQxU)t#{mX0f5^r^>{>l*3rl*a>h*|%JmQ{C^^_5B6d+yr{J zwZx5y=CUy@ml%cQJbZ;}W_{bx<@0E3UPLHaG|t-+LFcN`TA}sAk`9$E*q@ODBVt2y z@Q66YqK}A?>WB#Uts~VYoCMuY*9+e8lh%Zfn2h(Wu?YvN2DhgB)_C9hRVx)j0XJdz zScFIT{53Uz+OVQcfi@yu4u|`gAZL4sTrHQc-@wYNarydb?8+Ck3_pG|ZAZ710-j7` zx!F({yDb>Dj9Hu|^G_0v8X*<8K;*PFv_2NPqk(w2H7@Y&rTn25*DZ-Tr^hr4kt;d* z#q=<Jc}BMua;~dpE6&%`D<RQ1EhVazu*MQFj`Uj*V1?xG#*r3nXdEd6p2E5BmvIj9 z&?YyWDk0Dw%u}q1RHjuzB2>bgEoi_f3mPz7ORo9-)Qe_?aW8u7T2L=i)j+f|+SQcC z*Zd|pzE0FM5nijW&!0-gRljx|E*_UHU)dILw|31H*@?EwKfzT{FG8#1Km!c?eiY_B zRFf%WKPiB2SbJBoh3mBxZR{%1YArR72<R&BEZ1rY&L;k@tJt`!fSnTYD32ElxR&r> zV}B^sk}05;?&n%spw&_=)Y7?IsH-ffy{lO3)!J1i)eGz@cSllJxzLCt-QINlp#pkP zX-Z5SZX)F2oger|=lk}UxcGAS1#pG3M@Dq|k{oAHqb4KLCCAZSsNWN7atskRxsV6{ z6;j+3>GfboS^OvqFkzFuk@z|S--XL4B97j@OYd%37tkY<Tl#qaObg2WOSS|lFUnlY zzWPGA7*=0fvW&?A&xW4lI5{D?D(&i#atZ3lO^efLd=LVkB*)=81&k7EZyvZ+Yf}MZ zL`fb#n?=0jXdP6l_Q}UMaLKvjb65y1yrP>az-#o`w^7Yz>ryckG@B{FD2+q(D4Iq< z2%UAeYox!^>+(D+9^jnQ9Fv?B<4x1<r>VSMM1iqX!^f?#nHtRpLg%$`SPw$C07(x9 zwBT+7CxNh;YF{M3mf4GmFW0{H%hpJD^9~v7X|L?hG!i~@8PDrr5m3B@Mua|%v0iC3 zm>I6m9m=I^2Gb@ZWMM01*LZFo&{9<c;qt{3>d2NpO&gTU_l9l;0;7Cm(xa&5izA#Y z-}MdjcY66Q=JJJDN|PIKFcR+<yJSmKS_jPVcj0*xo}_E}SsJDf!^#(ILjuR*JlRwf z*LBzti$?im;3dx6)9|L<B|UFw?4{?e#9W4+w+y}QV3ZyHDlLj;V}tW{vcCSg!K%24 z@&>V#vz2<1cy_)%%BQJKa*`KKT9O#$5l%AFkYo`j`Fwr-b8VyfNp+>8#DZVqTH2P@ zYVNBa*kJ$NSbp^>TLQw)k}0Kf#l#_41VbCDx)Q2ctiCeD`zqB&bf`YuD(ks6aLlcg zlb!NmfF+hssUGP<jTY($Wr>^oYz}1|d^Y!rZVm-CuiPyt;{ezi$>VxeVU&*5Raz9a z+zPnd?y=~f2j8e7nMkRwH1gC{HcF-M4PAFy43sTd85qzyuYjqc20ootRDwIyoYJb$ zSv{y%F-Do@tJ0#VtS(?y--ysZ*IAuwB37}OXI|s7dQ&E)%(+%`?j9uDkQ_?l%i}Ax zuoX>lzUIDBqm3vre^PF-Fdu@xcqE{jz+Q~<87m57HhMy&i2+(P&Ed|d66-gpc|0Ra zQ7G4|{35TBzN}Q%{i!LCG^$TiWuC>SX~V!K(Y&^V%Zyp;wooQ&z2VnQq>yS0T6)>= znU5{Kpw`VzXrqKaR1V0d=tVn<V}Y3~4fR=G>3Y?uR)M6Z29R?62p@KU4+|UWlC-i} zjw#+loR6?@%Ey`rpQ?|6>%{4w(XhNQ!l#*hf&49((wKLjy1s{yPuoxoT*s&KXa7WU zT$<4`7vlYEk`G)zs{@VmFJ0FtQOmNpM%iK`GB)=vvEWl5E4p}>>IPy^Q^oZuX{xM( zM0-OLI~(ZRD9SJA)e6(<VmNZ<RdP9<BPPnmf!36b0%OI!AjvDEuBjD=24uvJQ1%iO zMEP1hqTG=wM{@zqX39lI%B_HM)m4ph0HtR`iPAx?ViQxkn<*_YQY!yMQu_X?)-s>3 zM>Tpz)~wQ;6l3Ik^bji)#p#7-`^2Z)wVB38q8H@|AzrVq3;i?)NiIQ^#+$34-aVVr zQRjO|sv9$#tykRX1!~193t`;3#jk!Uw!tS6)!Ka{IHT}@;R*0`zU~Yj4MiDjptHSn z!?`5+MFy+sk%iE~YqO_5v9L~i>H+(+=-Ra#-n2~hJTW=leJ<5?BIoHL<Ezr$VrYKP z>Vm<Z$v%rMLD)NzcaZ!U72>K~;pP?Do29wW$w5ku;Wz|x@euc&Y0mQ&uxpdkP`;I< zysLrVgf6_(AqU_J@gtIOpq*8lu10RK2kPnKJx2B{Y;GUkCWIq~Ofg2*6P$K>Qca%G z*{%cj7m{+*V{*)7>UFdw-5nDHcDP4U{sYcCK<@I&@8P^7e@X?Od01_p;T8I!M=2h9 z$U+=t(GNZF93o+y$?;uM?u3|}YBI#m6~dO-oonC_`U$vlo5_x4O2tQ)tMFucqI@ah zEjZMOP7yR#B+1s!_Bv!0+(_{}VG(O_M!6ZB^Q7meY>grpx+Atmr6rWvyAK(EVo<*Q zWR$nD!9Q+^1?=uLwt1S^+zl%H<|OM|>ut^r7Wswas3v$sAWstu&=kD8R7qJw24mTN zti%6b__xeh(*A$*Z~3T?jvxPnf6JAt4gM{=_^3a22tgaS=nA}nZofd81mOmI0>SSG z2fnv?!hb%~-lV<P{YyoO3J``%L$e2_@;2wN$|`?Jq6JQhwG(4LxE`@sJ2B>v`xdLG z!LS{Q<Ev!Fx~@AMtlk1cawJN>$&sr}vJs+Q8d{gxa=9uvI44Yof2W3PD|n3lUd>>P z{;~|GgNpeO1Dww$N0JK51y7s^y+NO;E?;c!tVl7_LL7l|+1+h)<uW`qT5rbVp(yrK z1@^%~yaoGxCZAvPO|=yTBKA$S56l@YfF)7C`m&f!j;Y|7#ZnS}Lun6=whoK0WFeqh z&AP@|qG2&+T1~53CE{HfV^!`FXl19?HRVXT>yy<;GJd46%SeUzQqp|VLNpJNYW+$m z=h+*9jWNYF{EaxzMtZ{6iuT|hp{cXT><(Wh0bk=TAlt{IE>+PzWjP;W`)i#i<A}DP zBo#heKH_&d+IKe9aq`+KtuRVOHWb(FvtV{Jx8HRE#SsZ+#>^(Y{izdVN%%g9AYKdg zX_F@APrMpId+iHM`iY8MaqYyp@mL`g6qb^B3_@o2#iFt|r!vMDHu<6?{6zsZz&HKk z%h%z?O+J1@ENS+ZJmDXmmRZETlkhj?qrE=+j7v0$E-38xv&&$kO^VX+M`Q?%Kxii> z(BG)R*=TH%yGo?WiPaN}7P-Sa$q!3XVyw7_Vx<MRCoxjCYH<kX-|&AxVz{M|;y&#! zBR9>#?q}E^p2zPG?C%33J*MISdlWSMJI_W3m}`oH!=ZtoeBkLVMnc<n70t9j?~p@) z87ehOw{%rcCK)}ZSo|RyO5lBcvZ@i8n`1HAZ^LZKj9|`SBq2L55%sBO$@Vr<_hd2< zoOHwBBhyd4NWB#bApxH!Me@egj7Vd-=g){1y%$yc@hJQayo)E-%)O+rOH=9LG2;|l zRh23f^{b*DybFHiC)8>m45fp2?81A2CWRkfSWT_jyR*jqZPfSqO%7|d+0&X8iLS&T z+ZLO%G)mkah7BXCSbeX1ZE==Hi`}1B`^95bEbaUk^iHbuP~QM6R8=|%sMJ`lRJi^w zi*=^#k!hZBk*ZQcq^^_T3`3oSNL44ngYN2Oqmu*J)+p9BcmxlfQ=`Zx{e-e8@b=r1 z&Y8R9&H$CVuaN0ti+tla!)Zkaq!lf$$n0`j(K4;*TGEOZ<)1_0%Lu4kGPx{@=EnrD zy_=^`d?mG{4?|&=nfYQGGz<43EX-@+Bv-^kc_w%rJD>_<^|m|GaOPj&nSoxj8WFjn zNY46tAME6^B|-(cFOdBqo3r1H<L%+fALufBzhHT_*rGhC>xqhA{;cvB)TZA;?y1;a z((JRad5R{Yj;D`5FXtcExtvz25@EWVwNwn#0$Xj8%?>m_)9@M+bk+O^S^IGhYd>Bm z?Z<LwsX0IaGJcD#e%SBv<>KA5Rbbx9o)TZ~DstTEE&~Z4-Cn|y(nR*0mYB$%6Dr)# z-U!^{_y^M4Tsb9Rd3lQq#I!KBy9C-@@#WrS|BZj6URUap$Nw+=&A)Yxr~LoP-~9G4 zIwt%7#^3zxFr&YDG)XxW9h(0y4(IFiFMqtIfB9^j0@=TO@);>%9m3(QSnLL22mE(P z;l5`1{s;f^X+-3I^Dmzl`v2lzzT@S({LB9r{@l0t|9|o4-rZcAKld8)wYReBZ{_b_ z0^g@Y_5PXW-eFs>w6(df#{Ze>$pA%h`|a`N?#)S8{&JVghOi5pVQ8W(z{nv98yp?f z?}xay*h7=t^)D8uB|IN#m$g_n#B-C3Vn3PGXOP8l!FxJzKAL^j0)_;(z<!!sPb97M z!zgvTx5|&+FQ>;uQqa%e%Rj5&Pa*s%37>tqJ^W0AA9wf!`D6VlB_?d5JA5oZ*OSi= z@Uule+xWSGe7={T8_DOM@>%>K+b0fMphiXPP{JkQUF3UnN%-xQQWO5Nnf$UIKR1`p zRfp&~N<RO^&n@M%;OAEI`H*}TX_OKgx{2e=fmFLHO3&Q3LzuaeLzv{ZOd>{-Sj^8I z<@1O994nvS<mb-v+0D;g<?}2^a<_s>#^K#BW0F{&QJCaEDaj`($qXjZ6UvRBdqKJJ zb8jd&e!fROcjo85@;Mrk98XCm;x#8@l398#n^2M#O7hP?h(wAcq44tn`Fxn4ZSr{+ zKM#`6zwmRqd|oe~MTNj5Gw`~XvDHNBx%~DIO!A9AFv)*2i4l^-tNc7lKIiiDX!-mC zKR+y=C-d`I`TRH}IfIhS#0zLOlB8T_P?Ccv$sSB%f+W#}pC`)acKn<rpIh+r)AG47 zKTnp=p^#()N-|r$l2@a9P)5%m#8kyWOm)XWqGFd+w(#={@_8LU=g8+D`S~UJ{1rb> zm(QQbXEE=zOqFJT^?qF){nAYdJ1ODGNGLA`db_0c7{gZxj$k;K;6R4261<1uYXt8A zJ{l1p1<VJC1A(2j4J98%$(xzPJR(tffZ*E%6^8Q(9%T3)!R-t`Ah<z7k$(!4U%<&P z(~ANAtn%Y@h<to`0Qp!bf8<g$pJ^>5S}!yFoS=i@5`tL_za%(@;nxI*0w3YT$1>)l zNarI<>tk8SN3R1oc*PyS^0<|0Ehkz{7_J~_X1J2zrTqj~5j@Rs4Z;2Uq5YpkK8l!+ zGM$eRz{d!^JT~md@>sJU`B=oXHWID(7?u#sXSkVQF2k(^r!d?`a3b(=`2_M&#(W$J z;sfht1oAPI_^=Tl@l5M?qScY%Zi2Tm+)J<#!~F#P`v@K+_?Lv@pTx%z=A)uEKBzqQ z??XO*--mpxWm+eQ*7pog5nRMj5S-8O3_%aWvjk@XA199^9~I08?8zA0=?J|%CK4Z+ z#77#_x<s_@Wmritp5ax3F$}L0Y{9UKU<2S|9PvRGYUo6y1|L)&f9*v+&hAA%cI_ov zVP>SYm0>-CYZ+PyE@Rk$;AadQ5qw)hvFjM}5oy+UbZYclDvz1OM-K7v7}IJ-v_>&( zPB4vO6v4g>TN3QXuoc1fz{e2cBbxb$tBnsTj|Rj?81Zp#57CMtTE`f6B)E%VEWynT zI}=>Zuq(l(5{gYnkq^}lRbt(IyuAncDA<F1yvVeA60K(#_9FNg!`=i(FuaH0K!$w@ z-UEE}BR&$D4>(MN;hcDhuPh1gL&@Jo$+uw=DMaFCh64!JV`w9IZ8yO|1kW-|CwN#w zQFsKC&*0=U&B3!t*=`&?x9-N|e_#?Lh{O_xqX@pwa5TZ!89q#KHp8(5?T~zbN<Nd5 z&(>=pSRNd^8;fBS@zI}YO(0r58BQel4~AI;TQYo_U}J`p37UY9pAI7**~~|75FZAq zLhM35j_pD|e%VE|>_n@G;R^(RV3<SjbA~Swe3#*Lg0D#^x)C3_%tt{G9|p<MPJBE= zd_2gs@`zSC!&eCQXE>K&cZRPLyq)1|1fzhD#fM19U_KU@1BD%fIB6n2uK$jF9RHoA z4WhM|;oAg%VK|>)A;b3we#h_wf(w6#{cPf6fmzqTtwY9q?RVtk72@MrrnQi0O<?#r z!3P;GAvlQPmjwGU{F-1K@bS(cByBJsMdmuC&M4yJCgS79PL?)^Rt3Wq1dlUZNpLsA zRRp&%Ttje;gu+UE6fqxVwedmav2Z8y@zGA?V-C~WNVKLiEFt(T!_5RAXSkK%NQT=8 z+JKKa2T9ssK91BFHWDcL9+Z45Ch<FwXv%Om!7zq<30^5@A%ox<h6f4$A)&BP@<%xN zin<5yP36ePx^fgkmoTjpMC(I_rwA4>6a<|N&k%fp;aP%D0Ux;sNXTG5K(V3@A@u%o z97YG1V|nysT9=4cCx(>-TQR&!(8};S!4QU31TXK9d|cg6LPiJ+86kBFp?~ba^4PNj z`6$}KLPiME`jKHhf=d`$2)@s-0m0W9HX=A1_{b(cBAJiqkUE9Xr-+a7#78>QYDTn@ z7&a%^lVKFWe=uxGuqDG*1RDb%Vjl?^%tu^pd{B8@E<-*plp!B`%UH-DTE8;vNU(@u zEWsZbb|&~a!>$D1l~807A92h_Vo04r=xpL+8u2lKY4s#p4>9aTFr8s<g8doZL$Eu; zz65UvKKAY<A%pqIsErRQkH*AD1o2U^ouv(;b&}x#f_oX-2>!xw5Wzx*=>)%%P-GAv z8O%p!NS#9HyW5eEH@71n)0x%?qV*iZQ3NM298K^+h7S`Q#BeOZe!$1(JtTH8AKA6> zLFI8f@zIv}urRF&M9cRZiyZ_j7-kVX&hTl1yBSU<xJ5#dNPJ{7AGslc(|v_Lt$g<z z^6}Mg$j3aUWhYv0hA$AD&M=4IvkYG%_&CGq1V;iND|eIF!F&{i1Ph^~DESOZK7mQ( z5s5n)zCy4a!?^^TF?^L^1jE+|UjJ1lpFqhMaPkXkB=7wdN6(YLV)DQJ%0dQ_C}#LJ z!4(YW6a0eVdjvmV_yNH;Ao*3hNXX#imxa_Rgiiexi{Uxq<6)+?kZ28K_&LG*7%m}r z7sD?J#xnex;BCN1cj99i^HCI1rw|%SeE7B@AE&mlkU_K#FkC_KH-;+-u4lN4;Btm* z2riaTeEB;G8O%pnNS#9H8{3c%H}Ua2)7nV1o@7`;@L`6V2@YYnm0%*nZ3KG&AMJ^c zGUnq*NS#9HZN$ed#7FfnENu|2iwt)YJjHM?!F>$(6Wqq|Ai;GKiuZPsw84B-)W!#u z$Kqd*k57I<K3-*7Cy3T8hNlQV&rlG2g5ep0qZythI2ic2iTJ2sK1`u?3ZXrSk2{Eu z=1l7n(Q3%Bl3?{#mNp3f$?!VC6AY^e?vYTuT29hNC`%i)@j>OWZY%Qf^H${JQ>GOb zinQKgSdZYV3@rp_Fl<0Dn_(k@<AIM5;v<szhz_lD^c+llq!J%@GOcDrs{_O41e-IA zB3PecOM+EfSlS?XK|=A;4w5#QkGRlaVfe@vOn(0s6he!cL=2Hw$*?2AuNcM>{D@&^ zg8yRJm0%tuf1`|q3{E~Vv`!&(@)jIMCvL%F7{;`E60H=5y$IgTus6ZZ4DTV>nqgmp zk-*0k;v<py$Ox@d2)())%j2)j$j5=rEMyR^GKK>P7BjRFT)}V<!7mu56Z}9z;oVL` z2J?{_TBi`2M|{jAKAvP+BZ$^mhNB1$V>p`NeGDHaco)O51Y?1Z@x(_a^O0Q}A5<QZ z#76_-<6<cb8AMAkoJjBh!z_ZoF?^cfdWMq;E|*Xo_>F`N<|8+>P9gMzQsm?9QsiS6 z)3OsSJHr<UKFKhL;KK}GA~=NMbb^V%$8h2!m-#5DjSni1SmL8S@zIcJ<q<6t!&eAi zEMaMb;3<Z$65Pk|HG<nD6u<mR(gyRfAhb>)ba@H#v9tvFc!z1tBU-O9e4F4bhVuzN z&+t8hPcZy|;Ar3@nfO@1d@QSt4=RsD;^Q9TqXW}gNVK9Deon9<!zBc(H?i13@K1(c z6FecISiOzJ4(6k%#_+Xk6DGfX6DGfsNh~K4-!NQ3Z~?=W1m9%1ir^~@*ARRWlJ85& z7jg1sdNKSFR1D7%ACriW5lm|%(Mn@jLa;Bx%>=tK+)A)L!)*j_0X~-hLed8Faim5u z)Z2ukr-_nRHnNaGBn~m$O>hUpy#zNh+)wZ)h6f3LDWSNFl0U-9SA^Cng#LRY7Q<^B zkq-ycIzhCaW_XI=IEI2?2E#K1lNp{R*bDghW-AF9%m?^7sY3{DLwrOLA7-X?iD*?8 zvyeehVR)6`L59}}Zf96UaD#;6cH+atE)pWc>J&o1EJi*S79$_|Oe-u5X}!#_9zh2~ z3&AXg4G4~5*ofdz;N!zBBxEoj(P4E8p}mNYIO5}0rqzsSHDTDCpqXJ5!Al!h$RK!{ zVJm|BB^0+1AJNQ5Ty1<%d2HB#e5~1kd@N#GF+}S<h8+p!GmIse%dj)SDGa+3oCtj6 zZzgGj`A7__QwSYOeAtMOc&62pXmw=Ri{PycdlPKL@E(Hx^(<`={A)d&{}LaG%tuCT zd{BApUypqJz8?8l%d}F6*7pnt5M0F2MsPmEK?FSv(+SQ5K4z4Xw84C2YJAY(Q4;<v zCI1v9{{WL1K_mt;97XURhNB7I!SG>%(G15Dycv?OE+HX<lg|#TbM(AXgnV2sLLu}} z5epeaYbV2r1WOoZ5&W6q(*(a}IGNzb5{egyk8I{6H>^$}bWRZtqqB>!Jf3D+cB1tt z!xsoXz%YkkD#Mou_GUPpU>D%y{3a4In2&<6I)%`isXQ7JAC>D^$RJu?hOZDj#BeUb z9SmP3xRK#&1b>oHOe8)En2!Zvbqb*$uR}iGU59+U%(Uhat*H#(CipbN`2@!?e2-uT z!w(2110RPrl90iCEUS$V*6|`fZYMsPFs+3|E0p2q1S<<!$RMaN{F2~7hF=rhE}<Aj zd@N%=io)s?LVqemKE5kNKHg(m%Zb(-3|H{z&Tu7<?hIG)=+1BrkM6+7Z^a~SFdt>L z@j>O0OnmesJ~}e3jYR7<h9x|@Gu+IhJHxF!y02wvgGYA>#Q@@?jQKbcR;LiUeJ%2_ zWi9gYJ=6M~Xf0;An@4wsdwFzcxSvONh6j0c2R;fnkl4X|RMf@?mB$$3V<ho$FVi|f zw0bZ+#iKhz!J|9FGd#L8Jj<i|8p%g5;seqz2@j*Bs@Gsrr`ND#!L5s7CAThyR|#%l zc%9&n466t(k<d1KBj{Ud`k((-{-CGstNKs=pzTtwCB6HBv7QV|LPjHdSKIh<n`e3% zT!%C0pO>QhYcQ+kDsogP-e2KEQ+yHKcS)T46KDZx{90Gc3BWebtSBX7j($JWGb>t& zXjN^rf2!KA%uR!8UX8l)?o;BiJyo?Cj(l%#l|R6K<x|H<Svg6{r~&pc<A~l8rq{G~ zdV_-L-OBX-lrmm{%`<AW&3)D8Zh==9*9YH+a#h+JIjha~78i?IpT9H{{6{X_&E@MX z%>q1+v8!BCv)>9D$T7Ck2sZfg`7`a|?pra~uN{@CCx$r7h;wJfW0^Uc$5)DO=VU`n z?rt*p&qM?6qPg}$*VhK~n541YTQY2O`|MMk)gks?&gxJ{BOr_BSga{~!Pv$HvGc46 z{ZK}Ox^*n5TE~HsxvGZ^A2?d(&WJPF8|BXEZgOlXN$qa-7RMj(u0y}c0~2Lc_kOAZ zWP33{ibLLVvVWWsqnf!{_RscY=4Pr*mV0{sgqTP%d3TlH;IaceGm%9`dfjhr6`N(u zh{TY>A#GC88TDz=ZbxAK+IysfNzMzxcGeVV7t`vB(p+Z@mI*5L{kq>H*(4s6(yVs_ zW6ds@)di*{y>r>%$X|!4nQYw*zhD(u^q=7Kdyf|c+b5;+=lck(GCwOy#E(J?9kO$8 zbZ<yoUlr}#Agg6lifpQHkEB=bGZNKQhF4w~sq;H4ihSB<`HvJ#EErxguqSvYEg5*1 z8B_5%!IPDvh0T3v)o~?-yyuKjnw6>je_9k>X@vVwS$ReF-D3j$zsvrL^mCzG`j7B^ z)*H6`bw!vx-IA_Ywi8O?Jgexl3w2=k!84jYOPl9y`p(d$YLpk1lS~B5&3QH_cUDhR zlKYZ1?+<KxcbTlNAF%UT^LE1Nwro@x+^EJ(qDCe2Wy_x&lOgWOg95PT{RPzXr`X89 z2EOv|q}zt7C!KB~x+fiq@#`v%LG3^WrMQn47DcDxRhkRpo8S1oZmy!-Os!4x@#bg< zEiFSF-3jA~Y%zNUb*)S&rYxvcaKfB}_hSkl$7`gg(Ylj(eFI!Gn>HGTuLsJ5ns+mx zbx_jses8QV*?y<Wl)Oh(``BC#4>+kDR+)^UlR0#R*%<n{6K%KHNCh5jVMAzHIb~LL z3WSDk*eMXj_~>Vj5l~!0E3TQp@_W4}?$F%S9F;<;vO)Ft1a0@@mx;MjFu&0P?cu0g z5p~gzSyBzuw$@qV7!8@FNR6dJTRWAX^V|5P&QFr&P5FM{r+c;TZ#hxi3qfrI{eKz8 zha&KXv)dPFuU0Gnm-RA@*2;AHjw-*=3DO59-3b!K7__oVH9}=5(`YRZojDH%3%WCW zss-JADcCB{pwYov_yr#uil`3c*YF;4s@zrg`{7`3_bI>T^rcwzdz6eu?AlL*<2S`s zszaM%5&bCm=w(+n#iA*|a?LDm2taiXev7~VXEEL-@_uEwj~Z<ppN88!FGRzg%8Sja z+2(z~{Op;tz+^JqUq$(2PBTsZsC_<e{-j|(wgt_{839<opLa3vx>=h0cR9(r|FXHO zAxCe5VK11EPK&v+S0}+`vzq>ow^#X7^UgVLb{(+a;k;p<SYW>winH<rjTm(C-*5?x zW@PB+`7>;AC9SI*ki6Rr^_4#(1Hzj@_;N1YyZ2G)S~pY$`?G=k?1lWuOzlip?|I1? z%$M%A<HbWzE|!8rlKZkXZ>fAvjXwe7F!<p}a{A2HSDJ?5Q$uUs1qy3p&HIs_+FJ9v zgyK`QJyrhgZq2({#!k&U;TVN>LQHgv5EQ!M7U2;S-69nDORjmPgbXb3edYB0e`ed@ z($xhKzrG4)y6+jbmtzgo&jqpaxEv!FsPP?gG(9AlPIq4s)kW}$)s;-1MrBaa8obDP zQg$lk4s6*oY~X|RcaC{K#@r8gp)LqN^!JoN`_!!MU)Tp&&Zm2rVmC<JKat;D><*=L zHvs;LPOGc@>G`kmWq}bfiK+2L@mn|-udNH3V92I-qIgqgkE|>x%d0f=f(uw=M<b8` zLLkOZE2@k}AbU?>xl4A$zq0`OPqqMw&YD8kRQXkBjcBY-%>E&GFwZ*#hrb+&tO|l6 zMIlTs;(?7i0eVLYF0%Wx)lWI3i4Rr>CKD}M{t!IA<+Z4h%t@I7EQL+N^Anf|LEjQb zzOV9QAb=@9RiP;wvNs3cfZ^g=9Fd}3;F^v|dqA-4Gh^0K-?@1Wr*m2?kyDJPS&XaP z-p-c)A-R}34UNXXl^RV%h~{6khUZ?FzlzLCVMAFf*D%VtD&u$^3jSp26SR*3_qDW! zHA9MV<hT7rSy@_eFDONDxr<k=;<vC>zLc13ElOxHv^u^gEL&X$de(&|_t1LCVF^4Q z+Csy3rf9Z_YQf;Tw`s7TW6e7cjUEc+f|zm??AvK_wd{8})Wd{(EtSgI8$q2eTc)P$ zzQKM?a21jdRF2)y%cC$FVt)Rk-b$3P>LCB1(c%SJ@XCIl=Fjp48DIIAjwc$wALlcC zkME&=lYi9Pi?Yk7%iAETGza=O`!s)^^#F(Zb^oW36EGoGfaU~F16=1r`30tc!(!1= zC}Uimw3LIKa#FJkthti>_ZFjQV;`#7-A`DiNi=Nf%|zzYGFAmiGhoGrbI{qRiZw6E zhU-0z-=gWV-Ht`xpOJE!dpAtPFFxp#E&DIDr<Dq=z_OE?{E^i(8K2aq@be&k@pUut zFZrE!J486EP3phO2;M*7<Ky4pzJZvw&QPxNs@QuQ6yhi4(4#Jhcgrcc=Qug9CP2*M zHxI7X3^HuPN3Sx%Z*U{`<OXWC*<uK!zW}IQ;19;Eqz^i<gS^{<>|NlJBmdcKT!W;! zFVO-HS2#DouBGhnn^#q#y{1}jDZ_jN)9s=X<g}Kn%3c0|wUEI+LurmOwWLbl9~qVX zcl!<Uea_#%m-5ZsO{xR-Tjn$f#19(r+qB<O$9l}VVqlz6-jX=ph!f{djVl@0-4tJ` z{Of`&$>E8;r^d^ovJXhitCTO}9j##>K#bUe1rlF5MtSOjp}%>C(LTYSxKGe#r4}ps z0*(X0@*M0txThvMB6z>$sY-#yH)i!#j>-|fAaU+bFrpP3<r&@IOJeVzW@8}mH<zAa z0qyeJJ0@CJoOhoWs|Wb~wXI=$`~hRQ31WE<8_PSu8%%+9#hlr}>DO5Q$KtBmeL2bf zyYM!urbYAV6!$SPWD}1Ihei2*pk6~nd>;qmn&w+rv>Usfx=7EJ>i!eld7l+6ddN0` zrey-l(|NCXW<%%9F!RiCrQ!hCQ>bZ=#N9JS^5h;76DEH3vyG8}{k_sb#{Io0<sVZt zJe>UN>_Q17G<zs{FUox!1(A;*MnOc%AV3QC9SS@HqiXC2X!RFK24-%=pY!B2bNL-N zxrNpTVG9rB@%(9#@#SJy5e#=GTc6}e`+n<+tSE>FvDKLR{;KTbBW><o!)0)8<-jPj zqmfK0mv>A_S_4}^=f#eT>=ZJ-9J=_1i>ejwtglo7Yy^0@MCmI9)n-ki?>7OW*V>2w zJJD-8ie6*o#OwLK3J2~?61p0FSwrZG520x(lSCm<fG1HXcPU8$86f}c{K6m%O<c-E z>621(fFPE3KE)(%Hl`ab=6z8^3VX;LkZyNP1ha6uiICJ7W4h<Rp~jm@5?D8LpagaY zNMMfXwMbu2m}yr2<40=VZ+%bf7-Wx6O(c%gkd&W-*}yY0It@evu+&u`3%BqaFe}yu znx$Ub0rT%o8v?f$VE;&)gMVBSFbAi^WTOA$?}N7!(EKUpSn0_A$<Y6|6tR?He%=4~ z*Id}jD#Lz5v{(x9=2zDypM3T;7bSiY_+GpsUn-reb@@l5f6S-Tj}t?IevvOgev<Oa z-4L8l`z^YhV0HOmKcGpFKOQKKj&cb?RvBp{U<8;#Di+;_fqfJJAWP)XG{0&qZwZKH zTd6YV{y-D`KKQer=jk7o;XKVwT0sPjm(;wgHQD2N`8A0mC~1rbxSKy-@dJIF4;23m zArlr=`GtRlT4wKp?1uiW{)hg5A+Y~n(E9)4zv=&91or<?|6TvztoQ$=f&KsU|DpeX zY3%=tjQ#)1zw7^pp=;c{G_e0K`XBoLm&X2YO!xd3|F8Z3JMRBW|A+oxXz2fj&VT>P zfX=V3-?jc<_;>w(VXggt>3`e**9P_fC3W=wwRQLZC4bldmjw0yC3W=wwYB&Ewf}4X zUsiknA6#4iFVXsc3HSeRYwQ1C*XaL)1N(nTVE->M_Wy5d^#4zw|9@Yj|7+)kr4bb% zoPc0vFb@a2w?^nJG8s)%|Ar;tToMrY81|;s=*jhtbTB@+8rMhd;MwDx8xXk_cB14Q zJ>)vlOg1dE_fklf9<qW$GWC$xC}gy@^m|4kt#{ZnB(iC!J2<oypR~u8glC`1gm2XI zpw*B?(NnIg(8aN~Z!yg#+R8w0v3jnMA*_HGsd%*T%Y~75j*2tI1&Pr}cEEmkme>Q~ zeN}&**`gG1OBLscp8#Lw_J`-eVj%?WtTyn2=epuG2!9=I67xYX7~UJTv5L(|Qaw=U zf<Znlv_K1e)et&Y3vH%Tn3h99W<yXm1)VR}gR&^-fFUT8f;Jn1MpMv_hM)`z`qU6) zqo6koL9rBcHrk|`8f251Uw)-Vk^^i%*=xKzylb@n?ET2=R-V)AQF#E0S+5VJTM%v( zgmH8+;N3xZ8I^sJR&x7UZn7v(!EiAmaD9#T59d^=_79f?+dnkglU?v7E*dIm(LgTU z*o3?Mjb*#B4erM&wIn%C&Y|&m>|K)FQ_iJ#$*DE50Y$c{{%?lS{9Br=o)>n}LPm4Q zty)MXheT*0SsZeCs2Y;ZA;-0l91ht*AtmHXw8RPPLUW1pW#S*l0gky?{niQ1lJnI) zLk*m-2ezibQs)8_{gbbk=!70&Liwv<7g7v^NxKxsEH(cW!4tmx&GwQdP}MN416p8^ z6^XjT-+dG834K&W7F;)ppZh^=O^RAK8r;iG(_cg<!_svU$<*!0`t3^my&yKCgfR`@ zonXtjH(=KNieLPEM8-^oBP==M|0d?C!It*M;29X4EJxm)I2?l`VMvazbY6Wr(K@e0 zd~-J)ltVJT+fy=`@>itbhu|8Mcxcd8o%TBiy}2FU^pt-@WYC*I@MavmsYJPjl9|$$ zl9>rF{`f*Q8a)0(D3?QAG(SS8bWiG?JEWBvK63L%w>P2USp)=i+rTt-iH~7#jAxFE zsrt-unx1hW#QiflCW+_iAzM5}4_P9U9x}y)^e|csrH2fWN)I;CmmXrpXRSz%m;}PB z<S2AWm2Q)`*CK69*~Fb&gvc*(^ziOA^jCCs>pc))AJà%O{RGXvXNAT?H87}8m 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�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
new file mode 100644 --- /dev/null +++ b/gui//Screen.cpp @@ -0,0 +1,1567 @@ +/* + This file is part of Konsole, an X terminal. + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +// Own +#include "Screen.h" + +// Standard +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <assert.h> +#include <string.h> +#include <ctype.h> + +// Qt +#include <QtCore/QTextStream> +#include <QtCore/QDate> + +// Konsole +#include "konsole_wcwidth.h" +#include "TerminalCharacterDecoder.h" + +using namespace Konsole; + +//FIXME: this is emulation specific. Use false for xterm, true for ANSI. +//FIXME: see if we can get this from terminfo. +#define BS_CLEARS false + +//Macro to convert x,y position on screen to position within an image. +// +//Originally the image was stored as one large contiguous block of +//memory, so a position within the image could be represented as an +//offset from the beginning of the block. For efficiency reasons this +//is no longer the case. +//Many internal parts of this class still use this representation for parameters and so on, +//notably moveImage() and clearImage(). +//This macro converts from an X,Y position into an image offset. +#ifndef loc +#define loc(X,Y) ((Y)*columns+(X)) +#endif + + +Character Screen::defaultChar = Character(' ', + CharacterColor(COLOR_SPACE_DEFAULT,DEFAULT_FORE_COLOR), + CharacterColor(COLOR_SPACE_DEFAULT,DEFAULT_BACK_COLOR), + DEFAULT_RENDITION); + +//#define REVERSE_WRAPPED_LINES // for wrapped line debug + +Screen::Screen(int l, int c) + : lines(l), + columns(c), + screenLines(new ImageLine[lines+1] ), + _scrolledLines(0), + _droppedLines(0), + hist(new HistoryScrollNone()), + cuX(0), cuY(0), + cu_re(0), + tmargin(0), bmargin(0), + tabstops(0), + sel_begin(0), sel_TL(0), sel_BR(0), + sel_busy(false), + columnmode(false), + ef_fg(CharacterColor()), ef_bg(CharacterColor()), ef_re(0), + sa_cuX(0), sa_cuY(0), + sa_cu_re(0), + lastPos(-1) +{ + lineProperties.resize(lines+1); + for (int i=0;i<lines+1;i++) + lineProperties[i]=LINE_DEFAULT; + + initTabStops(); + clearSelection(); + reset(); +} + +/*! Destructor +*/ + +Screen::~Screen() +{ + delete[] screenLines; + delete[] tabstops; + delete hist; +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Normalized Screen Operations */ +/* */ +/* ------------------------------------------------------------------------- */ + +// Cursor Setting -------------------------------------------------------------- + +/*! \section Cursor + + The `cursor' is a location within the screen that is implicitely used in + many operations. The operations within this section allow to manipulate + the cursor explicitly and to obtain it's value. + + The position of the cursor is guarantied to be between (including) 0 and + `columns-1' and `lines-1'. +*/ + +/*! + Move the cursor up. + + The cursor will not be moved beyond the top margin. +*/ + +void Screen::cursorUp(int n) +//=CUU +{ + if (n == 0) n = 1; // Default + int stop = cuY < tmargin ? 0 : tmargin; + cuX = qMin(columns-1,cuX); // nowrap! + cuY = qMax(stop,cuY-n); +} + +/*! + Move the cursor down. + + The cursor will not be moved beyond the bottom margin. +*/ + +void Screen::cursorDown(int n) +//=CUD +{ + if (n == 0) n = 1; // Default + int stop = cuY > bmargin ? lines-1 : bmargin; + cuX = qMin(columns-1,cuX); // nowrap! + cuY = qMin(stop,cuY+n); +} + +/*! + Move the cursor left. + + The cursor will not move beyond the first column. +*/ + +void Screen::cursorLeft(int n) +//=CUB +{ + if (n == 0) n = 1; // Default + cuX = qMin(columns-1,cuX); // nowrap! + cuX = qMax(0,cuX-n); +} + +/*! + Move the cursor left. + + The cursor will not move beyond the rightmost column. +*/ + +void Screen::cursorRight(int n) +//=CUF +{ + if (n == 0) n = 1; // Default + cuX = qMin(columns-1,cuX+n); +} + +void Screen::setMargins(int top, int bot) +//=STBM +{ + if (top == 0) top = 1; // Default + if (bot == 0) bot = lines; // Default + top = top - 1; // Adjust to internal lineno + bot = bot - 1; // Adjust to internal lineno + if ( !( 0 <= top && top < bot && bot < lines ) ) + { qDebug()<<" setRegion("<<top<<","<<bot<<") : bad range."; + return; // Default error action: ignore + } + tmargin = top; + bmargin = bot; + cuX = 0; + cuY = getMode(MODE_Origin) ? top : 0; + +} + +int Screen::topMargin() const +{ + return tmargin; +} +int Screen::bottomMargin() const +{ + return bmargin; +} + +void Screen::index() +//=IND +{ + if (cuY == bmargin) + { + scrollUp(1); + } + else if (cuY < lines-1) + cuY += 1; +} + +void Screen::reverseIndex() +//=RI +{ + if (cuY == tmargin) + scrollDown(tmargin,1); + else if (cuY > 0) + cuY -= 1; +} + +/*! + Move the cursor to the begin of the next line. + + If cursor is on bottom margin, the region between the + actual top and bottom margin is scrolled up. +*/ + +void Screen::NextLine() +//=NEL +{ + Return(); index(); +} + +void Screen::eraseChars(int n) +{ + if (n == 0) n = 1; // Default + int p = qMax(0,qMin(cuX+n-1,columns-1)); + clearImage(loc(cuX,cuY),loc(p,cuY),' '); +} + +void Screen::deleteChars(int n) +{ + Q_ASSERT( n >= 0 ); + + // always delete at least one char + if (n == 0) + n = 1; + + // if cursor is beyond the end of the line there is nothing to do + if ( cuX >= screenLines[cuY].count() ) + return; + + if ( cuX+n >= screenLines[cuY].count() ) + n = screenLines[cuY].count() - 1 - cuX; + + Q_ASSERT( n >= 0 ); + Q_ASSERT( cuX+n < screenLines[cuY].count() ); + + screenLines[cuY].remove(cuX,n); +} + +void Screen::insertChars(int n) +{ + if (n == 0) n = 1; // Default + + if ( screenLines[cuY].size() < cuX ) + screenLines[cuY].resize(cuX); + + screenLines[cuY].insert(cuX,n,' '); + + if ( screenLines[cuY].count() > columns ) + screenLines[cuY].resize(columns); +} + +void Screen::deleteLines(int n) +{ + if (n == 0) n = 1; // Default + scrollUp(cuY,n); +} + +/*! insert `n' lines at the cursor position. + + The cursor is not moved by the operation. +*/ + +void Screen::insertLines(int n) +{ + if (n == 0) n = 1; // Default + scrollDown(cuY,n); +} + +// Mode Operations ----------------------------------------------------------- + +/*! Set a specific mode. */ + +void Screen::setMode(int m) +{ + currParm.mode[m] = true; + switch(m) + { + case MODE_Origin : cuX = 0; cuY = tmargin; break; //FIXME: home + } +} + +/*! Reset a specific mode. */ + +void Screen::resetMode(int m) +{ + currParm.mode[m] = false; + switch(m) + { + case MODE_Origin : cuX = 0; cuY = 0; break; //FIXME: home + } +} + +/*! Save a specific mode. */ + +void Screen::saveMode(int m) +{ + saveParm.mode[m] = currParm.mode[m]; +} + +/*! Restore a specific mode. */ + +void Screen::restoreMode(int m) +{ + currParm.mode[m] = saveParm.mode[m]; +} + +bool Screen::getMode(int m) const +{ + return currParm.mode[m]; +} + +void Screen::saveCursor() +{ + sa_cuX = cuX; + sa_cuY = cuY; + sa_cu_re = cu_re; + sa_cu_fg = cu_fg; + sa_cu_bg = cu_bg; +} + +void Screen::restoreCursor() +{ + cuX = qMin(sa_cuX,columns-1); + cuY = qMin(sa_cuY,lines-1); + cu_re = sa_cu_re; + cu_fg = sa_cu_fg; + cu_bg = sa_cu_bg; + effectiveRendition(); +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Screen Operations */ +/* */ +/* ------------------------------------------------------------------------- */ + +/*! Resize the screen image + + The topmost left position is maintained, while lower lines + or right hand side columns might be removed or filled with + spaces to fit the new size. + + The region setting is reset to the whole screen and the + tab positions reinitialized. + + If the new image is narrower than the old image then text on lines + which extends past the end of the new image is preserved so that it becomes + visible again if the screen is later resized to make it larger. +*/ + +void Screen::resizeImage(int new_lines, int new_columns) +{ + if ((new_lines==lines) && (new_columns==columns)) return; + + if (cuY > new_lines-1) + { // attempt to preserve focus and lines + bmargin = lines-1; //FIXME: margin lost + for (int i = 0; i < cuY-(new_lines-1); i++) + { + addHistLine(); scrollUp(0,1); + } + } + + // create new screen lines and copy from old to new + + ImageLine* newScreenLines = new ImageLine[new_lines+1]; + for (int i=0; i < qMin(lines-1,new_lines+1) ;i++) + newScreenLines[i]=screenLines[i]; + for (int i=lines;(i > 0) && (i<new_lines+1);i++) + newScreenLines[i].resize( new_columns ); + + lineProperties.resize(new_lines+1); + for (int i=lines;(i > 0) && (i<new_lines+1);i++) + lineProperties[i] = LINE_DEFAULT; + + clearSelection(); + + delete[] screenLines; + screenLines = newScreenLines; + + lines = new_lines; + columns = new_columns; + cuX = qMin(cuX,columns-1); + cuY = qMin(cuY,lines-1); + + // FIXME: try to keep values, evtl. + tmargin=0; + bmargin=lines-1; + initTabStops(); + clearSelection(); +} + +void Screen::setDefaultMargins() +{ + tmargin = 0; + bmargin = lines-1; +} + + +/* + Clarifying rendition here and in the display. + + currently, the display's color table is + 0 1 2 .. 9 10 .. 17 + dft_fg, dft_bg, dim 0..7, intensive 0..7 + + cu_fg, cu_bg contain values 0..8; + - 0 = default color + - 1..8 = ansi specified color + + re_fg, re_bg contain values 0..17 + due to the TerminalDisplay's color table + + rendition attributes are + + attr widget screen + -------------- ------ ------ + RE_UNDERLINE XX XX affects foreground only + RE_BLINK XX XX affects foreground only + RE_BOLD XX XX affects foreground only + RE_REVERSE -- XX + RE_TRANSPARENT XX -- affects background only + RE_INTENSIVE XX -- affects foreground only + + Note that RE_BOLD is used in both widget + and screen rendition. Since xterm/vt102 + is to poor to distinguish between bold + (which is a font attribute) and intensive + (which is a color attribute), we translate + this and RE_BOLD in falls eventually appart + into RE_BOLD and RE_INTENSIVE. +*/ + +void Screen::reverseRendition(Character& p) const +{ + CharacterColor f = p.foregroundColor; + CharacterColor b = p.backgroundColor; + + p.foregroundColor = b; + p.backgroundColor = f; //p->r &= ~RE_TRANSPARENT; +} + +void Screen::effectiveRendition() +// calculate rendition +{ + //copy "current rendition" straight into "effective rendition", which is then later copied directly + //into the image[] array which holds the characters and their appearance properties. + //- The old version below filtered out all attributes other than underline and blink at this stage, + //so that they would not be copied into the image[] array and hence would not be visible by TerminalDisplay + //which actually paints the screen using the information from the image[] array. + //I don't know why it did this, but I'm fairly sure it was the wrong thing to do. The net result + //was that bold text wasn't printed in bold by Konsole. + ef_re = cu_re; + + //OLD VERSION: + //ef_re = cu_re & (RE_UNDERLINE | RE_BLINK); + + if (cu_re & RE_REVERSE) + { + ef_fg = cu_bg; + ef_bg = cu_fg; + } + else + { + ef_fg = cu_fg; + ef_bg = cu_bg; + } + + if (cu_re & RE_BOLD) + ef_fg.toggleIntensive(); +} + +/*! + returns the image. + + Get the size of the image by \sa getLines and \sa getColumns. + + NOTE that the image returned by this function must later be + freed. + +*/ + +void Screen::copyFromHistory(Character* dest, int startLine, int count) const +{ + Q_ASSERT( startLine >= 0 && count > 0 && startLine + count <= hist->getLines() ); + + for (int line = startLine; line < startLine + count; line++) + { + const int length = qMin(columns,hist->getLineLen(line)); + const int destLineOffset = (line-startLine)*columns; + + hist->getCells(line,0,length,dest + destLineOffset); + + for (int column = length; column < columns; column++) + dest[destLineOffset+column] = defaultChar; + + // invert selected text + if (sel_begin !=-1) + { + for (int column = 0; column < columns; column++) + { + if (isSelected(column,line)) + { + reverseRendition(dest[destLineOffset + column]); + } + } + } + } +} + +void Screen::copyFromScreen(Character* dest , int startLine , int count) const +{ + Q_ASSERT( startLine >= 0 && count > 0 && startLine + count <= lines ); + + for (int line = startLine; line < (startLine+count) ; line++) + { + int srcLineStartIndex = line*columns; + int destLineStartIndex = (line-startLine)*columns; + + for (int column = 0; column < columns; column++) + { + int srcIndex = srcLineStartIndex + column; + int destIndex = destLineStartIndex + column; + + dest[destIndex] = screenLines[srcIndex/columns].value(srcIndex%columns,defaultChar); + + // invert selected text + if (sel_begin != -1 && isSelected(column,line + hist->getLines())) + reverseRendition(dest[destIndex]); + } + + } +} + +void Screen::getImage( Character* dest, int size, int startLine, int endLine ) const +{ + Q_ASSERT( startLine >= 0 ); + Q_ASSERT( endLine >= startLine && endLine < hist->getLines() + lines ); + + const int mergedLines = endLine - startLine + 1; + + Q_ASSERT( size >= mergedLines * columns ); + + const int linesInHistoryBuffer = qBound(0,hist->getLines()-startLine,mergedLines); + const int linesInScreenBuffer = mergedLines - linesInHistoryBuffer; + + // copy lines from history buffer + if (linesInHistoryBuffer > 0) { + copyFromHistory(dest,startLine,linesInHistoryBuffer); + } + + // copy lines from screen buffer + if (linesInScreenBuffer > 0) { + copyFromScreen(dest + linesInHistoryBuffer*columns, + startLine + linesInHistoryBuffer - hist->getLines(), + linesInScreenBuffer); + } + + // invert display when in screen mode + if (getMode(MODE_Screen)) + { + for (int i = 0; i < mergedLines*columns; i++) + reverseRendition(dest[i]); // for reverse display + } + + // mark the character at the current cursor position + int cursorIndex = loc(cuX, cuY + linesInHistoryBuffer); + if(getMode(MODE_Cursor) && cursorIndex < columns*mergedLines) + dest[cursorIndex].rendition |= RE_CURSOR; +} + +QVector<LineProperty> Screen::getLineProperties( int startLine , int endLine ) const +{ + Q_ASSERT( startLine >= 0 ); + Q_ASSERT( endLine >= startLine && endLine < hist->getLines() + lines ); + + const int mergedLines = endLine-startLine+1; + const int linesInHistory = qBound(0,hist->getLines()-startLine,mergedLines); + const int linesInScreen = mergedLines - linesInHistory; + + QVector<LineProperty> result(mergedLines); + int index = 0; + + // copy properties for lines in history + for (int line = startLine; line < startLine + linesInHistory; line++) + { + //TODO Support for line properties other than wrapped lines + if (hist->isWrappedLine(line)) + { + result[index] = (LineProperty)(result[index] | LINE_WRAPPED); + } + index++; + } + + // copy properties for lines in screen buffer + const int firstScreenLine = startLine + linesInHistory - hist->getLines(); + for (int line = firstScreenLine; line < firstScreenLine+linesInScreen; line++) + { + result[index]=lineProperties[line]; + index++; + } + + return result; +} + +/*! +*/ + +void Screen::reset(bool clearScreen) +{ + setMode(MODE_Wrap ); saveMode(MODE_Wrap ); // wrap at end of margin + resetMode(MODE_Origin); saveMode(MODE_Origin); // position refere to [1,1] + resetMode(MODE_Insert); saveMode(MODE_Insert); // overstroke + setMode(MODE_Cursor); // cursor visible + resetMode(MODE_Screen); // screen not inverse + resetMode(MODE_NewLine); + + tmargin=0; + bmargin=lines-1; + + setDefaultRendition(); + saveCursor(); + + if ( clearScreen ) + clear(); +} + +/*! Clear the entire screen and home the cursor. +*/ + +void Screen::clear() +{ + clearEntireScreen(); + home(); +} + +void Screen::BackSpace() +{ + cuX = qMin(columns-1,cuX); // nowrap! + cuX = qMax(0,cuX-1); + // if (BS_CLEARS) image[loc(cuX,cuY)].character = ' '; + + if (screenLines[cuY].size() < cuX+1) + screenLines[cuY].resize(cuX+1); + + if (BS_CLEARS) screenLines[cuY][cuX].character = ' '; +} + +void Screen::Tabulate(int n) +{ + // note that TAB is a format effector (does not write ' '); + if (n == 0) n = 1; + while((n > 0) && (cuX < columns-1)) + { + cursorRight(1); while((cuX < columns-1) && !tabstops[cuX]) cursorRight(1); + n--; + } +} + +void Screen::backTabulate(int n) +{ + // note that TAB is a format effector (does not write ' '); + if (n == 0) n = 1; + while((n > 0) && (cuX > 0)) + { + cursorLeft(1); while((cuX > 0) && !tabstops[cuX]) cursorLeft(1); + n--; + } +} + +void Screen::clearTabStops() +{ + for (int i = 0; i < columns; i++) tabstops[i] = false; +} + +void Screen::changeTabStop(bool set) +{ + if (cuX >= columns) return; + tabstops[cuX] = set; +} + +void Screen::initTabStops() +{ + delete[] tabstops; + tabstops = new bool[columns]; + + // Arrg! The 1st tabstop has to be one longer than the other. + // i.e. the kids start counting from 0 instead of 1. + // Other programs might behave correctly. Be aware. + for (int i = 0; i < columns; i++) tabstops[i] = (i%8 == 0 && i != 0); +} + +/*! + This behaves either as IND (Screen::Index) or as NEL (Screen::NextLine) + depending on the NewLine Mode (LNM). This mode also + affects the key sequence returned for newline ([CR]LF). +*/ + +void Screen::NewLine() +{ + if (getMode(MODE_NewLine)) Return(); + index(); +} + +/*! put `c' literally onto the screen at the current cursor position. + + VT100 uses the convention to produce an automatic newline (am) + with the *first* character that would fall onto the next line (xenl). +*/ + +void Screen::checkSelection(int from, int to) +{ + if (sel_begin == -1) return; + int scr_TL = loc(0, hist->getLines()); + //Clear entire selection if it overlaps region [from, to] + if ( (sel_BR > (from+scr_TL) )&&(sel_TL < (to+scr_TL)) ) + { + clearSelection(); + } +} + +void Screen::ShowCharacter(unsigned short c) +{ + // Note that VT100 does wrapping BEFORE putting the character. + // This has impact on the assumption of valid cursor positions. + // We indicate the fact that a newline has to be triggered by + // putting the cursor one right to the last column of the screen. + + int w = konsole_wcwidth(c); + + if (w <= 0) + return; + + if (cuX+w > columns) { + if (getMode(MODE_Wrap)) { + lineProperties[cuY] = (LineProperty)(lineProperties[cuY] | LINE_WRAPPED); + NextLine(); + } + else + cuX = columns-w; + } + + // ensure current line vector has enough elements + int size = screenLines[cuY].size(); + if (size == 0 && cuY > 0) + { + screenLines[cuY].resize( qMax(screenLines[cuY-1].size() , cuX+w) ); + } + else + { + if (size < cuX+w) + { + screenLines[cuY].resize(cuX+w); + } + } + + if (getMode(MODE_Insert)) insertChars(w); + + lastPos = loc(cuX,cuY); + + // check if selection is still valid. + checkSelection(cuX,cuY); + + Character& currentChar = screenLines[cuY][cuX]; + + currentChar.character = c; + currentChar.foregroundColor = ef_fg; + currentChar.backgroundColor = ef_bg; + currentChar.rendition = ef_re; + + int i = 0; + int newCursorX = cuX + w--; + while(w) + { + i++; + + if ( screenLines[cuY].size() < cuX + i + 1 ) + screenLines[cuY].resize(cuX+i+1); + + Character& ch = screenLines[cuY][cuX + i]; + ch.character = 0; + ch.foregroundColor = ef_fg; + ch.backgroundColor = ef_bg; + ch.rendition = ef_re; + + w--; + } + cuX = newCursorX; +} + +void Screen::compose(const QString& /*compose*/) +{ + Q_ASSERT( 0 /*Not implemented yet*/ ); + +/* if (lastPos == -1) + return; + + QChar c(image[lastPos].character); + compose.prepend(c); + //compose.compose(); ### FIXME! + image[lastPos].character = compose[0].unicode();*/ +} + +int Screen::scrolledLines() const +{ + return _scrolledLines; +} +int Screen::droppedLines() const +{ + return _droppedLines; +} +void Screen::resetDroppedLines() +{ + _droppedLines = 0; +} +void Screen::resetScrolledLines() +{ + //kDebug() << "scrolled lines reset"; + + _scrolledLines = 0; +} + +// Region commands ------------------------------------------------------------- + +void Screen::scrollUp(int n) +{ + if (n == 0) n = 1; // Default + if (tmargin == 0) addHistLine(); // hist.history + scrollUp(tmargin, n); +} + +/*! scroll up `n' lines within current region. + The `n' new lines are cleared. + \sa setRegion \sa scrollDown +*/ + +QRect Screen::lastScrolledRegion() const +{ + return _lastScrolledRegion; +} + +void Screen::scrollUp(int from, int n) +{ + if (n <= 0 || from + n > bmargin) return; + + _scrolledLines -= n; + _lastScrolledRegion = QRect(0,tmargin,columns-1,(bmargin-tmargin)); + + //FIXME: make sure `tmargin', `bmargin', `from', `n' is in bounds. + moveImage(loc(0,from),loc(0,from+n),loc(columns-1,bmargin)); + clearImage(loc(0,bmargin-n+1),loc(columns-1,bmargin),' '); +} + +void Screen::scrollDown(int n) +{ + if (n == 0) n = 1; // Default + scrollDown(tmargin, n); +} + +/*! scroll down `n' lines within current region. + The `n' new lines are cleared. + \sa setRegion \sa scrollUp +*/ + +void Screen::scrollDown(int from, int n) +{ + + //kDebug() << "Screen::scrollDown( from: " << from << " , n: " << n << ")"; + + _scrolledLines += n; + +//FIXME: make sure `tmargin', `bmargin', `from', `n' is in bounds. + if (n <= 0) return; + if (from > bmargin) return; + if (from + n > bmargin) n = bmargin - from; + moveImage(loc(0,from+n),loc(0,from),loc(columns-1,bmargin-n)); + clearImage(loc(0,from),loc(columns-1,from+n-1),' '); +} + +void Screen::setCursorYX(int y, int x) +{ + setCursorY(y); setCursorX(x); +} + +void Screen::setCursorX(int x) +{ + if (x == 0) x = 1; // Default + x -= 1; // Adjust + cuX = qMax(0,qMin(columns-1, x)); +} + +void Screen::setCursorY(int y) +{ + if (y == 0) y = 1; // Default + y -= 1; // Adjust + cuY = qMax(0,qMin(lines -1, y + (getMode(MODE_Origin) ? tmargin : 0) )); +} + +void Screen::home() +{ + cuX = 0; + cuY = 0; +} + +void Screen::Return() +{ + cuX = 0; +} + +int Screen::getCursorX() const +{ + return cuX; +} + +int Screen::getCursorY() const +{ + return cuY; +} + +// Erasing --------------------------------------------------------------------- + +/*! \section Erasing + + This group of operations erase parts of the screen contents by filling + it with spaces colored due to the current rendition settings. + + Althought the cursor position is involved in most of these operations, + it is never modified by them. +*/ + +/*! fill screen between (including) `loca' (start) and `loce' (end) with spaces. + + This is an internal helper functions. The parameter types are internal + addresses of within the screen image and make use of the way how the + screen matrix is mapped to the image vector. +*/ + +void Screen::clearImage(int loca, int loce, char c) +{ + int scr_TL=loc(0,hist->getLines()); + //FIXME: check positions + + //Clear entire selection if it overlaps region to be moved... + if ( (sel_BR > (loca+scr_TL) )&&(sel_TL < (loce+scr_TL)) ) + { + clearSelection(); + } + + int topLine = loca/columns; + int bottomLine = loce/columns; + + Character clearCh(c,cu_fg,cu_bg,DEFAULT_RENDITION); + + //if the character being used to clear the area is the same as the + //default character, the affected lines can simply be shrunk. + bool isDefaultCh = (clearCh == Character()); + + for (int y=topLine;y<=bottomLine;y++) + { + lineProperties[y] = 0; + + int endCol = ( y == bottomLine) ? loce%columns : columns-1; + int startCol = ( y == topLine ) ? loca%columns : 0; + + QVector<Character>& line = screenLines[y]; + + if ( isDefaultCh && endCol == columns-1 ) + { + line.resize(startCol); + } + else + { + if (line.size() < endCol + 1) + line.resize(endCol+1); + + Character* data = line.data(); + for (int i=startCol;i<=endCol;i++) + data[i]=clearCh; + } + } +} + +/*! move image between (including) `sourceBegin' and `sourceEnd' to 'dest'. + + The 'dest', 'sourceBegin' and 'sourceEnd' parameters can be generated using + the loc(column,line) macro. + +NOTE: moveImage() can only move whole lines. + + This is an internal helper functions. The parameter types are internal + addresses of within the screen image and make use of the way how the + screen matrix is mapped to the image vector. +*/ + +void Screen::moveImage(int dest, int sourceBegin, int sourceEnd) +{ + //kDebug() << "moving image from (" << (sourceBegin/columns) + // << "," << (sourceEnd/columns) << ") to " << + // (dest/columns); + + Q_ASSERT( sourceBegin <= sourceEnd ); + + int lines=(sourceEnd-sourceBegin)/columns; + + //move screen image and line properties: + //the source and destination areas of the image may overlap, + //so it matters that we do the copy in the right order - + //forwards if dest < sourceBegin or backwards otherwise. + //(search the web for 'memmove implementation' for details) + if (dest < sourceBegin) + { + for (int i=0;i<=lines;i++) + { + screenLines[ (dest/columns)+i ] = screenLines[ (sourceBegin/columns)+i ]; + lineProperties[(dest/columns)+i]=lineProperties[(sourceBegin/columns)+i]; + } + } + else + { + for (int i=lines;i>=0;i--) + { + screenLines[ (dest/columns)+i ] = screenLines[ (sourceBegin/columns)+i ]; + lineProperties[(dest/columns)+i]=lineProperties[(sourceBegin/columns)+i]; + } + } + + if (lastPos != -1) + { + int diff = dest - sourceBegin; // Scroll by this amount + lastPos += diff; + if ((lastPos < 0) || (lastPos >= (lines*columns))) + lastPos = -1; + } + + // Adjust selection to follow scroll. + if (sel_begin != -1) + { + bool beginIsTL = (sel_begin == sel_TL); + int diff = dest - sourceBegin; // Scroll by this amount + int scr_TL=loc(0,hist->getLines()); + int srca = sourceBegin+scr_TL; // Translate index from screen to global + int srce = sourceEnd+scr_TL; // Translate index from screen to global + int desta = srca+diff; + int deste = srce+diff; + + if ((sel_TL >= srca) && (sel_TL <= srce)) + sel_TL += diff; + else if ((sel_TL >= desta) && (sel_TL <= deste)) + sel_BR = -1; // Clear selection (see below) + + if ((sel_BR >= srca) && (sel_BR <= srce)) + sel_BR += diff; + else if ((sel_BR >= desta) && (sel_BR <= deste)) + sel_BR = -1; // Clear selection (see below) + + if (sel_BR < 0) + { + clearSelection(); + } + else + { + if (sel_TL < 0) + sel_TL = 0; + } + + if (beginIsTL) + sel_begin = sel_TL; + else + sel_begin = sel_BR; + } +} + +void Screen::clearToEndOfScreen() +{ + clearImage(loc(cuX,cuY),loc(columns-1,lines-1),' '); +} + +void Screen::clearToBeginOfScreen() +{ + clearImage(loc(0,0),loc(cuX,cuY),' '); +} + +void Screen::clearEntireScreen() +{ + // Add entire screen to history + for (int i = 0; i < (lines-1); i++) + { + addHistLine(); scrollUp(0,1); + } + + clearImage(loc(0,0),loc(columns-1,lines-1),' '); +} + +/*! fill screen with 'E' + This is to aid screen alignment +*/ + +void Screen::helpAlign() +{ + clearImage(loc(0,0),loc(columns-1,lines-1),'E'); +} + +void Screen::clearToEndOfLine() +{ + clearImage(loc(cuX,cuY),loc(columns-1,cuY),' '); +} + +void Screen::clearToBeginOfLine() +{ + clearImage(loc(0,cuY),loc(cuX,cuY),' '); +} + +void Screen::clearEntireLine() +{ + clearImage(loc(0,cuY),loc(columns-1,cuY),' '); +} + +void Screen::setRendition(int re) +{ + cu_re |= re; + effectiveRendition(); +} + +void Screen::resetRendition(int re) +{ + cu_re &= ~re; + effectiveRendition(); +} + +void Screen::setDefaultRendition() +{ + setForeColor(COLOR_SPACE_DEFAULT,DEFAULT_FORE_COLOR); + setBackColor(COLOR_SPACE_DEFAULT,DEFAULT_BACK_COLOR); + cu_re = DEFAULT_RENDITION; + effectiveRendition(); +} + +void Screen::setForeColor(int space, int color) +{ + cu_fg = CharacterColor(space, color); + + if ( cu_fg.isValid() ) + effectiveRendition(); + else + setForeColor(COLOR_SPACE_DEFAULT,DEFAULT_FORE_COLOR); +} + +void Screen::setBackColor(int space, int color) +{ + cu_bg = CharacterColor(space, color); + + if ( cu_bg.isValid() ) + effectiveRendition(); + else + setBackColor(COLOR_SPACE_DEFAULT,DEFAULT_BACK_COLOR); +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Marking & Selection */ +/* */ +/* ------------------------------------------------------------------------- */ + +void Screen::clearSelection() +{ + sel_BR = -1; + sel_TL = -1; + sel_begin = -1; +} + +void Screen::getSelectionStart(int& column , int& line) +{ + if ( sel_TL != -1 ) + { + column = sel_TL % columns; + line = sel_TL / columns; + } + else + { + column = cuX + getHistLines(); + line = cuY + getHistLines(); + } +} +void Screen::getSelectionEnd(int& column , int& line) +{ + if ( sel_BR != -1 ) + { + column = sel_BR % columns; + line = sel_BR / columns; + } + else + { + column = cuX + getHistLines(); + line = cuY + getHistLines(); + } +} +void Screen::setSelectionStart(/*const ScreenCursor& viewCursor ,*/ const int x, const int y, const bool mode) +{ +// kDebug(1211) << "setSelBeginXY(" << x << "," << y << ")"; + sel_begin = loc(x,y); //+histCursor) ; + + /* FIXME, HACK to correct for x too far to the right... */ + if (x == columns) sel_begin--; + + sel_BR = sel_begin; + sel_TL = sel_begin; + columnmode = mode; +} + +void Screen::setSelectionEnd( const int x, const int y) +{ +// kDebug(1211) << "setSelExtentXY(" << x << "," << y << ")"; + if (sel_begin == -1) return; + int l = loc(x,y); // + histCursor); + + if (l < sel_begin) + { + sel_TL = l; + sel_BR = sel_begin; + } + else + { + /* FIXME, HACK to correct for x too far to the right... */ + if (x == columns) l--; + + sel_TL = sel_begin; + sel_BR = l; + } +} + +bool Screen::isSelected( const int x,const int y) const +{ + if (columnmode) { + int sel_Left,sel_Right; + if ( sel_TL % columns < sel_BR % columns ) { + sel_Left = sel_TL; sel_Right = sel_BR; + } else { + sel_Left = sel_BR; sel_Right = sel_TL; + } + return ( x >= sel_Left % columns ) && ( x <= sel_Right % columns ) && + ( y >= sel_TL / columns ) && ( y <= sel_BR / columns ); + //( y+histCursor >= sel_TL / columns ) && ( y+histCursor <= sel_BR / columns ); + } + else { + //int pos = loc(x,y+histCursor); + int pos = loc(x,y); + return ( pos >= sel_TL && pos <= sel_BR ); + } +} + +QString Screen::selectedText(bool preserveLineBreaks) +{ + QString result; + QTextStream stream(&result, QIODevice::ReadWrite); + + PlainTextDecoder decoder; + decoder.begin(&stream); + writeSelectionToStream(&decoder , preserveLineBreaks); + decoder.end(); + + return result; +} + +bool Screen::isSelectionValid() const +{ + return ( sel_TL >= 0 && sel_BR >= 0 ); +} + +void Screen::writeSelectionToStream(TerminalCharacterDecoder* decoder , + bool preserveLineBreaks) +{ + // do nothing if selection is invalid + if ( !isSelectionValid() ) + return; + + int top = sel_TL / columns; + int left = sel_TL % columns; + + int bottom = sel_BR / columns; + int right = sel_BR % columns; + + Q_ASSERT( top >= 0 && left >= 0 && bottom >= 0 && right >= 0 ); + + //kDebug() << "sel_TL = " << sel_TL; + //kDebug() << "columns = " << columns; + + for (int y=top;y<=bottom;y++) + { + int start = 0; + if ( y == top || columnmode ) start = left; + + int count = -1; + if ( y == bottom || columnmode ) count = right - start + 1; + + const bool appendNewLine = ( y != bottom ); + copyLineToStream( y, + start, + count, + decoder, + appendNewLine, + preserveLineBreaks ); + } +} + + +void Screen::copyLineToStream(int line , + int start, + int count, + TerminalCharacterDecoder* decoder, + bool appendNewLine, + bool preserveLineBreaks) +{ + //buffer to hold characters for decoding + //the buffer is static to avoid initialising every + //element on each call to copyLineToStream + //(which is unnecessary since all elements will be overwritten anyway) + static const int MAX_CHARS = 1024; + static Character characterBuffer[MAX_CHARS]; + + assert( count < MAX_CHARS ); + + LineProperty currentLineProperties = 0; + + //determine if the line is in the history buffer or the screen image + if (line < hist->getLines()) + { + const int lineLength = hist->getLineLen(line); + + // ensure that start position is before end of line + start = qMin(start,qMax(0,lineLength-1)); + + //retrieve line from history buffer + if (count == -1) + { + count = lineLength-start; + } + else + { + count = qMin(start+count,lineLength)-start; + } + + // safety checks + assert( start >= 0 ); + assert( count >= 0 ); + assert( (start+count) <= hist->getLineLen(line) ); + + hist->getCells(line,start,count,characterBuffer); + + if ( hist->isWrappedLine(line) ) + currentLineProperties |= LINE_WRAPPED; + } + else + { + if ( count == -1 ) + count = columns - start; + + assert( count >= 0 ); + + const int screenLine = line-hist->getLines(); + + Character* data = screenLines[screenLine].data(); + int length = screenLines[screenLine].count(); + + //retrieve line from screen image + for (int i=start;i < qMin(start+count,length);i++) + { + characterBuffer[i-start] = data[i]; + } + + // count cannot be any greater than length + count = qBound(0,count,length-start); + + Q_ASSERT( screenLine < lineProperties.count() ); + currentLineProperties |= lineProperties[screenLine]; + } + + //do not decode trailing whitespace characters + for (int i=count-1 ; i >= 0; i--) + if (QChar(characterBuffer[i].character).isSpace()) + count--; + else + break; + + // add new line character at end + const bool omitLineBreak = (currentLineProperties & LINE_WRAPPED) || + !preserveLineBreaks; + + if ( !omitLineBreak && appendNewLine && (count+1 < MAX_CHARS) ) + { + characterBuffer[count] = '\n'; + count++; + } + + //decode line and write to text stream + decoder->decodeLine( (Character*) characterBuffer , + count, currentLineProperties ); +} + +// Method below has been removed because of its reliance on 'histCursor' +// and I want to restrict the methods which have knowledge of the scroll position +// to just those which deal with selection and supplying final screen images. +// +/*void Screen::writeToStream(QTextStream* stream , TerminalCharacterDecoder* decoder) { + sel_begin = 0; + sel_BR = sel_begin; + sel_TL = sel_begin; + setSelectionEnd(columns-1,lines-1+hist->getLines()-histCursor); + + writeSelectionToStream(stream,decoder); + + clearSelection(); +}*/ + +void Screen::writeToStream(TerminalCharacterDecoder* decoder, int from, int to) +{ + sel_begin = loc(0,from); + sel_TL = sel_begin; + sel_BR = loc(columns-1,to); + writeSelectionToStream(decoder); + clearSelection(); +} + +QString Screen::getHistoryLine(int no) +{ + sel_begin = loc(0,no); + sel_TL = sel_begin; + sel_BR = loc(columns-1,no); + return selectedText(false); +} + +void Screen::addHistLine() +{ + // add line to history buffer + // we have to take care about scrolling, too... + + if (hasScroll()) + { + int oldHistLines = hist->getLines(); + + hist->addCellsVector(screenLines[0]); + hist->addLine( lineProperties[0] & LINE_WRAPPED ); + + int newHistLines = hist->getLines(); + + bool beginIsTL = (sel_begin == sel_TL); + + // If the history is full, increment the count + // of dropped lines + if ( newHistLines == oldHistLines ) + _droppedLines++; + + // Adjust selection for the new point of reference + if (newHistLines > oldHistLines) + { + if (sel_begin != -1) + { + sel_TL += columns; + sel_BR += columns; + } + } + + if (sel_begin != -1) + { + // Scroll selection in history up + int top_BR = loc(0, 1+newHistLines); + + if (sel_TL < top_BR) + sel_TL -= columns; + + if (sel_BR < top_BR) + sel_BR -= columns; + + if (sel_BR < 0) + { + clearSelection(); + } + else + { + if (sel_TL < 0) + sel_TL = 0; + } + + if (beginIsTL) + sel_begin = sel_TL; + else + sel_begin = sel_BR; + } + } + +} + +int Screen::getHistLines() +{ + return hist->getLines(); +} + +void Screen::setScroll(const HistoryType& t , bool copyPreviousScroll) +{ + clearSelection(); + + if ( copyPreviousScroll ) + hist = t.scroll(hist); + else + { + HistoryScroll* oldScroll = hist; + hist = t.scroll(0); + delete oldScroll; + } +} + +bool Screen::hasScroll() +{ + return hist->hasScroll(); +} + +const HistoryType& Screen::getScroll() +{ + return hist->getType(); +} + +void Screen::setLineProperty(LineProperty property , bool enable) +{ + if ( enable ) + { + lineProperties[cuY] = (LineProperty)(lineProperties[cuY] | property); + } + else + { + lineProperties[cuY] = (LineProperty)(lineProperties[cuY] & ~property); + } +} +void Screen::fillWithDefaultChar(Character* dest, int count) +{ + for (int i=0;i<count;i++) + dest[i] = defaultChar; +}
new file mode 100644 --- /dev/null +++ b/gui//Screen.h @@ -0,0 +1,662 @@ +/* + This file is part of Konsole, KDE's terminal. + + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef SCREEN_H +#define SCREEN_H + +// Qt +#include <QtCore/QRect> +#include <QtCore/QTextStream> +#include <QtCore/QVarLengthArray> + +// Konsole +#include "Character.h" +#include "History.h" + +#define MODE_Origin 0 +#define MODE_Wrap 1 +#define MODE_Insert 2 +#define MODE_Screen 3 +#define MODE_Cursor 4 +#define MODE_NewLine 5 +#define MODES_SCREEN 6 + +namespace Konsole +{ + +/*! +*/ +struct ScreenParm +{ + int mode[MODES_SCREEN]; +}; + +class TerminalCharacterDecoder; + +/** + \brief An image of characters with associated attributes. + + The terminal emulation ( Emulation ) receives a serial stream of + characters from the program currently running in the terminal. + From this stream it creates an image of characters which is ultimately + rendered by the display widget ( TerminalDisplay ). Some types of emulation + may have more than one screen image. + + getImage() is used to retrieve the currently visible image + which is then used by the display widget to draw the output from the + terminal. + + The number of lines of output history which are kept in addition to the current + screen image depends on the history scroll being used to store the output. + The scroll is specified using setScroll() + The output history can be retrieved using writeToStream() + + The screen image has a selection associated with it, specified using + setSelectionStart() and setSelectionEnd(). The selected text can be retrieved + using selectedText(). When getImage() is used to retrieve the the visible image, + characters which are part of the selection have their colours inverted. +*/ +class Screen +{ +public: + /** Construct a new screen image of size @p lines by @p columns. */ + Screen(int lines, int columns); + ~Screen(); + + // VT100/2 Operations + // Cursor Movement + + /** Move the cursor up by @p n lines. */ + void cursorUp (int n); + /** Move the cursor down by @p n lines. */ + void cursorDown (int n); + /** Move the cursor to the left by @p n columns. */ + void cursorLeft (int n); + /** Move the cursor to the right by @p n columns. */ + void cursorRight (int n); + /** Position the cursor on line @p y. */ + void setCursorY (int y); + /** Position the cursor at column @p x. */ + void setCursorX (int x); + /** Position the cursor at line @p y, column @p x. */ + void setCursorYX (int y, int x); + /** + * Sets the margins for scrolling the screen. + * + * @param topLine The top line of the new scrolling margin. + * @param bottomLine The bottom line of the new scrolling margin. + */ + void setMargins (int topLine , int bottomLine); + /** Returns the top line of the scrolling region. */ + int topMargin() const; + /** Returns the bottom line of the scrolling region. */ + int bottomMargin() const; + + /** + * Resets the scrolling margins back to the top and bottom lines + * of the screen. + */ + void setDefaultMargins(); + + /** + * Moves the cursor down one line, if the MODE_NewLine mode + * flag is enabled then the cursor is returned to the leftmost + * column first. + * + * Equivalent to NextLine() if the MODE_NewLine flag is set + * or index() otherwise. + */ + void NewLine (); + /** + * Moves the cursor down one line and positions it at the beginning + * of the line. + */ + void NextLine (); + + /** + * Move the cursor down one line. If the cursor is on the bottom + * line of the scrolling region (as returned by bottomMargin()) the + * scrolling region is scrolled up by one line instead. + */ + void index (); + /** + * Move the cursor up one line. If the cursor is on the top line + * of the scrolling region (as returned by topMargin()) the scrolling + * region is scrolled down by one line instead. + */ + void reverseIndex(); + + /** + * Scroll the scrolling region of the screen up by @p n lines. + * The scrolling region is initially the whole screen, but can be changed + * using setMargins() + */ + void scrollUp(int n); + /** + * Scroll the scrolling region of the screen down by @p n lines. + * The scrolling region is initially the whole screen, but can be changed + * using setMargins() + */ + void scrollDown(int n); + + /** + * Moves the cursor to the beginning of the current line. + * Equivalent to setCursorX(0) + */ + void Return (); + /** + * Moves the cursor one column to the left and erases the character + * at the new cursor position. + */ + void BackSpace (); + /** + * Moves the cursor @p n tab-stops to the right. + */ + void Tabulate (int n = 1); + /** + * Moves the cursor @p n tab-stops to the left. + */ + void backTabulate(int n); + + // Editing + + /** + * Erase @p n characters beginning from the current cursor position. + * This is equivalent to over-writing @p n characters starting with the current + * cursor position with spaces. + * If @p n is 0 then one character is erased. + */ + void eraseChars (int n); + /** + * Delete @p n characters beginning from the current cursor position. + * If @p n is 0 then one character is deleted. + */ + void deleteChars (int n); + /** + * Insert @p n blank characters beginning from the current cursor position. + * The position of the cursor is not altered. + * If @p n is 0 then one character is inserted. + */ + void insertChars (int n); + /** + * Removes @p n lines beginning from the current cursor position. + * The position of the cursor is not altered. + * If @p n is 0 then one line is removed. + */ + void deleteLines (int n); + /** + * Inserts @p lines beginning from the current cursor position. + * The position of the cursor is not altered. + * If @p n is 0 then one line is inserted. + */ + void insertLines (int n); + /** Clears all the tab stops. */ + void clearTabStops(); + /** Sets or removes a tab stop at the cursor's current column. */ + void changeTabStop(bool set); + + /** Resets (clears) the specified screen @p mode. */ + void resetMode (int mode); + /** Sets (enables) the specified screen @p mode. */ + void setMode (int mode); + /** + * Saves the state of the specified screen @p mode. It can be restored + * using restoreMode() + */ + void saveMode (int mode); + /** Restores the state of a screen @p mode saved by calling saveMode() */ + void restoreMode (int mode); + /** Returns whether the specified screen @p mode is enabled or not .*/ + bool getMode (int mode) const; + + /** + * Saves the current position and appearence (text color and style) of the cursor. + * It can be restored by calling restoreCursor() + */ + void saveCursor (); + /** Restores the position and appearence of the cursor. See saveCursor() */ + void restoreCursor(); + + /** Clear the whole screen, moving the current screen contents into the history first. */ + void clearEntireScreen(); + /** + * Clear the area of the screen from the current cursor position to the end of + * the screen. + */ + void clearToEndOfScreen(); + /** + * Clear the area of the screen from the current cursor position to the start + * of the screen. + */ + void clearToBeginOfScreen(); + /** Clears the whole of the line on which the cursor is currently positioned. */ + void clearEntireLine(); + /** Clears from the current cursor position to the end of the line. */ + void clearToEndOfLine(); + /** Clears from the current cursor position to the beginning of the line. */ + void clearToBeginOfLine(); + + /** Fills the entire screen with the letter 'E' */ + void helpAlign (); + + /** + * Enables the given @p rendition flag. Rendition flags control the appearence + * of characters on the screen. + * + * @see Character::rendition + */ + void setRendition (int rendition); + /** + * Disables the given @p rendition flag. Rendition flags control the appearence + * of characters on the screen. + * + * @see Character::rendition + */ + void resetRendition(int rendition); + + /** + * Sets the cursor's foreground color. + * @param space The color space used by the @p color argument + * @param color The new foreground color. The meaning of this depends on + * the color @p space used. + * + * @see CharacterColor + */ + void setForeColor (int space, int color); + /** + * Sets the cursor's background color. + * @param space The color space used by the @p color argumnet. + * @param color The new background color. The meaning of this depends on + * the color @p space used. + * + * @see CharacterColor + */ + void setBackColor (int space, int color); + /** + * Resets the cursor's color back to the default and sets the + * character's rendition flags back to the default settings. + */ + void setDefaultRendition(); + + /** Returns the column which the cursor is positioned at. */ + int getCursorX() const; + /** Returns the line which the cursor is positioned on. */ + int getCursorY() const; + + /** TODO Document me */ + void clear(); + /** + * Sets the position of the cursor to the 'home' position at the top-left + * corner of the screen (0,0) + */ + void home(); + /** + * Resets the state of the screen. This resets the various screen modes + * back to their default states. The cursor style and colors are reset + * (as if setDefaultRendition() had been called) + * + * <ul> + * <li>Line wrapping is enabled.</li> + * <li>Origin mode is disabled.</li> + * <li>Insert mode is disabled.</li> + * <li>Cursor mode is enabled. TODO Document me</li> + * <li>Screen mode is disabled. TODO Document me</li> + * <li>New line mode is disabled. TODO Document me</li> + * </ul> + * + * If @p clearScreen is true then the screen contents are erased entirely, + * otherwise they are unaltered. + */ + void reset(bool clearScreen = true); + + /** + * Displays a new character at the current cursor position. + * + * If the cursor is currently positioned at the right-edge of the screen and + * line wrapping is enabled then the character is added at the start of a new + * line below the current one. + * + * If the MODE_Insert screen mode is currently enabled then the character + * is inserted at the current cursor position, otherwise it will replace the + * character already at the current cursor position. + */ + void ShowCharacter(unsigned short c); + + // Do composition with last shown character FIXME: Not implemented yet for KDE 4 + void compose(const QString& compose); + + /** + * Resizes the image to a new fixed size of @p new_lines by @p new_columns. + * In the case that @p new_columns is smaller than the current number of columns, + * existing lines are not truncated. This prevents characters from being lost + * if the terminal display is resized smaller and then larger again. + * + * (note that in versions of Konsole prior to KDE 4, existing lines were + * truncated when making the screen image smaller) + */ + void resizeImage(int new_lines, int new_columns); + + /** + * Returns the current screen image. + * The result is an array of Characters of size [getLines()][getColumns()] which + * must be freed by the caller after use. + * + * @param dest Buffer to copy the characters into + * @param size Size of @p dest in Characters + * @param startLine Index of first line to copy + * @param endLine Index of last line to copy + */ + void getImage( Character* dest , int size , int startLine , int endLine ) const; + + /** + * Returns the additional attributes associated with lines in the image. + * The most important attribute is LINE_WRAPPED which specifies that the + * line is wrapped, + * other attributes control the size of characters in the line. + */ + QVector<LineProperty> getLineProperties( int startLine , int endLine ) const; + + + /** Return the number of lines. */ + int getLines() { return lines; } + /** Return the number of columns. */ + int getColumns() { return columns; } + /** Return the number of lines in the history buffer. */ + int getHistLines (); + /** + * Sets the type of storage used to keep lines in the history. + * If @p copyPreviousScroll is true then the contents of the previous + * history buffer are copied into the new scroll. + */ + void setScroll(const HistoryType& , bool copyPreviousScroll = true); + /** Returns the type of storage used to keep lines in the history. */ + const HistoryType& getScroll(); + /** + * Returns true if this screen keeps lines that are scrolled off the screen + * in a history buffer. + */ + bool hasScroll(); + + /** + * Sets the start of the selection. + * + * @param column The column index of the first character in the selection. + * @param line The line index of the first character in the selection. + * @param columnmode True if the selection is in column mode. + */ + void setSelectionStart(const int column, const int line, const bool columnmode); + + /** + * Sets the end of the current selection. + * + * @param column The column index of the last character in the selection. + * @param line The line index of the last character in the selection. + */ + void setSelectionEnd(const int column, const int line); + + /** + * Retrieves the start of the selection or the cursor position if there + * is no selection. + */ + void getSelectionStart(int& column , int& line); + + /** + * Retrieves the end of the selection or the cursor position if there + * is no selection. + */ + void getSelectionEnd(int& column , int& line); + + /** Clears the current selection */ + void clearSelection(); + + void setBusySelecting(bool busy) { sel_busy = busy; } + + /** + * Returns true if the character at (@p column, @p line) is part of the + * current selection. + */ + bool isSelected(const int column,const int line) const; + + /** + * Convenience method. Returns the currently selected text. + * @param preserveLineBreaks Specifies whether new line characters should + * be inserted into the returned text at the end of each terminal line. + */ + QString selectedText(bool preserveLineBreaks); + + /** + * Copies part of the output to a stream. + * + * @param decoder A decoder which coverts terminal characters into text + * @param from The first line in the history to retrieve + * @param to The last line in the history to retrieve + */ + void writeToStream(TerminalCharacterDecoder* decoder, int from, int to); + + /** + * Sets the selection to line @p no in the history and returns + * the text of that line from the history buffer. + */ + QString getHistoryLine(int no); + + /** + * Copies the selected characters, set using @see setSelBeginXY and @see setSelExtentXY + * into a stream. + * + * @param decoder A decoder which converts terminal characters into text. + * PlainTextDecoder is the most commonly used decoder which coverts characters + * into plain text with no formatting. + * @param preserveLineBreaks Specifies whether new line characters should + * be inserted into the returned text at the end of each terminal line. + */ + void writeSelectionToStream(TerminalCharacterDecoder* decoder , bool + preserveLineBreaks = true); + + /** TODO Document me */ + void checkSelection(int from, int to); + + /** + * Sets or clears an attribute of the current line. + * + * @param property The attribute to set or clear + * Possible properties are: + * LINE_WRAPPED: Specifies that the line is wrapped. + * LINE_DOUBLEWIDTH: Specifies that the characters in the current line should be double the normal width. + * LINE_DOUBLEHEIGHT:Specifies that the characters in the current line should be double the normal height. + * Double-height lines are formed of two lines containing the same characters, + * with both having the LINE_DOUBLEHEIGHT attribute. This allows other parts of the + * code to work on the assumption that all lines are the same height. + * + * @param enable true to apply the attribute to the current line or false to remove it + */ + void setLineProperty(LineProperty property , bool enable); + + + /** + * Returns the number of lines that the image has been scrolled up or down by, + * since the last call to resetScrolledLines(). + * + * a positive return value indicates that the image has been scrolled up, + * a negative return value indicates that the image has been scrolled down. + */ + int scrolledLines() const; + + /** + * Returns the region of the image which was last scrolled. + * + * This is the area of the image from the top margin to the + * bottom margin when the last scroll occurred. + */ + QRect lastScrolledRegion() const; + + /** + * Resets the count of the number of lines that the image has been scrolled up or down by, + * see scrolledLines() + */ + void resetScrolledLines(); + + /** + * Returns the number of lines of output which have been + * dropped from the history since the last call + * to resetDroppedLines() + * + * If the history is not unlimited then it will drop + * the oldest lines of output if new lines are added when + * it is full. + */ + int droppedLines() const; + + /** + * Resets the count of the number of lines dropped from + * the history. + */ + void resetDroppedLines(); + + /** + * Fills the buffer @p dest with @p count instances of the default (ie. blank) + * Character style. + */ + static void fillWithDefaultChar(Character* dest, int count); + +private: + + //copies a line of text from the screen or history into a stream using a + //specified character decoder + //line - the line number to copy, from 0 (the earliest line in the history) up to + // hist->getLines() + lines - 1 + //start - the first column on the line to copy + //count - the number of characters on the line to copy + //decoder - a decoder which coverts terminal characters (an Character array) into text + //appendNewLine - if true a new line character (\n) is appended to the end of the line + void copyLineToStream(int line, + int start, + int count, + TerminalCharacterDecoder* decoder, + bool appendNewLine, + bool preserveLineBreaks); + + //fills a section of the screen image with the character 'c' + //the parameters are specified as offsets from the start of the screen image. + //the loc(x,y) macro can be used to generate these values from a column,line pair. + void clearImage(int loca, int loce, char c); + + //move screen image between 'sourceBegin' and 'sourceEnd' to 'dest'. + //the parameters are specified as offsets from the start of the screen image. + //the loc(x,y) macro can be used to generate these values from a column,line pair. + void moveImage(int dest, int sourceBegin, int sourceEnd); + + void scrollUp(int from, int i); + void scrollDown(int from, int i); + + void addHistLine(); + + void initTabStops(); + + void effectiveRendition(); + void reverseRendition(Character& p) const; + + bool isSelectionValid() const; + + // copies 'count' lines from the screen buffer into 'dest', + // starting from 'startLine', where 0 is the first line in the screen buffer + void copyFromScreen(Character* dest, int startLine, int count) const; + // copies 'count' lines from the history buffer into 'dest', + // starting from 'startLine', where 0 is the first line in the history + void copyFromHistory(Character* dest, int startLine, int count) const; + + + // screen image ---------------- + int lines; + int columns; + + typedef QVector<Character> ImageLine; // [0..columns] + ImageLine* screenLines; // [lines] + + int _scrolledLines; + QRect _lastScrolledRegion; + + int _droppedLines; + + QVarLengthArray<LineProperty,64> lineProperties; + + // history buffer --------------- + HistoryScroll *hist; + + // cursor location + int cuX; + int cuY; + + // cursor color and rendition info + CharacterColor cu_fg; // foreground + CharacterColor cu_bg; // background + quint8 cu_re; // rendition + + // margins ---------------- + int tmargin; // top margin + int bmargin; // bottom margin + + // states ---------------- + ScreenParm currParm; + + // ---------------------------- + + bool* tabstops; + + // selection ------------------- + int sel_begin; // The first location selected. + int sel_TL; // TopLeft Location. + int sel_BR; // Bottom Right Location. + bool sel_busy; // Busy making a selection. + bool columnmode; // Column selection mode + + // effective colors and rendition ------------ + CharacterColor ef_fg; // These are derived from + CharacterColor ef_bg; // the cu_* variables above + quint8 ef_re; // to speed up operation + + // + // save cursor, rendition & states ------------ + // + + // cursor location + int sa_cuX; + int sa_cuY; + + // rendition info + quint8 sa_cu_re; + CharacterColor sa_cu_fg; + CharacterColor sa_cu_bg; + + // last position where we added a character + int lastPos; + + // modes + ScreenParm saveParm; + + static Character defaultChar; +}; + +} + +#endif // SCREEN_H
new file mode 100644 --- /dev/null +++ b/gui//ScreenWindow.cpp @@ -0,0 +1,296 @@ +/* + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +// Own +#include "ScreenWindow.h" + +// Qt +#include <QtCore> + +// Konsole +#include "Screen.h" + +using namespace Konsole; + +ScreenWindow::ScreenWindow(QObject* parent) + : QObject(parent) + , _windowBuffer(0) + , _windowBufferSize(0) + , _bufferNeedsUpdate(true) + , _windowLines(1) + , _currentLine(0) + , _trackOutput(true) + , _scrollCount(0) +{ +} +ScreenWindow::~ScreenWindow() +{ + delete[] _windowBuffer; +} +void ScreenWindow::setScreen(Screen* screen) +{ + Q_ASSERT( screen ); + + _screen = screen; +} + +Screen* ScreenWindow::screen() const +{ + return _screen; +} + +Character* ScreenWindow::getImage() +{ + // reallocate internal buffer if the window size has changed + int size = windowLines() * windowColumns(); + if (_windowBuffer == 0 || _windowBufferSize != size) + { + delete[] _windowBuffer; + _windowBufferSize = size; + _windowBuffer = new Character[size]; + _bufferNeedsUpdate = true; + } + + if (!_bufferNeedsUpdate) + return _windowBuffer; + + _screen->getImage(_windowBuffer,size, + currentLine(),endWindowLine()); + + // this window may look beyond the end of the screen, in which + // case there will be an unused area which needs to be filled + // with blank characters + fillUnusedArea(); + + _bufferNeedsUpdate = false; + return _windowBuffer; +} + +void ScreenWindow::fillUnusedArea() +{ + int screenEndLine = _screen->getHistLines() + _screen->getLines() - 1; + int windowEndLine = currentLine() + windowLines() - 1; + + int unusedLines = windowEndLine - screenEndLine; + int charsToFill = unusedLines * windowColumns(); + + Screen::fillWithDefaultChar(_windowBuffer + _windowBufferSize - charsToFill,charsToFill); +} + +// return the index of the line at the end of this window, or if this window +// goes beyond the end of the screen, the index of the line at the end +// of the screen. +// +// when passing a line number to a Screen method, the line number should +// never be more than endWindowLine() +// +int ScreenWindow::endWindowLine() const +{ + return qMin(currentLine() + windowLines() - 1, + lineCount() - 1); +} +QVector<LineProperty> ScreenWindow::getLineProperties() +{ + QVector<LineProperty> result = _screen->getLineProperties(currentLine(),endWindowLine()); + + if (result.count() != windowLines()) + result.resize(windowLines()); + + return result; +} + +QString ScreenWindow::selectedText( bool preserveLineBreaks ) const +{ + return _screen->selectedText( preserveLineBreaks ); +} + +void ScreenWindow::getSelectionStart( int& column , int& line ) +{ + _screen->getSelectionStart(column,line); + line -= currentLine(); +} +void ScreenWindow::getSelectionEnd( int& column , int& line ) +{ + _screen->getSelectionEnd(column,line); + line -= currentLine(); +} +void ScreenWindow::setSelectionStart( int column , int line , bool columnMode ) +{ + _screen->setSelectionStart( column , qMin(line + currentLine(),endWindowLine()) , columnMode); + + _bufferNeedsUpdate = true; + emit selectionChanged(); +} + +void ScreenWindow::setSelectionEnd( int column , int line ) +{ + _screen->setSelectionEnd( column , qMin(line + currentLine(),endWindowLine()) ); + + _bufferNeedsUpdate = true; + emit selectionChanged(); +} + +bool ScreenWindow::isSelected( int column , int line ) +{ + return _screen->isSelected( column , qMin(line + currentLine(),endWindowLine()) ); +} + +void ScreenWindow::clearSelection() +{ + _screen->clearSelection(); + + emit selectionChanged(); +} + +void ScreenWindow::setWindowLines(int lines) +{ + Q_ASSERT(lines > 0); + _windowLines = lines; +} +int ScreenWindow::windowLines() const +{ + return _windowLines; +} + +int ScreenWindow::windowColumns() const +{ + return _screen->getColumns(); +} + +int ScreenWindow::lineCount() const +{ + return _screen->getHistLines() + _screen->getLines(); +} + +int ScreenWindow::columnCount() const +{ + return _screen->getColumns(); +} + +QPoint ScreenWindow::cursorPosition() const +{ + QPoint position; + + position.setX( _screen->getCursorX() ); + position.setY( _screen->getCursorY() ); + + return position; +} + +int ScreenWindow::currentLine() const +{ + return qBound(0,_currentLine,lineCount()-windowLines()); +} + +void ScreenWindow::scrollBy( RelativeScrollMode mode , int amount ) +{ + if ( mode == ScrollLines ) + { + scrollTo( currentLine() + amount ); + } + else if ( mode == ScrollPages ) + { + scrollTo( currentLine() + amount * ( windowLines() / 2 ) ); + } +} + +bool ScreenWindow::atEndOfOutput() const +{ + return currentLine() == (lineCount()-windowLines()); +} + +void ScreenWindow::scrollTo( int line ) +{ + int maxCurrentLineNumber = lineCount() - windowLines(); + line = qBound(0,line,maxCurrentLineNumber); + + const int delta = line - _currentLine; + _currentLine = line; + + // keep track of number of lines scrolled by, + // this can be reset by calling resetScrollCount() + _scrollCount += delta; + + _bufferNeedsUpdate = true; + + emit scrolled(_currentLine); +} + +void ScreenWindow::setTrackOutput(bool trackOutput) +{ + _trackOutput = trackOutput; +} + +bool ScreenWindow::trackOutput() const +{ + return _trackOutput; +} + +int ScreenWindow::scrollCount() const +{ + return _scrollCount; +} + +void ScreenWindow::resetScrollCount() +{ + _scrollCount = 0; +} + +QRect ScreenWindow::scrollRegion() const +{ + bool equalToScreenSize = windowLines() == _screen->getLines(); + + if ( atEndOfOutput() && equalToScreenSize ) + return _screen->lastScrolledRegion(); + else + return QRect(0,0,windowColumns(),windowLines()); +} + +void ScreenWindow::notifyOutputChanged() +{ + // move window to the bottom of the screen and update scroll count + // if this window is currently tracking the bottom of the screen + if ( _trackOutput ) + { + _scrollCount -= _screen->scrolledLines(); + _currentLine = qMax(0,_screen->getHistLines() - (windowLines()-_screen->getLines())); + } + else + { + // if the history is not unlimited then it may + // have run out of space and dropped the oldest + // lines of output - in this case the screen + // window's current line number will need to + // be adjusted - otherwise the output will scroll + _currentLine = qMax(0,_currentLine - + _screen->droppedLines()); + + // ensure that the screen window's current position does + // not go beyond the bottom of the screen + _currentLine = qMin( _currentLine , _screen->getHistLines() ); + } + + _bufferNeedsUpdate = true; + + emit outputChanged(); +} + +//#include "moc_ScreenWindow.cpp"
new file mode 100644 --- /dev/null +++ b/gui//ScreenWindow.h @@ -0,0 +1,256 @@ +/* + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef SCREENWINDOW_H +#define SCREENWINDOW_H + +// Qt +#include <QtCore/QObject> +#include <QtCore/QPoint> +#include <QtCore/QRect> + +// Konsole +#include "Character.h" + +namespace Konsole +{ + +class Screen; + +/** + * Provides a window onto a section of a terminal screen. + * This window can then be rendered by a terminal display widget ( TerminalDisplay ). + * + * To use the screen window, create a new ScreenWindow() instance and associated it with + * a terminal screen using setScreen(). + * Use the scrollTo() method to scroll the window up and down on the screen. + * Call the getImage() method to retrieve the character image which is currently visible in the window. + * + * setTrackOutput() controls whether the window moves to the bottom of the associated screen when new + * lines are added to it. + * + * Whenever the output from the underlying screen is changed, the notifyOutputChanged() slot should + * be called. This in turn will update the window's position and emit the outputChanged() signal + * if necessary. + */ +class ScreenWindow : public QObject +{ +Q_OBJECT + +public: + /** + * Constructs a new screen window with the given parent. + * A screen must be specified by calling setScreen() before calling getImage() or getLineProperties(). + * + * You should not call this constructor directly, instead use the Emulation::createWindow() method + * to create a window on the emulation which you wish to view. This allows the emulation + * to notify the window when the associated screen has changed and synchronize selection updates + * between all views on a session. + */ + ScreenWindow(QObject* parent = 0); + virtual ~ScreenWindow(); + + /** Sets the screen which this window looks onto */ + void setScreen(Screen* screen); + /** Returns the screen which this window looks onto */ + Screen* screen() const; + + /** + * Returns the image of characters which are currently visible through this window + * onto the screen. + * + * The buffer is managed by the ScreenWindow instance and does not need to be + * deleted by the caller. + */ + Character* getImage(); + + /** + * Returns the line attributes associated with the lines of characters which + * are currently visible through this window + */ + QVector<LineProperty> getLineProperties(); + + /** + * Returns the number of lines which the region of the window + * specified by scrollRegion() has been scrolled by since the last call + * to resetScrollCount(). scrollRegion() is in most cases the + * whole window, but will be a smaller area in, for example, applications + * which provide split-screen facilities. + * + * This is not guaranteed to be accurate, but allows views to optimise + * rendering by reducing the amount of costly text rendering that + * needs to be done when the output is scrolled. + */ + int scrollCount() const; + + /** + * Resets the count of scrolled lines returned by scrollCount() + */ + void resetScrollCount(); + + /** + * Returns the area of the window which was last scrolled, this is + * usually the whole window area. + * + * Like scrollCount(), this is not guaranteed to be accurate, + * but allows views to optimise rendering. + */ + QRect scrollRegion() const; + + /** + * Sets the start of the selection to the given @p line and @p column within + * the window. + */ + void setSelectionStart( int column , int line , bool columnMode ); + /** + * Sets the end of the selection to the given @p line and @p column within + * the window. + */ + void setSelectionEnd( int column , int line ); + /** + * Retrieves the start of the selection within the window. + */ + void getSelectionStart( int& column , int& line ); + /** + * Retrieves the end of the selection within the window. + */ + void getSelectionEnd( int& column , int& line ); + /** + * Returns true if the character at @p line , @p column is part of the selection. + */ + bool isSelected( int column , int line ); + /** + * Clears the current selection + */ + void clearSelection(); + + /** Sets the number of lines in the window */ + void setWindowLines(int lines); + /** Returns the number of lines in the window */ + int windowLines() const; + /** Returns the number of columns in the window */ + int windowColumns() const; + + /** Returns the total number of lines in the screen */ + int lineCount() const; + /** Returns the total number of columns in the screen */ + int columnCount() const; + + /** Returns the index of the line which is currently at the top of this window */ + int currentLine() const; + + /** + * Returns the position of the cursor + * within the window. + */ + QPoint cursorPosition() const; + + /** + * Convenience method. Returns true if the window is currently at the bottom + * of the screen. + */ + bool atEndOfOutput() const; + + /** Scrolls the window so that @p line is at the top of the window */ + void scrollTo( int line ); + + enum RelativeScrollMode + { + ScrollLines, + ScrollPages + }; + + /** + * Scrolls the window relative to its current position on the screen. + * + * @param mode Specifies whether @p amount refers to the number of lines or the number + * of pages to scroll. + * @param amount The number of lines or pages ( depending on @p mode ) to scroll by. If + * this number is positive, the view is scrolled down. If this number is negative, the view + * is scrolled up. + */ + void scrollBy( RelativeScrollMode mode , int amount ); + + /** + * Specifies whether the window should automatically move to the bottom + * of the screen when new output is added. + * + * If this is set to true, the window will be moved to the bottom of the associated screen ( see + * screen() ) when the notifyOutputChanged() method is called. + */ + void setTrackOutput(bool trackOutput); + /** + * Returns whether the window automatically moves to the bottom of the screen as + * new output is added. See setTrackOutput() + */ + bool trackOutput() const; + + /** + * Returns the text which is currently selected. + * + * @param preserveLineBreaks See Screen::selectedText() + */ + QString selectedText( bool preserveLineBreaks ) const; + +public slots: + /** + * Notifies the window that the contents of the associated terminal screen have changed. + * This moves the window to the bottom of the screen if trackOutput() is true and causes + * the outputChanged() signal to be emitted. + */ + void notifyOutputChanged(); + +signals: + /** + * Emitted when the contents of the associated terminal screen ( see screen() ) changes. + */ + void outputChanged(); + + /** + * Emitted when the screen window is scrolled to a different position. + * + * @param line The line which is now at the top of the window. + */ + void scrolled(int line); + + /** + * Emitted when the selection is changed. + */ + void selectionChanged(); + +private: + int endWindowLine() const; + void fillUnusedArea(); + + Screen* _screen; // see setScreen() , screen() + Character* _windowBuffer; + int _windowBufferSize; + bool _bufferNeedsUpdate; + + int _windowLines; + int _currentLine; // see scrollTo() , currentLine() + bool _trackOutput; // see setTrackOutput() , trackOutput() + int _scrollCount; // count of lines which the window has been scrolled by since + // the last call to resetScrollCount() +}; + +} +#endif // SCREENWINDOW_H
new file mode 100644 --- /dev/null +++ b/gui//Session.cpp @@ -0,0 +1,1021 @@ +/* + This file is part of Konsole + + Copyright (C) 2006-2007 by Robert Knight <robertknight@gmail.com> + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +// Own +#include "Session.h" + +// Standard +#include <assert.h> +#include <stdlib.h> + +// Qt +#include <QtGui/QApplication> +#include <QtCore/QByteRef> +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QRegExp> +#include <QtCore/QStringList> +#include <QtCore> + +#include "Pty.h" +#include "TerminalDisplay.h" +#include "ShellCommand.h" +#include "Vt102Emulation.h" + +using namespace Konsole; + +int Session::lastSessionId = 0; + +Session::Session() : + _shellProcess(0) + , _emulation(0) + , _monitorActivity(false) + , _monitorSilence(false) + , _notifiedActivity(false) + , _autoClose(true) + , _wantedClose(false) + , _silenceSeconds(10) + , _addToUtmp(false) // disabled by default because of a bug encountered on certain systems + // which caused Konsole to hang when closing a tab and then opening a new + // one. A 'QProcess destroyed while still running' warning was being + // printed to the terminal. Likely a problem in KPty::logout() + // or KPty::login() which uses a QProcess to start /usr/bin/utempter + , _flowControl(true) + , _fullScripting(false) + , _sessionId(0) +// , _zmodemBusy(false) +// , _zmodemProc(0) +// , _zmodemProgress(0) + , _hasDarkBackground(false) +{ + //prepare DBus communication +// new SessionAdaptor(this); + _sessionId = ++lastSessionId; +// QDBusConnection::sessionBus().registerObject(QLatin1String("/Sessions/")+QString::number(_sessionId), this); + + //create teletype for I/O with shell process + _shellProcess = new Pty(); + + //create emulation backend + _emulation = new Vt102Emulation(); + + connect( _emulation, SIGNAL( titleChanged( int, const QString & ) ), + this, SLOT( setUserTitle( int, const QString & ) ) ); + connect( _emulation, SIGNAL( stateSet(int) ), + this, SLOT( activityStateSet(int) ) ); +// connect( _emulation, SIGNAL( zmodemDetected() ), this , +// SLOT( fireZModemDetected() ) ); + connect( _emulation, SIGNAL( changeTabTextColorRequest( int ) ), + this, SIGNAL( changeTabTextColorRequest( int ) ) ); + connect( _emulation, SIGNAL(profileChangeCommandReceived(const QString&)), + this, SIGNAL( profileChangeCommandReceived(const QString&)) ); + // TODO + // connect( _emulation,SIGNAL(imageSizeChanged(int,int)) , this , + // SLOT(onEmulationSizeChange(int,int)) ); + + //connect teletype to emulation backend + _shellProcess->setUtf8Mode(_emulation->utf8()); + + connect( _shellProcess,SIGNAL(receivedData(const char*,int)),this, + SLOT(onReceiveBlock(const char*,int)) ); + connect( _emulation,SIGNAL(sendData(const char*,int)),_shellProcess, + SLOT(sendData(const char*,int)) ); + connect( _emulation,SIGNAL(lockPtyRequest(bool)),_shellProcess,SLOT(lockPty(bool)) ); + connect( _emulation,SIGNAL(useUtf8Request(bool)),_shellProcess,SLOT(setUtf8Mode(bool)) ); + + + connect( _shellProcess,SIGNAL(done(int)), this, SLOT(done(int)) ); + + //setup timer for monitoring session activity + _monitorTimer = new QTimer(this); + _monitorTimer->setSingleShot(true); + connect(_monitorTimer, SIGNAL(timeout()), this, SLOT(monitorTimerDone())); +} + +WId Session::windowId() const +{ + // Returns a window ID for this session which is used + // to set the WINDOWID environment variable in the shell + // process. + // + // Sessions can have multiple views or no views, which means + // that a single ID is not always going to be accurate. + // + // If there are no views, the window ID is just 0. If + // there are multiple views, then the window ID for the + // top-level window which contains the first view is + // returned + + if ( _views.count() == 0 ) + return 0; + else + { + QWidget* window = _views.first(); + + Q_ASSERT( window ); + + while ( window->parentWidget() != 0 ) + window = window->parentWidget(); + + return window->winId(); + } +} + +void Session::setDarkBackground(bool darkBackground) +{ + _hasDarkBackground = darkBackground; +} +bool Session::hasDarkBackground() const +{ + return _hasDarkBackground; +} +bool Session::isRunning() const +{ + return _shellProcess->isRunning(); +} + +void Session::setCodec(QTextCodec* codec) +{ + emulation()->setCodec(codec); +} + +void Session::setProgram(const QString& program) +{ + _program = ShellCommand::expand(program); +} +void Session::setInitialWorkingDirectory(const QString& dir) +{ + _initialWorkingDir = ShellCommand::expand(dir); +} +void Session::setArguments(const QStringList& arguments) +{ + _arguments = ShellCommand::expand(arguments); +} + +QList<TerminalDisplay*> Session::views() const +{ + return _views; +} + +void Session::addView(TerminalDisplay* widget) +{ + Q_ASSERT( !_views.contains(widget) ); + + _views.append(widget); + + if ( _emulation != 0 ) + { + // connect emulation - view signals and slots + connect( widget , SIGNAL(keyPressedSignal(QKeyEvent*)) , _emulation , + SLOT(sendKeyEvent(QKeyEvent*)) ); + connect( widget , SIGNAL(mouseSignal(int,int,int,int)) , _emulation , + SLOT(sendMouseEvent(int,int,int,int)) ); + connect( widget , SIGNAL(sendStringToEmu(const char*)) , _emulation , + SLOT(sendString(const char*)) ); + + // allow emulation to notify view when the foreground process + // indicates whether or not it is interested in mouse signals + connect( _emulation , SIGNAL(programUsesMouseChanged(bool)) , widget , + SLOT(setUsesMouse(bool)) ); + + widget->setUsesMouse( _emulation->programUsesMouse() ); + + widget->setScreenWindow(_emulation->createWindow()); + } + + //connect view signals and slots + QObject::connect( widget ,SIGNAL(changedContentSizeSignal(int,int)),this, + SLOT(onViewSizeChange(int,int))); + + QObject::connect( widget ,SIGNAL(destroyed(QObject*)) , this , + SLOT(viewDestroyed(QObject*)) ); +//slot for close + QObject::connect(this, SIGNAL(finished()), widget, SLOT(close())); + +} + +void Session::viewDestroyed(QObject* view) +{ + TerminalDisplay* display = (TerminalDisplay*)view; + + Q_ASSERT( _views.contains(display) ); + + removeView(display); +} + +void Session::removeView(TerminalDisplay* widget) +{ + _views.removeAll(widget); + + disconnect(widget,0,this,0); + + if ( _emulation != 0 ) + { + // disconnect + // - key presses signals from widget + // - mouse activity signals from widget + // - string sending signals from widget + // + // ... and any other signals connected in addView() + disconnect( widget, 0, _emulation, 0); + + // disconnect state change signals emitted by emulation + disconnect( _emulation , 0 , widget , 0); + } + + // close the session automatically when the last view is removed + if ( _views.count() == 0 ) + { + close(); + } +} + +void Session::run() +{ + //check that everything is in place to run the session + if (_program.isEmpty()) + qDebug() << "Session::run() - program to run not set."; + if (_arguments.isEmpty()) + qDebug() << "Session::run() - no command line arguments specified."; + + // Upon a KPty error, there is no description on what that error was... + // Check to see if the given program is executable. + QString exec = QFile::encodeName(_program); + + // if 'exec' is not specified, fall back to default shell. if that + // is not set then fall back to /bin/sh + if ( exec.isEmpty() ) + exec = getenv("SHELL"); + if ( exec.isEmpty() ) + exec = "/bin/sh"; + + // if no arguments are specified, fall back to shell + QStringList arguments = _arguments.join(QChar(' ')).isEmpty() ? + QStringList() << exec : _arguments; + QString pexec = exec; + + if ( pexec.isEmpty() ) { + qDebug()<<"can not execute "<<exec<<endl; + QTimer::singleShot(1, this, SIGNAL(finished())); + return; + } + + QString cwd_save = QDir::currentPath(); + if (!_initialWorkingDir.isEmpty()) + _shellProcess->setWorkingDirectory(_initialWorkingDir); + else + _shellProcess->setWorkingDirectory(QDir::homePath()); + + _shellProcess->setXonXoff(_flowControl); + _shellProcess->setErase(_emulation->getErase()); + + // this is not strictly accurate use of the COLORFGBG variable. This does not + // tell the terminal exactly which colors are being used, but instead approximates + // the color scheme as "black on white" or "white on black" depending on whether + // the background color is deemed dark or not + QString backgroundColorHint = _hasDarkBackground ? "COLORFGBG=15;0" : "COLORFGBG=0;15"; + + int result = _shellProcess->start(QFile::encodeName(_program), + arguments, + _environment << backgroundColorHint, + windowId(), + _addToUtmp); + + if (result < 0) + { + return; + } + + _shellProcess->setWriteable(false); // We are reachable via kwrited. + + emit started(); +} + +void Session::setUserTitle( int what, const QString &caption ) +{ + //set to true if anything is actually changed (eg. old _nameTitle != new _nameTitle ) + bool modified = false; + + // (btw: what=0 changes _userTitle and icon, what=1 only icon, what=2 only _nameTitle + if ((what == 0) || (what == 2)) + { + if ( _userTitle != caption ) { + _userTitle = caption; + modified = true; + } + } + + if ((what == 0) || (what == 1)) + { + if ( _iconText != caption ) { + _iconText = caption; + modified = true; + } + } + + if (what == 11) + { + QString colorString = caption.section(';',0,0); + qDebug() << __FILE__ << __LINE__ << ": setting background colour to " << colorString; + QColor backColor = QColor(colorString); + if (backColor.isValid()){// change color via \033]11;Color\007 + if (backColor != _modifiedBackground) + { + _modifiedBackground = backColor; + + // bail out here until the code to connect the terminal display + // to the changeBackgroundColor() signal has been written + // and tested - just so we don't forget to do this. + Q_ASSERT( 0 ); + + emit changeBackgroundColorRequest(backColor); + } + } + } + + if (what == 30) + { + if ( _nameTitle != caption ) { + setTitle(Session::NameRole,caption); + return; + } + } + + if (what == 31) + { + QString cwd=caption; + cwd=cwd.replace( QRegExp("^~"), QDir::homePath() ); + emit openUrlRequest(cwd); + } + + // change icon via \033]32;Icon\007 + if (what == 32) + { + if ( _iconName != caption ) { + _iconName = caption; + + modified = true; + } + } + + if (what == 50) + { + emit profileChangeCommandReceived(caption); + return; + } + + if ( modified ) + emit titleChanged(); +} + +QString Session::userTitle() const +{ + return _userTitle; +} +void Session::setTabTitleFormat(TabTitleContext context , const QString& format) +{ + if ( context == LocalTabTitle ) + _localTabTitleFormat = format; + else if ( context == RemoteTabTitle ) + _remoteTabTitleFormat = format; +} +QString Session::tabTitleFormat(TabTitleContext context) const +{ + if ( context == LocalTabTitle ) + return _localTabTitleFormat; + else if ( context == RemoteTabTitle ) + return _remoteTabTitleFormat; + + return QString(); +} + +void Session::monitorTimerDone() +{ + //FIXME: The idea here is that the notification popup will appear to tell the user than output from + //the terminal has stopped and the popup will disappear when the user activates the session. + // + //This breaks with the addition of multiple views of a session. The popup should disappear + //when any of the views of the session becomes active + + + //FIXME: Make message text for this notification and the activity notification more descriptive. + if (_monitorSilence) { +// KNotification::event("Silence", ("Silence in session '%1'", _nameTitle), QPixmap(), +// QApplication::activeWindow(), +// KNotification::CloseWhenWidgetActivated); + emit stateChanged(NOTIFYSILENCE); + } + else + { + emit stateChanged(NOTIFYNORMAL); + } + + _notifiedActivity=false; +} + +void Session::activityStateSet(int state) +{ + if (state==NOTIFYBELL) + { + QString s; s.sprintf("Bell in session '%s'",_nameTitle.toAscii().data()); + + emit bellRequest( s ); + } + else if (state==NOTIFYACTIVITY) + { + if (_monitorSilence) { + _monitorTimer->start(_silenceSeconds*1000); + } + + if ( _monitorActivity ) { + //FIXME: See comments in Session::monitorTimerDone() + if (!_notifiedActivity) { +// KNotification::event("Activity", ("Activity in session '%1'", _nameTitle), QPixmap(), +// QApplication::activeWindow(), +// KNotification::CloseWhenWidgetActivated); + _notifiedActivity=true; + } + } + } + + if ( state==NOTIFYACTIVITY && !_monitorActivity ) + state = NOTIFYNORMAL; + if ( state==NOTIFYSILENCE && !_monitorSilence ) + state = NOTIFYNORMAL; + + emit stateChanged(state); +} + +void Session::onViewSizeChange(int /*height*/, int /*width*/) +{ + updateTerminalSize(); +} +void Session::onEmulationSizeChange(int lines , int columns) +{ + setSize( QSize(lines,columns) ); +} + +void Session::updateTerminalSize() +{ + QListIterator<TerminalDisplay*> viewIter(_views); + + int minLines = -1; + int minColumns = -1; + + // minimum number of lines and columns that views require for + // their size to be taken into consideration ( to avoid problems + // with new view widgets which haven't yet been set to their correct size ) + const int VIEW_LINES_THRESHOLD = 2; + const int VIEW_COLUMNS_THRESHOLD = 2; + + //select largest number of lines and columns that will fit in all visible views + while ( viewIter.hasNext() ) + { + TerminalDisplay* view = viewIter.next(); + if ( view->isHidden() == false && + view->lines() >= VIEW_LINES_THRESHOLD && + view->columns() >= VIEW_COLUMNS_THRESHOLD ) + { + minLines = (minLines == -1) ? view->lines() : qMin( minLines , view->lines() ); + minColumns = (minColumns == -1) ? view->columns() : qMin( minColumns , view->columns() ); + } + } + + // backend emulation must have a _terminal of at least 1 column x 1 line in size + if ( minLines > 0 && minColumns > 0 ) + { + _emulation->setImageSize( minLines , minColumns ); + _shellProcess->setWindowSize( minLines , minColumns ); + } +} + +void Session::refresh() +{ + // attempt to get the shell process to redraw the display + // + // this requires the program running in the shell + // to cooperate by sending an update in response to + // a window size change + // + // the window size is changed twice, first made slightly larger and then + // resized back to its normal size so that there is actually a change + // in the window size (some shells do nothing if the + // new and old sizes are the same) + // + // if there is a more 'correct' way to do this, please + // send an email with method or patches to konsole-devel@kde.org + + const QSize existingSize = _shellProcess->windowSize(); + _shellProcess->setWindowSize(existingSize.height(),existingSize.width()+1); + _shellProcess->setWindowSize(existingSize.height(),existingSize.width()); +} + +bool Session::sendSignal(int signal) +{ + return _shellProcess->kill(signal); +} + +void Session::close() +{ + _autoClose = true; + _wantedClose = true; + if (!_shellProcess->isRunning() || !sendSignal(SIGHUP)) + { + // Forced close. + QTimer::singleShot(1, this, SIGNAL(finished())); + } +} + +void Session::sendText(const QString &text) const +{ + _emulation->sendText(text); +} + +Session::~Session() +{ + delete _emulation; + delete _shellProcess; +// delete _zmodemProc; +} + +void Session::setProfileKey(const QString& key) +{ + _profileKey = key; + emit profileChanged(key); +} +QString Session::profileKey() const { return _profileKey; } + +void Session::done(int exitStatus) +{ + if (!_autoClose) + { + _userTitle = ("<Finished>"); + emit titleChanged(); + return; + } + if (!_wantedClose && (exitStatus || _shellProcess->signalled())) + { + QString message; + + if (_shellProcess->normalExit()) + message.sprintf ("Session '%s' exited with status %d.", _nameTitle.toAscii().data(), exitStatus); + else if (_shellProcess->signalled()) + { + if (_shellProcess->coreDumped()) + { + + message.sprintf("Session '%s' exited with signal %d and dumped core.", _nameTitle.toAscii().data(), _shellProcess->exitSignal()); + } + else { + message.sprintf("Session '%s' exited with signal %d.", _nameTitle.toAscii().data(), _shellProcess->exitSignal()); + } + } + else + message.sprintf ("Session '%s' exited unexpectedly.", _nameTitle.toAscii().data()); + + //FIXME: See comments in Session::monitorTimerDone() +// KNotification::event("Finished", message , QPixmap(), +// QApplication::activeWindow(), +// KNotification::CloseWhenWidgetActivated); + } + emit finished(); +} + +Emulation* Session::emulation() const +{ + return _emulation; +} + +QString Session::keyBindings() const +{ + return _emulation->keyBindings(); +} + +QStringList Session::environment() const +{ + return _environment; +} + +void Session::setEnvironment(const QStringList& environment) +{ + _environment = environment; +} + +int Session::sessionId() const +{ + return _sessionId; +} + +void Session::setKeyBindings(const QString &id) +{ + _emulation->setKeyBindings(id); +} + +void Session::setTitle(TitleRole role , const QString& newTitle) +{ + if ( title(role) != newTitle ) + { + if ( role == NameRole ) + _nameTitle = newTitle; + else if ( role == DisplayedTitleRole ) + _displayTitle = newTitle; + + emit titleChanged(); + } +} + +QString Session::title(TitleRole role) const +{ + if ( role == NameRole ) + return _nameTitle; + else if ( role == DisplayedTitleRole ) + return _displayTitle; + else + return QString(); +} + +void Session::setIconName(const QString& iconName) +{ + if ( iconName != _iconName ) + { + _iconName = iconName; + emit titleChanged(); + } +} + +void Session::setIconText(const QString& iconText) +{ + _iconText = iconText; + //kDebug(1211)<<"Session setIconText " << _iconText; +} + +QString Session::iconName() const +{ + return _iconName; +} + +QString Session::iconText() const +{ + return _iconText; +} + +void Session::setHistoryType(const HistoryType &hType) +{ + _emulation->setHistory(hType); +} + +const HistoryType& Session::historyType() const +{ + return _emulation->history(); +} + +void Session::clearHistory() +{ + _emulation->clearHistory(); +} + +QStringList Session::arguments() const +{ + return _arguments; +} + +QString Session::program() const +{ + return _program; +} + +// unused currently +bool Session::isMonitorActivity() const { return _monitorActivity; } +// unused currently +bool Session::isMonitorSilence() const { return _monitorSilence; } + +void Session::setMonitorActivity(bool _monitor) +{ + _monitorActivity=_monitor; + _notifiedActivity=false; + + activityStateSet(NOTIFYNORMAL); +} + +void Session::setMonitorSilence(bool _monitor) +{ + if (_monitorSilence==_monitor) + return; + + _monitorSilence=_monitor; + if (_monitorSilence) + { + _monitorTimer->start(_silenceSeconds*1000); + } + else + _monitorTimer->stop(); + + activityStateSet(NOTIFYNORMAL); +} + +void Session::setMonitorSilenceSeconds(int seconds) +{ + _silenceSeconds=seconds; + if (_monitorSilence) { + _monitorTimer->start(_silenceSeconds*1000); + } +} + +void Session::setAddToUtmp(bool set) +{ + _addToUtmp = set; +} + +void Session::setFlowControlEnabled(bool enabled) +{ + if (_flowControl == enabled) + return; + + _flowControl = enabled; + + if (_shellProcess) + _shellProcess->setXonXoff(_flowControl); + + emit flowControlEnabledChanged(enabled); +} +bool Session::flowControlEnabled() const +{ + return _flowControl; +} +//void Session::fireZModemDetected() +//{ +// if (!_zmodemBusy) +// { +// QTimer::singleShot(10, this, SIGNAL(zmodemDetected())); +// _zmodemBusy = true; +// } +//} + +//void Session::cancelZModem() +//{ +// _shellProcess->sendData("\030\030\030\030", 4); // Abort +// _zmodemBusy = false; +//} + +//void Session::startZModem(const QString &zmodem, const QString &dir, const QStringList &list) +//{ +// _zmodemBusy = true; +// _zmodemProc = new KProcess(); +// _zmodemProc->setOutputChannelMode( KProcess::SeparateChannels ); +// +// *_zmodemProc << zmodem << "-v" << list; +// +// if (!dir.isEmpty()) +// _zmodemProc->setWorkingDirectory(dir); +// +// _zmodemProc->start(); +// +// connect(_zmodemProc,SIGNAL (readyReadStandardOutput()), +// this, SLOT(zmodemReadAndSendBlock())); +// connect(_zmodemProc,SIGNAL (readyReadStandardError()), +// this, SLOT(zmodemReadStatus())); +// connect(_zmodemProc,SIGNAL (finished(int,QProcess::ExitStatus)), +// this, SLOT(zmodemFinished())); +// +// disconnect( _shellProcess,SIGNAL(block_in(const char*,int)), this, SLOT(onReceiveBlock(const char*,int)) ); +// connect( _shellProcess,SIGNAL(block_in(const char*,int)), this, SLOT(zmodemRcvBlock(const char*,int)) ); +// +// _zmodemProgress = new ZModemDialog(QApplication::activeWindow(), false, +// i18n("ZModem Progress")); +// +// connect(_zmodemProgress, SIGNAL(user1Clicked()), +// this, SLOT(zmodemDone())); +// +// _zmodemProgress->show(); +//} + +/*void Session::zmodemReadAndSendBlock() +{ + _zmodemProc->setReadChannel( QProcess::StandardOutput ); + QByteArray data = _zmodemProc->readAll(); + + if ( data.count() == 0 ) + return; + + _shellProcess->sendData(data.constData(),data.count()); +} +*/ +/* +void Session::zmodemReadStatus() +{ + _zmodemProc->setReadChannel( QProcess::StandardError ); + QByteArray msg = _zmodemProc->readAll(); + while(!msg.isEmpty()) + { + int i = msg.indexOf('\015'); + int j = msg.indexOf('\012'); + QByteArray txt; + if ((i != -1) && ((j == -1) || (i < j))) + { + msg = msg.mid(i+1); + } + else if (j != -1) + { + txt = msg.left(j); + msg = msg.mid(j+1); + } + else + { + txt = msg; + msg.truncate(0); + } + if (!txt.isEmpty()) + _zmodemProgress->addProgressText(QString::fromLocal8Bit(txt)); + } +} +*/ +/* +void Session::zmodemRcvBlock(const char *data, int len) +{ + QByteArray ba( data, len ); + + _zmodemProc->write( ba ); +} +*/ +/* +void Session::zmodemFinished() +{ + if (_zmodemProc) + { + delete _zmodemProc; + _zmodemProc = 0; + _zmodemBusy = false; + + disconnect( _shellProcess,SIGNAL(block_in(const char*,int)), this ,SLOT(zmodemRcvBlock(const char*,int)) ); + connect( _shellProcess,SIGNAL(block_in(const char*,int)), this, SLOT(onReceiveBlock(const char*,int)) ); + + _shellProcess->sendData("\030\030\030\030", 4); // Abort + _shellProcess->sendData("\001\013\n", 3); // Try to get prompt back + _zmodemProgress->transferDone(); + } +} +*/ +void Session::onReceiveBlock( const char* buf, int len ) +{ + _emulation->receiveData( buf, len ); + emit receivedData( QString::fromLatin1( buf, len ) ); +} + +QSize Session::size() +{ + return _emulation->imageSize(); +} + +void Session::setSize(const QSize& size) +{ + if ((size.width() <= 1) || (size.height() <= 1)) + return; + + emit resizeRequest(size); +} +int Session::foregroundProcessId() const +{ + return _shellProcess->foregroundProcessGroup(); +} +int Session::processId() const +{ + return _shellProcess->pid(); +} + +SessionGroup::SessionGroup() + : _masterMode(0) +{ +} +SessionGroup::~SessionGroup() +{ + // disconnect all + connectAll(false); +} +int SessionGroup::masterMode() const { return _masterMode; } +QList<Session*> SessionGroup::sessions() const { return _sessions.keys(); } +bool SessionGroup::masterStatus(Session* session) const { return _sessions[session]; } + +void SessionGroup::addSession(Session* session) +{ + _sessions.insert(session,false); + + QListIterator<Session*> masterIter(masters()); + + while ( masterIter.hasNext() ) + connectPair(masterIter.next(),session); +} +void SessionGroup::removeSession(Session* session) +{ + setMasterStatus(session,false); + + QListIterator<Session*> masterIter(masters()); + + while ( masterIter.hasNext() ) + disconnectPair(masterIter.next(),session); + + _sessions.remove(session); +} +void SessionGroup::setMasterMode(int mode) +{ + _masterMode = mode; + + connectAll(false); + connectAll(true); +} +QList<Session*> SessionGroup::masters() const +{ + return _sessions.keys(true); +} +void SessionGroup::connectAll(bool connect) +{ + QListIterator<Session*> masterIter(masters()); + + while ( masterIter.hasNext() ) + { + Session* master = masterIter.next(); + + QListIterator<Session*> otherIter(_sessions.keys()); + while ( otherIter.hasNext() ) + { + Session* other = otherIter.next(); + + if ( other != master ) + { + if ( connect ) + connectPair(master,other); + else + disconnectPair(master,other); + } + } + } +} +void SessionGroup::setMasterStatus(Session* session , bool master) +{ + bool wasMaster = _sessions[session]; + _sessions[session] = master; + + if ( !wasMaster && !master + || wasMaster && master ) + return; + + QListIterator<Session*> iter(_sessions.keys()); + while ( iter.hasNext() ) + { + Session* other = iter.next(); + + if ( other != session ) + { + if ( master ) + connectPair(session,other); + else + disconnectPair(session,other); + } + } +} +void SessionGroup::connectPair(Session* master , Session* other) +{ +// qDebug() << k_funcinfo; + + if ( _masterMode & CopyInputToAll ) + { + qDebug() << "Connection session " << master->nameTitle() << "to" << other->nameTitle(); + + connect( master->emulation() , SIGNAL(sendData(const char*,int)) , other->emulation() , + SLOT(sendString(const char*,int)) ); + } +} +void SessionGroup::disconnectPair(Session* master , Session* other) +{ +// qDebug() << k_funcinfo; + + if ( _masterMode & CopyInputToAll ) + { + qDebug() << "Disconnecting session " << master->nameTitle() << "from" << other->nameTitle(); + + disconnect( master->emulation() , SIGNAL(sendData(const char*,int)) , other->emulation() , + SLOT(sendString(const char*,int)) ); + } +} + +//#include "moc_Session.cpp"
new file mode 100644 --- /dev/null +++ b/gui//Session.h @@ -0,0 +1,621 @@ +/* + This file is part of Konsole, an X terminal. + + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef SESSION_H +#define SESSION_H + +// Qt +#include <QtCore/QStringList> +#include <QtCore> +#include <QWidget> + +// Konsole +#include "History.h" + +class KProcess; + +namespace Konsole +{ + +class Emulation; +class Pty; +class TerminalDisplay; +//class ZModemDialog; + +/** + * Represents a terminal session consisting of a pseudo-teletype and a terminal emulation. + * The pseudo-teletype (or PTY) handles I/O between the terminal process and Konsole. + * The terminal emulation ( Emulation and subclasses ) processes the output stream from the + * PTY and produces a character image which is then shown on views connected to the session. + * + * Each Session can be connected to one or more views by using the addView() method. + * The attached views can then display output from the program running in the terminal + * or send input to the program in the terminal in the form of keypresses and mouse + * activity. + */ +class Session : public QObject +{ +Q_OBJECT + +public: + Q_PROPERTY(QString name READ nameTitle) + Q_PROPERTY(int processId READ processId) + Q_PROPERTY(QString keyBindings READ keyBindings WRITE setKeyBindings) + Q_PROPERTY(QSize size READ size WRITE setSize) + + /** + * Constructs a new session. + * + * To start the terminal process, call the run() method, + * after specifying the program and arguments + * using setProgram() and setArguments() + * + * If no program or arguments are specified explicitly, the Session + * falls back to using the program specified in the SHELL environment + * variable. + */ + Session(); + ~Session(); + + /** + * Returns true if the session is currently running. This will be true + * after run() has been called successfully. + */ + bool isRunning() const; + + /** + * Sets the profile associated with this session. + * + * @param profileKey A key which can be used to obtain the current + * profile settings from the SessionManager + */ + void setProfileKey(const QString& profileKey); + /** + * Returns the profile key associated with this session. + * This can be passed to the SessionManager to obtain the current + * profile settings. + */ + QString profileKey() const; + + /** + * Adds a new view for this session. + * + * The viewing widget will display the output from the terminal and + * input from the viewing widget (key presses, mouse activity etc.) + * will be sent to the terminal. + * + * Views can be removed using removeView(). The session is automatically + * closed when the last view is removed. + */ + void addView(TerminalDisplay* widget); + /** + * Removes a view from this session. When the last view is removed, + * the session will be closed automatically. + * + * @p widget will no longer display output from or send input + * to the terminal + */ + void removeView(TerminalDisplay* widget); + + /** + * Returns the views connected to this session + */ + QList<TerminalDisplay*> views() const; + + /** + * Returns the terminal emulation instance being used to encode / decode + * characters to / from the process. + */ + Emulation* emulation() const; + + /** + * Returns the environment of this session as a list of strings like + * VARIABLE=VALUE + */ + QStringList environment() const; + /** + * Sets the environment for this session. + * @p environment should be a list of strings like + * VARIABLE=VALUE + */ + void setEnvironment(const QStringList& environment); + + /** Returns the unique ID for this session. */ + int sessionId() const; + + /** + * Return the session title set by the user (ie. the program running + * in the terminal), or an empty string if the user has not set a custom title + */ + QString userTitle() const; + + /** + * This enum describes the contexts for which separate + * tab title formats may be specified. + */ + enum TabTitleContext + { + /** Default tab title format */ + LocalTabTitle, + /** + * Tab title format used session currently contains + * a connection to a remote computer (via SSH) + */ + RemoteTabTitle + }; + /** + * Sets the format used by this session for tab titles. + * + * @param context The context whoose format should be set. + * @param format The tab title format. This may be a mixture + * of plain text and dynamic elements denoted by a '%' character + * followed by a letter. (eg. %d for directory). The dynamic + * elements available depend on the @p context + */ + void setTabTitleFormat(TabTitleContext context , const QString& format); + /** Returns the format used by this session for tab titles. */ + QString tabTitleFormat(TabTitleContext context) const; + + + /** Returns the arguments passed to the shell process when run() is called. */ + QStringList arguments() const; + /** Returns the program name of the shell process started when run() is called. */ + QString program() const; + + /** + * Sets the command line arguments which the session's program will be passed when + * run() is called. + */ + void setArguments(const QStringList& arguments); + /** Sets the program to be executed when run() is called. */ + void setProgram(const QString& program); + + /** Returns the session's current working directory. */ + QString initialWorkingDirectory() { return _initialWorkingDir; } + + /** + * Sets the initial working directory for the session when it is run + * This has no effect once the session has been started. + */ + void setInitialWorkingDirectory( const QString& dir ); + + /** + * Sets the type of history store used by this session. + * Lines of output produced by the terminal are added + * to the history store. The type of history store + * used affects the number of lines which can be + * remembered before they are lost and the storage + * (in memory, on-disk etc.) used. + */ + void setHistoryType(const HistoryType& type); + /** + * Returns the type of history store used by this session. + */ + const HistoryType& historyType() const; + /** + * Clears the history store used by this session. + */ + void clearHistory(); + + /** + * Enables monitoring for activity in the session. + * This will cause notifySessionState() to be emitted + * with the NOTIFYACTIVITY state flag when output is + * received from the terminal. + */ + void setMonitorActivity(bool); + /** Returns true if monitoring for activity is enabled. */ + bool isMonitorActivity() const; + + /** + * Enables monitoring for silence in the session. + * This will cause notifySessionState() to be emitted + * with the NOTIFYSILENCE state flag when output is not + * received from the terminal for a certain period of + * time, specified with setMonitorSilenceSeconds() + */ + void setMonitorSilence(bool); + /** + * Returns true if monitoring for inactivity (silence) + * in the session is enabled. + */ + bool isMonitorSilence() const; + /** See setMonitorSilence() */ + void setMonitorSilenceSeconds(int seconds); + + /** + * Sets the key bindings used by this session. The bindings + * specify how input key sequences are translated into + * the character stream which is sent to the terminal. + * + * @param id The name of the key bindings to use. The + * names of available key bindings can be determined using the + * KeyboardTranslatorManager class. + */ + void setKeyBindings(const QString& id); + /** Returns the name of the key bindings used by this session. */ + QString keyBindings() const; + + /** + * This enum describes the available title roles. + */ + enum TitleRole + { + /** The name of the session. */ + NameRole, + /** The title of the session which is displayed in tabs etc. */ + DisplayedTitleRole + }; + + /** Sets the session's title for the specified @p role to @p title. */ + void setTitle(TitleRole role , const QString& title); + /** Returns the session's title for the specified @p role. */ + QString title(TitleRole role) const; + /** Convenience method used to read the name property. Returns title(Session::NameRole). */ + QString nameTitle() const { return title(Session::NameRole); } + + /** Sets the name of the icon associated with this session. */ + void setIconName(const QString& iconName); + /** Returns the name of the icon associated with this session. */ + QString iconName() const; + + /** Sets the text of the icon associated with this session. */ + void setIconText(const QString& iconText); + /** Returns the text of the icon associated with this session. */ + QString iconText() const; + + /** Specifies whether a utmp entry should be created for the pty used by this session. */ + void setAddToUtmp(bool); + + /** Sends the specified @p signal to the terminal process. */ + bool sendSignal(int signal); + + /** + * Specifies whether to close the session automatically when the terminal + * process terminates. + */ + void setAutoClose(bool b) { _autoClose = b; } + + /** + * Sets whether flow control is enabled for this terminal + * session. + */ + void setFlowControlEnabled(bool enabled); + + /** Returns whether flow control is enabled for this terminal session. */ + bool flowControlEnabled() const; + + /** + * Sends @p text to the current foreground terminal program. + */ + void sendText(const QString& text) const; + + /** + * Returns the process id of the terminal process. + * This is the id used by the system API to refer to the process. + */ + int processId() const; + + /** + * Returns the process id of the terminal's foreground process. + * This is initially the same as processId() but can change + * as the user starts other programs inside the terminal. + */ + int foregroundProcessId() const; + + /** Returns the terminal session's window size in lines and columns. */ + QSize size(); + /** + * Emits a request to resize the session to accommodate + * the specified window size. + * + * @param size The size in lines and columns to request. + */ + void setSize(const QSize& size); + + /** Sets the text codec used by this session's terminal emulation. */ + void setCodec(QTextCodec* codec); + + /** + * Sets whether the session has a dark background or not. The session + * uses this information to set the COLORFGBG variable in the process's + * environment, which allows the programs running in the terminal to determine + * whether the background is light or dark and use appropriate colors by default. + * + * This has no effect once the session is running. + */ + void setDarkBackground(bool darkBackground); + /** + * Returns true if the session has a dark background. + * See setDarkBackground() + */ + bool hasDarkBackground() const; + + /** + * Attempts to get the shell program to redraw the current display area. + * This can be used after clearing the screen, for example, to get the + * shell to redraw the prompt line. + */ + void refresh(); + +// void startZModem(const QString &rz, const QString &dir, const QStringList &list); +// void cancelZModem(); +// bool isZModemBusy() { return _zmodemBusy; } + +public slots: + + /** + * Starts the terminal session. + * + * This creates the terminal process and connects the teletype to it. + */ + void run(); + + /** + * Closes the terminal session. This sends a hangup signal + * (SIGHUP) to the terminal process and causes the done(Session*) + * signal to be emitted. + */ + void close(); + + /** + * Changes the session title or other customizable aspects of the terminal + * emulation display. For a list of what may be changed see the + * Emulation::titleChanged() signal. + */ + void setUserTitle( int, const QString &caption ); + +signals: + + /** Emitted when the terminal process starts. */ + void started(); + + /** + * Emitted when the terminal process exits. + */ + void finished(); + + /** + * Emitted when output is received from the terminal process. + */ + void receivedData( const QString& text ); + + /** Emitted when the session's title has changed. */ + void titleChanged(); + + /** Emitted when the session's profile has changed. */ + void profileChanged(const QString& profile); + + /** + * Emitted when the activity state of this session changes. + * + * @param state The new state of the session. This may be one + * of NOTIFYNORMAL, NOTIFYSILENCE or NOTIFYACTIVITY + */ + void stateChanged(int state); + + /** Emitted when a bell event occurs in the session. */ + void bellRequest( const QString& message ); + + /** + * Requests that the color the text for any tabs associated with + * this session should be changed; + * + * TODO: Document what the parameter does + */ + void changeTabTextColorRequest(int); + + /** + * Requests that the background color of views on this session + * should be changed. + */ + void changeBackgroundColorRequest(const QColor&); + + /** TODO: Document me. */ + void openUrlRequest(const QString& url); + + /** TODO: Document me. */ +// void zmodemDetected(); + + /** + * Emitted when the terminal process requests a change + * in the size of the terminal window. + * + * @param size The requested window size in terms of lines and columns. + */ + void resizeRequest(const QSize& size); + + /** + * Emitted when a profile change command is received from the terminal. + * + * @param text The text of the command. This is a string of the form + * "PropertyName=Value;PropertyName=Value ..." + */ + void profileChangeCommandReceived(const QString& text); + + /** + * Emitted when the flow control state changes. + * + * @param enabled True if flow control is enabled or false otherwise. + */ + void flowControlEnabledChanged(bool enabled); + +private slots: + void done(int); + +// void fireZModemDetected(); + + void onReceiveBlock( const char* buffer, int len ); + void monitorTimerDone(); + + void onViewSizeChange(int height, int width); + void onEmulationSizeChange(int lines , int columns); + + void activityStateSet(int); + + //automatically detach views from sessions when view is destroyed + void viewDestroyed(QObject* view); + +// void zmodemReadStatus(); +// void zmodemReadAndSendBlock(); +// void zmodemRcvBlock(const char *data, int len); +// void zmodemFinished(); + +private: + + void updateTerminalSize(); + WId windowId() const; + + int _uniqueIdentifier; + + Pty* _shellProcess; + Emulation* _emulation; + + QList<TerminalDisplay*> _views; + + bool _monitorActivity; + bool _monitorSilence; + bool _notifiedActivity; + bool _masterMode; + bool _autoClose; + bool _wantedClose; + QTimer* _monitorTimer; + + int _silenceSeconds; + + QString _nameTitle; + QString _displayTitle; + QString _userTitle; + + QString _localTabTitleFormat; + QString _remoteTabTitleFormat; + + QString _iconName; + QString _iconText; // as set by: echo -en '\033]1;IconText\007 + bool _addToUtmp; + bool _flowControl; + bool _fullScripting; + + QString _program; + QStringList _arguments; + + QStringList _environment; + int _sessionId; + + QString _initialWorkingDir; + + // ZModem +// bool _zmodemBusy; +// KProcess* _zmodemProc; +// ZModemDialog* _zmodemProgress; + + // Color/Font Changes by ESC Sequences + + QColor _modifiedBackground; // as set by: echo -en '\033]11;Color\007 + + QString _profileKey; + + bool _hasDarkBackground; + + static int lastSessionId; + +}; + +/** + * Provides a group of sessions which is divided into master and slave sessions. + * Activity in master sessions can be propagated to all sessions within the group. + * The type of activity which is propagated and method of propagation is controlled + * by the masterMode() flags. + */ +class SessionGroup : public QObject +{ +Q_OBJECT + +public: + /** Constructs an empty session group. */ + SessionGroup(); + /** Destroys the session group and removes all connections between master and slave sessions. */ + ~SessionGroup(); + + /** Adds a session to the group. */ + void addSession( Session* session ); + /** Removes a session from the group. */ + void removeSession( Session* session ); + + /** Returns the list of sessions currently in the group. */ + QList<Session*> sessions() const; + + /** + * Sets whether a particular session is a master within the group. + * Changes or activity in the group's master sessions may be propagated + * to all the sessions in the group, depending on the current masterMode() + * + * @param session The session whoose master status should be changed. + * @param master True to make this session a master or false otherwise + */ + void setMasterStatus( Session* session , bool master ); + /** Returns the master status of a session. See setMasterStatus() */ + bool masterStatus( Session* session ) const; + + /** + * This enum describes the options for propagating certain activity or + * changes in the group's master sessions to all sessions in the group. + */ + enum MasterMode + { + /** + * Any input key presses in the master sessions are sent to all + * sessions in the group. + */ + CopyInputToAll = 1 + }; + + /** + * Specifies which activity in the group's master sessions is propagated + * to all sessions in the group. + * + * @param mode A bitwise OR of MasterMode flags. + */ + void setMasterMode( int mode ); + /** + * Returns a bitwise OR of the active MasterMode flags for this group. + * See setMasterMode() + */ + int masterMode() const; + +private: + void connectPair(Session* master , Session* other); + void disconnectPair(Session* master , Session* other); + void connectAll(bool connect); + QList<Session*> masters() const; + + // maps sessions to their master status + QHash<Session*,bool> _sessions; + + int _masterMode; +}; + +} + +#endif
new file mode 100644 --- /dev/null +++ b/gui//ShellCommand.cpp @@ -0,0 +1,168 @@ +/* + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +// Own +#include "ShellCommand.h" + +//some versions of gcc(4.3) require explicit include +#include <cstdlib> + + +using namespace Konsole; + +// expands environment variables in 'text' +// function copied from kdelibs/kio/kio/kurlcompletion.cpp +static bool expandEnv(QString& text); + +ShellCommand::ShellCommand(const QString& fullCommand) +{ + bool inQuotes = false; + + QString builder; + + for ( int i = 0 ; i < fullCommand.count() ; i++ ) + { + QChar ch = fullCommand[i]; + + const bool isLastChar = ( i == fullCommand.count() - 1 ); + const bool isQuote = ( ch == '\'' || ch == '\"' ); + + if ( !isLastChar && isQuote ) + inQuotes = !inQuotes; + else + { + if ( (!ch.isSpace() || inQuotes) && !isQuote ) + builder.append(ch); + + if ( (ch.isSpace() && !inQuotes) || ( i == fullCommand.count()-1 ) ) + { + _arguments << builder; + builder.clear(); + } + } + } +} +ShellCommand::ShellCommand(const QString& command , const QStringList& arguments) +{ + _arguments = arguments; + + if ( !_arguments.isEmpty() ) + _arguments[0] == command; +} +QString ShellCommand::fullCommand() const +{ + return _arguments.join(QChar(' ')); +} +QString ShellCommand::command() const +{ + if ( !_arguments.isEmpty() ) + return _arguments[0]; + else + return QString(); +} +QStringList ShellCommand::arguments() const +{ + return _arguments; +} +bool ShellCommand::isRootCommand() const +{ + Q_ASSERT(0); // not implemented yet + return false; +} +bool ShellCommand::isAvailable() const +{ + Q_ASSERT(0); // not implemented yet + return false; +} +QStringList ShellCommand::expand(const QStringList& items) +{ + QStringList result; + + foreach( QString item , items ) + result << expand(item); + + return result; +} +QString ShellCommand::expand(const QString& text) +{ + QString result = text; + expandEnv(result); + return result; +} + +/* + * expandEnv + * + * Expand environment variables in text. Escaped '$' characters are ignored. + * Return true if any variables were expanded + */ +static bool expandEnv( QString &text ) +{ + // Find all environment variables beginning with '$' + // + int pos = 0; + + bool expanded = false; + + while ( (pos = text.indexOf(QLatin1Char('$'), pos)) != -1 ) { + + // Skip escaped '$' + // + if ( pos > 0 && text.at(pos-1) == QLatin1Char('\\') ) { + pos++; + } + // Variable found => expand + // + else { + // Find the end of the variable = next '/' or ' ' + // + int pos2 = text.indexOf( QLatin1Char(' '), pos+1 ); + int pos_tmp = text.indexOf( QLatin1Char('/'), pos+1 ); + + if ( pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2) ) + pos2 = pos_tmp; + + if ( pos2 == -1 ) + pos2 = text.length(); + + // Replace if the variable is terminated by '/' or ' ' + // and defined + // + if ( pos2 >= 0 ) { + int len = pos2 - pos; + QString key = text.mid( pos+1, len-1); + QString value = + QString::fromLocal8Bit( ::getenv(key.toLocal8Bit()) ); + + if ( !value.isEmpty() ) { + expanded = true; + text.replace( pos, len, value ); + pos = pos + value.length(); + } + else { + pos = pos2; + } + } + } + } + + return expanded; +}
new file mode 100644 --- /dev/null +++ b/gui//ShellCommand.h @@ -0,0 +1,94 @@ +/* + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef SHELLCOMMAND_H +#define SHELLCOMMAND_H + +// Qt +#include <QtCore/QStringList> + +namespace Konsole +{ + +/** + * A class to parse and extract information about shell commands. + * + * ShellCommand can be used to: + * + * <ul> + * <li>Take a command-line (eg "/bin/sh -c /path/to/my/script") and split it + * into its component parts (eg. the command "/bin/sh" and the arguments + * "-c","/path/to/my/script") + * </li> + * <li>Take a command and a list of arguments and combine them to + * form a complete command line. + * </li> + * <li>Determine whether the binary specified by a command exists in the + * user's PATH. + * </li> + * <li>Determine whether a command-line specifies the execution of + * another command as the root user using su/sudo etc. + * </li> + * </ul> + */ +class ShellCommand +{ +public: + /** + * Constructs a ShellCommand from a command line. + * + * @param fullCommand The command line to parse. + */ + ShellCommand(const QString& fullCommand); + /** + * Constructs a ShellCommand with the specified @p command and @p arguments. + */ + ShellCommand(const QString& command , const QStringList& arguments); + + /** Returns the command. */ + QString command() const; + /** Returns the arguments. */ + QStringList arguments() const; + + /** + * Returns the full command line. + */ + QString fullCommand() const; + + /** Returns true if this is a root command. */ + bool isRootCommand() const; + /** Returns true if the program specified by @p command() exists. */ + bool isAvailable() const; + + /** Expands environment variables in @p text .*/ + static QString expand(const QString& text); + + /** Expands environment variables in each string in @p list. */ + static QStringList expand(const QStringList& items); + +private: + QStringList _arguments; +}; + +} + +#endif // SHELLCOMMAND_H +
new file mode 100644 --- /dev/null +++ b/gui//TODO @@ -0,0 +1,10 @@ +Global + - provide more compatibility for vttest + +Package + - migrate to autotools if needed + +Source + - provide more options for customization + - clean unused code + - add some QT3 support features if needed
new file mode 100644 --- /dev/null +++ b/gui//TerminalCharacterDecoder.cpp @@ -0,0 +1,227 @@ +/* + This file is part of Konsole, an X terminal. + + Copyright (C) 2006 by Robert Knight <robertknight@gmail.com> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + This program 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 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 Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +// Own +#include "TerminalCharacterDecoder.h" + +// Qt +#include <QtCore/QTextStream> + + +using namespace Konsole; + +PlainTextDecoder::PlainTextDecoder() + : _output(0) + , _includeTrailingWhitespace(true) +{ + +} +void PlainTextDecoder::setTrailingWhitespace(bool enable) +{ + _includeTrailingWhitespace = enable; +} +bool PlainTextDecoder::trailingWhitespace() const +{ + return _includeTrailingWhitespace; +} +void PlainTextDecoder::begin(QTextStream* output) +{ + _output = output; +} +void PlainTextDecoder::end() +{ + _output = 0; +} +void PlainTextDecoder::decodeLine(const Character* const characters, int count, LineProperty /*properties*/ + ) +{ + Q_ASSERT( _output ); + + //TODO should we ignore or respect the LINE_WRAPPED line property? + + //note: we build up a QString and send it to the text stream rather writing into the text + //stream a character at a time because it is more efficient. + //(since QTextStream always deals with QStrings internally anyway) + QString plainText; + plainText.reserve(count); + + int outputCount = count; + + // if inclusion of trailing whitespace is disabled then find the end of the + // line + if ( !_includeTrailingWhitespace ) + { + for (int i = count-1 ; i >= 0 ; i--) + { + if ( characters[i].character != ' ' ) + break; + else + outputCount--; + } + } + + for (int i=0;i<outputCount;i++) + { + plainText.append( QChar(characters[i].character) ); + } + + *_output << plainText; +} + +HTMLDecoder::HTMLDecoder() : + _output(0) + ,_colorTable(base_color_table) + ,_innerSpanOpen(false) + ,_lastRendition(DEFAULT_RENDITION) +{ + +} + +void HTMLDecoder::begin(QTextStream* output) +{ + _output = output; + + QString text; + + //open monospace span + openSpan(text,"font-family:monospace"); + + *output << text; +} + +void HTMLDecoder::end() +{ + Q_ASSERT( _output ); + + QString text; + + closeSpan(text); + + *_output << text; + + _output = 0; + +} + +//TODO: Support for LineProperty (mainly double width , double height) +void HTMLDecoder::decodeLine(const Character* const characters, int count, LineProperty /*properties*/ + ) +{ + Q_ASSERT( _output ); + + QString text; + + int spaceCount = 0; + + for (int i=0;i<count;i++) + { + QChar ch(characters[i].character); + + //check if appearance of character is different from previous char + if ( characters[i].rendition != _lastRendition || + characters[i].foregroundColor != _lastForeColor || + characters[i].backgroundColor != _lastBackColor ) + { + if ( _innerSpanOpen ) + closeSpan(text); + + _lastRendition = characters[i].rendition; + _lastForeColor = characters[i].foregroundColor; + _lastBackColor = characters[i].backgroundColor; + + //build up style string + QString style; + + if ( _lastRendition & RE_BOLD || + (_colorTable && characters[i].isBold(_colorTable)) ) + style.append("font-weight:bold;"); + + + if ( _lastRendition & RE_UNDERLINE ) + style.append("font-decoration:underline;"); + + //colours - a colour table must have been defined first + if ( _colorTable ) + { + style.append( QString("color:%1;").arg(_lastForeColor.color(_colorTable).name() ) ); + + if (!characters[i].isTransparent(_colorTable)) + { + style.append( QString("background-color:%1;").arg(_lastBackColor.color(_colorTable).name() ) ); + } + } + + //open the span with the current style + openSpan(text,style); + _innerSpanOpen = true; + } + + //handle whitespace + if (ch.isSpace()) + spaceCount++; + else + spaceCount = 0; + + + //output current character + if (spaceCount < 2) + { + //escape HTML tag characters and just display others as they are + if ( ch == '<' ) + text.append("<"); + else if (ch == '>') + text.append(">"); + else + text.append(ch); + } + else + { + text.append(" "); //HTML truncates multiple spaces, so use a space marker instead + } + + } + + //close any remaining open inner spans + if ( _innerSpanOpen ) + closeSpan(text); + + //start new line + text.append("<br>"); + + *_output << text; +} + +void HTMLDecoder::openSpan(QString& text , const QString& style) +{ + text.append( QString("<span style=\"%1\">").arg(style) ); +} + +void HTMLDecoder::closeSpan(QString& text) +{ + text.append("</span>"); +} + +void HTMLDecoder::setColorTable(const ColorEntry* table) +{ + _colorTable = table; +}
new file mode 100644 --- /dev/null +++ b/gui//TerminalCharacterDecoder.h @@ -0,0 +1,139 @@ +/* + This file is part of Konsole, an X terminal. + + Copyright (C) 2006-7 by Robert Knight <robertknight@gmail.com> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + This program 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 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 Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef TERMINAL_CHARACTER_DECODER_H +#define TERMINAL_CHARACTER_DECODER_H + +#include "Character.h" + +class QTextStream; + +namespace Konsole +{ + +/** + * Base class for terminal character decoders + * + * The decoder converts lines of terminal characters which consist of a unicode character, foreground + * and background colours and other appearance-related properties into text strings. + * + * Derived classes may produce either plain text with no other colour or appearance information, or + * they may produce text which incorporates these additional properties. + */ +class TerminalCharacterDecoder +{ +public: + virtual ~TerminalCharacterDecoder() {} + + /** Begin decoding characters. The resulting text is appended to @p output. */ + virtual void begin(QTextStream* output) = 0; + /** End decoding. */ + virtual void end() = 0; + + /** + * Converts a line of terminal characters with associated properties into a text string + * and writes the string into an output QTextStream. + * + * @param characters An array of characters of length @p count. + * @param properties Additional properties which affect all characters in the line + * @param output The output stream which receives the decoded text + */ + virtual void decodeLine(const Character* const characters, + int count, + LineProperty properties) = 0; +}; + +/** + * A terminal character decoder which produces plain text, ignoring colours and other appearance-related + * properties of the original characters. + */ +class PlainTextDecoder : public TerminalCharacterDecoder +{ +public: + PlainTextDecoder(); + + /** + * Set whether trailing whitespace at the end of lines should be included + * in the output. + * Defaults to true. + */ + void setTrailingWhitespace(bool enable); + /** + * Returns whether trailing whitespace at the end of lines is included + * in the output. + */ + bool trailingWhitespace() const; + + virtual void begin(QTextStream* output); + virtual void end(); + + virtual void decodeLine(const Character* const characters, + int count, + LineProperty properties); + + +private: + QTextStream* _output; + bool _includeTrailingWhitespace; +}; + +/** + * A terminal character decoder which produces pretty HTML markup + */ +class HTMLDecoder : public TerminalCharacterDecoder +{ +public: + /** + * Constructs an HTML decoder using a default black-on-white color scheme. + */ + HTMLDecoder(); + + /** + * Sets the colour table which the decoder uses to produce the HTML colour codes in its + * output + */ + void setColorTable( const ColorEntry* table ); + + virtual void decodeLine(const Character* const characters, + int count, + LineProperty properties); + + virtual void begin(QTextStream* output); + virtual void end(); + +private: + void openSpan(QString& text , const QString& style); + void closeSpan(QString& text); + + QTextStream* _output; + const ColorEntry* _colorTable; + bool _innerSpanOpen; + quint8 _lastRendition; + CharacterColor _lastForeColor; + CharacterColor _lastBackColor; + +}; + +} + +#endif
new file mode 100644 --- /dev/null +++ b/gui//TerminalDisplay.cpp @@ -0,0 +1,2724 @@ +/* + This file is part of Konsole, a terminal emulator for KDE. + + Copyright (C) 2006-7 by Robert Knight <robertknight@gmail.com> + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +// Own +#include "TerminalDisplay.h" + +// Qt +#include <QtGui/QApplication> +#include <QtGui/QBoxLayout> +#include <QtGui/QClipboard> +#include <QtGui/QKeyEvent> +#include <QtCore/QEvent> +#include <QtCore/QTime> +#include <QtCore/QFile> +#include <QtGui/QGridLayout> +#include <QtGui/QLabel> +#include <QtGui/QLayout> +#include <QtGui/QPainter> +#include <QtGui/QPixmap> +#include <QtGui/QScrollBar> +#include <QtGui/QStyle> +#include <QtCore> +#include <QtGui> + +#include "Filter.h" +#include "konsole_wcwidth.h" +#include "ScreenWindow.h" +#include "TerminalCharacterDecoder.h" +#include "ColorTables.h" + +using namespace Konsole; + +#ifndef loc +#define loc(X,Y) ((Y)*_columns+(X)) +#endif + +#define yMouseScroll 1 + +#define REPCHAR "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ + "abcdefgjijklmnopqrstuvwxyz" \ + "0123456789./+@" + +// scroll increment used when dragging selection at top/bottom of window. + +// static +bool TerminalDisplay::_antialiasText = true; +bool TerminalDisplay::HAVE_TRANSPARENCY = false; + +/* ------------------------------------------------------------------------- */ +/* */ +/* Colors */ +/* */ +/* ------------------------------------------------------------------------- */ + +/* Note that we use ANSI color order (bgr), while IBMPC color order is (rgb) + + Code 0 1 2 3 4 5 6 7 + ----------- ------- ------- ------- ------- ------- ------- ------- ------- + ANSI (bgr) Black Red Green Yellow Blue Magenta Cyan White + IBMPC (rgb) Black Blue Green Cyan Red Magenta Yellow White +*/ + +ScreenWindow* TerminalDisplay::screenWindow() const +{ + return _screenWindow; +} +void TerminalDisplay::setScreenWindow(ScreenWindow* window) +{ + // disconnect existing screen window if any + if ( _screenWindow ) + { + disconnect( _screenWindow , 0 , this , 0 ); + } + + _screenWindow = window; + + if ( window ) + { +//#warning "The order here is not specified - does it matter whether updateImage or updateLineProperties comes first?" + connect( _screenWindow , SIGNAL(outputChanged()) , this , SLOT(updateLineProperties()) ); + connect( _screenWindow , SIGNAL(outputChanged()) , this , SLOT(updateImage()) ); + window->setWindowLines(_lines); + } +} + +const ColorEntry* TerminalDisplay::colorTable() const +{ + return _colorTable; +} + +void TerminalDisplay::setColorTable(const ColorEntry table[]) +{ + for (int i = 0; i < TABLE_COLORS; i++) + _colorTable[i] = table[i]; + + QPalette p = palette(); + p.setColor( backgroundRole(), _colorTable[DEFAULT_BACK_COLOR].color ); + setPalette( p ); + + // Avoid propagating the palette change to the scroll bar + _scrollBar->setPalette( QApplication::palette() ); + + update(); +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Font */ +/* */ +/* ------------------------------------------------------------------------- */ + +/* + The VT100 has 32 special graphical characters. The usual vt100 extended + xterm fonts have these at 0x00..0x1f. + + QT's iso mapping leaves 0x00..0x7f without any changes. But the graphicals + come in here as proper unicode characters. + + We treat non-iso10646 fonts as VT100 extended and do the requiered mapping + from unicode to 0x00..0x1f. The remaining translation is then left to the + QCodec. +*/ + +static inline bool isLineChar(quint16 c) { return ((c & 0xFF80) == 0x2500);} +static inline bool isLineCharString(const QString& string) +{ + return (string.length() > 0) && (isLineChar(string.at(0).unicode())); +} + + +// assert for i in [0..31] : vt100extended(vt100_graphics[i]) == i. + +unsigned short Konsole::vt100_graphics[32] = +{ // 0/8 1/9 2/10 3/11 4/12 5/13 6/14 7/15 + 0x0020, 0x25C6, 0x2592, 0x2409, 0x240c, 0x240d, 0x240a, 0x00b0, + 0x00b1, 0x2424, 0x240b, 0x2518, 0x2510, 0x250c, 0x2514, 0x253c, + 0xF800, 0xF801, 0x2500, 0xF803, 0xF804, 0x251c, 0x2524, 0x2534, + 0x252c, 0x2502, 0x2264, 0x2265, 0x03C0, 0x2260, 0x00A3, 0x00b7 +}; + +void TerminalDisplay::fontChange(const QFont&) +{ + QFontMetrics fm(font()); + _fontHeight = fm.height() + _lineSpacing; + + + // waba TerminalDisplay 1.123: + // "Base character width on widest ASCII character. This prevents too wide + // characters in the presence of double wide (e.g. Japanese) characters." + // Get the width from representative normal width characters + _fontWidth = qRound((double)fm.width(REPCHAR)/(double)strlen(REPCHAR)); + + _fixedFont = true; + + int fw = fm.width(REPCHAR[0]); + for(unsigned int i=1; i< strlen(REPCHAR); i++) + { + if (fw != fm.width(REPCHAR[i])) + { + _fixedFont = false; + break; + } + } + + if (_fontWidth < 1) + _fontWidth=1; + + _fontAscent = fm.ascent(); + + emit changedFontMetricSignal( _fontHeight, _fontWidth ); + propagateSize(); + update(); +} + +void TerminalDisplay::setVTFont(const QFont& f) +{ + QFont font = f; + + QFontMetrics metrics(font); + + if ( metrics.height() < height() && metrics.maxWidth() < width() ) + { + // hint that text should be drawn without anti-aliasing. + // depending on the user's font configuration, this may not be respected + if (!_antialiasText) + font.setStyleStrategy( QFont::NoAntialias ); + + // experimental optimization. Konsole assumes that the terminal is using a + // mono-spaced font, in which case kerning information should have an effect. + // Disabling kerning saves some computation when rendering text. + font.setKerning(false); + + QWidget::setFont(font); + fontChange(font); + } +} + +void TerminalDisplay::setFont(const QFont &) +{ + // ignore font change request if not coming from konsole itself +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Constructor / Destructor */ +/* */ +/* ------------------------------------------------------------------------- */ + +TerminalDisplay::TerminalDisplay(QWidget *parent) +:QWidget(parent) +,_screenWindow(0) +,_allowBell(true) +,_gridLayout(0) +,_fontHeight(1) +,_fontWidth(1) +,_fontAscent(1) +,_lines(1) +,_columns(1) +,_usedLines(1) +,_usedColumns(1) +,_contentHeight(1) +,_contentWidth(1) +,_image(0) +,_randomSeed(0) +,_resizing(false) +,_terminalSizeHint(false) +,_terminalSizeStartup(true) +,_bidiEnabled(false) +,_actSel(0) +,_wordSelectionMode(false) +,_lineSelectionMode(false) +,_preserveLineBreaks(false) +,_columnSelectionMode(false) +,_scrollbarLocation(NoScrollBar) +,_wordCharacters(":@-./_~") +,_bellMode(SystemBeepBell) +,_blinking(false) +,_cursorBlinking(false) +,_hasBlinkingCursor(false) +,_ctrlDrag(false) +,_tripleClickMode(SelectWholeLine) +,_isFixedSize(false) +,_possibleTripleClick(false) +,_resizeWidget(0) +,_resizeTimer(0) +,_flowControlWarningEnabled(false) +,_outputSuspendedLabel(0) +,_lineSpacing(0) +,_colorsInverted(false) +,_blendColor(qRgba(0,0,0,0xff)) +,_filterChain(new TerminalImageFilterChain()) +,_cursorShape(BlockCursor) +{ + // terminal applications are not designed with Right-To-Left in mind, + // so the layout is forced to Left-To-Right + setLayoutDirection(Qt::LeftToRight); + + // The offsets are not yet calculated. + // Do not calculate these too often to be more smoothly when resizing + // konsole in opaque mode. + _topMargin = DEFAULT_TOP_MARGIN; + _leftMargin = DEFAULT_LEFT_MARGIN; + + // create scroll bar for scrolling output up and down + // set the scroll bar's slider to occupy the whole area of the scroll bar initially + _scrollBar = new QScrollBar(this); + setScroll(0,0); + _scrollBar->setCursor( Qt::ArrowCursor ); + connect(_scrollBar, SIGNAL(valueChanged(int)), this, + SLOT(scrollBarPositionChanged(int))); + + // setup timers for blinking cursor and text + _blinkTimer = new QTimer(this); + connect(_blinkTimer, SIGNAL(timeout()), this, SLOT(blinkEvent())); + _blinkCursorTimer = new QTimer(this); + connect(_blinkCursorTimer, SIGNAL(timeout()), this, SLOT(blinkCursorEvent())); + +// QCursor::setAutoHideCursor( this, true ); + + setUsesMouse(true); + setColorTable(whiteonblack_color_table); +// setColorTable(blackonlightyellow_color_table); + setMouseTracking(true); + + // Enable drag and drop + setAcceptDrops(true); // attempt + dragInfo.state = diNone; + + setFocusPolicy( Qt::WheelFocus ); + + // enable input method support + setAttribute(Qt::WA_InputMethodEnabled, true); + + // this is an important optimization, it tells Qt + // that TerminalDisplay will handle repainting its entire area. + setAttribute(Qt::WA_OpaquePaintEvent); + + _gridLayout = new QGridLayout(this); + _gridLayout->setMargin(0); + + setLayout( _gridLayout ); + + //set up a warning message when the user presses Ctrl+S to avoid confusion + connect( this,SIGNAL(flowControlKeyPressed(bool)),this,SLOT(outputSuspended(bool)) ); +} + +TerminalDisplay::~TerminalDisplay() +{ + qApp->removeEventFilter( this ); + + delete[] _image; + + delete _gridLayout; + delete _outputSuspendedLabel; + delete _filterChain; +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Display Operations */ +/* */ +/* ------------------------------------------------------------------------- */ + +/** + A table for emulating the simple (single width) unicode drawing chars. + It represents the 250x - 257x glyphs. If it's zero, we can't use it. + if it's not, it's encoded as follows: imagine a 5x5 grid where the points are numbered + 0 to 24 left to top, top to bottom. Each point is represented by the corresponding bit. + + Then, the pixels basically have the following interpretation: + _|||_ + -...- + -...- + -...- + _|||_ + +where _ = none + | = vertical line. + - = horizontal line. + */ + + +enum LineEncode +{ + TopL = (1<<1), + TopC = (1<<2), + TopR = (1<<3), + + LeftT = (1<<5), + Int11 = (1<<6), + Int12 = (1<<7), + Int13 = (1<<8), + RightT = (1<<9), + + LeftC = (1<<10), + Int21 = (1<<11), + Int22 = (1<<12), + Int23 = (1<<13), + RightC = (1<<14), + + LeftB = (1<<15), + Int31 = (1<<16), + Int32 = (1<<17), + Int33 = (1<<18), + RightB = (1<<19), + + BotL = (1<<21), + BotC = (1<<22), + BotR = (1<<23) +}; + +#include "LineFont.h" + +static void drawLineChar(QPainter& paint, int x, int y, int w, int h, uchar code) +{ + //Calculate cell midpoints, end points. + int cx = x + w/2; + int cy = y + h/2; + int ex = x + w - 1; + int ey = y + h - 1; + + quint32 toDraw = LineChars[code]; + + //Top _lines: + if (toDraw & TopL) + paint.drawLine(cx-1, y, cx-1, cy-2); + if (toDraw & TopC) + paint.drawLine(cx, y, cx, cy-2); + if (toDraw & TopR) + paint.drawLine(cx+1, y, cx+1, cy-2); + + //Bot _lines: + if (toDraw & BotL) + paint.drawLine(cx-1, cy+2, cx-1, ey); + if (toDraw & BotC) + paint.drawLine(cx, cy+2, cx, ey); + if (toDraw & BotR) + paint.drawLine(cx+1, cy+2, cx+1, ey); + + //Left _lines: + if (toDraw & LeftT) + paint.drawLine(x, cy-1, cx-2, cy-1); + if (toDraw & LeftC) + paint.drawLine(x, cy, cx-2, cy); + if (toDraw & LeftB) + paint.drawLine(x, cy+1, cx-2, cy+1); + + //Right _lines: + if (toDraw & RightT) + paint.drawLine(cx+2, cy-1, ex, cy-1); + if (toDraw & RightC) + paint.drawLine(cx+2, cy, ex, cy); + if (toDraw & RightB) + paint.drawLine(cx+2, cy+1, ex, cy+1); + + //Intersection points. + if (toDraw & Int11) + paint.drawPoint(cx-1, cy-1); + if (toDraw & Int12) + paint.drawPoint(cx, cy-1); + if (toDraw & Int13) + paint.drawPoint(cx+1, cy-1); + + if (toDraw & Int21) + paint.drawPoint(cx-1, cy); + if (toDraw & Int22) + paint.drawPoint(cx, cy); + if (toDraw & Int23) + paint.drawPoint(cx+1, cy); + + if (toDraw & Int31) + paint.drawPoint(cx-1, cy+1); + if (toDraw & Int32) + paint.drawPoint(cx, cy+1); + if (toDraw & Int33) + paint.drawPoint(cx+1, cy+1); + +} + +void TerminalDisplay::drawLineCharString( QPainter& painter, int x, int y, const QString& str, + const Character* attributes) +{ + const QPen& currentPen = painter.pen(); + + if ( attributes->rendition & RE_BOLD ) + { + QPen boldPen(currentPen); + boldPen.setWidth(3); + painter.setPen( boldPen ); + } + + for (int i=0 ; i < str.length(); i++) + { + uchar code = str[i].cell(); + if (LineChars[code]) + drawLineChar(painter, x + (_fontWidth*i), y, _fontWidth, _fontHeight, code); + } + + painter.setPen( currentPen ); +} + +void TerminalDisplay::setKeyboardCursorShape(KeyboardCursorShape shape) +{ + _cursorShape = shape; +} +TerminalDisplay::KeyboardCursorShape TerminalDisplay::keyboardCursorShape() const +{ + return _cursorShape; +} +void TerminalDisplay::setKeyboardCursorColor(bool useForegroundColor, const QColor& color) +{ + if (useForegroundColor) + _cursorColor = QColor(); // an invalid color means that + // the foreground color of the + // current character should + // be used + + else + _cursorColor = color; +} +QColor TerminalDisplay::keyboardCursorColor() const +{ + return _cursorColor; +} + +void TerminalDisplay::setOpacity(qreal opacity) +{ + QColor color(_blendColor); + color.setAlphaF(opacity); + + // enable automatic background filling to prevent the display + // flickering if there is no transparency + if ( color.alpha() == 255 ) + { + setAutoFillBackground(true); + } + else + { + setAutoFillBackground(false); + } + + _blendColor = color.rgba(); +} + +void TerminalDisplay::drawBackground(QPainter& painter, const QRect& rect, const QColor& backgroundColor, bool useOpacitySetting ) +{ + // the area of the widget showing the contents of the terminal display is drawn + // using the background color from the color scheme set with setColorTable() + // + // the area of the widget behind the scroll-bar is drawn using the background + // brush from the scroll-bar's palette, to give the effect of the scroll-bar + // being outside of the terminal display and visual consistency with other KDE + // applications. + // + QRect scrollBarArea = _scrollBar->isVisible() ? + rect.intersected(_scrollBar->geometry()) : + QRect(); + QRegion contentsRegion = QRegion(rect).subtracted(scrollBarArea); + QRect contentsRect = contentsRegion.boundingRect(); + + if ( HAVE_TRANSPARENCY && qAlpha(_blendColor) < 0xff && useOpacitySetting ) + { + QColor color(backgroundColor); + color.setAlpha(qAlpha(_blendColor)); + + painter.save(); + painter.setCompositionMode(QPainter::CompositionMode_Source); + painter.fillRect(contentsRect, color); + painter.restore(); + } + else { + painter.fillRect(contentsRect, backgroundColor); + } + + painter.fillRect(scrollBarArea,_scrollBar->palette().background()); +} + +void TerminalDisplay::drawCursor(QPainter& painter, + const QRect& rect, + const QColor& foregroundColor, + const QColor& /*backgroundColor*/, + bool& invertCharacterColor) +{ + QRect cursorRect = rect; + cursorRect.setHeight(_fontHeight - _lineSpacing - 1); + + if (!_cursorBlinking) + { + if ( _cursorColor.isValid() ) + painter.setPen(_cursorColor); + else { + painter.setPen(foregroundColor); + } + + if ( _cursorShape == BlockCursor ) + { + // draw the cursor outline, adjusting the area so that + // it is draw entirely inside 'rect' + int penWidth = qMax(1,painter.pen().width()); + + painter.drawRect(cursorRect.adjusted(penWidth/2, + penWidth/2, + - penWidth/2 - penWidth%2, + - penWidth/2 - penWidth%2)); + if ( hasFocus() ) + { + painter.fillRect(cursorRect, _cursorColor.isValid() ? _cursorColor : foregroundColor); + + if ( !_cursorColor.isValid() ) + { + // invert the colour used to draw the text to ensure that the character at + // the cursor position is readable + invertCharacterColor = true; + } + } + } + else if ( _cursorShape == UnderlineCursor ) + painter.drawLine(cursorRect.left(), + cursorRect.bottom(), + cursorRect.right(), + cursorRect.bottom()); + else if ( _cursorShape == IBeamCursor ) + painter.drawLine(cursorRect.left(), + cursorRect.top(), + cursorRect.left(), + cursorRect.bottom()); + + } +} + +void TerminalDisplay::drawCharacters(QPainter& painter, + const QRect& rect, + const QString& text, + const Character* style, + bool invertCharacterColor) +{ + // don't draw text which is currently blinking + if ( _blinking && (style->rendition & RE_BLINK) ) + return; + + // setup bold and underline + bool useBold = style->rendition & RE_BOLD || style->isBold(_colorTable) || font().bold(); + bool useUnderline = style->rendition & RE_UNDERLINE || font().underline(); + + QFont font = painter.font(); + if ( font.bold() != useBold + || font.underline() != useUnderline ) + { + font.setBold(useBold); + font.setUnderline(useUnderline); + painter.setFont(font); + } + + const CharacterColor& textColor = ( invertCharacterColor ? style->backgroundColor : style->foregroundColor ); + const QColor color = textColor.color(_colorTable); + + QPen pen = painter.pen(); + if ( pen.color() != color ) + { + pen.setColor(color); + painter.setPen(color); + } + // draw text + if ( isLineCharString(text) ) { + drawLineCharString(painter,rect.x(),rect.y(),text,style); + } + else + { + // the drawText(rect,flags,string) overload is used here with null flags + // instead of drawText(rect,string) because the (rect,string) overload causes + // the application's default layout direction to be used instead of + // the widget-specific layout direction, which should always be + // Qt::LeftToRight for this widget + painter.drawText(rect,0,text); + } +} + +void TerminalDisplay::drawTextFragment(QPainter& painter , + const QRect& rect, + const QString& text, + const Character* style) +{ + painter.save(); + + // setup painter + const QColor foregroundColor = style->foregroundColor.color(_colorTable); + const QColor backgroundColor = style->backgroundColor.color(_colorTable); + + // draw background if different from the display's background color + if ( backgroundColor != palette().background().color() ) + drawBackground(painter,rect,backgroundColor, false /* do not use transparency */); + + // draw cursor shape if the current character is the cursor + // this may alter the foreground and background colors + bool invertCharacterColor = false; + + if ( style->rendition & RE_CURSOR ) + drawCursor(painter,rect,foregroundColor,backgroundColor,invertCharacterColor); + // draw text + drawCharacters(painter,rect,text,style,invertCharacterColor); + + painter.restore(); +} + +void TerminalDisplay::setRandomSeed(uint randomSeed) { _randomSeed = randomSeed; } +uint TerminalDisplay::randomSeed() const { return _randomSeed; } + +#if 0 +/*! + Set XIM Position +*/ +void TerminalDisplay::setCursorPos(const int curx, const int cury) +{ + QPoint tL = contentsRect().topLeft(); + int tLx = tL.x(); + int tLy = tL.y(); + + int xpos, ypos; + ypos = _topMargin + tLy + _fontHeight*(cury-1) + _fontAscent; + xpos = _leftMargin + tLx + _fontWidth*curx; + //setMicroFocusHint(xpos, ypos, 0, _fontHeight); //### ??? + // fprintf(stderr, "x/y = %d/%d\txpos/ypos = %d/%d\n", curx, cury, xpos, ypos); + _cursorLine = cury; + _cursorCol = curx; +} +#endif + +// scrolls the image by 'lines', down if lines > 0 or up otherwise. +// +// the terminal emulation keeps track of the scrolling of the character +// image as it receives input, and when the view is updated, it calls scrollImage() +// with the final scroll amount. this improves performance because scrolling the +// display is much cheaper than re-rendering all the text for the +// part of the image which has moved up or down. +// Instead only new lines have to be drawn +// +// note: it is important that the area of the display which is +// scrolled aligns properly with the character grid - +// which has a top left point at (_leftMargin,_topMargin) , +// a cell width of _fontWidth and a cell height of _fontHeight). +void TerminalDisplay::scrollImage(int lines , const QRect& screenWindowRegion) +{ + // if the flow control warning is enabled this will interfere with the + // scrolling optimisations and cause artifacts. the simple solution here + // is to just disable the optimisation whilst it is visible + if ( _outputSuspendedLabel && _outputSuspendedLabel->isVisible() ) { + return; + } + + // constrain the region to the display + // the bottom of the region is capped to the number of lines in the display's + // internal image - 2, so that the height of 'region' is strictly less + // than the height of the internal image. + QRect region = screenWindowRegion; + region.setBottom( qMin(region.bottom(),this->_lines-2) ); + + if ( lines == 0 + || _image == 0 + || !region.isValid() + || (region.top() + abs(lines)) >= region.bottom() + || this->_lines <= region.height() ) return; + + QRect scrollRect; + + void* firstCharPos = &_image[ region.top() * this->_columns ]; + void* lastCharPos = &_image[ (region.top() + abs(lines)) * this->_columns ]; + + int top = _topMargin + (region.top() * _fontHeight); + int linesToMove = region.height() - abs(lines); + int bytesToMove = linesToMove * + this->_columns * + sizeof(Character); + + Q_ASSERT( linesToMove > 0 ); + Q_ASSERT( bytesToMove > 0 ); + + //scroll internal image + if ( lines > 0 ) + { + // check that the memory areas that we are going to move are valid + Q_ASSERT( (char*)lastCharPos + bytesToMove < + (char*)(_image + (this->_lines * this->_columns)) ); + + Q_ASSERT( (lines*this->_columns) < _imageSize ); + + //scroll internal image down + memmove( firstCharPos , lastCharPos , bytesToMove ); + + //set region of display to scroll, making sure that + //the region aligns correctly to the character grid + scrollRect = QRect( _leftMargin , top, + this->_usedColumns * _fontWidth , + linesToMove * _fontHeight ); + } + else + { + // check that the memory areas that we are going to move are valid + Q_ASSERT( (char*)firstCharPos + bytesToMove < + (char*)(_image + (this->_lines * this->_columns)) ); + + //scroll internal image up + memmove( lastCharPos , firstCharPos , bytesToMove ); + + //set region of the display to scroll, making sure that + //the region aligns correctly to the character grid + QPoint topPoint( _leftMargin , top + abs(lines)*_fontHeight ); + + scrollRect = QRect( topPoint , + QSize( this->_usedColumns*_fontWidth , + linesToMove * _fontHeight )); + } + + //scroll the display vertically to match internal _image + scroll( 0 , _fontHeight * (-lines) , scrollRect ); +} + +QRegion TerminalDisplay::hotSpotRegion() const +{ + QRegion region; + foreach( Filter::HotSpot* hotSpot , _filterChain->hotSpots() ) + { + QRect rect; + rect.setLeft(hotSpot->startColumn()); + rect.setTop(hotSpot->startLine()); + rect.setRight(hotSpot->endColumn()); + rect.setBottom(hotSpot->endLine()); + + region |= imageToWidget(rect); + } + return region; +} + +void TerminalDisplay::processFilters() +{ + if (!_screenWindow) + return; + + QRegion preUpdateHotSpots = hotSpotRegion(); + + // use _screenWindow->getImage() here rather than _image because + // other classes may call processFilters() when this display's + // ScreenWindow emits a scrolled() signal - which will happen before + // updateImage() is called on the display and therefore _image is + // out of date at this point + _filterChain->setImage( _screenWindow->getImage(), + _screenWindow->windowLines(), + _screenWindow->windowColumns(), + _screenWindow->getLineProperties() ); + _filterChain->process(); + + QRegion postUpdateHotSpots = hotSpotRegion(); + + update( preUpdateHotSpots | postUpdateHotSpots ); +} + +void TerminalDisplay::updateImage() +{ + if ( !_screenWindow ) + return; + + // optimization - scroll the existing image where possible and + // avoid expensive text drawing for parts of the image that + // can simply be moved up or down + scrollImage( _screenWindow->scrollCount() , + _screenWindow->scrollRegion() ); + _screenWindow->resetScrollCount(); + + Character* const newimg = _screenWindow->getImage(); + int lines = _screenWindow->windowLines(); + int columns = _screenWindow->windowColumns(); + + setScroll( _screenWindow->currentLine() , _screenWindow->lineCount() ); + + if (!_image) + updateImageSize(); // Create _image + + Q_ASSERT( this->_usedLines <= this->_lines ); + Q_ASSERT( this->_usedColumns <= this->_columns ); + + int y,x,len; + + QPoint tL = contentsRect().topLeft(); + + int tLx = tL.x(); + int tLy = tL.y(); + _hasBlinker = false; + + CharacterColor cf; // undefined + CharacterColor _clipboard; // undefined + int cr = -1; // undefined + + const int linesToUpdate = qMin(this->_lines, qMax(0,lines )); + const int columnsToUpdate = qMin(this->_columns,qMax(0,columns)); + + QChar *disstrU = new QChar[columnsToUpdate]; + char *dirtyMask = new char[columnsToUpdate+2]; + QRegion dirtyRegion; + + // debugging variable, this records the number of lines that are found to + // be 'dirty' ( ie. have changed from the old _image to the new _image ) and + // which therefore need to be repainted + int dirtyLineCount = 0; + + for (y = 0; y < linesToUpdate; y++) + { + const Character* currentLine = &_image[y*this->_columns]; + const Character* const newLine = &newimg[y*columns]; + + bool updateLine = false; + + // The dirty mask indicates which characters need repainting. We also + // mark surrounding neighbours dirty, in case the character exceeds + // its cell boundaries + memset(dirtyMask, 0, columnsToUpdate+2); + + for( x = 0 ; x < columnsToUpdate ; x++) + { + if ( newLine[x] != currentLine[x] ) + { + dirtyMask[x] = true; + } + } + + if (!_resizing) // not while _resizing, we're expecting a paintEvent + for (x = 0; x < columnsToUpdate; x++) + { + _hasBlinker |= (newLine[x].rendition & RE_BLINK); + + // Start drawing if this character or the next one differs. + // We also take the next one into account to handle the situation + // where characters exceed their cell width. + if (dirtyMask[x]) + { + quint16 c = newLine[x+0].character; + if ( !c ) + continue; + int p = 0; + disstrU[p++] = c; //fontMap(c); + bool lineDraw = isLineChar(c); + bool doubleWidth = (x+1 == columnsToUpdate) ? false : (newLine[x+1].character == 0); + cr = newLine[x].rendition; + _clipboard = newLine[x].backgroundColor; + if (newLine[x].foregroundColor != cf) cf = newLine[x].foregroundColor; + int lln = columnsToUpdate - x; + for (len = 1; len < lln; len++) + { + const Character& ch = newLine[x+len]; + + if (!ch.character) + continue; // Skip trailing part of multi-col chars. + + bool nextIsDoubleWidth = (x+len+1 == columnsToUpdate) ? false : (newLine[x+len+1].character == 0); + + if ( ch.foregroundColor != cf || + ch.backgroundColor != _clipboard || + ch.rendition != cr || + !dirtyMask[x+len] || + isLineChar(c) != lineDraw || + nextIsDoubleWidth != doubleWidth ) + break; + + disstrU[p++] = c; //fontMap(c); + } + + QString unistr(disstrU, p); + + bool saveFixedFont = _fixedFont; + if (lineDraw) + _fixedFont = false; + if (doubleWidth) + _fixedFont = false; + + updateLine = true; + + _fixedFont = saveFixedFont; + x += len - 1; + } + + } + + //both the top and bottom halves of double height _lines must always be redrawn + //although both top and bottom halves contain the same characters, only + //the top one is actually + //drawn. + if (_lineProperties.count() > y) + updateLine |= (_lineProperties[y] & LINE_DOUBLEHEIGHT); + + // if the characters on the line are different in the old and the new _image + // then this line must be repainted. + if (updateLine) + { + dirtyLineCount++; + + // add the area occupied by this line to the region which needs to be + // repainted + QRect dirtyRect = QRect( _leftMargin+tLx , + _topMargin+tLy+_fontHeight*y , + _fontWidth * columnsToUpdate , + _fontHeight ); + + dirtyRegion |= dirtyRect; + } + + // replace the line of characters in the old _image with the + // current line of the new _image + memcpy((void*)currentLine,(const void*)newLine,columnsToUpdate*sizeof(Character)); + } + + // if the new _image is smaller than the previous _image, then ensure that the area + // outside the new _image is cleared + if ( linesToUpdate < _usedLines ) + { + dirtyRegion |= QRect( _leftMargin+tLx , + _topMargin+tLy+_fontHeight*linesToUpdate , + _fontWidth * this->_columns , + _fontHeight * (_usedLines-linesToUpdate) ); + } + _usedLines = linesToUpdate; + + if ( columnsToUpdate < _usedColumns ) + { + dirtyRegion |= QRect( _leftMargin+tLx+columnsToUpdate*_fontWidth , + _topMargin+tLy , + _fontWidth * (_usedColumns-columnsToUpdate) , + _fontHeight * this->_lines ); + } + _usedColumns = columnsToUpdate; + + dirtyRegion |= _inputMethodData.previousPreeditRect; + + // update the parts of the display which have changed + update(dirtyRegion); + + if ( _hasBlinker && !_blinkTimer->isActive()) _blinkTimer->start( BLINK_DELAY ); + if (!_hasBlinker && _blinkTimer->isActive()) { _blinkTimer->stop(); _blinking = false; } + delete[] dirtyMask; + delete[] disstrU; + +} + +void TerminalDisplay::showResizeNotification() +{ + if (_terminalSizeHint && isVisible()) + { + if (_terminalSizeStartup) { + _terminalSizeStartup=false; + return; + } + if (!_resizeWidget) + { + _resizeWidget = new QLabel(("Size: XXX x XXX"), this); + _resizeWidget->setMinimumWidth(_resizeWidget->fontMetrics().width(("Size: XXX x XXX"))); + _resizeWidget->setMinimumHeight(_resizeWidget->sizeHint().height()); + _resizeWidget->setAlignment(Qt::AlignCenter); + + _resizeWidget->setStyleSheet("background-color:palette(window);border-style:solid;border-width:1px;border-color:palette(dark)"); + + _resizeTimer = new QTimer(this); + _resizeTimer->setSingleShot(true); + connect(_resizeTimer, SIGNAL(timeout()), _resizeWidget, SLOT(hide())); + + } + QString sizeStr; + sizeStr.sprintf("Size: %d x %d", _columns, _lines); + _resizeWidget->setText(sizeStr); + _resizeWidget->move((width()-_resizeWidget->width())/2, + (height()-_resizeWidget->height())/2+20); + _resizeWidget->show(); + _resizeTimer->start(1000); + } +} + +void TerminalDisplay::setBlinkingCursor(bool blink) +{ + _hasBlinkingCursor=blink; + + if (blink && !_blinkCursorTimer->isActive()) + _blinkCursorTimer->start(BLINK_DELAY); + + if (!blink && _blinkCursorTimer->isActive()) + { + _blinkCursorTimer->stop(); + if (_cursorBlinking) + blinkCursorEvent(); + else + _cursorBlinking = false; + } +} + +void TerminalDisplay::paintEvent( QPaintEvent* pe ) +{ +//qDebug("%s %d paintEvent", __FILE__, __LINE__); + QPainter paint(this); + + foreach (QRect rect, (pe->region() & contentsRect()).rects()) + { + drawBackground(paint,rect,palette().background().color(), true /* use opacity setting */); + drawContents(paint, rect); + } +// drawBackground(paint,contentsRect(),palette().background().color(), true /* use opacity setting */); +// drawContents(paint, contentsRect()); + drawInputMethodPreeditString(paint,preeditRect()); + paintFilters(paint); + + paint.end(); +} + +QPoint TerminalDisplay::cursorPosition() const +{ + if (_screenWindow) + return _screenWindow->cursorPosition(); + else + return QPoint(0,0); +} + +QRect TerminalDisplay::preeditRect() const +{ + const int preeditLength = string_width(_inputMethodData.preeditString); + + if ( preeditLength == 0 ) + return QRect(); + + return QRect(_leftMargin + _fontWidth*cursorPosition().x(), + _topMargin + _fontHeight*cursorPosition().y(), + _fontWidth*preeditLength, + _fontHeight); +} + +void TerminalDisplay::drawInputMethodPreeditString(QPainter& painter , const QRect& rect) +{ + if ( _inputMethodData.preeditString.isEmpty() ) { + return; + } + const QPoint cursorPos = cursorPosition(); + + bool invertColors = false; + const QColor background = _colorTable[DEFAULT_BACK_COLOR].color; + const QColor foreground = _colorTable[DEFAULT_FORE_COLOR].color; + const Character* style = &_image[loc(cursorPos.x(),cursorPos.y())]; + + drawBackground(painter,rect,background,true); + drawCursor(painter,rect,foreground,background,invertColors); + drawCharacters(painter,rect,_inputMethodData.preeditString,style,invertColors); + + _inputMethodData.previousPreeditRect = rect; +} + +FilterChain* TerminalDisplay::filterChain() const +{ + return _filterChain; +} + +void TerminalDisplay::paintFilters(QPainter& painter) +{ +//qDebug("%s %d paintFilters", __FILE__, __LINE__); + + // get color of character under mouse and use it to draw + // lines for filters + QPoint cursorPos = mapFromGlobal(QCursor::pos()); + int cursorLine; + int cursorColumn; + getCharacterPosition( cursorPos , cursorLine , cursorColumn ); + Character cursorCharacter = _image[loc(cursorColumn,cursorLine)]; + + painter.setPen( QPen(cursorCharacter.foregroundColor.color(colorTable())) ); + + // iterate over hotspots identified by the display's currently active filters + // and draw appropriate visuals to indicate the presence of the hotspot + + QList<Filter::HotSpot*> spots = _filterChain->hotSpots(); + QListIterator<Filter::HotSpot*> iter(spots); + while (iter.hasNext()) + { + Filter::HotSpot* spot = iter.next(); + + for ( int line = spot->startLine() ; line <= spot->endLine() ; line++ ) + { + int startColumn = 0; + int endColumn = _columns-1; // TODO use number of _columns which are actually + // occupied on this line rather than the width of the + // display in _columns + + // ignore whitespace at the end of the lines + while ( QChar(_image[loc(endColumn,line)].character).isSpace() && endColumn > 0 ) + endColumn--; + + // increment here because the column which we want to set 'endColumn' to + // is the first whitespace character at the end of the line + endColumn++; + + if ( line == spot->startLine() ) + startColumn = spot->startColumn(); + if ( line == spot->endLine() ) + endColumn = spot->endColumn(); + + // subtract one pixel from + // the right and bottom so that + // we do not overdraw adjacent + // hotspots + // + // subtracting one pixel from all sides also prevents an edge case where + // moving the mouse outside a link could still leave it underlined + // because the check below for the position of the cursor + // finds it on the border of the target area + QRect r; + r.setCoords( startColumn*_fontWidth + 1, line*_fontHeight + 1, + endColumn*_fontWidth - 1, (line+1)*_fontHeight - 1 ); + + // Underline link hotspots + if ( spot->type() == Filter::HotSpot::Link ) + { + QFontMetrics metrics(font()); + + // find the baseline (which is the invisible line that the characters in the font sit on, + // with some having tails dangling below) + int baseline = r.bottom() - metrics.descent(); + // find the position of the underline below that + int underlinePos = baseline + metrics.underlinePos(); + + if ( r.contains( mapFromGlobal(QCursor::pos()) ) ) + painter.drawLine( r.left() , underlinePos , + r.right() , underlinePos ); + } + // Marker hotspots simply have a transparent rectanglular shape + // drawn on top of them + else if ( spot->type() == Filter::HotSpot::Marker ) + { + //TODO - Do not use a hardcoded colour for this + painter.fillRect(r,QBrush(QColor(255,0,0,120))); + } + } + } +} +void TerminalDisplay::drawContents(QPainter &paint, const QRect &rect) +{ +//qDebug("%s %d drawContents and rect x=%d y=%d w=%d h=%d", __FILE__, __LINE__, rect.x(), rect.y(),rect.width(),rect.height()); + + QPoint tL = contentsRect().topLeft(); +// int tLx = tL.x(); + int tLy = tL.y(); + + int tLx = (_contentWidth - _usedColumns * _fontWidth)/2; +// int tLy = (_contentHeight - _usedLines * _fontHeight)/2; +//qDebug("%d %d %d %d", tLx, tLy, _contentWidth, _usedColumns * _fontWidth); + + int lux = qMin(_usedColumns-1, qMax(0,(rect.left() - tLx - _leftMargin ) / _fontWidth)); + int luy = qMin(_usedLines-1, qMax(0, (rect.top() - tLy - _topMargin ) / _fontHeight)); + int rlx = qMin(_usedColumns-1, qMax(0, (rect.right() - tLx - _leftMargin ) / _fontWidth)); + int rly = qMin(_usedLines-1, qMax(0, (rect.bottom() - tLy - _topMargin ) / _fontHeight)); + + const int bufferSize = _usedColumns; + QChar *disstrU = new QChar[bufferSize]; + for (int y = luy; y <= rly; y++) + { + quint16 c = _image[loc(lux,y)].character; + int x = lux; + if(!c && x) + x--; // Search for start of multi-column character + for (; x <= rlx; x++) + { + int len = 1; + int p = 0; + + // is this a single character or a sequence of characters ? + if ( _image[loc(x,y)].rendition & RE_EXTENDED_CHAR ) + { + // sequence of characters + ushort extendedCharLength = 0; + ushort* chars = ExtendedCharTable::instance + .lookupExtendedChar(_image[loc(x,y)].charSequence,extendedCharLength); + for ( int index = 0 ; index < extendedCharLength ; index++ ) + { + Q_ASSERT( p < bufferSize ); + disstrU[p++] = chars[index]; + } + } + else + { + // single character + c = _image[loc(x,y)].character; + if (c) + { + Q_ASSERT( p < bufferSize ); + disstrU[p++] = c; //fontMap(c); + } + } + + bool lineDraw = isLineChar(c); + bool doubleWidth = (_image[ qMin(loc(x,y)+1,_imageSize) ].character == 0); + CharacterColor currentForeground = _image[loc(x,y)].foregroundColor; + CharacterColor currentBackground = _image[loc(x,y)].backgroundColor; + quint8 currentRendition = _image[loc(x,y)].rendition; + + while (x+len <= rlx && + _image[loc(x+len,y)].foregroundColor == currentForeground && + _image[loc(x+len,y)].backgroundColor == currentBackground && + _image[loc(x+len,y)].rendition == currentRendition && + (_image[ qMin(loc(x+len,y)+1,_imageSize) ].character == 0) == doubleWidth && + isLineChar( c = _image[loc(x+len,y)].character) == lineDraw) // Assignment! + { + if (c) + disstrU[p++] = c; //fontMap(c); + if (doubleWidth) // assert((_image[loc(x+len,y)+1].character == 0)), see above if condition + len++; // Skip trailing part of multi-column character + len++; + } + if ((x+len < _usedColumns) && (!_image[loc(x+len,y)].character)) + len++; // Adjust for trailing part of multi-column character + + bool save__fixedFont = _fixedFont; + if (lineDraw) + _fixedFont = false; + if (doubleWidth) + _fixedFont = false; + QString unistr(disstrU,p); + + if (y < _lineProperties.size()) + { + if (_lineProperties[y] & LINE_DOUBLEWIDTH) { + paint.scale(2,1); + } + + if (_lineProperties[y] & LINE_DOUBLEHEIGHT) { + paint.scale(1,2); + } + } + + //calculate the area in which the text will be drawn + QRect textArea = QRect( _leftMargin+tLx+_fontWidth*x , + _topMargin+tLy+_fontHeight*y , + _fontWidth*len, + _fontHeight); + + //move the calculated area to take account of scaling applied to the painter. + //the position of the area from the origin (0,0) is scaled + //by the opposite of whatever + //transformation has been applied to the painter. this ensures that + //painting does actually start from textArea.topLeft() + //(instead of textArea.topLeft() * painter-scale) + QMatrix inverted = paint.matrix().inverted(); +// textArea.moveTopLeft( inverted.map(textArea.topLeft()) ); + textArea.moveCenter( inverted.map(textArea.center()) ); + + + //paint text fragment + drawTextFragment( paint, + textArea, + unistr, + &_image[loc(x,y)] ); //, + //0, + //!_isPrinting ); + + _fixedFont = save__fixedFont; + + //reset back to single-width, single-height _lines + paint.resetMatrix(); + + if (y < _lineProperties.size()-1) + { + //double-height _lines are represented by two adjacent _lines + //containing the same characters + //both _lines will have the LINE_DOUBLEHEIGHT attribute. + //If the current line has the LINE_DOUBLEHEIGHT attribute, + //we can therefore skip the next line + if (_lineProperties[y] & LINE_DOUBLEHEIGHT) + y++; + } + + x += len - 1; + } + } + delete [] disstrU; +} + +void TerminalDisplay::blinkEvent() +{ + _blinking = !_blinking; + + //TODO: Optimise to only repaint the areas of the widget + // where there is blinking text + // rather than repainting the whole widget. + update(); +} + +QRect TerminalDisplay::imageToWidget(const QRect& imageArea) const +{ +//qDebug("%s %d imageToWidget", __FILE__, __LINE__); + QRect result; + result.setLeft( _leftMargin + _fontWidth * imageArea.left() ); + result.setTop( _topMargin + _fontHeight * imageArea.top() ); + result.setWidth( _fontWidth * imageArea.width() ); + result.setHeight( _fontHeight * imageArea.height() ); + + return result; +} + +void TerminalDisplay::blinkCursorEvent() +{ + _cursorBlinking = !_cursorBlinking; + + QRect cursorRect = imageToWidget( QRect(cursorPosition(),QSize(1,1)) ); + + update(cursorRect); +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Resizing */ +/* */ +/* ------------------------------------------------------------------------- */ + +void TerminalDisplay::resizeEvent(QResizeEvent*) +{ + updateImageSize(); +} + +void TerminalDisplay::propagateSize() +{ + if (_isFixedSize) + { + setSize(_columns, _lines); + QWidget::setFixedSize(sizeHint()); + parentWidget()->adjustSize(); + parentWidget()->setFixedSize(parentWidget()->sizeHint()); + return; + } + if (_image) + updateImageSize(); +} + +void TerminalDisplay::updateImageSize() +{ +//qDebug("%s %d updateImageSize", __FILE__, __LINE__); + Character* oldimg = _image; + int oldlin = _lines; + int oldcol = _columns; + + makeImage(); + + + // copy the old image to reduce flicker + int lines = qMin(oldlin,_lines); + int columns = qMin(oldcol,_columns); + + if (oldimg) + { + for (int line = 0; line < lines; line++) + { + memcpy((void*)&_image[_columns*line], + (void*)&oldimg[oldcol*line],columns*sizeof(Character)); + } + delete[] oldimg; + } + + if (_screenWindow) + _screenWindow->setWindowLines(_lines); + + _resizing = (oldlin!=_lines) || (oldcol!=_columns); + + if ( _resizing ) + { + showResizeNotification(); + emit changedContentSizeSignal(_contentHeight, _contentWidth); // expose resizeEvent + } + + _resizing = false; +} + +//showEvent and hideEvent are reimplemented here so that it appears to other classes that the +//display has been resized when the display is hidden or shown. +// +//this allows +//TODO: Perhaps it would be better to have separate signals for show and hide instead of using +//the same signal as the one for a content size change +void TerminalDisplay::showEvent(QShowEvent*) +{ + emit changedContentSizeSignal(_contentHeight,_contentWidth); +} +void TerminalDisplay::hideEvent(QHideEvent*) +{ + emit changedContentSizeSignal(_contentHeight,_contentWidth); +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Scrollbar */ +/* */ +/* ------------------------------------------------------------------------- */ + +void TerminalDisplay::scrollBarPositionChanged(int) +{ + if ( !_screenWindow ) + return; + + _screenWindow->scrollTo( _scrollBar->value() ); + + // if the thumb has been moved to the bottom of the _scrollBar then set + // the display to automatically track new output, + // that is, scroll down automatically + // to how new _lines as they are added + const bool atEndOfOutput = (_scrollBar->value() == _scrollBar->maximum()); + _screenWindow->setTrackOutput( atEndOfOutput ); + + updateImage(); +} + +void TerminalDisplay::setScroll(int cursor, int slines) +{ +//qDebug("%s %d setScroll", __FILE__, __LINE__); + // update _scrollBar if the range or value has changed, + // otherwise return + // + // setting the range or value of a _scrollBar will always trigger + // a repaint, so it should be avoided if it is not necessary + if ( _scrollBar->minimum() == 0 && + _scrollBar->maximum() == (slines - _lines) && + _scrollBar->value() == cursor ) + { + return; + } + + disconnect(_scrollBar, SIGNAL(valueChanged(int)), this, SLOT(scrollBarPositionChanged(int))); + _scrollBar->setRange(0,slines - _lines); + _scrollBar->setSingleStep(1); + _scrollBar->setPageStep(_lines); + _scrollBar->setValue(cursor); + connect(_scrollBar, SIGNAL(valueChanged(int)), this, SLOT(scrollBarPositionChanged(int))); +} + +void TerminalDisplay::setScrollBarPosition(ScrollBarPosition position) +{ + if (_scrollbarLocation == position) { +// return; + } + + if ( position == NoScrollBar ) + _scrollBar->hide(); + else + _scrollBar->show(); + + _topMargin = _leftMargin = 1; + _scrollbarLocation = position; + + propagateSize(); + update(); +} + +void TerminalDisplay::mousePressEvent(QMouseEvent* ev) +{ + if ( _possibleTripleClick && (ev->button()==Qt::LeftButton) ) { + mouseTripleClickEvent(ev); + return; + } + + if ( !contentsRect().contains(ev->pos()) ) return; + + if ( !_screenWindow ) return; + + int charLine; + int charColumn; + getCharacterPosition(ev->pos(),charLine,charColumn); + QPoint pos = QPoint(charColumn,charLine); + + if ( ev->button() == Qt::LeftButton) + { + _lineSelectionMode = false; + _wordSelectionMode = false; + + emit isBusySelecting(true); // Keep it steady... + // Drag only when the Control key is hold + bool selected = false; + + // The receiver of the testIsSelected() signal will adjust + // 'selected' accordingly. + //emit testIsSelected(pos.x(), pos.y(), selected); + + selected = _screenWindow->isSelected(pos.x(),pos.y()); + + if ((!_ctrlDrag || ev->modifiers() & Qt::ControlModifier) && selected ) { + // The user clicked inside selected text + dragInfo.state = diPending; + dragInfo.start = ev->pos(); + } + else { + // No reason to ever start a drag event + dragInfo.state = diNone; + + _preserveLineBreaks = !( ( ev->modifiers() & Qt::ControlModifier ) && !(ev->modifiers() & Qt::AltModifier) ); + _columnSelectionMode = (ev->modifiers() & Qt::AltModifier) && (ev->modifiers() & Qt::ControlModifier); + + if (_mouseMarks || (ev->modifiers() & Qt::ShiftModifier)) + { + _screenWindow->clearSelection(); + + //emit clearSelectionSignal(); + pos.ry() += _scrollBar->value(); + _iPntSel = _pntSel = pos; + _actSel = 1; // left mouse button pressed but nothing selected yet. + + } + else + { + emit mouseSignal( 0, charColumn + 1, charLine + 1 +_scrollBar->value() -_scrollBar->maximum() , 0); + } + } + } + else if ( ev->button() == Qt::MidButton ) + { + if ( _mouseMarks || (!_mouseMarks && (ev->modifiers() & Qt::ShiftModifier)) ) + emitSelection(true,ev->modifiers() & Qt::ControlModifier); + else + emit mouseSignal( 1, charColumn +1, charLine +1 +_scrollBar->value() -_scrollBar->maximum() , 0); + } + else if ( ev->button() == Qt::RightButton ) + { + if (_mouseMarks || (ev->modifiers() & Qt::ShiftModifier)) + { + emit configureRequest( this, + ev->modifiers() & (Qt::ShiftModifier|Qt::ControlModifier), + ev->pos() + ); + } + else + emit mouseSignal( 2, charColumn +1, charLine +1 +_scrollBar->value() -_scrollBar->maximum() , 0); + } +} + +QList<QAction*> TerminalDisplay::filterActions(const QPoint& position) +{ + int charLine, charColumn; + getCharacterPosition(position,charLine,charColumn); + + Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine,charColumn); + + return spot ? spot->actions() : QList<QAction*>(); +} + +void TerminalDisplay::mouseMoveEvent(QMouseEvent* ev) +{ + int charLine = 0; + int charColumn = 0; + + getCharacterPosition(ev->pos(),charLine,charColumn); + + // handle filters + // change link hot-spot appearance on mouse-over + Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine,charColumn); + if ( spot && spot->type() == Filter::HotSpot::Link) + { + QRect previousHotspotArea = _mouseOverHotspotArea; + _mouseOverHotspotArea.setCoords( qMin(spot->startColumn() , spot->endColumn()) * _fontWidth, + spot->startLine() * _fontHeight, + qMax(spot->startColumn() , spot->endColumn()) * _fontHeight, + (spot->endLine()+1) * _fontHeight ); + + // display tooltips when mousing over links + // TODO: Extend this to work with filter types other than links + const QString& tooltip = spot->tooltip(); + if ( !tooltip.isEmpty() ) + { + QToolTip::showText( mapToGlobal(ev->pos()) , tooltip , this , _mouseOverHotspotArea ); + } + + update( _mouseOverHotspotArea | previousHotspotArea ); + } + else if ( _mouseOverHotspotArea.isValid() ) + { + update( _mouseOverHotspotArea ); + // set hotspot area to an invalid rectangle + _mouseOverHotspotArea = QRect(); + } + + // for auto-hiding the cursor, we need mouseTracking + if (ev->buttons() == Qt::NoButton ) return; + + // if the terminal is interested in mouse movements + // then emit a mouse movement signal, unless the shift + // key is being held down, which overrides this. + if (!_mouseMarks && !(ev->modifiers() & Qt::ShiftModifier)) + { + int button = 3; + if (ev->buttons() & Qt::LeftButton) + button = 0; + if (ev->buttons() & Qt::MidButton) + button = 1; + if (ev->buttons() & Qt::RightButton) + button = 2; + + + emit mouseSignal( button, + charColumn + 1, + charLine + 1 +_scrollBar->value() -_scrollBar->maximum(), + 1 ); + + return; + } + + if (dragInfo.state == diPending) + { + // we had a mouse down, but haven't confirmed a drag yet + // if the mouse has moved sufficiently, we will confirm + + int distance = 10; //KGlobalSettings::dndEventDelay(); + if ( ev->x() > dragInfo.start.x() + distance || ev->x() < dragInfo.start.x() - distance || + ev->y() > dragInfo.start.y() + distance || ev->y() < dragInfo.start.y() - distance) + { + // we've left the drag square, we can start a real drag operation now + emit isBusySelecting(false); // Ok.. we can breath again. + + _screenWindow->clearSelection(); + doDrag(); + } + return; + } + else if (dragInfo.state == diDragging) + { + // this isn't technically needed because mouseMoveEvent is suppressed during + // Qt drag operations, replaced by dragMoveEvent + return; + } + + if (_actSel == 0) return; + + // don't extend selection while pasting + if (ev->buttons() & Qt::MidButton) return; + + extendSelection( ev->pos() ); +} + +#if 0 +void TerminalDisplay::setSelectionEnd() +{ + extendSelection( _configureRequestPoint ); +} +#endif + +void TerminalDisplay::extendSelection( const QPoint& position ) +{ + QPoint pos = position; + + if ( !_screenWindow ) + return; + + //if ( !contentsRect().contains(ev->pos()) ) return; + QPoint tL = contentsRect().topLeft(); + int tLx = tL.x(); + int tLy = tL.y(); + int scroll = _scrollBar->value(); + + // we're in the process of moving the mouse with the left button pressed + // the mouse cursor will kept caught within the bounds of the text in + // this widget. + + // Adjust position within text area bounds. See FIXME above. + QPoint oldpos = pos; + if ( pos.x() < tLx+_leftMargin ) + pos.setX( tLx+_leftMargin ); + if ( pos.x() > tLx+_leftMargin+_usedColumns*_fontWidth-1 ) + pos.setX( tLx+_leftMargin+_usedColumns*_fontWidth ); + if ( pos.y() < tLy+_topMargin ) + pos.setY( tLy+_topMargin ); + if ( pos.y() > tLy+_topMargin+_usedLines*_fontHeight-1 ) + pos.setY( tLy+_topMargin+_usedLines*_fontHeight-1 ); + + if ( pos.y() == tLy+_topMargin+_usedLines*_fontHeight-1 ) + { + _scrollBar->setValue(_scrollBar->value()+yMouseScroll); // scrollforward + } + if ( pos.y() == tLy+_topMargin ) + { + _scrollBar->setValue(_scrollBar->value()-yMouseScroll); // scrollback + } + + int charColumn = 0; + int charLine = 0; + getCharacterPosition(pos,charLine,charColumn); + + QPoint here = QPoint(charColumn,charLine); //QPoint((pos.x()-tLx-_leftMargin+(_fontWidth/2))/_fontWidth,(pos.y()-tLy-_topMargin)/_fontHeight); + QPoint ohere; + QPoint _iPntSelCorr = _iPntSel; + _iPntSelCorr.ry() -= _scrollBar->value(); + QPoint _pntSelCorr = _pntSel; + _pntSelCorr.ry() -= _scrollBar->value(); + bool swapping = false; + + if ( _wordSelectionMode ) + { + // Extend to word boundaries + int i; + int selClass; + + bool left_not_right = ( here.y() < _iPntSelCorr.y() || + here.y() == _iPntSelCorr.y() && here.x() < _iPntSelCorr.x() ); + bool old_left_not_right = ( _pntSelCorr.y() < _iPntSelCorr.y() || + _pntSelCorr.y() == _iPntSelCorr.y() && _pntSelCorr.x() < _iPntSelCorr.x() ); + swapping = left_not_right != old_left_not_right; + + // Find left (left_not_right ? from here : from start) + QPoint left = left_not_right ? here : _iPntSelCorr; + i = loc(left.x(),left.y()); + if (i>=0 && i<=_imageSize) { + selClass = charClass(_image[i].character); + while ( ((left.x()>0) || (left.y()>0 && (_lineProperties[left.y()-1] & LINE_WRAPPED) )) + && charClass(_image[i-1].character) == selClass ) + { i--; if (left.x()>0) left.rx()--; else {left.rx()=_usedColumns-1; left.ry()--;} } + } + + // Find left (left_not_right ? from start : from here) + QPoint right = left_not_right ? _iPntSelCorr : here; + i = loc(right.x(),right.y()); + if (i>=0 && i<=_imageSize) { + selClass = charClass(_image[i].character); + while( ((right.x()<_usedColumns-1) || (right.y()<_usedLines-1 && (_lineProperties[right.y()] & LINE_WRAPPED) )) + && charClass(_image[i+1].character) == selClass ) + { i++; if (right.x()<_usedColumns-1) right.rx()++; else {right.rx()=0; right.ry()++; } } + } + + // Pick which is start (ohere) and which is extension (here) + if ( left_not_right ) + { + here = left; ohere = right; + } + else + { + here = right; ohere = left; + } + ohere.rx()++; + } + + if ( _lineSelectionMode ) + { + // Extend to complete line + bool above_not_below = ( here.y() < _iPntSelCorr.y() ); + + QPoint above = above_not_below ? here : _iPntSelCorr; + QPoint below = above_not_below ? _iPntSelCorr : here; + + while (above.y()>0 && (_lineProperties[above.y()-1] & LINE_WRAPPED) ) + above.ry()--; + while (below.y()<_usedLines-1 && (_lineProperties[below.y()] & LINE_WRAPPED) ) + below.ry()++; + + above.setX(0); + below.setX(_usedColumns-1); + + // Pick which is start (ohere) and which is extension (here) + if ( above_not_below ) + { + here = above; ohere = below; + } + else + { + here = below; ohere = above; + } + + QPoint newSelBegin = QPoint( ohere.x(), ohere.y() ); + swapping = !(_tripleSelBegin==newSelBegin); + _tripleSelBegin = newSelBegin; + + ohere.rx()++; + } + + int offset = 0; + if ( !_wordSelectionMode && !_lineSelectionMode ) + { + int i; + int selClass; + + bool left_not_right = ( here.y() < _iPntSelCorr.y() || + here.y() == _iPntSelCorr.y() && here.x() < _iPntSelCorr.x() ); + bool old_left_not_right = ( _pntSelCorr.y() < _iPntSelCorr.y() || + _pntSelCorr.y() == _iPntSelCorr.y() && _pntSelCorr.x() < _iPntSelCorr.x() ); + swapping = left_not_right != old_left_not_right; + + // Find left (left_not_right ? from here : from start) + QPoint left = left_not_right ? here : _iPntSelCorr; + + // Find left (left_not_right ? from start : from here) + QPoint right = left_not_right ? _iPntSelCorr : here; + if ( right.x() > 0 && !_columnSelectionMode ) + { + i = loc(right.x(),right.y()); + if (i>=0 && i<=_imageSize) { + selClass = charClass(_image[i-1].character); + if (selClass == ' ') + { + while ( right.x() < _usedColumns-1 && charClass(_image[i+1].character) == selClass && (right.y()<_usedLines-1) && + !(_lineProperties[right.y()] & LINE_WRAPPED)) + { i++; right.rx()++; } + if (right.x() < _usedColumns-1) + right = left_not_right ? _iPntSelCorr : here; + else + right.rx()++; // will be balanced later because of offset=-1; + } + } + } + + // Pick which is start (ohere) and which is extension (here) + if ( left_not_right ) + { + here = left; ohere = right; offset = 0; + } + else + { + here = right; ohere = left; offset = -1; + } + } + + if ((here == _pntSelCorr) && (scroll == _scrollBar->value())) return; // not moved + + if (here == ohere) return; // It's not left, it's not right. + + if ( _actSel < 2 || swapping ) + { + if ( _columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode ) + { + _screenWindow->setSelectionStart( ohere.x() , ohere.y() , true ); + } + else + { + _screenWindow->setSelectionStart( ohere.x()-1-offset , ohere.y() , false ); + } + + } + + _actSel = 2; // within selection + _pntSel = here; + _pntSel.ry() += _scrollBar->value(); + + if ( _columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode ) + { + _screenWindow->setSelectionEnd( here.x() , here.y() ); + } + else + { + _screenWindow->setSelectionEnd( here.x()+offset , here.y() ); + } + +} + +void TerminalDisplay::mouseReleaseEvent(QMouseEvent* ev) +{ + if ( !_screenWindow ) + return; + + int charLine; + int charColumn; + getCharacterPosition(ev->pos(),charLine,charColumn); + + if ( ev->button() == Qt::LeftButton) + { + emit isBusySelecting(false); + if(dragInfo.state == diPending) + { + // We had a drag event pending but never confirmed. Kill selection + _screenWindow->clearSelection(); + //emit clearSelectionSignal(); + } + else + { + if ( _actSel > 1 ) + { + setSelection( _screenWindow->selectedText(_preserveLineBreaks) ); + } + + _actSel = 0; + + //FIXME: emits a release event even if the mouse is + // outside the range. The procedure used in `mouseMoveEvent' + // applies here, too. + + if (!_mouseMarks && !(ev->modifiers() & Qt::ShiftModifier)) + emit mouseSignal( 3, // release + charColumn + 1, + charLine + 1 +_scrollBar->value() -_scrollBar->maximum() , 0); + } + dragInfo.state = diNone; + } + + + if ( !_mouseMarks && + ((ev->button() == Qt::RightButton && !(ev->modifiers() & Qt::ShiftModifier)) + || ev->button() == Qt::MidButton) ) + { + emit mouseSignal( 3, + charColumn + 1, + charLine + 1 +_scrollBar->value() -_scrollBar->maximum() , + 0); + } +} + +void TerminalDisplay::getCharacterPosition(const QPoint& widgetPoint,int& line,int& column) const +{ + + column = (widgetPoint.x() + _fontWidth/2 -contentsRect().left()-_leftMargin) / _fontWidth; + line = (widgetPoint.y()-contentsRect().top()-_topMargin) / _fontHeight; + + if ( line < 0 ) + line = 0; + if ( column < 0 ) + column = 0; + + if ( line >= _usedLines ) + line = _usedLines-1; + + // the column value returned can be equal to _usedColumns, which + // is the position just after the last character displayed in a line. + // + // this is required so that the user can select characters in the right-most + // column (or left-most for right-to-left input) + if ( column > _usedColumns ) + column = _usedColumns; +} + +void TerminalDisplay::updateLineProperties() +{ + if ( !_screenWindow ) + return; + + _lineProperties = _screenWindow->getLineProperties(); +} + +void TerminalDisplay::mouseDoubleClickEvent(QMouseEvent* ev) +{ + if ( ev->button() != Qt::LeftButton) return; + if ( !_screenWindow ) return; + + int charLine = 0; + int charColumn = 0; + + getCharacterPosition(ev->pos(),charLine,charColumn); + + QPoint pos(charColumn,charLine); + + // pass on double click as two clicks. + if (!_mouseMarks && !(ev->modifiers() & Qt::ShiftModifier)) + { + // Send just _ONE_ click event, since the first click of the double click + // was already sent by the click handler + emit mouseSignal( 0, + pos.x()+1, + pos.y()+1 +_scrollBar->value() -_scrollBar->maximum(), + 0 ); // left button + return; + } + + _screenWindow->clearSelection(); + QPoint bgnSel = pos; + QPoint endSel = pos; + int i = loc(bgnSel.x(),bgnSel.y()); + _iPntSel = bgnSel; + _iPntSel.ry() += _scrollBar->value(); + + _wordSelectionMode = true; + + // find word boundaries... + int selClass = charClass(_image[i].character); + { + // find the start of the word + int x = bgnSel.x(); + while ( ((x>0) || (bgnSel.y()>0 && (_lineProperties[bgnSel.y()-1] & LINE_WRAPPED) )) + && charClass(_image[i-1].character) == selClass ) + { + i--; + if (x>0) + x--; + else + { + x=_usedColumns-1; + bgnSel.ry()--; + } + } + + bgnSel.setX(x); + _screenWindow->setSelectionStart( bgnSel.x() , bgnSel.y() , false ); + + // find the end of the word + i = loc( endSel.x(), endSel.y() ); + x = endSel.x(); + while( ((x<_usedColumns-1) || (endSel.y()<_usedLines-1 && (_lineProperties[endSel.y()] & LINE_WRAPPED) )) + && charClass(_image[i+1].character) == selClass ) + { + i++; + if (x<_usedColumns-1) + x++; + else + { + x=0; + endSel.ry()++; + } + } + + endSel.setX(x); + + // In word selection mode don't select @ (64) if at end of word. + if ( ( QChar( _image[i].character ) == '@' ) && ( ( endSel.x() - bgnSel.x() ) > 0 ) ) + endSel.setX( x - 1 ); + + + _actSel = 2; // within selection + + _screenWindow->setSelectionEnd( endSel.x() , endSel.y() ); + + setSelection( _screenWindow->selectedText(_preserveLineBreaks) ); + } + + _possibleTripleClick=true; + + QTimer::singleShot(QApplication::doubleClickInterval(),this, + SLOT(tripleClickTimeout())); +} + +void TerminalDisplay::wheelEvent( QWheelEvent* ev ) +{ + if (ev->orientation() != Qt::Vertical) + return; + + if ( _mouseMarks ) + _scrollBar->event(ev); + else + { + int charLine; + int charColumn; + getCharacterPosition( ev->pos() , charLine , charColumn ); + + emit mouseSignal( ev->delta() > 0 ? 4 : 5, + charColumn + 1, + charLine + 1 +_scrollBar->value() -_scrollBar->maximum() , + 0); + } +} + +void TerminalDisplay::tripleClickTimeout() +{ + _possibleTripleClick=false; +} + +void TerminalDisplay::mouseTripleClickEvent(QMouseEvent* ev) +{ + if ( !_screenWindow ) return; + + int charLine; + int charColumn; + getCharacterPosition(ev->pos(),charLine,charColumn); + _iPntSel = QPoint(charColumn,charLine); + + _screenWindow->clearSelection(); + + _lineSelectionMode = true; + _wordSelectionMode = false; + + _actSel = 2; // within selection + emit isBusySelecting(true); // Keep it steady... + + while (_iPntSel.y()>0 && (_lineProperties[_iPntSel.y()-1] & LINE_WRAPPED) ) + _iPntSel.ry()--; + + if (_tripleClickMode == SelectForwardsFromCursor) { + // find word boundary start + int i = loc(_iPntSel.x(),_iPntSel.y()); + int selClass = charClass(_image[i].character); + int x = _iPntSel.x(); + + while ( ((x>0) || + (_iPntSel.y()>0 && (_lineProperties[_iPntSel.y()-1] & LINE_WRAPPED) ) + ) + && charClass(_image[i-1].character) == selClass ) + { + i--; + if (x>0) + x--; + else + { + x=_columns-1; + _iPntSel.ry()--; + } + } + + _screenWindow->setSelectionStart( x , _iPntSel.y() , false ); + _tripleSelBegin = QPoint( x, _iPntSel.y() ); + } + else if (_tripleClickMode == SelectWholeLine) { + _screenWindow->setSelectionStart( 0 , _iPntSel.y() , false ); + _tripleSelBegin = QPoint( 0, _iPntSel.y() ); + } + + while (_iPntSel.y()<_lines-1 && (_lineProperties[_iPntSel.y()] & LINE_WRAPPED) ) + _iPntSel.ry()++; + + _screenWindow->setSelectionEnd( _columns - 1 , _iPntSel.y() ); + + setSelection(_screenWindow->selectedText(_preserveLineBreaks)); + + _iPntSel.ry() += _scrollBar->value(); +} + + +bool TerminalDisplay::focusNextPrevChild( bool next ) +{ + if (next) + return false; // This disables changing the active part in konqueror + // when pressing Tab + return QWidget::focusNextPrevChild( next ); +} + + +int TerminalDisplay::charClass(quint16 ch) const +{ + QChar qch=QChar(ch); + if ( qch.isSpace() ) return ' '; + + if ( qch.isLetterOrNumber() || _wordCharacters.contains(qch, Qt::CaseInsensitive ) ) + return 'a'; + + // Everything else is weird + return 1; +} + +void TerminalDisplay::setWordCharacters(const QString& wc) +{ + _wordCharacters = wc; +} + +void TerminalDisplay::setUsesMouse(bool on) +{ + _mouseMarks = on; + setCursor( _mouseMarks ? Qt::IBeamCursor : Qt::ArrowCursor ); +} +bool TerminalDisplay::usesMouse() const +{ + return _mouseMarks; +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Clipboard */ +/* */ +/* ------------------------------------------------------------------------- */ + +#undef KeyPress + +void TerminalDisplay::emitSelection(bool useXselection,bool appendReturn) +{ + if ( !_screenWindow ) + return; + + // Paste Clipboard by simulating keypress events + QString text = QApplication::clipboard()->text(useXselection ? QClipboard::Selection : + QClipboard::Clipboard); + if(appendReturn) + text.append("\r"); + if ( ! text.isEmpty() ) + { + text.replace("\n", "\r"); + QKeyEvent e(QEvent::KeyPress, 0, Qt::NoModifier, text); + emit keyPressedSignal(&e); // expose as a big fat keypress event + + _screenWindow->clearSelection(); + } +} + +void TerminalDisplay::setSelection(const QString& t) +{ + QApplication::clipboard()->setText(t, QClipboard::Selection); +} + +void TerminalDisplay::copyClipboard() +{ + if ( !_screenWindow ) + return; + + QString text = _screenWindow->selectedText(_preserveLineBreaks); + QApplication::clipboard()->setText(text); +} + +void TerminalDisplay::pasteClipboard() +{ + emitSelection(false,false); +} + +void TerminalDisplay::pasteSelection() +{ + emitSelection(true,false); +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Keyboard */ +/* */ +/* ------------------------------------------------------------------------- */ + +void TerminalDisplay::setFlowControlWarningEnabled( bool enable ) +{ + _flowControlWarningEnabled = enable; + + // if the dialog is currently visible and the flow control warning has + // been disabled then hide the dialog + if (!enable) + outputSuspended(false); +} + +void TerminalDisplay::keyPressEvent( QKeyEvent* event ) +{ +//qDebug("%s %d keyPressEvent and key is %d", __FILE__, __LINE__, event->key()); + + bool emitKeyPressSignal = true; + + // XonXoff flow control + if (event->modifiers() & Qt::ControlModifier && _flowControlWarningEnabled) + { + if ( event->key() == Qt::Key_S ) { + //qDebug("%s %d keyPressEvent, output suspended", __FILE__, __LINE__); + emit flowControlKeyPressed(true /*output suspended*/); + } + else if ( event->key() == Qt::Key_Q ) { + //qDebug("%s %d keyPressEvent, output enabled", __FILE__, __LINE__); + emit flowControlKeyPressed(false /*output enabled*/); + } + } + + // Keyboard-based navigation + if ( event->modifiers() == Qt::ShiftModifier ) + { + bool update = true; + + if ( event->key() == Qt::Key_PageUp ) + { + //qDebug("%s %d pageup", __FILE__, __LINE__); + _screenWindow->scrollBy( ScreenWindow::ScrollPages , -1 ); + } + else if ( event->key() == Qt::Key_PageDown ) + { + //qDebug("%s %d pagedown", __FILE__, __LINE__); + _screenWindow->scrollBy( ScreenWindow::ScrollPages , 1 ); + } + else if ( event->key() == Qt::Key_Up ) + { + //qDebug("%s %d keyup", __FILE__, __LINE__); + _screenWindow->scrollBy( ScreenWindow::ScrollLines , -1 ); + } + else if ( event->key() == Qt::Key_Down ) + { + //qDebug("%s %d keydown", __FILE__, __LINE__); + _screenWindow->scrollBy( ScreenWindow::ScrollLines , 1 ); + } + else { + update = false; + } + + if ( update ) + { + //qDebug("%s %d updating", __FILE__, __LINE__); + _screenWindow->setTrackOutput( _screenWindow->atEndOfOutput() ); + + updateLineProperties(); + updateImage(); + + // do not send key press to terminal + emitKeyPressSignal = false; + } + } + + _screenWindow->setTrackOutput( true ); + + _actSel=0; // Key stroke implies a screen update, so TerminalDisplay won't + // know where the current selection is. + + if (_hasBlinkingCursor) + { + _blinkCursorTimer->start(BLINK_DELAY); + if (_cursorBlinking) + blinkCursorEvent(); + else + _cursorBlinking = false; + } + + if ( emitKeyPressSignal ) + emit keyPressedSignal(event); + + event->accept(); +} + +void TerminalDisplay::inputMethodEvent( QInputMethodEvent* event ) +{ + QKeyEvent keyEvent(QEvent::KeyPress,0,Qt::NoModifier,event->commitString()); + emit keyPressedSignal(&keyEvent); + + _inputMethodData.preeditString = event->preeditString(); + update(preeditRect() | _inputMethodData.previousPreeditRect); + + event->accept(); +} +QVariant TerminalDisplay::inputMethodQuery( Qt::InputMethodQuery query ) const +{ + const QPoint cursorPos = _screenWindow ? _screenWindow->cursorPosition() : QPoint(0,0); + switch ( query ) + { + case Qt::ImMicroFocus: + return imageToWidget(QRect(cursorPos.x(),cursorPos.y(),1,1)); + break; + case Qt::ImFont: + return font(); + break; + case Qt::ImCursorPosition: + // return the cursor position within the current line + return cursorPos.x(); + break; + case Qt::ImSurroundingText: + { + // return the text from the current line + QString lineText; + QTextStream stream(&lineText); + PlainTextDecoder decoder; + decoder.begin(&stream); + decoder.decodeLine(&_image[loc(0,cursorPos.y())],_usedColumns,_lineProperties[cursorPos.y()]); + decoder.end(); + return lineText; + } + break; + case Qt::ImCurrentSelection: + return QString(); + break; + } + + return QVariant(); +} + +bool TerminalDisplay::event( QEvent *e ) +{ + if ( e->type() == QEvent::ShortcutOverride ) + { + QKeyEvent* keyEvent = static_cast<QKeyEvent *>( e ); + + // a check to see if keyEvent->text() is empty is used + // to avoid intercepting the press of the modifier key on its own. + // + // this is important as it allows a press and release of the Alt key + // on its own to focus the menu bar, making it possible to + // work with the menu without using the mouse + if ( (keyEvent->modifiers() == Qt::AltModifier) && + !keyEvent->text().isEmpty() ) + { + keyEvent->accept(); + return true; + } + + // Override any of the following shortcuts because + // they are needed by the terminal + int keyCode = keyEvent->key() | keyEvent->modifiers(); + switch ( keyCode ) + { + // list is taken from the QLineEdit::event() code + case Qt::Key_Tab: + case Qt::Key_Delete: + case Qt::Key_Home: + case Qt::Key_End: + case Qt::Key_Backspace: + case Qt::Key_Left: + case Qt::Key_Right: + keyEvent->accept(); + return true; + } + } + return QWidget::event( e ); +} + +void TerminalDisplay::setBellMode(int mode) +{ + _bellMode=mode; +} + +void TerminalDisplay::enableBell() +{ + _allowBell = true; +} + +void TerminalDisplay::bell(const QString&) +{ + if (_bellMode==NoBell) return; + + //limit the rate at which bells can occur + //...mainly for sound effects where rapid bells in sequence + //produce a horrible noise + if ( _allowBell ) + { + _allowBell = false; + QTimer::singleShot(500,this,SLOT(enableBell())); + + if (_bellMode==SystemBeepBell) + { +// KNotification::beep(); + } + else if (_bellMode==NotifyBell) + { +// KNotification::event("BellVisible", message,QPixmap(),this); + } + else if (_bellMode==VisualBell) + { + swapColorTable(); + QTimer::singleShot(200,this,SLOT(swapColorTable())); + } + } +} + +void TerminalDisplay::swapColorTable() +{ + ColorEntry color = _colorTable[1]; + _colorTable[1]=_colorTable[0]; + _colorTable[0]= color; + _colorsInverted = !_colorsInverted; + update(); +} + +void TerminalDisplay::clearImage() +{ + // We initialize _image[_imageSize] too. See makeImage() + for (int i = 0; i <= _imageSize; i++) + { + _image[i].character = ' '; + _image[i].foregroundColor = CharacterColor(COLOR_SPACE_DEFAULT, + DEFAULT_FORE_COLOR); + _image[i].backgroundColor = CharacterColor(COLOR_SPACE_DEFAULT, + DEFAULT_BACK_COLOR); + _image[i].rendition = DEFAULT_RENDITION; + } +} + +void TerminalDisplay::calcGeometry() +{ + _scrollBar->resize(QApplication::style()->pixelMetric(QStyle::PM_ScrollBarExtent), + contentsRect().height()); + switch(_scrollbarLocation) + { + case NoScrollBar : + _leftMargin = DEFAULT_LEFT_MARGIN; + _contentWidth = contentsRect().width() - 2 * DEFAULT_LEFT_MARGIN; + break; + case ScrollBarLeft : + _leftMargin = DEFAULT_LEFT_MARGIN + _scrollBar->width(); + _contentWidth = contentsRect().width() - 2 * DEFAULT_LEFT_MARGIN - _scrollBar->width(); + _scrollBar->move(contentsRect().topLeft()); + break; + case ScrollBarRight: + _leftMargin = DEFAULT_LEFT_MARGIN; + _contentWidth = contentsRect().width() - 2 * DEFAULT_LEFT_MARGIN - _scrollBar->width(); + _scrollBar->move(contentsRect().topRight() - QPoint(_scrollBar->width()-1,0)); + break; + } + + _topMargin = DEFAULT_TOP_MARGIN; + _contentHeight = contentsRect().height() - 2 * DEFAULT_TOP_MARGIN + /* mysterious */ 1; + + if (!_isFixedSize) + { + // ensure that display is always at least one column wide + _columns = qMax(1,_contentWidth / _fontWidth); + _usedColumns = qMin(_usedColumns,_columns); + + // ensure that display is always at least one line high + _lines = qMax(1,_contentHeight / _fontHeight); + _usedLines = qMin(_usedLines,_lines); + } +} + +void TerminalDisplay::makeImage() +{ +//qDebug("%s %d makeImage", __FILE__, __LINE__); + calcGeometry(); + + // confirm that array will be of non-zero size, since the painting code + // assumes a non-zero array length + Q_ASSERT( _lines > 0 && _columns > 0 ); + Q_ASSERT( _usedLines <= _lines && _usedColumns <= _columns ); + + _imageSize=_lines*_columns; + + // We over-commit one character so that we can be more relaxed in dealing with + // certain boundary conditions: _image[_imageSize] is a valid but unused position + _image = new Character[_imageSize+1]; + + clearImage(); +} + +// calculate the needed size +void TerminalDisplay::setSize(int columns, int lines) +{ + //FIXME - Not quite correct, a small amount of additional space + // will be used for margins, the scrollbar etc. + // we need to allow for this so that '_size' does allow + // enough room for the specified number of columns and lines to fit + + QSize newSize = QSize( columns * _fontWidth , + lines * _fontHeight ); + + if ( newSize != size() ) + { + _size = newSize; + updateGeometry(); + } +} + +void TerminalDisplay::setFixedSize(int cols, int lins) +{ + _isFixedSize = true; + + //ensure that display is at least one line by one column in size + _columns = qMax(1,cols); + _lines = qMax(1,lins); + _usedColumns = qMin(_usedColumns,_columns); + _usedLines = qMin(_usedLines,_lines); + + if (_image) + { + delete[] _image; + makeImage(); + } + setSize(cols, lins); + QWidget::setFixedSize(_size); +} + +QSize TerminalDisplay::sizeHint() const +{ + return _size; +} + + +/* --------------------------------------------------------------------- */ +/* */ +/* Drag & Drop */ +/* */ +/* --------------------------------------------------------------------- */ + +void TerminalDisplay::dragEnterEvent(QDragEnterEvent* event) +{ + if (event->mimeData()->hasFormat("text/plain")) + event->acceptProposedAction(); +} + +void TerminalDisplay::dropEvent(QDropEvent* event) +{ +// KUrl::List urls = KUrl::List::fromMimeData(event->mimeData()); + + QString dropText; +/* if (!urls.isEmpty()) + { + for ( int i = 0 ; i < urls.count() ; i++ ) + { + KUrl url = KIO::NetAccess::mostLocalUrl( urls[i] , 0 ); + QString urlText; + + if (url.isLocalFile()) + urlText = url.path(); + else + urlText = url.url(); + + // in future it may be useful to be able to insert file names with drag-and-drop + // without quoting them (this only affects paths with spaces in) + urlText = KShell::quoteArg(urlText); + + dropText += urlText; + + if ( i != urls.count()-1 ) + dropText += ' '; + } + } + else + { + dropText = event->mimeData()->text(); + } +*/ + if(event->mimeData()->hasFormat("text/plain")) + { + emit sendStringToEmu(dropText.toLocal8Bit()); + } +} + +void TerminalDisplay::doDrag() +{ + dragInfo.state = diDragging; + dragInfo.dragObject = new QDrag(this); + QMimeData *mimeData = new QMimeData; + mimeData->setText(QApplication::clipboard()->text(QClipboard::Selection)); + dragInfo.dragObject->setMimeData(mimeData); + dragInfo.dragObject->start(Qt::CopyAction); + // Don't delete the QTextDrag object. Qt will delete it when it's done with it. +} + +void TerminalDisplay::outputSuspended(bool suspended) +{ + //create the label when this function is first called + if (!_outputSuspendedLabel) + { + //This label includes a link to an English language website + //describing the 'flow control' (Xon/Xoff) feature found in almost + //all terminal emulators. + //If there isn't a suitable article available in the target language the link + //can simply be removed. + _outputSuspendedLabel = new QLabel( ("<qt>Output has been " + "<a href=\"http://en.wikipedia.org/wiki/XON\">suspended</a>" + " by pressing Ctrl+S." + " Press <b>Ctrl+Q</b> to resume.</qt>"), + this ); + + QPalette palette(_outputSuspendedLabel->palette()); + + palette.setColor(QPalette::Normal, QPalette::WindowText, QColor(Qt::white)); + palette.setColor(QPalette::Normal, QPalette::Window, QColor(Qt::black)); +// KColorScheme::adjustForeground(palette,KColorScheme::NeutralText); +// KColorScheme::adjustBackground(palette,KColorScheme::NeutralBackground); + _outputSuspendedLabel->setPalette(palette); + _outputSuspendedLabel->setAutoFillBackground(true); + _outputSuspendedLabel->setBackgroundRole(QPalette::Base); + _outputSuspendedLabel->setFont(QApplication::font()); + _outputSuspendedLabel->setMargin(5); + + //enable activation of "Xon/Xoff" link in label + _outputSuspendedLabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse | + Qt::LinksAccessibleByKeyboard); + _outputSuspendedLabel->setOpenExternalLinks(true); + _outputSuspendedLabel->setVisible(false); + + _gridLayout->addWidget(_outputSuspendedLabel); + _gridLayout->addItem( new QSpacerItem(0,0,QSizePolicy::Expanding, + QSizePolicy::Expanding), + 1,0); + + } + + _outputSuspendedLabel->setVisible(suspended); +} + +uint TerminalDisplay::lineSpacing() const +{ + return _lineSpacing; +} + +void TerminalDisplay::setLineSpacing(uint i) +{ + _lineSpacing = i; + setVTFont(font()); // Trigger an update. +} + +//#include "moc_TerminalDisplay.cpp"
new file mode 100644 --- /dev/null +++ b/gui//TerminalDisplay.h @@ -0,0 +1,754 @@ +/* + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef TERMINALDISPLAY_H +#define TERMINALDISPLAY_H + +// Qt +#include <QtGui/QColor> +#include <QtCore/QPointer> +#include <QtGui/QWidget> + +// Konsole +#include "Filter.h" +#include "Character.h" +#include "ColorTables.h" + +class QDrag; +class QDragEnterEvent; +class QDropEvent; +class QLabel; +class QTimer; +class QEvent; +class QFrame; +class QGridLayout; +class QKeyEvent; +class QScrollBar; +class QShowEvent; +class QHideEvent; +class QWidget; + +//class KMenu; + +namespace Konsole +{ + +extern unsigned short vt100_graphics[32]; + +class ScreenWindow; + +/** + * A widget which displays output from a terminal emulation and sends input keypresses and mouse activity + * to the terminal. + * + * When the terminal emulation receives new output from the program running in the terminal, + * it will update the display by calling updateImage(). + * + * TODO More documentation + */ +class TerminalDisplay : public QWidget +{ + Q_OBJECT + +public: + /** Constructs a new terminal display widget with the specified parent. */ + TerminalDisplay(QWidget *parent=0); + virtual ~TerminalDisplay(); + + /** Returns the terminal color palette used by the display. */ + const ColorEntry* colorTable() const; + /** Sets the terminal color palette used by the display. */ + void setColorTable(const ColorEntry table[]); + /** + * Sets the seed used to generate random colors for the display + * (in color schemes that support them). + */ + void setRandomSeed(uint seed); + /** + * Returns the seed used to generate random colors for the display + * (in color schemes that support them). + */ + uint randomSeed() const; + + /** Sets the opacity of the terminal display. */ + void setOpacity(qreal opacity); + + /** + * This enum describes the location where the scroll bar is positioned in the display widget. + */ + enum ScrollBarPosition + { + /** Do not show the scroll bar. */ + NoScrollBar=0, + /** Show the scroll bar on the left side of the display. */ + ScrollBarLeft=1, + /** Show the scroll bar on the right side of the display. */ + ScrollBarRight=2 + }; + /** + * Specifies whether the terminal display has a vertical scroll bar, and if so whether it + * is shown on the left or right side of the display. + */ + void setScrollBarPosition(ScrollBarPosition position); + + /** + * Sets the current position and range of the display's scroll bar. + * + * @param cursor The position of the scroll bar's thumb. + * @param lines The maximum value of the scroll bar. + */ + void setScroll(int cursor, int lines); + + /** + * Returns the display's filter chain. When the image for the display is updated, + * the text is passed through each filter in the chain. Each filter can define + * hotspots which correspond to certain strings (such as URLs or particular words). + * Depending on the type of the hotspots created by the filter ( returned by Filter::Hotspot::type() ) + * the view will draw visual cues such as underlines on mouse-over for links or translucent + * rectangles for markers. + * + * To add a new filter to the view, call: + * viewWidget->filterChain()->addFilter( filterObject ); + */ + FilterChain* filterChain() const; + + /** + * Updates the filters in the display's filter chain. This will cause + * the hotspots to be updated to match the current image. + * + * WARNING: This function can be expensive depending on the + * image size and number of filters in the filterChain() + * + * TODO - This API does not really allow efficient usage. Revise it so + * that the processing can be done in a better way. + * + * eg: + * - Area of interest may be known ( eg. mouse cursor hovering + * over an area ) + */ + void processFilters(); + + /** + * Returns a list of menu actions created by the filters for the content + * at the given @p position. + */ + QList<QAction*> filterActions(const QPoint& position); + + /** Returns true if the cursor is set to blink or false otherwise. */ + bool blinkingCursor() { return _hasBlinkingCursor; } + /** Specifies whether or not the cursor blinks. */ + void setBlinkingCursor(bool blink); + + void setCtrlDrag(bool enable) { _ctrlDrag=enable; } + bool ctrlDrag() { return _ctrlDrag; } + + /** + * This enum describes the methods for selecting text when + * the user triple-clicks within the display. + */ + enum TripleClickMode + { + /** Select the whole line underneath the cursor. */ + SelectWholeLine, + /** Select from the current cursor position to the end of the line. */ + SelectForwardsFromCursor + }; + /** Sets how the text is selected when the user triple clicks within the display. */ + void setTripleClickMode(TripleClickMode mode) { _tripleClickMode = mode; } + /** See setTripleClickSelectionMode() */ + TripleClickMode tripleClickMode() { return _tripleClickMode; } + + void setLineSpacing(uint); + uint lineSpacing() const; + + void emitSelection(bool useXselection,bool appendReturn); + + /** + * This enum describes the available shapes for the keyboard cursor. + * See setKeyboardCursorShape() + */ + enum KeyboardCursorShape + { + /** A rectangular block which covers the entire area of the cursor character. */ + BlockCursor, + /** + * A single flat line which occupies the space at the bottom of the cursor + * character's area. + */ + UnderlineCursor, + /** + * An cursor shaped like the capital letter 'I', similar to the IBeam + * cursor used in Qt/KDE text editors. + */ + IBeamCursor + }; + /** + * Sets the shape of the keyboard cursor. This is the cursor drawn + * at the position in the terminal where keyboard input will appear. + * + * In addition the terminal display widget also has a cursor for + * the mouse pointer, which can be set using the QWidget::setCursor() + * method. + * + * Defaults to BlockCursor + */ + void setKeyboardCursorShape(KeyboardCursorShape shape); + /** + * Returns the shape of the keyboard cursor. See setKeyboardCursorShape() + */ + KeyboardCursorShape keyboardCursorShape() const; + + /** + * Sets the color used to draw the keyboard cursor. + * + * The keyboard cursor defaults to using the foreground color of the character + * underneath it. + * + * @param useForegroundColor If true, the cursor color will change to match + * the foreground color of the character underneath it as it is moved, in this + * case, the @p color parameter is ignored and the color of the character + * under the cursor is inverted to ensure that it is still readable. + * @param color The color to use to draw the cursor. This is only taken into + * account if @p useForegroundColor is false. + */ + void setKeyboardCursorColor(bool useForegroundColor , const QColor& color); + + /** + * Returns the color of the keyboard cursor, or an invalid color if the keyboard + * cursor color is set to change according to the foreground color of the character + * underneath it. + */ + QColor keyboardCursorColor() const; + + /** + * Returns the number of lines of text which can be displayed in the widget. + * + * This will depend upon the height of the widget and the current font. + * See fontHeight() + */ + int lines() { return _lines; } + /** + * Returns the number of characters of text which can be displayed on + * each line in the widget. + * + * This will depend upon the width of the widget and the current font. + * See fontWidth() + */ + int columns() { return _columns; } + + /** + * Returns the height of the characters in the font used to draw the text in the display. + */ + int fontHeight() { return _fontHeight; } + /** + * Returns the width of the characters in the display. + * This assumes the use of a fixed-width font. + */ + int fontWidth() { return _fontWidth; } + + void setSize(int cols, int lins); + void setFixedSize(int cols, int lins); + + // reimplemented + QSize sizeHint() const; + + /** + * Sets which characters, in addition to letters and numbers, + * are regarded as being part of a word for the purposes + * of selecting words in the display by double clicking on them. + * + * The word boundaries occur at the first and last characters which + * are either a letter, number, or a character in @p wc + * + * @param wc An array of characters which are to be considered parts + * of a word ( in addition to letters and numbers ). + */ + void setWordCharacters(const QString& wc); + /** + * Returns the characters which are considered part of a word for the + * purpose of selecting words in the display with the mouse. + * + * @see setWordCharacters() + */ + QString wordCharacters() { return _wordCharacters; } + + /** + * Sets the type of effect used to alert the user when a 'bell' occurs in the + * terminal session. + * + * The terminal session can trigger the bell effect by calling bell() with + * the alert message. + */ + void setBellMode(int mode); + /** + * Returns the type of effect used to alert the user when a 'bell' occurs in + * the terminal session. + * + * See setBellMode() + */ + int bellMode() { return _bellMode; } + + /** + * This enum describes the different types of sounds and visual effects which + * can be used to alert the user when a 'bell' occurs in the terminal + * session. + */ + enum BellMode + { + /** A system beep. */ + SystemBeepBell=0, + /** + * KDE notification. This may play a sound, show a passive popup + * or perform some other action depending on the user's settings. + */ + NotifyBell=1, + /** A silent, visual bell (eg. inverting the display's colors briefly) */ + VisualBell=2, + /** No bell effects */ + NoBell=3 + }; + + void setSelection(const QString &t); + + /** + * Reimplemented. Has no effect. Use setVTFont() to change the font + * used to draw characters in the display. + */ + virtual void setFont(const QFont &); + + /** Returns the font used to draw characters in the display */ + QFont getVTFont() { return font(); } + + /** + * Sets the font used to draw the display. Has no effect if @p font + * is larger than the size of the display itself. + */ + void setVTFont(const QFont& font); + + /** + * Specified whether anti-aliasing of text in the terminal display + * is enabled or not. Defaults to enabled. + */ + static void setAntialias( bool antialias ) { _antialiasText = antialias; } + /** + * Returns true if anti-aliasing of text in the terminal is enabled. + */ + static bool antialias() { return _antialiasText; } + + /** + * Sets whether or not the current height and width of the + * terminal in lines and columns is displayed whilst the widget + * is being resized. + */ + void setTerminalSizeHint(bool on) { _terminalSizeHint=on; } + /** + * Returns whether or not the current height and width of + * the terminal in lines and columns is displayed whilst the widget + * is being resized. + */ + bool terminalSizeHint() { return _terminalSizeHint; } + /** + * Sets whether the terminal size display is shown briefly + * after the widget is first shown. + * + * See setTerminalSizeHint() , isTerminalSizeHint() + */ + void setTerminalSizeStartup(bool on) { _terminalSizeStartup=on; } + + void setBidiEnabled(bool set) { _bidiEnabled=set; } + bool isBidiEnabled() { return _bidiEnabled; } + + /** + * Sets the terminal screen section which is displayed in this widget. + * When updateImage() is called, the display fetches the latest character image from the + * the associated terminal screen window. + * + * In terms of the model-view paradigm, the ScreenWindow is the model which is rendered + * by the TerminalDisplay. + */ + void setScreenWindow( ScreenWindow* window ); + /** Returns the terminal screen section which is displayed in this widget. See setScreenWindow() */ + ScreenWindow* screenWindow() const; + + static bool HAVE_TRANSPARENCY; + +public slots: + + /** + * Causes the terminal display to fetch the latest character image from the associated + * terminal screen ( see setScreenWindow() ) and redraw the display. + */ + void updateImage(); + /** + * Causes the terminal display to fetch the latest line status flags from the + * associated terminal screen ( see setScreenWindow() ). + */ + void updateLineProperties(); + + /** Copies the selected text to the clipboard. */ + void copyClipboard(); + /** + * Pastes the content of the clipboard into the + * display. + */ + void pasteClipboard(); + /** + * Pastes the content of the selection into the + * display. + */ + void pasteSelection(); + + /** + * Changes whether the flow control warning box should be shown when the flow control + * stop key (Ctrl+S) are pressed. + */ + void setFlowControlWarningEnabled(bool enabled); + + /** + * Causes the widget to display or hide a message informing the user that terminal + * output has been suspended (by using the flow control key combination Ctrl+S) + * + * @param suspended True if terminal output has been suspended and the warning message should + * be shown or false to indicate that terminal output has been resumed and that + * the warning message should disappear. + */ + void outputSuspended(bool suspended); + + /** + * Sets whether the program whoose output is being displayed in the view + * is interested in mouse events. + * + * If this is set to true, mouse signals will be emitted by the view when the user clicks, drags + * or otherwise moves the mouse inside the view. + * The user interaction needed to create selections will also change, and the user will be required + * to hold down the shift key to create a selection or perform other mouse activities inside the + * view area - since the program running in the terminal is being allowed to handle normal mouse + * events itself. + * + * @param usesMouse Set to true if the program running in the terminal is interested in mouse events + * or false otherwise. + */ + void setUsesMouse(bool usesMouse); + + /** See setUsesMouse() */ + bool usesMouse() const; + + /** + * Shows a notification that a bell event has occurred in the terminal. + * TODO: More documentation here + */ + void bell(const QString& message); + +signals: + + /** + * Emitted when the user presses a key whilst the terminal widget has focus. + */ + void keyPressedSignal(QKeyEvent *e); + + /** + * Emitted when the user presses the suspend or resume flow control key combinations + * + * @param suspend true if the user pressed Ctrl+S (the suspend output key combination) or + * false if the user pressed Ctrl+Q (the resume output key combination) + */ + void flowControlKeyPressed(bool suspend); + + /** + * A mouse event occurred. + * @param button The mouse button (0 for left button, 1 for middle button, 2 for right button, 3 for release) + * @param column The character column where the event occurred + * @param line The character row where the event occurred + * @param eventType The type of event. 0 for a mouse press / release or 1 for mouse motion + */ + void mouseSignal(int button, int column, int line, int eventType); + void changedFontMetricSignal(int height, int width); + void changedContentSizeSignal(int height, int width); + + /** + * Emitted when the user right clicks on the display, or right-clicks with the Shift + * key held down if usesMouse() is true. + * + * This can be used to display a context menu. + */ + void configureRequest( TerminalDisplay*, int state, const QPoint& position ); + + void isBusySelecting(bool); + void sendStringToEmu(const char*); + +protected: + virtual bool event( QEvent * ); + + virtual void paintEvent( QPaintEvent * ); + + virtual void showEvent(QShowEvent*); + virtual void hideEvent(QHideEvent*); + virtual void resizeEvent(QResizeEvent*); + + virtual void fontChange(const QFont &font); + + virtual void keyPressEvent(QKeyEvent* event); + virtual void mouseDoubleClickEvent(QMouseEvent* ev); + virtual void mousePressEvent( QMouseEvent* ); + virtual void mouseReleaseEvent( QMouseEvent* ); + virtual void mouseMoveEvent( QMouseEvent* ); + virtual void extendSelection( const QPoint& pos ); + virtual void wheelEvent( QWheelEvent* ); + + virtual bool focusNextPrevChild( bool next ); + + // drag and drop + virtual void dragEnterEvent(QDragEnterEvent* event); + virtual void dropEvent(QDropEvent* event); + void doDrag(); + enum DragState { diNone, diPending, diDragging }; + + struct _dragInfo { + DragState state; + QPoint start; + QDrag *dragObject; + } dragInfo; + + virtual int charClass(quint16) const; + + void clearImage(); + + void mouseTripleClickEvent(QMouseEvent* ev); + + // reimplemented + virtual void inputMethodEvent ( QInputMethodEvent* event ); + virtual QVariant inputMethodQuery( Qt::InputMethodQuery query ) const; + +protected slots: + + void scrollBarPositionChanged(int value); + void blinkEvent(); + void blinkCursorEvent(); + + //Renables bell noises and visuals. Used to disable further bells for a short period of time + //after emitting the first in a sequence of bell events. + void enableBell(); + +private slots: + + void swapColorTable(); + void tripleClickTimeout(); // resets possibleTripleClick + +private: + + // -- Drawing helpers -- + + // divides the part of the display specified by 'rect' into + // fragments according to their colors and styles and calls + // drawTextFragment() to draw the fragments + void drawContents(QPainter &paint, const QRect &rect); + // draws a section of text, all the text in this section + // has a common color and style + void drawTextFragment(QPainter& painter, const QRect& rect, + const QString& text, const Character* style); + // draws the background for a text fragment + // if useOpacitySetting is true then the color's alpha value will be set to + // the display's transparency (set with setOpacity()), otherwise the background + // will be drawn fully opaque + void drawBackground(QPainter& painter, const QRect& rect, const QColor& color, + bool useOpacitySetting); + // draws the cursor character + void drawCursor(QPainter& painter, const QRect& rect , const QColor& foregroundColor, + const QColor& backgroundColor , bool& invertColors); + // draws the characters or line graphics in a text fragment + void drawCharacters(QPainter& painter, const QRect& rect, const QString& text, + const Character* style, bool invertCharacterColor); + // draws a string of line graphics + void drawLineCharString(QPainter& painter, int x, int y, + const QString& str, const Character* attributes); + + // draws the preedit string for input methods + void drawInputMethodPreeditString(QPainter& painter , const QRect& rect); + + // -- + + // maps an area in the character image to an area on the widget + QRect imageToWidget(const QRect& imageArea) const; + + // maps a point on the widget to the position ( ie. line and column ) + // of the character at that point. + void getCharacterPosition(const QPoint& widgetPoint,int& line,int& column) const; + + // the area where the preedit string for input methods will be draw + QRect preeditRect() const; + + // shows a notification window in the middle of the widget indicating the terminal's + // current size in columns and lines + void showResizeNotification(); + + // scrolls the image by a number of lines. + // 'lines' may be positive ( to scroll the image down ) + // or negative ( to scroll the image up ) + // 'region' is the part of the image to scroll - currently only + // the top, bottom and height of 'region' are taken into account, + // the left and right are ignored. + void scrollImage(int lines , const QRect& region); + + void calcGeometry(); + void propagateSize(); + void updateImageSize(); + void makeImage(); + + void paintFilters(QPainter& painter); + + // returns a region covering all of the areas of the widget which contain + // a hotspot + QRegion hotSpotRegion() const; + + // returns the position of the cursor in columns and lines + QPoint cursorPosition() const; + + // the window onto the terminal screen which this display + // is currently showing. + QPointer<ScreenWindow> _screenWindow; + + bool _allowBell; + + QGridLayout* _gridLayout; + + bool _fixedFont; // has fixed pitch + int _fontHeight; // height + int _fontWidth; // width + int _fontAscent; // ascend + + int _leftMargin; // offset + int _topMargin; // offset + + int _lines; // the number of lines that can be displayed in the widget + int _columns; // the number of columns that can be displayed in the widget + + int _usedLines; // the number of lines that are actually being used, this will be less + // than 'lines' if the character image provided with setImage() is smaller + // than the maximum image size which can be displayed + + int _usedColumns; // the number of columns that are actually being used, this will be less + // than 'columns' if the character image provided with setImage() is smaller + // than the maximum image size which can be displayed + + int _contentHeight; + int _contentWidth; + Character* _image; // [lines][columns] + // only the area [usedLines][usedColumns] in the image contains valid data + + int _imageSize; + QVector<LineProperty> _lineProperties; + + ColorEntry _colorTable[TABLE_COLORS]; + uint _randomSeed; + + bool _resizing; + bool _terminalSizeHint; + bool _terminalSizeStartup; + bool _bidiEnabled; + bool _mouseMarks; + + QPoint _iPntSel; // initial selection point + QPoint _pntSel; // current selection point + QPoint _tripleSelBegin; // help avoid flicker + int _actSel; // selection state + bool _wordSelectionMode; + bool _lineSelectionMode; + bool _preserveLineBreaks; + bool _columnSelectionMode; + + QClipboard* _clipboard; + QScrollBar* _scrollBar; + ScrollBarPosition _scrollbarLocation; + QString _wordCharacters; + int _bellMode; + + bool _blinking; // hide text in paintEvent + bool _hasBlinker; // has characters to blink + bool _cursorBlinking; // hide cursor in paintEvent + bool _hasBlinkingCursor; // has blinking cursor enabled + bool _ctrlDrag; // require Ctrl key for drag + TripleClickMode _tripleClickMode; + bool _isFixedSize; //Columns / lines are locked. + QTimer* _blinkTimer; // active when hasBlinker + QTimer* _blinkCursorTimer; // active when hasBlinkingCursor + +// KMenu* _drop; + QString _dropText; + int _dndFileCount; + + bool _possibleTripleClick; // is set in mouseDoubleClickEvent and deleted + // after QApplication::doubleClickInterval() delay + + + QLabel* _resizeWidget; + QTimer* _resizeTimer; + + bool _flowControlWarningEnabled; + + //widgets related to the warning message that appears when the user presses Ctrl+S to suspend + //terminal output - informing them what has happened and how to resume output + QLabel* _outputSuspendedLabel; + + uint _lineSpacing; + + bool _colorsInverted; // true during visual bell + + QSize _size; + + QRgb _blendColor; + + // list of filters currently applied to the display. used for links and + // search highlight + TerminalImageFilterChain* _filterChain; + QRect _mouseOverHotspotArea; + + KeyboardCursorShape _cursorShape; + + // custom cursor color. if this is invalid then the foreground + // color of the character under the cursor is used + QColor _cursorColor; + + + struct InputMethodData + { + QString preeditString; + QRect previousPreeditRect; + }; + InputMethodData _inputMethodData; + + static bool _antialiasText; // do we antialias or not + + //the delay in milliseconds between redrawing blinking text + static const int BLINK_DELAY = 500; + static const int DEFAULT_LEFT_MARGIN = 1; + static const int DEFAULT_TOP_MARGIN = 1; + +public: + static void setTransparencyEnabled(bool enable) + { + HAVE_TRANSPARENCY = enable; + } +}; + +} + +#endif // TERMINALDISPLAY_H
new file mode 100644 --- /dev/null +++ b/gui//Vt102Emulation.cpp @@ -0,0 +1,1266 @@ +/* + This file is part of Konsole, an X terminal. + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +// Own +#include "Vt102Emulation.h" + +//#include <config-konsole.h> + + +#if defined(__osf__) || defined(__APPLE__) +#define AVOID_XKB +#endif + +// this allows konsole to be compiled without XKB and XTEST extensions +// even though it might be available on a particular system. +#if defined(AVOID_XKB) +#undef HAVE_XKB +#endif + +// Standard +#include <stdio.h> +#include <unistd.h> +#include <assert.h> + +// Qt +#include <QtCore/QEvent> +#include <QtGui/QKeyEvent> +#include <QtCore/QByteRef> + +// KDE +//#include <kdebug.h> +//#include <klocale.h> + +// Konsole +#include "KeyboardTranslator.h" +#include "Screen.h" + +#if defined(HAVE_XKB) +void scrolllock_set_off(); +void scrolllock_set_on(); +#endif + +using namespace Konsole; + +/* VT102 Terminal Emulation + + This class puts together the screens, the pty and the widget to a + complete terminal emulation. Beside combining it's componentes, it + handles the emulations's protocol. + + This module consists of the following sections: + + - Constructor/Destructor + - Incoming Bytes Event pipeline + - Outgoing Bytes + - Mouse Events + - Keyboard Events + - Modes and Charset State + - Diagnostics +*/ + +/* ------------------------------------------------------------------------- */ +/* */ +/* Constructor / Destructor */ +/* */ +/* ------------------------------------------------------------------------- */ + + +Vt102Emulation::Vt102Emulation() + : Emulation(), + _titleUpdateTimer(new QTimer(this)) +{ + _titleUpdateTimer->setSingleShot(true); + + QObject::connect(_titleUpdateTimer , SIGNAL(timeout()) , this , SLOT(updateTitle())); + + initTokenizer(); + reset(); +} + +Vt102Emulation::~Vt102Emulation() +{ +} + +void Vt102Emulation::clearEntireScreen() +{ + _currentScreen->clearEntireScreen(); + + bufferedUpdate(); +} + +void Vt102Emulation::reset() +{ + //kDebug(1211)<<"Vt102Emulation::reset() resetToken()"; + resetToken(); + //kDebug(1211)<<"Vt102Emulation::reset() resetModes()"; + resetModes(); + //kDebug(1211)<<"Vt102Emulation::reset() resetCharSet()"; + resetCharset(0); + //kDebug(1211)<<"Vt102Emulation::reset() reset screen0()"; + _screen[0]->reset(); + //kDebug(1211)<<"Vt102Emulation::reset() resetCharSet()"; + resetCharset(1); + //kDebug(1211)<<"Vt102Emulation::reset() reset _screen 1"; + _screen[1]->reset(); + //kDebug(1211)<<"Vt102Emulation::reset() setCodec()"; + setCodec(LocaleCodec); + //kDebug(1211)<<"Vt102Emulation::reset() done"; + + bufferedUpdate(); +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Processing the incoming byte stream */ +/* */ +/* ------------------------------------------------------------------------- */ + +/* Incoming Bytes Event pipeline + + This section deals with decoding the incoming character stream. + Decoding means here, that the stream is first separated into `tokens' + which are then mapped to a `meaning' provided as operations by the + `Screen' class or by the emulation class itself. + + The pipeline proceeds as follows: + + - Tokenizing the ESC codes (onReceiveChar) + - VT100 code page translation of plain characters (applyCharset) + - Interpretation of ESC codes (tau) + + The escape codes and their meaning are described in the + technical reference of this program. +*/ + +// Tokens ------------------------------------------------------------------ -- + +/* + Since the tokens are the central notion if this section, we've put them + in front. They provide the syntactical elements used to represent the + terminals operations as byte sequences. + + They are encodes here into a single machine word, so that we can later + switch over them easily. Depending on the token itself, additional + argument variables are filled with parameter values. + + The tokens are defined below: + + - CHR - Printable characters (32..255 but DEL (=127)) + - CTL - Control characters (0..31 but ESC (= 27), DEL) + - ESC - Escape codes of the form <ESC><CHR but `[]()+*#'> + - ESC_DE - Escape codes of the form <ESC><any of `()+*#%'> C + - CSI_PN - Escape codes of the form <ESC>'[' {Pn} ';' {Pn} C + - CSI_PS - Escape codes of the form <ESC>'[' {Pn} ';' ... C + - CSI_PR - Escape codes of the form <ESC>'[' '?' {Pn} ';' ... C + - CSI_PE - Escape codes of the form <ESC>'[' '!' {Pn} ';' ... C + - VT52 - VT52 escape codes + - <ESC><Chr> + - <ESC>'Y'{Pc}{Pc} + - XTE_HA - Xterm hacks <ESC>`]' {Pn} `;' {Text} <BEL> + note that this is handled differently + + The last two forms allow list of arguments. Since the elements of + the lists are treated individually the same way, they are passed + as individual tokens to the interpretation. Further, because the + meaning of the parameters are names (althought represented as numbers), + they are includes within the token ('N'). + +*/ + +#define TY_CONSTR(T,A,N) ( ((((int)N) & 0xffff) << 16) | ((((int)A) & 0xff) << 8) | (((int)T) & 0xff) ) + +#define TY_CHR( ) TY_CONSTR(0,0,0) +#define TY_CTL(A ) TY_CONSTR(1,A,0) +#define TY_ESC(A ) TY_CONSTR(2,A,0) +#define TY_ESC_CS(A,B) TY_CONSTR(3,A,B) +#define TY_ESC_DE(A ) TY_CONSTR(4,A,0) +#define TY_CSI_PS(A,N) TY_CONSTR(5,A,N) +#define TY_CSI_PN(A ) TY_CONSTR(6,A,0) +#define TY_CSI_PR(A,N) TY_CONSTR(7,A,N) + +#define TY_VT52(A ) TY_CONSTR(8,A,0) + +#define TY_CSI_PG(A ) TY_CONSTR(9,A,0) + +#define TY_CSI_PE(A ) TY_CONSTR(10,A,0) + +// Tokenizer --------------------------------------------------------------- -- + +/* The tokenizers state + + The state is represented by the buffer (pbuf, ppos), + and accompanied by decoded arguments kept in (argv,argc). + Note that they are kept internal in the tokenizer. +*/ + +void Vt102Emulation::resetToken() +{ + ppos = 0; argc = 0; argv[0] = 0; argv[1] = 0; +} + +void Vt102Emulation::addDigit(int dig) +{ + argv[argc] = 10*argv[argc] + dig; +} + +void Vt102Emulation::addArgument() +{ + argc = qMin(argc+1,MAXARGS-1); + argv[argc] = 0; +} + +void Vt102Emulation::pushToToken(int cc) +{ + pbuf[ppos] = cc; + ppos = qMin(ppos+1,MAXPBUF-1); +} + +// Character Classes used while decoding + +#define CTL 1 +#define CHR 2 +#define CPN 4 +#define DIG 8 +#define SCS 16 +#define GRP 32 +#define CPS 64 + +void Vt102Emulation::initTokenizer() +{ int i; quint8* s; + for(i = 0; i < 256; i++) tbl[ i] = 0; + for(i = 0; i < 32; i++) tbl[ i] |= CTL; + for(i = 32; i < 256; i++) tbl[ i] |= CHR; + for(s = (quint8*)"@ABCDGHILMPSTXZcdfry"; *s; s++) tbl[*s] |= CPN; +// resize = \e[8;<row>;<col>t + for(s = (quint8*)"t"; *s; s++) tbl[*s] |= CPS; + for(s = (quint8*)"0123456789" ; *s; s++) tbl[*s] |= DIG; + for(s = (quint8*)"()+*%" ; *s; s++) tbl[*s] |= SCS; + for(s = (quint8*)"()+*#[]%" ; *s; s++) tbl[*s] |= GRP; + resetToken(); +} + +/* Ok, here comes the nasty part of the decoder. + + Instead of keeping an explicit state, we deduce it from the + token scanned so far. It is then immediately combined with + the current character to form a scanning decision. + + This is done by the following defines. + + - P is the length of the token scanned so far. + - L (often P-1) is the position on which contents we base a decision. + - C is a character or a group of characters (taken from 'tbl'). + + Note that they need to applied in proper order. +*/ + +#define lec(P,L,C) (p == (P) && s[(L)] == (C)) +#define lun( ) (p == 1 && cc >= 32 ) +#define les(P,L,C) (p == (P) && s[L] < 256 && (tbl[s[(L)]] & (C)) == (C)) +#define eec(C) (p >= 3 && cc == (C)) +#define ees(C) (p >= 3 && cc < 256 && (tbl[ cc ] & (C)) == (C)) +#define eps(C) (p >= 3 && s[2] != '?' && s[2] != '!' && s[2] != '>' && cc < 256 && (tbl[ cc ] & (C)) == (C)) +#define epp( ) (p >= 3 && s[2] == '?' ) +#define epe( ) (p >= 3 && s[2] == '!' ) +#define egt( ) (p >= 3 && s[2] == '>' ) +#define Xpe (ppos>=2 && pbuf[1] == ']' ) +#define Xte (Xpe && cc == 7 ) +#define ces(C) ( cc < 256 && (tbl[ cc ] & (C)) == (C) && !Xte) + +#define ESC 27 +#define CNTL(c) ((c)-'@') + +// process an incoming unicode character + +void Vt102Emulation::receiveChar(int cc) +{ + int i; + if (cc == 127) return; //VT100: ignore. + + if (ces( CTL)) + { // DEC HACK ALERT! Control Characters are allowed *within* esc sequences in VT100 + // This means, they do neither a resetToken nor a pushToToken. Some of them, do + // of course. Guess this originates from a weakly layered handling of the X-on + // X-off protocol, which comes really below this level. + if (cc == CNTL('X') || cc == CNTL('Z') || cc == ESC) resetToken(); //VT100: CAN or SUB + if (cc != ESC) { tau( TY_CTL(cc+'@' ), 0, 0); return; } + } + + pushToToken(cc); // advance the state + + int* s = pbuf; + int p = ppos; + + if (getMode(MODE_Ansi)) // decide on proper action + { + if (lec(1,0,ESC)) { return; } + if (lec(1,0,ESC+128)) { s[0] = ESC; receiveChar('['); return; } + if (les(2,1,GRP)) { return; } + if (Xte ) { XtermHack(); resetToken(); return; } + if (Xpe ) { return; } + if (lec(3,2,'?')) { return; } + if (lec(3,2,'>')) { return; } + if (lec(3,2,'!')) { return; } + if (lun( )) { tau( TY_CHR(), applyCharset(cc), 0); resetToken(); return; } + if (lec(2,0,ESC)) { tau( TY_ESC(s[1]), 0, 0); resetToken(); return; } + if (les(3,1,SCS)) { tau( TY_ESC_CS(s[1],s[2]), 0, 0); resetToken(); return; } + if (lec(3,1,'#')) { tau( TY_ESC_DE(s[2]), 0, 0); resetToken(); return; } + if (eps( CPN)) { tau( TY_CSI_PN(cc), argv[0],argv[1]); resetToken(); return; } + +// resize = \e[8;<row>;<col>t + if (eps( CPS)) { tau( TY_CSI_PS(cc, argv[0]), argv[1], argv[2]); resetToken(); return; } + + if (epe( )) { tau( TY_CSI_PE(cc), 0, 0); resetToken(); return; } + if (ees( DIG)) { addDigit(cc-'0'); return; } + if (eec( ';')) { addArgument(); return; } + for (i=0;i<=argc;i++) + if ( epp( )) { tau( TY_CSI_PR(cc,argv[i]), 0, 0); } + else if(egt( )) { tau( TY_CSI_PG(cc ), 0, 0); } // spec. case for ESC]>0c or ESC]>c + else if (cc == 'm' && argc - i >= 4 && (argv[i] == 38 || argv[i] == 48) && argv[i+1] == 2) + { // ESC[ ... 48;2;<red>;<green>;<blue> ... m -or- ESC[ ... 38;2;<red>;<green>;<blue> ... m + i += 2; + tau( TY_CSI_PS(cc, argv[i-2]), COLOR_SPACE_RGB, (argv[i] << 16) | (argv[i+1] << 8) | argv[i+2]); + i += 2; + } + else if (cc == 'm' && argc - i >= 2 && (argv[i] == 38 || argv[i] == 48) && argv[i+1] == 5) + { // ESC[ ... 48;5;<index> ... m -or- ESC[ ... 38;5;<index> ... m + i += 2; + tau( TY_CSI_PS(cc, argv[i-2]), COLOR_SPACE_256, argv[i]); + } + else { tau( TY_CSI_PS(cc,argv[i]), 0, 0); } + resetToken(); + } + else // mode VT52 + { + if (lec(1,0,ESC)) return; + if (les(1,0,CHR)) { tau( TY_CHR( ), s[0], 0); resetToken(); return; } + if (lec(2,1,'Y')) return; + if (lec(3,1,'Y')) return; + if (p < 4) { tau( TY_VT52(s[1] ), 0, 0); resetToken(); return; } + tau( TY_VT52(s[1] ), s[2],s[3]); resetToken(); return; + } +} + +void Vt102Emulation::XtermHack() +{ int i,arg = 0; + for (i = 2; i < ppos && '0'<=pbuf[i] && pbuf[i]<'9' ; i++) + arg = 10*arg + (pbuf[i]-'0'); + if (pbuf[i] != ';') { ReportErrorToken(); return; } + QChar *str = new QChar[ppos-i-2]; + for (int j = 0; j < ppos-i-2; j++) str[j] = pbuf[i+1+j]; + QString unistr(str,ppos-i-2); + + // arg == 1 doesn't change the title. In XTerm it only changes the icon name + // (btw: arg=0 changes title and icon, arg=1 only icon, arg=2 only title +// emit changeTitle(arg,unistr); + _pendingTitleUpdates[arg] = unistr; + _titleUpdateTimer->start(20); + + delete [] str; +} + +void Vt102Emulation::updateTitle() +{ + QListIterator<int> iter( _pendingTitleUpdates.keys() ); + while (iter.hasNext()) { + int arg = iter.next(); + emit titleChanged( arg , _pendingTitleUpdates[arg] ); + } + + _pendingTitleUpdates.clear(); +} + +// Interpreting Codes --------------------------------------------------------- + +/* + Now that the incoming character stream is properly tokenized, + meaning is assigned to them. These are either operations of + the current _screen, or of the emulation class itself. + + The token to be interpreteted comes in as a machine word + possibly accompanied by two parameters. + + Likewise, the operations assigned to, come with up to two + arguments. One could consider to make up a proper table + from the function below. + + The technical reference manual provides more information + about this mapping. +*/ + +void Vt102Emulation::tau( int token, int p, int q ) +{ +#if 0 +int N = (token>>0)&0xff; +int A = (token>>8)&0xff; +switch( N ) +{ + case 0: printf("%c", (p < 128) ? p : '?'); + break; + case 1: if (A == 'J') printf("\r"); + else if (A == 'M') printf("\n"); + else printf("CTL-%c ", (token>>8)&0xff); + break; + case 2: printf("ESC-%c ", (token>>8)&0xff); + break; + case 3: printf("ESC_CS-%c-%c ", (token>>8)&0xff, (token>>16)&0xff); + break; + case 4: printf("ESC_DE-%c ", (token>>8)&0xff); + break; + case 5: printf("CSI-PS-%c-%d", (token>>8)&0xff, (token>>16)&0xff ); + break; + case 6: printf("CSI-PN-%c [%d]", (token>>8)&0xff, p); + break; + case 7: printf("CSI-PR-%c-%d", (token>>8)&0xff, (token>>16)&0xff ); + break; + case 8: printf("VT52-%c", (token>>8)&0xff); + break; + case 9: printf("CSI-PG-%c", (token>>8)&0xff); + break; + case 10: printf("CSI-PE-%c", (token>>8)&0xff); + break; +} +#endif + + switch (token) + { + + case TY_CHR( ) : _currentScreen->ShowCharacter (p ); break; //UTF16 + + // 127 DEL : ignored on input + + case TY_CTL('@' ) : /* NUL: ignored */ break; + case TY_CTL('A' ) : /* SOH: ignored */ break; + case TY_CTL('B' ) : /* STX: ignored */ break; + case TY_CTL('C' ) : /* ETX: ignored */ break; + case TY_CTL('D' ) : /* EOT: ignored */ break; + case TY_CTL('E' ) : reportAnswerBack ( ); break; //VT100 + case TY_CTL('F' ) : /* ACK: ignored */ break; + case TY_CTL('G' ) : emit stateSet(NOTIFYBELL); + break; //VT100 + case TY_CTL('H' ) : _currentScreen->BackSpace ( ); break; //VT100 + case TY_CTL('I' ) : _currentScreen->Tabulate ( ); break; //VT100 + case TY_CTL('J' ) : _currentScreen->NewLine ( ); break; //VT100 + case TY_CTL('K' ) : _currentScreen->NewLine ( ); break; //VT100 + case TY_CTL('L' ) : _currentScreen->NewLine ( ); break; //VT100 + case TY_CTL('M' ) : _currentScreen->Return ( ); break; //VT100 + + case TY_CTL('N' ) : useCharset ( 1); break; //VT100 + case TY_CTL('O' ) : useCharset ( 0); break; //VT100 + + case TY_CTL('P' ) : /* DLE: ignored */ break; + case TY_CTL('Q' ) : /* DC1: XON continue */ break; //VT100 + case TY_CTL('R' ) : /* DC2: ignored */ break; + case TY_CTL('S' ) : /* DC3: XOFF halt */ break; //VT100 + case TY_CTL('T' ) : /* DC4: ignored */ break; + case TY_CTL('U' ) : /* NAK: ignored */ break; + case TY_CTL('V' ) : /* SYN: ignored */ break; + case TY_CTL('W' ) : /* ETB: ignored */ break; + case TY_CTL('X' ) : _currentScreen->ShowCharacter ( 0x2592); break; //VT100 + case TY_CTL('Y' ) : /* EM : ignored */ break; + case TY_CTL('Z' ) : _currentScreen->ShowCharacter ( 0x2592); break; //VT100 + case TY_CTL('[' ) : /* ESC: cannot be seen here. */ break; + case TY_CTL('\\' ) : /* FS : ignored */ break; + case TY_CTL(']' ) : /* GS : ignored */ break; + case TY_CTL('^' ) : /* RS : ignored */ break; + case TY_CTL('_' ) : /* US : ignored */ break; + + case TY_ESC('D' ) : _currentScreen->index ( ); break; //VT100 + case TY_ESC('E' ) : _currentScreen->NextLine ( ); break; //VT100 + case TY_ESC('H' ) : _currentScreen->changeTabStop (true ); break; //VT100 + case TY_ESC('M' ) : _currentScreen->reverseIndex ( ); break; //VT100 + case TY_ESC('Z' ) : reportTerminalType ( ); break; + case TY_ESC('c' ) : reset ( ); break; + + case TY_ESC('n' ) : useCharset ( 2); break; + case TY_ESC('o' ) : useCharset ( 3); break; + case TY_ESC('7' ) : saveCursor ( ); break; + case TY_ESC('8' ) : restoreCursor ( ); break; + + case TY_ESC('=' ) : setMode (MODE_AppKeyPad); break; + case TY_ESC('>' ) : resetMode (MODE_AppKeyPad); break; + case TY_ESC('<' ) : setMode (MODE_Ansi ); break; //VT100 + + case TY_ESC_CS('(', '0') : setCharset (0, '0'); break; //VT100 + case TY_ESC_CS('(', 'A') : setCharset (0, 'A'); break; //VT100 + case TY_ESC_CS('(', 'B') : setCharset (0, 'B'); break; //VT100 + + case TY_ESC_CS(')', '0') : setCharset (1, '0'); break; //VT100 + case TY_ESC_CS(')', 'A') : setCharset (1, 'A'); break; //VT100 + case TY_ESC_CS(')', 'B') : setCharset (1, 'B'); break; //VT100 + + case TY_ESC_CS('*', '0') : setCharset (2, '0'); break; //VT100 + case TY_ESC_CS('*', 'A') : setCharset (2, 'A'); break; //VT100 + case TY_ESC_CS('*', 'B') : setCharset (2, 'B'); break; //VT100 + + case TY_ESC_CS('+', '0') : setCharset (3, '0'); break; //VT100 + case TY_ESC_CS('+', 'A') : setCharset (3, 'A'); break; //VT100 + case TY_ESC_CS('+', 'B') : setCharset (3, 'B'); break; //VT100 + + case TY_ESC_CS('%', 'G') : setCodec (Utf8Codec ); break; //LINUX + case TY_ESC_CS('%', '@') : setCodec (LocaleCodec ); break; //LINUX + + case TY_ESC_DE('3' ) : /* Double height line, top half */ + _currentScreen->setLineProperty( LINE_DOUBLEWIDTH , true ); + _currentScreen->setLineProperty( LINE_DOUBLEHEIGHT , true ); + break; + case TY_ESC_DE('4' ) : /* Double height line, bottom half */ + _currentScreen->setLineProperty( LINE_DOUBLEWIDTH , true ); + _currentScreen->setLineProperty( LINE_DOUBLEHEIGHT , true ); + break; + case TY_ESC_DE('5' ) : /* Single width, single height line*/ + _currentScreen->setLineProperty( LINE_DOUBLEWIDTH , false); + _currentScreen->setLineProperty( LINE_DOUBLEHEIGHT , false); + break; + case TY_ESC_DE('6' ) : /* Double width, single height line*/ + _currentScreen->setLineProperty( LINE_DOUBLEWIDTH , true); + _currentScreen->setLineProperty( LINE_DOUBLEHEIGHT , false); + break; + case TY_ESC_DE('8' ) : _currentScreen->helpAlign ( ); break; + +// resize = \e[8;<row>;<col>t + case TY_CSI_PS('t', 8) : setImageSize( q /* colums */, p /* lines */ ); break; + +// change tab text color : \e[28;<color>t color: 0-16,777,215 + case TY_CSI_PS('t', 28) : emit changeTabTextColorRequest ( p ); break; + + case TY_CSI_PS('K', 0) : _currentScreen->clearToEndOfLine ( ); break; + case TY_CSI_PS('K', 1) : _currentScreen->clearToBeginOfLine ( ); break; + case TY_CSI_PS('K', 2) : _currentScreen->clearEntireLine ( ); break; + case TY_CSI_PS('J', 0) : _currentScreen->clearToEndOfScreen ( ); break; + case TY_CSI_PS('J', 1) : _currentScreen->clearToBeginOfScreen ( ); break; + case TY_CSI_PS('J', 2) : _currentScreen->clearEntireScreen ( ); break; + case TY_CSI_PS('g', 0) : _currentScreen->changeTabStop (false ); break; //VT100 + case TY_CSI_PS('g', 3) : _currentScreen->clearTabStops ( ); break; //VT100 + case TY_CSI_PS('h', 4) : _currentScreen-> setMode (MODE_Insert ); break; + case TY_CSI_PS('h', 20) : setMode (MODE_NewLine ); break; + case TY_CSI_PS('i', 0) : /* IGNORE: attached printer */ break; //VT100 + case TY_CSI_PS('l', 4) : _currentScreen-> resetMode (MODE_Insert ); break; + case TY_CSI_PS('l', 20) : resetMode (MODE_NewLine ); break; + case TY_CSI_PS('s', 0) : saveCursor ( ); break; + case TY_CSI_PS('u', 0) : restoreCursor ( ); break; + + case TY_CSI_PS('m', 0) : _currentScreen->setDefaultRendition ( ); break; + case TY_CSI_PS('m', 1) : _currentScreen-> setRendition (RE_BOLD ); break; //VT100 + case TY_CSI_PS('m', 4) : _currentScreen-> setRendition (RE_UNDERLINE); break; //VT100 + case TY_CSI_PS('m', 5) : _currentScreen-> setRendition (RE_BLINK ); break; //VT100 + case TY_CSI_PS('m', 7) : _currentScreen-> setRendition (RE_REVERSE ); break; + case TY_CSI_PS('m', 10) : /* IGNORED: mapping related */ break; //LINUX + case TY_CSI_PS('m', 11) : /* IGNORED: mapping related */ break; //LINUX + case TY_CSI_PS('m', 12) : /* IGNORED: mapping related */ break; //LINUX + case TY_CSI_PS('m', 22) : _currentScreen->resetRendition (RE_BOLD ); break; + case TY_CSI_PS('m', 24) : _currentScreen->resetRendition (RE_UNDERLINE); break; + case TY_CSI_PS('m', 25) : _currentScreen->resetRendition (RE_BLINK ); break; + case TY_CSI_PS('m', 27) : _currentScreen->resetRendition (RE_REVERSE ); break; + + case TY_CSI_PS('m', 30) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 0); break; + case TY_CSI_PS('m', 31) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 1); break; + case TY_CSI_PS('m', 32) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 2); break; + case TY_CSI_PS('m', 33) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 3); break; + case TY_CSI_PS('m', 34) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 4); break; + case TY_CSI_PS('m', 35) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 5); break; + case TY_CSI_PS('m', 36) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 6); break; + case TY_CSI_PS('m', 37) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 7); break; + + case TY_CSI_PS('m', 38) : _currentScreen->setForeColor (p, q); break; + + case TY_CSI_PS('m', 39) : _currentScreen->setForeColor (COLOR_SPACE_DEFAULT, 0); break; + + case TY_CSI_PS('m', 40) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 0); break; + case TY_CSI_PS('m', 41) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 1); break; + case TY_CSI_PS('m', 42) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 2); break; + case TY_CSI_PS('m', 43) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 3); break; + case TY_CSI_PS('m', 44) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 4); break; + case TY_CSI_PS('m', 45) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 5); break; + case TY_CSI_PS('m', 46) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 6); break; + case TY_CSI_PS('m', 47) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 7); break; + + case TY_CSI_PS('m', 48) : _currentScreen->setBackColor (p, q); break; + + case TY_CSI_PS('m', 49) : _currentScreen->setBackColor (COLOR_SPACE_DEFAULT, 1); break; + + case TY_CSI_PS('m', 90) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 8); break; + case TY_CSI_PS('m', 91) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 9); break; + case TY_CSI_PS('m', 92) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 10); break; + case TY_CSI_PS('m', 93) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 11); break; + case TY_CSI_PS('m', 94) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 12); break; + case TY_CSI_PS('m', 95) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 13); break; + case TY_CSI_PS('m', 96) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 14); break; + case TY_CSI_PS('m', 97) : _currentScreen->setForeColor (COLOR_SPACE_SYSTEM, 15); break; + + case TY_CSI_PS('m', 100) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 8); break; + case TY_CSI_PS('m', 101) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 9); break; + case TY_CSI_PS('m', 102) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 10); break; + case TY_CSI_PS('m', 103) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 11); break; + case TY_CSI_PS('m', 104) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 12); break; + case TY_CSI_PS('m', 105) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 13); break; + case TY_CSI_PS('m', 106) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 14); break; + case TY_CSI_PS('m', 107) : _currentScreen->setBackColor (COLOR_SPACE_SYSTEM, 15); break; + + case TY_CSI_PS('n', 5) : reportStatus ( ); break; + case TY_CSI_PS('n', 6) : reportCursorPosition ( ); break; + case TY_CSI_PS('q', 0) : /* IGNORED: LEDs off */ break; //VT100 + case TY_CSI_PS('q', 1) : /* IGNORED: LED1 on */ break; //VT100 + case TY_CSI_PS('q', 2) : /* IGNORED: LED2 on */ break; //VT100 + case TY_CSI_PS('q', 3) : /* IGNORED: LED3 on */ break; //VT100 + case TY_CSI_PS('q', 4) : /* IGNORED: LED4 on */ break; //VT100 + case TY_CSI_PS('x', 0) : reportTerminalParms ( 2); break; //VT100 + case TY_CSI_PS('x', 1) : reportTerminalParms ( 3); break; //VT100 + + case TY_CSI_PN('@' ) : _currentScreen->insertChars (p ); break; + case TY_CSI_PN('A' ) : _currentScreen->cursorUp (p ); break; //VT100 + case TY_CSI_PN('B' ) : _currentScreen->cursorDown (p ); break; //VT100 + case TY_CSI_PN('C' ) : _currentScreen->cursorRight (p ); break; //VT100 + case TY_CSI_PN('D' ) : _currentScreen->cursorLeft (p ); break; //VT100 + case TY_CSI_PN('G' ) : _currentScreen->setCursorX (p ); break; //LINUX + case TY_CSI_PN('H' ) : _currentScreen->setCursorYX (p, q); break; //VT100 + case TY_CSI_PN('I' ) : _currentScreen->Tabulate (p ); break; + case TY_CSI_PN('L' ) : _currentScreen->insertLines (p ); break; + case TY_CSI_PN('M' ) : _currentScreen->deleteLines (p ); break; + case TY_CSI_PN('P' ) : _currentScreen->deleteChars (p ); break; + case TY_CSI_PN('S' ) : _currentScreen->scrollUp (p ); break; + case TY_CSI_PN('T' ) : _currentScreen->scrollDown (p ); break; + case TY_CSI_PN('X' ) : _currentScreen->eraseChars (p ); break; + case TY_CSI_PN('Z' ) : _currentScreen->backTabulate (p ); break; + case TY_CSI_PN('c' ) : reportTerminalType ( ); break; //VT100 + case TY_CSI_PN('d' ) : _currentScreen->setCursorY (p ); break; //LINUX + case TY_CSI_PN('f' ) : _currentScreen->setCursorYX (p, q); break; //VT100 + case TY_CSI_PN('r' ) : setMargins (p, q); break; //VT100 + case TY_CSI_PN('y' ) : /* IGNORED: Confidence test */ break; //VT100 + + case TY_CSI_PR('h', 1) : setMode (MODE_AppCuKeys); break; //VT100 + case TY_CSI_PR('l', 1) : resetMode (MODE_AppCuKeys); break; //VT100 + case TY_CSI_PR('s', 1) : saveMode (MODE_AppCuKeys); break; //FIXME + case TY_CSI_PR('r', 1) : restoreMode (MODE_AppCuKeys); break; //FIXME + + case TY_CSI_PR('l', 2) : resetMode (MODE_Ansi ); break; //VT100 + + case TY_CSI_PR('h', 3) : clearScreenAndSetColumns(132); break; //VT100 + case TY_CSI_PR('l', 3) : clearScreenAndSetColumns(80); break; //VT100 + + case TY_CSI_PR('h', 4) : /* IGNORED: soft scrolling */ break; //VT100 + case TY_CSI_PR('l', 4) : /* IGNORED: soft scrolling */ break; //VT100 + + case TY_CSI_PR('h', 5) : _currentScreen-> setMode (MODE_Screen ); break; //VT100 + case TY_CSI_PR('l', 5) : _currentScreen-> resetMode (MODE_Screen ); break; //VT100 + + case TY_CSI_PR('h', 6) : _currentScreen-> setMode (MODE_Origin ); break; //VT100 + case TY_CSI_PR('l', 6) : _currentScreen-> resetMode (MODE_Origin ); break; //VT100 + case TY_CSI_PR('s', 6) : _currentScreen-> saveMode (MODE_Origin ); break; //FIXME + case TY_CSI_PR('r', 6) : _currentScreen->restoreMode (MODE_Origin ); break; //FIXME + + case TY_CSI_PR('h', 7) : _currentScreen-> setMode (MODE_Wrap ); break; //VT100 + case TY_CSI_PR('l', 7) : _currentScreen-> resetMode (MODE_Wrap ); break; //VT100 + case TY_CSI_PR('s', 7) : _currentScreen-> saveMode (MODE_Wrap ); break; //FIXME + case TY_CSI_PR('r', 7) : _currentScreen->restoreMode (MODE_Wrap ); break; //FIXME + + case TY_CSI_PR('h', 8) : /* IGNORED: autorepeat on */ break; //VT100 + case TY_CSI_PR('l', 8) : /* IGNORED: autorepeat off */ break; //VT100 + case TY_CSI_PR('s', 8) : /* IGNORED: autorepeat on */ break; //VT100 + case TY_CSI_PR('r', 8) : /* IGNORED: autorepeat off */ break; //VT100 + + case TY_CSI_PR('h', 9) : /* IGNORED: interlace */ break; //VT100 + case TY_CSI_PR('l', 9) : /* IGNORED: interlace */ break; //VT100 + case TY_CSI_PR('s', 9) : /* IGNORED: interlace */ break; //VT100 + case TY_CSI_PR('r', 9) : /* IGNORED: interlace */ break; //VT100 + + case TY_CSI_PR('h', 12) : /* IGNORED: Cursor blink */ break; //att610 + case TY_CSI_PR('l', 12) : /* IGNORED: Cursor blink */ break; //att610 + case TY_CSI_PR('s', 12) : /* IGNORED: Cursor blink */ break; //att610 + case TY_CSI_PR('r', 12) : /* IGNORED: Cursor blink */ break; //att610 + + case TY_CSI_PR('h', 25) : setMode (MODE_Cursor ); break; //VT100 + case TY_CSI_PR('l', 25) : resetMode (MODE_Cursor ); break; //VT100 + case TY_CSI_PR('s', 25) : saveMode (MODE_Cursor ); break; //VT100 + case TY_CSI_PR('r', 25) : restoreMode (MODE_Cursor ); break; //VT100 + + case TY_CSI_PR('h', 41) : /* IGNORED: obsolete more(1) fix */ break; //XTERM + case TY_CSI_PR('l', 41) : /* IGNORED: obsolete more(1) fix */ break; //XTERM + case TY_CSI_PR('s', 41) : /* IGNORED: obsolete more(1) fix */ break; //XTERM + case TY_CSI_PR('r', 41) : /* IGNORED: obsolete more(1) fix */ break; //XTERM + + case TY_CSI_PR('h', 47) : setMode (MODE_AppScreen); break; //VT100 + case TY_CSI_PR('l', 47) : resetMode (MODE_AppScreen); break; //VT100 + case TY_CSI_PR('s', 47) : saveMode (MODE_AppScreen); break; //XTERM + case TY_CSI_PR('r', 47) : restoreMode (MODE_AppScreen); break; //XTERM + + case TY_CSI_PR('h', 67) : /* IGNORED: DECBKM */ break; //XTERM + case TY_CSI_PR('l', 67) : /* IGNORED: DECBKM */ break; //XTERM + case TY_CSI_PR('s', 67) : /* IGNORED: DECBKM */ break; //XTERM + case TY_CSI_PR('r', 67) : /* IGNORED: DECBKM */ break; //XTERM + + // XTerm defines the following modes: + // SET_VT200_MOUSE 1000 + // SET_VT200_HIGHLIGHT_MOUSE 1001 + // SET_BTN_EVENT_MOUSE 1002 + // SET_ANY_EVENT_MOUSE 1003 + // + + //Note about mouse modes: + //There are four mouse modes which xterm-compatible terminals can support - 1000,1001,1002,1003 + //Konsole currently supports mode 1000 (basic mouse press and release) and mode 1002 (dragging the mouse). + //TODO: Implementation of mouse modes 1001 (something called hilight tracking) and + //1003 (a slight variation on dragging the mouse) + // + + case TY_CSI_PR('h', 1000) : setMode (MODE_Mouse1000); break; //XTERM + case TY_CSI_PR('l', 1000) : resetMode (MODE_Mouse1000); break; //XTERM + case TY_CSI_PR('s', 1000) : saveMode (MODE_Mouse1000); break; //XTERM + case TY_CSI_PR('r', 1000) : restoreMode (MODE_Mouse1000); break; //XTERM + + case TY_CSI_PR('h', 1001) : /* IGNORED: hilite mouse tracking */ break; //XTERM + case TY_CSI_PR('l', 1001) : resetMode (MODE_Mouse1001); break; //XTERM + case TY_CSI_PR('s', 1001) : /* IGNORED: hilite mouse tracking */ break; //XTERM + case TY_CSI_PR('r', 1001) : /* IGNORED: hilite mouse tracking */ break; //XTERM + + case TY_CSI_PR('h', 1002) : setMode (MODE_Mouse1002); break; //XTERM + case TY_CSI_PR('l', 1002) : resetMode (MODE_Mouse1002); break; //XTERM + case TY_CSI_PR('s', 1002) : saveMode (MODE_Mouse1002); break; //XTERM + case TY_CSI_PR('r', 1002) : restoreMode (MODE_Mouse1002); break; //XTERM + + case TY_CSI_PR('h', 1003) : setMode (MODE_Mouse1003); break; //XTERM + case TY_CSI_PR('l', 1003) : resetMode (MODE_Mouse1003); break; //XTERM + case TY_CSI_PR('s', 1003) : saveMode (MODE_Mouse1003); break; //XTERM + case TY_CSI_PR('r', 1003) : restoreMode (MODE_Mouse1003); break; //XTERM + + case TY_CSI_PR('h', 1047) : setMode (MODE_AppScreen); break; //XTERM + case TY_CSI_PR('l', 1047) : _screen[1]->clearEntireScreen(); resetMode(MODE_AppScreen); break; //XTERM + case TY_CSI_PR('s', 1047) : saveMode (MODE_AppScreen); break; //XTERM + case TY_CSI_PR('r', 1047) : restoreMode (MODE_AppScreen); break; //XTERM + + //FIXME: Unitoken: save translations + case TY_CSI_PR('h', 1048) : saveCursor ( ); break; //XTERM + case TY_CSI_PR('l', 1048) : restoreCursor ( ); break; //XTERM + case TY_CSI_PR('s', 1048) : saveCursor ( ); break; //XTERM + case TY_CSI_PR('r', 1048) : restoreCursor ( ); break; //XTERM + + //FIXME: every once new sequences like this pop up in xterm. + // Here's a guess of what they could mean. + case TY_CSI_PR('h', 1049) : saveCursor(); _screen[1]->clearEntireScreen(); setMode(MODE_AppScreen); break; //XTERM + case TY_CSI_PR('l', 1049) : resetMode(MODE_AppScreen); restoreCursor(); break; //XTERM + + //FIXME: weird DEC reset sequence + case TY_CSI_PE('p' ) : /* IGNORED: reset ( ) */ break; + + //FIXME: when changing between vt52 and ansi mode evtl do some resetting. + case TY_VT52('A' ) : _currentScreen->cursorUp ( 1); break; //VT52 + case TY_VT52('B' ) : _currentScreen->cursorDown ( 1); break; //VT52 + case TY_VT52('C' ) : _currentScreen->cursorRight ( 1); break; //VT52 + case TY_VT52('D' ) : _currentScreen->cursorLeft ( 1); break; //VT52 + + case TY_VT52('F' ) : setAndUseCharset (0, '0'); break; //VT52 + case TY_VT52('G' ) : setAndUseCharset (0, 'B'); break; //VT52 + + case TY_VT52('H' ) : _currentScreen->setCursorYX (1,1 ); break; //VT52 + case TY_VT52('I' ) : _currentScreen->reverseIndex ( ); break; //VT52 + case TY_VT52('J' ) : _currentScreen->clearToEndOfScreen ( ); break; //VT52 + case TY_VT52('K' ) : _currentScreen->clearToEndOfLine ( ); break; //VT52 + case TY_VT52('Y' ) : _currentScreen->setCursorYX (p-31,q-31 ); break; //VT52 + case TY_VT52('Z' ) : reportTerminalType ( ); break; //VT52 + case TY_VT52('<' ) : setMode (MODE_Ansi ); break; //VT52 + case TY_VT52('=' ) : setMode (MODE_AppKeyPad); break; //VT52 + case TY_VT52('>' ) : resetMode (MODE_AppKeyPad); break; //VT52 + + case TY_CSI_PG('c' ) : reportSecondaryAttributes( ); break; //VT100 + + default : ReportErrorToken(); break; + }; +} + +void Vt102Emulation::clearScreenAndSetColumns(int columnCount) +{ + setImageSize(_currentScreen->getLines(),columnCount); + clearEntireScreen(); + setDefaultMargins(); + _currentScreen->setCursorYX(0,0); +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Terminal to Host protocol */ +/* */ +/* ------------------------------------------------------------------------- */ + +/* + Outgoing bytes originate from several sources: + + - Replies to Enquieries. + - Mouse Events + - Keyboard Events +*/ + +/*! +*/ + +void Vt102Emulation::sendString(const char* s , int length) +{ + if ( length >= 0 ) + emit sendData(s,length); + else + emit sendData(s,strlen(s)); +} + +// Replies ----------------------------------------------------------------- -- + +// This section copes with replies send as response to an enquiery control code. + +/*! +*/ + +void Vt102Emulation::reportCursorPosition() +{ char tmp[20]; + sprintf(tmp,"\033[%d;%dR",_currentScreen->getCursorY()+1,_currentScreen->getCursorX()+1); + sendString(tmp); +} + +/* + What follows here is rather obsolete and faked stuff. + The correspondent enquieries are neverthenless issued. +*/ + +/*! +*/ + +void Vt102Emulation::reportTerminalType() +{ + // Primary device attribute response (Request was: ^[[0c or ^[[c (from TT321 Users Guide)) + // VT220: ^[[?63;1;2;3;6;7;8c (list deps on emul. capabilities) + // VT100: ^[[?1;2c + // VT101: ^[[?1;0c + // VT102: ^[[?6v + if (getMode(MODE_Ansi)) + sendString("\033[?1;2c"); // I'm a VT100 + else + sendString("\033/Z"); // I'm a VT52 +} + +void Vt102Emulation::reportSecondaryAttributes() +{ + // Seconday device attribute response (Request was: ^[[>0c or ^[[>c) + if (getMode(MODE_Ansi)) + sendString("\033[>0;115;0c"); // Why 115? ;) + else + sendString("\033/Z"); // FIXME I don't think VT52 knows about it but kept for + // konsoles backward compatibility. +} + +void Vt102Emulation::reportTerminalParms(int p) +// DECREPTPARM +{ char tmp[100]; + sprintf(tmp,"\033[%d;1;1;112;112;1;0x",p); // not really true. + sendString(tmp); +} + +/*! +*/ + +void Vt102Emulation::reportStatus() +{ + sendString("\033[0n"); //VT100. Device status report. 0 = Ready. +} + +/*! +*/ + +#define ANSWER_BACK "" // This is really obsolete VT100 stuff. + +void Vt102Emulation::reportAnswerBack() +{ + sendString(ANSWER_BACK); +} + +// Mouse Handling ---------------------------------------------------------- -- + +/*! + Mouse clicks are possibly reported to the client + application if it has issued interest in them. + They are normally consumed by the widget for copy + and paste, but may be propagated from the widget + when gui->setMouseMarks is set via setMode(MODE_Mouse1000). + + `x',`y' are 1-based. + `ev' (event) indicates the button pressed (0-2) + or a general mouse release (3). + + eventType represents the kind of mouse action that occurred: + 0 = Mouse button press or release + 1 = Mouse drag +*/ + +void Vt102Emulation::sendMouseEvent( int cb, int cx, int cy , int eventType ) +{ char tmp[20]; + if ( cx<1 || cy<1 ) return; + // normal buttons are passed as 0x20 + button, + // mouse wheel (buttons 4,5) as 0x5c + button + if (cb >= 4) cb += 0x3c; + + //Mouse motion handling + if ( (getMode(MODE_Mouse1002) || getMode(MODE_Mouse1003)) && eventType == 1 ) + cb += 0x20; //add 32 to signify motion event + + sprintf(tmp,"\033[M%c%c%c",cb+0x20,cx+0x20,cy+0x20); + sendString(tmp); +} + +// Keyboard Handling ------------------------------------------------------- -- + +#define encodeMode(M,B) BITS(B,getMode(M)) +#define encodeStat(M,B) BITS(B,((ev->modifiers() & (M)) == (M))) + +void Vt102Emulation::sendText( const QString& text ) +{ + if (!text.isEmpty()) { + QKeyEvent event(QEvent::KeyPress, + 0, + Qt::NoModifier, + text); + sendKeyEvent(&event); // expose as a big fat keypress event + } + +} + +void Vt102Emulation::sendKeyEvent( QKeyEvent* event ) +{ + Qt::KeyboardModifiers modifiers = event->modifiers(); + KeyboardTranslator::States states = KeyboardTranslator::NoState; + + // get current states + if ( getMode(MODE_NewLine) ) states |= KeyboardTranslator::NewLineState; + if ( getMode(MODE_Ansi) ) states |= KeyboardTranslator::AnsiState; + if ( getMode(MODE_AppCuKeys)) states |= KeyboardTranslator::CursorKeysState; + if ( getMode(MODE_AppScreen)) states |= KeyboardTranslator::AlternateScreenState; + + // lookup key binding + if ( _keyTranslator ) + { + KeyboardTranslator::Entry entry = _keyTranslator->findEntry( + event->key() , + modifiers, + states ); + + // send result to terminal + QByteArray textToSend; + + // special handling for the Alt (aka. Meta) modifier. pressing + // Alt+[Character] results in Esc+[Character] being sent + // (unless there is an entry defined for this particular combination + // in the keyboard modifier) + bool wantsAltModifier = entry.modifiers() & entry.modifierMask() & Qt::AltModifier; + bool wantsAnyModifier = entry.state() & entry.stateMask() & KeyboardTranslator::AnyModifierState; + + if ( modifiers & Qt::AltModifier && !(wantsAltModifier || wantsAnyModifier) + && !event->text().isEmpty() ) + { + textToSend.prepend("\033"); + } + + if ( entry.command() != KeyboardTranslator::NoCommand ) + { + if (entry.command() & KeyboardTranslator::EraseCommand) + textToSend += getErase(); + // TODO command handling + } + else if ( !entry.text().isEmpty() ) + { + textToSend += _codec->fromUnicode(entry.text(true,modifiers)); + } + else + textToSend += _codec->fromUnicode(event->text()); + + sendData( textToSend.constData() , textToSend.length() ); + } + else + { + // print an error message to the terminal if no key translator has been + // set + QString translatorError = ("No keyboard translator available. " + "The information needed to convert key presses " + "into characters to send to the terminal " + "is missing."); + + reset(); + receiveData( translatorError.toAscii().constData() , translatorError.count() ); + } +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* VT100 Charsets */ +/* */ +/* ------------------------------------------------------------------------- */ + +// Character Set Conversion ------------------------------------------------ -- + +/* + The processing contains a VT100 specific code translation layer. + It's still in use and mainly responsible for the line drawing graphics. + + These and some other glyphs are assigned to codes (0x5f-0xfe) + normally occupied by the latin letters. Since this codes also + appear within control sequences, the extra code conversion + does not permute with the tokenizer and is placed behind it + in the pipeline. It only applies to tokens, which represent + plain characters. + + This conversion it eventually continued in TerminalDisplay.C, since + it might involve VT100 enhanced fonts, which have these + particular glyphs allocated in (0x00-0x1f) in their code page. +*/ + +#define CHARSET _charset[_currentScreen==_screen[1]] + +// Apply current character map. + +unsigned short Vt102Emulation::applyCharset(unsigned short c) +{ + if (CHARSET.graphic && 0x5f <= c && c <= 0x7e) return vt100_graphics[c-0x5f]; + if (CHARSET.pound && c == '#' ) return 0xa3; //This mode is obsolete + return c; +} + +/* + "Charset" related part of the emulation state. + This configures the VT100 _charset filter. + + While most operation work on the current _screen, + the following two are different. +*/ + +void Vt102Emulation::resetCharset(int scrno) +{ + _charset[scrno].cu_cs = 0; + strncpy(_charset[scrno].charset,"BBBB",4); + _charset[scrno].sa_graphic = false; + _charset[scrno].sa_pound = false; + _charset[scrno].graphic = false; + _charset[scrno].pound = false; +} + +void Vt102Emulation::setCharset(int n, int cs) // on both screens. +{ + _charset[0].charset[n&3] = cs; useCharset(_charset[0].cu_cs); + _charset[1].charset[n&3] = cs; useCharset(_charset[1].cu_cs); +} + +void Vt102Emulation::setAndUseCharset(int n, int cs) +{ + CHARSET.charset[n&3] = cs; + useCharset(n&3); +} + +void Vt102Emulation::useCharset(int n) +{ + CHARSET.cu_cs = n&3; + CHARSET.graphic = (CHARSET.charset[n&3] == '0'); + CHARSET.pound = (CHARSET.charset[n&3] == 'A'); //This mode is obsolete +} + +void Vt102Emulation::setDefaultMargins() +{ + _screen[0]->setDefaultMargins(); + _screen[1]->setDefaultMargins(); +} + +void Vt102Emulation::setMargins(int t, int b) +{ + _screen[0]->setMargins(t, b); + _screen[1]->setMargins(t, b); +} + +/*! Save the cursor position and the rendition attribute settings. */ + +void Vt102Emulation::saveCursor() +{ + CHARSET.sa_graphic = CHARSET.graphic; + CHARSET.sa_pound = CHARSET.pound; //This mode is obsolete + // we are not clear about these + //sa_charset = charsets[cScreen->_charset]; + //sa_charset_num = cScreen->_charset; + _currentScreen->saveCursor(); +} + +/*! Restore the cursor position and the rendition attribute settings. */ + +void Vt102Emulation::restoreCursor() +{ + CHARSET.graphic = CHARSET.sa_graphic; + CHARSET.pound = CHARSET.sa_pound; //This mode is obsolete + _currentScreen->restoreCursor(); +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Mode Operations */ +/* */ +/* ------------------------------------------------------------------------- */ + +/* + Some of the emulations state is either added to the state of the screens. + + This causes some scoping problems, since different emulations choose to + located the mode either to the current _screen or to both. + + For strange reasons, the extend of the rendition attributes ranges over + all screens and not over the actual _screen. + + We decided on the precise precise extend, somehow. +*/ + +// "Mode" related part of the state. These are all booleans. + +void Vt102Emulation::resetModes() +{ + resetMode(MODE_Mouse1000); saveMode(MODE_Mouse1000); + resetMode(MODE_Mouse1001); saveMode(MODE_Mouse1001); + resetMode(MODE_Mouse1002); saveMode(MODE_Mouse1002); + resetMode(MODE_Mouse1003); saveMode(MODE_Mouse1003); + + resetMode(MODE_AppScreen); saveMode(MODE_AppScreen); + // here come obsolete modes + resetMode(MODE_AppCuKeys); saveMode(MODE_AppCuKeys); + resetMode(MODE_NewLine ); + setMode(MODE_Ansi ); +} + +void Vt102Emulation::setMode(int m) +{ + _currParm.mode[m] = true; + switch (m) + { + case MODE_Mouse1000: + case MODE_Mouse1001: + case MODE_Mouse1002: + case MODE_Mouse1003: + emit programUsesMouseChanged(false); + break; + + case MODE_AppScreen : _screen[1]->clearSelection(); + setScreen(1); + break; + } + if (m < MODES_SCREEN || m == MODE_NewLine) + { + _screen[0]->setMode(m); + _screen[1]->setMode(m); + } +} + +void Vt102Emulation::resetMode(int m) +{ + _currParm.mode[m] = false; + switch (m) + { + case MODE_Mouse1000 : + case MODE_Mouse1001 : + case MODE_Mouse1002 : + case MODE_Mouse1003 : + emit programUsesMouseChanged(true); + break; + + case MODE_AppScreen : _screen[0]->clearSelection(); + setScreen(0); + break; + } + if (m < MODES_SCREEN || m == MODE_NewLine) + { + _screen[0]->resetMode(m); + _screen[1]->resetMode(m); + } +} + +void Vt102Emulation::saveMode(int m) +{ + _saveParm.mode[m] = _currParm.mode[m]; +} + +void Vt102Emulation::restoreMode(int m) +{ + if (_saveParm.mode[m]) + setMode(m); + else + resetMode(m); +} + +bool Vt102Emulation::getMode(int m) +{ + return _currParm.mode[m]; +} + +char Vt102Emulation::getErase() const +{ + KeyboardTranslator::Entry entry = _keyTranslator->findEntry( + Qt::Key_Backspace, + 0, + 0); + if ( entry.text().count() > 0 ) + return entry.text()[0]; + else + return '\b'; +} + +/* ------------------------------------------------------------------------- */ +/* */ +/* Diagnostic */ +/* */ +/* ------------------------------------------------------------------------- */ + +/*! shows the contents of the scan buffer. + + This functions is used for diagnostics. It is called by \e ReportErrorToken + to inform about strings that cannot be decoded or handled by the emulation. + + \sa ReportErrorToken +*/ + +static void hexdump(int* s, int len) +{ int i; + for (i = 0; i < len; i++) + { + if (s[i] == '\\') + printf("\\\\"); + else + if ((s[i]) > 32 && s[i] < 127) + printf("%c",s[i]); + else + printf("\\%04x(hex)",s[i]); + } +} + +void Vt102Emulation::scan_buffer_report() +{ + if (ppos == 0 || ppos == 1 && (pbuf[0] & 0xff) >= 32) return; + printf("token: "); hexdump(pbuf,ppos); printf("\n"); +} + +/*! +*/ + +void Vt102Emulation::ReportErrorToken() +{ +#ifndef NDEBUG + printf("undecodable "); scan_buffer_report(); +#endif +} + +//#include "moc_Vt102Emulation.cpp" +
new file mode 100644 --- /dev/null +++ b/gui//Vt102Emulation.h @@ -0,0 +1,192 @@ +/* + This file is part of Konsole, an X terminal. + + Copyright (C) 2007 by Robert Knight <robertknight@gmail.com> + Copyright (C) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + 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 Street, Fifth Floor, Boston, MA + 02110-1301 USA. +*/ + +#ifndef VT102EMULATION_H +#define VT102EMULATION_H + +// Standard Library +#include <stdio.h> + +// Qt +#include <QtGui/QKeyEvent> +#include <QtCore/QHash> +#include <QtCore/QTimer> + +// Konsole +#include "Emulation.h" +#include "Screen.h" + +#define MODE_AppScreen (MODES_SCREEN+0) +#define MODE_AppCuKeys (MODES_SCREEN+1) +#define MODE_AppKeyPad (MODES_SCREEN+2) +#define MODE_Mouse1000 (MODES_SCREEN+3) +#define MODE_Mouse1001 (MODES_SCREEN+4) +#define MODE_Mouse1002 (MODES_SCREEN+5) +#define MODE_Mouse1003 (MODES_SCREEN+6) +#define MODE_Ansi (MODES_SCREEN+7) +#define MODE_total (MODES_SCREEN+8) + +namespace Konsole +{ + +struct DECpar +{ + bool mode[MODE_total]; +}; + +struct CharCodes +{ + // coding info + char charset[4]; // + int cu_cs; // actual charset. + bool graphic; // Some VT100 tricks + bool pound ; // Some VT100 tricks + bool sa_graphic; // saved graphic + bool sa_pound; // saved pound +}; + +/** + * Provides an xterm compatible terminal emulation based on the DEC VT102 terminal. + * A full description of this terminal can be found at http://vt100.net/docs/vt102-ug/ + * + * In addition, various additional xterm escape sequences are supported to provide + * features such as mouse input handling. + * See http://rtfm.etla.org/xterm/ctlseq.html for a description of xterm's escape + * sequences. + * + */ +class Vt102Emulation : public Emulation +{ +Q_OBJECT + +public: + + /** Constructs a new emulation */ + Vt102Emulation(); + ~Vt102Emulation(); + + // reimplemented + virtual void clearEntireScreen(); + virtual void reset(); + + // reimplemented + virtual char getErase() const; + +public slots: + + // reimplemented + virtual void sendString(const char*,int length = -1); + virtual void sendText(const QString& text); + virtual void sendKeyEvent(QKeyEvent*); + virtual void sendMouseEvent( int buttons, int column, int line , int eventType ); + +protected: + // reimplemented + virtual void setMode (int mode); + virtual void resetMode (int mode); + + // reimplemented + virtual void receiveChar(int cc); + + +private slots: + + //causes changeTitle() to be emitted for each (int,QString) pair in pendingTitleUpdates + //used to buffer multiple title updates + void updateTitle(); + + +private: + unsigned short applyCharset(unsigned short c); + void setCharset(int n, int cs); + void useCharset(int n); + void setAndUseCharset(int n, int cs); + void saveCursor(); + void restoreCursor(); + void resetCharset(int scrno); + + void setMargins(int top, int bottom); + //set margins for all screens back to their defaults + void setDefaultMargins(); + + // returns true if 'mode' is set or false otherwise + bool getMode (int mode); + // saves the current boolean value of 'mode' + void saveMode (int mode); + // restores the boolean value of 'mode' + void restoreMode(int mode); + // resets all modes + void resetModes(); + + void resetToken(); +#define MAXPBUF 80 + void pushToToken(int cc); + int pbuf[MAXPBUF]; //FIXME: overflow? + int ppos; +#define MAXARGS 15 + void addDigit(int dig); + void addArgument(); + int argv[MAXARGS]; + int argc; + void initTokenizer(); + int tbl[256]; + + void scan_buffer_report(); //FIXME: rename + void ReportErrorToken(); //FIXME: rename + + void tau(int code, int p, int q); + void XtermHack(); + + void reportTerminalType(); + void reportSecondaryAttributes(); + void reportStatus(); + void reportAnswerBack(); + void reportCursorPosition(); + void reportTerminalParms(int p); + + void onScrollLock(); + void scrollLock(const bool lock); + + // clears the screen and resizes it to the specified + // number of columns + void clearScreenAndSetColumns(int columnCount); + + CharCodes _charset[2]; + + DECpar _currParm; + DECpar _saveParm; + + //hash table and timer for buffering calls to the session instance + //to update the name of the session + //or window title. + //these calls occur when certain escape sequences are seen in the + //output from the terminal + QHash<int,QString> _pendingTitleUpdates; + QTimer* _titleUpdateTimer; + +}; + +} + +#endif // VT102EMULATION_H
new file mode 100644 --- /dev/null +++ b/gui//default.keytab @@ -0,0 +1,128 @@ +# [README.default.Keytab] Buildin Keyboard Table +# +# To customize your keyboard, copy this file to something +# ending with .keytab and change it to meet you needs. +# Please read the README.KeyTab and the README.keyboard +# in this case. +# +# -------------------------------------------------------------- + +keyboard "Default (XFree 4)" + +# -------------------------------------------------------------- +# +# Note that this particular table is a "risc" version made to +# ease customization without bothering with obsolete details. +# See VT100.keytab for the more hairy stuff. +# +# -------------------------------------------------------------- + +# common keys + +key Escape : "\E" + +key Tab -Shift : "\t" +key Tab +Shift+Ansi : "\E[Z" +key Tab +Shift-Ansi : "\t" +key Backtab +Ansi : "\E[Z" +key Backtab -Ansi : "\t" + +key Return-Shift-NewLine : "\r" +key Return-Shift+NewLine : "\r\n" + +key Return+Shift : "\EOM" + +# Backspace and Delete codes are preserving CTRL-H. + +key Backspace : "\x7f" + +# Arrow keys in VT52 mode +# shift up/down are reserved for scrolling. +# shift left/right are reserved for switching between tabs (this is hardcoded). + +key Up -Shift-Ansi : "\EA" +key Down -Shift-Ansi : "\EB" +key Right-Shift-Ansi : "\EC" +key Left -Shift-Ansi : "\ED" + +# Arrow keys in ANSI mode with Application - and Normal Cursor Mode) + +key Up -Shift-AnyMod+Ansi+AppCuKeys : "\EOA" +key Down -Shift-AnyMod+Ansi+AppCuKeys : "\EOB" +key Right -Shift-AnyMod+Ansi+AppCuKeys : "\EOC" +key Left -Shift-AnyMod+Ansi+AppCuKeys : "\EOD" + +key Up -Shift-AnyMod+Ansi-AppCuKeys : "\E[A" +key Down -Shift-AnyMod+Ansi-AppCuKeys : "\E[B" +key Right -Shift-AnyMod+Ansi-AppCuKeys : "\E[C" +key Left -Shift-AnyMod+Ansi-AppCuKeys : "\E[D" + +key Up -Shift+AnyMod+Ansi : "\E[1;*A" +key Down -Shift+AnyMod+Ansi : "\E[1;*B" +key Right -Shift+AnyMod+Ansi : "\E[1;*C" +key Left -Shift+AnyMod+Ansi : "\E[1;*D" + +# other grey PC keys + +key Enter+NewLine : "\r\n" +key Enter-NewLine : "\r" + +key Home -AnyMod -AppCuKeys : "\E[H" +key End -AnyMod -AppCuKeys : "\E[F" +key Home -AnyMod +AppCuKeys : "\EOH" +key End -AnyMod +AppCuKeys : "\EOF" +key Home +AnyMod : "\E[1;*H" +key End +AnyMod : "\E[1;*F" + +key Insert -AnyMod : "\E[2~" +key Delete -AnyMod : "\E[3~" +key Insert +AnyMod : "\E[2;*~" +key Delete +AnyMod : "\E[3;*~" + +key Prior -Shift-AnyMod : "\E[5~" +key Next -Shift-AnyMod : "\E[6~" +key Prior -Shift+AnyMod : "\E[5;*~" +key Next -Shift+AnyMod : "\E[6;*~" + +# Function keys +key F1 -AnyMod : "\EOP" +key F2 -AnyMod : "\EOQ" +key F3 -AnyMod : "\EOR" +key F4 -AnyMod : "\EOS" +key F5 -AnyMod : "\E[15~" +key F6 -AnyMod : "\E[17~" +key F7 -AnyMod : "\E[18~" +key F8 -AnyMod : "\E[19~" +key F9 -AnyMod : "\E[20~" +key F10 -AnyMod : "\E[21~" +key F11 -AnyMod : "\E[23~" +key F12 -AnyMod : "\E[24~" + +key F1 +AnyMod : "\EO*P" +key F2 +AnyMod : "\EO*Q" +key F3 +AnyMod : "\EO*R" +key F4 +AnyMod : "\EO*S" +key F5 +AnyMod : "\E[15;*~" +key F6 +AnyMod : "\E[17;*~" +key F7 +AnyMod : "\E[18;*~" +key F8 +AnyMod : "\E[19;*~" +key F9 +AnyMod : "\E[20;*~" +key F10 +AnyMod : "\E[21;*~" +key F11 +AnyMod : "\E[23;*~" +key F12 +AnyMod : "\E[24;*~" + +# Work around dead keys + +key Space +Control : "\x00" + +# Some keys are used by konsole to cause operations. +# The scroll* operations refer to the history buffer. + +key Up +Shift-AppScreen : scrollLineUp +key Prior +Shift-AppScreen : scrollPageUp +key Down +Shift-AppScreen : scrollLineDown +key Next +Shift-AppScreen : scrollPageDown + +key ScrollLock : scrollLock + +# keypad characters are not offered differently by Qt.
new file mode 100644 --- /dev/null +++ b/gui//k3process.cpp @@ -0,0 +1,1053 @@ +/* + This file is part of the KDE libraries + Copyright (C) 1997 Christian Czezatke (e9025461@student.tuwien.ac.at) + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + + +#include "k3process.h" +//#include <config.h> + +#include "k3processcontroller.h" +#include "kpty.h" + +#ifdef __osf__ +#define _OSF_SOURCE +#include <float.h> +#endif + +#ifdef _AIX +#define _ALL_SOURCE +#endif + +#include <sys/socket.h> +#include <sys/ioctl.h> + +#include <sys/types.h> +#include <sys/time.h> +#include <sys/resource.h> +#include <sys/stat.h> +#include <sys/wait.h> + +#ifdef HAVE_SYS_SELECT_H +#include <sys/select.h> +#endif + +#include <errno.h> +#include <assert.h> +#include <fcntl.h> +#include <time.h> +#include <stdlib.h> +#include <signal.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <pwd.h> +#include <grp.h> + +#include <QtCore/QMap> +#include <QtCore/QFile> +#include <QtCore/QSocketNotifier> + +//#include <kdebug.h> +//#include <kstandarddirs.h> +//#include <kuser.h> + + +////////////////// +// private data // +////////////////// + +class K3ProcessPrivate { +public: + K3ProcessPrivate() : + usePty(K3Process::NoCommunication), + addUtmp(false), useShell(false), + pty(0), + priority(0) + { + } + + K3Process::Communication usePty; + bool addUtmp : 1; + bool useShell : 1; + + KPty *pty; + + int priority; + + QMap<QString,QString> env; + QString wd; + QByteArray shell; + QByteArray executable; +}; + +///////////////////////////// +// public member functions // +///////////////////////////// + +K3Process::K3Process( QObject* parent ) + : QObject( parent ), + run_mode(NotifyOnExit), + runs(false), + pid_(0), + status(0), + keepPrivs(false), + innot(0), + outnot(0), + errnot(0), + communication(NoCommunication), + input_data(0), + input_sent(0), + input_total(0), + d(new K3ProcessPrivate) +{ + K3ProcessController::ref(); + K3ProcessController::instance()->addKProcess(this); + + + out[0] = out[1] = -1; + in[0] = in[1] = -1; + err[0] = err[1] = -1; +} + +void +K3Process::setEnvironment(const QString &name, const QString &value) +{ + d->env.insert(name, value); +} + +void +K3Process::setWorkingDirectory(const QString &dir) +{ + d->wd = dir; +} + +void +K3Process::setupEnvironment() +{ + QMap<QString,QString>::Iterator it; + for(it = d->env.begin(); it != d->env.end(); ++it) + { + setenv(QFile::encodeName(it.key()).data(), + QFile::encodeName(it.value()).data(), 1); + } + if (!d->wd.isEmpty()) + { + chdir(QFile::encodeName(d->wd).data()); + } +} + +void +K3Process::setRunPrivileged(bool keepPrivileges) +{ + keepPrivs = keepPrivileges; +} + +bool +K3Process::runPrivileged() const +{ + return keepPrivs; +} + +bool +K3Process::setPriority(int prio) +{ + if (runs) { + if (setpriority(PRIO_PROCESS, pid_, prio)) + return false; + } else { + if (prio > 19 || prio < (geteuid() ? getpriority(PRIO_PROCESS, 0) : -20)) + return false; + } + d->priority = prio; + return true; +} + +K3Process::~K3Process() +{ + if (run_mode != DontCare) + kill(SIGKILL); + detach(); + + delete d->pty; + delete d; + + K3ProcessController::instance()->removeKProcess(this); + K3ProcessController::deref(); +} + +void K3Process::detach() +{ + if (runs) { + K3ProcessController::instance()->addProcess(pid_); + runs = false; + pid_ = 0; // close without draining + commClose(); // Clean up open fd's and socket notifiers. + } +} + +void K3Process::setBinaryExecutable(const char *filename) +{ + d->executable = filename; +} + +K3Process &K3Process::operator<<(const QStringList& args) +{ + QStringList::ConstIterator it = args.begin(); + for ( ; it != args.end() ; ++it ) + arguments.append(QFile::encodeName(*it)); + return *this; +} + +K3Process &K3Process::operator<<(const QByteArray& arg) +{ + return operator<< (arg.data()); +} + +K3Process &K3Process::operator<<(const char* arg) +{ + arguments.append(arg); + return *this; +} + +K3Process &K3Process::operator<<(const QString& arg) +{ + arguments.append(QFile::encodeName(arg)); + return *this; +} + +void K3Process::clearArguments() +{ + arguments.clear(); +} + +bool K3Process::start(RunMode runmode, Communication comm) +{ + if (runs) { + qDebug() << "Attempted to start an already running process" << endl; + return false; + } + + uint n = arguments.count(); + if (n == 0) { + qDebug() << "Attempted to start a process without arguments" << endl; + return false; + } + char **arglist; + QByteArray shellCmd; + if (d->useShell) + { + if (d->shell.isEmpty()) { + qDebug() << "Invalid shell specified" << endl; + return false; + } + + for (uint i = 0; i < n; i++) { + shellCmd += arguments[i]; + shellCmd += ' '; // CC: to separate the arguments + } + + arglist = static_cast<char **>(malloc( 4 * sizeof(char *))); + arglist[0] = d->shell.data(); + arglist[1] = (char *) "-c"; + arglist[2] = shellCmd.data(); + arglist[3] = 0; + } + else + { + arglist = static_cast<char **>(malloc( (n + 1) * sizeof(char *))); + for (uint i = 0; i < n; i++) + arglist[i] = arguments[i].data(); + arglist[n] = 0; + } + + run_mode = runmode; + + if (!setupCommunication(comm)) + { + qDebug() << "Could not setup Communication!" << endl; + free(arglist); + return false; + } + + // We do this in the parent because if we do it in the child process + // gdb gets confused when the application runs from gdb. +#ifdef HAVE_INITGROUPS + struct passwd *pw = geteuid() ? 0 : getpwuid(getuid()); +#endif + + int fd[2]; + if (pipe(fd)) + fd[0] = fd[1] = -1; // Pipe failed.. continue + + // we don't use vfork() because + // - it has unclear semantics and is not standardized + // - we do way too much magic in the child + pid_ = fork(); + if (pid_ == 0) { + // The child process + + close(fd[0]); + // Closing of fd[1] indicates that the execvp() succeeded! + fcntl(fd[1], F_SETFD, FD_CLOEXEC); + + if (!commSetupDoneC()) + qDebug() << "Could not finish comm setup in child!" << endl; + + // reset all signal handlers + struct sigaction act; + sigemptyset(&act.sa_mask); + act.sa_handler = SIG_DFL; + act.sa_flags = 0; + for (int sig = 1; sig < NSIG; sig++) + sigaction(sig, &act, 0L); + + if (d->priority) + setpriority(PRIO_PROCESS, 0, d->priority); + + if (!runPrivileged()) + { + setgid(getgid()); +#ifdef HAVE_INITGROUPS + if (pw) + initgroups(pw->pw_name, pw->pw_gid); +#endif + if (geteuid() != getuid()) + setuid(getuid()); + if (geteuid() != getuid()) + _exit(1); + } + + setupEnvironment(); + + if (runmode == DontCare || runmode == OwnGroup) + setsid(); + + const char *executable = arglist[0]; + if (!d->executable.isEmpty()) + executable = d->executable.data(); + execvp(executable, arglist); + + char resultByte = 1; + write(fd[1], &resultByte, 1); + _exit(-1); + } else if (pid_ == -1) { + // forking failed + + // commAbort(); + pid_ = 0; + free(arglist); + return false; + } + // the parent continues here + free(arglist); + + if (!commSetupDoneP()) + qDebug() << "Could not finish comm setup in parent!" << endl; + + // Check whether client could be started. + close(fd[1]); + for(;;) + { + char resultByte; + int n = ::read(fd[0], &resultByte, 1); + if (n == 1) + { + // exec() failed + close(fd[0]); + waitpid(pid_, 0, 0); + pid_ = 0; + commClose(); + return false; + } + if (n == -1) + { + if (errno == EINTR) + continue; // Ignore + } + break; // success + } + close(fd[0]); + + runs = true; + switch (runmode) + { + case Block: + for (;;) + { + commClose(); // drain only, unless obsolete reimplementation + if (!runs) + { + // commClose detected data on the process exit notifification pipe + K3ProcessController::instance()->unscheduleCheck(); + if (waitpid(pid_, &status, WNOHANG) != 0) // error finishes, too + { + commClose(); // this time for real (runs is false) + K3ProcessController::instance()->rescheduleCheck(); + break; + } + runs = true; // for next commClose() iteration + } + else + { + // commClose is an obsolete reimplementation and waited until + // all output channels were closed (or it was interrupted). + // there is a chance that it never gets here ... + waitpid(pid_, &status, 0); + runs = false; + break; + } + } + // why do we do this? i think this signal should be emitted _only_ + // after the process has successfully run _asynchronously_ --ossi + emit processExited(this); + break; + default: // NotifyOnExit & OwnGroup + input_data = 0; // Discard any data for stdin that might still be there + break; + } + return true; +} + + + +bool K3Process::kill(int signo) +{ + if (runs && pid_ > 0 && !::kill(run_mode == OwnGroup ? -pid_ : pid_, signo)) + return true; + return false; +} + + + +bool K3Process::isRunning() const +{ + return runs; +} + + + +pid_t K3Process::pid() const +{ + return pid_; +} + +#ifndef timersub +# define timersub(a, b, result) \ + do { \ + (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \ + (result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \ + if ((result)->tv_usec < 0) { \ + --(result)->tv_sec; \ + (result)->tv_usec += 1000000; \ + } \ + } while (0) +#endif + +bool K3Process::wait(int timeout) +{ + if (!runs) + return true; + +#ifndef __linux__ + struct timeval etv; +#endif + struct timeval tv, *tvp; + if (timeout < 0) + tvp = 0; + else + { +#ifndef __linux__ + gettimeofday(&etv, 0); + etv.tv_sec += timeout; +#else + tv.tv_sec = timeout; + tv.tv_usec = 0; +#endif + tvp = &tv; + } + + int fd = K3ProcessController::instance()->notifierFd(); + for(;;) + { + fd_set fds; + FD_ZERO( &fds ); + FD_SET( fd, &fds ); + +#ifndef __linux__ + if (tvp) + { + gettimeofday(&tv, 0); + timersub(&etv, &tv, &tv); + if (tv.tv_sec < 0) + tv.tv_sec = tv.tv_usec = 0; + } +#endif + + switch( select( fd+1, &fds, 0, 0, tvp ) ) + { + case -1: + if( errno == EINTR ) + break; + // fall through; should happen if tvp->tv_sec < 0 + case 0: + K3ProcessController::instance()->rescheduleCheck(); + return false; + default: + K3ProcessController::instance()->unscheduleCheck(); + if (waitpid(pid_, &status, WNOHANG) != 0) // error finishes, too + { + processHasExited(status); + K3ProcessController::instance()->rescheduleCheck(); + return true; + } + } + } + return false; +} + + + +bool K3Process::normalExit() const +{ + return (pid_ != 0) && !runs && WIFEXITED(status); +} + + +bool K3Process::signalled() const +{ + return (pid_ != 0) && !runs && WIFSIGNALED(status); +} + + +bool K3Process::coreDumped() const +{ +#ifdef WCOREDUMP + return signalled() && WCOREDUMP(status); +#else + return false; +#endif +} + + +int K3Process::exitStatus() const +{ + return WEXITSTATUS(status); +} + + +int K3Process::exitSignal() const +{ + return WTERMSIG(status); +} + + +bool K3Process::writeStdin(const char *buffer, int buflen) +{ + // if there is still data pending, writing new data + // to stdout is not allowed (since it could also confuse + // kprocess ...) + if (input_data != 0) + return false; + + if (communication & Stdin) { + input_data = buffer; + input_sent = 0; + input_total = buflen; + innot->setEnabled(true); + if (input_total) + slotSendData(0); + return true; + } else + return false; +} + +void K3Process::suspend() +{ + if (outnot) + outnot->setEnabled(false); +} + +void K3Process::resume() +{ + if (outnot) + outnot->setEnabled(true); +} + +bool K3Process::closeStdin() +{ + if (communication & Stdin) { + communication = communication & ~Stdin; + delete innot; + innot = 0; + if (!(d->usePty & Stdin)) + close(in[1]); + in[1] = -1; + return true; + } else + return false; +} + +bool K3Process::closeStdout() +{ + if (communication & Stdout) { + communication = communication & ~Stdout; + delete outnot; + outnot = 0; + if (!(d->usePty & Stdout)) + close(out[0]); + out[0] = -1; + return true; + } else + return false; +} + +bool K3Process::closeStderr() +{ + if (communication & Stderr) { + communication = communication & ~Stderr; + delete errnot; + errnot = 0; + if (!(d->usePty & Stderr)) + close(err[0]); + err[0] = -1; + return true; + } else + return false; +} + +bool K3Process::closePty() +{ + if (d->pty && d->pty->masterFd() >= 0) { + if (d->addUtmp) + d->pty->logout(); + d->pty->close(); + return true; + } else + return false; +} + +void K3Process::closeAll() +{ + closeStdin(); + closeStdout(); + closeStderr(); + closePty(); +} + +///////////////////////////// +// protected slots // +///////////////////////////// + + + +void K3Process::slotChildOutput(int fdno) +{ + if (!childOutput(fdno)) + closeStdout(); +} + + +void K3Process::slotChildError(int fdno) +{ + if (!childError(fdno)) + closeStderr(); +} + + +void K3Process::slotSendData(int) +{ + if (input_sent == input_total) { + innot->setEnabled(false); + input_data = 0; + emit wroteStdin(this); + } else { + int result = ::write(in[1], input_data+input_sent, input_total-input_sent); + if (result >= 0) + { + input_sent += result; + } + else if ((errno != EAGAIN) && (errno != EINTR)) + { + qDebug() << "Error writing to stdin of child process" << endl; + closeStdin(); + } + } +} + +void K3Process::setUseShell(bool useShell, const char *shell) +{ + d->useShell = useShell; + if (shell && *shell) + d->shell = shell; + else +// #ifdef NON_FREE // ... as they ship non-POSIX /bin/sh +#if !defined(__linux__) && !defined(__FreeBSD__) && !defined(__NetBSD__) && !defined(__OpenBSD__) && !defined(__GNU__) && !defined(__DragonFly__) + // Solaris POSIX ... + if (!access( "/usr/xpg4/bin/sh", X_OK )) + d->shell = "/usr/xpg4/bin/sh"; + else + // ... which links here anyway + if (!access( "/bin/ksh", X_OK )) + d->shell = "/bin/ksh"; + else + // dunno, maybe superfluous? + if (!access( "/usr/ucb/sh", X_OK )) + d->shell = "/usr/ucb/sh"; + else +#endif + d->shell = "/bin/sh"; +} + +void K3Process::setUsePty(Communication usePty, bool addUtmp) +{ + d->usePty = usePty; + d->addUtmp = addUtmp; + if (usePty) { + if (!d->pty) + d->pty = new KPty; + } else { + delete d->pty; + d->pty = 0; + } +} + +KPty *K3Process::pty() const +{ + return d->pty; +} + +QString K3Process::quote(const QString &arg) +{ + QChar q('\''); + return QString(arg).replace(q, "'\\''").prepend(q).append(q); +} + + +////////////////////////////// +// private member functions // +////////////////////////////// + + +void K3Process::processHasExited(int state) +{ + // only successfully run NotifyOnExit processes ever get here + + status = state; + runs = false; // do this before commClose, so it knows we're dead + + commClose(); // cleanup communication sockets + + if (run_mode != DontCare) + emit processExited(this); +} + + + +int K3Process::childOutput(int fdno) +{ + if (communication & NoRead) { + int len = -1; + emit receivedStdout(fdno, len); + errno = 0; // Make sure errno doesn't read "EAGAIN" + return len; + } + else + { + char buffer[1025]; + int len; + + len = ::read(fdno, buffer, 1024); + + if (len > 0) { + buffer[len] = 0; // Just in case. + emit receivedStdout(this, buffer, len); + } + return len; + } +} + +int K3Process::childError(int fdno) +{ + char buffer[1025]; + int len; + + len = ::read(fdno, buffer, 1024); + + if (len > 0) { + buffer[len] = 0; // Just in case. + emit receivedStderr(this, buffer, len); + } + return len; +} + + +int K3Process::setupCommunication(Communication comm) +{ + // PTY stuff // + if (d->usePty) + { + // cannot communicate on both stderr and stdout if they are both on the pty + if (!(~(comm & d->usePty) & (Stdout | Stderr))) { + qWarning() << "Invalid usePty/communication combination (" << d->usePty << "/" << comm << ")" << endl; + return 0; + } + if (!d->pty->open()) + return 0; + + int rcomm = comm & d->usePty; + int mfd = d->pty->masterFd(); + if (rcomm & Stdin) + in[1] = mfd; + if (rcomm & Stdout) + out[0] = mfd; + if (rcomm & Stderr) + err[0] = mfd; + } + + communication = comm; + + comm = comm & ~d->usePty; + if (comm & Stdin) { + if (socketpair(AF_UNIX, SOCK_STREAM, 0, in)) + goto fail0; + fcntl(in[0], F_SETFD, FD_CLOEXEC); + fcntl(in[1], F_SETFD, FD_CLOEXEC); + } + if (comm & Stdout) { + if (socketpair(AF_UNIX, SOCK_STREAM, 0, out)) + goto fail1; + fcntl(out[0], F_SETFD, FD_CLOEXEC); + fcntl(out[1], F_SETFD, FD_CLOEXEC); + } + if (comm & Stderr) { + if (socketpair(AF_UNIX, SOCK_STREAM, 0, err)) + goto fail2; + fcntl(err[0], F_SETFD, FD_CLOEXEC); + fcntl(err[1], F_SETFD, FD_CLOEXEC); + } + return 1; // Ok + fail2: + if (comm & Stdout) + { + close(out[0]); + close(out[1]); + out[0] = out[1] = -1; + } + fail1: + if (comm & Stdin) + { + close(in[0]); + close(in[1]); + in[0] = in[1] = -1; + } + fail0: + communication = NoCommunication; + return 0; // Error +} + + + +int K3Process::commSetupDoneP() +{ + int rcomm = communication & ~d->usePty; + if (rcomm & Stdin) + close(in[0]); + if (rcomm & Stdout) + close(out[1]); + if (rcomm & Stderr) + close(err[1]); + in[0] = out[1] = err[1] = -1; + + // Don't create socket notifiers if no interactive comm is to be expected + if (run_mode != NotifyOnExit && run_mode != OwnGroup) + return 1; + + if (communication & Stdin) { + fcntl(in[1], F_SETFL, O_NONBLOCK | fcntl(in[1], F_GETFL)); + innot = new QSocketNotifier(in[1], QSocketNotifier::Write, this); + Q_CHECK_PTR(innot); + innot->setEnabled(false); // will be enabled when data has to be sent + QObject::connect(innot, SIGNAL(activated(int)), + this, SLOT(slotSendData(int))); + } + + if (communication & Stdout) { + outnot = new QSocketNotifier(out[0], QSocketNotifier::Read, this); + Q_CHECK_PTR(outnot); + QObject::connect(outnot, SIGNAL(activated(int)), + this, SLOT(slotChildOutput(int))); + if (communication & NoRead) + suspend(); + } + + if (communication & Stderr) { + errnot = new QSocketNotifier(err[0], QSocketNotifier::Read, this ); + Q_CHECK_PTR(errnot); + QObject::connect(errnot, SIGNAL(activated(int)), + this, SLOT(slotChildError(int))); + } + + return 1; +} + + + +int K3Process::commSetupDoneC() +{ + int ok = 1; + if (d->usePty & Stdin) { + if (dup2(d->pty->slaveFd(), STDIN_FILENO) < 0) ok = 0; + } else if (communication & Stdin) { + if (dup2(in[0], STDIN_FILENO) < 0) ok = 0; + } else { + int null_fd = open( "/dev/null", O_RDONLY ); + if (dup2( null_fd, STDIN_FILENO ) < 0) ok = 0; + close( null_fd ); + } + struct linger so; + memset(&so, 0, sizeof(so)); + if (d->usePty & Stdout) { + if (dup2(d->pty->slaveFd(), STDOUT_FILENO) < 0) ok = 0; + } else if (communication & Stdout) { + if (dup2(out[1], STDOUT_FILENO) < 0 || + setsockopt(out[1], SOL_SOCKET, SO_LINGER, (char *)&so, sizeof(so))) + ok = 0; + if (communication & MergedStderr) { + if (dup2(out[1], STDERR_FILENO) < 0) + ok = 0; + } + } + if (d->usePty & Stderr) { + if (dup2(d->pty->slaveFd(), STDERR_FILENO) < 0) ok = 0; + } else if (communication & Stderr) { + if (dup2(err[1], STDERR_FILENO) < 0 || + setsockopt(err[1], SOL_SOCKET, SO_LINGER, (char *)&so, sizeof(so))) + ok = 0; + } + + // don't even think about closing all open fds here or anywhere else + + // PTY stuff // + if (d->usePty) { + d->pty->setCTty(); + if (d->addUtmp) + d->pty->login(getenv("USER"), getenv("DISPLAY")); + } + + return ok; +} + + + +void K3Process::commClose() +{ + closeStdin(); + + if (pid_) { // detached, failed, and killed processes have no output. basta. :) + // If both channels are being read we need to make sure that one socket + // buffer doesn't fill up whilst we are waiting for data on the other + // (causing a deadlock). Hence we need to use select. + + int notfd = K3ProcessController::instance()->notifierFd(); + + while ((communication & (Stdout | Stderr)) || runs) { + fd_set rfds; + FD_ZERO(&rfds); + struct timeval timeout, *p_timeout; + + int max_fd = 0; + if (communication & Stdout) { + FD_SET(out[0], &rfds); + max_fd = out[0]; + } + if (communication & Stderr) { + FD_SET(err[0], &rfds); + if (err[0] > max_fd) + max_fd = err[0]; + } + if (runs) { + FD_SET(notfd, &rfds); + if (notfd > max_fd) + max_fd = notfd; + // If the process is still running we block until we + // receive data or the process exits. + p_timeout = 0; // no timeout + } else { + // If the process has already exited, we only check + // the available data, we don't wait for more. + timeout.tv_sec = timeout.tv_usec = 0; // timeout immediately + p_timeout = &timeout; + } + + int fds_ready = select(max_fd+1, &rfds, 0, 0, p_timeout); + if (fds_ready < 0) { + if (errno == EINTR) + continue; + break; + } else if (!fds_ready) + break; + + if ((communication & Stdout) && FD_ISSET(out[0], &rfds)) + slotChildOutput(out[0]); + + if ((communication & Stderr) && FD_ISSET(err[0], &rfds)) + slotChildError(err[0]); + + if (runs && FD_ISSET(notfd, &rfds)) { + runs = false; // hack: signal potential exit + return; // don't close anything, we will be called again + } + } + } + + closeStdout(); + closeStderr(); + + closePty(); +} + + + +/////////////////////////// +// CC: Class K3ShellProcess +/////////////////////////// + +K3ShellProcess::K3ShellProcess(const char *shellname): + K3Process(), d(0) +{ + setUseShell( true, shellname ? shellname : getenv("SHELL") ); +} + +K3ShellProcess::~K3ShellProcess() { +} + +QString K3ShellProcess::quote(const QString &arg) +{ + return K3Process::quote(arg); +} + +bool K3ShellProcess::start(RunMode runmode, Communication comm) +{ + return K3Process::start(runmode, comm); +} + + +//#include "moc_k3process.cpp"
new file mode 100644 --- /dev/null +++ b/gui//k3process.h @@ -0,0 +1,890 @@ +/* This file is part of the KDE libraries + Copyright (C) 1997 Christian Czezakte (e9025461@student.tuwien.ac.at) + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef K3PROCESS_H +#define K3PROCESS_H + +#include <QtCore/QObject> + +#include <sys/types.h> // for pid_t +#include <sys/wait.h> +#include <signal.h> +#include <unistd.h> + +class QSocketNotifier; +class K3ProcessPrivate; +class KPty; + +/** + * @obsolete Use KProcess and KPtyProcess instead. + * + * Child process invocation, monitoring and control. + * This class works only in the application's main thread. + * + * <b>General usage and features:</b>\n + * + * This class allows a KDE application to start child processes without having + * to worry about UN*X signal handling issues and zombie process reaping. + * + * @see K3ProcIO + * + * Basically, this class distinguishes three different ways of running + * child processes: + * + * @li DontCare -- The child process is invoked and both the child + * process and the parent process continue concurrently. + * + * The process is started in an own session (see setsid(2)). + * + * @li NotifyOnExit -- The child process is invoked and both the + * child and the parent process run concurrently. + * + * When the child process exits, the K3Process instance + * corresponding to it emits the Qt signal processExited(). + * Since this signal is @em not emitted from within a UN*X + * signal handler, arbitrary function calls can be made. + * + * Be aware: When the K3Process object gets destructed, the child + * process will be killed if it is still running! + * This means in particular, that it usually makes no sense to use + * a K3Process on the stack with NotifyOnExit. + * + * @li OwnGroup -- like NotifyOnExit, but the child process is started + * in an own process group (and an own session, FWIW). The behavior of + * kill() changes to killing the whole process group - this makes + * this mode useful for implementing primitive job management. It can be + * used to work around broken wrapper scripts that don't propagate signals + * to the "real" program. However, use this with care, as you disturb the + * shell's job management if your program is started from the command line. + * + * @li Block -- The child process starts and the parent process + * is suspended until the child process exits. (@em Really not recommended + * for programs with a GUI.) + * In this mode the parent can read the child's output, but can't send it any + * input. + * + * K3Process also provides several functions for determining the exit status + * and the pid of the child process it represents. + * + * Furthermore it is possible to supply command-line arguments to the process + * in a clean fashion (no null-terminated stringlists and such...) + * + * A small usage example: + * \code + * K3Process *proc = new K3Process; + * + * *proc << "my_executable"; + * *proc << "These" << "are" << "the" << "command" << "line" << "args"; + * QApplication::connect(proc, SIGNAL(processExited(K3Process *)), + * pointer_to_my_object, SLOT(my_objects_slot(K3Process *))); + * proc->start(); + * \endcode + * + * This will start "my_executable" with the commandline arguments "These"... + * + * When the child process exits, the slot will be invoked. + * + * <b>Communication with the child process:</b>\n + * + * K3Process supports communication with the child process through + * stdin/stdout/stderr. + * + * The following functions are provided for getting data from the child + * process or sending data to the child's stdin (For more information, + * have a look at the documentation of each function): + * + * @li writeStdin() + * -- Transmit data to the child process' stdin. When all data was sent, the + * signal wroteStdin() is emitted. + * + * @li When data arrives at stdout or stderr, the signal receivedStdout() + * resp. receivedStderr() is emitted. + * + * @li You can shut down individual communication channels with + * closeStdin(), closeStdout(), and closeStderr(), resp. + * + * @author Christian Czezatke e9025461@student.tuwien.ac.at + * + **/ +class K3Process : public QObject +{ + Q_OBJECT + +public: + + /** + * Modes in which the communication channels can be opened. + * + * If communication for more than one channel is required, + * the values should be or'ed together, for example to get + * communication with stdout as well as with stdin, you would + * specify @p Stdin | @p Stdout + * + */ + enum CommunicationFlag { + NoCommunication = 0, /**< No communication with the process. */ + Stdin = 1, /**< Connect to write to the process with writeStdin(). */ + Stdout = 2, /**< Connect to read from the process' output. */ + Stderr = 4, /**< Connect to read from the process' stderr. */ + AllOutput = 6, /**< Connects to all output channels. */ + All = 7, /**< Connects to all channels. */ + NoRead = 8, /**< If specified with Stdout, no data is actually read from stdout, + * only the signal receivedStdout(int fd, int &len) is emitted. */ + CTtyOnly = NoRead, /**< Tells setUsePty() to create a PTY for the process + * and make it the process' controlling TTY, but does not + * redirect any I/O channel to the PTY. */ + MergedStderr = 16 /**< If specified with Stdout, the process' stderr will be + * redirected onto the same file handle as its stdout, i.e., + * all error output will be signalled with receivedStdout(). + * Don't specify Stderr if you specify MergedStderr. */ + }; + + Q_DECLARE_FLAGS(Communication, CommunicationFlag) + + /** + * Run-modes for a child process. + */ + enum RunMode { + /** + * The application does not receive notifications from the subprocess when + * it is finished or aborted. + */ + DontCare, + /** + * The application is notified when the subprocess dies. + */ + NotifyOnExit, + /** + * The application is suspended until the started process is finished. + */ + Block, + /** + * Same as NotifyOnExit, but the process is run in an own session, + * just like with DontCare. + */ + OwnGroup + }; + + /** + * Constructor + */ + explicit K3Process( QObject* parent=0L ); + + /** + *Destructor: + * + * If the process is running when the destructor for this class + * is called, the child process is killed with a SIGKILL, but + * only if the run mode is not of type @p DontCare. + * Processes started as @p DontCare keep running anyway. + */ + virtual ~K3Process(); + + /** + * Sets the executable and the command line argument list for this process. + * + * For example, doing an "ls -l /usr/local/bin" can be achieved by: + * \code + * K3Process p; + * ... + * p << "ls" << "-l" << "/usr/local/bin" + * \endcode + * + * @param arg the argument to add + * @return a reference to this K3Process + **/ + K3Process &operator<<(const QString& arg); + /** + * Similar to previous method, takes a char *, supposed to be in locale 8 bit already. + */ + K3Process &operator<<(const char * arg); + /** + * Similar to previous method, takes a QByteArray, supposed to be in locale 8 bit already. + * @param arg the argument to add + * @return a reference to this K3Process + */ + K3Process &operator<<(const QByteArray & arg); + + /** + * Sets the executable and the command line argument list for this process, + * in a single method call, or add a list of arguments. + * @param args the arguments to add + * @return a reference to this K3Process + **/ + K3Process &operator<<(const QStringList& args); + + /** + * Clear a command line argument list that has been set by using + * operator<<. + */ + void clearArguments(); + + /** + * Starts the process. + * For a detailed description of the + * various run modes and communication semantics, have a look at the + * general description of the K3Process class. Note that if you use + * setUsePty( Stdout | Stderr, \<bool\> ), you cannot use Stdout | Stderr + * here - instead, use Stdout only to receive the mixed output. + * + * The following problems could cause this function to + * return false: + * + * @li The process is already running. + * @li The command line argument list is empty. + * @li The the @p comm parameter is incompatible with the selected pty usage. + * @li The starting of the process failed (could not fork). + * @li The executable was not found. + * + * @param runmode The Run-mode for the process. + * @param comm Specifies which communication channels should be + * established to the child process (stdin/stdout/stderr). By default, + * no communication takes place and the respective communication + * signals will never get emitted. + * + * @return true on success, false on error + * (see above for error conditions) + **/ + virtual bool start(RunMode runmode = NotifyOnExit, + Communication comm = NoCommunication); + + /** + * Stop the process (by sending it a signal). + * + * @param signo The signal to send. The default is SIGTERM. + * @return true if the signal was delivered successfully. + */ + virtual bool kill(int signo = SIGTERM); + + /** + * Checks whether the process is running. + * @return true if the process is (still) considered to be running + */ + bool isRunning() const; + + /** Returns the process id of the process. + * + * If it is called after + * the process has exited, it returns the process id of the last + * child process that was created by this instance of K3Process. + * + * Calling it before any child process has been started by this + * K3Process instance causes pid() to return 0. + * @return the pid of the process or 0 if no process has been started yet. + **/ + pid_t pid() const; + + /** + * Suspend processing of data from stdout of the child process. + */ + void suspend(); + + /** + * Resume processing of data from stdout of the child process. + */ + void resume(); + + /** + * Suspend execution of the current thread until the child process dies + * or the timeout hits. This function is not recommended for programs + * with a GUI. + * @param timeout timeout in seconds. -1 means wait indefinitely. + * @return true if the process exited, false if the timeout hit. + */ + bool wait(int timeout = -1); + + /** + * Checks whether the process exited cleanly. + * + * @return true if the process has already finished and has exited + * "voluntarily", ie: it has not been killed by a signal. + */ + bool normalExit() const; + + /** + * Checks whether the process was killed by a signal. + * + * @return true if the process has already finished and has not exited + * "voluntarily", ie: it has been killed by a signal. + */ + bool signalled() const; + + /** + * Checks whether a killed process dumped core. + * + * @return true if signalled() returns true and the process + * dumped core. Note that on systems that don't define the + * WCOREDUMP macro, the return value is always false. + */ + bool coreDumped() const; + + /** + * Returns the exit status of the process. + * + * @return the exit status of the process. Note that this value + * is not valid if normalExit() returns false. + */ + int exitStatus() const; + + /** + * Returns the signal the process was killed by. + * + * @return the signal number that caused the process to exit. + * Note that this value is not valid if signalled() returns false. + */ + int exitSignal() const; + + /** + * Transmit data to the child process' stdin. + * + * This function may return false in the following cases: + * + * @li The process is not currently running. + * This implies that you cannot use this function in Block mode. + * + * @li Communication to stdin has not been requested in the start() call. + * + * @li Transmission of data to the child process by a previous call to + * writeStdin() is still in progress. + * + * Please note that the data is sent to the client asynchronously, + * so when this function returns, the data might not have been + * processed by the child process. + * That means that you must not free @p buffer or call writeStdin() + * again until either a wroteStdin() signal indicates that the + * data has been sent or a processExited() signal shows that + * the child process is no longer alive. + * + * If all the data has been sent to the client, the signal + * wroteStdin() will be emitted. + * + * This function does not work when the process is start()ed in Block mode. + * + * @param buffer the buffer to write + * @param buflen the length of the buffer + * @return false if an error has occurred + **/ + bool writeStdin(const char *buffer, int buflen); + + /** + * Shuts down the Stdin communication link. If no pty is used, this + * causes "EOF" to be indicated on the child's stdin file descriptor. + * + * @return false if no Stdin communication link exists (any more). + */ + bool closeStdin(); + + /** + * Shuts down the Stdout communication link. If no pty is used, any further + * attempts by the child to write to its stdout file descriptor will cause + * it to receive a SIGPIPE. + * + * @return false if no Stdout communication link exists (any more). + */ + bool closeStdout(); + + /** + * Shuts down the Stderr communication link. If no pty is used, any further + * attempts by the child to write to its stderr file descriptor will cause + * it to receive a SIGPIPE. + * + * @return false if no Stderr communication link exists (any more). + */ + bool closeStderr(); + + /** + * Deletes the optional utmp entry and closes the pty. + * + * Make sure to shut down any communication links that are using the pty + * before calling this function. + * + * @return false if the pty is not open (any more). + */ + bool closePty(); + + /** + * @brief Close stdin, stdout, stderr and the pty + * + * This is the same that calling all close* functions in a row: + * @see closeStdin, @see closeStdout, @see closeStderr and @see closePty + */ + void closeAll(); + + /** + * Lets you see what your arguments are for debugging. + * @return the list of arguments + */ + const QList<QByteArray> &args() /* const */ { return arguments; } + + /** + * Controls whether the started process should drop any + * setuid/setgid privileges or whether it should keep them. + * Note that this function is mostly a dummy, as the KDE libraries + * currently refuse to run with setuid/setgid privileges. + * + * The default is false: drop privileges + * @param keepPrivileges true to keep the privileges + */ + void setRunPrivileged(bool keepPrivileges); + + /** + * Returns whether the started process will drop any + * setuid/setgid privileges or whether it will keep them. + * @return true if the process runs privileged + */ + bool runPrivileged() const; + + /** + * Adds the variable @p name to the process' environment. + * This function must be called before starting the process. + * @param name the name of the environment variable + * @param value the new value for the environment variable + */ + void setEnvironment(const QString &name, const QString &value); + + /** + * Changes the current working directory (CWD) of the process + * to be started. + * This function must be called before starting the process. + * @param dir the new directory + */ + void setWorkingDirectory(const QString &dir); + + /** + * Specify whether to start the command via a shell or directly. + * The default is to start the command directly. + * If @p useShell is true @p shell will be used as shell, or + * if shell is empty, /bin/sh will be used. + * + * When using a shell, the caller should make sure that all filenames etc. + * are properly quoted when passed as argument. + * @see quote() + * @param useShell true if the command should be started via a shell + * @param shell the path to the shell that will execute the process, or + * 0 to use /bin/sh. Use getenv("SHELL") to use the user's + * default shell, but note that doing so is usually a bad idea + * for shell compatibility reasons. + */ + void setUseShell(bool useShell, const char *shell = 0); + + /** + * This function can be used to quote an argument string such that + * the shell processes it properly. This is e. g. necessary for + * user-provided file names which may contain spaces or quotes. + * It also prevents expansion of wild cards and environment variables. + * @param arg the argument to quote + * @return the quoted argument + */ + static QString quote(const QString &arg); + + /** + * Detaches K3Process from child process. All communication is closed. + * No exit notification is emitted any more for the child process. + * Deleting the K3Process will no longer kill the child process. + * Note that the current process remains the parent process of the + * child process. + */ + void detach(); + + /** + * Specify whether to create a pty (pseudo-terminal) for running the + * command. + * This function should be called before starting the process. + * + * @param comm for which stdio handles to use a pty. Note that it is not + * allowed to specify Stdout and Stderr at the same time both here and to + * start (there is only one pty, so they cannot be distinguished). + * @param addUtmp true if a utmp entry should be created for the pty + */ + void setUsePty(Communication comm, bool addUtmp); + + /** + * Obtains the pty object used by this process. The return value is + * valid only after setUsePty() was used with a non-zero argument. + * The pty is open only while the process is running. + * @return a pointer to the pty object + */ + KPty *pty() const; + + /** + * More or less intuitive constants for use with setPriority(). + */ + enum { PrioLowest = 20, PrioLow = 10, PrioLower = 5, PrioNormal = 0, + PrioHigher = -5, PrioHigh = -10, PrioHighest = -19 }; + + /** + * Sets the scheduling priority of the process. + * @param prio the new priority in the range -20 (high) to 19 (low). + * @return false on error; see setpriority(2) for possible reasons. + */ + bool setPriority(int prio); + +Q_SIGNALS: + /** + * Emitted after the process has terminated when + * the process was run in the @p NotifyOnExit (==default option to + * start() ) or the Block mode. + * @param proc a pointer to the process that has exited + **/ + void processExited(K3Process *proc); + + + /** + * Emitted, when output from the child process has + * been received on stdout. + * + * To actually get this signal, the Stdout communication link + * has to be turned on in start(). + * + * @param proc a pointer to the process that has received the output + * @param buffer The data received. + * @param buflen The number of bytes that are available. + * + * You should copy the information contained in @p buffer to your private + * data structures before returning from the slot. + * Example: + * \code + * QString myBuf = QLatin1String(buffer, buflen); + * \endcode + **/ + void receivedStdout(K3Process *proc, char *buffer, int buflen); + + /** + * Emitted when output from the child process has + * been received on stdout. + * + * To actually get this signal, the Stdout communication link + * has to be turned on in start() and the + * NoRead flag must have been passed. + * + * You will need to explicitly call resume() after your call to start() + * to begin processing data from the child process' stdout. This is + * to ensure that this signal is not emitted when no one is connected + * to it, otherwise this signal will not be emitted. + * + * The data still has to be read from file descriptor @p fd. + * @param fd the file descriptor that provides the data + * @param len the number of bytes that have been read from @p fd must + * be written here + **/ + void receivedStdout(int fd, int &len); // KDE4: change, broken API + + + /** + * Emitted, when output from the child process has + * been received on stderr. + * + * To actually get this signal, the Stderr communication link + * has to be turned on in start(). + * + * You should copy the information contained in @p buffer to your private + * data structures before returning from the slot. + * + * @param proc a pointer to the process that has received the data + * @param buffer The data received. + * @param buflen The number of bytes that are available. + **/ + void receivedStderr(K3Process *proc, char *buffer, int buflen); + + /** + * Emitted after all the data that has been + * specified by a prior call to writeStdin() has actually been + * written to the child process. + * @param proc a pointer to the process + **/ + void wroteStdin(K3Process *proc); + + +protected Q_SLOTS: + + /** + * This slot gets activated when data from the child's stdout arrives. + * It usually calls childOutput(). + * @param fdno the file descriptor for the output + */ + void slotChildOutput(int fdno); + + /** + * This slot gets activated when data from the child's stderr arrives. + * It usually calls childError(). + * @param fdno the file descriptor for the output + */ + void slotChildError(int fdno); + + /** + * Called when another bulk of data can be sent to the child's + * stdin. If there is no more data to be sent to stdin currently + * available, this function must disable the QSocketNotifier innot. + * @param dummy ignore this argument + */ + void slotSendData(int dummy); // KDE 4: remove dummy + +protected: + + /** + * Sets up the environment according to the data passed via + * setEnvironment() + */ + void setupEnvironment(); + + /** + * The list of the process' command line arguments. The first entry + * in this list is the executable itself. + */ + QList<QByteArray> arguments; + /** + * How to run the process (Block, NotifyOnExit, DontCare). You should + * not modify this data member directly from derived classes. + */ + RunMode run_mode; + /** + * true if the process is currently running. You should not + * modify this data member directly from derived classes. Please use + * isRunning() for reading the value of this data member since it + * will probably be made private in later versions of K3Process. + */ + bool runs; + + /** + * The PID of the currently running process. + * You should not modify this data member in derived classes. + * Please use pid() instead of directly accessing this + * member since it will probably be made private in + * later versions of K3Process. + */ + pid_t pid_; + + /** + * The process' exit status as returned by waitpid(). You should not + * modify the value of this data member from derived classes. You should + * rather use exitStatus() than accessing this data member directly + * since it will probably be made private in further versions of + * K3Process. + */ + int status; + + + /** + * If false, the child process' effective uid & gid will be reset to the + * real values. + * @see setRunPrivileged() + */ + bool keepPrivs; + + /** + * This function is called from start() right before a fork() takes + * place. According to the @p comm parameter this function has to initialize + * the in, out and err data members of K3Process. + * + * This function should return 1 if setting the needed communication channels + * was successful. + * + * The default implementation is to create UNIX STREAM sockets for the + * communication, but you could reimplement this function to establish a + * TCP/IP communication for network communication, for example. + */ + virtual int setupCommunication(Communication comm); + + /** + * Called right after a (successful) fork() on the parent side. This function + * will usually do some communications cleanup, like closing in[0], + * out[1] and out[1]. + * + * Furthermore, it must also create the QSocketNotifiers innot, + * outnot and errnot and connect their Qt signals to the respective + * K3Process slots. + * + * For a more detailed explanation, it is best to have a look at the default + * implementation in kprocess.cpp. + */ + virtual int commSetupDoneP(); + + /** + * Called right after a (successful) fork(), but before an exec() on the child + * process' side. It usually duplicates the in[0], out[1] and + * err[1] file handles to the respective standard I/O handles. + */ + virtual int commSetupDoneC(); + + + /** + * Immediately called after a successfully started process in NotifyOnExit + * mode has exited. This function normally calls commClose() + * and emits the processExited() signal. + * @param state the exit code of the process as returned by waitpid() + */ + virtual void processHasExited(int state); + + /** + * Cleans up the communication links to the child after it has exited. + * This function should act upon the values of pid() and runs. + * See the kprocess.cpp source for details. + * @li If pid() returns zero, the communication links should be closed + * only. + * @li if pid() returns non-zero and runs is false, all data + * immediately available from the communication links should be processed + * before closing them. + * @li if pid() returns non-zero and runs is true, the communication + * links should be monitored for data until the file handle returned by + * K3ProcessController::theKProcessController->notifierFd() becomes ready + * for reading - when it triggers, runs should be reset to false, and + * the function should be immediately left without closing anything. + * + * The previous semantics of this function are forward-compatible, but should + * be avoided, as they are prone to race conditions and can cause K3Process + * (and thus the whole program) to lock up under certain circumstances. At the + * end the function closes the communication links in any case. Additionally + * @li if runs is true, the communication links are monitored for data + * until all of them have returned EOF. Note that if any system function is + * interrupted (errno == EINTR) the polling loop should be aborted. + * @li if runs is false, all data immediately available from the + * communication links is processed. + */ + virtual void commClose(); + + /* KDE 4 - commClose will be changed to perform cleanup only in all cases * + * If @p notfd is -1, all data immediately available from the + * communication links should be processed. + * If @p notfd is not -1, the communication links should be monitored + * for data until the file handle @p notfd becomes ready for reading. + */ +// virtual void commDrain(int notfd); + + /** + * Specify the actual executable that should be started (first argument to execve) + * Normally the the first argument is the executable but you can + * override that with this function. + */ + void setBinaryExecutable(const char *filename); + + /** + * The socket descriptors for stdout. + */ + int out[2]; + /** + * The socket descriptors for stdin. + */ + int in[2]; + /** + * The socket descriptors for stderr. + */ + int err[2]; + + /** + * The socket notifier for in[1]. + */ + QSocketNotifier *innot; + /** + * The socket notifier for out[0]. + */ + QSocketNotifier *outnot; + /** + * The socket notifier for err[0]. + */ + QSocketNotifier *errnot; + + /** + * Lists the communication links that are activated for the child + * process. Should not be modified from derived classes. + */ + Communication communication; + + /** + * Called by slotChildOutput() this function copies data arriving from + * the child process' stdout to the respective buffer and emits the signal + * receivedStdout(). + */ + int childOutput(int fdno); + + /** + * Called by slotChildError() this function copies data arriving from + * the child process' stderr to the respective buffer and emits the signal + * receivedStderr(). + */ + int childError(int fdno); + + /** + * The buffer holding the data that has to be sent to the child + */ + const char *input_data; + /** + * The number of bytes already transmitted + */ + int input_sent; + /** + * The total length of input_data + */ + int input_total; + + /** + * K3ProcessController is a friend of K3Process because it has to have + * access to various data members. + */ + friend class K3ProcessController; + +private: + K3ProcessPrivate* const d; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(K3Process::Communication) + +class K3ShellProcessPrivate; + +/** +* @obsolete +* +* Use K3Process and K3Process::setUseShell(true) instead. +* +* @short A class derived from K3Process to start child +* processes through a shell. +* @author Christian Czezatke <e9025461@student.tuwien.ac.at> +*/ +class K3ShellProcess : public K3Process +{ + Q_OBJECT + +public: + + /** + * Constructor + * + * If no shellname is specified, the user's default shell is used. + */ + explicit K3ShellProcess(const char *shellname=0); + + /** + * Destructor. + */ + ~K3ShellProcess(); + + virtual bool start(RunMode runmode = NotifyOnExit, + Communication comm = NoCommunication); + + static QString quote(const QString &arg); + +private: + K3ShellProcessPrivate* const d; +}; + + + +#endif +
new file mode 100644 --- /dev/null +++ b/gui//k3processcontroller.cpp @@ -0,0 +1,334 @@ +/* This file is part of the KDE libraries + Copyright (C) 1997 Christian Czezakte (e9025461@student.tuwien.ac.at) + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "k3processcontroller.h" +#include "k3process.h" + +//#include <config.h> + +#include <sys/time.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> + +#include <QtCore/QSocketNotifier> + +class K3ProcessController::Private +{ +public: + Private() + : needcheck( false ), + notifier( 0 ) + { + } + + ~Private() + { + delete notifier; + } + + int fd[2]; + bool needcheck; + QSocketNotifier *notifier; + QList<K3Process*> kProcessList; + QList<int> unixProcessList; + static struct sigaction oldChildHandlerData; + static bool handlerSet; + static int refCount; + static K3ProcessController* instance; +}; + +K3ProcessController *K3ProcessController::Private::instance = 0; +int K3ProcessController::Private::refCount = 0; + +void K3ProcessController::ref() +{ + if ( !Private::refCount ) { + Private::instance = new K3ProcessController; + setupHandlers(); + } + Private::refCount++; +} + +void K3ProcessController::deref() +{ + Private::refCount--; + if( !Private::refCount ) { + resetHandlers(); + delete Private::instance; + Private::instance = 0; + } +} + +K3ProcessController* K3ProcessController::instance() +{ + /* + * there were no safety guards in previous revisions, is that ok? + if ( !Private::instance ) { + ref(); + } + */ + + return Private::instance; +} + +K3ProcessController::K3ProcessController() + : d( new Private ) +{ + if( pipe( d->fd ) ) + { + perror( "pipe" ); + abort(); + } + + fcntl( d->fd[0], F_SETFL, O_NONBLOCK ); // in case slotDoHousekeeping is called without polling first + fcntl( d->fd[1], F_SETFL, O_NONBLOCK ); // in case it fills up + fcntl( d->fd[0], F_SETFD, FD_CLOEXEC ); + fcntl( d->fd[1], F_SETFD, FD_CLOEXEC ); + + d->notifier = new QSocketNotifier( d->fd[0], QSocketNotifier::Read ); + d->notifier->setEnabled( true ); + QObject::connect( d->notifier, SIGNAL(activated(int)), + SLOT(slotDoHousekeeping())); +} + +K3ProcessController::~K3ProcessController() +{ +#ifndef Q_OS_MAC +/* not sure why, but this is causing lockups */ + close( d->fd[0] ); + close( d->fd[1] ); +#else +#warning FIXME: why does close() freeze up destruction? +#endif + + delete d; +} + + +extern "C" { +static void theReaper( int num ) +{ + K3ProcessController::theSigCHLDHandler( num ); +} +} + +#ifdef Q_OS_UNIX +struct sigaction K3ProcessController::Private::oldChildHandlerData; +#endif +bool K3ProcessController::Private::handlerSet = false; + +void K3ProcessController::setupHandlers() +{ + if( Private::handlerSet ) + return; + Private::handlerSet = true; + +#ifdef Q_OS_UNIX + struct sigaction act; + sigemptyset( &act.sa_mask ); + + act.sa_handler = SIG_IGN; + act.sa_flags = 0; + sigaction( SIGPIPE, &act, 0L ); + + act.sa_handler = theReaper; + act.sa_flags = SA_NOCLDSTOP; + // CC: take care of SunOS which automatically restarts interrupted system + // calls (and thus does not have SA_RESTART) +#ifdef SA_RESTART + act.sa_flags |= SA_RESTART; +#endif + sigaction( SIGCHLD, &act, &Private::oldChildHandlerData ); + + sigaddset( &act.sa_mask, SIGCHLD ); + // Make sure we don't block this signal. gdb tends to do that :-( + sigprocmask( SIG_UNBLOCK, &act.sa_mask, 0 ); +#else + //TODO: win32 +#endif +} + +void K3ProcessController::resetHandlers() +{ + if( !Private::handlerSet ) + return; + Private::handlerSet = false; + +#ifdef Q_OS_UNIX + sigset_t mask, omask; + sigemptyset( &mask ); + sigaddset( &mask, SIGCHLD ); + sigprocmask( SIG_BLOCK, &mask, &omask ); + + struct sigaction act; + sigaction( SIGCHLD, &Private::oldChildHandlerData, &act ); + if (act.sa_handler != theReaper) { + sigaction( SIGCHLD, &act, 0 ); + Private::handlerSet = true; + } + + sigprocmask( SIG_SETMASK, &omask, 0 ); +#else + //TODO: win32 +#endif + // there should be no problem with SIGPIPE staying SIG_IGN +} + +// the pipe is needed to sync the child reaping with our event processing, +// as otherwise there are race conditions, locking requirements, and things +// generally get harder +void K3ProcessController::theSigCHLDHandler( int arg ) +{ + int saved_errno = errno; + + char dummy = 0; + ::write( instance()->d->fd[1], &dummy, 1 ); + +#ifdef Q_OS_UNIX + if ( Private::oldChildHandlerData.sa_handler != SIG_IGN && + Private::oldChildHandlerData.sa_handler != SIG_DFL ) { + Private::oldChildHandlerData.sa_handler( arg ); // call the old handler + } +#else + //TODO: win32 +#endif + + errno = saved_errno; +} + +int K3ProcessController::notifierFd() const +{ + return d->fd[0]; +} + +void K3ProcessController::unscheduleCheck() +{ + char dummy[16]; // somewhat bigger - just in case several have queued up + if( ::read( d->fd[0], dummy, sizeof(dummy) ) > 0 ) + d->needcheck = true; +} + +void +K3ProcessController::rescheduleCheck() +{ + if( d->needcheck ) + { + d->needcheck = false; + char dummy = 0; + ::write( d->fd[1], &dummy, 1 ); + } +} + +void K3ProcessController::slotDoHousekeeping() +{ + char dummy[16]; // somewhat bigger - just in case several have queued up + ::read( d->fd[0], dummy, sizeof(dummy) ); + + int status; + again: + QList<K3Process*>::iterator it( d->kProcessList.begin() ); + QList<K3Process*>::iterator eit( d->kProcessList.end() ); + while( it != eit ) + { + K3Process *prc = *it; + if( prc->runs && waitpid( prc->pid_, &status, WNOHANG ) > 0 ) + { + prc->processHasExited( status ); + // the callback can nuke the whole process list and even 'this' + if (!instance()) + return; + goto again; + } + ++it; + } + QList<int>::iterator uit( d->unixProcessList.begin() ); + QList<int>::iterator ueit( d->unixProcessList.end() ); + while( uit != ueit ) + { + if( waitpid( *uit, 0, WNOHANG ) > 0 ) + { + uit = d->unixProcessList.erase( uit ); + deref(); // counterpart to addProcess, can invalidate 'this' + } else + ++uit; + } +} + +bool K3ProcessController::waitForProcessExit( int timeout ) +{ +#ifdef Q_OS_UNIX + for(;;) + { + struct timeval tv, *tvp; + if (timeout < 0) + tvp = 0; + else + { + tv.tv_sec = timeout; + tv.tv_usec = 0; + tvp = &tv; + } + + fd_set fds; + FD_ZERO( &fds ); + FD_SET( d->fd[0], &fds ); + + switch( select( d->fd[0]+1, &fds, 0, 0, tvp ) ) + { + case -1: + if( errno == EINTR ) + continue; + // fall through; should never happen + case 0: + return false; + default: + slotDoHousekeeping(); + return true; + } + } +#else + //TODO: win32 + return false; +#endif +} + +void K3ProcessController::addKProcess( K3Process* p ) +{ + d->kProcessList.append( p ); +} + +void K3ProcessController::removeKProcess( K3Process* p ) +{ + d->kProcessList.removeAll( p ); +} + +void K3ProcessController::addProcess( int pid ) +{ + d->unixProcessList.append( pid ); + ref(); // make sure we stay around when the K3Process goes away +} + +//#include "moc_k3processcontroller.cpp"
new file mode 100644 --- /dev/null +++ b/gui//k3processcontroller.h @@ -0,0 +1,137 @@ +/* This file is part of the KDE libraries + Copyright (C) 1997 Christian Czezakte (e9025461@student.tuwien.ac.at) + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef K3PROCCTRL_H +#define K3PROCCTRL_H + +#include <QtCore/QList> +#include <k3process.h> + + +/** + * @short Used internally by K3Process + * @internal + * @author Christian Czezatke <e9025461@student.tuwien.ac.at> + * + * A class for internal use by K3Process only. -- Exactly one instance + * of this class is created by KApplication. + * + * This class takes care of the actual (UN*X) signal handling. + */ +class K3ProcessController : public QObject +{ + Q_OBJECT + +public: + /** + * Create an instance if none exists yet. + * Called by KApplication::KApplication() + */ + static void ref(); + + /** + * Destroy the instance if one exists and it is not referenced any more. + * Called by KApplication::~KApplication() + */ + static void deref(); + + /** + * Only a single instance of this class is allowed at a time. + * This method provides access to that instance. + */ + static K3ProcessController *instance(); + + /** + * Automatically called upon SIGCHLD. Never call it directly. + * If your application (or some library it uses) redirects SIGCHLD, + * the new signal handler (and only it) should call the old handler + * returned by sigaction(). + * @internal + */ + static void theSigCHLDHandler(int signal); // KDE4: private + + /** + * Wait for any process to exit and handle their exit without + * starting an event loop. + * This function may cause K3Process to emit any of its signals. + * + * @param timeout the timeout in seconds. -1 means no timeout. + * @return true if a process exited, false + * if no process exited within @p timeout seconds. + */ + bool waitForProcessExit(int timeout); + + /** + * Call this function to defer processing of the data that became available + * on notifierFd(). + */ + void unscheduleCheck(); + + /** + * This function @em must be called at some point after calling + * unscheduleCheck(). + */ + void rescheduleCheck(); + + /* + * Obtain the file descriptor K3ProcessController uses to get notified + * about process exits. select() or poll() on it if you create a custom + * event loop that needs to act upon SIGCHLD. + * @return the file descriptor of the reading end of the notification pipe + */ + int notifierFd() const; + + /** + * @internal + */ + void addKProcess( K3Process* ); + /** + * @internal + */ + void removeKProcess( K3Process* ); + /** + * @internal + */ + void addProcess( int pid ); + +private Q_SLOTS: + void slotDoHousekeeping(); + +private: + friend class I_just_love_gcc; + + static void setupHandlers(); + static void resetHandlers(); + + // Disallow instantiation + K3ProcessController(); + ~K3ProcessController(); + + // Disallow assignment and copy-construction + K3ProcessController( const K3ProcessController& ); + K3ProcessController& operator= ( const K3ProcessController& ); + + class Private; + Private * const d; +}; + +#endif +
new file mode 100644 --- /dev/null +++ b/gui//kb-layouts/CVS/Entries @@ -0,0 +1,4 @@ +/default.keytab/1.1.1.1/Sat May 10 21:27:57 2008// +/linux.keytab/1.1.1.1/Sat May 10 21:27:57 2008// +/vt420pc.keytab/1.1.1.1/Sat May 10 21:27:57 2008// +D
new file mode 100644 --- /dev/null +++ b/gui//kb-layouts/CVS/Repository @@ -0,0 +1,1 @@ +qtermwidget/lib/kb-layouts
new file mode 100644 --- /dev/null +++ b/gui//kb-layouts/CVS/Root @@ -0,0 +1,1 @@ +:ext:e_k@qtermwidget.cvs.sourceforge.net:/cvsroot/qtermwidget
new file mode 100644 --- /dev/null +++ b/gui//kb-layouts/default.keytab @@ -0,0 +1,133 @@ +# [README.default.Keytab] Buildin Keyboard Table +# +# To customize your keyboard, copy this file to something +# ending with .keytab and change it to meet you needs. +# Please read the README.KeyTab and the README.keyboard +# in this case. +# +# -------------------------------------------------------------- + +keyboard "Default (XFree 4)" + +# -------------------------------------------------------------- +# +# Note that this particular table is a "risc" version made to +# ease customization without bothering with obsolete details. +# See VT100.keytab for the more hairy stuff. +# +# -------------------------------------------------------------- + +# common keys + +key Escape : "\E" + +key Tab -Shift : "\t" +key Tab +Shift+Ansi : "\E[Z" +key Tab +Shift-Ansi : "\t" +key Backtab +Ansi : "\E[Z" +key Backtab -Ansi : "\t" + +key Return-Shift-NewLine : "\r" +key Return-Shift+NewLine : "\r\n" + +key Return+Shift : "\EOM" + +# Backspace and Delete codes are preserving CTRL-H. + +key Backspace : "\x7f" + +# Arrow keys in VT52 mode +# shift up/down are reserved for scrolling. +# shift left/right are reserved for switching between tabs (this is hardcoded). + +key Up -Shift-Ansi : "\EA" +key Down -Shift-Ansi : "\EB" +key Right-Shift-Ansi : "\EC" +key Left -Shift-Ansi : "\ED" + +# Arrow keys in ANSI mode with Application - and Normal Cursor Mode) + +key Up -Shift-AnyMod+Ansi+AppCuKeys : "\EOA" +key Down -Shift-AnyMod+Ansi+AppCuKeys : "\EOB" +key Right -Shift-AnyMod+Ansi+AppCuKeys : "\EOC" +key Left -Shift-AnyMod+Ansi+AppCuKeys : "\EOD" + +key Up -Shift-AnyMod+Ansi-AppCuKeys : "\E[A" +key Down -Shift-AnyMod+Ansi-AppCuKeys : "\E[B" +key Right -Shift-AnyMod+Ansi-AppCuKeys : "\E[C" +key Left -Shift-AnyMod+Ansi-AppCuKeys : "\E[D" + +key Up -Shift+AnyMod+Ansi : "\E[1;*A" +key Down -Shift+AnyMod+Ansi : "\E[1;*B" +key Right -Shift+AnyMod+Ansi : "\E[1;*C" +key Left -Shift+AnyMod+Ansi : "\E[1;*D" + +# other grey PC keys + +key Enter+NewLine : "\r\n" +key Enter-NewLine : "\r" + +key Home -AnyMod -AppCuKeys : "\E[H" +key End -AnyMod -AppCuKeys : "\E[F" +key Home -AnyMod +AppCuKeys : "\EOH" +key End -AnyMod +AppCuKeys : "\EOF" +key Home +AnyMod : "\E[1;*H" +key End +AnyMod : "\E[1;*F" + +key Insert -AnyMod : "\E[2~" +key Delete -AnyMod : "\E[3~" +key Insert +AnyMod : "\E[2;*~" +key Delete +AnyMod : "\E[3;*~" + +key Prior -Shift-AnyMod : "\E[5~" +key Next -Shift-AnyMod : "\E[6~" +key Prior -Shift+AnyMod : "\E[5;*~" +key Next -Shift+AnyMod : "\E[6;*~" + +# Function keys +key F1 -AnyMod : "\EOP" +key F2 -AnyMod : "\EOQ" +key F3 -AnyMod : "\EOR" +key F4 -AnyMod : "\EOS" +key F5 -AnyMod : "\E[15~" +key F6 -AnyMod : "\E[17~" +key F7 -AnyMod : "\E[18~" +key F8 -AnyMod : "\E[19~" +key F9 -AnyMod : "\E[20~" +key F10 -AnyMod : "\E[21~" +key F11 -AnyMod : "\E[23~" +key F12 -AnyMod : "\E[24~" + +key F1 +AnyMod : "\EO*P" +key F2 +AnyMod : "\EO*Q" +key F3 +AnyMod : "\EO*R" +key F4 +AnyMod : "\EO*S" +key F5 +AnyMod : "\E[15;*~" +key F6 +AnyMod : "\E[17;*~" +key F7 +AnyMod : "\E[18;*~" +key F8 +AnyMod : "\E[19;*~" +key F9 +AnyMod : "\E[20;*~" +key F10 +AnyMod : "\E[21;*~" +key F11 +AnyMod : "\E[23;*~" +key F12 +AnyMod : "\E[24;*~" + +# Work around dead keys + +key Space +Control : "\x00" + +# Some keys are used by konsole to cause operations. +# The scroll* operations refer to the history buffer. + +key Up +Shift-AppScreen : scrollLineUp +key Prior +Shift-AppScreen : scrollPageUp +key Down +Shift-AppScreen : scrollLineDown +key Next +Shift-AppScreen : scrollPageDown + +#key Up +Shift : scrollLineUp +#key Prior +Shift : scrollPageUp +#key Down +Shift : scrollLineDown +#key Next +Shift : scrollPageDown + +key ScrollLock : scrollLock + +# keypad characters are not offered differently by Qt.
new file mode 100644 --- /dev/null +++ b/gui//kb-layouts/linux.keytab @@ -0,0 +1,133 @@ +# [linux.keytab] Konsole Keyboard Table (Linux console keys) +# +# -------------------------------------------------------------- + +# NOT TESTED, MAY NEED SOME CLEANUPS +keyboard "Linux console" + +# -------------------------------------------------------------- +# +# This configuration table allows to customize the +# meaning of the keys. +# +# The syntax is that each entry has the form : +# +# "key" Keyname { ("+"|"-") Modename } ":" (String|Operation) +# +# Keynames are those defined in <qnamespace.h> with the +# "Qt::Key_" removed. (We'd better insert the list here) +# +# Mode names are : +# +# - Shift +# - Alt +# - Control +# +# The VT100 emulation has two modes that can affect the +# sequences emitted by certain keys. These modes are +# under control of the client program. +# +# - Newline : effects Return and Enter key. +# - Application : effects Up and Down key. +# +# - Ansi : effects Up and Down key (This is for VT52, really). +# +# Operations are +# +# - scrollUpLine +# - scrollUpPage +# - scrollDownLine +# - scrollDownPage +# +# - emitSelection +# +# If the key is not found here, the text of the +# key event as provided by QT is emitted, possibly +# preceeded by ESC if the Alt key is pressed. +# +# -------------------------------------------------------------- + +key Escape : "\E" +key Tab : "\t" + +# VT100 can add an extra \n after return. +# The NewLine mode is set by an escape sequence. + +key Return-NewLine : "\r" +key Return+NewLine : "\r\n" + +# Some desperately try to save the ^H. + +key Backspace : "\x7f" +key Delete : "\E[3~" + +# These codes are for the VT52 mode of VT100 +# The Ansi mode (i.e. VT100 mode) is set by +# an escape sequence + +key Up -Shift-Ansi : "\EA" +key Down -Shift-Ansi : "\EB" +key Right-Shift-Ansi : "\EC" +key Left -Shift-Ansi : "\ED" + +# VT100 emits a mode bit together +# with the arrow keys.The AppCuKeys +# mode is set by an escape sequence. + +key Up -Shift+Ansi+AppCuKeys : "\EOA" +key Down -Shift+Ansi+AppCuKeys : "\EOB" +key Right-Shift+Ansi+AppCuKeys : "\EOC" +key Left -Shift+Ansi+AppCuKeys : "\EOD" + +key Up -Shift+Ansi-AppCuKeys : "\E[A" +key Down -Shift+Ansi-AppCuKeys : "\E[B" +key Right-Shift+Ansi-AppCuKeys : "\E[C" +key Left -Shift+Ansi-AppCuKeys : "\E[D" + +# linux functions keys F1-F5 differ from xterm + +key F1 : "\E[[A" +key F2 : "\E[[B" +key F3 : "\E[[C" +key F4 : "\E[[D" +key F5 : "\E[[E" + +key F6 : "\E[17~" +key F7 : "\E[18~" +key F8 : "\E[19~" +key F9 : "\E[20~" +key F10 : "\E[21~" +key F11 : "\E[23~" +key F12 : "\E[24~" + +key Home : "\E[1~" +key End : "\E[4~" + +key Prior -Shift : "\E[5~" +key Next -Shift : "\E[6~" +key Insert-Shift : "\E[2~" + +# Keypad-Enter. See comment on Return above. + +key Enter+NewLine : "\r\n" +key Enter-NewLine : "\r" + +key Space +Control : "\x00" + +# some of keys are used by konsole. + +key Up +Shift : scrollLineUp +key Prior +Shift : scrollPageUp +key Down +Shift : scrollLineDown +key Next +Shift : scrollPageDown + +key ScrollLock : scrollLock + +#---------------------------------------------------------- + +# keypad characters as offered by Qt +# cannot be recognized as such. + +#---------------------------------------------------------- + +# Following other strings as emitted by konsole.
new file mode 100644 --- /dev/null +++ b/gui//kb-layouts/vt420pc.keytab @@ -0,0 +1,163 @@ +# [vt420pc.keytab] Konsole Keyboard Table (VT420pc keys) +# adapted by ferdinand gassauer f.gassauer@aon.at +# Nov 2000 +# +################################################################ +# +# The escape sequences emmited by the +# keys Shift+F1 to Shift+F12 might not fit your needs +# +################# IMPORTANT NOTICE ############################# +# the key bindings (Kcontrol -> look and feel -> keybindgs) +# overrule the settings in this file. The key bindings might be +# changed by the user WITHOUT notification of the maintainer of +# the keytab file. Konsole will not work as expected by +# the maintainer of the keytab file. +################################################################ +# +# -------------------------------------------------------------- + +keyboard "DEC VT420 Terminal" + +# -------------------------------------------------------------- +# +# This configuration table allows to customize the +# meaning of the keys. +# +# The syntax is that each entry has the form : +# +# "key" Keyname { ("+"|"-") Modename } ":" (String|Operation) +# +# Keynames are those defined in <qnamespace.h> with the +# "Qt::Key_" removed. (We'd better insert the list here) +# +# Mode names are : +# +# - Shift +# - Alt +# - Control +# +# The VT100 emulation has two modes that can affect the +# sequences emitted by certain keys. These modes are +# under control of the client program. +# +# - Newline : effects Return and Enter key. +# - Application : effects Up and Down key. +# +# - Ansi : effects Up and Down key (This is for VT52, really). +# +# Operations are +# +# - scrollUpLine +# - scrollUpPage +# - scrollDownLine +# - scrollDownPage +# +# - emitSelection +# +# If the key is not found here, the text of the +# key event as provided by QT is emitted, possibly +# preceeded by ESC if the Alt key is pressed. +# +# -------------------------------------------------------------- + +key Escape : "\E" +key Tab : "\t" +key Backtab: "\E[Z" + +# VT100 can add an extra \n after return. +# The NewLine mode is set by an escape sequence. + +key Return-NewLine : "\r" +key Return+NewLine : "\r\n" + +# Some desperately try to save the ^H. +# may be not everyone wants this + +key Backspace : "\x08" # Control H +key Delete : "\x7f" + +# These codes are for the VT420pc +# The Ansi mode (i.e. VT100 mode) is set by +# an escape sequence + +key Up -Shift-Ansi : "\EA" +key Down -Shift-Ansi : "\EB" +key Right-Shift-Ansi : "\EC" +key Left -Shift-Ansi : "\ED" + +# VT100 emits a mode bit together +# with the arrow keys.The AppCuKeys +# mode is set by an escape sequence. + +key Up -Shift+Ansi+AppCuKeys : "\EOA" +key Down -Shift+Ansi+AppCuKeys : "\EOB" +key Right-Shift+Ansi+AppCuKeys : "\EOC" +key Left -Shift+Ansi+AppCuKeys : "\EOD" + +key Up -Shift+Ansi-AppCuKeys : "\E[A" +key Down -Shift+Ansi-AppCuKeys : "\E[B" +key Right-Shift+Ansi-AppCuKeys : "\E[C" +key Left -Shift+Ansi-AppCuKeys : "\E[D" + +# function keys + +key F1 -Shift : "\E[11~" +key F2 -Shift : "\E[12~" +key F3 -Shift : "\E[13~" +key F4 -Shift : "\E[14~" +key F5 -Shift : "\E[15~" +key F6 -Shift : "\E[17~" +key F7 -Shift : "\E[18~" +key F8 -Shift : "\E[19~" +key F9 -Shift : "\E[20~" +key F10-Shift : "\E[21~" +key F11-Shift : "\E[23~" +key F12-Shift : "\E[24~" +# +# Shift F1-F12 +# +key F1 +Shift : "\E[11;2~" +key F2 +Shift : "\E[12;2~" +key F3 +Shift : "\E[13;2~" +key F4 +Shift : "\E[14;2~" +key F5 +Shift : "\E[15;2~" +key F6 +Shift : "\E[17;2~" +key F7 +Shift : "\E[18;2~" +key F8 +Shift : "\E[19;2~" +key F9 +Shift : "\E[20;2~" +key F10+Shift : "\E[21;2~" +key F11+Shift : "\E[23;2~" +key F12+Shift : "\E[24;2~" + +key Home : "\E[H" +key End : "\E[F" + +key Prior -Shift : "\E[5~" +key Next -Shift : "\E[6~" +key Insert-Shift : "\E[2~" + +# Keypad-Enter. See comment on Return above. + +key Enter+NewLine : "\r\n" +key Enter-NewLine : "\r" + +key Space +Control : "\x00" + +# some of keys are used by konsole. + +key Up +Shift : scrollLineUp +key Prior +Shift : scrollPageUp +key Down +Shift : scrollLineDown +key Next +Shift : scrollPageDown + +key ScrollLock : scrollLock + +#---------------------------------------------------------- + +# keypad characters as offered by Qt +# cannot be recognized as such. + +#---------------------------------------------------------- + +# Following other strings as emitted by konsole.
new file mode 100644 --- /dev/null +++ b/gui//konsole_wcwidth.cpp @@ -0,0 +1,216 @@ +/* $XFree86: xc/programs/xterm/wcwidth.character,v 1.3 2001/07/29 22:08:16 tsi Exp $ */ +/* + * This is an implementation of wcwidth() and wcswidth() as defined in + * "The Single UNIX Specification, Version 2, The Open Group, 1997" + * <http://www.UNIX-systems.org/online.html> + * + * Markus Kuhn -- 2001-01-12 -- public domain + */ + +#include "konsole_wcwidth.h" + +struct interval { + unsigned short first; + unsigned short last; +}; + +/* auxiliary function for binary search in interval table */ +static int bisearch(quint16 ucs, const struct interval *table, int max) { + int min = 0; + int mid; + + if (ucs < table[0].first || ucs > table[max].last) + return 0; + while (max >= min) { + mid = (min + max) / 2; + if (ucs > table[mid].last) + min = mid + 1; + else if (ucs < table[mid].first) + max = mid - 1; + else + return 1; + } + + return 0; +} + + +/* The following functions define the column width of an ISO 10646 + * character as follows: + * + * - The null character (U+0000) has a column width of 0. + * + * - Other C0/C1 control characters and DEL will lead to a return + * value of -1. + * + * - Non-spacing and enclosing combining characters (general + * category code Mn or Me in the Unicode database) have a + * column width of 0. + * + * - Other format characters (general category code Cf in the Unicode + * database) and ZERO WIDTH SPACE (U+200B) have a column width of 0. + * + * - Hangul Jamo medial vowels and final consonants (U+1160-U+11FF) + * have a column width of 0. + * + * - Spacing characters in the East Asian Wide (W) or East Asian + * FullWidth (F) category as defined in Unicode Technical + * Report #11 have a column width of 2. + * + * - All remaining characters (including all printable + * ISO 8859-1 and WGL4 characters, Unicode control characters, + * etc.) have a column width of 1. + * + * This implementation assumes that quint16 characters are encoded + * in ISO 10646. + */ + +int konsole_wcwidth(quint16 ucs) +{ + /* sorted list of non-overlapping intervals of non-spacing characters */ + static const struct interval combining[] = { + { 0x0300, 0x034E }, { 0x0360, 0x0362 }, { 0x0483, 0x0486 }, + { 0x0488, 0x0489 }, { 0x0591, 0x05A1 }, { 0x05A3, 0x05B9 }, + { 0x05BB, 0x05BD }, { 0x05BF, 0x05BF }, { 0x05C1, 0x05C2 }, + { 0x05C4, 0x05C4 }, { 0x064B, 0x0655 }, { 0x0670, 0x0670 }, + { 0x06D6, 0x06E4 }, { 0x06E7, 0x06E8 }, { 0x06EA, 0x06ED }, + { 0x070F, 0x070F }, { 0x0711, 0x0711 }, { 0x0730, 0x074A }, + { 0x07A6, 0x07B0 }, { 0x0901, 0x0902 }, { 0x093C, 0x093C }, + { 0x0941, 0x0948 }, { 0x094D, 0x094D }, { 0x0951, 0x0954 }, + { 0x0962, 0x0963 }, { 0x0981, 0x0981 }, { 0x09BC, 0x09BC }, + { 0x09C1, 0x09C4 }, { 0x09CD, 0x09CD }, { 0x09E2, 0x09E3 }, + { 0x0A02, 0x0A02 }, { 0x0A3C, 0x0A3C }, { 0x0A41, 0x0A42 }, + { 0x0A47, 0x0A48 }, { 0x0A4B, 0x0A4D }, { 0x0A70, 0x0A71 }, + { 0x0A81, 0x0A82 }, { 0x0ABC, 0x0ABC }, { 0x0AC1, 0x0AC5 }, + { 0x0AC7, 0x0AC8 }, { 0x0ACD, 0x0ACD }, { 0x0B01, 0x0B01 }, + { 0x0B3C, 0x0B3C }, { 0x0B3F, 0x0B3F }, { 0x0B41, 0x0B43 }, + { 0x0B4D, 0x0B4D }, { 0x0B56, 0x0B56 }, { 0x0B82, 0x0B82 }, + { 0x0BC0, 0x0BC0 }, { 0x0BCD, 0x0BCD }, { 0x0C3E, 0x0C40 }, + { 0x0C46, 0x0C48 }, { 0x0C4A, 0x0C4D }, { 0x0C55, 0x0C56 }, + { 0x0CBF, 0x0CBF }, { 0x0CC6, 0x0CC6 }, { 0x0CCC, 0x0CCD }, + { 0x0D41, 0x0D43 }, { 0x0D4D, 0x0D4D }, { 0x0DCA, 0x0DCA }, + { 0x0DD2, 0x0DD4 }, { 0x0DD6, 0x0DD6 }, { 0x0E31, 0x0E31 }, + { 0x0E34, 0x0E3A }, { 0x0E47, 0x0E4E }, { 0x0EB1, 0x0EB1 }, + { 0x0EB4, 0x0EB9 }, { 0x0EBB, 0x0EBC }, { 0x0EC8, 0x0ECD }, + { 0x0F18, 0x0F19 }, { 0x0F35, 0x0F35 }, { 0x0F37, 0x0F37 }, + { 0x0F39, 0x0F39 }, { 0x0F71, 0x0F7E }, { 0x0F80, 0x0F84 }, + { 0x0F86, 0x0F87 }, { 0x0F90, 0x0F97 }, { 0x0F99, 0x0FBC }, + { 0x0FC6, 0x0FC6 }, { 0x102D, 0x1030 }, { 0x1032, 0x1032 }, + { 0x1036, 0x1037 }, { 0x1039, 0x1039 }, { 0x1058, 0x1059 }, + { 0x1160, 0x11FF }, { 0x17B7, 0x17BD }, { 0x17C6, 0x17C6 }, + { 0x17C9, 0x17D3 }, { 0x180B, 0x180E }, { 0x18A9, 0x18A9 }, + { 0x200B, 0x200F }, { 0x202A, 0x202E }, { 0x206A, 0x206F }, + { 0x20D0, 0x20E3 }, { 0x302A, 0x302F }, { 0x3099, 0x309A }, + { 0xFB1E, 0xFB1E }, { 0xFE20, 0xFE23 }, { 0xFEFF, 0xFEFF }, + { 0xFFF9, 0xFFFB } + }; + + /* test for 8-bit control characters */ + if (ucs == 0) + return 0; + if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) + return -1; + + /* binary search in table of non-spacing characters */ + if (bisearch(ucs, combining, + sizeof(combining) / sizeof(struct interval) - 1)) + return 0; + + /* if we arrive here, ucs is not a combining or C0/C1 control character */ + + return 1 + + (ucs >= 0x1100 && + (ucs <= 0x115f || /* Hangul Jamo init. consonants */ + (ucs >= 0x2e80 && ucs <= 0xa4cf && (ucs & ~0x0011) != 0x300a && + ucs != 0x303f) || /* CJK ... Yi */ + (ucs >= 0xac00 && ucs <= 0xd7a3) || /* Hangul Syllables */ + (ucs >= 0xf900 && ucs <= 0xfaff) || /* CJK Compatibility Ideographs */ + (ucs >= 0xfe30 && ucs <= 0xfe6f) || /* CJK Compatibility Forms */ + (ucs >= 0xff00 && ucs <= 0xff5f) || /* Fullwidth Forms */ + (ucs >= 0xffe0 && ucs <= 0xffe6) /* do not compare UINT16 with 0x20000 || + (ucs >= 0x20000 && ucs <= 0x2ffff) */)); +} + +#if 0 +/* + * The following function is the same as konsole_wcwidth(), except that + * spacing characters in the East Asian Ambiguous (A) category as + * defined in Unicode Technical Report #11 have a column width of 2. + * This experimental variant might be useful for users of CJK legacy + * encodings who want to migrate to UCS. It is not otherwise + * recommended for general use. + */ +int konsole_wcwidth_cjk(quint16 ucs) +{ + /* sorted list of non-overlapping intervals of East Asian Ambiguous + * characters */ + static const struct interval ambiguous[] = { + { 0x00A1, 0x00A1 }, { 0x00A4, 0x00A4 }, { 0x00A7, 0x00A8 }, + { 0x00AA, 0x00AA }, { 0x00AD, 0x00AD }, { 0x00B0, 0x00B4 }, + { 0x00B6, 0x00BA }, { 0x00BC, 0x00BF }, { 0x00C6, 0x00C6 }, + { 0x00D0, 0x00D0 }, { 0x00D7, 0x00D8 }, { 0x00DE, 0x00E1 }, + { 0x00E6, 0x00E6 }, { 0x00E8, 0x00EA }, { 0x00EC, 0x00ED }, + { 0x00F0, 0x00F0 }, { 0x00F2, 0x00F3 }, { 0x00F7, 0x00FA }, + { 0x00FC, 0x00FC }, { 0x00FE, 0x00FE }, { 0x0101, 0x0101 }, + { 0x0111, 0x0111 }, { 0x0113, 0x0113 }, { 0x011B, 0x011B }, + { 0x0126, 0x0127 }, { 0x012B, 0x012B }, { 0x0131, 0x0133 }, + { 0x0138, 0x0138 }, { 0x013F, 0x0142 }, { 0x0144, 0x0144 }, + { 0x0148, 0x014A }, { 0x014D, 0x014D }, { 0x0152, 0x0153 }, + { 0x0166, 0x0167 }, { 0x016B, 0x016B }, { 0x01CE, 0x01CE }, + { 0x01D0, 0x01D0 }, { 0x01D2, 0x01D2 }, { 0x01D4, 0x01D4 }, + { 0x01D6, 0x01D6 }, { 0x01D8, 0x01D8 }, { 0x01DA, 0x01DA }, + { 0x01DC, 0x01DC }, { 0x0251, 0x0251 }, { 0x0261, 0x0261 }, + { 0x02C7, 0x02C7 }, { 0x02C9, 0x02CB }, { 0x02CD, 0x02CD }, + { 0x02D0, 0x02D0 }, { 0x02D8, 0x02DB }, { 0x02DD, 0x02DD }, + { 0x0391, 0x03A1 }, { 0x03A3, 0x03A9 }, { 0x03B1, 0x03C1 }, + { 0x03C3, 0x03C9 }, { 0x0401, 0x0401 }, { 0x0410, 0x044F }, + { 0x0451, 0x0451 }, { 0x2010, 0x2010 }, { 0x2013, 0x2016 }, + { 0x2018, 0x2019 }, { 0x201C, 0x201D }, { 0x2020, 0x2021 }, + { 0x2025, 0x2027 }, { 0x2030, 0x2030 }, { 0x2032, 0x2033 }, + { 0x2035, 0x2035 }, { 0x203B, 0x203B }, { 0x2074, 0x2074 }, + { 0x207F, 0x207F }, { 0x2081, 0x2084 }, { 0x20AC, 0x20AC }, + { 0x2103, 0x2103 }, { 0x2105, 0x2105 }, { 0x2109, 0x2109 }, + { 0x2113, 0x2113 }, { 0x2121, 0x2122 }, { 0x2126, 0x2126 }, + { 0x212B, 0x212B }, { 0x2154, 0x2155 }, { 0x215B, 0x215B }, + { 0x215E, 0x215E }, { 0x2160, 0x216B }, { 0x2170, 0x2179 }, + { 0x2190, 0x2199 }, { 0x21D2, 0x21D2 }, { 0x21D4, 0x21D4 }, + { 0x2200, 0x2200 }, { 0x2202, 0x2203 }, { 0x2207, 0x2208 }, + { 0x220B, 0x220B }, { 0x220F, 0x220F }, { 0x2211, 0x2211 }, + { 0x2215, 0x2215 }, { 0x221A, 0x221A }, { 0x221D, 0x2220 }, + { 0x2223, 0x2223 }, { 0x2225, 0x2225 }, { 0x2227, 0x222C }, + { 0x222E, 0x222E }, { 0x2234, 0x2237 }, { 0x223C, 0x223D }, + { 0x2248, 0x2248 }, { 0x224C, 0x224C }, { 0x2252, 0x2252 }, + { 0x2260, 0x2261 }, { 0x2264, 0x2267 }, { 0x226A, 0x226B }, + { 0x226E, 0x226F }, { 0x2282, 0x2283 }, { 0x2286, 0x2287 }, + { 0x2295, 0x2295 }, { 0x2299, 0x2299 }, { 0x22A5, 0x22A5 }, + { 0x22BF, 0x22BF }, { 0x2312, 0x2312 }, { 0x2460, 0x24BF }, + { 0x24D0, 0x24E9 }, { 0x2500, 0x254B }, { 0x2550, 0x2574 }, + { 0x2580, 0x258F }, { 0x2592, 0x2595 }, { 0x25A0, 0x25A1 }, + { 0x25A3, 0x25A9 }, { 0x25B2, 0x25B3 }, { 0x25B6, 0x25B7 }, + { 0x25BC, 0x25BD }, { 0x25C0, 0x25C1 }, { 0x25C6, 0x25C8 }, + { 0x25CB, 0x25CB }, { 0x25CE, 0x25D1 }, { 0x25E2, 0x25E5 }, + { 0x25EF, 0x25EF }, { 0x2605, 0x2606 }, { 0x2609, 0x2609 }, + { 0x260E, 0x260F }, { 0x261C, 0x261C }, { 0x261E, 0x261E }, + { 0x2640, 0x2640 }, { 0x2642, 0x2642 }, { 0x2660, 0x2661 }, + { 0x2663, 0x2665 }, { 0x2667, 0x266A }, { 0x266C, 0x266D }, + { 0x266F, 0x266F }, { 0x300A, 0x300B }, { 0x301A, 0x301B }, + { 0xE000, 0xF8FF }, { 0xFFFD, 0xFFFD } + }; + + /* binary search in table of non-spacing characters */ + if (bisearch(ucs, ambiguous, + sizeof(ambiguous) / sizeof(struct interval) - 1)) + return 2; + + return konsole_wcwidth(ucs); +} +#endif + +// single byte char: +1, multi byte char: +2 +int string_width( const QString &txt ) +{ + int w = 0; + for ( int i = 0; i < txt.length(); ++i ) + w += konsole_wcwidth( txt[ i ].unicode() ); + return w; +}
new file mode 100644 --- /dev/null +++ b/gui//konsole_wcwidth.h @@ -0,0 +1,24 @@ +/* $XFree86: xc/programs/xterm/wcwidth.h,v 1.2 2001/06/18 19:09:27 dickey Exp $ */ + +/* Markus Kuhn -- 2001-01-12 -- public domain */ +/* Adaptions for KDE by Waldo Bastian <bastian@kde.org> */ +/* + Rewritten for QT4 by e_k <e_k at users.sourceforge.net> +*/ + + +#ifndef _KONSOLE_WCWIDTH_H_ +#define _KONSOLE_WCWIDTH_H_ + +// Qt +#include <QtCore/QBool> +#include <QtCore/QString> + +int konsole_wcwidth(quint16 ucs); +#if 0 +int konsole_wcwidth_cjk(Q_UINT16 ucs); +#endif + +int string_width( const QString &txt ); + +#endif
new file mode 100644 --- /dev/null +++ b/gui//kpty.cpp @@ -0,0 +1,624 @@ +/* + + This file is part of the KDE libraries + Copyright (C) 2002 Waldo Bastian <bastian@kde.org> + Copyright (C) 2002-2003,2007 Oswald Buddenhagen <ossi@kde.org> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "kpty_p.h" + +#ifdef __sgi +#define __svr4__ +#endif + +#ifdef __osf__ +#define _OSF_SOURCE +#include <float.h> +#endif + +#ifdef _AIX +#define _ALL_SOURCE +#endif + +// __USE_XOPEN isn't defined by default in ICC +// (needed for ptsname(), grantpt() and unlockpt()) +#ifdef __INTEL_COMPILER +# ifndef __USE_XOPEN +# define __USE_XOPEN +# endif +#endif + +#include <sys/types.h> +#include <sys/ioctl.h> +#include <sys/time.h> +#include <sys/resource.h> +#include <sys/stat.h> +#include <sys/param.h> + +#include <errno.h> +#include <fcntl.h> +#include <time.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <grp.h> + +#if defined(HAVE_PTY_H) +# include <pty.h> +#endif + +#ifdef HAVE_LIBUTIL_H +# include <libutil.h> +#elif defined(HAVE_UTIL_H) +# include <util.h> +#endif + +#ifdef HAVE_UTEMPTER +extern "C" { +# include <utempter.h> +} +#else +# include <utmp.h> +# ifdef HAVE_UTMPX +# include <utmpx.h> +# endif +# if !defined(_PATH_UTMPX) && defined(_UTMPX_FILE) +# define _PATH_UTMPX _UTMPX_FILE +# endif +# if !defined(_PATH_WTMPX) && defined(_WTMPX_FILE) +# define _PATH_WTMPX _WTMPX_FILE +# endif +#endif + +/* for HP-UX (some versions) the extern C is needed, and for other + platforms it doesn't hurt */ +extern "C" { +#include <termios.h> +#if defined(HAVE_TERMIO_H) +# include <termio.h> // struct winsize on some systems +#endif +} + +#if defined (_HPUX_SOURCE) +# define _TERMIOS_INCLUDED +# include <bsdtty.h> +#endif + +#ifdef HAVE_SYS_STROPTS_H +# include <sys/stropts.h> // Defines I_PUSH +# define _NEW_TTY_CTRL +#endif + +#if defined (__FreeBSD__) || defined (__NetBSD__) || defined (__OpenBSD__) || defined (__bsdi__) || defined(__APPLE__) || defined (__DragonFly__) +# define _tcgetattr(fd, ttmode) ioctl(fd, TIOCGETA, (char *)ttmode) +#else +# if defined(_HPUX_SOURCE) || defined(__Lynx__) || defined (__CYGWIN__) +# define _tcgetattr(fd, ttmode) tcgetattr(fd, ttmode) +# else +# define _tcgetattr(fd, ttmode) ioctl(fd, TCGETS, (char *)ttmode) +# endif +#endif + +#if defined (__FreeBSD__) || defined (__NetBSD__) || defined (__OpenBSD__) || defined (__bsdi__) || defined(__APPLE__) || defined (__DragonFly__) +# define _tcsetattr(fd, ttmode) ioctl(fd, TIOCSETA, (char *)ttmode) +#else +# if defined(_HPUX_SOURCE) || defined(__CYGWIN__) +# define _tcsetattr(fd, ttmode) tcsetattr(fd, TCSANOW, ttmode) +# else +# define _tcsetattr(fd, ttmode) ioctl(fd, TCSETS, (char *)ttmode) +# endif +#endif + +//#include <kdebug.h> +//#include <kstandarddirs.h> // findExe + +#include <QtCore> + +// not defined on HP-UX for example +#ifndef CTRL +# define CTRL(x) ((x) & 037) +#endif + +#define TTY_GROUP "tty" + +/////////////////////// +// private functions // +/////////////////////// + +////////////////// +// private data // +////////////////// + +KPtyPrivate::KPtyPrivate() : + masterFd(-1), slaveFd(-1) +{ +} + +bool KPtyPrivate::chownpty(bool) +{ +// return !QProcess::execute(KStandardDirs::findExe("kgrantpty"), +// QStringList() << (grant?"--grant":"--revoke") << QString::number(masterFd)); + return true; +} + +///////////////////////////// +// public member functions // +///////////////////////////// + +KPty::KPty() : + d_ptr(new KPtyPrivate) +{ + d_ptr->q_ptr = this; +} + +KPty::KPty(KPtyPrivate *d) : + d_ptr(d) +{ + d_ptr->q_ptr = this; +} + +KPty::~KPty() +{ + close(); + delete d_ptr; +} + +bool KPty::open() +{ + Q_D(KPty); + + if (d->masterFd >= 0) + return true; + + QByteArray ptyName; + + // Find a master pty that we can open //////////////////////////////// + + // Because not all the pty animals are created equal, they want to + // be opened by several different methods. + + // We try, as we know them, one by one. + +#ifdef HAVE_OPENPTY + + char ptsn[PATH_MAX]; + if (::openpty( &d->masterFd, &d->slaveFd, ptsn, 0, 0)) + { + d->masterFd = -1; + d->slaveFd = -1; + kWarning(175) << "Can't open a pseudo teletype"; + return false; + } + d->ttyName = ptsn; + +#else + +#ifdef HAVE__GETPTY // irix + + char *ptsn = _getpty(&d->masterFd, O_RDWR|O_NOCTTY, S_IRUSR|S_IWUSR, 0); + if (ptsn) { + d->ttyName = ptsn; + goto grantedpt; + } + +#elif defined(HAVE_PTSNAME) || defined(TIOCGPTN) + +#ifdef HAVE_POSIX_OPENPT + d->masterFd = ::posix_openpt(O_RDWR|O_NOCTTY); +#elif defined(HAVE_GETPT) + d->masterFd = ::getpt(); +#elif defined(PTM_DEVICE) + d->masterFd = ::open(PTM_DEVICE, O_RDWR|O_NOCTTY); +#else +# error No method to open a PTY master detected. +#endif + + if (d->masterFd >= 0) + { + +#ifdef HAVE_PTSNAME + char *ptsn = ptsname(d->masterFd); + if (ptsn) { + d->ttyName = ptsn; +#else + int ptyno; + if (!ioctl(d->masterFd, TIOCGPTN, &ptyno)) { + d->ttyName = QByteArray("/dev/pts/") + QByteArray::number(ptyno); +#endif +#ifdef HAVE_GRANTPT + if (!grantpt(d->masterFd)) + goto grantedpt; +#else + + goto gotpty; +#endif + } + ::close(d->masterFd); + d->masterFd = -1; + } +#endif // HAVE_PTSNAME || TIOCGPTN + + // Linux device names, FIXME: Trouble on other systems? + for (const char* s3 = "pqrstuvwxyzabcde"; *s3; s3++) + { + for (const char* s4 = "0123456789abcdef"; *s4; s4++) + { + ptyName = QString().sprintf("/dev/pty%c%c", *s3, *s4).toAscii(); + d->ttyName = QString().sprintf("/dev/tty%c%c", *s3, *s4).toAscii(); + + d->masterFd = ::open(ptyName.data(), O_RDWR); + if (d->masterFd >= 0) + { +#ifdef Q_OS_SOLARIS + /* Need to check the process group of the pty. + * If it exists, then the slave pty is in use, + * and we need to get another one. + */ + int pgrp_rtn; + if (ioctl(d->masterFd, TIOCGPGRP, &pgrp_rtn) == 0 || errno != EIO) { + ::close(d->masterFd); + d->masterFd = -1; + continue; + } +#endif /* Q_OS_SOLARIS */ + if (!access(d->ttyName.data(),R_OK|W_OK)) // checks availability based on permission bits + { + if (!geteuid()) + { + struct group* p = getgrnam(TTY_GROUP); + if (!p) + p = getgrnam("wheel"); + gid_t gid = p ? p->gr_gid : getgid (); + + chown(d->ttyName.data(), getuid(), gid); + chmod(d->ttyName.data(), S_IRUSR|S_IWUSR|S_IWGRP); + } + goto gotpty; + } + ::close(d->masterFd); + d->masterFd = -1; + } + } + } + + qWarning() << "Can't open a pseudo teletype"; + return false; + + gotpty: + struct stat st; + if (stat(d->ttyName.data(), &st)) { + return false; // this just cannot happen ... *cough* Yeah right, I just + // had it happen when pty #349 was allocated. I guess + // there was some sort of leak? I only had a few open. + } + if (((st.st_uid != getuid()) || + (st.st_mode & (S_IRGRP|S_IXGRP|S_IROTH|S_IWOTH|S_IXOTH))) && + !d->chownpty(true)) + { + qWarning() + << "chownpty failed for device " << ptyName << "::" << d->ttyName + << "\nThis means the communication can be eavesdropped." << endl; + } + +#if defined (HAVE__GETPTY) || defined (HAVE_GRANTPT) + grantedpt: +#endif + +#ifdef HAVE_REVOKE + revoke(d->ttyName.data()); +#endif + +#ifdef HAVE_UNLOCKPT + unlockpt(d->masterFd); +#elif defined(TIOCSPTLCK) + int flag = 0; + ioctl(d->masterFd, TIOCSPTLCK, &flag); +#endif + + d->slaveFd = ::open(d->ttyName.data(), O_RDWR | O_NOCTTY); + if (d->slaveFd < 0) + { + qWarning() << "Can't open slave pseudo teletype"; + ::close(d->masterFd); + d->masterFd = -1; + return false; + } + +#if (defined(__svr4__) || defined(__sgi__)) + // Solaris + ioctl(d->slaveFd, I_PUSH, "ptem"); + ioctl(d->slaveFd, I_PUSH, "ldterm"); +#endif + +#endif /* HAVE_OPENPTY */ + + fcntl(d->masterFd, F_SETFD, FD_CLOEXEC); + fcntl(d->slaveFd, F_SETFD, FD_CLOEXEC); + + return true; +} + +void KPty::closeSlave() +{ + Q_D(KPty); + + if (d->slaveFd < 0) + return; + ::close(d->slaveFd); + d->slaveFd = -1; +} + +void KPty::close() +{ + Q_D(KPty); + + if (d->masterFd < 0) + return; + closeSlave(); + // don't bother resetting unix98 pty, it will go away after closing master anyway. + if (memcmp(d->ttyName.data(), "/dev/pts/", 9)) { + if (!geteuid()) { + struct stat st; + if (!stat(d->ttyName.data(), &st)) { + chown(d->ttyName.data(), 0, st.st_gid == getgid() ? 0 : -1); + chmod(d->ttyName.data(), S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH); + } + } else { + fcntl(d->masterFd, F_SETFD, 0); + d->chownpty(false); + } + } + ::close(d->masterFd); + d->masterFd = -1; +} + +void KPty::setCTty() +{ + Q_D(KPty); + + // Setup job control ////////////////////////////////// + + // Become session leader, process group leader, + // and get rid of the old controlling terminal. + setsid(); + + // make our slave pty the new controlling terminal. +#ifdef TIOCSCTTY + ioctl(d->slaveFd, TIOCSCTTY, 0); +#else + // __svr4__ hack: the first tty opened after setsid() becomes controlling tty + ::close(::open(d->ttyName, O_WRONLY, 0)); +#endif + + // make our new process group the foreground group on the pty + int pgrp = getpid(); +#if defined(_POSIX_VERSION) || defined(__svr4__) + tcsetpgrp(d->slaveFd, pgrp); +#elif defined(TIOCSPGRP) + ioctl(d->slaveFd, TIOCSPGRP, (char *)&pgrp); +#endif +} + +void KPty::login(const char *user, const char *remotehost) +{ +#ifdef HAVE_UTEMPTER + Q_D(KPty); + + addToUtmp(d->ttyName, remotehost, d->masterFd); + Q_UNUSED(user); +#else +# ifdef HAVE_UTMPX + struct utmpx l_struct; +# else + struct utmp l_struct; +# endif + memset(&l_struct, 0, sizeof(l_struct)); + // note: strncpy without terminators _is_ correct here. man 4 utmp + + if (user) + strncpy(l_struct.ut_name, user, sizeof(l_struct.ut_name)); + + if (remotehost) { + strncpy(l_struct.ut_host, remotehost, sizeof(l_struct.ut_host)); +# ifdef HAVE_STRUCT_UTMP_UT_SYSLEN + l_struct.ut_syslen = qMin(strlen(remotehost), sizeof(l_struct.ut_host)); +# endif + } + +# ifndef __GLIBC__ + Q_D(KPty); + const char *str_ptr = d->ttyName.data(); + if (!memcmp(str_ptr, "/dev/", 5)) + str_ptr += 5; + strncpy(l_struct.ut_line, str_ptr, sizeof(l_struct.ut_line)); +# ifdef HAVE_STRUCT_UTMP_UT_ID + strncpy(l_struct.ut_id, + str_ptr + strlen(str_ptr) - sizeof(l_struct.ut_id), + sizeof(l_struct.ut_id)); +# endif +# endif + +# ifdef HAVE_UTMPX + gettimeofday(&l_struct.ut_tv, 0); +# else + l_struct.ut_time = time(0); +# endif + +# ifdef HAVE_LOGIN +# ifdef HAVE_LOGINX + ::loginx(&l_struct); +# else + ::login(&l_struct); +# endif +# else +# ifdef HAVE_STRUCT_UTMP_UT_TYPE + l_struct.ut_type = USER_PROCESS; +# endif +# ifdef HAVE_STRUCT_UTMP_UT_PID + l_struct.ut_pid = getpid(); +# ifdef HAVE_STRUCT_UTMP_UT_SESSION + l_struct.ut_session = getsid(0); +# endif +# endif +# ifdef HAVE_UTMPX + utmpxname(_PATH_UTMPX); + setutxent(); + pututxline(&l_struct); + endutxent(); + updwtmpx(_PATH_WTMPX, &l_struct); +# else + utmpname(_PATH_UTMP); + setutent(); + pututline(&l_struct); + endutent(); + updwtmp(_PATH_WTMP, &l_struct); +# endif +# endif +#endif +} + +void KPty::logout() +{ +#ifdef HAVE_UTEMPTER + Q_D(KPty); + + removeLineFromUtmp(d->ttyName, d->masterFd); +#else + Q_D(KPty); + + const char *str_ptr = d->ttyName.data(); + if (!memcmp(str_ptr, "/dev/", 5)) + str_ptr += 5; +# ifdef __GLIBC__ + else { + const char *sl_ptr = strrchr(str_ptr, '/'); + if (sl_ptr) + str_ptr = sl_ptr + 1; + } +# endif +# ifdef HAVE_LOGIN +# ifdef HAVE_LOGINX + ::logoutx(str_ptr, 0, DEAD_PROCESS); +# else + ::logout(str_ptr); +# endif +# else +# ifdef HAVE_UTMPX + struct utmpx l_struct, *ut; +# else + struct utmp l_struct, *ut; +# endif + memset(&l_struct, 0, sizeof(l_struct)); + + strncpy(l_struct.ut_line, str_ptr, sizeof(l_struct.ut_line)); + +# ifdef HAVE_UTMPX + utmpxname(_PATH_UTMPX); + setutxent(); + if ((ut = getutxline(&l_struct))) { +# else + utmpname(_PATH_UTMP); + setutent(); + if ((ut = getutline(&l_struct))) { +# endif + memset(ut->ut_name, 0, sizeof(*ut->ut_name)); + memset(ut->ut_host, 0, sizeof(*ut->ut_host)); +# ifdef HAVE_STRUCT_UTMP_UT_SYSLEN + ut->ut_syslen = 0; +# endif +# ifdef HAVE_STRUCT_UTMP_UT_TYPE + ut->ut_type = DEAD_PROCESS; +# endif +# ifdef HAVE_UTMPX + gettimeofday(ut->ut_tv, 0); + pututxline(ut); + } + endutxent(); +# else + ut->ut_time = time(0); + pututline(ut); + } + endutent(); +# endif +# endif +#endif +} + +// XXX Supposedly, tc[gs]etattr do not work with the master on Solaris. +// Please verify. + +bool KPty::tcGetAttr(struct ::termios *ttmode) const +{ + Q_D(const KPty); + + return _tcgetattr(d->masterFd, ttmode) == 0; +} + +bool KPty::tcSetAttr(struct ::termios *ttmode) +{ + Q_D(KPty); + + return _tcsetattr(d->masterFd, ttmode) == 0; +} + +bool KPty::setWinSize(int lines, int columns) +{ + Q_D(KPty); + + struct winsize winSize; + memset(&winSize, 0, sizeof(winSize)); + winSize.ws_row = (unsigned short)lines; + winSize.ws_col = (unsigned short)columns; + return ioctl(d->masterFd, TIOCSWINSZ, (char *)&winSize) == 0; +} + +bool KPty::setEcho(bool echo) +{ + struct ::termios ttmode; + if (!tcGetAttr(&ttmode)) + return false; + if (!echo) + ttmode.c_lflag &= ~ECHO; + else + ttmode.c_lflag |= ECHO; + return tcSetAttr(&ttmode); +} + +const char *KPty::ttyName() const +{ + Q_D(const KPty); + + return d->ttyName.data(); +} + +int KPty::masterFd() const +{ + Q_D(const KPty); + + return d->masterFd; +} + +int KPty::slaveFd() const +{ + Q_D(const KPty); + + return d->slaveFd; +}
new file mode 100644 --- /dev/null +++ b/gui//kpty.h @@ -0,0 +1,188 @@ +/* This file is part of the KDE libraries + + Copyright (C) 2003,2007 Oswald Buddenhagen <ossi@kde.org> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef kpty_h +#define kpty_h + +#include <QtCore> + +struct KPtyPrivate; +struct termios; + +/** + * Provides primitives for opening & closing a pseudo TTY pair, assigning the + * controlling TTY, utmp registration and setting various terminal attributes. + */ +class KPty { + Q_DECLARE_PRIVATE(KPty) + +public: + + /** + * Constructor + */ + KPty(); + + /** + * Destructor: + * + * If the pty is still open, it will be closed. Note, however, that + * an utmp registration is @em not undone. + */ + ~KPty(); + + /** + * Create a pty master/slave pair. + * + * @return true if a pty pair was successfully opened + */ + bool open(); + + /** + * Close the pty master/slave pair. + */ + void close(); + + /** + * Close the pty slave descriptor. + * + * When creating the pty, KPty also opens the slave and keeps it open. + * Consequently the master will never receive an EOF notification. + * Usually this is the desired behavior, as a closed pty slave can be + * reopened any time - unlike a pipe or socket. However, in some cases + * pipe-alike behavior might be desired. + * + * After this function was called, slaveFd() and setCTty() cannot be + * used. + */ + void closeSlave(); + + /** + * Creates a new session and process group and makes this pty the + * controlling tty. + */ + void setCTty(); + + /** + * Creates an utmp entry for the tty. + * This function must be called after calling setCTty and + * making this pty the stdin. + * @param user the user to be logged on + * @param remotehost the host from which the login is coming. This is + * @em not the local host. For remote logins it should be the hostname + * of the client. For local logins from inside an X session it should + * be the name of the X display. Otherwise it should be empty. + */ + void login(const char *user = 0, const char *remotehost = 0); + + /** + * Removes the utmp entry for this tty. + */ + void logout(); + + /** + * Wrapper around tcgetattr(3). + * + * This function can be used only while the PTY is open. + * You will need an #include <termios.h> to do anything useful + * with it. + * + * @param ttmode a pointer to a termios structure. + * Note: when declaring ttmode, @c struct @c ::termios must be used - + * without the '::' some version of HP-UX thinks, this declares + * the struct in your class, in your method. + * @return @c true on success, false otherwise + */ + bool tcGetAttr(struct ::termios *ttmode) const; + + /** + * Wrapper around tcsetattr(3) with mode TCSANOW. + * + * This function can be used only while the PTY is open. + * + * @param ttmode a pointer to a termios structure. + * @return @c true on success, false otherwise. Note that success means + * that @em at @em least @em one attribute could be set. + */ + bool tcSetAttr(struct ::termios *ttmode); + + /** + * Change the logical (screen) size of the pty. + * The default is 24 lines by 80 columns. + * + * This function can be used only while the PTY is open. + * + * @param lines the number of rows + * @param columns the number of columns + * @return @c true on success, false otherwise + */ + bool setWinSize(int lines, int columns); + + /** + * Set whether the pty should echo input. + * + * Echo is on by default. + * If the output of automatically fed (non-interactive) PTY clients + * needs to be parsed, disabling echo often makes it much simpler. + * + * This function can be used only while the PTY is open. + * + * @param echo true if input should be echoed. + * @return @c true on success, false otherwise + */ + bool setEcho(bool echo); + + /** + * @return the name of the slave pty device. + * + * This function should be called only while the pty is open. + */ + const char *ttyName() const; + + /** + * @return the file descriptor of the master pty + * + * This function should be called only while the pty is open. + */ + int masterFd() const; + + /** + * @return the file descriptor of the slave pty + * + * This function should be called only while the pty slave is open. + */ + int slaveFd() const; + +protected: + /** + * @internal + */ + KPty(KPtyPrivate *d); + + /** + * @internal + */ + KPtyPrivate * const d_ptr; +}; + +#endif +
new file mode 100644 --- /dev/null +++ b/gui//kpty_p.h @@ -0,0 +1,44 @@ +/* This file is part of the KDE libraries + + Copyright (C) 2003,2007 Oswald Buddenhagen <ossi@kde.org> + + Rewritten for QT4 by e_k <e_k at users.sourceforge.net>, Copyright (C)2008 + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef kpty_p_h +#define kpty_p_h + +#include "kpty.h" + +#include <QtCore/QByteArray> + +struct KPtyPrivate { + Q_DECLARE_PUBLIC(KPty) + + KPtyPrivate(); + bool chownpty(bool grant); + + int masterFd; + int slaveFd; + + QByteArray ttyName; + + KPty *q_ptr; +}; + +#endif
new file mode 100644 --- /dev/null +++ b/gui//lib.pro @@ -0,0 +1,48 @@ +TEMPLATE = lib +VERSION = 0.1.0 +DESTDIR = .. + +TARGET = qtermwidget + +CONFIG += qt debug_and_release warn_on build_all staticlib #dll + +QT += core gui + +MOC_DIR = ../.moc + +CONFIG(debug, debug|release) { + OBJECTS_DIR = ../.objs_d + TARGET = qtermwidget_d +} else { + OBJECTS_DIR = ../.objs + TARGET = qtermwidget +} + +DEFINES += HAVE_POSIX_OPENPT +#or DEFINES += HAVE_GETPT + +HEADERS = TerminalCharacterDecoder.h Character.h CharacterColor.h \ + KeyboardTranslator.h \ + ExtendedDefaultTranslator.h \ + Screen.h History.h BlockArray.h konsole_wcwidth.h \ + ScreenWindow.h \ + Emulation.h \ + Vt102Emulation.h TerminalDisplay.h Filter.h LineFont.h \ + Pty.h kpty.h kpty_p.h k3process.h k3processcontroller.h \ + Session.h ShellCommand.h \ + qtermwidget.h + +SOURCES = TerminalCharacterDecoder.cpp \ + KeyboardTranslator.cpp \ + Screen.cpp History.cpp BlockArray.cpp konsole_wcwidth.cpp \ + ScreenWindow.cpp \ + Emulation.cpp \ + Vt102Emulation.cpp TerminalDisplay.cpp Filter.cpp \ + Pty.cpp kpty.cpp k3process.cpp k3processcontroller.cpp \ + Session.cpp ShellCommand.cpp \ + qtermwidget.cpp + + + + + \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/gui//qtermwidget.cpp @@ -0,0 +1,222 @@ +/* Copyright (C) 2008 e_k (e_k@users.sourceforge.net) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + + +#include "qtermwidget.h" + +#include "Session.h" +#include "TerminalDisplay.h" + +using namespace Konsole; + +void *createTermWidget(int startnow, void *parent) +{ + return (void*) new QTermWidget(startnow, (QWidget*)parent); +} + +struct TermWidgetImpl +{ + TermWidgetImpl(QWidget* parent = 0); + + TerminalDisplay *m_terminalDisplay; + Session *m_session; + + Session* createSession(); + TerminalDisplay* createTerminalDisplay(Session *session, QWidget* parent); +}; + +TermWidgetImpl::TermWidgetImpl(QWidget* parent) +{ + this->m_session = createSession(); + this->m_terminalDisplay = createTerminalDisplay(this->m_session, parent); +} + + +Session *TermWidgetImpl::createSession() +{ + Session *session = new Session(); + + session->setTitle(Session::NameRole, "QTermWidget"); + session->setProgram("/bin/bash"); + QStringList args(""); + session->setArguments(args); + session->setAutoClose(true); + + session->setCodec(QTextCodec::codecForName("UTF-8")); + + session->setFlowControlEnabled(true); + session->setHistoryType(HistoryTypeBuffer(1000)); + + session->setDarkBackground(true); + + session->setKeyBindings(""); + return session; +} + +TerminalDisplay *TermWidgetImpl::createTerminalDisplay(Session *session, QWidget* parent) +{ +// TerminalDisplay* display = new TerminalDisplay(this); + TerminalDisplay* display = new TerminalDisplay(parent); + + display->setBellMode(TerminalDisplay::NotifyBell); + display->setTerminalSizeHint(true); + display->setTripleClickMode(TerminalDisplay::SelectWholeLine); + display->setTerminalSizeStartup(true); + + display->setRandomSeed(session->sessionId() * 31); + + return display; +} + + +QTermWidget::QTermWidget(int startnow, QWidget *parent) +:QWidget(parent) +{ + m_impl = new TermWidgetImpl(this); + + init(); + + if (startnow && m_impl->m_session) { + m_impl->m_session->run(); + } + + this->setFocus( Qt::OtherFocusReason ); + m_impl->m_terminalDisplay->resize(this->size()); + + this->setFocusProxy(m_impl->m_terminalDisplay); +} + +void QTermWidget::startShellProgram() +{ + if ( m_impl->m_session->isRunning() ) + return; + + m_impl->m_session->run(); +} + +void QTermWidget::init() +{ + m_impl->m_terminalDisplay->setSize(80, 40); + + QFont font = QApplication::font(); + font.setFamily("Monospace"); + font.setPointSize(10); + font.setStyleHint(QFont::TypeWriter); + setTerminalFont(font); + setScrollBarPosition(NoScrollBar); + + m_impl->m_session->addView(m_impl->m_terminalDisplay); + + connect(m_impl->m_session, SIGNAL(finished()), this, SLOT(sessionFinished())); +} + + +QTermWidget::~QTermWidget() +{ + emit destroyed(); +} + + +void QTermWidget::setTerminalFont(QFont &font) +{ + if (!m_impl->m_terminalDisplay) + return; + m_impl->m_terminalDisplay->setVTFont(font); +} + +void QTermWidget::setShellProgram(QString &progname) +{ + if (!m_impl->m_session) + return; + m_impl->m_session->setProgram(progname); +} + +void QTermWidget::setArgs(QStringList &args) +{ + if (!m_impl->m_session) + return; + m_impl->m_session->setArguments(args); +} + +void QTermWidget::setTextCodec(QTextCodec *codec) +{ + if (!m_impl->m_session) + return; + m_impl->m_session->setCodec(codec); +} + +void QTermWidget::setColorScheme(int scheme) +{ + switch(scheme) { + case COLOR_SCHEME_WHITE_ON_BLACK: + m_impl->m_terminalDisplay->setColorTable(whiteonblack_color_table); + break; + case COLOR_SCHEME_GREEN_ON_BLACK: + m_impl->m_terminalDisplay->setColorTable(greenonblack_color_table); + break; + case COLOR_SCHEME_BLACK_ON_LIGHT_YELLOW: + m_impl->m_terminalDisplay->setColorTable(blackonlightyellow_color_table); + break; + default: //do nothing + break; + }; +} + +void QTermWidget::setSize(int h, int v) +{ + if (!m_impl->m_terminalDisplay) + return; + m_impl->m_terminalDisplay->setSize(h, v); +} + +void QTermWidget::setHistorySize(int lines) +{ + if (lines < 0) + m_impl->m_session->setHistoryType(HistoryTypeFile()); + else + m_impl->m_session->setHistoryType(HistoryTypeBuffer(lines)); +} + +void QTermWidget::setScrollBarPosition(ScrollBarPosition pos) +{ + if (!m_impl->m_terminalDisplay) + return; + m_impl->m_terminalDisplay->setScrollBarPosition((TerminalDisplay::ScrollBarPosition)pos); +} + +void QTermWidget::sendText(QString &text) +{ + m_impl->m_session->sendText(text); +} + +void QTermWidget::resizeEvent(QResizeEvent*) +{ +//qDebug("global window resizing...with %d %d", this->size().width(), this->size().height()); + m_impl->m_terminalDisplay->resize(this->size()); +} + + + +void QTermWidget::sessionFinished() +{ + emit finished(); +} + + +//#include "moc_consoleq.cpp" +
new file mode 100644 --- /dev/null +++ b/gui//qtermwidget.h @@ -0,0 +1,108 @@ +/* Copyright (C) 2008 e_k (e_k@users.sourceforge.net) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + + +#ifndef _Q_TERM_WIDGET +#define _Q_TERM_WIDGET + +#include <QtGui> + +struct TermWidgetImpl; + +enum COLOR_SCHEME { COLOR_SCHEME_WHITE_ON_BLACK = 1, + COLOR_SCHEME_GREEN_ON_BLACK, + COLOR_SCHEME_BLACK_ON_LIGHT_YELLOW }; + +class QTermWidget : public QWidget +{ + Q_OBJECT +public: + + enum ScrollBarPosition + { + /** Do not show the scroll bar. */ + NoScrollBar=0, + /** Show the scroll bar on the left side of the display. */ + ScrollBarLeft=1, + /** Show the scroll bar on the right side of the display. */ + ScrollBarRight=2 + }; + + + //Creation of widget + QTermWidget(int startnow = 1, //start shell programm immediatelly + QWidget *parent = 0); + ~QTermWidget(); + + //start shell program if it was not started in constructor + void startShellProgram(); + + //look-n-feel, if you don`t like defaults + + // Terminal font + // Default is application font with family Monospace, size 10 + void setTerminalFont(QFont &font); + + // Shell program, default is /bin/bash + void setShellProgram(QString &progname); + + // Shell program args, default is none + void setArgs(QStringList &args); + + //Text codec, default is UTF-8 + void setTextCodec(QTextCodec *codec); + + //Color scheme, default is white on black + void setColorScheme(int scheme); + + //set size + void setSize(int h, int v); + + // History size for scrolling + void setHistorySize(int lines); //infinite if lines < 0 + + // Presence of scrollbar + void setScrollBarPosition(ScrollBarPosition); + + // Send some text to terminal + void sendText(QString &text); + +signals: + void finished(); + +protected: + virtual void resizeEvent(QResizeEvent *); + +protected slots: + void sessionFinished(); + +private: + void init(); + TermWidgetImpl *m_impl; +}; + + +//Maybe useful, maybe not + +#ifdef __cplusplus +extern "C" +#endif +void *createTermWidget(int startnow, void *parent); + +#endif +