diff options
119 files changed, 10052 insertions, 15631 deletions
diff --git a/.elpaignore b/.elpaignore new file mode 100644 index 0000000000..f0f644ee08 --- /dev/null +++ b/.elpaignore @@ -0,0 +1,2 @@ +LICENSE +README.md diff --git a/.gitignore b/.gitignore index 661df346c2..9e4b0ee5b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,3 @@ -# Files I don't care to see in git-status/commit -/cgit -cgit.conf -CGIT-CFLAGS -VERSION -cgitrc.5 -cgitrc.5.fo -cgitrc.5.html -cgitrc.5.pdf -cgitrc.5.xml -*.o -*.d +*.elc +*-pkg.el +*-autoloads.el diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 5c6ecb4f89..0000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "git"] - url = https://git.kernel.org/pub/scm/git/git.git - path = git diff --git a/.mailmap b/.mailmap deleted file mode 100644 index 03b54796cf..0000000000 --- a/.mailmap +++ /dev/null @@ -1,10 +0,0 @@ -Florian Pritz <bluewind@xinu.at> <bluewind@xssn.at> -Harley Laue <losinggeneration@gmail.com> <losinggeneration@aim.com> -John Keeping <john@keeping.me.uk> <john@metanate.com> -Lars Hjemli <hjemli@gmail.com> <larsh@hal-2004.(none)> -Lars Hjemli <hjemli@gmail.com> <larsh@hatman.(none)> -Lars Hjemli <hjemli@gmail.com> <larsh@slackbox.hjemli.net> -Lars Hjemli <hjemli@gmail.com> <larsh@slaptop.hjemli.net> -Lukas Fleischer <lfleischer@lfos.de> <cgit@cryptocrack.de> -Lukas Fleischer <lfleischer@lfos.de> <info@cryptocrack.de> -Stefan Bühler <source@stbuehler.de> <lighttpd@stbuehler.de> diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index 031de338f9..0000000000 --- a/AUTHORS +++ /dev/null @@ -1,13 +0,0 @@ -Maintainer: - Jason A. Donenfeld <Jason@zx2c4.com> - -Contributors: - Jason A. Donenfeld <Jason@zx2c4.com> - Lukas Fleischer <cgit@cryptocrack.de> - Johan Herland <johan@herland.net> - Lars Hjemli <hjemli@gmail.com> - Ferry Huberts <ferry.huberts@pelagic.nl> - John Keeping <john@keeping.me.uk> - -Previous Maintainer: - Lars Hjemli <hjemli@gmail.com> diff --git a/COPYING b/COPYING deleted file mode 100644 index d159169d10..0000000000 --- a/COPYING +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser 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., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - <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 Lesser General -Public License instead of this License. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..9cecc1d466 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. 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 +them 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 prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. 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. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state 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 3 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, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program 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, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU 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 Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/Makefile b/Makefile deleted file mode 100644 index 96ad7cd878..0000000000 --- a/Makefile +++ /dev/null @@ -1,170 +0,0 @@ -all:: - -CGIT_VERSION = v1.2.1 -CGIT_SCRIPT_NAME = cgit.cgi -CGIT_SCRIPT_PATH = /var/www/htdocs/cgit -CGIT_DATA_PATH = $(CGIT_SCRIPT_PATH) -CGIT_CONFIG = /etc/cgitrc -CACHE_ROOT = /var/cache/cgit -prefix = /usr/local -libdir = $(prefix)/lib -filterdir = $(libdir)/cgit/filters -docdir = $(prefix)/share/doc/cgit -htmldir = $(docdir) -pdfdir = $(docdir) -mandir = $(prefix)/share/man -SHA1_HEADER = <openssl/sha.h> -GIT_VER = 2.23.0 -GIT_URL = https://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.xz -INSTALL = install -COPYTREE = cp -r -MAN5_TXT = $(wildcard *.5.txt) -MAN_TXT = $(MAN5_TXT) -DOC_MAN5 = $(patsubst %.txt,%,$(MAN5_TXT)) -DOC_HTML = $(patsubst %.txt,%.html,$(MAN_TXT)) -DOC_PDF = $(patsubst %.txt,%.pdf,$(MAN_TXT)) - -ASCIIDOC = asciidoc -ASCIIDOC_EXTRA = -ASCIIDOC_HTML = xhtml11 -ASCIIDOC_COMMON = $(ASCIIDOC) $(ASCIIDOC_EXTRA) -TXT_TO_HTML = $(ASCIIDOC_COMMON) -b $(ASCIIDOC_HTML) - -# Define NO_C99_FORMAT if your formatted IO functions (printf/scanf et.al.) -# do not support the 'size specifiers' introduced by C99, namely ll, hh, -# j, z, t. (representing long long int, char, intmax_t, size_t, ptrdiff_t). -# some C compilers supported these specifiers prior to C99 as an extension. -# -# Define HAVE_LINUX_SENDFILE to use sendfile() - -#-include config.mak - --include git/config.mak.uname -# -# Let the user override the above settings. -# --include cgit.conf - -export CGIT_VERSION CGIT_SCRIPT_NAME CGIT_SCRIPT_PATH CGIT_DATA_PATH CGIT_CONFIG CACHE_ROOT - -# -# Define a way to invoke make in subdirs quietly, shamelessly ripped -# from git.git -# -QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir -QUIET_SUBDIR1 = - -ifneq ($(findstring w,$(MAKEFLAGS)),w) -PRINT_DIR = --no-print-directory -else # "make -w" -NO_SUBDIR = : -endif - -ifndef V - QUIET_SUBDIR0 = +@subdir= - QUIET_SUBDIR1 = ;$(NO_SUBDIR) echo ' ' SUBDIR $$subdir; \ - $(MAKE) $(PRINT_DIR) -C $$subdir - QUIET_TAGS = @echo ' ' TAGS $@; - export V -endif - -.SUFFIXES: - -all:: cgit - -cgit: - $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) -f ../cgit.mk ../cgit $(EXTRA_GIT_TARGETS) NO_CURL=1 - -sparse: - $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) -f ../cgit.mk NO_CURL=1 cgit-sparse - -test: - @$(MAKE) --no-print-directory cgit EXTRA_GIT_TARGETS=all - $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all - -install: all - $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_SCRIPT_PATH) - $(INSTALL) -m 0755 cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) - $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_DATA_PATH) - $(INSTALL) -m 0644 cgit.css $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css - $(INSTALL) -m 0644 cgit.png $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png - $(INSTALL) -m 0644 favicon.ico $(DESTDIR)$(CGIT_DATA_PATH)/favicon.ico - $(INSTALL) -m 0644 robots.txt $(DESTDIR)$(CGIT_DATA_PATH)/robots.txt - $(INSTALL) -m 0755 -d $(DESTDIR)$(filterdir) - $(COPYTREE) filters/* $(DESTDIR)$(filterdir) - -install-doc: install-man install-html install-pdf - -install-man: doc-man - $(INSTALL) -m 0755 -d $(DESTDIR)$(mandir)/man5 - $(INSTALL) -m 0644 $(DOC_MAN5) $(DESTDIR)$(mandir)/man5 - -install-html: doc-html - $(INSTALL) -m 0755 -d $(DESTDIR)$(htmldir) - $(INSTALL) -m 0644 $(DOC_HTML) $(DESTDIR)$(htmldir) - -install-pdf: doc-pdf - $(INSTALL) -m 0755 -d $(DESTDIR)$(pdfdir) - $(INSTALL) -m 0644 $(DOC_PDF) $(DESTDIR)$(pdfdir) - -uninstall: - rm -f $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) - rm -f $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css - rm -f $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png - rm -f $(DESTDIR)$(CGIT_DATA_PATH)/favicon.ico - -uninstall-doc: uninstall-man uninstall-html uninstall-pdf - -uninstall-man: - @for i in $(DOC_MAN5); do \ - rm -fv $(DESTDIR)$(mandir)/man5/$$i; \ - done - -uninstall-html: - @for i in $(DOC_HTML); do \ - rm -fv $(DESTDIR)$(htmldir)/$$i; \ - done - -uninstall-pdf: - @for i in $(DOC_PDF); do \ - rm -fv $(DESTDIR)$(pdfdir)/$$i; \ - done - -doc: doc-man doc-html doc-pdf -doc-man: doc-man5 -doc-man5: $(DOC_MAN5) -doc-html: $(DOC_HTML) -doc-pdf: $(DOC_PDF) - -%.5 : %.5.txt - a2x -f manpage $< - -$(DOC_HTML): %.html : %.txt - $(TXT_TO_HTML) -o $@+ $< && \ - mv $@+ $@ - -$(DOC_PDF): %.pdf : %.txt - a2x -f pdf cgitrc.5.txt - -clean: clean-doc - $(RM) cgit VERSION CGIT-CFLAGS *.o tags - $(RM) -r .deps - -cleanall: clean - $(MAKE) -C git clean - -clean-doc: - $(RM) cgitrc.5 cgitrc.5.html cgitrc.5.pdf cgitrc.5.xml cgitrc.5.fo - -get-git: - curl -L $(GIT_URL) | tar -xJf - && rm -rf git && mv git-$(GIT_VER) git - -tags: - $(QUIET_TAGS)find . -name '*.[ch]' | xargs ctags - -.PHONY: all cgit git get-git -.PHONY: clean clean-doc cleanall -.PHONY: doc doc-html doc-man doc-pdf -.PHONY: install install-doc install-html install-man install-pdf -.PHONY: tags test -.PHONY: uninstall uninstall-doc uninstall-html uninstall-man uninstall-pdf diff --git a/README b/README deleted file mode 100644 index 7a6b4a40ca..0000000000 --- a/README +++ /dev/null @@ -1,99 +0,0 @@ -cgit - CGI for Git -================== - -This is an attempt to create a fast web interface for the Git SCM, using a -built-in cache to decrease server I/O pressure. - -Installation ------------- - -Building cgit involves building a proper version of Git. How to do this -depends on how you obtained the cgit sources: - -a) If you're working in a cloned cgit repository, you first need to -initialize and update the Git submodule: - - $ git submodule init # register the Git submodule in .git/config - $ $EDITOR .git/config # if you want to specify a different url for git - $ git submodule update # clone/fetch and checkout correct git version - -b) If you're building from a cgit tarball, you can download a proper git -version like this: - - $ make get-git - -When either a) or b) has been performed, you can build and install cgit like -this: - - $ make - $ sudo make install - -This will install `cgit.cgi` and `cgit.css` into `/var/www/htdocs/cgit`. You -can configure this location (and a few other things) by providing a `cgit.conf` -file (see the Makefile for details). - -If you'd like to compile without Lua support, you may use: - - $ make NO_LUA=1 - -And if you'd like to specify a Lua implementation, you may use: - - $ make LUA_PKGCONFIG=lua5.1 - -If this is not specified, the Lua implementation will be auto-detected, -preferring LuaJIT if many are present. Acceptable values are generally "lua", -"luajit", "lua5.1", and "lua5.2". - - -Dependencies ------------- - -* libzip -* libcrypto (OpenSSL) -* libssl (OpenSSL) -* optional: luajit or lua, most reliably used when pkg-config is available - -Apache configuration --------------------- - -A new `Directory` section must probably be added for cgit, possibly something -like this: - - <Directory "/var/www/htdocs/cgit/"> - AllowOverride None - Options +ExecCGI - Order allow,deny - Allow from all - </Directory> - - -Runtime configuration ---------------------- - -The file `/etc/cgitrc` is read by cgit before handling a request. In addition -to runtime parameters, this file may also contain a list of repositories -displayed by cgit (see `cgitrc.5.txt` for further details). - -The cache ---------- - -When cgit is invoked it looks for a cache file matching the request and -returns it to the client. If no such cache file exists (or if it has expired), -the content for the request is written into the proper cache file before the -file is returned. - -If the cache file has expired but cgit is unable to obtain a lock for it, the -stale cache file is returned to the client. This is done to favour page -throughput over page freshness. - -The generated content contains the complete response to the client, including -the HTTP headers `Modified` and `Expires`. - -Online presence ---------------- - -* The cgit homepage is hosted by cgit at <https://git.zx2c4.com/cgit/about/> - -* Patches, bug reports, discussions and support should go to the cgit - mailing list: <cgit@lists.zx2c4.com>. To sign up, visit - <https://lists.zx2c4.com/mailman/listinfo/cgit> diff --git a/README.md b/README.md new file mode 100644 index 0000000000..58585cdce9 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Emacs X Window Manager + +EXWM (Emacs X Window Manager) is a full-featured tiling X window manager +for Emacs built on top of [XELB](https://github.com/emacs-exwm/xelb). + +It features: ++ Fully keyboard-driven operations ++ Hybrid layout modes (tiling & stacking) ++ Dynamic workspace support ++ ICCCM/EWMH compliance + +Optional features: ++ RandR (multi-monitor) support ++ System tray ++ Input method ++ Background setting support ++ XSETTINGS server + +Please check out the +[screenshots](https://github.com/emacs-exwm/exwm/wiki/Screenshots) +to get an overview of what EXWM is capable of, and the +[user guide](https://github.com/emacs-exwm/exwm/wiki) +for installation instructions and a detailed explanation of its usage. diff --git a/cache.c b/cache.c deleted file mode 100644 index 2c70be784d..0000000000 --- a/cache.c +++ /dev/null @@ -1,468 +0,0 @@ -/* cache.c: cache management - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - * - * - * The cache is just a directory structure where each file is a cache slot, - * and each filename is based on the hash of some key (e.g. the cgit url). - * Each file contains the full key followed by the cached content for that - * key. - * - */ - -#include "cgit.h" -#include "cache.h" -#include "html.h" -#ifdef HAVE_LINUX_SENDFILE -#include <sys/sendfile.h> -#endif - -#define CACHE_BUFSIZE (1024 * 4) - -struct cache_slot { - const char *key; - size_t keylen; - int ttl; - cache_fill_fn fn; - int cache_fd; - int lock_fd; - int stdout_fd; - const char *cache_name; - const char *lock_name; - int match; - struct stat cache_st; - int bufsize; - char buf[CACHE_BUFSIZE]; -}; - -/* Open an existing cache slot and fill the cache buffer with - * (part of) the content of the cache file. Return 0 on success - * and errno otherwise. - */ -static int open_slot(struct cache_slot *slot) -{ - char *bufz; - ssize_t bufkeylen = -1; - - slot->cache_fd = open(slot->cache_name, O_RDONLY); - if (slot->cache_fd == -1) - return errno; - - if (fstat(slot->cache_fd, &slot->cache_st)) - return errno; - - slot->bufsize = xread(slot->cache_fd, slot->buf, sizeof(slot->buf)); - if (slot->bufsize < 0) - return errno; - - bufz = memchr(slot->buf, 0, slot->bufsize); - if (bufz) - bufkeylen = bufz - slot->buf; - - if (slot->key) - slot->match = bufkeylen == slot->keylen && - !memcmp(slot->key, slot->buf, bufkeylen + 1); - - return 0; -} - -/* Close the active cache slot */ -static int close_slot(struct cache_slot *slot) -{ - int err = 0; - if (slot->cache_fd > 0) { - if (close(slot->cache_fd)) - err = errno; - else - slot->cache_fd = -1; - } - return err; -} - -/* Print the content of the active cache slot (but skip the key). */ -static int print_slot(struct cache_slot *slot) -{ -#ifdef HAVE_LINUX_SENDFILE - off_t start_off; - int ret; - - start_off = slot->keylen + 1; - - do { - ret = sendfile(STDOUT_FILENO, slot->cache_fd, &start_off, - slot->cache_st.st_size - start_off); - if (ret < 0) { - if (errno == EAGAIN || errno == EINTR) - continue; - return errno; - } - return 0; - } while (1); -#else - ssize_t i, j; - - i = lseek(slot->cache_fd, slot->keylen + 1, SEEK_SET); - if (i != slot->keylen + 1) - return errno; - - do { - i = j = xread(slot->cache_fd, slot->buf, sizeof(slot->buf)); - if (i > 0) - j = xwrite(STDOUT_FILENO, slot->buf, i); - } while (i > 0 && j == i); - - if (i < 0 || j != i) - return errno; - else - return 0; -#endif -} - -/* Check if the slot has expired */ -static int is_expired(struct cache_slot *slot) -{ - if (slot->ttl < 0) - return 0; - else - return slot->cache_st.st_mtime + slot->ttl * 60 < time(NULL); -} - -/* Check if the slot has been modified since we opened it. - * NB: If stat() fails, we pretend the file is modified. - */ -static int is_modified(struct cache_slot *slot) -{ - struct stat st; - - if (stat(slot->cache_name, &st)) - return 1; - return (st.st_ino != slot->cache_st.st_ino || - st.st_mtime != slot->cache_st.st_mtime || - st.st_size != slot->cache_st.st_size); -} - -/* Close an open lockfile */ -static int close_lock(struct cache_slot *slot) -{ - int err = 0; - if (slot->lock_fd > 0) { - if (close(slot->lock_fd)) - err = errno; - else - slot->lock_fd = -1; - } - return err; -} - -/* Create a lockfile used to store the generated content for a cache - * slot, and write the slot key + \0 into it. - * Returns 0 on success and errno otherwise. - */ -static int lock_slot(struct cache_slot *slot) -{ - struct flock lock = { - .l_type = F_WRLCK, - .l_whence = SEEK_SET, - .l_start = 0, - .l_len = 0, - }; - - slot->lock_fd = open(slot->lock_name, O_RDWR | O_CREAT, - S_IRUSR | S_IWUSR); - if (slot->lock_fd == -1) - return errno; - if (fcntl(slot->lock_fd, F_SETLK, &lock) < 0) { - int saved_errno = errno; - close(slot->lock_fd); - slot->lock_fd = -1; - return saved_errno; - } - if (xwrite(slot->lock_fd, slot->key, slot->keylen + 1) < 0) - return errno; - return 0; -} - -/* Release the current lockfile. If `replace_old_slot` is set the - * lockfile replaces the old cache slot, otherwise the lockfile is - * just deleted. - */ -static int unlock_slot(struct cache_slot *slot, int replace_old_slot) -{ - int err; - - if (replace_old_slot) - err = rename(slot->lock_name, slot->cache_name); - else - err = unlink(slot->lock_name); - - /* Restore stdout and close the temporary FD. */ - if (slot->stdout_fd >= 0) { - dup2(slot->stdout_fd, STDOUT_FILENO); - close(slot->stdout_fd); - slot->stdout_fd = -1; - } - - if (err) - return errno; - - return 0; -} - -/* Generate the content for the current cache slot by redirecting - * stdout to the lock-fd and invoking the callback function - */ -static int fill_slot(struct cache_slot *slot) -{ - /* Preserve stdout */ - slot->stdout_fd = dup(STDOUT_FILENO); - if (slot->stdout_fd == -1) - return errno; - - /* Redirect stdout to lockfile */ - if (dup2(slot->lock_fd, STDOUT_FILENO) == -1) - return errno; - - /* Generate cache content */ - slot->fn(); - - /* Make sure any buffered data is flushed to the file */ - if (fflush(stdout)) - return errno; - - /* update stat info */ - if (fstat(slot->lock_fd, &slot->cache_st)) - return errno; - - return 0; -} - -/* Crude implementation of 32-bit FNV-1 hash algorithm, - * see http://www.isthe.com/chongo/tech/comp/fnv/ for details - * about the magic numbers. - */ -#define FNV_OFFSET 0x811c9dc5 -#define FNV_PRIME 0x01000193 - -unsigned long hash_str(const char *str) -{ - unsigned long h = FNV_OFFSET; - unsigned char *s = (unsigned char *)str; - - if (!s) - return h; - - while (*s) { - h *= FNV_PRIME; - h ^= *s++; - } - return h; -} - -static int process_slot(struct cache_slot *slot) -{ - int err; - - err = open_slot(slot); - if (!err && slot->match) { - if (is_expired(slot)) { - if (!lock_slot(slot)) { - /* If the cachefile has been replaced between - * `open_slot` and `lock_slot`, we'll just - * serve the stale content from the original - * cachefile. This way we avoid pruning the - * newly generated slot. The same code-path - * is chosen if fill_slot() fails for some - * reason. - * - * TODO? check if the new slot contains the - * same key as the old one, since we would - * prefer to serve the newest content. - * This will require us to open yet another - * file-descriptor and read and compare the - * key from the new file, so for now we're - * lazy and just ignore the new file. - */ - if (is_modified(slot) || fill_slot(slot)) { - unlock_slot(slot, 0); - close_lock(slot); - } else { - close_slot(slot); - unlock_slot(slot, 1); - slot->cache_fd = slot->lock_fd; - } - } - } - if ((err = print_slot(slot)) != 0) { - cache_log("[cgit] error printing cache %s: %s (%d)\n", - slot->cache_name, - strerror(err), - err); - } - close_slot(slot); - return err; - } - - /* If the cache slot does not exist (or its key doesn't match the - * current key), lets try to create a new cache slot for this - * request. If this fails (for whatever reason), lets just generate - * the content without caching it and fool the caller to believe - * everything worked out (but print a warning on stdout). - */ - - close_slot(slot); - if ((err = lock_slot(slot)) != 0) { - cache_log("[cgit] Unable to lock slot %s: %s (%d)\n", - slot->lock_name, strerror(err), err); - slot->fn(); - return 0; - } - - if ((err = fill_slot(slot)) != 0) { - cache_log("[cgit] Unable to fill slot %s: %s (%d)\n", - slot->lock_name, strerror(err), err); - unlock_slot(slot, 0); - close_lock(slot); - slot->fn(); - return 0; - } - // We've got a valid cache slot in the lock file, which - // is about to replace the old cache slot. But if we - // release the lockfile and then try to open the new cache - // slot, we might get a race condition with a concurrent - // writer for the same cache slot (with a different key). - // Lets avoid such a race by just printing the content of - // the lock file. - slot->cache_fd = slot->lock_fd; - unlock_slot(slot, 1); - if ((err = print_slot(slot)) != 0) { - cache_log("[cgit] error printing cache %s: %s (%d)\n", - slot->cache_name, - strerror(err), - err); - } - close_slot(slot); - return err; -} - -/* Print cached content to stdout, generate the content if necessary. */ -int cache_process(int size, const char *path, const char *key, int ttl, - cache_fill_fn fn) -{ - unsigned long hash; - int i; - struct strbuf filename = STRBUF_INIT; - struct strbuf lockname = STRBUF_INIT; - struct cache_slot slot; - int result; - - /* If the cache is disabled, just generate the content */ - if (size <= 0 || ttl == 0) { - fn(); - return 0; - } - - /* Verify input, calculate filenames */ - if (!path) { - cache_log("[cgit] Cache path not specified, caching is disabled\n"); - fn(); - return 0; - } - if (!key) - key = ""; - hash = hash_str(key) % size; - strbuf_addstr(&filename, path); - strbuf_ensure_end(&filename, '/'); - for (i = 0; i < 8; i++) { - strbuf_addf(&filename, "%x", (unsigned char)(hash & 0xf)); - hash >>= 4; - } - strbuf_addbuf(&lockname, &filename); - strbuf_addstr(&lockname, ".lock"); - slot.fn = fn; - slot.ttl = ttl; - slot.stdout_fd = -1; - slot.cache_name = filename.buf; - slot.lock_name = lockname.buf; - slot.key = key; - slot.keylen = strlen(key); - result = process_slot(&slot); - - strbuf_release(&filename); - strbuf_release(&lockname); - return result; -} - -/* Return a strftime formatted date/time - * NB: the result from this function is to shared memory - */ -static char *sprintftime(const char *format, time_t time) -{ - static char buf[64]; - struct tm *tm; - - if (!time) - return NULL; - tm = gmtime(&time); - strftime(buf, sizeof(buf)-1, format, tm); - return buf; -} - -int cache_ls(const char *path) -{ - DIR *dir; - struct dirent *ent; - int err = 0; - struct cache_slot slot = { NULL }; - struct strbuf fullname = STRBUF_INIT; - size_t prefixlen; - - if (!path) { - cache_log("[cgit] cache path not specified\n"); - return -1; - } - dir = opendir(path); - if (!dir) { - err = errno; - cache_log("[cgit] unable to open path %s: %s (%d)\n", - path, strerror(err), err); - return err; - } - strbuf_addstr(&fullname, path); - strbuf_ensure_end(&fullname, '/'); - prefixlen = fullname.len; - while ((ent = readdir(dir)) != NULL) { - if (strlen(ent->d_name) != 8) - continue; - strbuf_setlen(&fullname, prefixlen); - strbuf_addstr(&fullname, ent->d_name); - slot.cache_name = fullname.buf; - if ((err = open_slot(&slot)) != 0) { - cache_log("[cgit] unable to open path %s: %s (%d)\n", - fullname.buf, strerror(err), err); - continue; - } - htmlf("%s %s %10"PRIuMAX" %s\n", - fullname.buf, - sprintftime("%Y-%m-%d %H:%M:%S", - slot.cache_st.st_mtime), - (uintmax_t)slot.cache_st.st_size, - slot.buf); - close_slot(&slot); - } - closedir(dir); - strbuf_release(&fullname); - return 0; -} - -/* Print a message to stdout */ -void cache_log(const char *format, ...) -{ - va_list args; - va_start(args, format); - vfprintf(stderr, format, args); - va_end(args); -} - diff --git a/cache.h b/cache.h deleted file mode 100644 index 470da4fc15..0000000000 --- a/cache.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Since git has it's own cache.h which we include, - * lets test on CGIT_CACHE_H to avoid confusion - */ - -#ifndef CGIT_CACHE_H -#define CGIT_CACHE_H - -typedef void (*cache_fill_fn)(void); - - -/* Print cached content to stdout, generate the content if necessary. - * - * Parameters - * size max number of cache files - * path directory used to store cache files - * key the key used to lookup cache files - * ttl max cache time in seconds for this key - * fn content generator function for this key - * - * Return value - * 0 indicates success, everything else is an error - */ -extern int cache_process(int size, const char *path, const char *key, int ttl, - cache_fill_fn fn); - - -/* List info about all cache entries on stdout */ -extern int cache_ls(const char *path); - -/* Print a message to stdout */ -__attribute__((format (printf,1,2))) -extern void cache_log(const char *format, ...); - -extern unsigned long hash_str(const char *str); - -#endif /* CGIT_CACHE_H */ diff --git a/cgit.c b/cgit.c deleted file mode 100644 index ac8c6418ba..0000000000 --- a/cgit.c +++ /dev/null @@ -1,1112 +0,0 @@ -/* cgit.c: cgi for the git scm - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "cache.h" -#include "cmd.h" -#include "configfile.h" -#include "html.h" -#include "ui-shared.h" -#include "ui-stats.h" -#include "ui-blob.h" -#include "ui-summary.h" -#include "scan-tree.h" - -const char *cgit_version = CGIT_VERSION; - -__attribute__((constructor)) -static void constructor_environment() -{ - /* Do not look in /etc/ for gitconfig and gitattributes. */ - setenv("GIT_CONFIG_NOSYSTEM", "1", 1); - setenv("GIT_ATTR_NOSYSTEM", "1", 1); - unsetenv("HOME"); - unsetenv("XDG_CONFIG_HOME"); -} - -static void add_mimetype(const char *name, const char *value) -{ - struct string_list_item *item; - - item = string_list_insert(&ctx.cfg.mimetypes, name); - item->util = xstrdup(value); -} - -static void process_cached_repolist(const char *path); - -static void repo_config(struct cgit_repo *repo, const char *name, const char *value) -{ - const char *path; - struct string_list_item *item; - - if (!strcmp(name, "name")) - repo->name = xstrdup(value); - else if (!strcmp(name, "clone-url")) - repo->clone_url = xstrdup(value); - else if (!strcmp(name, "desc")) - repo->desc = xstrdup(value); - else if (!strcmp(name, "owner")) - repo->owner = xstrdup(value); - else if (!strcmp(name, "homepage")) - repo->homepage = xstrdup(value); - else if (!strcmp(name, "defbranch")) - repo->defbranch = xstrdup(value); - else if (!strcmp(name, "extra-head-content")) - repo->extra_head_content = xstrdup(value); - else if (!strcmp(name, "snapshots")) - repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); - else if (!strcmp(name, "enable-blame")) - repo->enable_blame = atoi(value); - else if (!strcmp(name, "enable-commit-graph")) - repo->enable_commit_graph = atoi(value); - else if (!strcmp(name, "enable-log-filecount")) - repo->enable_log_filecount = atoi(value); - else if (!strcmp(name, "enable-log-linecount")) - repo->enable_log_linecount = atoi(value); - else if (!strcmp(name, "enable-remote-branches")) - repo->enable_remote_branches = atoi(value); - else if (!strcmp(name, "enable-subject-links")) - repo->enable_subject_links = atoi(value); - else if (!strcmp(name, "enable-html-serving")) - repo->enable_html_serving = atoi(value); - else if (!strcmp(name, "branch-sort")) { - if (!strcmp(value, "age")) - repo->branch_sort = 1; - if (!strcmp(value, "name")) - repo->branch_sort = 0; - } else if (!strcmp(name, "commit-sort")) { - if (!strcmp(value, "date")) - repo->commit_sort = 1; - if (!strcmp(value, "topo")) - repo->commit_sort = 2; - } else if (!strcmp(name, "max-stats")) - repo->max_stats = cgit_find_stats_period(value, NULL); - else if (!strcmp(name, "module-link")) - repo->module_link= xstrdup(value); - else if (skip_prefix(name, "module-link.", &path)) { - item = string_list_append(&repo->submodules, xstrdup(path)); - item->util = xstrdup(value); - } else if (!strcmp(name, "section")) - repo->section = xstrdup(value); - else if (!strcmp(name, "snapshot-prefix")) - repo->snapshot_prefix = xstrdup(value); - else if (!strcmp(name, "readme") && value != NULL) { - if (repo->readme.items == ctx.cfg.readme.items) - memset(&repo->readme, 0, sizeof(repo->readme)); - string_list_append(&repo->readme, xstrdup(value)); - } else if (!strcmp(name, "logo") && value != NULL) - repo->logo = xstrdup(value); - else if (!strcmp(name, "logo-link") && value != NULL) - repo->logo_link = xstrdup(value); - else if (!strcmp(name, "hide")) - repo->hide = atoi(value); - else if (!strcmp(name, "ignore")) - repo->ignore = atoi(value); - else if (ctx.cfg.enable_filter_overrides) { - if (!strcmp(name, "about-filter")) - repo->about_filter = cgit_new_filter(value, ABOUT); - else if (!strcmp(name, "commit-filter")) - repo->commit_filter = cgit_new_filter(value, COMMIT); - else if (!strcmp(name, "source-filter")) - repo->source_filter = cgit_new_filter(value, SOURCE); - else if (!strcmp(name, "email-filter")) - repo->email_filter = cgit_new_filter(value, EMAIL); - else if (!strcmp(name, "owner-filter")) - repo->owner_filter = cgit_new_filter(value, OWNER); - } -} - -static void config_cb(const char *name, const char *value) -{ - const char *arg; - - if (!strcmp(name, "section")) - ctx.cfg.section = xstrdup(value); - else if (!strcmp(name, "repo.url")) - ctx.repo = cgit_add_repo(value); - else if (ctx.repo && !strcmp(name, "repo.path")) - ctx.repo->path = trim_end(value, '/'); - else if (ctx.repo && skip_prefix(name, "repo.", &arg)) - repo_config(ctx.repo, arg, value); - else if (!strcmp(name, "readme")) - string_list_append(&ctx.cfg.readme, xstrdup(value)); - else if (!strcmp(name, "root-title")) - ctx.cfg.root_title = xstrdup(value); - else if (!strcmp(name, "root-desc")) - ctx.cfg.root_desc = xstrdup(value); - else if (!strcmp(name, "root-readme")) - ctx.cfg.root_readme = xstrdup(value); - else if (!strcmp(name, "css")) - ctx.cfg.css = xstrdup(value); - else if (!strcmp(name, "favicon")) - ctx.cfg.favicon = xstrdup(value); - else if (!strcmp(name, "footer")) - ctx.cfg.footer = xstrdup(value); - else if (!strcmp(name, "head-include")) - ctx.cfg.head_include = xstrdup(value); - else if (!strcmp(name, "header")) - ctx.cfg.header = xstrdup(value); - else if (!strcmp(name, "logo")) - ctx.cfg.logo = xstrdup(value); - else if (!strcmp(name, "logo-link")) - ctx.cfg.logo_link = xstrdup(value); - else if (!strcmp(name, "module-link")) - ctx.cfg.module_link = xstrdup(value); - else if (!strcmp(name, "strict-export")) - ctx.cfg.strict_export = xstrdup(value); - else if (!strcmp(name, "virtual-root")) - ctx.cfg.virtual_root = ensure_end(value, '/'); - else if (!strcmp(name, "noplainemail")) - ctx.cfg.noplainemail = atoi(value); - else if (!strcmp(name, "noheader")) - ctx.cfg.noheader = atoi(value); - else if (!strcmp(name, "snapshots")) - ctx.cfg.snapshots = cgit_parse_snapshots_mask(value); - else if (!strcmp(name, "enable-filter-overrides")) - ctx.cfg.enable_filter_overrides = atoi(value); - else if (!strcmp(name, "enable-follow-links")) - ctx.cfg.enable_follow_links = atoi(value); - else if (!strcmp(name, "enable-http-clone")) - ctx.cfg.enable_http_clone = atoi(value); - else if (!strcmp(name, "enable-index-links")) - ctx.cfg.enable_index_links = atoi(value); - else if (!strcmp(name, "enable-index-owner")) - ctx.cfg.enable_index_owner = atoi(value); - else if (!strcmp(name, "enable-blame")) - ctx.cfg.enable_blame = atoi(value); - else if (!strcmp(name, "enable-commit-graph")) - ctx.cfg.enable_commit_graph = atoi(value); - else if (!strcmp(name, "enable-log-filecount")) - ctx.cfg.enable_log_filecount = atoi(value); - else if (!strcmp(name, "enable-log-linecount")) - ctx.cfg.enable_log_linecount = atoi(value); - else if (!strcmp(name, "enable-remote-branches")) - ctx.cfg.enable_remote_branches = atoi(value); - else if (!strcmp(name, "enable-subject-links")) - ctx.cfg.enable_subject_links = atoi(value); - else if (!strcmp(name, "enable-html-serving")) - ctx.cfg.enable_html_serving = atoi(value); - else if (!strcmp(name, "enable-tree-linenumbers")) - ctx.cfg.enable_tree_linenumbers = atoi(value); - else if (!strcmp(name, "enable-git-config")) - ctx.cfg.enable_git_config = atoi(value); - else if (!strcmp(name, "max-stats")) - ctx.cfg.max_stats = cgit_find_stats_period(value, NULL); - else if (!strcmp(name, "cache-size")) - ctx.cfg.cache_size = atoi(value); - else if (!strcmp(name, "cache-root")) - ctx.cfg.cache_root = xstrdup(expand_macros(value)); - else if (!strcmp(name, "cache-root-ttl")) - ctx.cfg.cache_root_ttl = atoi(value); - else if (!strcmp(name, "cache-repo-ttl")) - ctx.cfg.cache_repo_ttl = atoi(value); - else if (!strcmp(name, "cache-scanrc-ttl")) - ctx.cfg.cache_scanrc_ttl = atoi(value); - else if (!strcmp(name, "cache-static-ttl")) - ctx.cfg.cache_static_ttl = atoi(value); - else if (!strcmp(name, "cache-dynamic-ttl")) - ctx.cfg.cache_dynamic_ttl = atoi(value); - else if (!strcmp(name, "cache-about-ttl")) - ctx.cfg.cache_about_ttl = atoi(value); - else if (!strcmp(name, "cache-snapshot-ttl")) - ctx.cfg.cache_snapshot_ttl = atoi(value); - else if (!strcmp(name, "case-sensitive-sort")) - ctx.cfg.case_sensitive_sort = atoi(value); - else if (!strcmp(name, "about-filter")) - ctx.cfg.about_filter = cgit_new_filter(value, ABOUT); - else if (!strcmp(name, "commit-filter")) - ctx.cfg.commit_filter = cgit_new_filter(value, COMMIT); - else if (!strcmp(name, "email-filter")) - ctx.cfg.email_filter = cgit_new_filter(value, EMAIL); - else if (!strcmp(name, "owner-filter")) - ctx.cfg.owner_filter = cgit_new_filter(value, OWNER); - else if (!strcmp(name, "auth-filter")) - ctx.cfg.auth_filter = cgit_new_filter(value, AUTH); - else if (!strcmp(name, "embedded")) - ctx.cfg.embedded = atoi(value); - else if (!strcmp(name, "max-atom-items")) - ctx.cfg.max_atom_items = atoi(value); - else if (!strcmp(name, "max-message-length")) - ctx.cfg.max_msg_len = atoi(value); - else if (!strcmp(name, "max-repodesc-length")) - ctx.cfg.max_repodesc_len = atoi(value); - else if (!strcmp(name, "max-blob-size")) - ctx.cfg.max_blob_size = atoi(value); - else if (!strcmp(name, "max-repo-count")) - ctx.cfg.max_repo_count = atoi(value); - else if (!strcmp(name, "max-commit-count")) - ctx.cfg.max_commit_count = atoi(value); - else if (!strcmp(name, "project-list")) - ctx.cfg.project_list = xstrdup(expand_macros(value)); - else if (!strcmp(name, "scan-path")) - if (ctx.cfg.cache_size) - process_cached_repolist(expand_macros(value)); - else if (ctx.cfg.project_list) - scan_projects(expand_macros(value), - ctx.cfg.project_list, repo_config); - else - scan_tree(expand_macros(value), repo_config); - else if (!strcmp(name, "scan-hidden-path")) - ctx.cfg.scan_hidden_path = atoi(value); - else if (!strcmp(name, "section-from-path")) - ctx.cfg.section_from_path = atoi(value); - else if (!strcmp(name, "repository-sort")) - ctx.cfg.repository_sort = xstrdup(value); - else if (!strcmp(name, "section-sort")) - ctx.cfg.section_sort = atoi(value); - else if (!strcmp(name, "source-filter")) - ctx.cfg.source_filter = cgit_new_filter(value, SOURCE); - else if (!strcmp(name, "summary-log")) - ctx.cfg.summary_log = atoi(value); - else if (!strcmp(name, "summary-branches")) - ctx.cfg.summary_branches = atoi(value); - else if (!strcmp(name, "summary-tags")) - ctx.cfg.summary_tags = atoi(value); - else if (!strcmp(name, "side-by-side-diffs")) - ctx.cfg.difftype = atoi(value) ? DIFF_SSDIFF : DIFF_UNIFIED; - else if (!strcmp(name, "agefile")) - ctx.cfg.agefile = xstrdup(value); - else if (!strcmp(name, "mimetype-file")) - ctx.cfg.mimetype_file = xstrdup(value); - else if (!strcmp(name, "renamelimit")) - ctx.cfg.renamelimit = atoi(value); - else if (!strcmp(name, "remove-suffix")) - ctx.cfg.remove_suffix = atoi(value); - else if (!strcmp(name, "robots")) - ctx.cfg.robots = xstrdup(value); - else if (!strcmp(name, "clone-prefix")) - ctx.cfg.clone_prefix = xstrdup(value); - else if (!strcmp(name, "clone-url")) - ctx.cfg.clone_url = xstrdup(value); - else if (!strcmp(name, "local-time")) - ctx.cfg.local_time = atoi(value); - else if (!strcmp(name, "commit-sort")) { - if (!strcmp(value, "date")) - ctx.cfg.commit_sort = 1; - if (!strcmp(value, "topo")) - ctx.cfg.commit_sort = 2; - } else if (!strcmp(name, "branch-sort")) { - if (!strcmp(value, "age")) - ctx.cfg.branch_sort = 1; - if (!strcmp(value, "name")) - ctx.cfg.branch_sort = 0; - } else if (skip_prefix(name, "mimetype.", &arg)) - add_mimetype(arg, value); - else if (!strcmp(name, "include")) - parse_configfile(expand_macros(value), config_cb); -} - -static void querystring_cb(const char *name, const char *value) -{ - if (!value) - value = ""; - - if (!strcmp(name,"r")) { - ctx.qry.repo = xstrdup(value); - ctx.repo = cgit_get_repoinfo(value); - } else if (!strcmp(name, "p")) { - ctx.qry.page = xstrdup(value); - } else if (!strcmp(name, "url")) { - if (*value == '/') - value++; - ctx.qry.url = xstrdup(value); - cgit_parse_url(value); - } else if (!strcmp(name, "qt")) { - ctx.qry.grep = xstrdup(value); - } else if (!strcmp(name, "q")) { - ctx.qry.search = xstrdup(value); - } else if (!strcmp(name, "h")) { - ctx.qry.head = xstrdup(value); - ctx.qry.has_symref = 1; - } else if (!strcmp(name, "id")) { - ctx.qry.sha1 = xstrdup(value); - ctx.qry.has_sha1 = 1; - } else if (!strcmp(name, "id2")) { - ctx.qry.sha2 = xstrdup(value); - ctx.qry.has_sha1 = 1; - } else if (!strcmp(name, "ofs")) { - ctx.qry.ofs = atoi(value); - } else if (!strcmp(name, "path")) { - ctx.qry.path = trim_end(value, '/'); - } else if (!strcmp(name, "name")) { - ctx.qry.name = xstrdup(value); - } else if (!strcmp(name, "s")) { - ctx.qry.sort = xstrdup(value); - } else if (!strcmp(name, "showmsg")) { - ctx.qry.showmsg = atoi(value); - } else if (!strcmp(name, "period")) { - ctx.qry.period = xstrdup(value); - } else if (!strcmp(name, "dt")) { - ctx.qry.difftype = atoi(value); - ctx.qry.has_difftype = 1; - } else if (!strcmp(name, "ss")) { - /* No longer generated, but there may be links out there. */ - ctx.qry.difftype = atoi(value) ? DIFF_SSDIFF : DIFF_UNIFIED; - ctx.qry.has_difftype = 1; - } else if (!strcmp(name, "all")) { - ctx.qry.show_all = atoi(value); - } else if (!strcmp(name, "context")) { - ctx.qry.context = atoi(value); - } else if (!strcmp(name, "ignorews")) { - ctx.qry.ignorews = atoi(value); - } else if (!strcmp(name, "follow")) { - ctx.qry.follow = atoi(value); - } -} - -static void prepare_context(void) -{ - memset(&ctx, 0, sizeof(ctx)); - ctx.cfg.agefile = "info/web/last-modified"; - ctx.cfg.cache_size = 0; - ctx.cfg.cache_max_create_time = 5; - ctx.cfg.cache_root = CGIT_CACHE_ROOT; - ctx.cfg.cache_about_ttl = 15; - ctx.cfg.cache_snapshot_ttl = 5; - ctx.cfg.cache_repo_ttl = 5; - ctx.cfg.cache_root_ttl = 5; - ctx.cfg.cache_scanrc_ttl = 15; - ctx.cfg.cache_dynamic_ttl = 5; - ctx.cfg.cache_static_ttl = -1; - ctx.cfg.case_sensitive_sort = 1; - ctx.cfg.branch_sort = 0; - ctx.cfg.commit_sort = 0; - ctx.cfg.css = "/cgit.css"; - ctx.cfg.logo = "/cgit.png"; - ctx.cfg.favicon = "/favicon.ico"; - ctx.cfg.local_time = 0; - ctx.cfg.enable_http_clone = 1; - ctx.cfg.enable_index_owner = 1; - ctx.cfg.enable_tree_linenumbers = 1; - ctx.cfg.enable_git_config = 0; - ctx.cfg.max_repo_count = 50; - ctx.cfg.max_commit_count = 50; - ctx.cfg.max_lock_attempts = 5; - ctx.cfg.max_msg_len = 80; - ctx.cfg.max_repodesc_len = 80; - ctx.cfg.max_blob_size = 0; - ctx.cfg.max_stats = 0; - ctx.cfg.project_list = NULL; - ctx.cfg.renamelimit = -1; - ctx.cfg.remove_suffix = 0; - ctx.cfg.robots = "index, nofollow"; - ctx.cfg.root_title = "Git repository browser"; - ctx.cfg.root_desc = "a fast webinterface for the git dscm"; - ctx.cfg.scan_hidden_path = 0; - ctx.cfg.script_name = CGIT_SCRIPT_NAME; - ctx.cfg.section = ""; - ctx.cfg.repository_sort = "name"; - ctx.cfg.section_sort = 1; - ctx.cfg.summary_branches = 10; - ctx.cfg.summary_log = 10; - ctx.cfg.summary_tags = 10; - ctx.cfg.max_atom_items = 10; - ctx.cfg.difftype = DIFF_UNIFIED; - ctx.env.cgit_config = getenv("CGIT_CONFIG"); - ctx.env.http_host = getenv("HTTP_HOST"); - ctx.env.https = getenv("HTTPS"); - ctx.env.no_http = getenv("NO_HTTP"); - ctx.env.path_info = getenv("PATH_INFO"); - ctx.env.query_string = getenv("QUERY_STRING"); - ctx.env.request_method = getenv("REQUEST_METHOD"); - ctx.env.script_name = getenv("SCRIPT_NAME"); - ctx.env.server_name = getenv("SERVER_NAME"); - ctx.env.server_port = getenv("SERVER_PORT"); - ctx.env.http_cookie = getenv("HTTP_COOKIE"); - ctx.env.http_referer = getenv("HTTP_REFERER"); - ctx.env.content_length = getenv("CONTENT_LENGTH") ? strtoul(getenv("CONTENT_LENGTH"), NULL, 10) : 0; - ctx.env.authenticated = 0; - ctx.page.mimetype = "text/html"; - ctx.page.charset = PAGE_ENCODING; - ctx.page.filename = NULL; - ctx.page.size = 0; - ctx.page.modified = time(NULL); - ctx.page.expires = ctx.page.modified; - ctx.page.etag = NULL; - string_list_init(&ctx.cfg.mimetypes, 1); - if (ctx.env.script_name) - ctx.cfg.script_name = xstrdup(ctx.env.script_name); - if (ctx.env.query_string) - ctx.qry.raw = xstrdup(ctx.env.query_string); - if (!ctx.env.cgit_config) - ctx.env.cgit_config = CGIT_CONFIG; -} - -struct refmatch { - char *req_ref; - char *first_ref; - int match; -}; - -static int find_current_ref(const char *refname, const struct object_id *oid, - int flags, void *cb_data) -{ - struct refmatch *info; - - info = (struct refmatch *)cb_data; - if (!strcmp(refname, info->req_ref)) - info->match = 1; - if (!info->first_ref) - info->first_ref = xstrdup(refname); - return info->match; -} - -static void free_refmatch_inner(struct refmatch *info) -{ - if (info->first_ref) - free(info->first_ref); -} - -static char *find_default_branch(struct cgit_repo *repo) -{ - struct refmatch info; - char *ref; - - info.req_ref = repo->defbranch; - info.first_ref = NULL; - info.match = 0; - for_each_branch_ref(find_current_ref, &info); - if (info.match) - ref = info.req_ref; - else - ref = info.first_ref; - if (ref) - ref = xstrdup(ref); - free_refmatch_inner(&info); - - return ref; -} - -static char *guess_defbranch(void) -{ - const char *ref, *refname; - struct object_id oid; - - ref = resolve_ref_unsafe("HEAD", 0, &oid, NULL); - if (!ref || !skip_prefix(ref, "refs/heads/", &refname)) - return "master"; - return xstrdup(refname); -} - -/* The caller must free filename and ref after calling this. */ -static inline void parse_readme(const char *readme, char **filename, char **ref, struct cgit_repo *repo) -{ - const char *colon; - - *filename = NULL; - *ref = NULL; - - if (!readme || !readme[0]) - return; - - /* Check if the readme is tracked in the git repo. */ - colon = strchr(readme, ':'); - if (colon && strlen(colon) > 1) { - /* If it starts with a colon, we want to use - * the default branch */ - if (colon == readme && repo->defbranch) - *ref = xstrdup(repo->defbranch); - else - *ref = xstrndup(readme, colon - readme); - readme = colon + 1; - } - - /* Prepend repo path to relative readme path unless tracked. */ - if (!(*ref) && readme[0] != '/') - *filename = fmtalloc("%s/%s", repo->path, readme); - else - *filename = xstrdup(readme); -} -static void choose_readme(struct cgit_repo *repo) -{ - int found; - char *filename, *ref; - struct string_list_item *entry; - - if (!repo->readme.nr) - return; - - found = 0; - for_each_string_list_item(entry, &repo->readme) { - parse_readme(entry->string, &filename, &ref, repo); - if (!filename) { - free(filename); - free(ref); - continue; - } - if (ref) { - if (cgit_ref_path_exists(filename, ref, 1)) { - found = 1; - break; - } - } - else if (!access(filename, R_OK)) { - found = 1; - break; - } - free(filename); - free(ref); - } - repo->readme.strdup_strings = 1; - string_list_clear(&repo->readme, 0); - repo->readme.strdup_strings = 0; - if (found) - string_list_append(&repo->readme, filename)->util = ref; -} - -static void print_no_repo_clone_urls(const char *url) -{ - html("<tr><td><a rel='vcs-git' href='"); - html_url_path(url); - html("' title='"); - html_attr(ctx.repo->name); - html(" Git repository'>"); - html_txt(url); - html("</a></td></tr>\n"); -} - -static void prepare_repo_env(int *nongit) -{ - /* The path to the git repository. */ - setenv("GIT_DIR", ctx.repo->path, 1); - - /* Setup the git directory and initialize the notes system. Both of these - * load local configuration from the git repository, so we do them both while - * the HOME variables are unset. */ - setup_git_directory_gently(nongit); - init_display_notes(NULL); -} - -static int prepare_repo_cmd(int nongit) -{ - struct object_id oid; - int rc; - - if (nongit) { - const char *name = ctx.repo->name; - rc = errno; - ctx.page.title = fmtalloc("%s - %s", ctx.cfg.root_title, - "config error"); - ctx.repo = NULL; - cgit_print_http_headers(); - cgit_print_docstart(); - cgit_print_pageheader(); - cgit_print_error("Failed to open %s: %s", name, - rc ? strerror(rc) : "Not a valid git repository"); - cgit_print_docend(); - return 1; - } - ctx.page.title = fmtalloc("%s - %s", ctx.repo->name, ctx.repo->desc); - - if (!ctx.repo->defbranch) - ctx.repo->defbranch = guess_defbranch(); - - if (!ctx.qry.head) { - ctx.qry.nohead = 1; - ctx.qry.head = find_default_branch(ctx.repo); - } - - if (!ctx.qry.head) { - cgit_print_http_headers(); - cgit_print_docstart(); - cgit_print_pageheader(); - cgit_print_error("Repository seems to be empty"); - if (!strcmp(ctx.qry.page, "summary")) { - html("<table class='list'><tr class='nohover'><td> </td></tr><tr class='nohover'><th class='left'>Clone</th></tr>\n"); - cgit_prepare_repo_env(ctx.repo); - cgit_add_clone_urls(print_no_repo_clone_urls); - html("</table>\n"); - } - cgit_print_docend(); - return 1; - } - - if (get_oid(ctx.qry.head, &oid)) { - char *old_head = ctx.qry.head; - ctx.qry.head = xstrdup(ctx.repo->defbranch); - cgit_print_error_page(404, "Not found", - "Invalid branch: %s", old_head); - free(old_head); - return 1; - } - string_list_sort(&ctx.repo->submodules); - cgit_prepare_repo_env(ctx.repo); - choose_readme(ctx.repo); - return 0; -} - -static inline void open_auth_filter(const char *function) -{ - cgit_open_filter(ctx.cfg.auth_filter, function, - ctx.env.http_cookie ? ctx.env.http_cookie : "", - ctx.env.request_method ? ctx.env.request_method : "", - ctx.env.query_string ? ctx.env.query_string : "", - ctx.env.http_referer ? ctx.env.http_referer : "", - ctx.env.path_info ? ctx.env.path_info : "", - ctx.env.http_host ? ctx.env.http_host : "", - ctx.env.https ? ctx.env.https : "", - ctx.qry.repo ? ctx.qry.repo : "", - ctx.qry.page ? ctx.qry.page : "", - cgit_currentfullurl(), - cgit_loginurl()); -} - -/* We intentionally keep this rather small, instead of looping and - * feeding it to the filter a couple bytes at a time. This way, the - * filter itself does not need to handle any denial of service or - * buffer bloat issues. If this winds up being too small, people - * will complain on the mailing list, and we'll increase it as needed. */ -#define MAX_AUTHENTICATION_POST_BYTES 4096 -/* The filter is expected to spit out "Status: " and all headers. */ -static inline void authenticate_post(void) -{ - char buffer[MAX_AUTHENTICATION_POST_BYTES]; - ssize_t len; - - open_auth_filter("authenticate-post"); - len = ctx.env.content_length; - if (len > MAX_AUTHENTICATION_POST_BYTES) - len = MAX_AUTHENTICATION_POST_BYTES; - if ((len = read(STDIN_FILENO, buffer, len)) < 0) - die_errno("Could not read POST from stdin"); - if (write(STDOUT_FILENO, buffer, len) < 0) - die_errno("Could not write POST to stdout"); - cgit_close_filter(ctx.cfg.auth_filter); - exit(0); -} - -static inline void authenticate_cookie(void) -{ - /* If we don't have an auth_filter, consider all cookies valid, and thus return early. */ - if (!ctx.cfg.auth_filter) { - ctx.env.authenticated = 1; - return; - } - - /* If we're having something POST'd to /login, we're authenticating POST, - * instead of the cookie, so call authenticate_post and bail out early. - * This pattern here should match /?p=login with POST. */ - if (ctx.env.request_method && ctx.qry.page && !ctx.repo && \ - !strcmp(ctx.env.request_method, "POST") && !strcmp(ctx.qry.page, "login")) { - authenticate_post(); - return; - } - - /* If we've made it this far, we're authenticating the cookie for real, so do that. */ - open_auth_filter("authenticate-cookie"); - ctx.env.authenticated = cgit_close_filter(ctx.cfg.auth_filter); -} - -static void process_request(void) -{ - struct cgit_cmd *cmd; - int nongit = 0; - - /* If we're not yet authenticated, no matter what page we're on, - * display the authentication body from the auth_filter. This should - * never be cached. */ - if (!ctx.env.authenticated) { - ctx.page.title = "Authentication Required"; - cgit_print_http_headers(); - cgit_print_docstart(); - cgit_print_pageheader(); - open_auth_filter("body"); - cgit_close_filter(ctx.cfg.auth_filter); - cgit_print_docend(); - return; - } - - if (ctx.repo) - prepare_repo_env(&nongit); - - cmd = cgit_get_cmd(); - if (!cmd) { - ctx.page.title = "cgit error"; - cgit_print_error_page(404, "Not found", "Invalid request"); - return; - } - - if (!ctx.cfg.enable_http_clone && cmd->is_clone) { - ctx.page.title = "cgit error"; - cgit_print_error_page(404, "Not found", "Invalid request"); - return; - } - - if (cmd->want_repo && !ctx.repo) { - cgit_print_error_page(400, "Bad request", - "No repository selected"); - return; - } - - /* If cmd->want_vpath is set, assume ctx.qry.path contains a "virtual" - * in-project path limit to be made available at ctx.qry.vpath. - * Otherwise, no path limit is in effect (ctx.qry.vpath = NULL). - */ - ctx.qry.vpath = cmd->want_vpath ? ctx.qry.path : NULL; - - if (ctx.repo && prepare_repo_cmd(nongit)) - return; - - cmd->fn(); -} - -static int cmp_repos(const void *a, const void *b) -{ - const struct cgit_repo *ra = a, *rb = b; - return strcmp(ra->url, rb->url); -} - -static char *build_snapshot_setting(int bitmap) -{ - const struct cgit_snapshot_format *f; - struct strbuf result = STRBUF_INIT; - - for (f = cgit_snapshot_formats; f->suffix; f++) { - if (cgit_snapshot_format_bit(f) & bitmap) { - if (result.len) - strbuf_addch(&result, ' '); - strbuf_addstr(&result, f->suffix); - } - } - return strbuf_detach(&result, NULL); -} - -static char *get_first_line(char *txt) -{ - char *t = xstrdup(txt); - char *p = strchr(t, '\n'); - if (p) - *p = '\0'; - return t; -} - -static void print_repo(FILE *f, struct cgit_repo *repo) -{ - struct string_list_item *item; - fprintf(f, "repo.url=%s\n", repo->url); - fprintf(f, "repo.name=%s\n", repo->name); - fprintf(f, "repo.path=%s\n", repo->path); - if (repo->owner) - fprintf(f, "repo.owner=%s\n", repo->owner); - if (repo->desc) { - char *tmp = get_first_line(repo->desc); - fprintf(f, "repo.desc=%s\n", tmp); - free(tmp); - } - for_each_string_list_item(item, &repo->readme) { - if (item->util) - fprintf(f, "repo.readme=%s:%s\n", (char *)item->util, item->string); - else - fprintf(f, "repo.readme=%s\n", item->string); - } - if (repo->defbranch) - fprintf(f, "repo.defbranch=%s\n", repo->defbranch); - if (repo->extra_head_content) - fprintf(f, "repo.extra-head-content=%s\n", repo->extra_head_content); - if (repo->module_link) - fprintf(f, "repo.module-link=%s\n", repo->module_link); - if (repo->section) - fprintf(f, "repo.section=%s\n", repo->section); - if (repo->homepage) - fprintf(f, "repo.homepage=%s\n", repo->homepage); - if (repo->clone_url) - fprintf(f, "repo.clone-url=%s\n", repo->clone_url); - fprintf(f, "repo.enable-blame=%d\n", - repo->enable_blame); - fprintf(f, "repo.enable-commit-graph=%d\n", - repo->enable_commit_graph); - fprintf(f, "repo.enable-log-filecount=%d\n", - repo->enable_log_filecount); - fprintf(f, "repo.enable-log-linecount=%d\n", - repo->enable_log_linecount); - if (repo->about_filter && repo->about_filter != ctx.cfg.about_filter) - cgit_fprintf_filter(repo->about_filter, f, "repo.about-filter="); - if (repo->commit_filter && repo->commit_filter != ctx.cfg.commit_filter) - cgit_fprintf_filter(repo->commit_filter, f, "repo.commit-filter="); - if (repo->source_filter && repo->source_filter != ctx.cfg.source_filter) - cgit_fprintf_filter(repo->source_filter, f, "repo.source-filter="); - if (repo->email_filter && repo->email_filter != ctx.cfg.email_filter) - cgit_fprintf_filter(repo->email_filter, f, "repo.email-filter="); - if (repo->owner_filter && repo->owner_filter != ctx.cfg.owner_filter) - cgit_fprintf_filter(repo->owner_filter, f, "repo.owner-filter="); - if (repo->snapshots != ctx.cfg.snapshots) { - char *tmp = build_snapshot_setting(repo->snapshots); - fprintf(f, "repo.snapshots=%s\n", tmp ? tmp : ""); - free(tmp); - } - if (repo->snapshot_prefix) - fprintf(f, "repo.snapshot-prefix=%s\n", repo->snapshot_prefix); - if (repo->max_stats != ctx.cfg.max_stats) - fprintf(f, "repo.max-stats=%s\n", - cgit_find_stats_periodname(repo->max_stats)); - if (repo->logo) - fprintf(f, "repo.logo=%s\n", repo->logo); - if (repo->logo_link) - fprintf(f, "repo.logo-link=%s\n", repo->logo_link); - fprintf(f, "repo.enable-remote-branches=%d\n", repo->enable_remote_branches); - fprintf(f, "repo.enable-subject-links=%d\n", repo->enable_subject_links); - fprintf(f, "repo.enable-html-serving=%d\n", repo->enable_html_serving); - if (repo->branch_sort == 1) - fprintf(f, "repo.branch-sort=age\n"); - if (repo->commit_sort) { - if (repo->commit_sort == 1) - fprintf(f, "repo.commit-sort=date\n"); - else if (repo->commit_sort == 2) - fprintf(f, "repo.commit-sort=topo\n"); - } - fprintf(f, "repo.hide=%d\n", repo->hide); - fprintf(f, "repo.ignore=%d\n", repo->ignore); - fprintf(f, "\n"); -} - -static void print_repolist(FILE *f, struct cgit_repolist *list, int start) -{ - int i; - - for (i = start; i < list->count; i++) - print_repo(f, &list->repos[i]); -} - -/* Scan 'path' for git repositories, save the resulting repolist in 'cached_rc' - * and return 0 on success. - */ -static int generate_cached_repolist(const char *path, const char *cached_rc) -{ - struct strbuf locked_rc = STRBUF_INIT; - int result = 0; - int idx; - FILE *f; - - strbuf_addf(&locked_rc, "%s.lock", cached_rc); - f = fopen(locked_rc.buf, "wx"); - if (!f) { - /* Inform about the error unless the lockfile already existed, - * since that only means we've got concurrent requests. - */ - result = errno; - if (result != EEXIST) - fprintf(stderr, "[cgit] Error opening %s: %s (%d)\n", - locked_rc.buf, strerror(result), result); - goto out; - } - idx = cgit_repolist.count; - if (ctx.cfg.project_list) - scan_projects(path, ctx.cfg.project_list, repo_config); - else - scan_tree(path, repo_config); - print_repolist(f, &cgit_repolist, idx); - if (rename(locked_rc.buf, cached_rc)) - fprintf(stderr, "[cgit] Error renaming %s to %s: %s (%d)\n", - locked_rc.buf, cached_rc, strerror(errno), errno); - fclose(f); -out: - strbuf_release(&locked_rc); - return result; -} - -static void process_cached_repolist(const char *path) -{ - struct stat st; - struct strbuf cached_rc = STRBUF_INIT; - time_t age; - unsigned long hash; - - hash = hash_str(path); - if (ctx.cfg.project_list) - hash += hash_str(ctx.cfg.project_list); - strbuf_addf(&cached_rc, "%s/rc-%8lx", ctx.cfg.cache_root, hash); - - if (stat(cached_rc.buf, &st)) { - /* Nothing is cached, we need to scan without forking. And - * if we fail to generate a cached repolist, we need to - * invoke scan_tree manually. - */ - if (generate_cached_repolist(path, cached_rc.buf)) { - if (ctx.cfg.project_list) - scan_projects(path, ctx.cfg.project_list, - repo_config); - else - scan_tree(path, repo_config); - } - goto out; - } - - parse_configfile(cached_rc.buf, config_cb); - - /* If the cached configfile hasn't expired, lets exit now */ - age = time(NULL) - st.st_mtime; - if (age <= (ctx.cfg.cache_scanrc_ttl * 60)) - goto out; - - /* The cached repolist has been parsed, but it was old. So lets - * rescan the specified path and generate a new cached repolist - * in a child-process to avoid latency for the current request. - */ - if (fork()) - goto out; - - exit(generate_cached_repolist(path, cached_rc.buf)); -out: - strbuf_release(&cached_rc); -} - -static void cgit_parse_args(int argc, const char **argv) -{ - int i; - const char *arg; - int scan = 0; - - for (i = 1; i < argc; i++) { - if (!strcmp(argv[i], "--version")) { - printf("CGit %s | https://git.zx2c4.com/cgit/\n\nCompiled in features:\n", CGIT_VERSION); -#ifdef NO_LUA - printf("[-] "); -#else - printf("[+] "); -#endif - printf("Lua scripting\n"); -#ifndef HAVE_LINUX_SENDFILE - printf("[-] "); -#else - printf("[+] "); -#endif - printf("Linux sendfile() usage\n"); - - exit(0); - } - if (skip_prefix(argv[i], "--cache=", &arg)) { - ctx.cfg.cache_root = xstrdup(arg); - } else if (!strcmp(argv[i], "--nohttp")) { - ctx.env.no_http = "1"; - } else if (skip_prefix(argv[i], "--query=", &arg)) { - ctx.qry.raw = xstrdup(arg); - } else if (skip_prefix(argv[i], "--repo=", &arg)) { - ctx.qry.repo = xstrdup(arg); - } else if (skip_prefix(argv[i], "--page=", &arg)) { - ctx.qry.page = xstrdup(arg); - } else if (skip_prefix(argv[i], "--head=", &arg)) { - ctx.qry.head = xstrdup(arg); - ctx.qry.has_symref = 1; - } else if (skip_prefix(argv[i], "--sha1=", &arg)) { - ctx.qry.sha1 = xstrdup(arg); - ctx.qry.has_sha1 = 1; - } else if (skip_prefix(argv[i], "--ofs=", &arg)) { - ctx.qry.ofs = atoi(arg); - } else if (skip_prefix(argv[i], "--scan-tree=", &arg) || - skip_prefix(argv[i], "--scan-path=", &arg)) { - /* - * HACK: The global snapshot bit mask defines the set - * of allowed snapshot formats, but the config file - * hasn't been parsed yet so the mask is currently 0. - * By setting all bits high before scanning we make - * sure that any in-repo cgitrc snapshot setting is - * respected by scan_tree(). - * - * NOTE: We assume that there aren't more than 8 - * different snapshot formats supported by cgit... - */ - ctx.cfg.snapshots = 0xFF; - scan++; - scan_tree(arg, repo_config); - } - } - if (scan) { - qsort(cgit_repolist.repos, cgit_repolist.count, - sizeof(struct cgit_repo), cmp_repos); - print_repolist(stdout, &cgit_repolist, 0); - exit(0); - } -} - -static int calc_ttl(void) -{ - if (!ctx.repo) - return ctx.cfg.cache_root_ttl; - - if (!ctx.qry.page) - return ctx.cfg.cache_repo_ttl; - - if (!strcmp(ctx.qry.page, "about")) - return ctx.cfg.cache_about_ttl; - - if (!strcmp(ctx.qry.page, "snapshot")) - return ctx.cfg.cache_snapshot_ttl; - - if (ctx.qry.has_sha1) - return ctx.cfg.cache_static_ttl; - - if (ctx.qry.has_symref) - return ctx.cfg.cache_dynamic_ttl; - - return ctx.cfg.cache_repo_ttl; -} - -int cmd_main(int argc, const char **argv) -{ - const char *path; - int err, ttl; - - cgit_init_filters(); - atexit(cgit_cleanup_filters); - - prepare_context(); - cgit_repolist.length = 0; - cgit_repolist.count = 0; - cgit_repolist.repos = NULL; - - cgit_parse_args(argc, argv); - parse_configfile(expand_macros(ctx.env.cgit_config), config_cb); - ctx.repo = NULL; - http_parse_querystring(ctx.qry.raw, querystring_cb); - - /* If virtual-root isn't specified in cgitrc, lets pretend - * that virtual-root equals SCRIPT_NAME, minus any possibly - * trailing slashes. - */ - if (!ctx.cfg.virtual_root && ctx.cfg.script_name) - ctx.cfg.virtual_root = ensure_end(ctx.cfg.script_name, '/'); - - /* If no url parameter is specified on the querystring, lets - * use PATH_INFO as url. This allows cgit to work with virtual - * urls without the need for rewriterules in the webserver (as - * long as PATH_INFO is included in the cache lookup key). - */ - path = ctx.env.path_info; - if (!ctx.qry.url && path) { - if (path[0] == '/') - path++; - ctx.qry.url = xstrdup(path); - if (ctx.qry.raw) { - char *newqry = fmtalloc("%s?%s", path, ctx.qry.raw); - free(ctx.qry.raw); - ctx.qry.raw = newqry; - } else - ctx.qry.raw = xstrdup(ctx.qry.url); - cgit_parse_url(ctx.qry.url); - } - - /* Before we go any further, we set ctx.env.authenticated by checking to see - * if the supplied cookie is valid. All cookies are valid if there is no - * auth_filter. If there is an auth_filter, the filter decides. */ - authenticate_cookie(); - - ttl = calc_ttl(); - if (ttl < 0) - ctx.page.expires += 10 * 365 * 24 * 60 * 60; /* 10 years */ - else - ctx.page.expires += ttl * 60; - if (!ctx.env.authenticated || (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))) - ctx.cfg.cache_size = 0; - err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, - ctx.qry.raw, ttl, process_request); - cgit_cleanup_filters(); - if (err) - cgit_print_error("Error processing page: %s (%d)", - strerror(err), err); - return err; -} diff --git a/cgit.css b/cgit.css deleted file mode 100644 index d4aadbfa10..0000000000 --- a/cgit.css +++ /dev/null @@ -1,895 +0,0 @@ -div#cgit { - padding: 0em; - margin: 0em; - font-family: sans-serif; - font-size: 10pt; - color: #333; - background: white; - padding: 4px; -} - -div#cgit a { - color: blue; - text-decoration: none; -} - -div#cgit a:hover { - text-decoration: underline; -} - -div#cgit table { - border-collapse: collapse; -} - -div#cgit table#header { - width: 100%; - margin-bottom: 1em; -} - -div#cgit table#header td.logo { - width: 96px; - vertical-align: top; -} - -div#cgit table#header td.main { - font-size: 250%; - padding-left: 10px; - white-space: nowrap; -} - -div#cgit table#header td.main a { - color: #000; -} - -div#cgit table#header td.form { - text-align: right; - vertical-align: bottom; - padding-right: 1em; - padding-bottom: 2px; - white-space: nowrap; -} - -div#cgit table#header td.form form, -div#cgit table#header td.form input, -div#cgit table#header td.form select { - font-size: 90%; -} - -div#cgit table#header td.sub { - color: #777; - border-top: solid 1px #ccc; - padding-left: 10px; -} - -div#cgit table.tabs { - border-bottom: solid 3px #ccc; - border-collapse: collapse; - margin-top: 2em; - margin-bottom: 0px; - width: 100%; -} - -div#cgit table.tabs td { - padding: 0px 1em; - vertical-align: bottom; -} - -div#cgit table.tabs td a { - padding: 2px 0.75em; - color: #777; - font-size: 110%; -} - -div#cgit table.tabs td a.active { - color: #000; - background-color: #ccc; -} - -div#cgit table.tabs a[href^="http://"]:after, div#cgit table.tabs a[href^="https://"]:after { - content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfgAhcJDQY+gm2TAAAAHWlUWHRDb21tZW50AAAAAABDcmVhdGVkIHdpdGggR0lNUGQuZQcAAABbSURBVAhbY2BABs4MU4CwhYHBh2Erww4wrGFQZHjI8B8IgUIscJWyDHcggltQhI4zGDCcRwhChPggHIggP1QoAVmQkSETrGoHsiAEsACtBYN0oDAMbgU6EBcAAL2eHUt4XUU4AAAAAElFTkSuQmCC); - opacity: 0.5; - margin: 0 0 0 5px; -} - -div#cgit table.tabs td.form { - text-align: right; -} - -div#cgit table.tabs td.form form { - padding-bottom: 2px; - font-size: 90%; - white-space: nowrap; -} - -div#cgit table.tabs td.form input, -div#cgit table.tabs td.form select { - font-size: 90%; -} - -div#cgit div.path { - margin: 0px; - padding: 5px 2em 2px 2em; - color: #000; - background-color: #eee; -} - -div#cgit div.content { - margin: 0px; - padding: 2em; - border-bottom: solid 3px #ccc; -} - - -div#cgit table.list { - width: 100%; - border: none; - border-collapse: collapse; -} - -div#cgit table.list tr { - background: white; -} - -div#cgit table.list tr.logheader { - background: #eee; -} - -div#cgit table.list tr:nth-child(even) { - background: #f7f7f7; -} - -div#cgit table.list tr:nth-child(odd) { - background: white; -} - -div#cgit table.list tr:hover { - background: #eee; -} - -div#cgit table.list tr.nohover { - background: white; -} - -div#cgit table.list tr.nohover:hover { - background: white; -} - -div#cgit table.list tr.nohover-highlight:hover:nth-child(even) { - background: #f7f7f7; -} - -div#cgit table.list tr.nohover-highlight:hover:nth-child(odd) { - background: white; -} - -div#cgit table.list th { - font-weight: bold; - /* color: #888; - border-top: dashed 1px #888; - border-bottom: dashed 1px #888; - */ - padding: 0.1em 0.5em 0.05em 0.5em; - vertical-align: baseline; -} - -div#cgit table.list td { - border: none; - padding: 0.1em 0.5em 0.1em 0.5em; -} - -div#cgit table.list td.commitgraph { - font-family: monospace; - white-space: pre; -} - -div#cgit table.list td.commitgraph .column1 { - color: #a00; -} - -div#cgit table.list td.commitgraph .column2 { - color: #0a0; -} - -div#cgit table.list td.commitgraph .column3 { - color: #aa0; -} - -div#cgit table.list td.commitgraph .column4 { - color: #00a; -} - -div#cgit table.list td.commitgraph .column5 { - color: #a0a; -} - -div#cgit table.list td.commitgraph .column6 { - color: #0aa; -} - -div#cgit table.list td.logsubject { - font-family: monospace; - font-weight: bold; -} - -div#cgit table.list td.logmsg { - font-family: monospace; - white-space: pre; - padding: 0 0.5em; -} - -div#cgit table.list td a { - color: black; -} - -div#cgit table.list td a.ls-dir { - font-weight: bold; - color: #00f; -} - -div#cgit table.list td a:hover { - color: #00f; -} - -div#cgit img { - border: none; -} - -div#cgit input#switch-btn { - margin: 2px 0px 0px 0px; -} - -div#cgit td#sidebar input.txt { - width: 100%; - margin: 2px 0px 0px 0px; -} - -div#cgit table#grid { - margin: 0px; -} - -div#cgit td#content { - vertical-align: top; - padding: 1em 2em 1em 1em; - border: none; -} - -div#cgit div#summary { - vertical-align: top; - margin-bottom: 1em; -} - -div#cgit table#downloads { - float: right; - border-collapse: collapse; - border: solid 1px #777; - margin-left: 0.5em; - margin-bottom: 0.5em; -} - -div#cgit table#downloads th { - background-color: #ccc; -} - -div#cgit div#blob { - border: solid 1px black; -} - -div#cgit div.error { - color: red; - font-weight: bold; - margin: 1em 2em; -} - -div#cgit a.ls-blob, div#cgit a.ls-dir, div#cgit .ls-mod { - font-family: monospace; -} - -div#cgit td.ls-size { - text-align: right; - font-family: monospace; - width: 10em; -} - -div#cgit td.ls-mode { - font-family: monospace; - width: 10em; -} - -div#cgit table.blob { - margin-top: 0.5em; - border-top: solid 1px black; -} - -div#cgit table.blob td.hashes, -div#cgit table.blob td.lines { - margin: 0; padding: 0 0 0 0.5em; - vertical-align: top; - color: black; -} - -div#cgit table.blob td.linenumbers { - margin: 0; padding: 0 0.5em 0 0.5em; - vertical-align: top; - text-align: right; - border-right: 1px solid gray; -} - -div#cgit table.blob pre { - padding: 0; margin: 0; -} - -div#cgit table.blob td.linenumbers a, -div#cgit table.ssdiff td.lineno a { - color: gray; - text-align: right; - text-decoration: none; -} - -div#cgit table.blob td.linenumbers a:hover, -div#cgit table.ssdiff td.lineno a:hover { - color: black; -} - -div#cgit table.blame td.hashes, -div#cgit table.blame td.lines, -div#cgit table.blame td.linenumbers { - padding: 0; -} - -div#cgit table.blame td.hashes div.alt, -div#cgit table.blame td.lines div.alt { - padding: 0 0.5em 0 0.5em; -} - -div#cgit table.blame td.linenumbers div.alt { - padding: 0 0.5em 0 0; -} - -div#cgit table.blame div.alt:nth-child(even) { - background: #eee; -} - -div#cgit table.blame div.alt:nth-child(odd) { - background: white; -} - -div#cgit table.blame td.lines > div { - position: relative; -} - -div#cgit table.blame td.lines > div > pre { - padding: 0 0 0 0.5em; - position: absolute; - top: 0; -} - -div#cgit table.bin-blob { - margin-top: 0.5em; - border: solid 1px black; -} - -div#cgit table.bin-blob th { - font-family: monospace; - white-space: pre; - border: solid 1px #777; - padding: 0.5em 1em; -} - -div#cgit table.bin-blob td { - font-family: monospace; - white-space: pre; - border-left: solid 1px #777; - padding: 0em 1em; -} - -div#cgit table.nowrap td { - white-space: nowrap; -} - -div#cgit table.commit-info { - border-collapse: collapse; - margin-top: 1.5em; -} - -div#cgit div.cgit-panel { - float: right; - margin-top: 1.5em; -} - -div#cgit div.cgit-panel table { - border-collapse: collapse; - border: solid 1px #aaa; - background-color: #eee; -} - -div#cgit div.cgit-panel th { - text-align: center; -} - -div#cgit div.cgit-panel td { - padding: 0.25em 0.5em; -} - -div#cgit div.cgit-panel td.label { - padding-right: 0.5em; -} - -div#cgit div.cgit-panel td.ctrl { - padding-left: 0.5em; -} - -div#cgit table.commit-info th { - text-align: left; - font-weight: normal; - padding: 0.1em 1em 0.1em 0.1em; - vertical-align: top; -} - -div#cgit table.commit-info td { - font-weight: normal; - padding: 0.1em 1em 0.1em 0.1em; -} - -div#cgit div.commit-subject { - font-weight: bold; - font-size: 125%; - margin: 1.5em 0em 0.5em 0em; - padding: 0em; -} - -div#cgit div.commit-msg { - white-space: pre; - font-family: monospace; -} - -div#cgit div.notes-header { - font-weight: bold; - padding-top: 1.5em; -} - -div#cgit div.notes { - white-space: pre; - font-family: monospace; - border: solid 1px #ee9; - background-color: #ffd; - padding: 0.3em 2em 0.3em 1em; - float: left; -} - -div#cgit div.notes-footer { - clear: left; -} - -div#cgit div.diffstat-header { - font-weight: bold; - padding-top: 1.5em; -} - -div#cgit table.diffstat { - border-collapse: collapse; - border: solid 1px #aaa; - background-color: #eee; -} - -div#cgit table.diffstat th { - font-weight: normal; - text-align: left; - text-decoration: underline; - padding: 0.1em 1em 0.1em 0.1em; - font-size: 100%; -} - -div#cgit table.diffstat td { - padding: 0.2em 0.2em 0.1em 0.1em; - font-size: 100%; - border: none; -} - -div#cgit table.diffstat td.mode { - white-space: nowrap; -} - -div#cgit table.diffstat td span.modechange { - padding-left: 1em; - color: red; -} - -div#cgit table.diffstat td.add a { - color: green; -} - -div#cgit table.diffstat td.del a { - color: red; -} - -div#cgit table.diffstat td.upd a { - color: blue; -} - -div#cgit table.diffstat td.graph { - width: 500px; - vertical-align: middle; -} - -div#cgit table.diffstat td.graph table { - border: none; -} - -div#cgit table.diffstat td.graph td { - padding: 0px; - border: 0px; - height: 7pt; -} - -div#cgit table.diffstat td.graph td.add { - background-color: #5c5; -} - -div#cgit table.diffstat td.graph td.rem { - background-color: #c55; -} - -div#cgit div.diffstat-summary { - color: #888; - padding-top: 0.5em; -} - -div#cgit table.diff { - width: 100%; -} - -div#cgit table.diff td { - font-family: monospace; - white-space: pre; -} - -div#cgit table.diff td div.head { - font-weight: bold; - margin-top: 1em; - color: black; -} - -div#cgit table.diff td div.hunk { - color: #009; -} - -div#cgit table.diff td div.add { - color: green; -} - -div#cgit table.diff td div.del { - color: red; -} - -div#cgit .sha1 { - font-family: monospace; - font-size: 90%; -} - -div#cgit .left { - text-align: left; -} - -div#cgit .right { - text-align: right; -} - -div#cgit table.list td.reposection { - font-style: italic; - color: #888; -} - -div#cgit a.button { - font-size: 80%; - padding: 0em 0.5em; -} - -div#cgit a.primary { - font-size: 100%; -} - -div#cgit a.secondary { - font-size: 90%; -} - -div#cgit td.toplevel-repo { - -} - -div#cgit table.list td.sublevel-repo { - padding-left: 1.5em; -} - -div#cgit ul.pager { - list-style-type: none; - text-align: center; - margin: 1em 0em 0em 0em; - padding: 0; -} - -div#cgit ul.pager li { - display: inline-block; - margin: 0.25em 0.5em; -} - -div#cgit ul.pager a { - color: #777; -} - -div#cgit ul.pager .current { - font-weight: bold; -} - -div#cgit span.age-mins { - font-weight: bold; - color: #080; -} - -div#cgit span.age-hours { - color: #080; -} - -div#cgit span.age-days { - color: #040; -} - -div#cgit span.age-weeks { - color: #444; -} - -div#cgit span.age-months { - color: #888; -} - -div#cgit span.age-years { - color: #bbb; -} - -div#cgit span.insertions { - color: #080; -} - -div#cgit span.deletions { - color: #800; -} - -div#cgit div.footer { - margin-top: 0.5em; - text-align: center; - font-size: 80%; - color: #ccc; -} - -div#cgit div.footer a { - color: #ccc; - text-decoration: none; -} - -div#cgit div.footer a:hover { - text-decoration: underline; -} - -div#cgit a.branch-deco { - color: #000; - margin: 0px 0.5em; - padding: 0px 0.25em; - background-color: #88ff88; - border: solid 1px #007700; -} - -div#cgit a.tag-deco { - color: #000; - margin: 0px 0.5em; - padding: 0px 0.25em; - background-color: #ffff88; - border: solid 1px #777700; -} - -div#cgit a.tag-annotated-deco { - color: #000; - margin: 0px 0.5em; - padding: 0px 0.25em; - background-color: #ffcc88; - border: solid 1px #777700; -} - -div#cgit a.remote-deco { - color: #000; - margin: 0px 0.5em; - padding: 0px 0.25em; - background-color: #ccccff; - border: solid 1px #000077; -} - -div#cgit a.deco { - color: #000; - margin: 0px 0.5em; - padding: 0px 0.25em; - background-color: #ff8888; - border: solid 1px #770000; -} - -div#cgit div.commit-subject a.branch-deco, -div#cgit div.commit-subject a.tag-deco, -div#cgit div.commit-subject a.tag-annotated-deco, -div#cgit div.commit-subject a.remote-deco, -div#cgit div.commit-subject a.deco { - margin-left: 1em; - font-size: 75%; -} - -div#cgit table.stats { - border: solid 1px black; - border-collapse: collapse; -} - -div#cgit table.stats th { - text-align: left; - padding: 1px 0.5em; - background-color: #eee; - border: solid 1px black; -} - -div#cgit table.stats td { - text-align: right; - padding: 1px 0.5em; - border: solid 1px black; -} - -div#cgit table.stats td.total { - font-weight: bold; - text-align: left; -} - -div#cgit table.stats td.sum { - color: #c00; - font-weight: bold; -/* background-color: #eee; */ -} - -div#cgit table.stats td.left { - text-align: left; -} - -div#cgit table.vgraph { - border-collapse: separate; - border: solid 1px black; - height: 200px; -} - -div#cgit table.vgraph th { - background-color: #eee; - font-weight: bold; - border: solid 1px white; - padding: 1px 0.5em; -} - -div#cgit table.vgraph td { - vertical-align: bottom; - padding: 0px 10px; -} - -div#cgit table.vgraph div.bar { - background-color: #eee; -} - -div#cgit table.hgraph { - border: solid 1px black; - width: 800px; -} - -div#cgit table.hgraph th { - background-color: #eee; - font-weight: bold; - border: solid 1px black; - padding: 1px 0.5em; -} - -div#cgit table.hgraph td { - vertical-align: middle; - padding: 2px 2px; -} - -div#cgit table.hgraph div.bar { - background-color: #eee; - height: 1em; -} - -div#cgit table.ssdiff { - width: 100%; -} - -div#cgit table.ssdiff td { - font-size: 75%; - font-family: monospace; - white-space: pre; - padding: 1px 4px 1px 4px; - border-left: solid 1px #aaa; - border-right: solid 1px #aaa; -} - -div#cgit table.ssdiff td.add { - color: black; - background: #cfc; - min-width: 50%; -} - -div#cgit table.ssdiff td.add_dark { - color: black; - background: #aca; - min-width: 50%; -} - -div#cgit table.ssdiff span.add { - background: #cfc; - font-weight: bold; -} - -div#cgit table.ssdiff td.del { - color: black; - background: #fcc; - min-width: 50%; -} - -div#cgit table.ssdiff td.del_dark { - color: black; - background: #caa; - min-width: 50%; -} - -div#cgit table.ssdiff span.del { - background: #fcc; - font-weight: bold; -} - -div#cgit table.ssdiff td.changed { - color: black; - background: #ffc; - min-width: 50%; -} - -div#cgit table.ssdiff td.changed_dark { - color: black; - background: #cca; - min-width: 50%; -} - -div#cgit table.ssdiff td.lineno { - color: black; - background: #eee; - text-align: right; - width: 3em; - min-width: 3em; -} - -div#cgit table.ssdiff td.hunk { - color: black; - background: #ccf; - border-top: solid 1px #aaa; - border-bottom: solid 1px #aaa; -} - -div#cgit table.ssdiff td.head { - border-top: solid 1px #aaa; - border-bottom: solid 1px #aaa; -} - -div#cgit table.ssdiff td.head div.head { - font-weight: bold; - color: black; -} - -div#cgit table.ssdiff td.foot { - border-top: solid 1px #aaa; - border-left: none; - border-right: none; - border-bottom: none; -} - -div#cgit table.ssdiff td.space { - border: none; -} - -div#cgit table.ssdiff td.space div { - min-height: 3em; -} diff --git a/cgit.h b/cgit.h deleted file mode 100644 index 7ec46b4846..0000000000 --- a/cgit.h +++ /dev/null @@ -1,398 +0,0 @@ -#ifndef CGIT_H -#define CGIT_H - - -#include <git-compat-util.h> -#include <stdbool.h> - -#include <cache.h> -#include <grep.h> -#include <object.h> -#include <object-store.h> -#include <tree.h> -#include <commit.h> -#include <tag.h> -#include <diff.h> -#include <diffcore.h> -#include <argv-array.h> -#include <refs.h> -#include <revision.h> -#include <log-tree.h> -#include <archive.h> -#include <string-list.h> -#include <xdiff-interface.h> -#include <xdiff/xdiff.h> -#include <utf8.h> -#include <notes.h> -#include <graph.h> - -/* Add isgraph(x) to Git's sane ctype support (see git-compat-util.h) */ -#undef isgraph -#define isgraph(x) (isprint((x)) && !isspace((x))) - - -/* - * Limits used for relative dates - */ -#define TM_MIN 60 -#define TM_HOUR (TM_MIN * 60) -#define TM_DAY (TM_HOUR * 24) -#define TM_WEEK (TM_DAY * 7) -#define TM_YEAR (TM_DAY * 365) -#define TM_MONTH (TM_YEAR / 12.0) - - -/* - * Default encoding - */ -#define PAGE_ENCODING "UTF-8" - -#define BIT(x) (1U << (x)) - -typedef void (*configfn)(const char *name, const char *value); -typedef void (*filepair_fn)(struct diff_filepair *pair); -typedef void (*linediff_fn)(char *line, int len); - -typedef enum { - DIFF_UNIFIED, DIFF_SSDIFF, DIFF_STATONLY -} diff_type; - -typedef enum { - ABOUT, COMMIT, SOURCE, EMAIL, AUTH, OWNER -} filter_type; - -struct cgit_filter { - int (*open)(struct cgit_filter *, va_list ap); - int (*close)(struct cgit_filter *); - void (*fprintf)(struct cgit_filter *, FILE *, const char *prefix); - void (*cleanup)(struct cgit_filter *); - int argument_count; -}; - -struct cgit_exec_filter { - struct cgit_filter base; - char *cmd; - char **argv; - int old_stdout; - int pid; -}; - -struct cgit_repo { - char *url; - char *name; - char *path; - char *desc; - char *extra_head_content; - char *owner; - char *homepage; - char *defbranch; - char *module_link; - struct string_list readme; - char *section; - char *clone_url; - char *logo; - char *logo_link; - char *snapshot_prefix; - int snapshots; - int enable_blame; - int enable_commit_graph; - int enable_log_filecount; - int enable_log_linecount; - int enable_remote_branches; - int enable_subject_links; - int enable_html_serving; - int max_stats; - int branch_sort; - int commit_sort; - time_t mtime; - struct cgit_filter *about_filter; - struct cgit_filter *commit_filter; - struct cgit_filter *source_filter; - struct cgit_filter *email_filter; - struct cgit_filter *owner_filter; - struct string_list submodules; - int hide; - int ignore; -}; - -typedef void (*repo_config_fn)(struct cgit_repo *repo, const char *name, - const char *value); - -struct cgit_repolist { - int length; - int count; - struct cgit_repo *repos; -}; - -struct commitinfo { - struct commit *commit; - char *author; - char *author_email; - unsigned long author_date; - int author_tz; - char *committer; - char *committer_email; - unsigned long committer_date; - int committer_tz; - char *subject; - char *msg; - char *msg_encoding; -}; - -struct taginfo { - char *tagger; - char *tagger_email; - unsigned long tagger_date; - int tagger_tz; - char *msg; -}; - -struct refinfo { - const char *refname; - struct object *object; - union { - struct taginfo *tag; - struct commitinfo *commit; - }; -}; - -struct reflist { - struct refinfo **refs; - int alloc; - int count; -}; - -struct cgit_query { - int has_symref; - int has_sha1; - int has_difftype; - char *raw; - char *repo; - char *page; - char *search; - char *grep; - char *head; - char *sha1; - char *sha2; - char *path; - char *name; - char *url; - char *period; - int ofs; - int nohead; - char *sort; - int showmsg; - diff_type difftype; - int show_all; - int context; - int ignorews; - int follow; - char *vpath; -}; - -struct cgit_config { - char *agefile; - char *cache_root; - char *clone_prefix; - char *clone_url; - char *css; - char *favicon; - char *footer; - char *head_include; - char *header; - char *logo; - char *logo_link; - char *mimetype_file; - char *module_link; - char *project_list; - struct string_list readme; - char *robots; - char *root_title; - char *root_desc; - char *root_readme; - char *script_name; - char *section; - char *repository_sort; - char *virtual_root; /* Always ends with '/'. */ - char *strict_export; - int cache_size; - int cache_dynamic_ttl; - int cache_max_create_time; - int cache_repo_ttl; - int cache_root_ttl; - int cache_scanrc_ttl; - int cache_static_ttl; - int cache_about_ttl; - int cache_snapshot_ttl; - int case_sensitive_sort; - int embedded; - int enable_filter_overrides; - int enable_follow_links; - int enable_http_clone; - int enable_index_links; - int enable_index_owner; - int enable_blame; - int enable_commit_graph; - int enable_log_filecount; - int enable_log_linecount; - int enable_remote_branches; - int enable_subject_links; - int enable_html_serving; - int enable_tree_linenumbers; - int enable_git_config; - int local_time; - int max_atom_items; - int max_repo_count; - int max_commit_count; - int max_lock_attempts; - int max_msg_len; - int max_repodesc_len; - int max_blob_size; - int max_stats; - int noplainemail; - int noheader; - int renamelimit; - int remove_suffix; - int scan_hidden_path; - int section_from_path; - int snapshots; - int section_sort; - int summary_branches; - int summary_log; - int summary_tags; - diff_type difftype; - int branch_sort; - int commit_sort; - struct string_list mimetypes; - struct cgit_filter *about_filter; - struct cgit_filter *commit_filter; - struct cgit_filter *source_filter; - struct cgit_filter *email_filter; - struct cgit_filter *owner_filter; - struct cgit_filter *auth_filter; -}; - -struct cgit_page { - time_t modified; - time_t expires; - size_t size; - const char *mimetype; - const char *charset; - const char *filename; - const char *etag; - const char *title; - int status; - const char *statusmsg; -}; - -struct cgit_environment { - const char *cgit_config; - const char *http_host; - const char *https; - const char *no_http; - const char *path_info; - const char *query_string; - const char *request_method; - const char *script_name; - const char *server_name; - const char *server_port; - const char *http_cookie; - const char *http_referer; - unsigned int content_length; - int authenticated; -}; - -struct cgit_context { - struct cgit_environment env; - struct cgit_query qry; - struct cgit_config cfg; - struct cgit_repo *repo; - struct cgit_page page; -}; - -typedef int (*write_archive_fn_t)(const char *, const char *); - -struct cgit_snapshot_format { - const char *suffix; - const char *mimetype; - write_archive_fn_t write_func; -}; - -extern const char *cgit_version; - -extern struct cgit_repolist cgit_repolist; -extern struct cgit_context ctx; -extern const struct cgit_snapshot_format cgit_snapshot_formats[]; - -extern char *cgit_default_repo_desc; -extern struct cgit_repo *cgit_add_repo(const char *url); -extern struct cgit_repo *cgit_get_repoinfo(const char *url); -extern void cgit_repo_config_cb(const char *name, const char *value); - -extern int chk_zero(int result, char *msg); -extern int chk_positive(int result, char *msg); -extern int chk_non_negative(int result, char *msg); - -extern char *trim_end(const char *str, char c); -extern char *ensure_end(const char *str, char c); - -extern void strbuf_ensure_end(struct strbuf *sb, char c); - -extern void cgit_add_ref(struct reflist *list, struct refinfo *ref); -extern void cgit_free_reflist_inner(struct reflist *list); -extern int cgit_refs_cb(const char *refname, const struct object_id *oid, - int flags, void *cb_data); - -extern void cgit_free_commitinfo(struct commitinfo *info); -extern void cgit_free_taginfo(struct taginfo *info); - -void cgit_diff_tree_cb(struct diff_queue_struct *q, - struct diff_options *options, void *data); - -extern int cgit_diff_files(const struct object_id *old_oid, - const struct object_id *new_oid, - unsigned long *old_size, unsigned long *new_size, - int *binary, int context, int ignorews, - linediff_fn fn); - -extern void cgit_diff_tree(const struct object_id *old_oid, - const struct object_id *new_oid, - filepair_fn fn, const char *prefix, int ignorews); - -extern void cgit_diff_commit(struct commit *commit, filepair_fn fn, - const char *prefix); - -__attribute__((format (printf,1,2))) -extern char *fmt(const char *format,...); - -__attribute__((format (printf,1,2))) -extern char *fmtalloc(const char *format,...); - -extern struct commitinfo *cgit_parse_commit(struct commit *commit); -extern struct taginfo *cgit_parse_tag(struct tag *tag); -extern void cgit_parse_url(const char *url); - -extern const char *cgit_repobasename(const char *reponame); - -extern int cgit_parse_snapshots_mask(const char *str); -extern const struct object_id *cgit_snapshot_get_sig(const char *ref, - const struct cgit_snapshot_format *f); -extern const unsigned cgit_snapshot_format_bit(const struct cgit_snapshot_format *f); - -extern int cgit_open_filter(struct cgit_filter *filter, ...); -extern int cgit_close_filter(struct cgit_filter *filter); -extern void cgit_fprintf_filter(struct cgit_filter *filter, FILE *f, const char *prefix); -extern void cgit_exec_filter_init(struct cgit_exec_filter *filter, char *cmd, char **argv); -extern struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype); -extern void cgit_cleanup_filters(void); -extern void cgit_init_filters(void); - -extern void cgit_prepare_repo_env(struct cgit_repo * repo); - -extern int readfile(const char *path, char **buf, size_t *size); - -extern char *expand_macros(const char *txt); - -extern char *get_mimetype_for_filename(const char *filename); - -#endif /* CGIT_H */ diff --git a/cgit.mk b/cgit.mk deleted file mode 100644 index 3fcc1ca314..0000000000 --- a/cgit.mk +++ /dev/null @@ -1,141 +0,0 @@ -# This Makefile is run in the "git" directory in order to re-use Git's -# build variables and operating system detection. Hence all files in -# CGit's directory must be prefixed with "../". -include Makefile - -CGIT_PREFIX = ../ - --include $(CGIT_PREFIX)cgit.conf - -# The CGIT_* variables are inherited when this file is called from the -# main Makefile - they are defined there. - -$(CGIT_PREFIX)VERSION: force-version - @cd $(CGIT_PREFIX) && '$(SHELL_PATH_SQ)' ./gen-version.sh "$(CGIT_VERSION)" --include $(CGIT_PREFIX)VERSION -.PHONY: force-version - -# CGIT_CFLAGS is a separate variable so that we can track it separately -# and avoid rebuilding all of Git when these variables change. -CGIT_CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"' -CGIT_CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"' -CGIT_CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"' - -PKG_CONFIG ?= pkg-config - -ifdef NO_C99_FORMAT - CFLAGS += -DNO_C99_FORMAT -endif - -ifdef NO_LUA - LUA_MESSAGE := linking without specified Lua support - CGIT_CFLAGS += -DNO_LUA -else -ifeq ($(LUA_PKGCONFIG),) - LUA_PKGCONFIG := $(shell for pc in luajit lua lua5.2 lua5.1; do \ - $(PKG_CONFIG) --exists $$pc 2>/dev/null && echo $$pc && break; \ - done) - LUA_MODE := autodetected -else - LUA_MODE := specified -endif -ifneq ($(LUA_PKGCONFIG),) - LUA_MESSAGE := linking with $(LUA_MODE) $(LUA_PKGCONFIG) - LUA_LIBS := $(shell $(PKG_CONFIG) --libs $(LUA_PKGCONFIG) 2>/dev/null) - LUA_CFLAGS := $(shell $(PKG_CONFIG) --cflags $(LUA_PKGCONFIG) 2>/dev/null) - CGIT_LIBS += $(LUA_LIBS) - CGIT_CFLAGS += $(LUA_CFLAGS) -else - LUA_MESSAGE := linking without autodetected Lua support - NO_LUA := YesPlease - CGIT_CFLAGS += -DNO_LUA -endif - -endif - -# Add -ldl to linker flags on systems that commonly use GNU libc. -ifneq (,$(filter $(uname_S),Linux GNU GNU/kFreeBSD)) - CGIT_LIBS += -ldl -endif - -# glibc 2.1+ offers sendfile which the most common C library on Linux -ifeq ($(uname_S),Linux) - HAVE_LINUX_SENDFILE = YesPlease -endif - -ifdef HAVE_LINUX_SENDFILE - CGIT_CFLAGS += -DHAVE_LINUX_SENDFILE -endif - -CGIT_OBJ_NAMES += cgit.o -CGIT_OBJ_NAMES += cache.o -CGIT_OBJ_NAMES += cmd.o -CGIT_OBJ_NAMES += configfile.o -CGIT_OBJ_NAMES += filter.o -CGIT_OBJ_NAMES += html.o -CGIT_OBJ_NAMES += parsing.o -CGIT_OBJ_NAMES += scan-tree.o -CGIT_OBJ_NAMES += shared.o -CGIT_OBJ_NAMES += ui-atom.o -CGIT_OBJ_NAMES += ui-blame.o -CGIT_OBJ_NAMES += ui-blob.o -CGIT_OBJ_NAMES += ui-clone.o -CGIT_OBJ_NAMES += ui-commit.o -CGIT_OBJ_NAMES += ui-diff.o -CGIT_OBJ_NAMES += ui-log.o -CGIT_OBJ_NAMES += ui-patch.o -CGIT_OBJ_NAMES += ui-plain.o -CGIT_OBJ_NAMES += ui-refs.o -CGIT_OBJ_NAMES += ui-repolist.o -CGIT_OBJ_NAMES += ui-shared.o -CGIT_OBJ_NAMES += ui-snapshot.o -CGIT_OBJ_NAMES += ui-ssdiff.o -CGIT_OBJ_NAMES += ui-stats.o -CGIT_OBJ_NAMES += ui-summary.o -CGIT_OBJ_NAMES += ui-tag.o -CGIT_OBJ_NAMES += ui-tree.o - -CGIT_OBJS := $(addprefix $(CGIT_PREFIX),$(CGIT_OBJ_NAMES)) - -# Only cgit.c reference CGIT_VERSION so we only rebuild its objects when the -# version changes. -CGIT_VERSION_OBJS := $(addprefix $(CGIT_PREFIX),cgit.o cgit.sp) -$(CGIT_VERSION_OBJS): $(CGIT_PREFIX)VERSION -$(CGIT_VERSION_OBJS): EXTRA_CPPFLAGS = \ - -DCGIT_VERSION='"$(CGIT_VERSION)"' - -# Git handles dependencies using ":=" so dependencies in CGIT_OBJ are not -# handled by that and we must handle them ourselves. -cgit_dep_files := $(foreach f,$(CGIT_OBJS),$(dir $f).depend/$(notdir $f).d) -cgit_dep_files_present := $(wildcard $(cgit_dep_files)) -ifneq ($(cgit_dep_files_present),) -include $(cgit_dep_files_present) -endif - -ifeq ($(wildcard $(CGIT_PREFIX).depend),) -missing_dep_dirs += $(CGIT_PREFIX).depend -endif - -$(CGIT_PREFIX).depend: - @mkdir -p $@ - -$(CGIT_PREFIX)CGIT-CFLAGS: FORCE - @FLAGS='$(subst ','\'',$(CGIT_CFLAGS))'; \ - if test x"$$FLAGS" != x"`cat ../CGIT-CFLAGS 2>/dev/null`" ; then \ - echo 1>&2 " * new CGit build flags"; \ - echo "$$FLAGS" >$(CGIT_PREFIX)CGIT-CFLAGS; \ - fi - -$(CGIT_OBJS): %.o: %.c GIT-CFLAGS $(CGIT_PREFIX)CGIT-CFLAGS $(missing_dep_dirs) - $(QUIET_CC)$(CC) -o $*.o -c $(dep_args) $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $(CGIT_CFLAGS) $< - -$(CGIT_PREFIX)cgit: $(CGIT_OBJS) GIT-LDFLAGS $(GITLIBS) - @echo 1>&1 " * $(LUA_MESSAGE)" - $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS) $(CGIT_LIBS) - -CGIT_SP_OBJS := $(patsubst %.o,%.sp,$(CGIT_OBJS)) - -$(CGIT_SP_OBJS): %.sp: %.c GIT-CFLAGS $(CGIT_PREFIX)CGIT-CFLAGS FORCE - $(QUIET_SP)cgcc -no-compile $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $(CGIT_CFLAGS) $(SPARSE_FLAGS) $< - -cgit-sparse: $(CGIT_SP_OBJS) diff --git a/cgit.png b/cgit.png deleted file mode 100644 index 425528ee39..0000000000 --- a/cgit.png +++ /dev/null Binary files differdiff --git a/cgitrc.5.txt b/cgitrc.5.txt deleted file mode 100644 index ba77826fd0..0000000000 --- a/cgitrc.5.txt +++ /dev/null @@ -1,1008 +0,0 @@ -:man source: cgit -:man manual: cgit - -CGITRC(5) -======== - - -NAME ----- -cgitrc - runtime configuration for cgit - - -SYNOPSIS --------- -Cgitrc contains all runtime settings for cgit, including the list of git -repositories, formatted as a line-separated list of NAME=VALUE pairs. Blank -lines, and lines starting with '#', are ignored. - - -LOCATION --------- -The default location of cgitrc, defined at compile time, is /etc/cgitrc. At -runtime, cgit will consult the environment variable CGIT_CONFIG and, if -defined, use its value instead. - - -GLOBAL SETTINGS ---------------- -about-filter:: - Specifies a command which will be invoked to format the content of - about pages (both top-level and for each repository). The command will - get the content of the about-file on its STDIN, the name of the file - as the first argument, and the STDOUT from the command will be - included verbatim on the about page. Default value: none. See - also: "FILTER API". - -agefile:: - Specifies a path, relative to each repository path, which can be used - to specify the date and time of the youngest commit in the repository. - The first line in the file is used as input to the "parse_date" - function in libgit. Recommended timestamp-format is "yyyy-mm-dd - hh:mm:ss". You may want to generate this file from a post-receive - hook. Default value: "info/web/last-modified". - -auth-filter:: - Specifies a command that will be invoked for authenticating repository - access. Receives quite a few arguments, and data on both stdin and - stdout for authentication processing. Details follow later in this - document. If no auth-filter is specified, no authentication is - performed. Default value: none. See also: "FILTER API". - -branch-sort:: - Flag which, when set to "age", enables date ordering in the branch ref - list, and when set to "name" enables ordering by branch name. Default - value: "name". - -cache-about-ttl:: - Number which specifies the time-to-live, in minutes, for the cached - version of the repository about page. See also: "CACHE". Default - value: "15". - -cache-dynamic-ttl:: - Number which specifies the time-to-live, in minutes, for the cached - version of repository pages accessed without a fixed SHA1. See also: - "CACHE". Default value: "5". - -cache-repo-ttl:: - Number which specifies the time-to-live, in minutes, for the cached - version of the repository summary page. See also: "CACHE". Default - value: "5". - -cache-root:: - Path used to store the cgit cache entries. Default value: - "/var/cache/cgit". See also: "MACRO EXPANSION". - -cache-root-ttl:: - Number which specifies the time-to-live, in minutes, for the cached - version of the repository index page. See also: "CACHE". Default - value: "5". - -cache-scanrc-ttl:: - Number which specifies the time-to-live, in minutes, for the result - of scanning a path for git repositories. See also: "CACHE". Default - value: "15". - -case-sensitive-sort:: - Sort items in the repo list case sensitively. Default value: "1". - See also: repository-sort, section-sort. - -cache-size:: - The maximum number of entries in the cgit cache. When set to "0", - caching is disabled. See also: "CACHE". Default value: "0" - -cache-snapshot-ttl:: - Number which specifies the time-to-live, in minutes, for the cached - version of snapshots. See also: "CACHE". Default value: "5". - -cache-static-ttl:: - Number which specifies the time-to-live, in minutes, for the cached - version of repository pages accessed with a fixed SHA1. See also: - "CACHE". Default value: -1". - -clone-prefix:: - Space-separated list of common prefixes which, when combined with a - repository url, generates valid clone urls for the repository. This - setting is only used if `repo.clone-url` is unspecified. Default value: - none. - -clone-url:: - Space-separated list of clone-url templates. This setting is only - used if `repo.clone-url` is unspecified. Default value: none. See - also: "MACRO EXPANSION", "FILTER API". - -commit-filter:: - Specifies a command which will be invoked to format commit messages. - The command will get the message on its STDIN, and the STDOUT from the - command will be included verbatim as the commit message, i.e. this can - be used to implement bugtracker integration. Default value: none. - See also: "FILTER API". - -commit-sort:: - Flag which, when set to "date", enables strict date ordering in the - commit log, and when set to "topo" enables strict topological - ordering. If unset, the default ordering of "git log" is used. Default - value: unset. - -css:: - Url which specifies the css document to include in all cgit pages. - Default value: "/cgit.css". - -email-filter:: - Specifies a command which will be invoked to format names and email - address of committers, authors, and taggers, as represented in various - places throughout the cgit interface. This command will receive an - email address and an origin page string as its command line arguments, - and the text to format on STDIN. It is to write the formatted text back - out onto STDOUT. Default value: none. See also: "FILTER API". - -embedded:: - Flag which, when set to "1", will make cgit generate a html fragment - suitable for embedding in other html pages. Default value: none. See - also: "noheader". - -enable-blame:: - Flag which, when set to "1", will allow cgit to provide a "blame" page - for files, and will make it generate links to that page in appropriate - places. Default value: "0". - -enable-commit-graph:: - Flag which, when set to "1", will make cgit print an ASCII-art commit - history graph to the left of the commit messages in the repository - log page. Default value: "0". - -enable-filter-overrides:: - Flag which, when set to "1", allows all filter settings to be - overridden in repository-specific cgitrc files. Default value: none. - -enable-follow-links:: - Flag which, when set to "1", allows users to follow a file in the log - view. Default value: "0". - -enable-git-config:: - Flag which, when set to "1", will allow cgit to use git config to set - any repo specific settings. This option is used in conjunction with - "scan-path", and must be defined prior, to augment repo-specific - settings. The keys gitweb.owner, gitweb.category, gitweb.description, - and gitweb.homepage will map to the cgit keys repo.owner, repo.section, - repo.desc, and repo.homepage respectively. All git config keys that begin - with "cgit." will be mapped to the corresponding "repo." key in cgit. - Default value: "0". See also: scan-path, section-from-path. - -enable-http-clone:: - If set to "1", cgit will act as a dumb HTTP endpoint for git clones. - You can add "http://$HTTP_HOST$SCRIPT_NAME/$CGIT_REPO_URL" to clone-url - to expose this feature. If you use an alternate way of serving git - repositories, you may wish to disable this. Default value: "1". - -enable-html-serving:: - Flag which, when set to "1", will allow the /plain handler to serve - mimetype headers that result in the file being treated as HTML by the - browser. When set to "0", such file types are returned instead as - text/plain or application/octet-stream. Default value: "0". See also: - "repo.enable-html-serving". - -enable-index-links:: - Flag which, when set to "1", will make cgit generate extra links for - each repo in the repository index (specifically, to the "summary", - "commit" and "tree" pages). Default value: "0". - -enable-index-owner:: - Flag which, when set to "1", will make cgit display the owner of - each repo in the repository index. Default value: "1". - -enable-log-filecount:: - Flag which, when set to "1", will make cgit print the number of - modified files for each commit on the repository log page. Default - value: "0". - -enable-log-linecount:: - Flag which, when set to "1", will make cgit print the number of added - and removed lines for each commit on the repository log page. Default - value: "0". - -enable-remote-branches:: - Flag which, when set to "1", will make cgit display remote branches - in the summary and refs views. Default value: "0". See also: - "repo.enable-remote-branches". - -enable-subject-links:: - Flag which, when set to "1", will make cgit use the subject of the - parent commit as link text when generating links to parent commits - in commit view. Default value: "0". See also: - "repo.enable-subject-links". - -enable-tree-linenumbers:: - Flag which, when set to "1", will make cgit generate linenumber links - for plaintext blobs printed in the tree view. Default value: "1". - -favicon:: - Url used as link to a shortcut icon for cgit. It is suggested to use - the value "/favicon.ico" since certain browsers will ignore other - values. Default value: "/favicon.ico". - -footer:: - The content of the file specified with this option will be included - verbatim at the bottom of all pages (i.e. it replaces the standard - "generated by..." message. Default value: none. - -head-include:: - The content of the file specified with this option will be included - verbatim in the html HEAD section on all pages. Default value: none. - -header:: - The content of the file specified with this option will be included - verbatim at the top of all pages. Default value: none. - -include:: - Name of a configfile to include before the rest of the current config- - file is parsed. Default value: none. See also: "MACRO EXPANSION". - -local-time:: - Flag which, if set to "1", makes cgit print commit and tag times in the - servers timezone. Default value: "0". - -logo:: - Url which specifies the source of an image which will be used as a logo - on all cgit pages. Default value: "/cgit.png". - -logo-link:: - Url loaded when clicking on the cgit logo image. If unspecified the - calculated url of the repository index page will be used. Default - value: none. - -max-atom-items:: - Specifies the number of items to display in atom feeds view. Default - value: "10". - -max-blob-size:: - Specifies the maximum size of a blob to display HTML for in KBytes. - Default value: "0" (limit disabled). - -max-commit-count:: - Specifies the number of entries to list per page in "log" view. Default - value: "50". - -max-message-length:: - Specifies the maximum number of commit message characters to display in - "log" view. Default value: "80". - -max-repo-count:: - Specifies the number of entries to list per page on the repository - index page. Default value: "50". - -max-repodesc-length:: - Specifies the maximum number of repo description characters to display - on the repository index page. Default value: "80". - -max-stats:: - Set the default maximum statistics period. Valid values are "week", - "month", "quarter" and "year". If unspecified, statistics are - disabled. Default value: none. See also: "repo.max-stats". - -mimetype.<ext>:: - Set the mimetype for the specified filename extension. This is used - by the `plain` command when returning blob content. - -mimetype-file:: - Specifies the file to use for automatic mimetype lookup. If specified - then this field is used as a fallback when no "mimetype.<ext>" match is - found. If unspecified then no such lookup is performed. The typical file - to use on a Linux system is /etc/mime.types. The format of the file must - comply to: - - a comment line is an empty line or a line starting with a hash (#), - optionally preceded by whitespace - - a non-comment line starts with the mimetype (like image/png), followed - by one or more file extensions (like jpg), all separated by whitespace - Default value: none. See also: "mimetype.<ext>". - -module-link:: - Text which will be used as the formatstring for a hyperlink when a - submodule is printed in a directory listing. The arguments for the - formatstring are the path and SHA1 of the submodule commit. Default - value: none. - -noplainemail:: - If set to "1" showing full author email addresses will be disabled. - Default value: "0". - -noheader:: - Flag which, when set to "1", will make cgit omit the standard header - on all pages. Default value: none. See also: "embedded". - -owner-filter:: - Specifies a command which will be invoked to format the Owner - column of the main page. The command will get the owner on STDIN, - and the STDOUT from the command will be included verbatim in the - table. This can be used to link to additional context such as an - owners home page. When active this filter is used instead of the - default owner query url. Default value: none. - See also: "FILTER API". - -project-list:: - A list of subdirectories inside of scan-path, relative to it, that - should loaded as git repositories. This must be defined prior to - scan-path. Default value: none. See also: scan-path, "MACRO - EXPANSION". - -readme:: - Text which will be used as default value for "repo.readme". Multiple - config keys may be specified, and cgit will use the first found file - in this list. This is useful in conjunction with scan-path. Default - value: none. See also: scan-path, repo.readme. - -remove-suffix:: - If set to "1" and scan-path is enabled, if any repositories are found - with a suffix of ".git", this suffix will be removed for the url and - name. This must be defined prior to scan-path. Default value: "0". - See also: scan-path. - -renamelimit:: - Maximum number of files to consider when detecting renames. The value - "-1" uses the compiletime value in git (for further info, look at - `man git-diff`). Default value: "-1". - -repository-sort:: - The way in which repositories in each section are sorted. Valid values - are "name" for sorting by the repo name or "age" for sorting by the - most recently updated repository. Default value: "name". See also: - section, case-sensitive-sort, section-sort. - -robots:: - Text used as content for the "robots" meta-tag. Default value: - "index, nofollow". - -root-desc:: - Text printed below the heading on the repository index page. Default - value: "a fast webinterface for the git dscm". - -root-readme:: - The content of the file specified with this option will be included - verbatim below the "about" link on the repository index page. Default - value: none. - -root-title:: - Text printed as heading on the repository index page. Default value: - "Git Repository Browser". - -scan-hidden-path:: - If set to "1" and scan-path is enabled, scan-path will recurse into - directories whose name starts with a period ('.'). Otherwise, - scan-path will stay away from such directories (considered as - "hidden"). Note that this does not apply to the ".git" directory in - non-bare repos. This must be defined prior to scan-path. - Default value: 0. See also: scan-path. - -scan-path:: - A path which will be scanned for repositories. If caching is enabled, - the result will be cached as a cgitrc include-file in the cache - directory. If project-list has been defined prior to scan-path, - scan-path loads only the directories listed in the file pointed to by - project-list. Be advised that only the global settings taken - before the scan-path directive will be applied to each repository. - Default value: none. See also: cache-scanrc-ttl, project-list, - "MACRO EXPANSION". - -section:: - The name of the current repository section - all repositories defined - after this option will inherit the current section name. Default value: - none. - -section-sort:: - Flag which, when set to "1", will sort the sections on the repository - listing by name. Set this flag to "0" if the order in the cgitrc file should - be preserved. Default value: "1". See also: section, - case-sensitive-sort, repository-sort. - -section-from-path:: - A number which, if defined prior to scan-path, specifies how many - path elements from each repo path to use as a default section name. - If negative, cgit will discard the specified number of path elements - above the repo directory. Default value: "0". - -side-by-side-diffs:: - If set to "1" shows side-by-side diffs instead of unidiffs per - default. Default value: "0". - -snapshots:: - Text which specifies the default set of snapshot formats that cgit - generates links for. The value is a space-separated list of zero or - more of the values "tar", "tar.gz", "tar.bz2", "tar.xz" and "zip". - The special value "all" enables all snapshot formats. - Default value: none. - -source-filter:: - Specifies a command which will be invoked to format plaintext blobs - in the tree view. The command will get the blob content on its STDIN - and the name of the blob as its only command line argument. The STDOUT - from the command will be included verbatim as the blob contents, i.e. - this can be used to implement e.g. syntax highlighting. Default value: - none. See also: "FILTER API". - -summary-branches:: - Specifies the number of branches to display in the repository "summary" - view. Default value: "10". - -summary-log:: - Specifies the number of log entries to display in the repository - "summary" view. Default value: "10". - -summary-tags:: - Specifies the number of tags to display in the repository "summary" - view. Default value: "10". - -strict-export:: - Filename which, if specified, needs to be present within the repository - for cgit to allow access to that repository. This can be used to emulate - gitweb's EXPORT_OK and STRICT_EXPORT functionality and limit cgit's - repositories to match those exported by git-daemon. This option must - be defined prior to scan-path. - -virtual-root:: - Url which, if specified, will be used as root for all cgit links. It - will also cause cgit to generate 'virtual urls', i.e. urls like - '/cgit/tree/README' as opposed to '?r=cgit&p=tree&path=README'. Default - value: none. - NOTE: cgit has recently learned how to use PATH_INFO to achieve the - same kind of virtual urls, so this option will probably be deprecated. - - -REPOSITORY SETTINGS -------------------- -repo.about-filter:: - Override the default about-filter. Default value: none. See also: - "enable-filter-overrides". See also: "FILTER API". - -repo.branch-sort:: - Flag which, when set to "age", enables date ordering in the branch ref - list, and when set to "name" enables ordering by branch name. Default - value: "name". - -repo.clone-url:: - A list of space-separated urls which can be used to clone this repo. - Default value: none. See also: "MACRO EXPANSION". - -repo.commit-filter:: - Override the default commit-filter. Default value: none. See also: - "enable-filter-overrides". See also: "FILTER API". - -repo.commit-sort:: - Flag which, when set to "date", enables strict date ordering in the - commit log, and when set to "topo" enables strict topological - ordering. If unset, the default ordering of "git log" is used. Default - value: unset. - -repo.defbranch:: - The name of the default branch for this repository. If no such branch - exists in the repository, the first branch name (when sorted) is used - as default instead. Default value: branch pointed to by HEAD, or - "master" if there is no suitable HEAD. - -repo.desc:: - The value to show as repository description. Default value: none. - -repo.email-filter:: - Override the default email-filter. Default value: none. See also: - "enable-filter-overrides". See also: "FILTER API". - -repo.enable-blame:: - A flag which can be used to disable the global setting - `enable-blame'. Default value: none. - -repo.enable-commit-graph:: - A flag which can be used to disable the global setting - `enable-commit-graph'. Default value: none. - -repo.enable-html-serving:: - A flag which can be used to override the global setting - `enable-html-serving`. Default value: none. - -repo.enable-log-filecount:: - A flag which can be used to disable the global setting - `enable-log-filecount'. Default value: none. - -repo.enable-log-linecount:: - A flag which can be used to disable the global setting - `enable-log-linecount'. Default value: none. - -repo.enable-remote-branches:: - Flag which, when set to "1", will make cgit display remote branches - in the summary and refs views. Default value: <enable-remote-branches>. - -repo.enable-subject-links:: - A flag which can be used to override the global setting - `enable-subject-links'. Default value: none. - -repo.extra-head-content:: - This value will be added verbatim to the head section of each page - displayed for this repo. Default value: none. - -repo.hide:: - Flag which, when set to "1", hides the repository from the repository - index. The repository can still be accessed by providing a direct path. - Default value: "0". See also: "repo.ignore". - -repo.homepage:: - The value to show as repository homepage. Default value: none. - -repo.ignore:: - Flag which, when set to "1", ignores the repository. The repository - is not shown in the index and cannot be accessed by providing a direct - path. Default value: "0". See also: "repo.hide". - -repo.logo:: - Url which specifies the source of an image which will be used as a logo - on this repo's pages. Default value: global logo. - -repo.logo-link:: - Url loaded when clicking on the cgit logo image. If unspecified the - calculated url of the repository index page will be used. Default - value: global logo-link. - -repo.module-link:: - Text which will be used as the formatstring for a hyperlink when a - submodule is printed in a directory listing. The arguments for the - formatstring are the path and SHA1 of the submodule commit. Default - value: <module-link> - -repo.module-link.<path>:: - Text which will be used as the formatstring for a hyperlink when a - submodule with the specified subdirectory path is printed in a - directory listing. The only argument for the formatstring is the SHA1 - of the submodule commit. Default value: none. - -repo.max-stats:: - Override the default maximum statistics period. Valid values are equal - to the values specified for the global "max-stats" setting. Default - value: none. - -repo.name:: - The value to show as repository name. Default value: <repo.url>. - -repo.owner:: - A value used to identify the owner of the repository. Default value: - none. - -repo.owner-filter:: - Override the default owner-filter. Default value: none. See also: - "enable-filter-overrides". See also: "FILTER API". - -repo.path:: - An absolute path to the repository directory. For non-bare repositories - this is the .git-directory. Default value: none. - -repo.readme:: - A path (relative to <repo.path>) which specifies a file to include - verbatim as the "About" page for this repo. You may also specify a - git refspec by head or by hash by prepending the refspec followed by - a colon. For example, "master:docs/readme.mkd". If the value begins - with a colon, i.e. ":docs/readme.rst", the default branch of the - repository will be used. Sharing any file will expose that entire - directory tree to the "/about/PATH" endpoints, so be sure that there - are no non-public files located in the same directory as the readme - file. Default value: <readme>. - -repo.section:: - Override the current section name for this repository. Default value: - none. - -repo.snapshots:: - A mask of snapshot formats for this repo that cgit generates links for, - restricted by the global "snapshots" setting. Default value: - <snapshots>. - -repo.snapshot-prefix:: - Prefix to use for snapshot links instead of the repository basename. - For example, the "linux-stable" repository may wish to set this to - "linux" so that snapshots are in the format "linux-3.15.4" instead - of "linux-stable-3.15.4". Default value: <empty> meaning to use - the repository basename. - -repo.source-filter:: - Override the default source-filter. Default value: none. See also: - "enable-filter-overrides". See also: "FILTER API". - -repo.url:: - The relative url used to access the repository. This must be the first - setting specified for each repo. Default value: none. - - -REPOSITORY-SPECIFIC CGITRC FILE -------------------------------- -When the option "scan-path" is used to auto-discover git repositories, cgit -will try to parse the file "cgitrc" within any found repository. Such a -repo-specific config file may contain any of the repo-specific options -described above, except "repo.url" and "repo.path". Additionally, the "filter" -options are only acknowledged in repo-specific config files when -"enable-filter-overrides" is set to "1". - -Note: the "repo." prefix is dropped from the option names in repo-specific -config files, e.g. "repo.desc" becomes "desc". - - -FILTER API ----------- -By default, filters are separate processes that are executed each time they -are needed. Alternative technologies may be used by prefixing the filter -specification with the relevant string; available values are: - -'exec:':: - The default "one process per filter" mode. - -'lua:':: - Executes the script using a built-in Lua interpreter. The script is - loaded once per execution of cgit, and may be called multiple times - during cgit's lifetime, making it a good choice for repeated filters - such as the 'email filter'. It responds to three functions: - - 'filter_open(argument1, argument2, argument3, ...)':: - This is called upon activation of the filter for a particular - set of data. - 'filter_write(buffer)':: - This is called whenever cgit writes data to the webpage. - 'filter_close()':: - This is called when the current filtering operation is - completed. It must return an integer value. Usually 0 - indicates success. - - Additionally, cgit exposes to the Lua the following built-in functions: - - 'html(str)':: - Writes 'str' to the webpage. - 'html_txt(str)':: - HTML escapes and writes 'str' to the webpage. - 'html_attr(str)':: - HTML escapes for an attribute and writes "str' to the webpage. - 'html_url_path(str)':: - URL escapes for a path and writes 'str' to the webpage. - 'html_url_arg(str)':: - URL escapes for an argument and writes 'str' to the webpage. - 'html_include(file)':: - Includes 'file' in webpage. - - -Parameters are provided to filters as follows. - -about filter:: - This filter is given a single parameter: the filename of the source - file to filter. The filter can use the filename to determine (for - example) the type of syntax to follow when formatting the readme file. - The about text that is to be filtered is available on standard input - and the filtered text is expected on standard output. - -auth filter:: - The authentication filter receives 12 parameters: - - filter action, explained below, which specifies which action the - filter is called for - - http cookie - - http method - - http referer - - http path - - http https flag - - cgit repo - - cgit page - - cgit url - - cgit login url - When the filter action is "body", this filter must write to output the - HTML for displaying the login form, which POSTs to the login url. When - the filter action is "authenticate-cookie", this filter must validate - the http cookie and return a 0 if it is invalid or 1 if it is invalid, - in the exit code / close function. If the filter action is - "authenticate-post", this filter receives POST'd parameters on - standard input, and should write a complete CGI response, preferably - with a 302 redirect, and write to output one or more "Set-Cookie" - HTTP headers, each followed by a newline. - - Please see `filters/simple-authentication.lua` for a clear example - script that may be modified. - -commit filter:: - This filter is given no arguments. The commit message text that is to - be filtered is available on standard input and the filtered text is - expected on standard output. - -email filter:: - This filter is given two parameters: the email address of the relevant - author and a string indicating the originating page. The filter will - then receive the text string to format on standard input and is - expected to write to standard output the formatted text to be included - in the page. - -owner filter:: - This filter is given no arguments. The owner text is available on - standard input and the filter is expected to write to standard - output. The output is included in the Owner column. - -source filter:: - This filter is given a single parameter: the filename of the source - file to filter. The filter can use the filename to determine (for - example) the syntax highlighting mode. The contents of the source - file that is to be filtered is available on standard input and the - filtered contents is expected on standard output. - - -All filters are handed the following environment variables: - -- CGIT_REPO_URL (from repo.url) -- CGIT_REPO_NAME (from repo.name) -- CGIT_REPO_PATH (from repo.path) -- CGIT_REPO_OWNER (from repo.owner) -- CGIT_REPO_DEFBRANCH (from repo.defbranch) -- CGIT_REPO_SECTION (from repo.section) -- CGIT_REPO_CLONE_URL (from repo.clone-url) - -If a setting is not defined for a repository and the corresponding global -setting is also not defined (if applicable), then the corresponding -environment variable will be unset. - - -MACRO EXPANSION ---------------- -The following cgitrc options support a simple macro expansion feature, -where tokens prefixed with "$" are replaced with the value of a similarly -named environment variable: - -- cache-root -- include -- project-list -- scan-path - -Macro expansion will also happen on the content of $CGIT_CONFIG, if -defined. - -One usage of this feature is virtual hosting, which in its simplest form -can be accomplished by adding the following line to /etc/cgitrc: - - include=/etc/cgitrc.d/$HTTP_HOST - -The following options are expanded during request processing, and support -the environment variables defined in "FILTER API": - -- clone-url -- repo.clone-url - - -CACHE ------ - -All cache ttl values are in minutes. Negative ttl values indicate that a page -type will never expire, and thus the first time a URL is accessed, the result -will be cached indefinitely, even if the underlying git repository changes. -Conversely, when a ttl value is zero, the cache is disabled for that -particular page type, and the page type is never cached. - -SIGNATURES ----------- - -Cgit can host .asc signatures corresponding to various snapshot formats, -through use of git notes. For example, the following command may be used to -add a signature to a .tar.xz archive: - - git notes --ref=refs/notes/signatures/tar.xz add -C "$( - gpg --output - --armor --detach-sign cgit-1.1.tar.xz | - git hash-object -w --stdin - )" v1.1 - -If it is instead desirable to attach a signature of the underlying .tar, this -will be linked, as a special case, beside a .tar.* link that does not have its -own signature. For example, a signature of a tarball of the latest tag might -be added with a similar command: - - tag="$(git describe --abbrev=0)" - git notes --ref=refs/notes/signatures/tar add -C "$( - git archive --format tar --prefix "cgit-${tag#v}/" "$tag" | - gpg --output - --armor --detach-sign | - git hash-object -w --stdin - )" "$tag" - -Since git-archive(1) is expected to produce stable output between versions, -this allows one to generate a long-term signature of the contents of a given -tag. - -EXAMPLE CGITRC FILE -------------------- - -.... -# Enable caching of up to 1000 output entries -cache-size=1000 - - -# Specify some default clone urls using macro expansion -clone-url=git://foo.org/$CGIT_REPO_URL git@foo.org:$CGIT_REPO_URL - -# Specify the css url -css=/css/cgit.css - - -# Show owner on index page -enable-index-owner=1 - - -# Allow http transport git clone -enable-http-clone=1 - - -# Show extra links for each repository on the index page -enable-index-links=1 - - -# Enable blame page and create links to it from tree page -enable-blame=1 - - -# Enable ASCII art commit history graph on the log pages -enable-commit-graph=1 - - -# Show number of affected files per commit on the log pages -enable-log-filecount=1 - - -# Show number of added/removed lines per commit on the log pages -enable-log-linecount=1 - - -# Sort branches by date -branch-sort=age - - -# Add a cgit favicon -favicon=/favicon.ico - - -# Use a custom logo -logo=/img/mylogo.png - - -# Enable statistics per week, month and quarter -max-stats=quarter - - -# Set the title and heading of the repository index page -root-title=example.com git repositories - - -# Set a subheading for the repository index page -root-desc=tracking the foobar development - - -# Include some more info about example.com on the index page -root-readme=/var/www/htdocs/about.html - - -# Allow download of tar.gz, tar.bz2 and zip-files -snapshots=tar.gz tar.bz2 zip - - -## -## List of common mimetypes -## - -mimetype.gif=image/gif -mimetype.html=text/html -mimetype.jpg=image/jpeg -mimetype.jpeg=image/jpeg -mimetype.pdf=application/pdf -mimetype.png=image/png -mimetype.svg=image/svg+xml - - -# Highlight source code with python pygments-based highlighter -source-filter=/var/www/cgit/filters/syntax-highlighting.py - -# Format markdown, restructuredtext, manpages, text files, and html files -# through the right converters -about-filter=/var/www/cgit/filters/about-formatting.sh - -## -## Search for these files in the root of the default branch of repositories -## for coming up with the about page: -## -readme=:README.md -readme=:readme.md -readme=:README.mkd -readme=:readme.mkd -readme=:README.rst -readme=:readme.rst -readme=:README.html -readme=:readme.html -readme=:README.htm -readme=:readme.htm -readme=:README.txt -readme=:readme.txt -readme=:README -readme=:readme -readme=:INSTALL.md -readme=:install.md -readme=:INSTALL.mkd -readme=:install.mkd -readme=:INSTALL.rst -readme=:install.rst -readme=:INSTALL.html -readme=:install.html -readme=:INSTALL.htm -readme=:install.htm -readme=:INSTALL.txt -readme=:install.txt -readme=:INSTALL -readme=:install - - -## -## List of repositories. -## PS: Any repositories listed when section is unset will not be -## displayed under a section heading -## PPS: This list could be kept in a different file (e.g. '/etc/cgitrepos') -## and included like this: -## include=/etc/cgitrepos -## - - -repo.url=foo -repo.path=/pub/git/foo.git -repo.desc=the master foo repository -repo.owner=fooman@example.com -repo.readme=info/web/about.html - - -repo.url=bar -repo.path=/pub/git/bar.git -repo.desc=the bars for your foo -repo.owner=barman@example.com -repo.readme=info/web/about.html - - -# The next repositories will be displayed under the 'extras' heading -section=extras - - -repo.url=baz -repo.path=/pub/git/baz.git -repo.desc=a set of extensions for bar users - -repo.url=wiz -repo.path=/pub/git/wiz.git -repo.desc=the wizard of foo - - -# Add some mirrored repositories -section=mirrors - - -repo.url=git -repo.path=/pub/git/git.git -repo.desc=the dscm - - -repo.url=linux -repo.path=/pub/git/linux.git -repo.desc=the kernel - -# Disable adhoc downloads of this repo -repo.snapshots=0 - -# Disable line-counts for this repo -repo.enable-log-linecount=0 - -# Restrict the max statistics period for this repo -repo.max-stats=month -.... - - -BUGS ----- -Comments currently cannot appear on the same line as a setting; the comment -will be included as part of the value. E.g. this line: - - robots=index # allow indexing - -will generate the following html element: - - <meta name='robots' content='index # allow indexing'/> - - - -AUTHOR ------- -Lars Hjemli <hjemli@gmail.com> -Jason A. Donenfeld <Jason@zx2c4.com> diff --git a/cmd.c b/cmd.c deleted file mode 100644 index bf6d8f516f..0000000000 --- a/cmd.c +++ /dev/null @@ -1,208 +0,0 @@ -/* cmd.c: the cgit command dispatcher - * - * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "cmd.h" -#include "cache.h" -#include "ui-shared.h" -#include "ui-atom.h" -#include "ui-blame.h" -#include "ui-blob.h" -#include "ui-clone.h" -#include "ui-commit.h" -#include "ui-diff.h" -#include "ui-log.h" -#include "ui-patch.h" -#include "ui-plain.h" -#include "ui-refs.h" -#include "ui-repolist.h" -#include "ui-snapshot.h" -#include "ui-stats.h" -#include "ui-summary.h" -#include "ui-tag.h" -#include "ui-tree.h" - -static void HEAD_fn(void) -{ - cgit_clone_head(); -} - -static void atom_fn(void) -{ - cgit_print_atom(ctx.qry.head, ctx.qry.path, ctx.cfg.max_atom_items); -} - -static void about_fn(void) -{ - if (ctx.repo) { - size_t path_info_len = ctx.env.path_info ? strlen(ctx.env.path_info) : 0; - if (!ctx.qry.path && - ctx.qry.url[strlen(ctx.qry.url) - 1] != '/' && - (!path_info_len || ctx.env.path_info[path_info_len - 1] != '/')) { - char *currenturl = cgit_currenturl(); - char *redirect = fmtalloc("%s/", currenturl); - cgit_redirect(redirect, true); - free(currenturl); - free(redirect); - } else if (ctx.repo->readme.nr) - cgit_print_repo_readme(ctx.qry.path); - else if (ctx.repo->homepage) - cgit_redirect(ctx.repo->homepage, false); - else { - char *currenturl = cgit_currenturl(); - char *redirect = fmtalloc("%s../", currenturl); - cgit_redirect(redirect, false); - free(currenturl); - free(redirect); - } - } else - cgit_print_site_readme(); -} - -static void blame_fn(void) -{ - if (ctx.repo->enable_blame) - cgit_print_blame(); - else - cgit_print_error_page(403, "Forbidden", "Blame is disabled"); -} - -static void blob_fn(void) -{ - cgit_print_blob(ctx.qry.sha1, ctx.qry.path, ctx.qry.head, 0); -} - -static void commit_fn(void) -{ - cgit_print_commit(ctx.qry.sha1, ctx.qry.path); -} - -static void diff_fn(void) -{ - cgit_print_diff(ctx.qry.sha1, ctx.qry.sha2, ctx.qry.path, 1, 0); -} - -static void rawdiff_fn(void) -{ - cgit_print_diff(ctx.qry.sha1, ctx.qry.sha2, ctx.qry.path, 1, 1); -} - -static void info_fn(void) -{ - cgit_clone_info(); -} - -static void log_fn(void) -{ - cgit_print_log(ctx.qry.sha1, ctx.qry.ofs, ctx.cfg.max_commit_count, - ctx.qry.grep, ctx.qry.search, ctx.qry.path, 1, - ctx.repo->enable_commit_graph, - ctx.repo->commit_sort); -} - -static void ls_cache_fn(void) -{ - ctx.page.mimetype = "text/plain"; - ctx.page.filename = "ls-cache.txt"; - cgit_print_http_headers(); - cache_ls(ctx.cfg.cache_root); -} - -static void objects_fn(void) -{ - cgit_clone_objects(); -} - -static void repolist_fn(void) -{ - cgit_print_repolist(); -} - -static void patch_fn(void) -{ - cgit_print_patch(ctx.qry.sha1, ctx.qry.sha2, ctx.qry.path); -} - -static void plain_fn(void) -{ - cgit_print_plain(); -} - -static void refs_fn(void) -{ - cgit_print_refs(); -} - -static void snapshot_fn(void) -{ - cgit_print_snapshot(ctx.qry.head, ctx.qry.sha1, ctx.qry.path, - ctx.qry.nohead); -} - -static void stats_fn(void) -{ - cgit_show_stats(); -} - -static void summary_fn(void) -{ - cgit_print_summary(); -} - -static void tag_fn(void) -{ - cgit_print_tag(ctx.qry.sha1); -} - -static void tree_fn(void) -{ - cgit_print_tree(ctx.qry.sha1, ctx.qry.path); -} - -#define def_cmd(name, want_repo, want_vpath, is_clone) \ - {#name, name##_fn, want_repo, want_vpath, is_clone} - -struct cgit_cmd *cgit_get_cmd(void) -{ - static struct cgit_cmd cmds[] = { - def_cmd(HEAD, 1, 0, 1), - def_cmd(atom, 1, 0, 0), - def_cmd(about, 0, 0, 0), - def_cmd(blame, 1, 1, 0), - def_cmd(blob, 1, 0, 0), - def_cmd(commit, 1, 1, 0), - def_cmd(diff, 1, 1, 0), - def_cmd(info, 1, 0, 1), - def_cmd(log, 1, 1, 0), - def_cmd(ls_cache, 0, 0, 0), - def_cmd(objects, 1, 0, 1), - def_cmd(patch, 1, 1, 0), - def_cmd(plain, 1, 0, 0), - def_cmd(rawdiff, 1, 1, 0), - def_cmd(refs, 1, 0, 0), - def_cmd(repolist, 0, 0, 0), - def_cmd(snapshot, 1, 0, 0), - def_cmd(stats, 1, 1, 0), - def_cmd(summary, 1, 0, 0), - def_cmd(tag, 1, 0, 0), - def_cmd(tree, 1, 1, 0), - }; - int i; - - if (ctx.qry.page == NULL) { - if (ctx.repo) - ctx.qry.page = "summary"; - else - ctx.qry.page = "repolist"; - } - - for (i = 0; i < sizeof(cmds)/sizeof(*cmds); i++) - if (!strcmp(ctx.qry.page, cmds[i].name)) - return &cmds[i]; - return NULL; -} diff --git a/cmd.h b/cmd.h deleted file mode 100644 index 6249b1d892..0000000000 --- a/cmd.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef CMD_H -#define CMD_H - -typedef void (*cgit_cmd_fn)(void); - -struct cgit_cmd { - const char *name; - cgit_cmd_fn fn; - unsigned int want_repo:1, - want_vpath:1, - is_clone:1; -}; - -extern struct cgit_cmd *cgit_get_cmd(void); - -#endif /* CMD_H */ diff --git a/configfile.c b/configfile.c deleted file mode 100644 index e0391091e1..0000000000 --- a/configfile.c +++ /dev/null @@ -1,90 +0,0 @@ -/* configfile.c: parsing of config files - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include <git-compat-util.h> -#include "configfile.h" - -static int next_char(FILE *f) -{ - int c = fgetc(f); - if (c == '\r') { - c = fgetc(f); - if (c != '\n') { - ungetc(c, f); - c = '\r'; - } - } - return c; -} - -static void skip_line(FILE *f) -{ - int c; - - while ((c = next_char(f)) && c != '\n' && c != EOF) - ; -} - -static int read_config_line(FILE *f, struct strbuf *name, struct strbuf *value) -{ - int c = next_char(f); - - strbuf_reset(name); - strbuf_reset(value); - - /* Skip comments and preceding spaces. */ - for(;;) { - if (c == EOF) - return 0; - else if (c == '#' || c == ';') - skip_line(f); - else if (!isspace(c)) - break; - c = next_char(f); - } - - /* Read variable name. */ - while (c != '=') { - if (c == '\n' || c == EOF) - return 0; - strbuf_addch(name, c); - c = next_char(f); - } - - /* Read variable value. */ - c = next_char(f); - while (c != '\n' && c != EOF) { - strbuf_addch(value, c); - c = next_char(f); - } - - return 1; -} - -int parse_configfile(const char *filename, configfile_value_fn fn) -{ - static int nesting; - struct strbuf name = STRBUF_INIT; - struct strbuf value = STRBUF_INIT; - FILE *f; - - /* cancel deeply nested include-commands */ - if (nesting > 8) - return -1; - if (!(f = fopen(filename, "r"))) - return -1; - nesting++; - while (read_config_line(f, &name, &value)) - fn(name.buf, value.buf); - nesting--; - fclose(f); - strbuf_release(&name); - strbuf_release(&value); - return 0; -} - diff --git a/configfile.h b/configfile.h deleted file mode 100644 index af7ca19735..0000000000 --- a/configfile.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef CONFIGFILE_H -#define CONFIGFILE_H - -#include "cgit.h" - -typedef void (*configfile_value_fn)(const char *name, const char *value); - -extern int parse_configfile(const char *filename, configfile_value_fn fn); - -#endif /* CONFIGFILE_H */ diff --git a/contrib/hooks/post-receive.agefile b/contrib/hooks/post-receive.agefile deleted file mode 100755 index 2f72ae9c0d..0000000000 --- a/contrib/hooks/post-receive.agefile +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -# -# An example hook to update the "agefile" for CGit's idle time calculation. -# -# This hook assumes that you are using the default agefile location of -# "info/web/last-modified". If you change the value in your cgitrc then you -# must also change it here. -# -# To install the hook, copy (or link) it to the file "hooks/post-receive" in -# each of your repositories. -# - -agefile="$(git rev-parse --git-dir)"/info/web/last-modified - -mkdir -p "$(dirname "$agefile")" && -git for-each-ref \ - --sort=-authordate --count=1 \ - --format='%(authordate:iso8601)' \ - >"$agefile" diff --git a/exwm-background.el b/exwm-background.el new file mode 100644 index 0000000000..fa663d8fe6 --- /dev/null +++ b/exwm-background.el @@ -0,0 +1,199 @@ +;;; exwm-background.el --- X Background Module for EXWM -*- lexical-binding: t -*- + +;; Copyright (C) 2022-2024 Free Software Foundation, Inc. + +;; Author: Steven Allen <steven@stebalien.com> + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This module adds X background color setting support to EXWM. + +;; To use this module, load and enable it as follows: +;; (require 'exwm-background) +;; (exwm-background-enable) +;; +;; By default, this will apply the theme's background color. However, that +;; color can be customized via the `exwm-background-color' setting. + +;;; Code: + +(require 'exwm-core) + +(defcustom exwm-background-color nil + "Background color for Xorg." + :type '(choice + (color :tag "Background Color") + (const :tag "Default" nil)) + :group 'exwm + :initialize #'custom-initialize-default + :set (lambda (symbol value) + (set-default-toplevel-value symbol value) + (exwm-background--update))) + +(defconst exwm-background--properties '("_XROOTPMAP_ID" "_XSETROOT_ID" "ESETROOT_PMAP_ID") + "The background properties to set. +We can't need to set these so that compositing window managers +can correctly display the background color.") + +(defvar exwm-background--connection nil + "The X connection used for setting the background. +We use a separate connection as other background-setting tools +may kill this connection when they replace it.") + +(defvar exwm-background--pixmap nil + "Cached background pixmap.") + +(defvar exwm-background--atoms nil + "Cached background atoms.") + +(defun exwm-background--update (&rest _) + "Update the EXWM background." + + ;; Always reconnect as any tool that sets the background may have disconnected us (to force X to + ;; free resources). + (exwm-background--connect) + + (let ((gc (xcb:generate-id exwm-background--connection)) + (color (exwm--color->pixel (or exwm-background-color + (face-background 'default))))) + ;; Fill the pixmap. + (xcb:+request exwm-background--connection + (make-instance 'xcb:CreateGC + :cid gc :drawable exwm-background--pixmap + :value-mask (logior xcb:GC:Foreground + xcb:GC:GraphicsExposures) + :foreground color + :graphics-exposures 0)) + + (xcb:+request exwm-background--connection + (make-instance 'xcb:PolyFillRectangle + :gc gc :drawable exwm-background--pixmap + :rectangles + (list + (make-instance + 'xcb:RECTANGLE + :x 0 :y 0 :width 1 :height 1)))) + (xcb:+request exwm-background--connection (make-instance 'xcb:FreeGC :gc gc))) + + ;; Reapply it to force an update (also clobber anyone else who may have set it). + (xcb:+request exwm-background--connection + (make-instance 'xcb:ChangeWindowAttributes + :window exwm--root + :value-mask xcb:CW:BackPixmap + :background-pixmap exwm-background--pixmap)) + + (let (old) + ;; Collect old pixmaps so we can kill other background clients (all the background setting tools + ;; seem to do this). + (dolist (atom exwm-background--atoms) + (when-let* ((reply (xcb:+request-unchecked+reply exwm-background--connection + (make-instance 'xcb:GetProperty + :delete 0 + :window exwm--root + :property atom + :type xcb:Atom:PIXMAP + :long-offset 0 + :long-length 1))) + (value (vconcat (slot-value reply 'value))) + ((length= value 4)) + (pixmap (funcall (if xcb:lsb #'xcb:-unpack-u4-lsb #'xcb:-unpack-u4) + value 0)) + ((not (or (= pixmap exwm-background--pixmap) + (member pixmap old))))) + (push pixmap old))) + + ;; Change the background. + (dolist (atom exwm-background--atoms) + (xcb:+request exwm-background--connection + (make-instance 'xcb:ChangeProperty + :window exwm--root + :property atom + :type xcb:Atom:PIXMAP + :format 32 + :mode xcb:PropMode:Replace + :data-len 1 + :data + (funcall (if xcb:lsb + #'xcb:-pack-u4-lsb + #'xcb:-pack-u4) + exwm-background--pixmap)))) + + ;; Kill the old background clients. + (dolist (pixmap old) + (xcb:+request exwm-background--connection + (make-instance 'xcb:KillClient :resource pixmap)))) + + (xcb:flush exwm-background--connection)) + +(defun exwm-background--connected-p () + (and exwm-background--connection + (process-live-p (slot-value exwm-background--connection 'process)))) + +(defun exwm-background--connect () + (unless (exwm-background--connected-p) + (setq exwm-background--connection (xcb:connect)) + ;;prevent query message on exit + (set-process-query-on-exit-flag (slot-value exwm-background--connection 'process) nil) + + ;; Intern the background property atoms. + (setq exwm-background--atoms + (mapcar + (lambda (prop) (exwm--intern-atom prop exwm-background--connection)) + exwm-background--properties)) + + ;; Create the pixmap. + (setq exwm-background--pixmap (xcb:generate-id exwm-background--connection)) + (xcb:+request exwm-background--connection + (make-instance 'xcb:CreatePixmap + :depth + (slot-value + (xcb:+request-unchecked+reply exwm-background--connection + (make-instance 'xcb:GetGeometry :drawable exwm--root)) + 'depth) + :pid exwm-background--pixmap + :drawable exwm--root + :width 1 :height 1)))) + +(defun exwm-background--init () + "Initialize background module." + (exwm--log) + (add-hook 'enable-theme-functions 'exwm-background--update) + (add-hook 'disable-theme-functions 'exwm-background--update) + (exwm-background--update)) + +(defun exwm-background--exit () + "Uninitialize the background module." + (exwm--log) + (remove-hook 'enable-theme-functions 'exwm-background--update) + (remove-hook 'disable-theme-functions 'exwm-background--update) + (when (and exwm-background--connection + (slot-value exwm-background--connection 'connected)) + (xcb:disconnect exwm-background--connection)) + (setq exwm-background--pixmap nil + exwm-background--connection nil + exwm-background--atoms nil)) + +(defun exwm-background-enable () + "Enable background support for EXWM." + (exwm--log) + (add-hook 'exwm-init-hook #'exwm-background--init) + (add-hook 'exwm-exit-hook #'exwm-background--exit)) + +(provide 'exwm-background) + +;;; exwm-background.el ends here diff --git a/exwm-config.el b/exwm-config.el new file mode 100644 index 0000000000..a9f21e9c8c --- /dev/null +++ b/exwm-config.el @@ -0,0 +1,131 @@ +;;; exwm-config.el --- Predefined configurations -*- lexical-binding: t -*- + +;; Copyright (C) 2015-2024 Free Software Foundation, Inc. + +;; Author: Chris Feng <chris.w.feng@gmail.com> + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This module contains typical (yet minimal) configurations of EXWM. + +;;; Code: + +(require 'exwm) +(require 'ido) + +(define-obsolete-function-alias 'exwm-config-default + #'exwm-config-example "27.1") + +(defun exwm-config-example () + "Default configuration of EXWM." + ;; Set the initial workspace number. + (unless (get 'exwm-workspace-number 'saved-value) + (setq exwm-workspace-number 4)) + ;; Make class name the buffer name + (add-hook 'exwm-update-class-hook + (lambda () + (exwm-workspace-rename-buffer exwm-class-name))) + ;; Global keybindings. + (unless (get 'exwm-input-global-keys 'saved-value) + (setq exwm-input-global-keys + `( + ;; 's-r': Reset (to line-mode). + ([?\s-r] . exwm-reset) + ;; 's-w': Switch workspace. + ([?\s-w] . exwm-workspace-switch) + ;; 's-&': Launch application. + ([?\s-&] . (lambda (command) + (interactive (list (read-shell-command "$ "))) + (start-process-shell-command command nil command))) + ;; 's-N': Switch to certain workspace. + ,@(mapcar (lambda (i) + `(,(kbd (format "s-%d" i)) . + (lambda () + (interactive) + (exwm-workspace-switch-create ,i)))) + (number-sequence 0 9))))) + ;; Line-editing shortcuts + (unless (get 'exwm-input-simulation-keys 'saved-value) + (setq exwm-input-simulation-keys + '(([?\C-b] . [left]) + ([?\C-f] . [right]) + ([?\C-p] . [up]) + ([?\C-n] . [down]) + ([?\C-a] . [home]) + ([?\C-e] . [end]) + ([?\M-v] . [prior]) + ([?\C-v] . [next]) + ([?\C-d] . [delete]) + ([?\C-k] . [S-end delete])))) + ;; Enable EXWM + (exwm-enable) + ;; Configure Ido + (exwm-config-ido) + ;; Other configurations + (exwm-config-misc)) + +(defun exwm-config--fix/ido-buffer-window-other-frame () + "Fix `ido-buffer-window-other-frame'." + (defalias 'exwm-config-ido-buffer-window-other-frame + (symbol-function #'ido-buffer-window-other-frame)) + (defun ido-buffer-window-other-frame (buffer) + "This is a version redefined by EXWM. + +You can find the original one at `exwm-config-ido-buffer-window-other-frame'." + (with-current-buffer (window-buffer (selected-window)) + (if (and (derived-mode-p 'exwm-mode) + exwm--floating-frame) + ;; Switch from a floating frame. + (with-current-buffer buffer + (if (and (derived-mode-p 'exwm-mode) + exwm--floating-frame + (eq exwm--frame exwm-workspace--current)) + ;; Switch to another floating frame. + (frame-root-window exwm--floating-frame) + ;; Do not switch if the buffer is not on the current workspace. + (or (get-buffer-window buffer exwm-workspace--current) + (selected-window)))) + (with-current-buffer buffer + (when (derived-mode-p 'exwm-mode) + (if (eq exwm--frame exwm-workspace--current) + (when exwm--floating-frame + ;; Switch to a floating frame on the current workspace. + (frame-selected-window exwm--floating-frame)) + ;; Do not switch to exwm-mode buffers on other workspace (which + ;; won't work unless `exwm-layout-show-all-buffers' is set) + (unless exwm-layout-show-all-buffers + (selected-window))))))))) + +(defun exwm-config-ido () + "Configure Ido to work with EXWM." + (ido-mode 1) + (add-hook 'exwm-init-hook #'exwm-config--fix/ido-buffer-window-other-frame)) + +(defun exwm-config-misc () + "Other configurations." + ;; Make more room + (menu-bar-mode -1) + (tool-bar-mode -1) + (scroll-bar-mode -1) + (fringe-mode 1)) + + + +(provide 'exwm-config) + +;;; exwm-config.el ends here diff --git a/exwm-core.el b/exwm-core.el new file mode 100644 index 0000000000..e0d644d941 --- /dev/null +++ b/exwm-core.el @@ -0,0 +1,411 @@ +;;; exwm-core.el --- Core definitions -*- lexical-binding: t -*- + +;; Copyright (C) 2015-2024 Free Software Foundation, Inc. + +;; Author: Chris Feng <chris.w.feng@gmail.com> + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This module includes core definitions of variables, macros, functions, etc +;; shared by various other modules. + +;;; Code: + +(require 'kmacro) + +(require 'xcb) +(require 'xcb-icccm) +(require 'xcb-ewmh) +(require 'xcb-debug) + +(defgroup exwm-debug nil + "Debugging." + :group 'exwm) + +(defcustom exwm-debug-log-time-function #'exwm-debug-log-uptime + "Function used for generating timestamps in `exwm-debug' logs. + +Here are some predefined candidates: +`exwm-debug-log-uptime': Display the uptime of this Emacs instance. +`exwm-debug-log-time': Display time of day. +`nil': Disable timestamp." + :type `(choice (const :tag "Emacs uptime" ,#'exwm-debug-log-uptime) + (const :tag "Time of day" ,#'exwm-debug-log-time) + (const :tag "Off" nil) + (function :tag "Other")) + :set (lambda (symbol value) + (set-default symbol value) + ;; Also change the format for XELB to make logs consistent + ;; (as they share the same buffer). + (setq xcb-debug:log-time-function value))) + +(defalias 'exwm-debug-log-uptime 'xcb-debug:log-uptime + "Add uptime to `exwm-debug' logs.") + +(defalias 'exwm-debug-log-time 'xcb-debug:log-time + "Add time of day to `exwm-debug' logs.") + +(defvar exwm--connection nil "X connection.") + +(defvar exwm--terminal nil + "Terminal corresponding to `exwm--connection'.") + +(defvar exwm--wmsn-window nil + "An X window owning the WM_S0 selection.") + +(defvar exwm--wmsn-acquire-timeout 3 + "Number of seconds to wait for other window managers to release the selection.") + +(defvar exwm--guide-window nil + "An X window separating workspaces and X windows.") + +(defvar exwm--id-buffer-alist nil "Alist of (<X window ID> . <Emacs buffer>).") + +(defvar exwm--root nil "Root window.") + +(defvar exwm-input--global-prefix-keys) +(defvar exwm-input--simulation-keys) +(defvar exwm-input-line-mode-passthrough) +(defvar exwm-input-prefix-keys) +(declare-function exwm-input--fake-key "exwm-input.el" (event)) +(declare-function exwm-input--on-KeyPress-line-mode "exwm-input.el" + (key-press raw-data)) +(declare-function exwm-floating-hide "exwm-floating.el") +(declare-function exwm-floating-toggle-floating "exwm-floating.el") +(declare-function exwm-input-release-keyboard "exwm-input.el") +(declare-function exwm-input-send-next-key "exwm-input.el" (times)) +(declare-function exwm-layout-set-fullscreen "exwm-layout.el" (&optional id)) +(declare-function exwm-layout-toggle-mode-line "exwm-layout.el") +(declare-function exwm-manage--kill-buffer-query-function "exwm-manage.el") +(declare-function exwm-workspace-move-window "exwm-workspace.el" + (frame-or-index &optional id)) + +(define-minor-mode exwm-debug + "Debug-logging enabled if non-nil." + :global t + :group 'exwm-debug) + +(defmacro exwm--debug (&rest forms) + "Evaluate FORMS if mode `exwm-debug' is active." + (when exwm-debug `(progn ,@forms))) + +(defmacro exwm--log (&optional format-string &rest objects) + "Emit a message prepending the name of the function being executed. + +FORMAT-STRING is a string specifying the message to output, as in +`format'. The OBJECTS arguments specify the substitutions." + (unless format-string (setq format-string "")) + `(when exwm-debug + (xcb-debug:message ,(concat "%s%s:\t" format-string "\n") + (if exwm-debug-log-time-function + (funcall exwm-debug-log-time-function) + "") + (xcb-debug:compile-time-function-name) + ,@objects) + nil)) + +(defsubst exwm--id->buffer (id) + "X window ID => Emacs buffer." + (declare (indent defun)) + (cdr (assoc id exwm--id-buffer-alist))) + +(defsubst exwm--buffer->id (buffer) + "Emacs buffer BUFFER => X window ID." + (declare (indent defun)) + (car (rassoc buffer exwm--id-buffer-alist))) + +(defun exwm--lock (&rest _args) + "Lock (disable all events)." + (exwm--log) + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window exwm--root + :value-mask xcb:CW:EventMask + :event-mask xcb:EventMask:NoEvent)) + (xcb:flush exwm--connection)) + +(defun exwm--unlock (&rest _args) + "Unlock (enable all events)." + (exwm--log) + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window exwm--root + :value-mask xcb:CW:EventMask + :event-mask (eval-when-compile + (logior xcb:EventMask:SubstructureRedirect + xcb:EventMask:StructureNotify)))) + (xcb:flush exwm--connection)) + +(defun exwm--set-geometry (xwin x y width height) + "Set the geometry of X window XWIN to WIDTHxHEIGHT+X+Y. + +Nil can be passed as placeholder." + (exwm--log "Setting #x%x to %sx%s+%s+%s" xwin width height x y) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window xwin + :value-mask (logior (if x xcb:ConfigWindow:X 0) + (if y xcb:ConfigWindow:Y 0) + (if width xcb:ConfigWindow:Width 0) + (if height xcb:ConfigWindow:Height 0)) + :x x :y y :width width :height height))) + +(defun exwm--intern-atom (atom &optional conn) + "Intern X11 ATOM. +If CONN is non-nil, use it instead of the value of the variable +`exwm--connection'." + (slot-value (xcb:+request-unchecked+reply (or conn exwm--connection) + (make-instance 'xcb:InternAtom + :only-if-exists 0 + :name-len (length atom) + :name atom)) + 'atom)) + +(defmacro exwm--defer (secs function &rest args) + "Defer the execution of FUNCTION. + +The action is to call FUNCTION with arguments ARGS. If Emacs is not idle, +defer the action until Emacs is idle. Otherwise, defer the action until at +least SECS seconds later." + `(run-with-idle-timer (+ (float-time (or (current-idle-time) + (seconds-to-time (- ,secs)))) + ,secs) + nil + ,function + ,@args)) + +(defsubst exwm--terminal-p (&optional frame) + "Return t when FRAME's terminal is EXWM's terminal. +If FRAME is null, use selected frame." + (declare (indent defun)) + (eq exwm--terminal (frame-terminal frame))) + +(defun exwm--get-client-event-mask () + "Return event mask set on all managed windows." + (logior xcb:EventMask:StructureNotify + xcb:EventMask:PropertyChange + (if mouse-autoselect-window + xcb:EventMask:EnterWindow 0))) + +(defun exwm--color->pixel (color) + "Convert COLOR to PIXEL (index in TrueColor colormap)." + (when (and color + (eq (x-display-visual-class) 'true-color)) + (let ((rgb (color-values color))) + (logior (ash (ash (pop rgb) -8) 16) + (ash (ash (pop rgb) -8) 8) + (ash (pop rgb) -8))))) + +(defun exwm--get-visual-depth-colormap (conn id) + "Get visual, depth and colormap from X window ID. +Return a three element list with the respective results. + +If CONN is non-nil, use it instead of the value of the variable +`exwm--connection'." + (let (ret-visual ret-depth ret-colormap) + (with-slots (visual colormap) + (xcb:+request-unchecked+reply conn + (make-instance 'xcb:GetWindowAttributes :window id)) + (setq ret-visual visual) + (setq ret-colormap colormap)) + (with-slots (depth) + (xcb:+request-unchecked+reply conn + (make-instance 'xcb:GetGeometry :drawable id)) + (setq ret-depth depth)) + (list ret-visual ret-depth ret-colormap))) + +;; Internal variables +(defvar-local exwm--id nil) ;window ID +(defvar-local exwm--configurations nil) ;initial configurations. +(defvar-local exwm--frame nil) ;workspace frame +(defvar-local exwm--floating-frame nil) ;floating frame +(defvar-local exwm--mode-line-format nil) ;save mode-line-format +(defvar-local exwm--floating-frame-position nil) ;set when hidden. +(defvar-local exwm--fixed-size nil) ;fixed size +(defvar-local exwm--selected-input-mode 'line-mode + "Input mode as selected by the user. +One of `line-mode' or `char-mode'.") +(defvar-local exwm--input-mode 'line-mode + "Actual input mode, i.e. whether mouse and keyboard are grabbed.") +;; Properties +(defvar-local exwm--desktop nil "_NET_WM_DESKTOP.") +(defvar-local exwm-window-type nil "_NET_WM_WINDOW_TYPE.") +(defvar-local exwm--geometry nil) +(defvar-local exwm-class-name nil "Class name in WM_CLASS.") +(defvar-local exwm-instance-name nil "Instance name in WM_CLASS.") +(defvar-local exwm-title nil "Window title (either _NET_WM_NAME or WM_NAME).") +(defvar-local exwm--title-is-utf8 nil) +(defvar-local exwm-transient-for nil "WM_TRANSIENT_FOR.") +(defvar-local exwm--protocols nil) +(defvar-local exwm-state xcb:icccm:WM_STATE:NormalState "WM_STATE.") +(defvar-local exwm--ewmh-state nil "_NET_WM_STATE.") +;; _NET_WM_NORMAL_HINTS +(defvar-local exwm--normal-hints-x nil) +(defvar-local exwm--normal-hints-y nil) +(defvar-local exwm--normal-hints-width nil) +(defvar-local exwm--normal-hints-height nil) +(defvar-local exwm--normal-hints-min-width nil) +(defvar-local exwm--normal-hints-min-height nil) +(defvar-local exwm--normal-hints-max-width nil) +(defvar-local exwm--normal-hints-max-height nil) +;; (defvar-local exwm--normal-hints-win-gravity nil) +;; WM_HINTS +(defvar-local exwm--hints-input nil) +(defvar-local exwm--hints-urgency nil) +;; _MOTIF_WM_HINTS +(defvar-local exwm--mwm-hints-decorations t) + +(defvar exwm-mode-map + (let ((map (make-sparse-keymap))) + (define-key map "\C-c\C-d\C-l" #'xcb-debug:clear) + (define-key map "\C-c\C-d\C-m" #'xcb-debug:mark) + (define-key map "\C-c\C-d\C-t" #'exwm-debug) + (define-key map "\C-c\C-f" #'exwm-layout-set-fullscreen) + (define-key map "\C-c\C-h" #'exwm-floating-hide) + (define-key map "\C-c\C-k" #'exwm-input-release-keyboard) + (define-key map "\C-c\C-m" #'exwm-workspace-move-window) + (define-key map "\C-c\C-q" #'exwm-input-send-next-key) + (define-key map "\C-c\C-t\C-f" #'exwm-floating-toggle-floating) + (define-key map "\C-c\C-t\C-m" #'exwm-layout-toggle-mode-line) + map) + "Keymap for `exwm-mode'.") + +(defvar exwm--kmacro-map + (let ((map (make-sparse-keymap))) + (define-key map [t] + (lambda () + (interactive) + (cond + ((or exwm-input-line-mode-passthrough + ;; Do not test `exwm-input--during-command'. + (active-minibuffer-window) + (memq last-input-event exwm-input--global-prefix-keys) + (memq last-input-event exwm-input-prefix-keys) + (lookup-key exwm-mode-map (vector last-input-event)) + (gethash last-input-event exwm-input--simulation-keys)) + (set-transient-map (make-composed-keymap (list exwm-mode-map + global-map))) + (push last-input-event unread-command-events)) + (t + (exwm-input--fake-key last-input-event))))) + map) + "Keymap used when executing keyboard macros.") + +;; This menu mainly acts as an reminder for users. Thus it should be as +;; detailed as possible, even some entries do not make much sense here. +;; Also, inactive entries should be disabled rather than hidden. +(easy-menu-define exwm-mode-menu exwm-mode-map + "Menu for `exwm-mode'." + '("EXWM" + "---" + "*General*" + "---" + ["Toggle floating" exwm-floating-toggle-floating] + ["Toggle fullscreen mode" exwm-layout-toggle-fullscreen] + ["Hide window" exwm-floating-hide exwm--floating-frame] + ["Close window" (kill-buffer (current-buffer))] + + "---" + "*Resizing*" + "---" + ["Toggle mode-line" exwm-layout-toggle-mode-line] + ["Enlarge window vertically" exwm-layout-enlarge-window] + ["Enlarge window horizontally" exwm-layout-enlarge-window-horizontally] + ["Shrink window vertically" exwm-layout-shrink-window] + ["Shrink window horizontally" exwm-layout-shrink-window-horizontally] + + "---" + "*Keyboard*" + "---" + ["Toggle keyboard mode" exwm-input-toggle-keyboard] + ["Send key" exwm-input-send-next-key (eq exwm--input-mode 'line-mode)] + ;; This is merely a reference. + ("Send simulation key" :filter + (lambda (&rest _args) + (let (result) + (maphash + (lambda (key value) + (when (sequencep key) + (setq result (append result + `([ + ,(format "Send '%s'" + (key-description value)) + (lambda () + (interactive) + (dolist (i ',value) + (exwm-input--fake-key i))) + :keys ,(key-description key)]))))) + exwm-input--simulation-keys) + result))) + + ["Define global binding" exwm-input-set-key] + + "---" + "*Workspace*" + "---" + ["Add workspace" exwm-workspace-add] + ["Delete current workspace" exwm-workspace-delete] + ["Move workspace to" exwm-workspace-move] + ["Swap workspaces" exwm-workspace-swap] + ["Move X window to" exwm-workspace-move-window] + ["Move X window from" exwm-workspace-switch-to-buffer] + ["Toggle minibuffer" exwm-workspace-toggle-minibuffer] + ["Switch workspace" exwm-workspace-switch] + ;; Place this entry at bottom to avoid selecting others by accident. + ("Switch to" :filter + (lambda (&rest _args) + (mapcar (lambda (i) + `[,(format "Workspace %d" i) + (lambda () + (interactive) + (exwm-workspace-switch ,i)) + (/= ,i exwm-workspace-current-index)]) + (number-sequence 0 (1- (exwm-workspace--count)))))))) + +(define-derived-mode exwm-mode nil "EXWM" + "Major mode for managing X windows. + +\\{exwm-mode-map}" + ;; + (setq mode-name + '(:eval (propertize "EXWM" 'face + (when (cl-some (lambda (i) + (frame-parameter i 'exwm-urgency)) + exwm-workspace--list) + 'font-lock-warning-face)))) + ;; Change major-mode is not allowed + (add-hook 'change-major-mode-hook #'kill-buffer nil t) + ;; Kill buffer -> close window + (add-hook 'kill-buffer-query-functions + #'exwm-manage--kill-buffer-query-function nil t) + ;; Redirect events when executing keyboard macros. + (push `(executing-kbd-macro . ,exwm--kmacro-map) + minor-mode-overriding-map-alist) + (setq buffer-read-only t + cursor-type nil + left-margin-width nil + right-margin-width nil + left-fringe-width 0 + right-fringe-width 0 + vertical-scroll-bar nil)) + + + +(provide 'exwm-core) + +;;; exwm-core.el ends here diff --git a/exwm-floating.el b/exwm-floating.el new file mode 100644 index 0000000000..34d06a30db --- /dev/null +++ b/exwm-floating.el @@ -0,0 +1,780 @@ +;;; exwm-floating.el --- Floating Module for EXWM -*- lexical-binding: t -*- + +;; Copyright (C) 2015-2024 Free Software Foundation, Inc. + +;; Author: Chris Feng <chris.w.feng@gmail.com> + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This module deals with the conversion between floating and non-floating +;; states and implements moving/resizing operations on floating windows. + +;;; Code: + +(require 'xcb-cursor) +(require 'exwm-core) + +(defgroup exwm-floating nil + "Floating." + :group 'exwm) + +(defcustom exwm-floating-setup-hook nil + "Normal hook run when an X window has been made floating. +This hook runs in the context of the corresponding buffer." + :type 'hook) + +(defcustom exwm-floating-exit-hook nil + "Normal hook run when an X window has exited floating state. +This hook runs in the context of the corresponding buffer." + :type 'hook) + +(defcustom exwm-floating-border-color "navy" + "Border color of floating windows." + :type 'color + :initialize #'custom-initialize-default + :set (lambda (symbol value) + (set-default symbol value) + ;; Change border color for all floating X windows. + (when exwm--connection + (let ((border-pixel (exwm--color->pixel value))) + (when border-pixel + (dolist (pair exwm--id-buffer-alist) + (with-current-buffer (cdr pair) + (when exwm--floating-frame + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window + (frame-parameter exwm--floating-frame + 'exwm-container) + :value-mask xcb:CW:BorderPixel + :border-pixel border-pixel))))) + (xcb:flush exwm--connection)))))) + +(defcustom exwm-floating-border-width 1 + "Border width of floating windows." + :type '(integer + :validate (lambda (widget) + (when (< (widget-value widget) 0) + (widget-put widget :error "Border width is at least 0") + widget))) + :initialize #'custom-initialize-default + :set (lambda (symbol value) + (let ((delta (- value exwm-floating-border-width)) + container) + (set-default symbol value) + ;; Change border width for all floating X windows. + (dolist (pair exwm--id-buffer-alist) + (with-current-buffer (cdr pair) + (when exwm--floating-frame + (setq container (frame-parameter exwm--floating-frame + 'exwm-container)) + (with-slots (x y) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetGeometry + :drawable container)) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window container + :value-mask + (logior xcb:ConfigWindow:X + xcb:ConfigWindow:Y + xcb:ConfigWindow:BorderWidth) + :border-width value + :x (- x delta) + :y (- y delta))))))) + (when exwm--connection + (xcb:flush exwm--connection))))) + +;; Cursors for moving/resizing a window +(defvar exwm-floating--cursor-move nil) +(defvar exwm-floating--cursor-top-left nil) +(defvar exwm-floating--cursor-top nil) +(defvar exwm-floating--cursor-top-right nil) +(defvar exwm-floating--cursor-right nil) +(defvar exwm-floating--cursor-bottom-right nil) +(defvar exwm-floating--cursor-bottom nil) +(defvar exwm-floating--cursor-bottom-left nil) +(defvar exwm-floating--cursor-left nil) + +(defvar exwm-floating--moveresize-calculate nil + "Calculate move/resize parameters [buffer event-mask x y width height].") + +(defvar exwm-workspace--current) +(defvar exwm-workspace--frame-y-offset) +(defvar exwm-workspace--window-y-offset) +(declare-function exwm-layout--hide "exwm-layout.el" (id)) +(declare-function exwm-layout--iconic-state-p "exwm-layout.el" (&optional id)) +(declare-function exwm-layout--refresh "exwm-layout.el" ()) +(declare-function exwm-layout--show "exwm-layout.el" (id &optional window)) +(declare-function exwm-workspace--position "exwm-workspace.el" (frame)) +(declare-function exwm-workspace--update-offsets "exwm-workspace.el" ()) +(declare-function exwm-workspace--workarea "exwm-workspace.el" (frame)) + +(defun exwm-floating--set-allowed-actions (id tilling) + "Set _NET_WM_ALLOWED_ACTIONS." + (exwm--log "#x%x" id) + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_WM_ALLOWED_ACTIONS + :window id + :data (if tilling + (vector xcb:Atom:_NET_WM_ACTION_MINIMIZE + xcb:Atom:_NET_WM_ACTION_FULLSCREEN + xcb:Atom:_NET_WM_ACTION_CHANGE_DESKTOP + xcb:Atom:_NET_WM_ACTION_CLOSE) + (vector xcb:Atom:_NET_WM_ACTION_MOVE + xcb:Atom:_NET_WM_ACTION_RESIZE + xcb:Atom:_NET_WM_ACTION_MINIMIZE + xcb:Atom:_NET_WM_ACTION_FULLSCREEN + xcb:Atom:_NET_WM_ACTION_CHANGE_DESKTOP + xcb:Atom:_NET_WM_ACTION_CLOSE))))) + +(defun exwm-floating--set-floating (id) + "Make window ID floating." + (let ((window (get-buffer-window (exwm--id->buffer id)))) + (when window + ;; Hide the non-floating X window first. + (set-window-buffer window (other-buffer nil t)))) + (let* ((original-frame (buffer-local-value 'exwm--frame + (exwm--id->buffer id))) + ;; Create new frame + (frame (with-current-buffer + (or (get-buffer "*scratch*") + (progn + (set-buffer-major-mode + (get-buffer-create "*scratch*")) + (get-buffer "*scratch*"))) + (make-frame + `((minibuffer . ,(minibuffer-window exwm--frame)) + (tab-bar-lines . 0) + (tab-bar-lines-keep-state . t) + (left . ,(* window-min-width -10000)) + (top . ,(* window-min-height -10000)) + (width . ,window-min-width) + (height . ,window-min-height) + (unsplittable . t))))) ;and fix the size later + (outer-id (string-to-number (frame-parameter frame 'outer-window-id))) + (window-id (string-to-number (frame-parameter frame 'window-id))) + (frame-container (xcb:generate-id exwm--connection)) + (window (frame-first-window frame)) ;and it's the only window + (x (slot-value exwm--geometry 'x)) + (y (slot-value exwm--geometry 'y)) + (width (slot-value exwm--geometry 'width)) + (height (slot-value exwm--geometry 'height))) + ;; Force drawing menu-bar & tool-bar. + (redisplay t) + (exwm-workspace--update-offsets) + (exwm--log "Floating geometry (original): %dx%d%+d%+d" width height x y) + ;; Save frame parameters. + (set-frame-parameter frame 'exwm-outer-id outer-id) + (set-frame-parameter frame 'exwm-id window-id) + (set-frame-parameter frame 'exwm-container frame-container) + ;; Fix illegal parameters + ;; FIXME: check normal hints restrictions + (with-slots ((x* x) (y* y) (width* width) (height* height)) + (exwm-workspace--workarea original-frame) + ;; Center floating windows + (when (and (or (= x 0) (= x x*)) + (or (= y 0) (= y y*))) + (let ((buffer (exwm--id->buffer exwm-transient-for)) + window edges) + (when (and buffer (setq window (get-buffer-window buffer))) + (setq edges (window-inside-absolute-pixel-edges window)) + (unless (and (<= width (- (elt edges 2) (elt edges 0))) + (<= height (- (elt edges 3) (elt edges 1)))) + (setq edges nil))) + (if edges + ;; Put at the center of leading window + (setq x (+ x* (/ (- (elt edges 2) (elt edges 0) width) 2)) + y (+ y* (/ (- (elt edges 3) (elt edges 1) height) 2))) + ;; Put at the center of screen + (setq x (/ (- width* width) 2) + y (/ (- height* height) 2))))) + (if (> width width*) + ;; Too wide + (progn (setq x x* + width width*)) + ;; Invalid width + (when (= 0 width) (setq width (/ width* 2))) + ;; Make sure at least half of the window is visible + (unless (< x* (+ x (/ width 2)) (+ x* width*)) + (setq x (+ x* (/ (- width* width) 2))))) + (if (> height height*) + ;; Too tall + (setq y y* + height height*) + ;; Invalid height + (when (= 0 height) (setq height (/ height* 2))) + ;; Make sure at least half of the window is visible + (unless (< y* (+ y (/ height 2)) (+ y* height*)) + (setq y (+ y* (/ (- height* height) 2))))) + ;; The geometry can be overridden by user options. + (let ((x** (plist-get exwm--configurations 'x)) + (y** (plist-get exwm--configurations 'y)) + (width** (plist-get exwm--configurations 'width)) + (height** (plist-get exwm--configurations 'height))) + (if (integerp x**) + (setq x (+ x* x**)) + (when (and (floatp x**) + (>= 1 x** 0)) + (setq x (+ x* (round (* x** width*)))))) + (if (integerp y**) + (setq y (+ y* y**)) + (when (and (floatp y**) + (>= 1 y** 0)) + (setq y (+ y* (round (* y** height*)))))) + (if (integerp width**) + (setq width width**) + (when (and (floatp width**) + (> 1 width** 0)) + (setq width (max 1 (round (* width** width*)))))) + (if (integerp height**) + (setq height height**) + (when (and (floatp height**) + (> 1 height** 0)) + (setq height (max 1 (round (* height** height*)))))))) + (exwm--set-geometry id x y nil nil) + (xcb:flush exwm--connection) + (exwm--log "Floating geometry (corrected): %dx%d%+d%+d" width height x y) + ;; Fit frame to client + ;; It seems we have to make the frame invisible in order to resize it + ;; timely. + ;; The frame will be made visible by `select-frame-set-input-focus'. + (make-frame-invisible frame) + (let* ((edges (window-inside-pixel-edges window)) + (frame-width (+ width (- (frame-pixel-width frame) + (- (elt edges 2) (elt edges 0))))) + (frame-height (+ height (- (frame-pixel-height frame) + (- (elt edges 3) (elt edges 1))) + ;; Use `frame-outer-height' in the future. + exwm-workspace--frame-y-offset)) + (floating-mode-line (plist-get exwm--configurations + 'floating-mode-line)) + (floating-header-line (plist-get exwm--configurations + 'floating-header-line)) + (border-pixel (exwm--color->pixel exwm-floating-border-color))) + (if floating-mode-line + (setq exwm--mode-line-format (or exwm--mode-line-format + mode-line-format) + mode-line-format floating-mode-line) + (if (and (not (plist-member exwm--configurations 'floating-mode-line)) + exwm--mwm-hints-decorations) + (when exwm--mode-line-format + (setq mode-line-format exwm--mode-line-format)) + ;; The mode-line need to be hidden in floating mode. + (setq frame-height (- frame-height (window-mode-line-height + (frame-root-window frame))) + exwm--mode-line-format (or exwm--mode-line-format + mode-line-format) + mode-line-format nil))) + (if floating-header-line + (setq header-line-format floating-header-line) + (if (and (not (plist-member exwm--configurations + 'floating-header-line)) + exwm--mwm-hints-decorations) + (setq header-line-format nil) + ;; The header-line need to be hidden in floating mode. + (setq frame-height (- frame-height (window-header-line-height + (frame-root-window frame))) + header-line-format nil))) + (set-frame-size frame frame-width frame-height t) + ;; Create the frame container as the parent of the frame. + (xcb:+request exwm--connection + (make-instance 'xcb:CreateWindow + :depth 0 + :wid frame-container + :parent exwm--root + :x x + :y (- y exwm-workspace--window-y-offset) + :width width + :height height + :border-width + (with-current-buffer (exwm--id->buffer id) + (let ((border-witdh (plist-get exwm--configurations + 'border-width))) + (if (and (integerp border-witdh) + (>= border-witdh 0)) + border-witdh + exwm-floating-border-width))) + :class xcb:WindowClass:InputOutput + :visual 0 + :value-mask (logior xcb:CW:BackPixmap + (if border-pixel + xcb:CW:BorderPixel 0) + xcb:CW:OverrideRedirect) + :background-pixmap xcb:BackPixmap:ParentRelative + :border-pixel border-pixel + :override-redirect 1)) + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_WM_NAME + :window frame-container + :data + (format "EXWM floating frame container for 0x%x" id))) + ;; Map it. + (xcb:+request exwm--connection + (make-instance 'xcb:MapWindow :window frame-container)) + ;; Put the X window right above this frame container. + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window id + :value-mask (logior xcb:ConfigWindow:Sibling + xcb:ConfigWindow:StackMode) + :sibling frame-container + :stack-mode xcb:StackMode:Above))) + ;; Reparent this frame to its container. + (xcb:+request exwm--connection + (make-instance 'xcb:ReparentWindow + :window outer-id :parent frame-container :x 0 :y 0)) + (exwm-floating--set-allowed-actions id nil) + (xcb:flush exwm--connection) + ;; Set window/buffer + (with-current-buffer (exwm--id->buffer id) + (setq window-size-fixed exwm--fixed-size + exwm--floating-frame frame) + ;; Do the refresh manually. + (remove-hook 'window-configuration-change-hook #'exwm-layout--refresh) + (set-window-buffer window (current-buffer)) ;this changes current buffer + (add-hook 'window-configuration-change-hook #'exwm-layout--refresh) + (set-window-dedicated-p window t) + (exwm-layout--show id window)) + (with-current-buffer (exwm--id->buffer id) + (if (exwm-layout--iconic-state-p id) + ;; Hide iconic floating X windows. + (exwm-floating-hide) + (with-selected-frame exwm--frame + (exwm-layout--refresh))) + (select-frame-set-input-focus frame)) + ;; FIXME: Strangely, the Emacs frame can move itself at this point + ;; when there are left/top struts set. Force resetting its + ;; position seems working, but it'd better to figure out why. + ;; FIXME: This also happens in another case (#220) where the cause is + ;; still unclear. + (exwm--set-geometry outer-id 0 0 nil nil) + (xcb:flush exwm--connection)) + (with-current-buffer (exwm--id->buffer id) + (run-hooks 'exwm-floating-setup-hook)) + ;; Redraw the frame. + (redisplay t)) + +(defun exwm-floating--unset-floating (id) + "Make window ID non-floating." + (exwm--log "#x%x" id) + (let ((buffer (exwm--id->buffer id))) + (with-current-buffer buffer + (when exwm--floating-frame + ;; The X window is already mapped. + ;; Unmap the X window. + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window id :value-mask xcb:CW:EventMask + :event-mask xcb:EventMask:NoEvent)) + (xcb:+request exwm--connection + (make-instance 'xcb:UnmapWindow :window id)) + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window id :value-mask xcb:CW:EventMask + :event-mask (exwm--get-client-event-mask))) + ;; Reparent the floating frame back to the root window. + (let ((frame-id (frame-parameter exwm--floating-frame 'exwm-outer-id)) + (frame-container (frame-parameter exwm--floating-frame + 'exwm-container))) + (xcb:+request exwm--connection + (make-instance 'xcb:UnmapWindow :window frame-id)) + (xcb:+request exwm--connection + (make-instance 'xcb:ReparentWindow + :window frame-id + :parent exwm--root + :x 0 :y 0)) + ;; Also destroy its container. + (xcb:+request exwm--connection + (make-instance 'xcb:DestroyWindow :window frame-container)))) + ;; Place the X window just above the reference X window. + ;; (the stacking order won't change from now on). + ;; Also hide the possible floating border. + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window id + :value-mask (logior xcb:ConfigWindow:BorderWidth + xcb:ConfigWindow:Sibling + xcb:ConfigWindow:StackMode) + :border-width 0 + :sibling exwm--guide-window + :stack-mode xcb:StackMode:Above))) + (exwm-floating--set-allowed-actions id t) + (xcb:flush exwm--connection) + (with-current-buffer buffer + (when exwm--floating-frame ;from floating to non-floating + (set-window-dedicated-p (frame-first-window exwm--floating-frame) nil) + ;; Select a tiling window and delete the old frame. + (select-window (frame-selected-window exwm-workspace--current)) + (with-current-buffer buffer + (delete-frame exwm--floating-frame)))) + (with-current-buffer buffer + (setq window-size-fixed nil + exwm--floating-frame nil) + (if (not (plist-member exwm--configurations 'tiling-mode-line)) + (when exwm--mode-line-format + (setq mode-line-format exwm--mode-line-format)) + (setq exwm--mode-line-format (or exwm--mode-line-format + mode-line-format) + mode-line-format (plist-get exwm--configurations + 'tiling-mode-line))) + (if (not (plist-member exwm--configurations 'tiling-header-line)) + (setq header-line-format nil) + (setq header-line-format (plist-get exwm--configurations + 'tiling-header-line)))) + ;; Only show X windows in normal state. + (unless (exwm-layout--iconic-state-p) + (pop-to-buffer-same-window buffer))) + (with-current-buffer (exwm--id->buffer id) + (run-hooks 'exwm-floating-exit-hook))) + +;;;###autoload +(cl-defun exwm-floating-toggle-floating () + "Toggle the current window between floating and non-floating states." + (interactive) + (exwm--log) + (unless (derived-mode-p 'exwm-mode) + (cl-return-from exwm-floating-toggle-floating)) + (with-current-buffer (window-buffer) + (if exwm--floating-frame + (exwm-floating--unset-floating exwm--id) + (exwm-floating--set-floating exwm--id)))) + +;;;###autoload +(defun exwm-floating-hide () + "Hide the current floating X window (which would show again when selected)." + (interactive) + (exwm--log) + (when (and (derived-mode-p 'exwm-mode) + exwm--floating-frame) + (exwm-layout--hide exwm--id) + (select-frame-set-input-focus exwm-workspace--current))) + +(defun exwm-floating--start-moveresize (id &optional type) + "Start move/resize." + (exwm--log "#x%x" id) + (let ((buffer-or-id (or (exwm--id->buffer id) id)) + frame container-or-id x y width height cursor) + (if (bufferp buffer-or-id) + ;; Managed. + (with-current-buffer buffer-or-id + (setq frame exwm--floating-frame + container-or-id (frame-parameter exwm--floating-frame + 'exwm-container))) + ;; Unmanaged. + (setq container-or-id id)) + (when (and container-or-id + ;; Test if the pointer can be grabbed + (= xcb:GrabStatus:Success + (slot-value + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GrabPointer + :owner-events 0 + :grab-window container-or-id + :event-mask xcb:EventMask:NoEvent + :pointer-mode xcb:GrabMode:Async + :keyboard-mode xcb:GrabMode:Async + :confine-to xcb:Window:None + :cursor xcb:Cursor:None + :time xcb:Time:CurrentTime)) + 'status))) + (with-slots (root-x root-y win-x win-y) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:QueryPointer :window id)) + (if (not (bufferp buffer-or-id)) + ;; Unmanaged. + (unless (eq type xcb:ewmh:_NET_WM_MOVERESIZE_MOVE) + (with-slots ((width* width) + (height* height)) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetGeometry :drawable id)) + (setq width width* + height height*))) + ;; Managed. + (select-window (frame-first-window frame)) ;transfer input focus + (setq width (frame-pixel-width frame) + height (frame-pixel-height frame)) + (unless type + ;; Determine the resize type according to the pointer position + ;; Clicking the center 1/3 part to resize has no effect + (setq x (/ (* 3 win-x) (float width)) + y (/ (* 3 win-y) (float height)) + type (cond ((and (< x 1) (< y 1)) + xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_TOPLEFT) + ((and (> x 2) (< y 1)) + xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_TOPRIGHT) + ((and (> x 2) (> y 2)) + xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT) + ((and (< x 1) (> y 2)) + xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT) + ((> x 2) xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_RIGHT) + ((> y 2) xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_BOTTOM) + ((< x 1) xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_LEFT) + ((< y 1) xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_TOP))))) + (if (not type) + (exwm-floating--stop-moveresize) + (cond ((= type xcb:ewmh:_NET_WM_MOVERESIZE_MOVE) + (setq cursor exwm-floating--cursor-move + exwm-floating--moveresize-calculate + (lambda (x y) + (vector buffer-or-id + (eval-when-compile + (logior xcb:ConfigWindow:X + xcb:ConfigWindow:Y)) + (- x win-x) (- y win-y) 0 0)))) + ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_TOPLEFT) + (setq cursor exwm-floating--cursor-top-left + exwm-floating--moveresize-calculate + (lambda (x y) + (vector buffer-or-id + (eval-when-compile + (logior xcb:ConfigWindow:X + xcb:ConfigWindow:Y + xcb:ConfigWindow:Width + xcb:ConfigWindow:Height)) + (- x win-x) (- y win-y) + (- (+ root-x width) x) + (- (+ root-y height) y))))) + ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_TOP) + (setq cursor exwm-floating--cursor-top + exwm-floating--moveresize-calculate + (lambda (_x y) + (vector buffer-or-id + (eval-when-compile + (logior xcb:ConfigWindow:Y + xcb:ConfigWindow:Height)) + 0 (- y win-y) 0 (- (+ root-y height) y))))) + ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_TOPRIGHT) + (setq cursor exwm-floating--cursor-top-right + exwm-floating--moveresize-calculate + (lambda (x y) + (vector buffer-or-id + (eval-when-compile + (logior xcb:ConfigWindow:Y + xcb:ConfigWindow:Width + xcb:ConfigWindow:Height)) + 0 (- y win-y) (- x (- root-x width)) + (- (+ root-y height) y))))) + ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_RIGHT) + (setq cursor exwm-floating--cursor-right + exwm-floating--moveresize-calculate + (lambda (x _y) + (vector buffer-or-id + xcb:ConfigWindow:Width + 0 0 (- x (- root-x width)) 0)))) + ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT) + (setq cursor exwm-floating--cursor-bottom-right + exwm-floating--moveresize-calculate + (lambda (x y) + (vector buffer-or-id + (eval-when-compile + (logior xcb:ConfigWindow:Width + xcb:ConfigWindow:Height)) + 0 0 (- x (- root-x width)) + (- y (- root-y height)))))) + ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_BOTTOM) + (setq cursor exwm-floating--cursor-bottom + exwm-floating--moveresize-calculate + (lambda (_x y) + (vector buffer-or-id + xcb:ConfigWindow:Height + 0 0 0 (- y (- root-y height)))))) + ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT) + (setq cursor exwm-floating--cursor-bottom-left + exwm-floating--moveresize-calculate + (lambda (x y) + (vector buffer-or-id + (eval-when-compile + (logior xcb:ConfigWindow:X + xcb:ConfigWindow:Width + xcb:ConfigWindow:Height)) + (- x win-x) + 0 + (- (+ root-x width) x) + (- y (- root-y height)))))) + ((= type xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_LEFT) + (setq cursor exwm-floating--cursor-left + exwm-floating--moveresize-calculate + (lambda (x _y) + (vector buffer-or-id + (eval-when-compile + (logior xcb:ConfigWindow:X + xcb:ConfigWindow:Width)) + (- x win-x) 0 (- (+ root-x width) x) 0))))) + ;; Select events and change cursor (should always succeed) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GrabPointer + :owner-events 0 :grab-window container-or-id + :event-mask (eval-when-compile + (logior xcb:EventMask:ButtonRelease + xcb:EventMask:ButtonMotion)) + :pointer-mode xcb:GrabMode:Async + :keyboard-mode xcb:GrabMode:Async + :confine-to xcb:Window:None + :cursor cursor + :time xcb:Time:CurrentTime))))))) + +(defun exwm-floating--stop-moveresize (&rest _args) + "Stop move/resize." + (exwm--log) + (xcb:+request exwm--connection + (make-instance 'xcb:UngrabPointer :time xcb:Time:CurrentTime)) + (when exwm-floating--moveresize-calculate + (let (result buffer-or-id outer-id container-id) + (setq result (funcall exwm-floating--moveresize-calculate 0 0) + buffer-or-id (aref result 0)) + (when (bufferp buffer-or-id) + (with-current-buffer buffer-or-id + (setq outer-id (frame-parameter exwm--floating-frame 'exwm-outer-id) + container-id (frame-parameter exwm--floating-frame + 'exwm-container)) + (with-slots (x y width height border-width) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetGeometry + :drawable container-id)) + ;; Notify Emacs frame about this the position change. + (xcb:+request exwm--connection + (make-instance 'xcb:SendEvent + :propagate 0 + :destination outer-id + :event-mask xcb:EventMask:StructureNotify + :event + (xcb:marshal + (make-instance 'xcb:ConfigureNotify + :event outer-id + :window outer-id + :above-sibling xcb:Window:None + :x (+ x border-width) + :y (+ y border-width) + :width width + :height height + :border-width 0 + :override-redirect 0) + exwm--connection))) + (xcb:flush exwm--connection)) + (exwm-layout--show exwm--id + (frame-root-window exwm--floating-frame))))) + (setq exwm-floating--moveresize-calculate nil))) + +(defun exwm-floating--do-moveresize (data _synthetic) + "Perform move/resize." + (when exwm-floating--moveresize-calculate + (let* ((obj (make-instance 'xcb:MotionNotify)) + result value-mask x y width height buffer-or-id container-or-id) + (xcb:unmarshal obj data) + (setq result (funcall exwm-floating--moveresize-calculate + (slot-value obj 'root-x) (slot-value obj 'root-y)) + buffer-or-id (aref result 0) + value-mask (aref result 1) + x (aref result 2) + y (aref result 3) + width (max 1 (aref result 4)) + height (max 1 (aref result 5))) + (if (not (bufferp buffer-or-id)) + ;; Unmanaged. + (setq container-or-id buffer-or-id) + ;; Managed. + (setq container-or-id + (with-current-buffer buffer-or-id + (frame-parameter exwm--floating-frame 'exwm-container)) + x (- x exwm-floating-border-width) + ;; Use `frame-outer-height' in the future. + y (- y exwm-floating-border-width + exwm-workspace--window-y-offset) + height (+ height exwm-workspace--window-y-offset))) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window container-or-id + :value-mask (aref result 1) + :x x + :y y + :width width + :height height)) + (when (bufferp buffer-or-id) + ;; Managed. + (setq value-mask (logand value-mask (logior xcb:ConfigWindow:Width + xcb:ConfigWindow:Height))) + (when (/= 0 value-mask) + (with-current-buffer buffer-or-id + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window (frame-parameter exwm--floating-frame + 'exwm-outer-id) + :value-mask value-mask + :width width + :height height))))) + (xcb:flush exwm--connection)))) + +(defun exwm-floating-move (&optional delta-x delta-y) + "Move a floating window right by DELTA-X pixels and down by DELTA-Y pixels. + +Both DELTA-X and DELTA-Y default to 1. This command should be bound locally." + (exwm--log "delta-x: %s, delta-y: %s" delta-x delta-y) + (unless (and (derived-mode-p 'exwm-mode) exwm--floating-frame) + (user-error "[EXWM] `exwm-floating-move' is only for floating X windows")) + (unless delta-x (setq delta-x 1)) + (unless delta-y (setq delta-y 1)) + (unless (and (= 0 delta-x) (= 0 delta-y)) + (let* ((floating-container (frame-parameter exwm--floating-frame + 'exwm-container)) + (geometry (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetGeometry + :drawable floating-container))) + (edges (window-inside-absolute-pixel-edges))) + (with-slots (x y) geometry + (exwm--set-geometry floating-container + (+ x delta-x) (+ y delta-y) nil nil)) + (exwm--set-geometry exwm--id + (+ (pop edges) delta-x) + (+ (pop edges) delta-y) + nil nil)) + (xcb:flush exwm--connection))) + +(defun exwm-floating--init () + "Initialize floating module." + (exwm--log) + ;; Initialize cursors for moving/resizing a window + (xcb:cursor:init exwm--connection) + (setq exwm-floating--cursor-move + (xcb:cursor:load-cursor exwm--connection "fleur") + exwm-floating--cursor-top-left + (xcb:cursor:load-cursor exwm--connection "top_left_corner") + exwm-floating--cursor-top + (xcb:cursor:load-cursor exwm--connection "top_side") + exwm-floating--cursor-top-right + (xcb:cursor:load-cursor exwm--connection "top_right_corner") + exwm-floating--cursor-right + (xcb:cursor:load-cursor exwm--connection "right_side") + exwm-floating--cursor-bottom-right + (xcb:cursor:load-cursor exwm--connection "bottom_right_corner") + exwm-floating--cursor-bottom + (xcb:cursor:load-cursor exwm--connection "bottom_side") + exwm-floating--cursor-bottom-left + (xcb:cursor:load-cursor exwm--connection "bottom_left_corner") + exwm-floating--cursor-left + (xcb:cursor:load-cursor exwm--connection "left_side"))) + +(defun exwm-floating--exit () + "Exit the floating module." + (exwm--log)) + + + +(provide 'exwm-floating) + +;;; exwm-floating.el ends here diff --git a/exwm-input.el b/exwm-input.el new file mode 100644 index 0000000000..f1f035c91a --- /dev/null +++ b/exwm-input.el @@ -0,0 +1,1248 @@ +;;; exwm-input.el --- Input Module for EXWM -*- lexical-binding: t -*- + +;; Copyright (C) 2015-2024 Free Software Foundation, Inc. + +;; Author: Chris Feng <chris.w.feng@gmail.com> + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This module deals with key/mouse matters, including: +;; + Input focus, +;; + Key/Button event handling, +;; + Key events filtering and simulation. + +;; Todo: +;; + Pointer simulation mode (e.g. 'C-c 1'/'C-c 2' for single/double click, +;; move with arrow keys). +;; + Simulation keys to mimic Emacs key bindings for text edit (redo, select, +;; cancel, clear, etc). Some of them are not present on common keyboard +;; (keycode = 0). May need to use XKB extension. + +;;; Code: + +(require 'xcb-keysyms) +(require 'exwm-core) + +(defgroup exwm-input nil + "Input." + :group 'exwm) + +(defcustom exwm-input-prefix-keys + '(?\C-x ?\C-u ?\C-h ?\M-x ?\M-` ?\M-& ?\M-:) + "List of prefix keys EXWM should forward to Emacs when in `line-mode'. + +The point is to make keys like 'C-x C-f' forwarded to Emacs in `line-mode'. +There is no need to add prefix keys for global/simulation keys or those +defined in `exwm-mode-map' here." + :type '(repeat key-sequence) + :get (lambda (symbol) + (mapcar #'vector (default-value symbol))) + :set (lambda (symbol value) + (set symbol (mapcar (lambda (i) + (if (sequencep i) + (aref i 0) + i)) + value)))) + +(defcustom exwm-input-move-event 's-down-mouse-1 + "Emacs event to start moving a window." + :type 'key-sequence + :get (lambda (symbol) + (let ((value (default-value symbol))) + (if (mouse-event-p value) + value + (vector value)))) + :set (lambda (symbol value) + (set symbol (if (sequencep value) + (aref value 0) + value)))) + +(defcustom exwm-input-resize-event 's-down-mouse-3 + "Emacs event to start resizing a window." + :type 'key-sequence + :get (lambda (symbol) + (let ((value (default-value symbol))) + (if (mouse-event-p value) + value + (vector value)))) + :set (lambda (symbol value) + (set symbol (if (sequencep value) + (aref value 0) + value)))) + +(defcustom exwm-input-line-mode-passthrough nil + "Non-nil makes `line-mode' forward all events to Emacs." + :type 'boolean) + +;; Input focus update requests should be accumulated for a short time +;; interval so that only the last one need to be processed. This not +;; improves the overall performance, but avoids the problem of input +;; focus loop, which is a result of the interaction with Emacs frames. +;; +;; FIXME: The time interval is hard to decide and perhaps machine-dependent. +;; A value too small can cause redundant updates of input focus, +;; and even worse, dead loops. OTOH a large value would bring +;; laggy experience. +(defconst exwm-input--update-focus-interval 0.01 + "Time interval (in seconds) for accumulating input focus update requests.") + +(defconst exwm-input--passthrough-functions '(read-char + read-char-exclusive + read-key-sequence-vector + read-key-sequence + read-event) + "Low-level read functions that must be exempted from EXWM input handling.") + +(defvar exwm-input--during-command nil + "Indicate whether between `pre-command-hook' and `post-command-hook'.") + +(defvar exwm-input--global-keys nil "Global key bindings.") + +(defvar exwm-input--global-prefix-keys nil + "List of prefix keys of global key bindings.") + +(defvar exwm-input--line-mode-cache nil "Cache for incomplete key sequence.") + +(defvar exwm-input--local-simulation-keys nil + "Whether simulation keys are local.") + +(defvar exwm-input--simulation-keys nil "Simulation keys in `line-mode'.") + +(defvar exwm-input--skip-buffer-list-update nil + "Skip the upcoming `buffer-list-update'.") + +(defvar exwm-input--temp-line-mode nil + "Non-nil indicates it's in temporary line-mode for `char-mode'.") + +(defvar exwm-input--timestamp-atom nil) + +(defvar exwm-input--timestamp-callback nil) + +(defvar exwm-input--timestamp-window nil) + +(defvar exwm-input--update-focus-timer nil + "Timer for deferring the update of input focus.") + +(defvar exwm-input--update-focus-lock nil + "Lock for solving input focus update contention.") + +(defvar exwm-input--update-focus-window nil "The (Emacs) window to be focused. +This value should always be overwritten.") + +(defvar exwm-input--echo-area-timer nil "Timer for detecting echo area dirty.") + +(defvar exwm-input--event-hook nil + "Hook to run when EXWM receives an event.") + +(defvar exwm-input-input-mode-change-hook nil + "Hook to run when an input mode changes on an `exwm-mode' buffer. +Current buffer will be the `exwm-mode' buffer when this hook runs.") + +(defvar exwm-workspace--current) +(declare-function exwm-floating--do-moveresize "exwm-floating.el" + (data _synthetic)) +(declare-function exwm-floating--start-moveresize "exwm-floating.el" + (id &optional type)) +(declare-function exwm-floating--stop-moveresize "exwm-floating.el" + (&rest _args)) +(declare-function exwm-layout--iconic-state-p "exwm-layout.el" (&optional id)) +(declare-function exwm-layout--show "exwm-layout.el" (id &optional window)) +(declare-function exwm-reset "exwm.el" ()) +(declare-function exwm-workspace--minibuffer-own-frame-p "exwm-workspace.el") +(declare-function exwm-workspace--workspace-p "exwm-workspace.el" (workspace)) +(declare-function exwm-workspace-switch "exwm-workspace.el" + (frame-or-index &optional force)) + +(defun exwm-input--set-focus (id) + "Set input focus to window ID in a proper way." + (let ((from (slot-value (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetInputFocus)) + 'focus)) + tree) + (if (or (exwm--id->buffer from) + (eq from id)) + (exwm--log "#x%x => #x%x" (or from 0) (or id 0)) + ;; Attempt to find the top-level X window for a 'focus proxy'. + (unless (= from xcb:Window:None) + (setq tree (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:QueryTree + :window from))) + (when tree + (setq from (slot-value tree 'parent)))) + (exwm--log "#x%x (corrected) => #x%x" (or from 0) (or id 0))) + (when (and (exwm--id->buffer id) + ;; Avoid redundant input focus transfer. + (not (eq from id))) + (with-current-buffer (exwm--id->buffer id) + (exwm-input--update-timestamp + (lambda (timestamp id send-input-focus wm-take-focus) + (when send-input-focus + (xcb:+request exwm--connection + (make-instance 'xcb:SetInputFocus + :revert-to xcb:InputFocus:Parent + :focus id + :time timestamp))) + (when wm-take-focus + (let ((event (make-instance 'xcb:icccm:WM_TAKE_FOCUS + :window id + :time timestamp))) + (setq event (xcb:marshal event exwm--connection)) + (xcb:+request exwm--connection + (make-instance 'xcb:icccm:SendEvent + :destination id + :event event)))) + (exwm-input--set-active-window id) + (xcb:flush exwm--connection)) + id + (or exwm--hints-input + (not (memq xcb:Atom:WM_TAKE_FOCUS exwm--protocols))) + (memq xcb:Atom:WM_TAKE_FOCUS exwm--protocols)))))) + +(defun exwm-input--update-timestamp (callback &rest args) + "Fetch the latest timestamp from the server and feed it to CALLBACK. + +ARGS are additional arguments to CALLBACK." + (setq exwm-input--timestamp-callback (cons callback args)) + (exwm--log) + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeProperty + :mode xcb:PropMode:Replace + :window exwm-input--timestamp-window + :property exwm-input--timestamp-atom + :type xcb:Atom:CARDINAL + :format 32 + :data-len 0 + :data nil)) + (xcb:flush exwm--connection)) + +(defun exwm-input--on-PropertyNotify (data _synthetic) + "Handle PropertyNotify events." + (exwm--log) + (when exwm-input--timestamp-callback + (let ((obj (make-instance 'xcb:PropertyNotify))) + (xcb:unmarshal obj data) + (when (= exwm-input--timestamp-window + (slot-value obj 'window)) + (apply (car exwm-input--timestamp-callback) + (slot-value obj 'time) + (cdr exwm-input--timestamp-callback)) + (setq exwm-input--timestamp-callback nil))))) + +(defvar exwm-input--last-enter-notify-position nil) + +(defun exwm-input--on-EnterNotify (data _synthetic) + "Handle EnterNotify events." + (let ((evt (make-instance 'xcb:EnterNotify)) + buffer window frame frame-xid edges fake-evt) + (xcb:unmarshal evt data) + (with-slots (time root event root-x root-y event-x event-y state) evt + (setq buffer (exwm--id->buffer event) + window (get-buffer-window buffer t)) + (exwm--log "buffer=%s; window=%s" buffer window) + (when (and buffer window (not (eq window (selected-window))) + (not (equal exwm-input--last-enter-notify-position + (vector root-x root-y)))) + (setq frame (window-frame window) + frame-xid (frame-parameter frame 'exwm-id)) + (unless (eq frame exwm-workspace--current) + (if (exwm-workspace--workspace-p frame) + ;; The X window is on another workspace. + (exwm-workspace-switch frame) + (with-current-buffer buffer + (when (and (derived-mode-p 'exwm-mode) + (not (eq exwm--frame exwm-workspace--current))) + ;; The floating X window is on another workspace. + (exwm-workspace-switch exwm--frame))))) + ;; Send a fake MotionNotify event to Emacs. + (setq edges (window-inside-pixel-edges window) + fake-evt (make-instance 'xcb:MotionNotify + :detail 0 + :time time + :root root + :event frame-xid + :child xcb:Window:None + :root-x root-x + :root-y root-y + :event-x (+ event-x (elt edges 0)) + :event-y (+ event-y (elt edges 1)) + :state state + :same-screen 1)) + (xcb:+request exwm--connection + (make-instance 'xcb:SendEvent + :propagate 0 + :destination frame-xid + :event-mask xcb:EventMask:NoEvent + :event (xcb:marshal fake-evt exwm--connection))) + (xcb:flush exwm--connection)) + (setq exwm-input--last-enter-notify-position (vector root-x root-y))))) + +(defun exwm-input--on-keysyms-update () + (exwm--log) + (let ((exwm-input--global-prefix-keys nil)) + (exwm-input--update-global-prefix-keys))) + +(defun exwm-input--on-buffer-list-update () + "Run in `buffer-list-update-hook' to track input focus." + (when (and ; this hook is called incesantly; place cheap tests on top + (not exwm-input--skip-buffer-list-update) + (exwm--terminal-p) ; skip other terminals, e.g. TTY client frames + (not (frame-parameter nil 'no-accept-focus))) + (exwm--log "current-buffer=%S selected-window=%S" + (current-buffer) (selected-window)) + (redirect-frame-focus (selected-frame) nil) + (setq exwm-input--update-focus-window (selected-window)) + (exwm-input--update-focus-defer))) + +(defun exwm-input--update-focus-defer () + "Schedule a deferred update to input focus. +Instead of immediately focusing the current window, it defers the focus change +until the selected window stops changing (debouncing input focus updates)." + (when exwm-input--update-focus-timer + (cancel-timer exwm-input--update-focus-timer)) + (setq exwm-input--update-focus-timer + ;; Attempt to accumulate successive events close enough. + (run-with-timer exwm-input--update-focus-interval + nil + #'exwm-input--update-focus-commit))) + +(defun exwm-input--update-focus-commit () + "Attempt to update the window focus. +If we're currently updating the window focus, re-schedule a focus update +attempt later." + (if exwm-input--update-focus-lock + (exwm-input--update-focus-defer) + (let ((exwm-input--update-focus-lock t)) + (exwm-input--update-focus exwm-input--update-focus-window)))) + +(defun exwm-input--update-focus (window) + "Update input focus to WINDOW." + (when (window-live-p window) + (exwm--log "focus-window=%s focus-buffer=%s" window (window-buffer window)) + (with-current-buffer (window-buffer window) + (if (derived-mode-p 'exwm-mode) + (if (not (eq exwm--frame exwm-workspace--current)) + (progn + (set-frame-parameter exwm--frame 'exwm-selected-window window) + (exwm--defer 0 #'exwm-workspace-switch exwm--frame)) + (exwm--log "Set focus on #x%x" exwm--id) + (when exwm--floating-frame + ;; Adjust stacking orders of the floating X window. + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window exwm--id + :value-mask xcb:ConfigWindow:StackMode + :stack-mode xcb:StackMode:TopIf)) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window (frame-parameter exwm--floating-frame + 'exwm-container) + :value-mask (logior + xcb:ConfigWindow:Sibling + xcb:ConfigWindow:StackMode) + :sibling exwm--id + :stack-mode xcb:StackMode:Below)) + ;; This floating X window might be hide by `exwm-floating-hide'. + (when (exwm-layout--iconic-state-p) + (exwm-layout--show exwm--id window)) + (xcb:flush exwm--connection)) + (exwm-input--set-focus exwm--id)) + (when (eq (selected-window) window) + (exwm--log "Focus on %s" window) + (if (and (exwm-workspace--workspace-p (selected-frame)) + (not (eq (selected-frame) exwm-workspace--current))) + ;; The focus is on another workspace (e.g. it got clicked) + ;; so switch to it. + (progn + (exwm--log "Switching to %s's workspace %s (%s)" + window + (window-frame window) + (selected-frame)) + (set-frame-parameter (selected-frame) 'exwm-selected-window + window) + (exwm--defer 0 #'exwm-workspace-switch (selected-frame))) + ;; The focus is still on the current workspace. + (if (not (and (exwm-workspace--minibuffer-own-frame-p) + (minibufferp))) + (x-focus-frame (window-frame window)) + ;; X input focus should be set on the previously selected + ;; frame. + (x-focus-frame (window-frame (minibuffer-window)))) + (exwm-input--set-active-window) + (xcb:flush exwm--connection))))))) + +(defun exwm-input--set-active-window (&optional id) + "Set _NET_ACTIVE_WINDOW." + (exwm--log) + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_ACTIVE_WINDOW + :window exwm--root + :data (or id xcb:Window:None)))) + +(defun exwm-input--on-ButtonPress (data _synthetic) + "Handle ButtonPress event." + (let ((obj (make-instance 'xcb:ButtonPress)) + (mode xcb:Allow:SyncPointer) + button-event window buffer frame fake-last-command) + (xcb:unmarshal obj data) + (exwm--log "major-mode=%s buffer=%s" + major-mode (buffer-name (current-buffer))) + (with-slots (detail event state) obj + (setq button-event (xcb:keysyms:keysym->event exwm--connection + detail state) + buffer (exwm--id->buffer event) + window (get-buffer-window buffer t)) + (cond ((and (eq button-event exwm-input-move-event) + buffer + ;; Either an undecorated or a floating X window. + (with-current-buffer buffer + (or (not (derived-mode-p 'exwm-mode)) + exwm--floating-frame))) + ;; Move + (exwm-floating--start-moveresize + event xcb:ewmh:_NET_WM_MOVERESIZE_MOVE)) + ((and (eq button-event exwm-input-resize-event) + buffer + (with-current-buffer buffer + (or (not (derived-mode-p 'exwm-mode)) + exwm--floating-frame))) + ;; Resize + (exwm-floating--start-moveresize event)) + (buffer + ;; Click to focus + (setq fake-last-command t) + (unless (eq window (selected-window)) + (setq frame (window-frame window)) + (unless (eq frame exwm-workspace--current) + (if (exwm-workspace--workspace-p frame) + ;; The X window is on another workspace + (exwm-workspace-switch frame) + (with-current-buffer buffer + (when (and (derived-mode-p 'exwm-mode) + (not (eq exwm--frame + exwm-workspace--current))) + ;; The floating X window is on another workspace + (exwm-workspace-switch exwm--frame))))) + ;; It has been reported that the `window' may have be deleted + (if (window-live-p window) + (select-window window) + (setq window (get-buffer-window buffer t)) + (when window (select-window window)))) + ;; Also process keybindings. + (with-current-buffer buffer + (when (derived-mode-p 'exwm-mode) + (cl-case exwm--input-mode + (line-mode + (setq mode (exwm-input--on-ButtonPress-line-mode + buffer button-event))) + (char-mode + (setq mode (exwm-input--on-ButtonPress-char-mode))))))) + (t + ;; Replay this event by default. + (setq fake-last-command t) + (setq mode xcb:Allow:ReplayPointer))) + (when fake-last-command + (if buffer + (with-current-buffer buffer + (exwm-input--fake-last-command)) + (exwm-input--fake-last-command)))) + (xcb:+request exwm--connection + (make-instance 'xcb:AllowEvents :mode mode :time xcb:Time:CurrentTime)) + (xcb:flush exwm--connection)) + (run-hooks 'exwm-input--event-hook)) + +(defun exwm-input--on-KeyPress (data _synthetic) + "Handle KeyPress event." + (with-current-buffer (window-buffer (selected-window)) + (let ((obj (make-instance 'xcb:KeyPress))) + (xcb:unmarshal obj data) + (exwm--log "major-mode=%s buffer=%s" + major-mode (buffer-name (current-buffer))) + (if (derived-mode-p 'exwm-mode) + (cl-case exwm--input-mode + (line-mode + (exwm-input--on-KeyPress-line-mode obj data)) + (char-mode + (exwm-input--on-KeyPress-char-mode obj data))) + (exwm-input--on-KeyPress-char-mode obj))) + (run-hooks 'exwm-input--event-hook))) + +(defun exwm-input--on-CreateNotify (data _synthetic) + "Handle CreateNotify events." + (exwm--log) + (let ((evt (make-instance 'xcb:CreateNotify))) + (xcb:unmarshal evt data) + (with-slots (window) evt + (exwm-input--grab-global-prefix-keys window)))) + +(defun exwm-input--update-global-prefix-keys () + "Update `exwm-input--global-prefix-keys'." + (exwm--log) + (when exwm--connection + (let ((original exwm-input--global-prefix-keys)) + (setq exwm-input--global-prefix-keys nil) + (dolist (i exwm-input--global-keys) + (cl-pushnew (elt i 0) exwm-input--global-prefix-keys)) + (unless (equal original exwm-input--global-prefix-keys) + (apply #'exwm-input--grab-global-prefix-keys + (slot-value (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:QueryTree + :window exwm--root)) + 'children)))))) + +(defun exwm-input--grab-global-prefix-keys (&rest xwins) + (exwm--log) + (let ((req (make-instance 'xcb:GrabKey + :owner-events 0 + :grab-window nil + :modifiers nil + :key nil + :pointer-mode xcb:GrabMode:Async + :keyboard-mode xcb:GrabMode:Async)) + keysyms keycode alt-modifier) + (dolist (k exwm-input--global-prefix-keys) + (setq keysyms (xcb:keysyms:event->keysyms exwm--connection k)) + (if (not keysyms) + (warn "Key unavailable: %s" (key-description (vector k))) + (setq keycode (xcb:keysyms:keysym->keycode exwm--connection + (caar keysyms))) + (exwm--log "Grabbing key=%s (keysyms=%s keycode=%s)" + (single-key-description k) keysyms keycode) + (dolist (keysym keysyms) + (setf (slot-value req 'modifiers) (cdr keysym) + (slot-value req 'key) keycode) + ;; Also grab this key with num-lock mask set. + (when (and (/= 0 xcb:keysyms:num-lock-mask) + (= 0 (logand (cdr keysym) xcb:keysyms:num-lock-mask))) + (setf alt-modifier (logior (cdr keysym) + xcb:keysyms:num-lock-mask))) + (dolist (xwin xwins) + (setf (slot-value req 'grab-window) xwin) + (xcb:+request exwm--connection req) + (when alt-modifier + (setf (slot-value req 'modifiers) alt-modifier) + (xcb:+request exwm--connection req)))))) + (xcb:flush exwm--connection))) + +(defun exwm-input--set-key (key command) + (exwm--log "key: %s, command: %s" key command) + (global-set-key key command) + (cl-pushnew key exwm-input--global-keys)) + +(defcustom exwm-input-global-keys nil + "Global keys. + +It is an alist of the form (key . command), meaning giving KEY (a key +sequence) a global binding as COMMAND. + +Notes: +* Setting the value directly (rather than customizing it) after EXWM + finishes initialization has no effect." + :type '(alist :key-type key-sequence :value-type function) + :set (lambda (symbol value) + (when (boundp symbol) + (dolist (i (symbol-value symbol)) + (global-unset-key (car i)))) + (set symbol value) + (setq exwm-input--global-keys nil) + (dolist (i value) + (exwm-input--set-key (car i) (cdr i))) + (when exwm--connection + (exwm-input--update-global-prefix-keys)))) + +;;;###autoload +(defun exwm-input-set-key (key command) + "Set a global key binding. + +The new key binding only takes effect in real time when this command is +called interactively, and is lost when this session ends unless it's +specifically saved in the Customize interface for `exwm-input-global-keys'. + +In configuration you should customize or set `exwm-input-global-keys' +instead." + (interactive "KSet key globally: \nCSet key %s to command: ") + (exwm--log) + (setq exwm-input-global-keys (append exwm-input-global-keys + (list (cons key command)))) + (exwm-input--set-key key command) + (when (called-interactively-p 'any) + (exwm-input--update-global-prefix-keys))) + +(defsubst exwm-input--unread-event (event) + (declare (indent defun)) + (setq unread-command-events + (append unread-command-events `((t . ,event))))) + +(defun exwm-input--mimic-read-event (event) + "Process EVENT as if it were returned by `read-event'." + (exwm--log) + (unless (eq 0 extra-keyboard-modifiers) + (setq event (event-convert-list (append (event-modifiers + extra-keyboard-modifiers) + event)))) + (when (characterp event) + (let ((event* (when keyboard-translate-table + (aref keyboard-translate-table event)))) + (when event* + (setq event event*)))) + event) + +(cl-defun exwm-input--translate (key) + (let (translation) + (dolist (map (list input-decode-map + local-function-key-map + key-translation-map)) + (setq translation (lookup-key map key)) + (if (functionp translation) + (cl-return-from exwm-input--translate (funcall translation nil)) + (when (vectorp translation) + (cl-return-from exwm-input--translate translation))))) + key) + +(defun exwm-input--cache-event (event &optional temp-line-mode) + "Cache EVENT." + (exwm--log "%s" event) + (setq exwm-input--line-mode-cache + (vconcat exwm-input--line-mode-cache (vector event))) + ;; Attempt to translate this key sequence. + (setq exwm-input--line-mode-cache + (exwm-input--translate exwm-input--line-mode-cache)) + ;; When the key sequence is complete (not a keymap). + ;; Note that `exwm-input--line-mode-cache' might get translated to nil, for + ;; example 'mouse--down-1-maybe-follows-link' does this. + (if (and exwm-input--line-mode-cache + (keymapp (key-binding exwm-input--line-mode-cache))) + ;; Grab keyboard temporarily to intercept the complete key sequence. + (when temp-line-mode + (setq exwm-input--temp-line-mode t) + (exwm-input--grab-keyboard)) + (setq exwm-input--line-mode-cache nil) + (when exwm-input--temp-line-mode + (setq exwm-input--temp-line-mode nil) + (exwm-input--release-keyboard)))) + +(defun exwm-input--event-passthrough-p (event) + "Whether EVENT should be passed to Emacs. +Current buffer must be an `exwm-mode' buffer." + (or exwm-input-line-mode-passthrough + exwm-input--during-command + ;; Forward the event when there is an incomplete key + ;; sequence or when the minibuffer is active. + exwm-input--line-mode-cache + (eq (active-minibuffer-window) (selected-window)) + ;; + (memq event exwm-input--global-prefix-keys) + (memq event exwm-input-prefix-keys) + (when overriding-terminal-local-map + (lookup-key overriding-terminal-local-map + (vector event))) + (lookup-key (current-local-map) (vector event)) + (gethash event exwm-input--simulation-keys))) + +(defun exwm-input--noop (&rest _args) + "A placeholder command." + (interactive)) + +(defun exwm-input--fake-last-command () + "Fool some packages into thinking there is a change in the buffer." + (setq last-command #'exwm-input--noop) + ;; The Emacs manual says: + ;; > Quitting is suppressed while running pre-command-hook and + ;; > post-command-hook. If an error happens while executing one of these + ;; > hooks, it does not terminate execution of the hook; instead the error is + ;; > silenced and the function in which the error occurred is removed from the + ;; > hook. + ;; We supress errors but neither continue execution nor we remove from the + ;; hook. + (condition-case err + (run-hooks 'pre-command-hook) + ((error) + (exwm--log "Error occurred while running pre-command-hook: %s" + (error-message-string err)) + (xcb-debug:backtrace))) + (condition-case err + (run-hooks 'post-command-hook) + ((error) + (exwm--log "Error occurred while running post-command-hook: %s" + (error-message-string err)) + (xcb-debug:backtrace)))) + +(defun exwm-input--on-KeyPress-line-mode (key-press raw-data) + "Parse X KeyPress event to Emacs key event and then feed the command loop." + (with-slots (detail state) key-press + (let ((keysym (xcb:keysyms:keycode->keysym exwm--connection detail state)) + event raw-event mode) + (exwm--log "%s" keysym) + (when (and (/= 0 (car keysym)) + (setq raw-event (xcb:keysyms:keysym->event + exwm--connection (car keysym) + (logand state (lognot (cdr keysym))))) + (setq event (exwm-input--mimic-read-event raw-event)) + (exwm-input--event-passthrough-p event)) + (setq mode xcb:Allow:AsyncKeyboard) + (exwm-input--cache-event event) + (exwm-input--unread-event raw-event)) + (unless mode + (if (= 0 (logand #x6000 state)) ;Check the 13~14 bits. + ;; Not an XKB state; just replay it. + (setq mode xcb:Allow:ReplayKeyboard) + ;; An XKB state; sent it with SendEvent. + ;; FIXME: Can this also be replayed? + ;; FIXME: KeyRelease events are lost. + (setq mode xcb:Allow:AsyncKeyboard) + (xcb:+request exwm--connection + (make-instance 'xcb:SendEvent + :propagate 0 + :destination (slot-value key-press 'event) + :event-mask xcb:EventMask:NoEvent + :event raw-data))) + (when event + (if (not defining-kbd-macro) + (exwm-input--fake-last-command) + ;; Make Emacs aware of this event when defining keyboard macros. + (set-transient-map `(keymap (t . ,#'exwm-input--noop))) + (exwm-input--unread-event event)))) + (xcb:+request exwm--connection + (make-instance 'xcb:AllowEvents + :mode mode + :time xcb:Time:CurrentTime)) + (xcb:flush exwm--connection)))) + +(defun exwm-input--on-KeyPress-char-mode (key-press &optional _raw-data) + "Handle KeyPress event in `char-mode'." + (with-slots (detail state) key-press + (let ((keysym (xcb:keysyms:keycode->keysym exwm--connection detail state)) + event raw-event) + (exwm--log "%s" keysym) + (when (and (/= 0 (car keysym)) + (setq raw-event (xcb:keysyms:keysym->event + exwm--connection (car keysym) + (logand state (lognot (cdr keysym))))) + (setq event (exwm-input--mimic-read-event raw-event))) + (if (not (derived-mode-p 'exwm-mode)) + (exwm-input--unread-event raw-event) + (exwm-input--cache-event event t) + (exwm-input--unread-event raw-event))))) + (xcb:+request exwm--connection + (make-instance 'xcb:AllowEvents + :mode xcb:Allow:AsyncKeyboard + :time xcb:Time:CurrentTime)) + (xcb:flush exwm--connection)) + +(defun exwm-input--on-ButtonPress-line-mode (buffer button-event) + "Handle button events in line mode. +BUFFER is the `exwm-mode' buffer the event was generated +on. BUTTON-EVENT is the X event converted into an Emacs event. + +The return value is used as event_mode to release the original +button event." + (with-current-buffer buffer + (let ((read-event (exwm-input--mimic-read-event button-event))) + (exwm--log "%s" read-event) + (if (and read-event + (exwm-input--event-passthrough-p read-event)) + ;; The event should be forwarded to emacs + (progn + (exwm-input--cache-event read-event) + (exwm-input--unread-event button-event) + xcb:Allow:SyncPointer) + ;; The event should be replayed + xcb:Allow:ReplayPointer)))) + +(defun exwm-input--on-ButtonPress-char-mode () + "Handle button events in `char-mode'. +The return value is used as event_mode to release the original +button event." + (exwm--log) + xcb:Allow:ReplayPointer) + +(defun exwm-input--update-mode-line (id) + "Update the propertized `mode-line-process' for window ID." + (exwm--log "#x%x" id) + (let (help-echo cmd mode) + (with-current-buffer (exwm--id->buffer id) + (cl-case exwm--input-mode + (line-mode + (setq mode "line" + help-echo "mouse-1: Switch to char-mode" + cmd (lambda () + (interactive) + (exwm-input-release-keyboard id)))) + (char-mode + (setq mode "char" + help-echo "mouse-1: Switch to line-mode" + cmd (lambda () + (interactive) + (exwm-input-grab-keyboard id))))) + (setq mode-line-process + `(": " + (:propertize ,mode + help-echo ,help-echo + mouse-face mode-line-highlight + local-map + (keymap + (mode-line + keymap + (down-mouse-1 . ,cmd)))))) + (force-mode-line-update)))) + +(defun exwm-input--grab-keyboard (&optional id) + "Grab all key events on window ID." + (unless id (setq id (exwm--buffer->id (window-buffer)))) + (when id + (exwm--log "id=#x%x" id) + (when (xcb:+request-checked+request-check exwm--connection + (make-instance 'xcb:GrabKey + :owner-events 0 + :grab-window id + :modifiers xcb:ModMask:Any + :key xcb:Grab:Any + :pointer-mode xcb:GrabMode:Async + :keyboard-mode xcb:GrabMode:Sync)) + (exwm--log "Failed to grab keyboard for #x%x" id)) + (let ((buffer (exwm--id->buffer id))) + (when buffer + (with-current-buffer buffer + (setq exwm--input-mode 'line-mode) + (run-hooks 'exwm-input-input-mode-change-hook)))))) + +(defun exwm-input--release-keyboard (&optional id) + "Ungrab all key events on window ID." + (unless id (setq id (exwm--buffer->id (window-buffer)))) + (when id + (exwm--log "id=#x%x" id) + (when (xcb:+request-checked+request-check exwm--connection + (make-instance 'xcb:UngrabKey + :key xcb:Grab:Any + :grab-window id + :modifiers xcb:ModMask:Any)) + (exwm--log "Failed to release keyboard for #x%x" id)) + (exwm-input--grab-global-prefix-keys id) + (let ((buffer (exwm--id->buffer id))) + (when buffer + (with-current-buffer buffer + (setq exwm--input-mode 'char-mode) + (run-hooks 'exwm-input-input-mode-change-hook)))))) + +;;;###autoload +(defun exwm-input-grab-keyboard (&optional id) + "Switch to `line-mode'." + (interactive (list (when (derived-mode-p 'exwm-mode) + (exwm--buffer->id (window-buffer))))) + (when id + (exwm--log "id=#x%x" id) + (setq exwm--selected-input-mode 'line-mode) + (exwm-input--grab-keyboard id) + (exwm-input--update-mode-line id))) + +;;;###autoload +(defun exwm-input-release-keyboard (&optional id) + "Switch to `char-mode`." + (interactive (list (when (derived-mode-p 'exwm-mode) + (exwm--buffer->id (window-buffer))))) + (when id + (exwm--log "id=#x%x" id) + (setq exwm--selected-input-mode 'char-mode) + (exwm-input--release-keyboard id) + (exwm-input--update-mode-line id))) + +;;;###autoload +(defun exwm-input-toggle-keyboard (&optional id) + "Toggle between `line-mode' and `char-mode'." + (interactive (list (when (derived-mode-p 'exwm-mode) + (exwm--buffer->id (window-buffer))))) + (when id + (exwm--log "id=#x%x" id) + (with-current-buffer (exwm--id->buffer id) + (cl-case exwm--input-mode + (line-mode + (exwm-input-release-keyboard id)) + (char-mode + (exwm-reset)))))) + +(defun exwm-input--fake-key (event) + "Fake a key event equivalent to Emacs event EVENT." + (let* ((keysyms (xcb:keysyms:event->keysyms exwm--connection event)) + keycode id) + (when (= 0 (caar keysyms)) + (user-error "[EXWM] Invalid key: %s" (single-key-description event))) + (setq keycode (xcb:keysyms:keysym->keycode exwm--connection + (caar keysyms))) + (when (/= 0 keycode) + (setq id (exwm--buffer->id (window-buffer (selected-window)))) + (exwm--log "id=#x%x event=%s keycode" id event keycode) + (dolist (class '(xcb:KeyPress xcb:KeyRelease)) + (xcb:+request exwm--connection + (make-instance 'xcb:SendEvent + :propagate 0 :destination id + :event-mask xcb:EventMask:NoEvent + :event (xcb:marshal + (make-instance class + :detail keycode + :time xcb:Time:CurrentTime + :root exwm--root :event id + :child 0 + :root-x 0 :root-y 0 + :event-x 0 :event-y 0 + :state (cdar keysyms) + :same-screen 1) + exwm--connection))))) + (xcb:flush exwm--connection))) + +;;;###autoload +(cl-defun exwm-input-send-next-key (times &optional end-key) + "Send next key to client window. + +EXWM will prompt for the key to send. This command can be prefixed to send +multiple keys. If END-KEY is non-nil, stop sending keys if it's pressed." + (interactive "p") + (exwm--log) + (unless (derived-mode-p 'exwm-mode) + (cl-return-from exwm-input-send-next-key)) + (when (> times 12) (setq times 12)) + (let (key keys) + (dotimes (i times) + ;; Skip events not from keyboard + (let ((exwm-input-line-mode-passthrough t)) + (catch 'break + (while t + (setq key (read-key (format "Send key: %s (%d/%d) %s" + (key-description keys) + (1+ i) times + (if end-key + (concat "To exit, press: " + (key-description + (list end-key))) + "")))) + (unless (listp key) (throw 'break nil))))) + (setq keys (vconcat keys (vector key))) + (when (eq key end-key) (cl-return-from exwm-input-send-next-key)) + (exwm-input--fake-key key)))) + +(defun exwm-input--set-simulation-keys (simulation-keys &optional no-refresh) + "Set simulation keys." + (exwm--log "%s" simulation-keys) + (unless no-refresh + ;; Unbind simulation keys. + (let ((hash (buffer-local-value 'exwm-input--simulation-keys + (current-buffer)))) + (when (hash-table-p hash) + (maphash (lambda (key _value) + (when (sequencep key) + (if exwm-input--local-simulation-keys + (local-unset-key key) + (define-key exwm-mode-map key nil)))) + hash))) + ;; Abandon the old hash table. + (setq exwm-input--simulation-keys (make-hash-table :test #'equal))) + (dolist (i simulation-keys) + (let ((original (vconcat (car i))) + (simulated (cdr i))) + (setq simulated (if (sequencep simulated) + (append simulated nil) + (list simulated))) + ;; The key stored is a key sequence (vector). + ;; The value stored is a list of key events. + (puthash original simulated exwm-input--simulation-keys) + ;; Also mark the prefix key as used. + (puthash (aref original 0) t exwm-input--simulation-keys))) + ;; Update keymaps. + (maphash (lambda (key _value) + (when (sequencep key) + (if exwm-input--local-simulation-keys + (local-set-key key #'exwm-input-send-simulation-key) + (define-key exwm-mode-map key + #'exwm-input-send-simulation-key)))) + exwm-input--simulation-keys)) + +(defcustom exwm-input-simulation-keys nil + "Simulation keys. + +It is an alist of the form (original-key . simulated-key), where both +original-key and simulated-key are key sequences. Original-key is what you +type to an X window in `line-mode' which then gets translated to simulated-key +by EXWM and forwarded to the X window. + +Notes: +* Setting the value directly (rather than customizing it) after EXWM + finishes initialization has no effect. +* Original-keys consist of multiple key events are only supported in Emacs + 26.2 and later. +* A minority of applications do not accept simulated keys by default. It's + required to customize them to accept events sent by SendEvent. +* The predefined examples in the Customize interface are not guaranteed to + work for all applications. This can be tweaked on a per application basis + with `exwm-input-set-local-simulation-keys'." + :type '(alist :key-type (key-sequence :tag "Original") + :value-type (choice (key-sequence :tag "User-defined") + (key-sequence :tag "Move left" [left]) + (key-sequence :tag "Move right" [right]) + (key-sequence :tag "Move up" [up]) + (key-sequence :tag "Move down" [down]) + (key-sequence :tag "Move to BOL" [home]) + (key-sequence :tag "Move to EOL" [end]) + (key-sequence :tag "Page up" [prior]) + (key-sequence :tag "Page down" [next]) + (key-sequence :tag "Copy" [C-c]) + (key-sequence :tag "Paste" [C-v]) + (key-sequence :tag "Delete" [delete]) + (key-sequence :tag "Delete to EOL" + [S-end delete]))) + :set (lambda (symbol value) + (set symbol value) + (exwm-input--set-simulation-keys value))) + +(defcustom exwm-input-pre-post-command-blacklist '(exit-minibuffer + abort-recursive-edit + minibuffer-keyboard-quit) + "Commands impossible to detect with `post-command-hook'." + :type '(repeat function)) + +(cl-defun exwm-input--read-keys (prompt stop-key) + (let ((cursor-in-echo-area t) + keys key) + (while (not (eq key stop-key)) + (setq key (read-key (format "%s (terminate with %s): %s" + prompt + (key-description (vector stop-key)) + (key-description keys))) + keys (vconcat keys (vector key)))) + (when (> (length keys) 1) + (substring keys 0 -1)))) + +;;;###autoload +(defun exwm-input-set-simulation-key (original-key simulated-key) + "Set a simulation key. + +The simulation key takes effect in real time, but is lost when this session +ends unless it's specifically saved in the Customize interface for +`exwm-input-simulation-keys'." + (interactive + (let (original simulated) + (setq original (exwm-input--read-keys "Translate from" ?\C-g)) + (when original + (setq simulated (exwm-input--read-keys + (format "Translate from %s to" + (key-description original)) + ?\C-g))) + (list original simulated))) + (exwm--log "original: %s, simulated: %s" original-key simulated-key) + (when (and original-key simulated-key) + (let ((entry `((,original-key . ,simulated-key)))) + (setq exwm-input-simulation-keys (append exwm-input-simulation-keys + entry)) + (exwm-input--set-simulation-keys entry t)))) + +(defun exwm-input--unset-simulation-keys () + "Clear simulation keys and key bindings defined." + (exwm--log) + (when (hash-table-p exwm-input--simulation-keys) + (maphash (lambda (key _value) + (when (sequencep key) + (define-key exwm-mode-map key nil))) + exwm-input--simulation-keys) + (clrhash exwm-input--simulation-keys))) + +(defun exwm-input-set-local-simulation-keys (simulation-keys) + "Set buffer-local simulation keys. + +SIMULATION-KEYS is an alist of the form (original-key . simulated-key), +where both ORIGINAL-KEY and SIMULATED-KEY are key sequences." + (exwm--log) + (make-local-variable 'exwm-input--simulation-keys) + (use-local-map (copy-keymap exwm-mode-map)) + (let ((exwm-input--local-simulation-keys t)) + (exwm-input--set-simulation-keys simulation-keys))) + +;;;###autoload +(cl-defun exwm-input-send-simulation-key (times) + "Fake a key event according to the last input key sequence." + (interactive "p") + (exwm--log) + (unless (derived-mode-p 'exwm-mode) + (cl-return-from exwm-input-send-simulation-key)) + (let ((keys (gethash (this-single-command-keys) + exwm-input--simulation-keys))) + (dotimes (_ times) + (dolist (key keys) + (exwm-input--fake-key key))))) + +;;;###autoload +(defmacro exwm-input-invoke-factory (keys) + "Make a command that invokes KEYS when called. + +One use is to access the keymap bound to KEYS (as prefix keys) in `char-mode'." + (let* ((keys (kbd keys)) + (description (key-description keys))) + `(defun ,(intern (concat "exwm-input--invoke--" description)) () + ,(format "Invoke `%s'." description) + (interactive) + (mapc (lambda (key) + (exwm-input--cache-event key t) + (exwm-input--unread-event key)) + ',(listify-key-sequence keys))))) + +(defun exwm-input--on-pre-command () + "Run in `pre-command-hook'." + (unless (or (eq this-command #'exwm-input--noop) + (memq this-command exwm-input-pre-post-command-blacklist)) + (setq exwm-input--during-command t))) + +(defun exwm-input--on-post-command () + "Run in `post-command-hook'." + (unless (eq this-command #'exwm-input--noop) + (setq exwm-input--during-command nil))) + +(defun exwm-input--on-minibuffer-setup () + "Run in `minibuffer-setup-hook' to grab keyboard if necessary." + (let* ((window (or (minibuffer-selected-window) ; minibuffer-setup-hook + (selected-window))) ; echo-area-clear-hook + (frame (window-frame window))) + (when (exwm--terminal-p frame) + (with-current-buffer (window-buffer window) + (when (and (derived-mode-p 'exwm-mode) + (eq exwm--selected-input-mode 'char-mode)) + (exwm--log "Grab #x%x window=%s frame=%s" exwm--id window frame) + (exwm-input--grab-keyboard exwm--id)))))) + +(defun exwm-input--on-minibuffer-exit () + "Run in `minibuffer-exit-hook' to release keyboard if necessary." + (let* ((window (or (minibuffer-selected-window) ; minibuffer-setup-hook + (selected-window))) ; echo-area-clear-hook + (frame (window-frame window))) + (when (exwm--terminal-p frame) + (with-current-buffer (window-buffer window) + (when (and (derived-mode-p 'exwm-mode) + (eq exwm--selected-input-mode 'char-mode) + (eq exwm--input-mode 'line-mode)) + (exwm--log "Release #x%x window=%s frame=%s" exwm--id window frame) + (exwm-input--release-keyboard exwm--id)))))) + +(defun exwm-input--on-echo-area-dirty () + "Run when new message arrives to grab keyboard if necessary." + (when (and cursor-in-echo-area + (not (active-minibuffer-window))) + (exwm--log) + (exwm-input--on-minibuffer-setup))) + +(defun exwm-input--on-echo-area-clear () + "Run in `echo-area-clear-hook' to release keyboard if necessary." + (unless (current-message) + (exwm--log) + (exwm-input--on-minibuffer-exit))) + +(defun exwm-input--call-with-passthrough (function &rest args) + "Bind `exwm-input-line-mode-passthrough' and call FUNCTION with ARGS." + (let ((exwm-input-line-mode-passthrough t)) + (apply function args))) + +(defun exwm-input--init () + "Initialize the keyboard module." + (exwm--log) + ;; Refresh keyboard mapping + (xcb:keysyms:init exwm--connection #'exwm-input--on-keysyms-update) + ;; Create the X window and intern the atom used to fetch timestamp. + (setq exwm-input--timestamp-window (xcb:generate-id exwm--connection)) + (xcb:+request exwm--connection + (make-instance 'xcb:CreateWindow + :depth 0 + :wid exwm-input--timestamp-window + :parent exwm--root + :x -1 + :y -1 + :width 1 + :height 1 + :border-width 0 + :class xcb:WindowClass:CopyFromParent + :visual 0 + :value-mask xcb:CW:EventMask + :event-mask xcb:EventMask:PropertyChange)) + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_WM_NAME + :window exwm-input--timestamp-window + :data "EXWM: exwm-input--timestamp-window")) + (setq exwm-input--timestamp-atom (exwm--intern-atom "_TIME")) + ;; Initialize global keys. + (dolist (i exwm-input-global-keys) + (exwm-input--set-key (car i) (cdr i))) + ;; Initialize simulation keys. + (when exwm-input-simulation-keys + (exwm-input--set-simulation-keys exwm-input-simulation-keys)) + ;; Attach event listeners + (xcb:+event exwm--connection 'xcb:PropertyNotify + #'exwm-input--on-PropertyNotify) + (xcb:+event exwm--connection 'xcb:CreateNotify #'exwm-input--on-CreateNotify) + (xcb:+event exwm--connection 'xcb:KeyPress #'exwm-input--on-KeyPress) + (xcb:+event exwm--connection 'xcb:ButtonPress #'exwm-input--on-ButtonPress) + (xcb:+event exwm--connection 'xcb:ButtonRelease + #'exwm-floating--stop-moveresize) + (xcb:+event exwm--connection 'xcb:MotionNotify + #'exwm-floating--do-moveresize) + (when mouse-autoselect-window + (xcb:+event exwm--connection 'xcb:EnterNotify + #'exwm-input--on-EnterNotify)) + ;; Control `exwm-input--during-command' + (add-hook 'pre-command-hook #'exwm-input--on-pre-command) + (add-hook 'post-command-hook #'exwm-input--on-post-command) + ;; Grab/Release keyboard when minibuffer/echo becomes active/inactive. + (add-hook 'minibuffer-setup-hook #'exwm-input--on-minibuffer-setup) + (add-hook 'minibuffer-exit-hook #'exwm-input--on-minibuffer-exit) + (setq exwm-input--echo-area-timer + (run-with-idle-timer 0 t #'exwm-input--on-echo-area-dirty)) + (add-hook 'echo-area-clear-hook #'exwm-input--on-echo-area-clear) + ;; Update focus when buffer list updates + (add-hook 'buffer-list-update-hook #'exwm-input--on-buffer-list-update) + + (dolist (fun exwm-input--passthrough-functions) + (advice-add fun :around #'exwm-input--call-with-passthrough))) + +(defun exwm-input--post-init () + "The second stage in the initialization of the input module." + (exwm--log) + (exwm-input--update-global-prefix-keys)) + +(defun exwm-input--exit () + "Exit the input module." + (exwm--log) + (dolist (fun exwm-input--passthrough-functions) + (advice-remove fun #'exwm-input--call-with-passthrough)) + (exwm-input--unset-simulation-keys) + (remove-hook 'pre-command-hook #'exwm-input--on-pre-command) + (remove-hook 'post-command-hook #'exwm-input--on-post-command) + (remove-hook 'minibuffer-setup-hook #'exwm-input--on-minibuffer-setup) + (remove-hook 'minibuffer-exit-hook #'exwm-input--on-minibuffer-exit) + (when exwm-input--echo-area-timer + (cancel-timer exwm-input--echo-area-timer) + (setq exwm-input--echo-area-timer nil)) + (remove-hook 'echo-area-clear-hook #'exwm-input--on-echo-area-clear) + (remove-hook 'buffer-list-update-hook #'exwm-input--on-buffer-list-update) + (when exwm-input--update-focus-timer + (cancel-timer exwm-input--update-focus-timer)) + ;; Make input focus working even without a WM. + (when (slot-value exwm--connection 'connected) + (xcb:+request exwm--connection + (make-instance 'xcb:SetInputFocus + :revert-to xcb:InputFocus:PointerRoot + :focus exwm--root + :time xcb:Time:CurrentTime)) + (xcb:flush exwm--connection))) + + + +(provide 'exwm-input) + +;;; exwm-input.el ends here diff --git a/exwm-layout.el b/exwm-layout.el new file mode 100644 index 0000000000..8649c11ffd --- /dev/null +++ b/exwm-layout.el @@ -0,0 +1,631 @@ +;;; exwm-layout.el --- Layout Module for EXWM -*- lexical-binding: t -*- + +;; Copyright (C) 2015-2024 Free Software Foundation, Inc. + +;; Author: Chris Feng <chris.w.feng@gmail.com> + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This module is responsible for keeping X client window properly displayed. + +;;; Code: + +(require 'exwm-core) + +(defgroup exwm-layout nil + "Layout." + :group 'exwm) + +(defcustom exwm-layout-auto-iconify t + "Non-nil to automatically iconify unused X windows when possible." + :type 'boolean) + +(defcustom exwm-layout-show-all-buffers nil + "Non-nil to allow switching to buffers on other workspaces." + :type 'boolean) + +(defconst exwm-layout--floating-hidden-position -101 + "Where to place hidden floating X windows.") + +(defvar exwm-layout--other-buffer-exclude-buffers nil + "List of buffers that should not be selected by `other-buffer'.") + +(defvar exwm-layout--other-buffer-exclude-exwm-mode-buffers nil + "When non-nil, prevent EXWM buffers from being selected by `other-buffer'.") + +(defvar exwm-layout--timer nil "Timer used to track echo area changes.") + +(defvar exwm-workspace--current) +(defvar exwm-workspace--frame-y-offset) +(declare-function exwm-input--release-keyboard "exwm-input.el") +(declare-function exwm-input--grab-keyboard "exwm-input.el") +(declare-function exwm-input-grab-keyboard "exwm-input.el") +(declare-function exwm-workspace--active-p "exwm-workspace.el" (frame)) +(declare-function exwm-workspace--get-geometry "exwm-workspace.el" (frame)) +(declare-function exwm-workspace--minibuffer-own-frame-p "exwm-workspace.el") +(declare-function exwm-workspace--workspace-p "exwm-workspace.el" + (workspace)) +(declare-function exwm-workspace-move-window "exwm-workspace.el" + (frame-or-index &optional id)) + +(defun exwm-layout--set-state (id state) + "Set WM_STATE of X window ID to STATE." + (exwm--log "id=#x%x" id) + (xcb:+request exwm--connection + (make-instance 'xcb:icccm:set-WM_STATE + :window id :state state :icon xcb:Window:None)) + (with-current-buffer (exwm--id->buffer id) + (setq exwm-state state))) + +(defun exwm-layout--iconic-state-p (&optional id) + "Check whether X window ID is in iconic state." + (= xcb:icccm:WM_STATE:IconicState + (if id + (buffer-local-value 'exwm-state (exwm--id->buffer id)) + exwm-state))) + +(defun exwm-layout--set-ewmh-state (id) + "Set _NET_WM_STATE of X window ID to the value of variable `exwm--ewmh-state'." + (with-current-buffer (exwm--id->buffer id) + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_WM_STATE + :window exwm--id + :data exwm--ewmh-state)))) + +(defun exwm-layout--fullscreen-p () + "Check whether current `exwm-mode' buffer is in fullscreen state." + (when (derived-mode-p 'exwm-mode) + (memq xcb:Atom:_NET_WM_STATE_FULLSCREEN exwm--ewmh-state))) + +(defun exwm-layout--auto-iconify () + "Helper function to iconify unused X windows. +See variable `exwm-layout-auto-iconify'." + (when (and exwm-layout-auto-iconify + (not exwm-transient-for)) + (let ((xwin exwm--id) + (state exwm-state)) + (dolist (pair exwm--id-buffer-alist) + (with-current-buffer (cdr pair) + (when (and exwm--floating-frame + (eq exwm-transient-for xwin) + (not (eq exwm-state state))) + (if (eq state xcb:icccm:WM_STATE:NormalState) + (exwm-layout--refresh-floating exwm--floating-frame) + (exwm-layout--hide exwm--id)))))))) + +(defun exwm-layout--show (id &optional window) + "Show window ID exactly fit in the Emacs window WINDOW." + (exwm--log "Show #x%x in %s" id window) + (let* ((edges (window-inside-absolute-pixel-edges window)) + (x (pop edges)) + (y (pop edges)) + (width (- (pop edges) x)) + (height (- (pop edges) y)) + frame-x frame-y frame-width frame-height) + (with-current-buffer (exwm--id->buffer id) + (when exwm--floating-frame + (setq frame-width (frame-pixel-width exwm--floating-frame) + frame-height (+ (frame-pixel-height exwm--floating-frame) + ;; Use `frame-outer-height' in the future. + exwm-workspace--frame-y-offset)) + (when exwm--floating-frame-position + (setq frame-x (elt exwm--floating-frame-position 0) + frame-y (elt exwm--floating-frame-position 1) + x (+ x frame-x (- exwm-layout--floating-hidden-position)) + y (+ y frame-y (- exwm-layout--floating-hidden-position))) + (setq exwm--floating-frame-position nil)) + (exwm--set-geometry (frame-parameter exwm--floating-frame + 'exwm-container) + frame-x frame-y frame-width frame-height)) + (when (exwm-layout--fullscreen-p) + (with-slots ((x* x) + (y* y) + (width* width) + (height* height)) + (exwm-workspace--get-geometry exwm--frame) + (setq x x* + y y* + width width* + height height*))) + (exwm--set-geometry id x y width height) + (xcb:+request exwm--connection (make-instance 'xcb:MapWindow :window id)) + (exwm-layout--set-state id xcb:icccm:WM_STATE:NormalState) + (setq exwm--ewmh-state + (delq xcb:Atom:_NET_WM_STATE_HIDDEN exwm--ewmh-state)) + (exwm-layout--set-ewmh-state id) + (exwm-layout--auto-iconify))) + (xcb:flush exwm--connection)) + +(defun exwm-layout--hide (id) + "Hide window ID." + (with-current-buffer (exwm--id->buffer id) + (unless (or (exwm-layout--iconic-state-p) + (and exwm--floating-frame + exwm--desktop + (= 4294967295. exwm--desktop))) + (exwm--log "Hide #x%x" id) + (when exwm--floating-frame + (let* ((container (frame-parameter exwm--floating-frame + 'exwm-container)) + (geometry (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetGeometry + :drawable container)))) + (setq exwm--floating-frame-position + (vector (slot-value geometry 'x) (slot-value geometry 'y))) + (exwm--set-geometry container exwm-layout--floating-hidden-position + exwm-layout--floating-hidden-position + 1 + 1))) + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window id :value-mask xcb:CW:EventMask + :event-mask xcb:EventMask:NoEvent)) + (xcb:+request exwm--connection + (make-instance 'xcb:UnmapWindow :window id)) + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window id :value-mask xcb:CW:EventMask + :event-mask (exwm--get-client-event-mask))) + (exwm-layout--set-state id xcb:icccm:WM_STATE:IconicState) + (cl-pushnew xcb:Atom:_NET_WM_STATE_HIDDEN exwm--ewmh-state) + (exwm-layout--set-ewmh-state id) + (exwm-layout--auto-iconify) + (xcb:flush exwm--connection)))) + +;;;###autoload +(cl-defun exwm-layout-set-fullscreen (&optional id) + "Make window ID fullscreen." + (interactive) + (exwm--log "id=#x%x" (or id 0)) + (unless (and (or id (derived-mode-p 'exwm-mode)) + (not (exwm-layout--fullscreen-p))) + (cl-return-from exwm-layout-set-fullscreen)) + (with-current-buffer (if id (exwm--id->buffer id) (window-buffer)) + ;; Expand the X window to fill the whole screen. + (with-slots (x y width height) (exwm-workspace--get-geometry exwm--frame) + (exwm--set-geometry exwm--id x y width height)) + ;; Raise the X window. + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window exwm--id + :value-mask (logior xcb:ConfigWindow:BorderWidth + xcb:ConfigWindow:StackMode) + :border-width 0 + :stack-mode xcb:StackMode:Above)) + (cl-pushnew xcb:Atom:_NET_WM_STATE_FULLSCREEN exwm--ewmh-state) + (exwm-layout--set-ewmh-state exwm--id) + (xcb:flush exwm--connection) + (set-window-dedicated-p (get-buffer-window) t) + (exwm-input--release-keyboard exwm--id))) + +;;;###autoload +(cl-defun exwm-layout-unset-fullscreen (&optional id) + "Restore X window ID from fullscreen state." + (interactive) + (exwm--log "id=#x%x" (or id 0)) + (unless (and (or id (derived-mode-p 'exwm-mode)) + (exwm-layout--fullscreen-p)) + (cl-return-from exwm-layout-unset-fullscreen)) + (with-current-buffer (if id (exwm--id->buffer id) (window-buffer)) + ;; `exwm-layout--show' relies on `exwm--ewmh-state' to decide whether to + ;; fullscreen the window. + (setq exwm--ewmh-state + (delq xcb:Atom:_NET_WM_STATE_FULLSCREEN exwm--ewmh-state)) + (exwm-layout--set-ewmh-state exwm--id) + (if exwm--floating-frame + (exwm-layout--show exwm--id (frame-root-window exwm--floating-frame)) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window exwm--id + :value-mask (logior xcb:ConfigWindow:Sibling + xcb:ConfigWindow:StackMode) + :sibling exwm--guide-window + :stack-mode xcb:StackMode:Above)) + (let ((window (get-buffer-window nil t))) + (when window + (exwm-layout--show exwm--id window)))) + (xcb:flush exwm--connection) + (set-window-dedicated-p (get-buffer-window) nil) + (when (eq 'line-mode exwm--selected-input-mode) + (exwm-input--grab-keyboard exwm--id)))) + +;;;###autoload +(cl-defun exwm-layout-toggle-fullscreen (&optional id) + "Toggle fullscreen mode of X window ID." + (interactive (list (exwm--buffer->id (window-buffer)))) + (exwm--log "id=#x%x" (or id 0)) + (unless (or id (derived-mode-p 'exwm-mode)) + (cl-return-from exwm-layout-toggle-fullscreen)) + (when id + (with-current-buffer (exwm--id->buffer id) + (if (exwm-layout--fullscreen-p) + (exwm-layout-unset-fullscreen id) + (exwm-layout-set-fullscreen id))))) + +(defun exwm-layout--other-buffer-predicate (buffer) + "Return non-nil when the BUFFER may be displayed in selected frame. + +Prevents EXWM-mode buffers already being displayed on some other window from +being selected. + +Should be set as `buffer-predicate' frame parameter for all +frames. Used by `other-buffer'. + +When variable `exwm-layout--other-buffer-exclude-exwm-mode-buffers' +is t EXWM buffers are never selected by `other-buffer'. + +When variable `exwm-layout--other-buffer-exclude-buffers' is a +list of buffers, EXWM buffers belonging to that list are never +selected by `other-buffer'." + (or (not (with-current-buffer buffer (derived-mode-p 'exwm-mode))) + (and (not exwm-layout--other-buffer-exclude-exwm-mode-buffers) + (not (memq buffer exwm-layout--other-buffer-exclude-buffers)) + ;; Do not select if already shown in some window. + (not (get-buffer-window buffer t))))) + +(defun exwm-layout--set-client-list-stacking () + "Set _NET_CLIENT_LIST_STACKING." + (exwm--log) + (let (id clients-floating clients clients-iconic clients-other) + (dolist (pair exwm--id-buffer-alist) + (setq id (car pair)) + (with-current-buffer (cdr pair) + (if (eq exwm--frame exwm-workspace--current) + (if exwm--floating-frame + ;; A floating X window on the current workspace. + (setq clients-floating (cons id clients-floating)) + (if (get-buffer-window (cdr pair) exwm-workspace--current) + ;; A normal tilling X window on the current workspace. + (setq clients (cons id clients)) + ;; An iconic tilling X window on the current workspace. + (setq clients-iconic (cons id clients-iconic)))) + ;; X window on other workspaces. + (setq clients-other (cons id clients-other))))) + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_CLIENT_LIST_STACKING + :window exwm--root + :data (vconcat (append clients-other clients-iconic + clients clients-floating)))))) + +(defun exwm-layout--refresh (&optional frame) + "Refresh layout of FRAME. +If FRAME is nil, refresh layout of selected frame." + ;; `window-size-change-functions' sets this argument while + ;; `window-configuration-change-hook' makes the frame selected. + (unless frame + (setq frame (selected-frame))) + (exwm--log "frame=%s" frame) + (if (not (exwm-workspace--workspace-p frame)) + (if (frame-parameter frame 'exwm-outer-id) + (exwm-layout--refresh-floating frame) + (exwm-layout--refresh-other frame)) + (exwm-layout--refresh-workspace frame))) + +(defun exwm-layout--refresh-floating (frame) + "Refresh floating frame FRAME." + (exwm--log "Refresh floating %s" frame) + (let ((window (frame-first-window frame))) + (with-current-buffer (window-buffer window) + (when (and (derived-mode-p 'exwm-mode) + ;; It may be a buffer waiting to be killed. + (exwm--id->buffer exwm--id)) + (exwm--log "Refresh floating window #x%x" exwm--id) + (if (exwm-workspace--active-p exwm--frame) + (exwm-layout--show exwm--id window) + (exwm-layout--hide exwm--id)))))) + +(defun exwm-layout--refresh-other (frame) + "Refresh client or nox frame FRAME." + ;; Other frames (e.g. terminal/graphical frame of emacsclient) + ;; We shall bury all `exwm-mode' buffers in this case + (exwm--log "Refresh other %s" frame) + (let ((windows (window-list frame 'nomini)) ;exclude minibuffer + (exwm-layout--other-buffer-exclude-exwm-mode-buffers t)) + (dolist (window windows) + (with-current-buffer (window-buffer window) + (when (derived-mode-p 'exwm-mode) + (if (window-prev-buffers window) + (switch-to-prev-buffer window) + (switch-to-next-buffer window))))))) + +(defun exwm-layout--refresh-workspace (frame) + "Refresh workspace frame FRAME." + (exwm--log "Refresh workspace %s" frame) + ;; Workspaces other than the active one can also be refreshed (RandR) + (let (covered-buffers ;EXWM-buffers covered by a new X window. + vacated-windows) ;Windows previously displaying EXWM-buffers. + (dolist (pair exwm--id-buffer-alist) + (with-current-buffer (cdr pair) + (when (and (not exwm--floating-frame) ;exclude floating X windows + (or exwm-layout-show-all-buffers + ;; Exclude X windows on other workspaces + (eq frame exwm--frame))) + (let (;; List of windows in current frame displaying the `exwm-mode' + ;; buffers. + (windows (get-buffer-window-list (current-buffer) 'nomini + frame))) + (if (not windows) + (when (eq frame exwm--frame) + ;; Hide it if it was being shown in this workspace. + (exwm-layout--hide exwm--id)) + (let ((window (car windows))) + (if (eq frame exwm--frame) + ;; Show it if `frame' is active, hide otherwise. + (if (exwm-workspace--active-p frame) + (exwm-layout--show exwm--id window) + (exwm-layout--hide exwm--id)) + ;; It was last shown in other workspace; move it here. + (exwm-workspace-move-window frame exwm--id)) + ;; Vacate any other windows (in any workspace) showing this + ;; `exwm-mode' buffer. + (setq vacated-windows + (append vacated-windows (remove + window + (get-buffer-window-list + (current-buffer) 'nomini t)))) + ;; Note any `exwm-mode' buffer is being covered by another + ;; `exwm-mode' buffer. We want to avoid that `exwm-mode' + ;; buffer to be reappear in any of the vacated windows. + (let ((prev-buffer (car-safe + (car-safe (window-prev-buffers window))))) + (and + prev-buffer + (with-current-buffer prev-buffer + (derived-mode-p 'exwm-mode)) + (push prev-buffer covered-buffers))))))))) + ;; Set some sensible buffer to vacated windows. + (let ((exwm-layout--other-buffer-exclude-buffers covered-buffers)) + (dolist (window vacated-windows) + (if (window-prev-buffers window) + (switch-to-prev-buffer window) + (switch-to-next-buffer window)))) + ;; Make sure windows floating / on other workspaces are excluded + (let ((exwm-layout--other-buffer-exclude-exwm-mode-buffers t)) + (dolist (window (window-list frame 'nomini)) + (with-current-buffer (window-buffer window) + (when (and (derived-mode-p 'exwm-mode) + (or exwm--floating-frame (not (eq frame exwm--frame)))) + (if (window-prev-buffers window) + (switch-to-prev-buffer window) + (switch-to-next-buffer window)))))) + (exwm-layout--set-client-list-stacking) + (xcb:flush exwm--connection))) + +(defun exwm-layout--on-minibuffer-setup () + "Refresh layout when minibuffer grows." + (exwm--log) + ;; Only when active minibuffer's frame is an EXWM frame. + (let* ((mini-window (active-minibuffer-window)) + (frame (window-frame mini-window))) + (when (exwm-workspace--workspace-p frame) + (exwm--defer 0 (lambda () + (when (< 1 (window-height mini-window)) + (exwm-layout--refresh frame))))))) + +(defun exwm-layout--on-echo-area-change (&optional dirty) + "Run when message arrives or in `echo-area-clear-hook' to refresh layout. +If DIRTY is non-nil, refresh layout immediately." + (let ((frame (window-frame (active-minibuffer-window))) + (msg (current-message))) + ;; Check whether the frame where current window's minibuffer resides (not + ;; current window's frame for floating windows!) must be adjusted. + (when (and msg + (exwm-workspace--workspace-p frame) + (or (cl-position ?\n msg) + (> (length msg) (frame-width frame)))) + (exwm--log) + (if dirty + (exwm-layout--refresh exwm-workspace--current) + (exwm--defer 0 #'exwm-layout--refresh exwm-workspace--current))))) + +;;;###autoload +(defun exwm-layout-enlarge-window (delta &optional horizontal) + "Make the selected window DELTA pixels taller. + +If no argument is given, make the selected window one pixel taller. If the +optional argument HORIZONTAL is non-nil, make selected window DELTA pixels +wider. If DELTA is negative, shrink selected window by -DELTA pixels. + +Normal hints are checked and regarded if the selected window is displaying an +`exwm-mode' buffer. However, this may violate the normal hints set on other X +windows." + (interactive "p") + (exwm--log) + (cond + ((zerop delta)) ;no operation + ((window-minibuffer-p)) ;avoid resize minibuffer-window + ((not (and (derived-mode-p 'exwm-mode) exwm--floating-frame)) + ;; Resize on tiling layout + (unless (= 0 (window-resizable nil delta horizontal nil t)) ;not resizable + (let ((window-resize-pixelwise t)) + (window-resize nil delta horizontal nil t)))) + ;; Resize on floating layout + (exwm--fixed-size) ;fixed size + (horizontal + (let* ((width (frame-pixel-width)) + (edges (window-inside-pixel-edges)) + (inner-width (- (elt edges 2) (elt edges 0))) + (margin (- width inner-width))) + (if (> delta 0) + (if (not exwm--normal-hints-max-width) + (cl-incf width delta) + (if (>= inner-width exwm--normal-hints-max-width) + (setq width nil) + (setq width (min (+ exwm--normal-hints-max-width margin) + (+ width delta))))) + (if (not exwm--normal-hints-min-width) + (cl-incf width delta) + (if (<= inner-width exwm--normal-hints-min-width) + (setq width nil) + (setq width (max (+ exwm--normal-hints-min-width margin) + (+ width delta)))))) + (when (and width (> width 0)) + (setf (slot-value exwm--geometry 'width) width) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window (frame-parameter exwm--floating-frame + 'exwm-outer-id) + :value-mask xcb:ConfigWindow:Width + :width width)) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window (frame-parameter exwm--floating-frame + 'exwm-container) + :value-mask xcb:ConfigWindow:Width + :width width)) + (xcb:flush exwm--connection)))) + (t + (let* ((height (+ (frame-pixel-height) exwm-workspace--frame-y-offset)) + (edges (window-inside-pixel-edges)) + (inner-height (- (elt edges 3) (elt edges 1))) + (margin (- height inner-height))) + (if (> delta 0) + (if (not exwm--normal-hints-max-height) + (cl-incf height delta) + (if (>= inner-height exwm--normal-hints-max-height) + (setq height nil) + (setq height (min (+ exwm--normal-hints-max-height margin) + (+ height delta))))) + (if (not exwm--normal-hints-min-height) + (cl-incf height delta) + (if (<= inner-height exwm--normal-hints-min-height) + (setq height nil) + (setq height (max (+ exwm--normal-hints-min-height margin) + (+ height delta)))))) + (when (and height (> height 0)) + (setf (slot-value exwm--geometry 'height) height) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window (frame-parameter exwm--floating-frame + 'exwm-outer-id) + :value-mask xcb:ConfigWindow:Height + :height height)) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window (frame-parameter exwm--floating-frame + 'exwm-container) + :value-mask xcb:ConfigWindow:Height + :height height)) + (xcb:flush exwm--connection)))))) + +;;;###autoload +(defun exwm-layout-enlarge-window-horizontally (delta) + "Make the selected window DELTA pixels wider. + +See also `exwm-layout-enlarge-window'." + (interactive "p") + (exwm--log "%s" delta) + (exwm-layout-enlarge-window delta t)) + +;;;###autoload +(defun exwm-layout-shrink-window (delta) + "Make the selected window DELTA pixels lower. + +See also `exwm-layout-enlarge-window'." + (interactive "p") + (exwm--log "%s" delta) + (exwm-layout-enlarge-window (- delta))) + +;;;###autoload +(defun exwm-layout-shrink-window-horizontally (delta) + "Make the selected window DELTA pixels narrower. + +See also `exwm-layout-enlarge-window'." + (interactive "p") + (exwm--log "%s" delta) + (exwm-layout-enlarge-window (- delta) t)) + +;;;###autoload +(defun exwm-layout-hide-mode-line () + "Hide mode-line." + (interactive) + (exwm--log) + (when (and (derived-mode-p 'exwm-mode) mode-line-format) + (let (mode-line-height) + (when exwm--floating-frame + (setq mode-line-height (window-mode-line-height + (frame-root-window exwm--floating-frame)))) + (setq exwm--mode-line-format mode-line-format + mode-line-format nil) + (if (not exwm--floating-frame) + (exwm-layout--show exwm--id) + (set-frame-height exwm--floating-frame + (- (frame-pixel-height exwm--floating-frame) + mode-line-height) + nil t))))) + +;;;###autoload +(defun exwm-layout-show-mode-line () + "Show mode-line." + (interactive) + (exwm--log) + (when (and (derived-mode-p 'exwm-mode) (not mode-line-format)) + (setq mode-line-format exwm--mode-line-format + exwm--mode-line-format nil) + (if (not exwm--floating-frame) + (exwm-layout--show exwm--id) + (set-frame-height exwm--floating-frame + (+ (frame-pixel-height exwm--floating-frame) + (window-mode-line-height (frame-root-window + exwm--floating-frame))) + nil t) + (call-interactively #'exwm-input-grab-keyboard)) + (force-mode-line-update))) + +;;;###autoload +(defun exwm-layout-toggle-mode-line () + "Toggle the display of mode-line." + (interactive) + (exwm--log) + (when (derived-mode-p 'exwm-mode) + (if mode-line-format + (exwm-layout-hide-mode-line) + (exwm-layout-show-mode-line)))) + +(defun exwm-layout--init () + "Initialize layout module." + ;; Auto refresh layout + (exwm--log) + (add-hook 'window-configuration-change-hook #'exwm-layout--refresh) + ;; The behavior of `window-configuration-change-hook' will be changed. + (when (fboundp 'window-pixel-width-before-size-change) + (add-hook 'window-size-change-functions #'exwm-layout--refresh)) + (unless (exwm-workspace--minibuffer-own-frame-p) + ;; Refresh when minibuffer grows + (add-hook 'minibuffer-setup-hook #'exwm-layout--on-minibuffer-setup t) + (setq exwm-layout--timer + (run-with-idle-timer 0 t #'exwm-layout--on-echo-area-change t)) + (add-hook 'echo-area-clear-hook #'exwm-layout--on-echo-area-change))) + +(defun exwm-layout--exit () + "Exit the layout module." + (exwm--log) + (remove-hook 'window-configuration-change-hook #'exwm-layout--refresh) + (when (fboundp 'window-pixel-width-before-size-change) + (remove-hook 'window-size-change-functions #'exwm-layout--refresh)) + (remove-hook 'minibuffer-setup-hook #'exwm-layout--on-minibuffer-setup) + (when exwm-layout--timer + (cancel-timer exwm-layout--timer) + (setq exwm-layout--timer nil)) + (remove-hook 'echo-area-clear-hook #'exwm-layout--on-echo-area-change)) + + + +(provide 'exwm-layout) + +;;; exwm-layout.el ends here diff --git a/exwm-manage.el b/exwm-manage.el new file mode 100644 index 0000000000..ab66e298ac --- /dev/null +++ b/exwm-manage.el @@ -0,0 +1,833 @@ +;;; exwm-manage.el --- Window Management Module for -*- lexical-binding: t -*- +;;; EXWM + +;; Copyright (C) 2015-2024 Free Software Foundation, Inc. + +;; Author: Chris Feng <chris.w.feng@gmail.com> + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This is the fundamental module of EXWM that deals with window management. + +;;; Code: + +(require 'exwm-core) + +(defgroup exwm-manage nil + "Manage." + :group 'exwm) + +(defcustom exwm-manage-finish-hook nil + "Normal hook run after a window is just managed. +This hook runs in the context of the corresponding `exwm-mode' buffer." + :type 'hook) + +(defcustom exwm-manage-force-tiling nil + "Non-nil to force managing all X windows in tiling layout. +You can still make the X windows floating afterwards." + :type 'boolean) + +(defcustom exwm-manage-ping-timeout 3 + "Seconds to wait before killing a client." + :type 'integer) + +(defcustom exwm-manage-configurations nil + "Per-application configurations. + +Configuration options allow to override various default behaviors of EXWM +and only take effect when they are present. Note for certain options +specifying nil is not exactly the same as leaving them out. Currently +possible choices: +* floating: Force floating (non-nil) or tiling (nil) on startup. +* x/y/width/height: Override the initial geometry (floating X window only). +* border-width: Override the border width (only visible when floating). +* fullscreen: Force full screen (non-nil) on startup. +* floating-mode-line: `mode-line-format' used when floating. +* tiling-mode-line: `mode-line-format' used when tiling. +* floating-header-line: `header-line-format' used when floating. +* tiling-header-line: `header-line-format' used when tiling. +* char-mode: Force char-mode (non-nil) on startup. +* prefix-keys: `exwm-input-prefix-keys' local to this X window. +* simulation-keys: `exwm-input-simulation-keys' local to this X window. +* workspace: The initial workspace. +* managed: Force to manage (non-nil) or not manage (nil) the X window. + +For each X window managed for the first time, matching criteria (sexps) are +evaluated sequentially and the first configuration with a non-nil matching +criterion would be applied. Apart from generic forms, one would typically +want to match against EXWM internal variables such as `exwm-title', +`exwm-class-name' and `exwm-instance-name'." + :type '(alist :key-type (sexp :tag "Matching criterion" nil) + :value-type + (plist :tag "Configurations" + :options + (((const :tag "Floating" floating) boolean) + ((const :tag "X" x) number) + ((const :tag "Y" y) number) + ((const :tag "Width" width) number) + ((const :tag "Height" height) number) + ((const :tag "Border width" border-width) integer) + ((const :tag "Fullscreen" fullscreen) boolean) + ((const :tag "Floating mode-line" floating-mode-line) + sexp) + ((const :tag "Tiling mode-line" tiling-mode-line) sexp) + ((const :tag "Floating header-line" + floating-header-line) + sexp) + ((const :tag "Tiling header-line" tiling-header-line) + sexp) + ((const :tag "Char-mode" char-mode) boolean) + ((const :tag "Prefix keys" prefix-keys) + (repeat key-sequence)) + ((const :tag "Simulation keys" simulation-keys) + (alist :key-type (key-sequence :tag "From") + :value-type (key-sequence :tag "To"))) + ((const :tag "Workspace" workspace) integer) + ((const :tag "Managed" managed) boolean) + ;; For forward compatibility. + ((other) sexp)))) + ;; TODO: This is admittedly ugly. We'd be better off with an event type. + :get (lambda (symbol) + (mapcar (lambda (pair) + (let* ((match (car pair)) + (config (cdr pair)) + (prefix-keys (plist-get config 'prefix-keys))) + (when prefix-keys + (setq config (copy-tree config) + config (plist-put config 'prefix-keys + (mapcar (lambda (i) + (if (sequencep i) + i + (vector i))) + prefix-keys)))) + (cons match config))) + (default-value symbol))) + :set (lambda (symbol value) + (set symbol + (mapcar (lambda (pair) + (let* ((match (car pair)) + (config (cdr pair)) + (prefix-keys (plist-get config 'prefix-keys))) + (when prefix-keys + (setq config (copy-tree config) + config (plist-put config 'prefix-keys + (mapcar (lambda (i) + (if (sequencep i) + (aref i 0) + i)) + prefix-keys)))) + (cons match config))) + value)))) + +;; FIXME: Make the following values as small as possible. +(defconst exwm-manage--height-delta-min 5) +(defconst exwm-manage--width-delta-min 5) + +;; The _MOTIF_WM_HINTS atom (see <Xm/MwmUtil.h> for more details) +;; It's currently only used in 'exwm-manage' module +(defvar exwm-manage--_MOTIF_WM_HINTS nil "_MOTIF_WM_HINTS atom.") + +(defvar exwm-manage--desktop nil "The desktop X window.") + +(defvar exwm-manage--frame-outer-id-list nil + "List of window-outer-id's of all frames.") + +(defvar exwm-manage--ping-lock nil + "Non-nil indicates EXWM is pinging a window.") + +(defvar exwm-input--skip-buffer-list-update) +(defvar exwm-input-prefix-keys) +(defvar exwm-workspace--current) +(defvar exwm-workspace--id-struts-alist) +(defvar exwm-workspace--list) +(defvar exwm-workspace--switch-history-outdated) +(defvar exwm-workspace-current-index) +(declare-function exwm--update-class "exwm.el" (id &optional force)) +(declare-function exwm--update-hints "exwm.el" (id &optional force)) +(declare-function exwm--update-normal-hints "exwm.el" (id &optional force)) +(declare-function exwm--update-protocols "exwm.el" (id &optional force)) +(declare-function exwm--update-struts "exwm.el" (id)) +(declare-function exwm--update-title "exwm.el" (id)) +(declare-function exwm--update-transient-for "exwm.el" (id &optional force)) +(declare-function exwm--update-desktop "exwm.el" (id &optional force)) +(declare-function exwm--update-window-type "exwm.el" (id &optional force)) +(declare-function exwm-floating--set-floating "exwm-floating.el" (id)) +(declare-function exwm-floating--unset-floating "exwm-floating.el" (id)) +(declare-function exwm-input-grab-keyboard "exwm-input.el" (&optional id)) +(declare-function exwm-input-release-keyboard "exwm-input.el" (&optional id)) +(declare-function exwm-input-set-local-simulation-keys "exwm-input.el") +(declare-function exwm-layout--fullscreen-p "exwm-layout.el" ()) +(declare-function exwm-layout--iconic-state-p "exwm-layout.el" (&optional id)) +(declare-function exwm-layout-set-fullscreen "exwm-layout.el" (&optional id)) +(declare-function exwm-workspace--get-geometry "exwm-workspace.el" (frame)) +(declare-function exwm-workspace--position "exwm-workspace.el" (frame)) +(declare-function exwm-workspace--set-fullscreen "exwm-workspace.el" (frame)) +(declare-function exwm-workspace--update-struts "exwm-workspace.el" ()) +(declare-function exwm-workspace--update-workareas "exwm-workspace.el" ()) +(declare-function exwm-workspace--workarea "exwm-workspace.el" (frame)) + +(defun exwm-manage--update-geometry (id &optional force) + "Update geometry of X window ID. +Override current geometry if FORCE is non-nil." + (exwm--log "id=#x%x" id) + (with-current-buffer (exwm--id->buffer id) + (unless (and exwm--geometry (not force)) + (let ((reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetGeometry :drawable id)))) + (setq exwm--geometry + (or reply + ;; Provide a reasonable fallback value. + (make-instance 'xcb:RECTANGLE + :x 0 + :y 0 + :width (/ (x-display-pixel-width) 2) + :height (/ (x-display-pixel-height) 2)))))))) + +(defun exwm-manage--update-ewmh-state (id) + "Update _NET_WM_STATE of X window ID." + (exwm--log "id=#x%x" id) + (with-current-buffer (exwm--id->buffer id) + (unless exwm--ewmh-state + (let ((reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:ewmh:get-_NET_WM_STATE + :window id)))) + (when reply + (setq exwm--ewmh-state (append (slot-value reply 'value) nil))))))) + +(defun exwm-manage--update-mwm-hints (id &optional force) + "Update _MOTIF_WM_HINTS of X window ID. +Override current hinds if FORCE is non-nil." + (exwm--log "id=#x%x" id) + (with-current-buffer (exwm--id->buffer id) + (unless (and (not exwm--mwm-hints-decorations) (not force)) + (let ((reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:icccm:-GetProperty + :window id + :property exwm-manage--_MOTIF_WM_HINTS + :type exwm-manage--_MOTIF_WM_HINTS + :long-length 5)))) + (when reply + ;; Check MotifWmHints.decorations. + (with-slots (value) reply + (setq value (append value nil)) + (when (and value + ;; See <Xm/MwmUtil.h> for fields definitions. + (/= 0 (logand + (elt value 0) ;MotifWmHints.flags + 2)) ;MWM_HINTS_DECORATIONS + (= 0 + (elt value 2))) ;MotifWmHints.decorations + (setq exwm--mwm-hints-decorations nil)))))))) + +(defun exwm-manage--update-default-directory (id) + "Update the `default-directory' of X window ID. +Sets the `default-directory' of the EXWM buffer associated with X window to +match its current working directory. + +This only works when procfs is mounted, which may not be the case on some BSDs." + (with-current-buffer (exwm--id->buffer id) + (if-let* ((response (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:ewmh:get-_NET_WM_PID + :window id))) + (pid (slot-value response 'value)) + (cwd (file-symlink-p (format "/proc/%d/cwd" pid))) + ((file-accessible-directory-p cwd))) + (setq default-directory (file-name-as-directory cwd)) + (setq default-directory (expand-file-name "~/"))))) + + +(defun exwm-manage--set-client-list () + "Set _NET_CLIENT_LIST." + (exwm--log) + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_CLIENT_LIST + :window exwm--root + :data (vconcat (mapcar #'car exwm--id-buffer-alist))))) + +(cl-defun exwm-manage--get-configurations () + "Retrieve configurations for this buffer." + (exwm--log) + (when (derived-mode-p 'exwm-mode) + (dolist (i exwm-manage-configurations) + (save-current-buffer + (when (with-demoted-errors "Problematic configuration: %S" + (eval (car i) t)) + (cl-return-from exwm-manage--get-configurations (cdr i))))))) + +(defun exwm-manage--manage-window (id) + "Manage window ID." + (exwm--log "Try to manage #x%x" id) + (catch 'return + ;; Ensure it's alive + (when (xcb:+request-checked+request-check exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window id :value-mask xcb:CW:EventMask + :event-mask (exwm--get-client-event-mask))) + (throw 'return 'dead)) + ;; Add this X window to save-set. + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeSaveSet + :mode xcb:SetMode:Insert + :window id)) + (with-current-buffer (let ((exwm-input--skip-buffer-list-update t)) + (generate-new-buffer "*EXWM*")) + ;; Keep the oldest X window first. + (setq exwm--id-buffer-alist + (nconc exwm--id-buffer-alist `((,id . ,(current-buffer))))) + (exwm-mode) + (setq exwm--id id + exwm--frame exwm-workspace--current) + (exwm--update-window-type id) + (exwm--update-class id) + (exwm--update-transient-for id) + (exwm--update-normal-hints id) + (exwm--update-hints id) + (exwm-manage--update-geometry id) + (exwm-manage--update-mwm-hints id) + (exwm--update-title id) + (exwm--update-protocols id) + (setq exwm--configurations (exwm-manage--get-configurations)) + ;; OverrideRedirect is not checked here. + (when (and + ;; The user has specified to manage it. + (not (plist-get exwm--configurations 'managed)) + (or + ;; The user has specified not to manage it. + (plist-member exwm--configurations 'managed) + ;; This is not a type of X window we can manage. + (and exwm-window-type + (not (cl-intersection + exwm-window-type + (list xcb:Atom:_NET_WM_WINDOW_TYPE_UTILITY + xcb:Atom:_NET_WM_WINDOW_TYPE_DIALOG + xcb:Atom:_NET_WM_WINDOW_TYPE_NORMAL)))) + ;; Check the _MOTIF_WM_HINTS property to not manage floating X + ;; windows without decoration. + (and (not exwm--mwm-hints-decorations) + (not exwm--hints-input) + ;; Floating windows only + (or exwm-transient-for exwm--fixed-size + (memq xcb:Atom:_NET_WM_WINDOW_TYPE_UTILITY + exwm-window-type) + (memq xcb:Atom:_NET_WM_WINDOW_TYPE_DIALOG + exwm-window-type))))) + (exwm--log "No need to manage #x%x" id) + ;; Update struts. + (when (memq xcb:Atom:_NET_WM_WINDOW_TYPE_DOCK exwm-window-type) + (exwm--update-struts id)) + ;; Remove all events + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window id :value-mask xcb:CW:EventMask + :event-mask + (if (memq xcb:Atom:_NET_WM_WINDOW_TYPE_DOCK + exwm-window-type) + ;; Listen for PropertyChange (struts) and + ;; UnmapNotify/DestroyNotify event of the dock. + (exwm--get-client-event-mask) + xcb:EventMask:NoEvent))) + ;; The window needs to be mapped + (xcb:+request exwm--connection + (make-instance 'xcb:MapWindow :window id)) + (with-slots (x y width height) exwm--geometry + ;; Center window of type _NET_WM_WINDOW_TYPE_SPLASH + (when (memq xcb:Atom:_NET_WM_WINDOW_TYPE_SPLASH exwm-window-type) + (with-slots ((x* x) (y* y) (width* width) (height* height)) + (exwm-workspace--workarea exwm--frame) + (exwm--set-geometry id + (+ x* (/ (- width* width) 2)) + (+ y* (/ (- height* height) 2)) + nil + nil)))) + ;; Check for desktop. + (when (memq xcb:Atom:_NET_WM_WINDOW_TYPE_DESKTOP exwm-window-type) + ;; There should be only one desktop X window. + (setq exwm-manage--desktop id) + ;; Put it at bottom. + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window id + :value-mask xcb:ConfigWindow:StackMode + :stack-mode xcb:StackMode:Below))) + (xcb:flush exwm--connection) + (setq exwm--id-buffer-alist (assq-delete-all id exwm--id-buffer-alist)) + (let ((kill-buffer-query-functions nil) + (exwm-input--skip-buffer-list-update t)) + (kill-buffer (current-buffer))) + (throw 'return 'ignored)) + (let ((index (plist-get exwm--configurations 'workspace))) + (when (and index (< index (length exwm-workspace--list))) + (setq exwm--frame (elt exwm-workspace--list index)))) + ;; Manage the window + (exwm--log "Manage #x%x" id) + (xcb:+request exwm--connection ;remove border + (make-instance 'xcb:ConfigureWindow + :window id :value-mask xcb:ConfigWindow:BorderWidth + :border-width 0)) + (dolist (button ;grab buttons to set focus / move / resize + (list xcb:ButtonIndex:1 xcb:ButtonIndex:2 xcb:ButtonIndex:3)) + (xcb:+request exwm--connection + (make-instance 'xcb:GrabButton + :owner-events 0 :grab-window id + :event-mask xcb:EventMask:ButtonPress + :pointer-mode xcb:GrabMode:Sync + :keyboard-mode xcb:GrabMode:Async + :confine-to xcb:Window:None :cursor xcb:Cursor:None + :button button :modifiers xcb:ModMask:Any))) + (exwm-manage--set-client-list) + (xcb:flush exwm--connection) + (if (plist-member exwm--configurations 'floating) + ;; User has specified whether it should be floating. + (if (plist-get exwm--configurations 'floating) + (exwm-floating--set-floating id) + (with-selected-window (frame-selected-window exwm--frame) + (exwm-floating--unset-floating id))) + ;; Try to determine if it should be floating. + (if (and (not exwm-manage-force-tiling) + (or exwm-transient-for exwm--fixed-size + (memq xcb:Atom:_NET_WM_WINDOW_TYPE_UTILITY + exwm-window-type) + (memq xcb:Atom:_NET_WM_WINDOW_TYPE_DIALOG + exwm-window-type))) + (exwm-floating--set-floating id) + (with-selected-window (frame-selected-window exwm--frame) + (exwm-floating--unset-floating id)))) + (if (plist-get exwm--configurations 'char-mode) + (exwm-input-release-keyboard id) + (exwm-input-grab-keyboard id)) + (when-let ((simulation-keys (plist-get exwm--configurations 'simulation-keys))) + (exwm-input-set-local-simulation-keys simulation-keys)) + (when-let ((prefix-keys (plist-get exwm--configurations 'prefix-keys))) + (setq-local exwm-input-prefix-keys prefix-keys)) + (setq exwm-workspace--switch-history-outdated t) + (exwm--update-desktop id) + (exwm-manage--update-ewmh-state id) + (exwm-manage--update-default-directory id) + (when (or (plist-get exwm--configurations 'fullscreen) + (exwm-layout--fullscreen-p)) + (setq exwm--ewmh-state (delq xcb:Atom:_NET_WM_STATE_FULLSCREEN + exwm--ewmh-state)) + (exwm-layout-set-fullscreen id)) + (run-hooks 'exwm-manage-finish-hook)))) + +(defun exwm-manage--unmanage-window (id &optional withdraw-only) + "Unmanage window ID. + +If WITHDRAW-ONLY is non-nil, the X window will be properly placed back to the +root window. Set WITHDRAW-ONLY to `quit' if this functions is used when window +manager is shutting down." + (let ((buffer (exwm--id->buffer id))) + (exwm--log "Unmanage #x%x (buffer: %s, widthdraw: %s)" + id buffer withdraw-only) + (setq exwm--id-buffer-alist (assq-delete-all id exwm--id-buffer-alist)) + ;; Update workspaces when a dock is destroyed. + (when (and (null withdraw-only) + (assq id exwm-workspace--id-struts-alist)) + (setq exwm-workspace--id-struts-alist + (assq-delete-all id exwm-workspace--id-struts-alist)) + (exwm-workspace--update-struts) + (exwm-workspace--update-workareas) + (dolist (f exwm-workspace--list) + (exwm-workspace--set-fullscreen f))) + (when (and (buffer-live-p buffer) + ;; Invoked from `exwm-manage--exit' upon disconnection. + (slot-value exwm--connection 'connected)) + (with-current-buffer buffer + ;; Unmap the X window. + (xcb:+request exwm--connection + (make-instance 'xcb:UnmapWindow :window id)) + ;; + (setq exwm-workspace--switch-history-outdated t) + ;; + (when withdraw-only + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window id :value-mask xcb:CW:EventMask + :event-mask xcb:EventMask:NoEvent)) + ;; Delete WM_STATE property + (xcb:+request exwm--connection + (make-instance 'xcb:DeleteProperty + :window id :property xcb:Atom:WM_STATE)) + (cond + ((eq withdraw-only 'quit) + ;; Remap the window when exiting. + (xcb:+request exwm--connection + (make-instance 'xcb:MapWindow :window id))) + (t + ;; Remove _NET_WM_DESKTOP. + (xcb:+request exwm--connection + (make-instance 'xcb:DeleteProperty + :window id + :property xcb:Atom:_NET_WM_DESKTOP))))) + (when exwm--floating-frame + ;; Unmap the floating frame before destroying its container. + (let ((window (frame-parameter exwm--floating-frame 'exwm-outer-id)) + (container (frame-parameter exwm--floating-frame + 'exwm-container))) + (xcb:+request exwm--connection + (make-instance 'xcb:UnmapWindow :window window)) + (xcb:+request exwm--connection + (make-instance 'xcb:ReparentWindow + :window window :parent exwm--root :x 0 :y 0)) + (xcb:+request exwm--connection + (make-instance 'xcb:DestroyWindow :window container)))) + (when (exwm-layout--fullscreen-p) + (let ((window (get-buffer-window))) + (when window + (set-window-dedicated-p window nil)))) + (exwm-manage--set-client-list) + (xcb:flush exwm--connection)) + (let ((kill-buffer-func + (lambda (buffer) + (when (buffer-local-value 'exwm--floating-frame buffer) + (select-window + (frame-selected-window exwm-workspace--current))) + (with-current-buffer buffer + (let ((kill-buffer-query-functions nil)) + (kill-buffer buffer)))))) + (exwm--defer 0 kill-buffer-func buffer) + (when (active-minibuffer-window) + (exit-minibuffer)))))) + +(defun exwm-manage--scan () + "Search for existing windows and try to manage them." + (exwm--log) + (let* ((tree (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:QueryTree + :window exwm--root))) + reply) + (dolist (i (slot-value tree 'children)) + (setq reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetWindowAttributes + :window i))) + ;; It's possible the X window has been destroyed. + (when reply + (with-slots (override-redirect map-state) reply + (when (and (= 0 override-redirect) + (= xcb:MapState:Viewable map-state)) + (xcb:+request exwm--connection + (make-instance 'xcb:UnmapWindow + :window i)) + (xcb:flush exwm--connection) + (exwm-manage--manage-window i))))))) + +(defun exwm-manage--kill-buffer-query-function () + "Run in `kill-buffer-query-functions'." + (exwm--log "id=#x%x; buffer=%s" (or exwm--id 0) (current-buffer)) + (catch 'return + (when (or (not exwm--connection) + (not (slot-value exwm--connection 'connected))) + (throw 'return t)) + (when (or (not exwm--id) + (xcb:+request-checked+request-check exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window exwm--id + :value-mask xcb:CW:EventMask + :event-mask (exwm--get-client-event-mask)))) + ;; The X window is no longer alive so just close the buffer. + (when exwm--floating-frame + (let ((window (frame-parameter exwm--floating-frame 'exwm-outer-id)) + (container (frame-parameter exwm--floating-frame + 'exwm-container))) + (xcb:+request exwm--connection + (make-instance 'xcb:UnmapWindow :window window)) + (xcb:+request exwm--connection + (make-instance 'xcb:ReparentWindow + :window window + :parent exwm--root + :x 0 :y 0)) + (xcb:+request exwm--connection + (make-instance 'xcb:DestroyWindow + :window container)))) + (xcb:flush exwm--connection) + (throw 'return t)) + (unless (memq xcb:Atom:WM_DELETE_WINDOW exwm--protocols) + ;; The X window does not support WM_DELETE_WINDOW; destroy it. + (xcb:+request exwm--connection + (make-instance 'xcb:DestroyWindow :window exwm--id)) + (xcb:flush exwm--connection) + ;; Wait for DestroyNotify event. + (throw 'return nil)) + (let ((id exwm--id)) + ;; Try to close the X window with WM_DELETE_WINDOW client message. + (xcb:+request exwm--connection + (make-instance 'xcb:icccm:SendEvent + :destination id + :event (xcb:marshal + (make-instance 'xcb:icccm:WM_DELETE_WINDOW + :window id) + exwm--connection))) + (xcb:flush exwm--connection) + ;; + (unless (memq xcb:Atom:_NET_WM_PING exwm--protocols) + ;; For X windows without _NET_WM_PING support, we'd better just + ;; wait for DestroyNotify events. + (throw 'return nil)) + ;; Try to determine if the X window is dead with _NET_WM_PING. + (setq exwm-manage--ping-lock t) + (xcb:+request exwm--connection + (make-instance 'xcb:SendEvent + :propagate 0 + :destination id + :event-mask xcb:EventMask:NoEvent + :event (xcb:marshal + (make-instance 'xcb:ewmh:_NET_WM_PING + :window id + :timestamp 0 + :client-window id) + exwm--connection))) + (xcb:flush exwm--connection) + (with-timeout (exwm-manage-ping-timeout + (if (y-or-n-p (format "'%s' is not responding. \ +Would you like to kill it? " + (buffer-name))) + (progn (exwm-manage--kill-client id) + ;; Kill the unresponsive X window and + ;; wait for DestroyNotify event. + (throw 'return nil)) + ;; Give up. + (throw 'return nil))) + (while (and exwm-manage--ping-lock + (exwm--id->buffer id)) ;may have been destroyed. + (accept-process-output nil 0.1)) + ;; Give up. + (throw 'return nil))))) + +(defun exwm-manage--kill-client (&optional id) + "Kill X client ID. +If ID is nil, kill X window corresponding to current buffer." + (unless id (setq id (exwm--buffer->id (current-buffer)))) + (exwm--log "id=#x%x" id) + (let* ((response (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:ewmh:get-_NET_WM_PID :window id))) + (pid (and response (slot-value response 'value))) + (request (make-instance 'xcb:KillClient :resource id))) + (if (not pid) + (xcb:+request exwm--connection request) + ;; What if the PID is fake/wrong? + (signal-process pid 'SIGKILL) + ;; Ensure it's dead + (run-with-timer exwm-manage-ping-timeout nil + (lambda () + (xcb:+request exwm--connection request)))) + (xcb:flush exwm--connection))) + +(defun exwm-manage--add-frame (frame) + "Run in `after-make-frame-functions'. +FRAME is the newly created frame." + (exwm--log "frame=%s" frame) + (when (display-graphic-p frame) + (push (string-to-number (frame-parameter frame 'outer-window-id)) + exwm-manage--frame-outer-id-list))) + +(defun exwm-manage--remove-frame (frame) + "Run in `delete-frame-functions'. +FRAME is the frame to be deleted." + (exwm--log "frame=%s" frame) + (when (display-graphic-p frame) + (setq exwm-manage--frame-outer-id-list + (delq (string-to-number (frame-parameter frame 'outer-window-id)) + exwm-manage--frame-outer-id-list)))) + +(defun exwm-manage--on-ConfigureRequest (data _synthetic) + "Handle ConfigureRequest event. +DATA contains unmarshalled ConfigureRequest event data." + (exwm--log) + (let ((obj (make-instance 'xcb:ConfigureRequest)) + buffer edges width-delta height-delta) + (xcb:unmarshal obj data) + (with-slots (window x y width height + border-width sibling stack-mode value-mask) + obj + (exwm--log "#x%x (#x%x) @%dx%d%+d%+d; \ +border-width: %d; sibling: #x%x; stack-mode: %d" + window value-mask width height x y + border-width sibling stack-mode) + (if (and (setq buffer (exwm--id->buffer window)) + (with-current-buffer buffer + (or (exwm-layout--fullscreen-p) + ;; Make sure it's a floating X window wanting to resize + ;; itself. + (or (not exwm--floating-frame) + (progn + (setq edges + (window-inside-pixel-edges + (get-buffer-window buffer t)) + width-delta (- width (- (elt edges 2) + (elt edges 0))) + height-delta (- height (- (elt edges 3) + (elt edges 1)))) + ;; We cannot do resizing precisely for now. + (and (if (= 0 (logand value-mask + xcb:ConfigWindow:Width)) + t + (< (abs width-delta) + exwm-manage--width-delta-min)) + (if (= 0 (logand value-mask + xcb:ConfigWindow:Height)) + t + (< (abs height-delta) + exwm-manage--height-delta-min)))))))) + ;; Send client message for managed windows + (with-current-buffer buffer + (setq edges + (if (exwm-layout--fullscreen-p) + (with-slots (x y width height) + (exwm-workspace--get-geometry exwm--frame) + (list x y width height)) + (window-inside-absolute-pixel-edges + (get-buffer-window buffer t)))) + (exwm--log "Reply with ConfigureNotify (edges): %s" edges) + (xcb:+request exwm--connection + (make-instance 'xcb:SendEvent + :propagate 0 :destination window + :event-mask xcb:EventMask:StructureNotify + :event (xcb:marshal + (make-instance + 'xcb:ConfigureNotify + :event window :window window + :above-sibling xcb:Window:None + :x (elt edges 0) :y (elt edges 1) + :width (- (elt edges 2) (elt edges 0)) + :height (- (elt edges 3) (elt edges 1)) + :border-width 0 :override-redirect 0) + exwm--connection)))) + (if buffer + (with-current-buffer buffer + (exwm--log "ConfigureWindow (resize floating X window)") + (exwm--set-geometry (frame-parameter exwm--floating-frame + 'exwm-outer-id) + nil + nil + (+ (frame-pixel-width exwm--floating-frame) + width-delta) + (+ (frame-pixel-height exwm--floating-frame) + height-delta))) + (exwm--log "ConfigureWindow (preserve geometry)") + ;; Configure the unmanaged window. + ;; But Emacs frames should be excluded. Generally we don't + ;; receive ConfigureRequest events from Emacs frames since we + ;; have set OverrideRedirect on them, but this is not true for + ;; Lucid build (as of 25.1). + (unless (memq window exwm-manage--frame-outer-id-list) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window window + :value-mask value-mask + :x x :y y :width width :height height + :border-width border-width + :sibling sibling + :stack-mode stack-mode))))))) + (xcb:flush exwm--connection)) + +(defun exwm-manage--on-MapRequest (data _synthetic) + "Handle MapRequest event. +DATA contains unmarshalled MapRequest event data." + (let ((obj (make-instance 'xcb:MapRequest))) + (xcb:unmarshal obj data) + (with-slots (parent window) obj + (exwm--log "id=#x%x parent=#x%x" window parent) + (if (assoc window exwm--id-buffer-alist) + (with-current-buffer (exwm--id->buffer window) + (if (exwm-layout--iconic-state-p) + ;; State change: iconic => normal. + (when (eq exwm--frame exwm-workspace--current) + (pop-to-buffer-same-window (current-buffer))) + (exwm--log "#x%x is already managed" window))) + (if (/= exwm--root parent) + (progn (xcb:+request exwm--connection + (make-instance 'xcb:MapWindow :window window)) + (xcb:flush exwm--connection)) + (exwm--log "#x%x" window) + (exwm-manage--manage-window window)))))) + +(defun exwm-manage--on-UnmapNotify (data _synthetic) + "Handle UnmapNotify event. +DATA contains unmarshalled UnmapNotify event data." + (let ((obj (make-instance 'xcb:UnmapNotify))) + (xcb:unmarshal obj data) + (with-slots (window) obj + (exwm--log "id=#x%x" window) + (exwm-manage--unmanage-window window t)))) + +(defun exwm-manage--on-MapNotify (data _synthetic) + "Handle MapNotify event. +DATA contains unmarshalled MapNotify event data." + (let ((obj (make-instance 'xcb:MapNotify))) + (xcb:unmarshal obj data) + (with-slots (window) obj + (when (assoc window exwm--id-buffer-alist) + (exwm--log "id=#x%x" window) + ;; With this we ensure that a "window hierarchy change" happens after + ;; mapping the window, as some servers (XQuartz) do not generate it. + (with-current-buffer (exwm--id->buffer window) + (if exwm--floating-frame + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window window + :value-mask xcb:ConfigWindow:StackMode + :stack-mode xcb:StackMode:Above)) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window window + :value-mask (logior xcb:ConfigWindow:Sibling + xcb:ConfigWindow:StackMode) + :sibling exwm--guide-window + :stack-mode xcb:StackMode:Above)))) + (xcb:flush exwm--connection))))) + +(defun exwm-manage--on-DestroyNotify (data synthetic) + "Handle DestroyNotify event. +DATA contains unmarshalled DestroyNotify event data. +SYNTHETIC indicates whether the event is a synthetic event." + (unless synthetic + (exwm--log) + (let ((obj (make-instance 'xcb:DestroyNotify))) + (xcb:unmarshal obj data) + (exwm--log "#x%x" (slot-value obj 'window)) + (exwm-manage--unmanage-window (slot-value obj 'window))))) + +(defun exwm-manage--init () + "Initialize manage module." + ;; Intern _MOTIF_WM_HINTS + (exwm--log) + (setq exwm-manage--_MOTIF_WM_HINTS (exwm--intern-atom "_MOTIF_WM_HINTS")) + (add-hook 'after-make-frame-functions #'exwm-manage--add-frame) + (add-hook 'delete-frame-functions #'exwm-manage--remove-frame) + (xcb:+event exwm--connection 'xcb:ConfigureRequest + #'exwm-manage--on-ConfigureRequest) + (xcb:+event exwm--connection 'xcb:MapRequest #'exwm-manage--on-MapRequest) + (xcb:+event exwm--connection 'xcb:UnmapNotify #'exwm-manage--on-UnmapNotify) + (xcb:+event exwm--connection 'xcb:MapNotify #'exwm-manage--on-MapNotify) + (xcb:+event exwm--connection 'xcb:DestroyNotify + #'exwm-manage--on-DestroyNotify)) + +(defun exwm-manage--exit () + "Exit the manage module." + (exwm--log) + (dolist (pair exwm--id-buffer-alist) + (exwm-manage--unmanage-window (car pair) 'quit)) + (remove-hook 'after-make-frame-functions #'exwm-manage--add-frame) + (remove-hook 'delete-frame-functions #'exwm-manage--remove-frame) + (setq exwm-manage--_MOTIF_WM_HINTS nil)) + + + +(provide 'exwm-manage) + +;;; exwm-manage.el ends here diff --git a/exwm-randr.el b/exwm-randr.el new file mode 100644 index 0000000000..7f0e50559b --- /dev/null +++ b/exwm-randr.el @@ -0,0 +1,369 @@ +;;; exwm-randr.el --- RandR Module for EXWM -*- lexical-binding: t -*- + +;; Copyright (C) 2015-2024 Free Software Foundation, Inc. + +;; Author: Chris Feng <chris.w.feng@gmail.com> + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This module adds RandR support for EXWM. Currently it requires external +;; tools such as xrandr(1) to properly configure RandR first. This +;; dependency may be removed in the future, but more work is needed before +;; that. + +;; To use this module, load, enable it and configure +;; `exwm-randr-workspace-monitor-plist' and `exwm-randr-screen-change-hook' +;; as follows: +;; +;; (require 'exwm-randr) +;; (setq exwm-randr-workspace-monitor-plist '(0 "VGA1")) +;; (add-hook 'exwm-randr-screen-change-hook +;; (lambda () +;; (start-process-shell-command +;; "xrandr" nil "xrandr --output VGA1 --left-of LVDS1 --auto"))) +;; (exwm-randr-enable) +;; +;; With above lines, workspace 0 should be assigned to the output named "VGA1", +;; staying at the left of other workspaces on the output "LVDS1". Please refer +;; to xrandr(1) for the configuration of RandR. + +;; References: +;; + RandR (http://www.x.org/archive/X11R7.7/doc/randrproto/randrproto.txt) + +;;; Code: + +(require 'xcb-randr) + +(require 'exwm-core) +(require 'exwm-workspace) + +(declare-function x-get-atom-name "C source code" (VALUE &optional FRAME)) + +(defgroup exwm-randr nil + "RandR." + :group 'exwm) + +(defcustom exwm-randr-refresh-hook nil + "Normal hook run when the RandR module just refreshed." + :type 'hook) + +(defcustom exwm-randr-screen-change-hook nil + "Normal hook run when screen changes." + :type 'hook) + +(defcustom exwm-randr-workspace-monitor-plist nil + "Plist mapping workspaces to monitors. + +In RandR 1.5 a monitor is a rectangle region decoupled from the physical +size of screens, and can be identified with `xrandr --listmonitors' (name of +the primary monitor is prefixed with an `*'). When no monitor is created it +automatically fallback to RandR 1.2 output which represents the physical +screen size. RandR 1.5 monitors can be created with `xrandr --setmonitor'. +For example, to split an output (`LVDS-1') of size 1280x800 into two +side-by-side monitors one could invoke (the digits after `/' are size in mm) + + xrandr --setmonitor *LVDS-1-L 640/135x800/163+0+0 LVDS-1 + xrandr --setmonitor LVDS-1-R 640/135x800/163+640+0 none + +If a monitor is not active, the workspaces mapped to it are displayed on the +primary monitor until it becomes active (if ever). Unspecified workspaces +are all mapped to the primary monitor. For example, with the following +setting workspace other than 1 and 3 would always be displayed on the +primary monitor where workspace 1 and 3 would be displayed on their +corresponding monitors whenever the monitors are active. + + \\='(1 \"HDMI-1\" 3 \"DP-1\")" + :type '(plist :key-type integer :value-type string)) + +(defvar exwm-randr--last-timestamp 0 "Used for debouncing events.") + +(defvar exwm-randr--prev-screen-change-seqnum nil + "The most recent ScreenChangeNotify sequence number.") + +(defvar exwm-randr--compatibility-mode nil + "Non-nil when the server does not support RandR 1.5 protocol.") + +(defun exwm-randr--get-monitors () + "Get RandR 1.5 monitors." + (exwm--log) + (let (monitor-name geometry monitor-geometry-alist primary-monitor) + (with-slots (timestamp monitors) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:randr:GetMonitors + :window exwm--root + :get-active 1)) + (when (> timestamp exwm-randr--last-timestamp) + (setq exwm-randr--last-timestamp timestamp)) + (dolist (monitor monitors) + (with-slots (name primary x y width height) monitor + (setq monitor-name (x-get-atom-name name) + geometry (make-instance 'xcb:RECTANGLE + :x x + :y y + :width width + :height height) + monitor-geometry-alist (cons (cons monitor-name geometry) + monitor-geometry-alist)) + (exwm--log "%s: %sx%s+%s+%s" monitor-name x y width height) + ;; Save primary monitor when available (fallback to the first one). + (when (or (/= 0 primary) + (not primary-monitor)) + (setq primary-monitor monitor-name))))) + (exwm--log "Primary monitor: %s" primary-monitor) + (list primary-monitor monitor-geometry-alist + (exwm-randr--get-monitor-alias primary-monitor + monitor-geometry-alist)))) + +(defun exwm-randr--get-outputs () + "Get RandR 1.2 outputs. + +Only used when RandR 1.5 is not supported by the server." + (exwm--log) + (let (output-name geometry output-geometry-alist primary-output) + (with-slots (config-timestamp outputs) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:randr:GetScreenResourcesCurrent + :window exwm--root)) + (when (> config-timestamp exwm-randr--last-timestamp) + (setq exwm-randr--last-timestamp config-timestamp)) + (dolist (output outputs) + (with-slots (crtc connection name) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:randr:GetOutputInfo + :output output + :config-timestamp config-timestamp)) + (when (and (= connection xcb:randr:Connection:Connected) + (/= crtc 0)) + (with-slots (x y width height) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:randr:GetCrtcInfo + :crtc crtc + :config-timestamp config-timestamp)) + (setq output-name (decode-coding-string + (apply #'unibyte-string name) 'utf-8) + geometry (make-instance 'xcb:RECTANGLE + :x x + :y y + :width width + :height height) + output-geometry-alist (cons (cons output-name geometry) + output-geometry-alist)) + (exwm--log "%s: %sx%s+%s+%s" output-name x y width height) + ;; The primary output is the first one. + (unless primary-output + (setq primary-output output-name))))))) + (exwm--log "Primary output: %s" primary-output) + (list primary-output output-geometry-alist + (exwm-randr--get-monitor-alias primary-output + output-geometry-alist)))) + +(defun exwm-randr--get-monitor-alias (primary-monitor monitor-geometry-alist) + "Generate monitor aliases using PRIMARY-MONITOR MONITOR-GEOMETRY-ALIST. + +In a mirroring setup some monitors overlap and should be treated as one." + (let (monitor-position-alist monitor-alias-alist monitor-name geometry) + (setq monitor-position-alist (with-slots (x y) + (cdr (assoc primary-monitor + monitor-geometry-alist)) + (list (cons primary-monitor (vector x y))))) + (setq monitor-alias-alist (list (cons primary-monitor primary-monitor))) + (dolist (pair monitor-geometry-alist) + (setq monitor-name (car pair) + geometry (cdr pair)) + (unless (assoc monitor-name monitor-alias-alist) + (let* ((position (vector (slot-value geometry 'x) + (slot-value geometry 'y))) + (alias (car (rassoc position monitor-position-alist)))) + (if alias + (setq monitor-alias-alist (cons (cons monitor-name alias) + monitor-alias-alist)) + (setq monitor-position-alist (cons (cons monitor-name position) + monitor-position-alist) + monitor-alias-alist (cons (cons monitor-name monitor-name) + monitor-alias-alist)))))) + monitor-alias-alist)) + +;;;###autoload +(defun exwm-randr-refresh () + "Refresh workspaces according to the updated RandR info." + (interactive) + (exwm--log) + (let* ((result (if exwm-randr--compatibility-mode + (exwm-randr--get-outputs) + (exwm-randr--get-monitors))) + (primary-monitor (elt result 0)) + (monitor-geometry-alist (elt result 1)) + (monitor-alias-alist (elt result 2)) + container-monitor-alist container-frame-alist) + (when (and primary-monitor monitor-geometry-alist) + (when exwm-workspace--fullscreen-frame-count + ;; Not all workspaces are fullscreen; reset this counter. + (setq exwm-workspace--fullscreen-frame-count 0)) + (dotimes (i (exwm-workspace--count)) + (let* ((monitor (plist-get exwm-randr-workspace-monitor-plist i)) + (geometry (cdr (assoc monitor monitor-geometry-alist))) + (frame (elt exwm-workspace--list i)) + (container (frame-parameter frame 'exwm-container))) + (if geometry + ;; Unify monitor names in case it's a mirroring setup. + (setq monitor (cdr (assoc monitor monitor-alias-alist))) + ;; Missing monitors fallback to the primary one. + (setq monitor primary-monitor + geometry (cdr (assoc primary-monitor + monitor-geometry-alist)))) + (setq container-monitor-alist (nconc + `((,container . ,(intern monitor))) + container-monitor-alist) + container-frame-alist (nconc `((,container . ,frame)) + container-frame-alist)) + (set-frame-parameter frame 'exwm-randr-monitor monitor) + (set-frame-parameter frame 'exwm-geometry geometry))) + ;; Update workareas. + (exwm-workspace--update-workareas) + ;; Resize workspace. + (dolist (f exwm-workspace--list) + (exwm-workspace--set-fullscreen f)) + (xcb:flush exwm--connection) + ;; Raise the minibuffer if it's active. + (when (and (active-minibuffer-window) + (exwm-workspace--minibuffer-own-frame-p)) + (exwm-workspace--show-minibuffer)) + ;; Set _NET_DESKTOP_GEOMETRY. + (exwm-workspace--set-desktop-geometry) + ;; Update active/inactive workspaces. + (dolist (w exwm-workspace--list) + (exwm-workspace--set-active w nil)) + ;; Mark the workspace on the top of each monitor as active. + (dolist (xwin + (reverse + (slot-value (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:QueryTree + :window exwm--root)) + 'children))) + (let ((monitor (cdr (assq xwin container-monitor-alist)))) + (when monitor + (setq container-monitor-alist + (rassq-delete-all monitor container-monitor-alist)) + (exwm-workspace--set-active (cdr (assq xwin container-frame-alist)) + t)))) + (xcb:flush exwm--connection) + (run-hooks 'exwm-randr-refresh-hook)))) + +(defun exwm-randr--on-ScreenChangeNotify (data _synthetic) + "Handle `ScreenChangeNotify' event. + +Run `exwm-randr-screen-change-hook' (usually user scripts to configure RandR)." + (exwm--log) + (let ((evt (make-instance 'xcb:randr:ScreenChangeNotify))) + (xcb:unmarshal evt data) + (let ((seqnum (slot-value evt '~sequence))) + (unless (equal seqnum exwm-randr--prev-screen-change-seqnum) + (setq exwm-randr--prev-screen-change-seqnum seqnum) + (run-hooks 'exwm-randr-screen-change-hook))))) + +(defun exwm-randr--on-Notify (data _synthetic) + "Handle `CrtcChangeNotify' and `OutputChangeNotify' events. + +Refresh when any CRTC/output changes." + (exwm--log) + (let ((evt (make-instance 'xcb:randr:Notify)) + notify) + (xcb:unmarshal evt data) + (with-slots (subCode u) evt + (cl-case subCode + (xcb:randr:Notify:CrtcChange + (setq notify (slot-value u 'cc))) + (xcb:randr:Notify:OutputChange + (setq notify (slot-value u 'oc)))) + (when notify + (with-slots (timestamp) notify + (when (> timestamp exwm-randr--last-timestamp) + (exwm-randr-refresh) + (setq exwm-randr--last-timestamp timestamp))))))) + +(defun exwm-randr--on-ConfigureNotify (data _synthetic) + "Handle `ConfigureNotify' event. + +Refresh when any RandR 1.5 monitor changes." + (exwm--log) + (let ((evt (make-instance 'xcb:ConfigureNotify))) + (xcb:unmarshal evt data) + (with-slots (window) evt + (when (eq window exwm--root) + (exwm-randr-refresh))))) + +(defun exwm-randr--init () + "Initialize RandR extension and EXWM RandR module." + (exwm--log) + (when (= 0 (slot-value (xcb:get-extension-data exwm--connection 'xcb:randr) + 'present)) + (error "[EXWM] RandR extension is not supported by the server")) + (with-slots (major-version minor-version) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:randr:QueryVersion + :major-version 1 :minor-version 5)) + (cond ((and (= major-version 1) (= minor-version 5)) + (setq exwm-randr--compatibility-mode nil)) + ((and (= major-version 1) (>= minor-version 2)) + (setq exwm-randr--compatibility-mode t)) + (t + (error "[EXWM] The server only support RandR version up to %d.%d" + major-version minor-version))) + ;; External monitor(s) may already be connected. + (run-hooks 'exwm-randr-screen-change-hook) + (exwm-randr-refresh) + ;; Listen for `ScreenChangeNotify' to notify external tools to + ;; configure RandR and `CrtcChangeNotify/OutputChangeNotify' to + ;; refresh the workspace layout. + (xcb:+event exwm--connection 'xcb:randr:ScreenChangeNotify + #'exwm-randr--on-ScreenChangeNotify) + (xcb:+event exwm--connection 'xcb:randr:Notify + #'exwm-randr--on-Notify) + (xcb:+event exwm--connection 'xcb:ConfigureNotify + #'exwm-randr--on-ConfigureNotify) + (xcb:+request exwm--connection + (make-instance 'xcb:randr:SelectInput + :window exwm--root + :enable (logior + xcb:randr:NotifyMask:ScreenChange + xcb:randr:NotifyMask:CrtcChange + xcb:randr:NotifyMask:OutputChange))) + (xcb:flush exwm--connection) + (add-hook 'exwm-workspace-list-change-hook #'exwm-randr-refresh)) + ;; Prevent frame parameters introduced by this module from being + ;; saved/restored. + (dolist (i '(exwm-randr-monitor)) + (unless (assq i frameset-filter-alist) + (push (cons i :never) frameset-filter-alist)))) + +(defun exwm-randr--exit () + "Exit the RandR module." + (exwm--log) + (remove-hook 'exwm-workspace-list-change-hook #'exwm-randr-refresh)) + +(defun exwm-randr-enable () + "Enable RandR support for EXWM." + (exwm--log) + (add-hook 'exwm-init-hook #'exwm-randr--init) + (add-hook 'exwm-exit-hook #'exwm-randr--exit)) + + + +(provide 'exwm-randr) + +;;; exwm-randr.el ends here diff --git a/exwm-systemtray.el b/exwm-systemtray.el new file mode 100644 index 0000000000..9e57dae4eb --- /dev/null +++ b/exwm-systemtray.el @@ -0,0 +1,701 @@ +;;; exwm-systemtray.el --- System Tray Module for -*- lexical-binding: t -*- +;;; EXWM + +;; Copyright (C) 2016-2024 Free Software Foundation, Inc. + +;; Author: Chris Feng <chris.w.feng@gmail.com> + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This module adds system tray support for EXWM. + +;; To use this module, load and enable it as follows: +;; (require 'exwm-systemtray) +;; (exwm-systemtray-enable) + +;;; Code: + +(require 'xcb-ewmh) +(require 'xcb-icccm) +(require 'xcb-xembed) +(require 'xcb-systemtray) + +(require 'exwm-core) +(require 'exwm-workspace) + +(declare-function exwm-workspace--workarea "exwm-workspace.el" (frame)) + +(defclass exwm-systemtray--icon () + ((width :initarg :width) + (height :initarg :height) + (visible :initarg :visible)) + :documentation "Attributes of a system tray icon.") + +(defclass xcb:systemtray:-ClientMessage + (xcb:icccm:--ClientMessage xcb:ClientMessage) + ((format :initform 32) + (type :initform 'xcb:Atom:MANAGER) + (time :initarg :time :type xcb:TIMESTAMP) ;new slot + (selection :initarg :selection :type xcb:ATOM) ;new slot + (owner :initarg :owner :type xcb:WINDOW)) ;new slot + :documentation "A systemtray client message.") + +(defgroup exwm-systemtray nil + "System tray." + :group 'exwm) + +(defcustom exwm-systemtray-height nil + "System tray height. + +You shall use the default value if using auto-hide minibuffer." + :type 'integer) + +(defcustom exwm-systemtray-icon-gap 2 + "Gap between icons." + :type 'integer) + +(defvar exwm-systemtray--connection nil "The X connection.") + +(defvar exwm-systemtray--embedder-window nil "The embedder window.") +(defvar exwm-systemtray--embedder-window-depth nil + "The embedder window's depth.") + +(defcustom exwm-systemtray-background-color 'workspace-background + "Background color of systemtray. +This should be a color, the symbol `workspace-background' for the background +color of current workspace frame, or the symbol `transparent' for transparent +background. + +Transparent background is not yet supported when Emacs uses 32-bit depth +visual, as reported by `x-display-planes'. The X resource \"Emacs.visualClass: +TrueColor-24\" can be used to force Emacs to use 24-bit depth." + :type '(choice (const :tag "Transparent" transparent) + (const :tag "Frame background" workspace-background) + (color :tag "Color")) + :initialize #'custom-initialize-default + :set (lambda (symbol value) + (when (and (eq value 'transparent) + (not (exwm-systemtray--transparency-supported-p))) + (display-warning 'exwm-systemtray + "Transparent background is not supported yet when \ +using 32-bit depth. Using `workspace-background' instead.") + (setq value 'workspace-background)) + (set-default symbol value) + (when (and exwm-systemtray--connection + exwm-systemtray--embedder-window) + ;; Change the background color for embedder. + (exwm-systemtray--set-background-color) + ;; Unmap & map to take effect immediately. + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:UnmapWindow + :window exwm-systemtray--embedder-window)) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:MapWindow + :window exwm-systemtray--embedder-window)) + (xcb:flush exwm-systemtray--connection)))) + +;; GTK icons require at least 16 pixels to show normally. +(defconst exwm-systemtray--icon-min-size 16 "Minimum icon size.") + +(defvar exwm-systemtray--list nil "The icon list.") + +(defvar exwm-systemtray--selection-owner-window nil + "The selection owner window.") + +(defvar xcb:Atom:_NET_SYSTEM_TRAY_S0) + +(defun exwm-systemtray--embed (icon) + "Embed an ICON." + (exwm--log "Try to embed #x%x" icon) + (let ((info (xcb:+request-unchecked+reply exwm-systemtray--connection + (make-instance 'xcb:xembed:get-_XEMBED_INFO + :window icon))) + width* height* visible) + (when info + (exwm--log "Embed #x%x" icon) + (with-slots (width height) + (xcb:+request-unchecked+reply exwm-systemtray--connection + (make-instance 'xcb:GetGeometry :drawable icon)) + (setq height* exwm-systemtray-height + width* (round (* width (/ (float height*) height)))) + (when (< width* exwm-systemtray--icon-min-size) + (setq width* exwm-systemtray--icon-min-size + height* (round (* height (/ (float width*) width))))) + (exwm--log "Resize from %dx%d to %dx%d" + width height width* height*)) + ;; Add this icon to save-set. + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ChangeSaveSet + :mode xcb:SetMode:Insert + :window icon)) + ;; Reparent to the embedder. + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ReparentWindow + :window icon + :parent exwm-systemtray--embedder-window + :x 0 + ;; Vertically centered. + :y (/ (- exwm-systemtray-height height*) 2))) + ;; Resize the icon. + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ConfigureWindow + :window icon + :value-mask (logior xcb:ConfigWindow:Width + xcb:ConfigWindow:Height + xcb:ConfigWindow:BorderWidth) + :width width* + :height height* + :border-width 0)) + ;; Set event mask. + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ChangeWindowAttributes + :window icon + :value-mask xcb:CW:EventMask + :event-mask (logior xcb:EventMask:ResizeRedirect + xcb:EventMask:KeyPress + xcb:EventMask:PropertyChange))) + ;; Grab all keys and forward them to Emacs frame. + (unless (exwm-workspace--minibuffer-own-frame-p) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:GrabKey + :owner-events 0 + :grab-window icon + :modifiers xcb:ModMask:Any + :key xcb:Grab:Any + :pointer-mode xcb:GrabMode:Async + :keyboard-mode xcb:GrabMode:Async))) + (setq visible (slot-value info 'flags)) + (if visible + (setq visible + (/= 0 (logand (slot-value info 'flags) xcb:xembed:MAPPED))) + ;; Default to visible. + (setq visible t)) + (when visible + (exwm--log "Map the window") + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:MapWindow :window icon))) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:xembed:SendEvent + :destination icon + :event + (xcb:marshal + (make-instance 'xcb:xembed:EMBEDDED-NOTIFY + :window icon + :time xcb:Time:CurrentTime + :embedder + exwm-systemtray--embedder-window + :version 0) + exwm-systemtray--connection))) + (push `(,icon . ,(make-instance 'exwm-systemtray--icon + :width width* + :height height* + :visible visible)) + exwm-systemtray--list) + (exwm-systemtray--refresh)))) + +(defun exwm-systemtray--unembed (icon) + "Unembed an ICON." + (exwm--log "Unembed #x%x" icon) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:UnmapWindow :window icon)) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ReparentWindow + :window icon + :parent exwm--root + :x 0 :y 0)) + (setq exwm-systemtray--list + (assq-delete-all icon exwm-systemtray--list)) + (exwm-systemtray--refresh)) + +(defun exwm-systemtray--refresh () + "Refresh the system tray." + (exwm--log) + ;; Make sure to redraw the embedder. + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:UnmapWindow + :window exwm-systemtray--embedder-window)) + (let ((x exwm-systemtray-icon-gap) + map) + (dolist (pair exwm-systemtray--list) + (when (slot-value (cdr pair) 'visible) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ConfigureWindow + :window (car pair) + :value-mask xcb:ConfigWindow:X + :x x)) + (setq x (+ x (slot-value (cdr pair) 'width) + exwm-systemtray-icon-gap)) + (setq map t))) + (let ((workarea (exwm-workspace--workarea exwm-workspace-current-index))) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ConfigureWindow + :window exwm-systemtray--embedder-window + :value-mask (logior xcb:ConfigWindow:X + xcb:ConfigWindow:Width) + :x (- (slot-value workarea 'width) x) + :width x))) + (when map + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:MapWindow + :window exwm-systemtray--embedder-window)))) + (xcb:flush exwm-systemtray--connection)) + +(defun exwm-systemtray--refresh-background-color (&optional remap) + "Refresh background color after theme change or workspace switch. +If REMAP is not nil, map and unmap the embedder window so that the background is +redrawn." + ;; Only `workspace-background' is dependent on current theme and workspace. + (when (eq 'workspace-background exwm-systemtray-background-color) + (exwm-systemtray--set-background-color) + (when remap + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:UnmapWindow + :window exwm-systemtray--embedder-window)) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:MapWindow + :window exwm-systemtray--embedder-window)) + (xcb:flush exwm-systemtray--connection)))) + +(defun exwm-systemtray--set-background-color () + "Change the background color of the embedder. +The color is set according to `exwm-systemtray-background-color'. + +Note that this function does not change the current contents of the embedder +window; unmap & map are necessary for the background color to take effect." + (when (and exwm-systemtray--connection + exwm-systemtray--embedder-window) + (let* ((color (cl-case exwm-systemtray-background-color + ((transparent nil) ; nil means transparent as well + (if (exwm-systemtray--transparency-supported-p) + nil + (message "%s" "[EXWM] system tray does not support \ +`transparent' background; using `workspace-background' instead") + (face-background 'default exwm-workspace--current))) + (workspace-background + (face-background 'default exwm-workspace--current)) + (t exwm-systemtray-background-color))) + (background-pixel (exwm--color->pixel color))) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ChangeWindowAttributes + :window exwm-systemtray--embedder-window + ;; Either-or. A `background-pixel' of nil + ;; means simulate transparency. We use + ;; `xcb:CW:BackPixmap' together with + ;; `xcb:BackPixmap:ParentRelative' do that, + ;; but this only works when the parent + ;; window's visual (Emacs') has the same + ;; visual depth. + :value-mask (if background-pixel + xcb:CW:BackPixel + xcb:CW:BackPixmap) + ;; Due to the :value-mask above, + ;; :background-pixmap only takes effect when + ;; `transparent' is requested and supported + ;; (visual depth of Emacs and of system tray + ;; are equal). Setting + ;; `xcb:BackPixmap:ParentRelative' when + ;; that's not the case would produce an + ;; `xcb:Match' error. + :background-pixmap xcb:BackPixmap:ParentRelative + :background-pixel background-pixel))))) + +(defun exwm-systemtray--transparency-supported-p () + "Check whether transparent background is supported. +EXWM system tray supports transparency when the visual depth of the system tray +window matches that of Emacs. The visual depth of the system tray window is the +default visual depth of the display. + +Sections \"Visual and background pixmap handling\" and +\"_NET_SYSTEM_TRAY_VISUAL\" of the System Tray Protocol Specification +\(https://specifications.freedesktop.org/systemtray-spec/systemtray-spec-latest.html#visuals) +indicate how to support actual transparency." + (let ((planes (x-display-planes))) + (if exwm-systemtray--embedder-window-depth + (= planes exwm-systemtray--embedder-window-depth) + (<= planes 24)))) + +(defun exwm-systemtray--on-DestroyNotify (data _synthetic) + "Unembed icons on DestroyNotify. +Argument DATA contains the raw event data." + (exwm--log) + (let ((obj (make-instance 'xcb:DestroyNotify))) + (xcb:unmarshal obj data) + (with-slots (window) obj + (when (assoc window exwm-systemtray--list) + (exwm-systemtray--unembed window))))) + +(defun exwm-systemtray--on-ReparentNotify (data _synthetic) + "Unembed icons on ReparentNotify. +Argument DATA contains the raw event data." + (exwm--log) + (let ((obj (make-instance 'xcb:ReparentNotify))) + (xcb:unmarshal obj data) + (with-slots (window parent) obj + (when (and (/= parent exwm-systemtray--embedder-window) + (assoc window exwm-systemtray--list)) + (exwm-systemtray--unembed window))))) + +(defun exwm-systemtray--on-ResizeRequest (data _synthetic) + "Resize the tray icon on ResizeRequest. +Argument DATA contains the raw event data." + (exwm--log) + (let ((obj (make-instance 'xcb:ResizeRequest)) + attr) + (xcb:unmarshal obj data) + (with-slots (window width height) obj + (when (setq attr (cdr (assoc window exwm-systemtray--list))) + (with-slots ((width* width) + (height* height)) + attr + (setq height* exwm-systemtray-height + width* (round (* width (/ (float height*) height)))) + (when (< width* exwm-systemtray--icon-min-size) + (setq width* exwm-systemtray--icon-min-size + height* (round (* height (/ (float width*) width))))) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ConfigureWindow + :window window + :value-mask (logior xcb:ConfigWindow:Y + xcb:ConfigWindow:Width + xcb:ConfigWindow:Height) + ;; Vertically centered. + :y (/ (- exwm-systemtray-height height*) 2) + :width width* + :height height*))) + (exwm-systemtray--refresh))))) + +(defun exwm-systemtray--on-PropertyNotify (data _synthetic) + "Map/Unmap the tray icon on PropertyNotify. +Argument DATA contains the raw event data." + (exwm--log) + (let ((obj (make-instance 'xcb:PropertyNotify)) + attr info visible) + (xcb:unmarshal obj data) + (with-slots (window atom state) obj + (when (and (eq state xcb:Property:NewValue) + (eq atom xcb:Atom:_XEMBED_INFO) + (setq attr (cdr (assoc window exwm-systemtray--list)))) + (setq info (xcb:+request-unchecked+reply exwm-systemtray--connection + (make-instance 'xcb:xembed:get-_XEMBED_INFO + :window window))) + (when info + (setq visible (/= 0 (logand (slot-value info 'flags) + xcb:xembed:MAPPED))) + (exwm--log "#x%x visible? %s" window visible) + (if visible + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:MapWindow :window window)) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:UnmapWindow :window window))) + (setf (slot-value attr 'visible) visible) + (exwm-systemtray--refresh)))))) + +(defun exwm-systemtray--on-ClientMessage (data _synthetic) + "Handle client messages. +Argument DATA contains the raw event data." + (let ((obj (make-instance 'xcb:ClientMessage)) + opcode data32) + (xcb:unmarshal obj data) + (with-slots (window type data) obj + (when (eq type xcb:Atom:_NET_SYSTEM_TRAY_OPCODE) + (setq data32 (slot-value data 'data32) + opcode (elt data32 1)) + (exwm--log "opcode: %s" opcode) + (cond ((= opcode xcb:systemtray:opcode:REQUEST-DOCK) + (unless (assoc (elt data32 2) exwm-systemtray--list) + (exwm-systemtray--embed (elt data32 2)))) + ;; Not implemented (rarely used nowadays). + ((or (= opcode xcb:systemtray:opcode:BEGIN-MESSAGE) + (= opcode xcb:systemtray:opcode:CANCEL-MESSAGE))) + (t + (exwm--log "Unknown opcode message: %s" obj))))))) + +(defun exwm-systemtray--on-KeyPress (data _synthetic) + "Forward all KeyPress events to Emacs frame. +Argument DATA contains the raw event data." + (exwm--log) + ;; This function is only executed when there's no autohide minibuffer, + ;; a workspace frame has the input focus and the pointer is over a + ;; tray icon. + (let ((dest (frame-parameter (selected-frame) 'exwm-outer-id)) + (obj (make-instance 'xcb:KeyPress))) + (xcb:unmarshal obj data) + (setf (slot-value obj 'event) dest) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:SendEvent + :propagate 0 + :destination dest + :event-mask xcb:EventMask:NoEvent + :event (xcb:marshal obj exwm-systemtray--connection)))) + (xcb:flush exwm-systemtray--connection)) + +(defun exwm-systemtray--on-workspace-switch () + "Reparent/Refresh the system tray in `exwm-workspace-switch-hook'." + (exwm--log) + (unless (exwm-workspace--minibuffer-own-frame-p) + (exwm-workspace--update-offsets) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ReparentWindow + :window exwm-systemtray--embedder-window + :parent (string-to-number + (frame-parameter exwm-workspace--current + 'window-id)) + :x 0 + :y (- (slot-value (exwm-workspace--workarea + exwm-workspace-current-index) + 'height) + exwm-workspace--frame-y-offset + exwm-systemtray-height)))) + (exwm-systemtray--refresh-background-color) + (exwm-systemtray--refresh)) + +(defun exwm-systemtray--on-theme-change (_theme) + "Refresh system tray upon theme change." + (exwm-systemtray--refresh-background-color 'remap)) + +(defun exwm-systemtray--refresh-all () + "Reposition/Refresh the system tray." + (exwm--log) + (unless (exwm-workspace--minibuffer-own-frame-p) + (exwm-workspace--update-offsets) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ConfigureWindow + :window exwm-systemtray--embedder-window + :value-mask xcb:ConfigWindow:Y + :y (- (slot-value (exwm-workspace--workarea + exwm-workspace-current-index) + 'height) + exwm-workspace--frame-y-offset + exwm-systemtray-height)))) + (exwm-systemtray--refresh)) + +(cl-defun exwm-systemtray--init () + "Initialize system tray module." + (exwm--log) + (cl-assert (not exwm-systemtray--connection)) + (cl-assert (not exwm-systemtray--list)) + (cl-assert (not exwm-systemtray--selection-owner-window)) + (cl-assert (not exwm-systemtray--embedder-window)) + (unless exwm-systemtray-height + (setq exwm-systemtray-height (max exwm-systemtray--icon-min-size + (with-selected-window (minibuffer-window) + (line-pixel-height))))) + ;; Create a new connection. + (setq exwm-systemtray--connection (xcb:connect)) + (set-process-query-on-exit-flag (slot-value exwm-systemtray--connection + 'process) + nil) + ;; Initialize XELB modules. + (xcb:xembed:init exwm-systemtray--connection t) + (xcb:systemtray:init exwm-systemtray--connection t) + ;; Acquire the manager selection _NET_SYSTEM_TRAY_S0. + (with-slots (owner) + (xcb:+request-unchecked+reply exwm-systemtray--connection + (make-instance 'xcb:GetSelectionOwner + :selection xcb:Atom:_NET_SYSTEM_TRAY_S0)) + (when (/= owner xcb:Window:None) + (xcb:disconnect exwm-systemtray--connection) + (setq exwm-systemtray--connection nil) + (warn "[EXWM] Other system tray detected") + (cl-return-from exwm-systemtray--init))) + (let ((id (xcb:generate-id exwm-systemtray--connection))) + (setq exwm-systemtray--selection-owner-window id) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:CreateWindow + :depth 0 + :wid id + :parent exwm--root + :x 0 + :y 0 + :width 1 + :height 1 + :border-width 0 + :class xcb:WindowClass:InputOnly + :visual 0 + :value-mask xcb:CW:OverrideRedirect + :override-redirect 1)) + ;; Get the selection ownership. + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:SetSelectionOwner + :owner id + :selection xcb:Atom:_NET_SYSTEM_TRAY_S0 + :time xcb:Time:CurrentTime)) + ;; Send a client message to announce the selection. + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:SendEvent + :propagate 0 + :destination exwm--root + :event-mask xcb:EventMask:StructureNotify + :event (xcb:marshal + (make-instance 'xcb:systemtray:-ClientMessage + :window exwm--root + :time xcb:Time:CurrentTime + :selection + xcb:Atom:_NET_SYSTEM_TRAY_S0 + :owner id) + exwm-systemtray--connection))) + ;; Set _NET_WM_NAME. + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ewmh:set-_NET_WM_NAME + :window id + :data "EXWM: exwm-systemtray--selection-owner-window")) + ;; Set the _NET_SYSTEM_TRAY_ORIENTATION property. + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:xembed:set-_NET_SYSTEM_TRAY_ORIENTATION + :window id + :data xcb:systemtray:ORIENTATION:HORZ))) + ;; Create the embedder. + (let ((id (xcb:generate-id exwm-systemtray--connection)) + frame parent embedder-depth embedder-visual embedder-colormap y) + (setq exwm-systemtray--embedder-window id) + (if (exwm-workspace--minibuffer-own-frame-p) + (setq frame exwm-workspace--minibuffer + y (if (>= (line-pixel-height) exwm-systemtray-height) + ;; Bottom aligned. + (- (line-pixel-height) exwm-systemtray-height) + ;; Vertically centered. + (/ (- (line-pixel-height) exwm-systemtray-height) 2))) + (exwm-workspace--update-offsets) + (setq frame exwm-workspace--current + ;; Bottom aligned. + y (- (slot-value (exwm-workspace--workarea + exwm-workspace-current-index) + 'height) + exwm-workspace--frame-y-offset + exwm-systemtray-height))) + (setq parent (string-to-number (frame-parameter frame 'window-id))) + ;; Use default depth, visual and colormap (from root window), instead of + ;; Emacs frame's. See Section "Visual and background pixmap handling" in + ;; "System Tray Protocol Specification 0.3". + (let* ((vdc (exwm--get-visual-depth-colormap exwm-systemtray--connection + exwm--root))) + (setq embedder-visual (car vdc)) + (setq embedder-depth (cadr vdc)) + (setq embedder-colormap (caddr vdc))) + ;; Note down the embedder window's depth. It will be used to check whether + ;; we can use xcb:BackPixmap:ParentRelative to emulate transparency. + (setq exwm-systemtray--embedder-window-depth embedder-depth) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:CreateWindow + :depth embedder-depth + :wid id + :parent parent + :x 0 + :y y + :width 1 + :height exwm-systemtray-height + :border-width 0 + :class xcb:WindowClass:InputOutput + :visual embedder-visual + :colormap embedder-colormap + :value-mask (logior xcb:CW:BorderPixel + xcb:CW:Colormap + xcb:CW:EventMask) + :border-pixel 0 + :event-mask xcb:EventMask:SubstructureNotify)) + (exwm-systemtray--set-background-color) + ;; Set _NET_WM_NAME. + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ewmh:set-_NET_WM_NAME + :window id + :data "EXWM: exwm-systemtray--embedder-window")) + ;; Set _NET_WM_WINDOW_TYPE. + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ewmh:set-_NET_WM_WINDOW_TYPE + :window id + :data (vector xcb:Atom:_NET_WM_WINDOW_TYPE_DOCK))) + ;; Set _NET_SYSTEM_TRAY_VISUAL. + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:xembed:set-_NET_SYSTEM_TRAY_VISUAL + :window exwm-systemtray--selection-owner-window + :data embedder-visual))) + (xcb:flush exwm-systemtray--connection) + ;; Attach event listeners. + (xcb:+event exwm-systemtray--connection 'xcb:DestroyNotify + #'exwm-systemtray--on-DestroyNotify) + (xcb:+event exwm-systemtray--connection 'xcb:ReparentNotify + #'exwm-systemtray--on-ReparentNotify) + (xcb:+event exwm-systemtray--connection 'xcb:ResizeRequest + #'exwm-systemtray--on-ResizeRequest) + (xcb:+event exwm-systemtray--connection 'xcb:PropertyNotify + #'exwm-systemtray--on-PropertyNotify) + (xcb:+event exwm-systemtray--connection 'xcb:ClientMessage + #'exwm-systemtray--on-ClientMessage) + (unless (exwm-workspace--minibuffer-own-frame-p) + (xcb:+event exwm-systemtray--connection 'xcb:KeyPress + #'exwm-systemtray--on-KeyPress)) + ;; Add hook to move/reparent the embedder. + (add-hook 'exwm-workspace-switch-hook #'exwm-systemtray--on-workspace-switch) + (add-hook 'exwm-workspace--update-workareas-hook + #'exwm-systemtray--refresh-all) + ;; Add hook to update background colors. + (add-hook 'enable-theme-functions #'exwm-systemtray--on-theme-change) + (add-hook 'disable-theme-functions #'exwm-systemtray--on-theme-change) + (add-hook 'menu-bar-mode-hook #'exwm-systemtray--refresh-all) + (add-hook 'tool-bar-mode-hook #'exwm-systemtray--refresh-all) + (when (boundp 'exwm-randr-refresh-hook) + (add-hook 'exwm-randr-refresh-hook #'exwm-systemtray--refresh-all)) + ;; The struts can be updated already. + (when exwm-workspace--workareas + (exwm-systemtray--refresh-all))) + +(defun exwm-systemtray--exit () + "Exit the systemtray module." + (exwm--log) + (when exwm-systemtray--connection + (when (slot-value exwm-systemtray--connection 'connected) + ;; Hide & reparent out the embedder before disconnection to prevent + ;; embedded icons from being reparented to an Emacs frame (which is the + ;; parent of the embedder). + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:UnmapWindow + :window exwm-systemtray--embedder-window)) + (xcb:+request exwm-systemtray--connection + (make-instance 'xcb:ReparentWindow + :window exwm-systemtray--embedder-window + :parent exwm--root + :x 0 + :y 0)) + (xcb:disconnect exwm-systemtray--connection)) + (setq exwm-systemtray--connection nil + exwm-systemtray--list nil + exwm-systemtray--selection-owner-window nil + exwm-systemtray--embedder-window nil + exwm-systemtray--embedder-window-depth nil) + (remove-hook 'exwm-workspace-switch-hook + #'exwm-systemtray--on-workspace-switch) + (remove-hook 'exwm-workspace--update-workareas-hook + #'exwm-systemtray--refresh-all) + (remove-hook 'enable-theme-functions #'exwm-systemtray--on-theme-change) + (remove-hook 'disable-theme-functions #'exwm-systemtray--on-theme-change) + (remove-hook 'menu-bar-mode-hook #'exwm-systemtray--refresh-all) + (remove-hook 'tool-bar-mode-hook #'exwm-systemtray--refresh-all) + (when (boundp 'exwm-randr-refresh-hook) + (remove-hook 'exwm-randr-refresh-hook #'exwm-systemtray--refresh-all)))) + +(defun exwm-systemtray-enable () + "Enable system tray support for EXWM." + (exwm--log) + (add-hook 'exwm-init-hook #'exwm-systemtray--init) + (add-hook 'exwm-exit-hook #'exwm-systemtray--exit)) + + + +(provide 'exwm-systemtray) + +;;; exwm-systemtray.el ends here diff --git a/exwm-workspace.el b/exwm-workspace.el new file mode 100644 index 0000000000..89be697159 --- /dev/null +++ b/exwm-workspace.el @@ -0,0 +1,1768 @@ +;;; exwm-workspace.el --- Workspace Module for EXWM -*- lexical-binding: t -*- + +;; Copyright (C) 1015-2024 Free Software Foundation, Inc. + +;; Author: Chris Feng <chris.w.feng@gmail.com> + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This module adds workspace support for EXWM. + +;;; Code: + +(require 'server) + +(require 'exwm-core) + +(defgroup exwm-workspace nil + "Workspace." + :group 'exwm) + +(defcustom exwm-workspace-switch-hook nil + "Normal hook run after switching workspace." + :type 'hook) + +(defcustom exwm-workspace-list-change-hook nil + "Normal hook run when the workspace list is changed. +This happens when a workspace is added, deleted, moved, etc." + :type 'hook) + +(defcustom exwm-workspace-show-all-buffers nil + "Non-nil to show buffers on other workspaces." + :type 'boolean) + +(defcustom exwm-workspace-warp-cursor nil + "Non-nil to warp cursor automatically after workspace switch." + :type 'boolean) + +(defcustom exwm-workspace-number 1 + "Initial number of workspaces." + :type 'integer) + +(defcustom exwm-workspace-index-map #'number-to-string + "Function for mapping a workspace index to a string for display. + +By default `number-to-string' is applied which yields 0 1 2 ... ." + :type 'function) + +(defcustom exwm-workspace-minibuffer-position nil + "Position of the minibuffer frame. + +A restart is required for this change to take effect." + :type '(choice (const :tag "Bottom (fixed)" nil) + (const :tag "Bottom (auto-hide)" bottom) + (const :tag "Top (auto-hide)" top))) + +(defcustom exwm-workspace-display-echo-area-timeout 1 + "Timeout for displaying echo area." + :type 'integer) + +(defcustom exwm-workspace-switch-create-limit 10 + "Number of workspaces `exwm-workspace-switch-create' is allowed to create." + :type 'integer) + +(defvar exwm-workspace-current-index 0 "Index of current active workspace.") + +(defvar exwm-workspace--attached-minibuffer-height 0 + "Height (in pixel) of the attached minibuffer. + +If the minibuffer is detached, this value is 0.") + +(defvar exwm-workspace--create-silently nil + "When non-nil workspaces are created in the background (not switched to). + +Please manually run the hook `exwm-workspace-list-change-hook' afterwards.") + +(defvar exwm-workspace--current nil "Current active workspace.") + +(defvar exwm-workspace--display-echo-area-timer nil + "Timer for auto-hiding echo area.") + +(defvar exwm-workspace--id-struts-alist nil "Alist of X window and struts.") + +(defvar exwm-workspace--fullscreen-frame-count 0 + "Count the fullscreen workspace frames.") + +(defvar exwm-workspace--list nil "List of all workspaces (Emacs frames).") + +(defvar exwm-workspace--minibuffer nil + "The minibuffer frame shared among all frames.") + +(defvar exwm-workspace--original-handle-focus-in + (symbol-function #'handle-focus-in)) +(defvar exwm-workspace--original-handle-focus-out + (symbol-function #'handle-focus-out)) + +(defvar exwm-workspace--prompt-add-allowed nil + "Non-nil to allow adding workspace from the prompt.") + +(defvar exwm-workspace--prompt-delete-allowed nil + "Non-nil to allow deleting workspace from the prompt.") + +(defvar exwm-workspace--struts nil "Areas occupied by struts.") + +(defvar exwm-workspace--switch-history nil + "History for `read-from-minibuffer' to interactively switch workspace.") + +(defvar exwm-workspace--switch-history-outdated nil + "Non-nil to indicate `exwm-workspace--switch-history' is outdated.") + +(defvar exwm-workspace--timer nil "Timer used to track echo area changes.") + +(defvar exwm-workspace--update-workareas-hook nil + "Normal hook run when workareas get updated.") + +(defvar exwm-workspace--workareas nil "Workareas (struts excluded).") + +(defvar exwm-workspace--frame-y-offset 0 + "Offset between Emacs inner & outer frame in Y.") +(defvar exwm-workspace--window-y-offset 0 + "Offset between Emacs first window & outer frame in Y.") + +(defvar exwm-input--during-command) +(defvar exwm-input--event-hook) +(defvar exwm-layout-show-all-buffers) +(defvar exwm-manage--desktop) +(declare-function exwm-input--on-buffer-list-update "exwm-input.el" ()) +(declare-function exwm-layout--fullscreen-p "exwm-layout.el" ()) +(declare-function exwm-layout--hide "exwm-layout.el" (id)) +(declare-function exwm-layout--other-buffer-predicate "exwm-layout.el" + (buffer)) +(declare-function exwm-layout--refresh "exwm-layout.el") +(declare-function exwm-layout--show "exwm-layout.el" (id &optional window)) + +(defsubst exwm-workspace--position (frame) + "Retrieve index of given FRAME in workspace list. +NIL if FRAME is not a workspace." + (declare (indent defun)) + (cl-position frame exwm-workspace--list)) + +(defsubst exwm-workspace--count () + "Retrieve total number of workspaces." + (length exwm-workspace--list)) + +(defsubst exwm-workspace--workspace-p (frame) + "Return t if FRAME is a workspace." + (declare (indent defun)) + (memq frame exwm-workspace--list)) + +(defsubst exwm-workspace--workarea (frame) + "Return workarea corresponding to FRAME. +FRAME may be either a workspace frame or a workspace position." + (declare (indent defun)) + (elt exwm-workspace--workareas + (if (integerp frame) + frame + (exwm-workspace--position frame)))) + +(defvar exwm-workspace--switch-map nil + "Keymap used for interactively selecting workspace.") + +(defun exwm-workspace--init-switch-map () + "Initialize variable `exwm-workspace--switch-map'." + (let ((map (make-sparse-keymap))) + (define-key map [t] (lambda () (interactive))) + (define-key map "+" #'exwm-workspace--prompt-add) + (define-key map "-" #'exwm-workspace--prompt-delete) + (dotimes (i 10) + (define-key map (int-to-string i) + #'exwm-workspace--switch-map-nth-prefix)) + (unless (eq exwm-workspace-index-map #'number-to-string) + ;; Add extra (and possibly override) keys for selecting workspace. + (dotimes (i 10) + (let ((key (funcall exwm-workspace-index-map i))) + (when (and (stringp key) + (= (length key) 1) + (<= 0 (elt key 0) 127)) + (define-key map key + (lambda () + (interactive) + (exwm-workspace--switch-map-select-nth i))))))) + (define-key map "\C-a" (lambda () (interactive) (goto-history-element 1))) + (define-key map "\C-e" (lambda () + (interactive) + (goto-history-element (exwm-workspace--count)))) + (define-key map "\C-g" #'abort-recursive-edit) + (define-key map "\C-]" #'abort-recursive-edit) + (define-key map "\C-j" #'exit-minibuffer) + ;; (define-key map "\C-m" #'exit-minibuffer) ;not working + (define-key map [return] #'exit-minibuffer) + (define-key map " " #'exit-minibuffer) + (define-key map "\C-f" #'previous-history-element) + (define-key map "\C-b" #'next-history-element) + ;; Alternative keys + (define-key map [right] #'previous-history-element) + (define-key map [left] #'next-history-element) + (setq exwm-workspace--switch-map map))) + +(defun exwm-workspace--workspace-from-frame-or-index (frame-or-index) + "Retrieve the workspace frame from FRAME-OR-INDEX." + (cond + ((framep frame-or-index) + (unless (exwm-workspace--position frame-or-index) + (user-error "[EXWM] Frame is not a workspace %S" frame-or-index)) + frame-or-index) + ((integerp frame-or-index) + (unless (and (<= 0 frame-or-index) + (< frame-or-index (exwm-workspace--count))) + (user-error "[EXWM] Workspace index out of range: %d" frame-or-index)) + (elt exwm-workspace--list frame-or-index)) + (t (user-error "[EXWM] Invalid workspace: %s" frame-or-index)))) + +(defun exwm-workspace--prompt-for-workspace (&optional prompt) + "Prompt for a workspace, returning the workspace frame. +Show PROMPT to the user if non-nil." + (exwm-workspace--update-switch-history) + (let* ((current-idx (exwm-workspace--position exwm-workspace--current)) + (history-add-new-input nil) ;prevent modifying history + (history-idx (read-from-minibuffer + (or prompt "Workspace: ") + (elt exwm-workspace--switch-history current-idx) + exwm-workspace--switch-map nil + `(exwm-workspace--switch-history . ,(1+ current-idx)))) + (workspace-idx (cl-position history-idx exwm-workspace--switch-history + :test #'equal))) + (elt exwm-workspace--list workspace-idx))) + +(defun exwm-workspace--prompt-add () + "Add workspace from the prompt." + (interactive) + (when exwm-workspace--prompt-add-allowed + (let ((exwm-workspace--create-silently t)) + (make-frame) + (run-hooks 'exwm-workspace-list-change-hook)) + (exwm-workspace--update-switch-history) + (goto-history-element minibuffer-history-position))) + +(defun exwm-workspace--prompt-delete () + "Delete workspace from the prompt." + (interactive) + (when (and exwm-workspace--prompt-delete-allowed + (< 1 (exwm-workspace--count))) + (let ((frame (elt exwm-workspace--list (1- minibuffer-history-position)))) + (if (eq frame exwm-workspace--current) + ;; Abort the recursive minibuffer if deleting the current workspace. + (progn + (exwm--defer 0 #'delete-frame frame) + (abort-recursive-edit)) + (delete-frame frame) + (exwm-workspace--update-switch-history) + (goto-history-element (min minibuffer-history-position + (exwm-workspace--count))))))) + +(defun exwm-workspace--update-switch-history () + "Update the history for switching workspace to reflect the latest status." + (when exwm-workspace--switch-history-outdated + (setq exwm-workspace--switch-history-outdated nil) + (let* ((num (exwm-workspace--count)) + (sequence (number-sequence 0 (1- num))) + (not-empty (make-vector num nil))) + (dolist (i exwm--id-buffer-alist) + (with-current-buffer (cdr i) + (when exwm--frame + (setf (aref not-empty + (exwm-workspace--position exwm--frame)) + t)))) + (setq exwm-workspace--switch-history + (mapcar + (lambda (i) + (mapconcat + (lambda (j) + (format (if (= i j) "[%s]" " %s ") + (propertize + (apply exwm-workspace-index-map (list j)) + 'face + (cond ((frame-parameter (elt exwm-workspace--list j) + 'exwm-urgency) + '(:foreground "orange")) + ((aref not-empty j) '(:foreground "green")) + (t nil))))) + sequence "")) + sequence))))) + +;;;###autoload +(defun exwm-workspace--get-geometry (frame) + "Return the geometry of frame FRAME." + (or (frame-parameter frame 'exwm-geometry) + (make-instance 'xcb:RECTANGLE + :x 0 + :y 0 + :width (x-display-pixel-width) + :height (x-display-pixel-height)))) + +;;;###autoload +(defun exwm-workspace--current-height () + "Return the height of current workspace." + (let ((geometry (frame-parameter exwm-workspace--current 'exwm-geometry))) + (if geometry + (slot-value geometry 'height) + (x-display-pixel-height)))) + +;;;###autoload +(defun exwm-workspace--minibuffer-own-frame-p () + "Reports whether the minibuffer is displayed in its own frame." + (memq exwm-workspace-minibuffer-position '(top bottom))) + +(defun exwm-workspace--update-struts () + "Update `exwm-workspace--struts'." + (setq exwm-workspace--struts nil) + (let (struts struts*) + (dolist (pair exwm-workspace--id-struts-alist) + (setq struts (cdr pair)) + (when struts + (dotimes (i 4) + (when (/= 0 (aref struts i)) + (setq struts* + (vector (aref [left right top bottom] i) + (aref struts i) + (when (= 12 (length struts)) + (substring struts (+ 4 (* i 2)) (+ 6 (* i 2)))))) + (if (= 0 (mod i 2)) + ;; Make left/top processed first. + (push struts* exwm-workspace--struts) + (setq exwm-workspace--struts + (append exwm-workspace--struts (list struts*)))))))) + (exwm--log "%s" exwm-workspace--struts))) + +(defun exwm-workspace--update-workareas () + "Update `exwm-workspace--workareas'." + (let* ((root-width (x-display-pixel-width)) + (root-height (x-display-pixel-height)) + ;; Get workareas prior to struts. + (workareas (mapcar + (lambda (frame) + (if-let (rect (frame-parameter frame 'exwm-geometry)) + ;; Use the 'exwm-geometry' frame parameter if it + ;; exists. Make sure to clone it, will be modified + ;; below! + (clone rect) + ;; Fall back to use the screen size. + (make-instance 'xcb:RECTANGLE + :x 0 + :y 0 + :width root-width + :height root-height))) + exwm-workspace--list))) + ;; Exclude areas occupied by struts. + (dolist (struts exwm-workspace--struts) + (let* ((edge (aref struts 0)) + (size (aref struts 1)) + (position (aref struts 2)) + (beg (and position (aref position 0))) + (end (and position (aref position 1))) + delta) + (dolist (w workareas) + (with-slots (x y width height) w + (pcase edge + ;; Left and top are always processed first. + ('left + (setq delta (- size x)) + (when (and (< 0 delta) + (< delta width) + (or (not position) + (< (max beg y) + (min end (+ y height))))) + (cl-decf width delta) + (setf x size))) + ('right + (setq delta (- size (- root-width x width))) + (when (and (< 0 delta) + (< delta width) + (or (not position) + (< (max beg y) + (min end (+ y height))))) + (cl-decf width delta))) + ('top + (setq delta (- size y)) + (when (and (< 0 delta) + (< delta height) + (or (not position) + (< (max beg x) + (min end (+ x width))))) + (cl-decf height delta) + (setf y size))) + ('bottom + (setq delta (- size (- root-height y height))) + (when (and (< 0 delta) + (< delta height) + (or (not position) + (< (max beg x) + (min end (+ x width))))) + (cl-decf height delta)))))))) + ;; Save the result. + (setq exwm-workspace--workareas workareas) + (xcb:flush exwm--connection)) + (exwm--log "%s" exwm-workspace--workareas) + (run-hooks 'exwm-workspace--update-workareas-hook)) + +(defun exwm-workspace--update-offsets () + "Update `exwm-workspace--frame-y-offset'/`exwm-workspace--window-y-offset'." + (exwm--log) + (if (not (and exwm-workspace--list + (or menu-bar-mode tool-bar-mode))) + (setq exwm-workspace--frame-y-offset 0 + exwm-workspace--window-y-offset 0) + (redisplay t) + (let* ((frame (elt exwm-workspace--list 0)) + (edges (window-inside-absolute-pixel-edges (frame-first-window + frame)))) + (with-slots (y) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetGeometry + :drawable (frame-parameter frame + 'exwm-container))) + (with-slots ((y* y)) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetGeometry + :drawable (frame-parameter frame + 'exwm-outer-id))) + (with-slots ((y** y)) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetGeometry + :drawable (frame-parameter frame 'exwm-id))) + (setq exwm-workspace--frame-y-offset (- y** y*) + exwm-workspace--window-y-offset (- (elt edges 1) y)))))))) + +(defun exwm-workspace--set-active (frame active) + "Make frame FRAME active on its monitor. +ACTIVE indicates whether to set the frame active or inactive." + (exwm--log "active=%s; frame=%s" active frame) + (set-frame-parameter frame 'exwm-active active) + (if active + (exwm-workspace--set-fullscreen frame) + (exwm--set-geometry (frame-parameter frame 'exwm-container) nil nil 1 1)) + (exwm-layout--refresh frame) + (xcb:flush exwm--connection)) + +(defun exwm-workspace--active-p (frame) + "Return non-nil if FRAME is active." + (frame-parameter frame 'exwm-active)) + +(defun exwm-workspace--set-fullscreen (frame) + "Make frame FRAME fullscreen according to `exwm-workspace--workareas'." + (exwm--log "frame=%s" frame) + (let ((id (frame-parameter frame 'exwm-outer-id)) + (container (frame-parameter frame 'exwm-container))) + (with-slots (x y width height) + (exwm-workspace--workarea frame) + (exwm--log "x=%s; y=%s; w=%s; h=%s" x y width height) + (when (and (eq frame exwm-workspace--current) + (exwm-workspace--minibuffer-own-frame-p)) + (exwm-workspace--resize-minibuffer-frame)) + (if (exwm-workspace--active-p frame) + (exwm--set-geometry container x y width height) + (exwm--set-geometry container x y 1 1)) + (exwm--set-geometry id nil nil width height) + (xcb:flush exwm--connection))) + ;; This is only used for workspace initialization. + (when exwm-workspace--fullscreen-frame-count + (cl-incf exwm-workspace--fullscreen-frame-count))) + +(defun exwm-workspace--resize-minibuffer-frame () + "Resize minibuffer (and its container) to fit the size of workspace." + (cl-assert (exwm-workspace--minibuffer-own-frame-p)) + (let ((workarea (exwm-workspace--workarea exwm-workspace-current-index)) + (container (frame-parameter exwm-workspace--minibuffer + 'exwm-container)) + y width) + (setq y (if (eq exwm-workspace-minibuffer-position 'top) + (- (slot-value workarea 'y) + exwm-workspace--attached-minibuffer-height) + ;; Reset the frame size. + (set-frame-height exwm-workspace--minibuffer 1) + (redisplay) ;FIXME. + (+ (slot-value workarea 'y) (slot-value workarea 'height) + (- (frame-pixel-height exwm-workspace--minibuffer)) + exwm-workspace--attached-minibuffer-height)) + width (slot-value workarea 'width)) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window container + :value-mask (logior xcb:ConfigWindow:X + xcb:ConfigWindow:Y + xcb:ConfigWindow:Width + (if exwm-manage--desktop + xcb:ConfigWindow:Sibling + 0) + xcb:ConfigWindow:StackMode) + :x (slot-value workarea 'x) + :y y + :width width + :sibling exwm-manage--desktop + :stack-mode (if exwm-manage--desktop + xcb:StackMode:Above + xcb:StackMode:Below))) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window (frame-parameter exwm-workspace--minibuffer + 'exwm-outer-id) + :value-mask xcb:ConfigWindow:Width + :width width)) + (exwm--log "y: %s, width: %s" y width))) + +(defun exwm-workspace--switch-map-nth-prefix (&optional prefix-digits) + "Allow selecting a workspace by number. + +PREFIX-DIGITS is a list of the digits introduced so far." + (interactive) + (let* ((k (aref (substring (this-command-keys-vector) -1) 0)) + (d (- k ?0)) + ;; Convert prefix-digits to number. For example, '(2 1) to 120. + (o 1) + (pn (apply #'+ (mapcar (lambda (x) + (setq o (* 10 o)) + (* o x)) + prefix-digits))) + (n (+ pn d)) + prefix-length index-max index-length) + (if (or (= n 0) + (> n + (setq index-max (1- (exwm-workspace--count)))) + (>= (setq prefix-length (length prefix-digits)) + (setq index-length (floor (log index-max 10)))) + ;; Check if it's still possible to do a match. + (> (* n (expt 10 (- index-length prefix-length))) + index-max)) + (exwm-workspace--switch-map-select-nth n) + ;; Go ahead if there are enough digits to select any workspace. + (set-transient-map + (let ((map (make-sparse-keymap)) + (cmd (let ((digits (cons d prefix-digits))) + (lambda () + (interactive) + (exwm-workspace--switch-map-nth-prefix digits))))) + (dotimes (i 10) + (define-key map (int-to-string i) cmd)) + ;; Accept + (define-key map [return] + (lambda () + (interactive) + (exwm-workspace--switch-map-select-nth n))) + map))))) + +(defun exwm-workspace--switch-map-select-nth (n) + "Select Nth workspace." + (interactive) + (goto-history-element (1+ n)) + (exit-minibuffer)) + +;;;###autoload +(defun exwm-workspace-switch (frame-or-index &optional force) + "Switch to workspace FRAME-OR-INDEX (0-based). + +Query for the index if not specified when called interactively. Passing a +workspace frame as the first option or making use of the rest options are +for internal use only. + +When FORCE is true, allow switching to current workspace." + (interactive + (list + (cond + ((null current-prefix-arg) + (unless (and (derived-mode-p 'exwm-mode) + ;; The prompt is invisible in fullscreen mode. + (exwm-layout--fullscreen-p)) + (let ((exwm-workspace--prompt-add-allowed t) + (exwm-workspace--prompt-delete-allowed t)) + (exwm-workspace--prompt-for-workspace "Switch to [+/-]: ")))) + ((and (integerp current-prefix-arg) + (<= 0 current-prefix-arg (exwm-workspace--count))) + current-prefix-arg) + (t 0)))) + (exwm--log) + (let* ((frame (exwm-workspace--workspace-from-frame-or-index frame-or-index)) + (old-frame exwm-workspace--current) + (index (exwm-workspace--position frame)) + (window (frame-parameter frame 'exwm-selected-window))) + (when (or force (not (eq frame exwm-workspace--current))) + (unless (window-live-p window) + (setq window (frame-selected-window frame))) + (when (and (not (eq frame old-frame)) + (frame-live-p old-frame)) + (with-selected-frame old-frame + (funcall exwm-workspace--original-handle-focus-out + (list 'focus-out frame)))) + ;; Raise this frame. + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window (frame-parameter frame 'exwm-container) + :value-mask (logior xcb:ConfigWindow:Sibling + xcb:ConfigWindow:StackMode) + :sibling exwm--guide-window + :stack-mode xcb:StackMode:Below)) + (setq exwm-workspace--current frame + exwm-workspace-current-index index) + (unless (exwm-workspace--workspace-p (selected-frame)) + ;; Save the floating frame window selected on the previous workspace. + (set-frame-parameter (buffer-local-value 'exwm--frame (window-buffer)) + 'exwm-selected-window (selected-window))) + ;; Show/Hide X windows. + (let ((monitor-old (frame-parameter old-frame 'exwm-randr-monitor)) + (monitor-new (frame-parameter frame 'exwm-randr-monitor)) + (active-old (exwm-workspace--active-p old-frame)) + (active-new (exwm-workspace--active-p frame)) + workspaces-to-hide) + (cond + ((not active-old) + (exwm-workspace--set-active frame t)) + ((equal monitor-old monitor-new) + (exwm-workspace--set-active frame t) + (unless (eq frame old-frame) + (exwm-workspace--set-active old-frame nil) + (setq workspaces-to-hide (list old-frame)))) + (active-new) + (t + (dolist (w exwm-workspace--list) + (when (and (exwm-workspace--active-p w) + (equal monitor-new + (frame-parameter w 'exwm-randr-monitor))) + (exwm-workspace--set-active w nil) + (setq workspaces-to-hide (append workspaces-to-hide (list w))))) + (exwm-workspace--set-active frame t))) + (dolist (i exwm--id-buffer-alist) + (with-current-buffer (cdr i) + (if (memq exwm--frame workspaces-to-hide) + (exwm-layout--hide exwm--id) + (when (eq frame exwm--frame) + (let ((window (get-buffer-window nil t))) + (when window + (exwm-layout--show exwm--id window)))))))) + (select-window window) + (x-focus-frame (window-frame window)) ;The real input focus. + (set-frame-parameter frame 'exwm-selected-window nil) + (if (exwm-workspace--minibuffer-own-frame-p) + ;; Resize the minibuffer frame. + (exwm-workspace--resize-minibuffer-frame) + ;; Set a default minibuffer frame. + (setq default-minibuffer-frame frame)) + ;; Hide windows in other workspaces by preprending a space + (unless exwm-workspace-show-all-buffers + (dolist (i exwm--id-buffer-alist) + (with-current-buffer (cdr i) + (let ((name (replace-regexp-in-string "^\\s-*" "" + (buffer-name)))) + (exwm-workspace-rename-buffer (if (eq frame exwm--frame) + name + (concat " " name))))))) + ;; Update demands attention flag + (set-frame-parameter frame 'exwm-urgency nil) + ;; Update switch workspace history + (setq exwm-workspace--switch-history-outdated t) + ;; Set _NET_CURRENT_DESKTOP + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_CURRENT_DESKTOP + :window exwm--root :data index)) + (xcb:flush exwm--connection)) + (when exwm-workspace-warp-cursor + (with-slots (win-x win-y) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:QueryPointer + :window (frame-parameter frame + 'exwm-outer-id))) + (when (or (< win-x 0) + (< win-y 0) + (> win-x (frame-pixel-width frame)) + (> win-y (frame-pixel-height frame))) + (xcb:+request exwm--connection + (make-instance 'xcb:WarpPointer + :src-window xcb:Window:None + :dst-window (frame-parameter frame + 'exwm-outer-id) + :src-x 0 + :src-y 0 + :src-width 0 + :src-height 0 + :dst-x (/ (frame-pixel-width frame) 2) + :dst-y (/ (frame-pixel-height frame) 2))) + (xcb:flush exwm--connection)))) + (funcall exwm-workspace--original-handle-focus-in (list 'focus-in frame)) + (run-hooks 'exwm-workspace-switch-hook))) + +;;;###autoload +(defun exwm-workspace-switch-create (frame-or-index) + "Switch to workspace FRAME-OR-INDEX creating it first non-existent. + +Passing a workspace frame as the first option is for internal use only." + (interactive + (list + (cond + ((integerp current-prefix-arg) + current-prefix-arg) + (t 0)))) + (unless frame-or-index + (setq frame-or-index 0)) + (exwm--log "%s" frame-or-index) + (if (or (framep frame-or-index) + (< frame-or-index (exwm-workspace--count))) + (exwm-workspace-switch frame-or-index) + (let ((exwm-workspace--create-silently t)) + (dotimes (_ (min exwm-workspace-switch-create-limit + (1+ (- frame-or-index + (exwm-workspace--count))))) + (make-frame)) + (run-hooks 'exwm-workspace-list-change-hook)) + (exwm-workspace-switch frame-or-index))) + +;;;###autoload +(defun exwm-workspace-swap (workspace1 workspace2) + "Interchange position of WORKSPACE1 with that of WORKSPACE2." + (interactive + (unless (and (derived-mode-p 'exwm-mode) + ;; The prompt is invisible in fullscreen mode. + (exwm-layout--fullscreen-p)) + (let (w1 w2) + (let ((exwm-workspace--prompt-add-allowed t) + (exwm-workspace--prompt-delete-allowed t)) + (setq w1 (exwm-workspace--prompt-for-workspace + "Pick a workspace [+/-]: "))) + (setq w2 (exwm-workspace--prompt-for-workspace + (format "Swap workspace %d with: " + (exwm-workspace--position w1)))) + (list w1 w2)))) + (exwm--log) + (let ((pos1 (exwm-workspace--position workspace1)) + (pos2 (exwm-workspace--position workspace2))) + (if (or (not pos1) (not pos2) (= pos1 pos2)) + (user-error "[EXWM] Cannot swap %s and %s" workspace1 workspace2) + (setf (elt exwm-workspace--list pos1) workspace2) + (setf (elt exwm-workspace--list pos2) workspace1) + ;; Update the _NET_WM_DESKTOP property of each X window affected. + (dolist (pair exwm--id-buffer-alist) + (when (memq (buffer-local-value 'exwm--frame (cdr pair)) + (list workspace1 workspace2)) + (exwm-workspace--set-desktop (car pair)))) + (xcb:flush exwm--connection) + (when (memq exwm-workspace--current (list workspace1 workspace2)) + ;; With the current workspace involved, lots of stuffs need refresh. + (set-frame-parameter exwm-workspace--current 'exwm-selected-window + (selected-window)) + (exwm-workspace-switch exwm-workspace--current t)) + (run-hooks 'exwm-workspace-list-change-hook)))) + +;;;###autoload +(defun exwm-workspace-move (workspace nth) + "Move WORKSPACE to the NTH position. + +When called interactively, prompt for a workspace and move current one just +before it." + (interactive + (cond + ((null current-prefix-arg) + (unless (and (derived-mode-p 'exwm-mode) + ;; The prompt is invisible in fullscreen mode. + (exwm-layout--fullscreen-p)) + (list exwm-workspace--current + (exwm-workspace--position + (exwm-workspace--prompt-for-workspace "Move workspace to: "))))) + ((and (integerp current-prefix-arg) + (<= 0 current-prefix-arg (exwm-workspace--count))) + (list exwm-workspace--current current-prefix-arg)) + (t (list exwm-workspace--current 0)))) + (exwm--log) + (let ((pos (exwm-workspace--position workspace)) + flag start end index) + (if (= nth pos) + (user-error "[EXWM] Cannot move to same position") + ;; Set if the current workspace is involved. + (setq flag (or (eq workspace exwm-workspace--current) + (eq (elt exwm-workspace--list nth) + exwm-workspace--current))) + ;; Do the move. + (with-no-warnings ;For Emacs 24. + (pop (nthcdr pos exwm-workspace--list))) + (push workspace (nthcdr nth exwm-workspace--list)) + ;; Update the _NET_WM_DESKTOP property of each X window affected. + (setq start (min pos nth) + end (max pos nth)) + (dolist (pair exwm--id-buffer-alist) + (setq index (exwm-workspace--position + (buffer-local-value 'exwm--frame (cdr pair)))) + (unless (or (< index start) (> index end)) + (exwm-workspace--set-desktop (car pair)))) + (when flag + ;; With the current workspace involved, lots of stuffs need refresh. + (set-frame-parameter exwm-workspace--current 'exwm-selected-window + (selected-window)) + (exwm-workspace-switch exwm-workspace--current t)) + (run-hooks 'exwm-workspace-list-change-hook)))) + +;;;###autoload +(defun exwm-workspace-add (&optional index) + "Add a workspace as the INDEX-th workspace, or the last one if INDEX is nil. + +INDEX must not exceed the current number of workspaces." + (interactive) + (exwm--log "%s" index) + (if (and index + ;; No need to move if it's the last one. + (< index (exwm-workspace--count))) + (exwm-workspace-move (make-frame) index) + (make-frame))) + +;;;###autoload +(defun exwm-workspace-delete (&optional frame-or-index) + "Delete the workspace FRAME-OR-INDEX." + (interactive) + (exwm--log "%s" frame-or-index) + (when (< 1 (exwm-workspace--count)) + (let ((frame (if frame-or-index + (exwm-workspace--workspace-from-frame-or-index + frame-or-index) + exwm-workspace--current))) + (delete-frame frame)))) + +(defun exwm-workspace--set-desktop (id) + "Set _NET_WM_DESKTOP for X window ID." + (exwm--log "#x%x" id) + (with-current-buffer (exwm--id->buffer id) + (let ((desktop (exwm-workspace--position exwm--frame))) + (setq exwm--desktop desktop) + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_WM_DESKTOP + :window id + :data desktop))))) + +;;;###autoload +(cl-defun exwm-workspace-move-window (frame-or-index &optional id) + "Move window ID to workspace FRAME-OR-INDEX." + (interactive (list + (cond + ((null current-prefix-arg) + (let ((exwm-workspace--prompt-add-allowed t) + (exwm-workspace--prompt-delete-allowed t)) + (exwm-workspace--prompt-for-workspace "Move to [+/-]: "))) + ((and (integerp current-prefix-arg) + (<= 0 current-prefix-arg (exwm-workspace--count))) + current-prefix-arg) + (t 0)))) + (let ((frame (exwm-workspace--workspace-from-frame-or-index frame-or-index)) + old-frame container) + (unless id (setq id (exwm--buffer->id (window-buffer)))) + (unless id + (cl-return-from exwm-workspace-move-window)) + (exwm--log "Moving #x%x to %s" id frame-or-index) + (with-current-buffer (exwm--id->buffer id) + (unless (eq exwm--frame frame) + (unless exwm-workspace-show-all-buffers + (let ((name (replace-regexp-in-string "^\\s-*" "" (buffer-name)))) + (exwm-workspace-rename-buffer + (if (eq frame exwm-workspace--current) + name + (concat " " name))))) + (setq old-frame exwm--frame + exwm--frame frame) + (if (not exwm--floating-frame) + ;; Tiling. + (if (get-buffer-window nil frame) + (when (eq frame exwm-workspace--current) + (exwm-layout--refresh frame)) + (set-window-buffer (get-buffer-window nil t) + (other-buffer nil t)) + (unless (eq frame exwm-workspace--current) + ;; Clear the 'exwm-selected-window' frame parameter. + (set-frame-parameter frame 'exwm-selected-window nil)) + (set-window-buffer (frame-selected-window frame) + (exwm--id->buffer id)) + (if (eq frame exwm-workspace--current) + (select-window (frame-selected-window frame)) + (unless (exwm-workspace--active-p frame) + (exwm-layout--hide id)))) + ;; Floating. + (setq container (frame-parameter exwm--floating-frame + 'exwm-container)) + (unless (equal (frame-parameter old-frame 'exwm-randr-monitor) + (frame-parameter frame 'exwm-randr-monitor)) + (with-slots (x y) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetGeometry + :drawable container)) + (with-slots ((x1 x) + (y1 y)) + (exwm-workspace--get-geometry old-frame) + (with-slots ((x2 x) + (y2 y)) + (exwm-workspace--get-geometry frame) + (setq x (+ x (- x2 x1)) + y (+ y (- y2 y1))))) + (exwm--set-geometry id x y nil nil) + (exwm--set-geometry container x y nil nil))) + (if (exwm-workspace--minibuffer-own-frame-p) + (if (eq frame exwm-workspace--current) + (select-window (frame-root-window exwm--floating-frame)) + (select-window (frame-selected-window exwm-workspace--current)) + (unless (exwm-workspace--active-p frame) + (exwm-layout--hide id))) + ;; The frame needs to be recreated since it won't use the + ;; minibuffer on the new workspace. + ;; The code is mostly copied from `exwm-floating--set-floating'. + (let* ((old-frame exwm--floating-frame) + (new-frame + (with-current-buffer + (or (get-buffer "*scratch*") + (progn + (set-buffer-major-mode + (get-buffer-create "*scratch*")) + (get-buffer "*scratch*"))) + (make-frame + `((minibuffer . ,(minibuffer-window frame)) + (left . ,(* window-min-width -100)) + (top . ,(* window-min-height -100)) + (width . ,window-min-width) + (height . ,window-min-height) + (unsplittable . t))))) + (outer-id (string-to-number + (frame-parameter new-frame + 'outer-window-id))) + (window-id (string-to-number + (frame-parameter new-frame 'window-id))) + (window (frame-root-window new-frame))) + (set-frame-parameter new-frame 'exwm-outer-id outer-id) + (set-frame-parameter new-frame 'exwm-id window-id) + (set-frame-parameter new-frame 'exwm-container container) + (make-frame-invisible new-frame) + (set-frame-size new-frame + (frame-pixel-width old-frame) + (frame-pixel-height old-frame) + t) + (xcb:+request exwm--connection + (make-instance 'xcb:ReparentWindow + :window outer-id + :parent container + :x 0 :y 0)) + (xcb:flush exwm--connection) + (with-current-buffer (exwm--id->buffer id) + (setq window-size-fixed nil + exwm--floating-frame new-frame) + (set-window-dedicated-p (frame-root-window old-frame) nil) + (remove-hook 'window-configuration-change-hook + #'exwm-layout--refresh) + (set-window-buffer window (current-buffer)) + (add-hook 'window-configuration-change-hook + #'exwm-layout--refresh) + (set-window-dedicated-p window t)) + ;; Select a tiling window and delete the old frame. + (select-window (frame-selected-window exwm-workspace--current)) + (delete-frame old-frame) + ;; The rest is the same. + (make-frame-visible new-frame) + (exwm--set-geometry outer-id 0 0 nil nil) + (xcb:flush exwm--connection) + (redisplay) + (if (eq frame exwm-workspace--current) + (with-current-buffer (exwm--id->buffer id) + (select-window (frame-root-window exwm--floating-frame))) + (unless (exwm-workspace--active-p frame) + (exwm-layout--hide id))))) + ;; Update the 'exwm-selected-window' frame parameter. + (when (not (eq frame exwm-workspace--current)) + (with-current-buffer (exwm--id->buffer id) + (set-frame-parameter frame 'exwm-selected-window + (frame-root-window + exwm--floating-frame))))) + ;; Set _NET_WM_DESKTOP. + (exwm-workspace--set-desktop id) + (xcb:flush exwm--connection))) + (setq exwm-workspace--switch-history-outdated t))) + +;;;###autoload +(defun exwm-workspace-switch-to-buffer (buffer-or-name) + "Make selected window display BUFFER-OR-NAME." + (interactive + (let ((inhibit-quit t)) + ;; Show all buffers + (unless exwm-workspace-show-all-buffers + (dolist (pair exwm--id-buffer-alist) + (with-current-buffer (cdr pair) + (when (= ?\s (aref (buffer-name) 0)) + (let ((buffer-list-update-hook + (remq #'exwm-input--on-buffer-list-update + buffer-list-update-hook))) + (rename-buffer (substring (buffer-name) 1))))))) + (prog1 + (with-local-quit + (list (get-buffer (read-buffer-to-switch "Switch to buffer: ")))) + ;; Hide buffers on other workspaces + (unless exwm-workspace-show-all-buffers + (dolist (pair exwm--id-buffer-alist) + (with-current-buffer (cdr pair) + (unless (or (eq exwm--frame exwm-workspace--current) + (= ?\s (aref (buffer-name) 0))) + (let ((buffer-list-update-hook + (remq #'exwm-input--on-buffer-list-update + buffer-list-update-hook))) + (rename-buffer (concat " " (buffer-name))))))))))) + (exwm--log) + (when buffer-or-name + (with-current-buffer buffer-or-name + (if (derived-mode-p 'exwm-mode) + ;; EXWM buffer. + (if (eq exwm--frame exwm-workspace--current) + ;; On the current workspace. + (if (not exwm--floating-frame) + (switch-to-buffer buffer-or-name) + ;; Select the floating frame. + (select-frame-set-input-focus exwm--floating-frame) + (select-window (frame-root-window exwm--floating-frame))) + ;; On another workspace. + (if exwm-layout-show-all-buffers + (exwm-workspace-move-window exwm-workspace--current + exwm--id) + (let ((window (get-buffer-window buffer-or-name exwm--frame))) + (if window + (set-frame-parameter exwm--frame + 'exwm-selected-window window) + (set-window-buffer (frame-selected-window exwm--frame) + buffer-or-name))) + (exwm-workspace-switch exwm--frame))) + ;; Ordinary buffer. + (switch-to-buffer buffer-or-name))))) + +(defun exwm-workspace-rename-buffer (newname) + "Rename current buffer to NEWNAME." + (let ((hidden (= ?\s (aref newname 0))) + (basename (replace-regexp-in-string "<[0-9]+>$" "" newname)) + (counter 1) + tmp) + (when hidden (setq basename (substring basename 1))) + (setq newname basename) + (while (and (setq tmp (or (get-buffer newname) + (get-buffer (concat " " newname)))) + (not (eq tmp (current-buffer)))) + (setq newname (format "%s<%d>" basename (cl-incf counter)))) + (let ((buffer-list-update-hook + (remq #'exwm-input--on-buffer-list-update + buffer-list-update-hook))) + (rename-buffer (concat (and hidden " ") newname))))) + +(defun exwm-workspace--x-create-frame (orig-x-create-frame params) + "Set override-redirect on the frame created by `x-create-frame'. +ORIG-X-CREATE-FRAME is the advised function `x-create-frame'. +PARAMS are the original arguments." + (exwm--log) + (let ((frame (funcall orig-x-create-frame params))) + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window (string-to-number + (frame-parameter frame 'outer-window-id)) + :value-mask xcb:CW:OverrideRedirect + :override-redirect 1)) + (xcb:flush exwm--connection) + frame)) + +(defsubst exwm-workspace--minibuffer-attached-p () + "Return non-nil if the minibuffer is attached. + +Please check `exwm-workspace--minibuffer-own-frame-p' first." + (assq (frame-parameter exwm-workspace--minibuffer 'exwm-container) + exwm-workspace--id-struts-alist)) + +;;;###autoload +(defun exwm-workspace-attach-minibuffer () + "Attach the minibuffer making it always visible." + (interactive) + (exwm--log) + (when (and (exwm-workspace--minibuffer-own-frame-p) + (not (exwm-workspace--minibuffer-attached-p))) + ;; Reset the frame size. + (set-frame-height exwm-workspace--minibuffer 1) + (redisplay) ;FIXME. + (setq exwm-workspace--attached-minibuffer-height + (frame-pixel-height exwm-workspace--minibuffer)) + (exwm-workspace--show-minibuffer) + (let ((container (frame-parameter exwm-workspace--minibuffer + 'exwm-container))) + (push (cons container + (if (eq exwm-workspace-minibuffer-position 'top) + (vector 0 0 exwm-workspace--attached-minibuffer-height 0) + (vector 0 0 0 exwm-workspace--attached-minibuffer-height))) + exwm-workspace--id-struts-alist) + (exwm-workspace--update-struts) + (exwm-workspace--update-workareas) + (dolist (f exwm-workspace--list) + (exwm-workspace--set-fullscreen f))))) + +;;;###autoload +(defun exwm-workspace-detach-minibuffer () + "Detach the minibuffer so that it automatically hides." + (interactive) + (exwm--log) + (when (and (exwm-workspace--minibuffer-own-frame-p) + (exwm-workspace--minibuffer-attached-p)) + (setq exwm-workspace--attached-minibuffer-height 0) + (let ((container (frame-parameter exwm-workspace--minibuffer + 'exwm-container))) + (setq exwm-workspace--id-struts-alist + (assq-delete-all container exwm-workspace--id-struts-alist)) + (exwm-workspace--update-struts) + (exwm-workspace--update-workareas) + (dolist (f exwm-workspace--list) + (exwm-workspace--set-fullscreen f)) + (exwm-workspace--hide-minibuffer)))) + +;;;###autoload +(defun exwm-workspace-toggle-minibuffer () + "Attach the minibuffer if it's detached, or detach it if it's attached." + (interactive) + (exwm--log) + (when (exwm-workspace--minibuffer-own-frame-p) + (if (exwm-workspace--minibuffer-attached-p) + (exwm-workspace-detach-minibuffer) + (exwm-workspace-attach-minibuffer)))) + +(defun exwm-workspace--update-minibuffer-height (&optional echo-area) + "Update the minibuffer frame height. +When ECHO-AREA is non-nil, take the size of the echo area into +account when calculating the height." + (when (exwm--terminal-p) + (let ((height + (with-current-buffer + (window-buffer (minibuffer-window exwm-workspace--minibuffer)) + (max 1 + (if echo-area + (let ((width (frame-width exwm-workspace--minibuffer)) + (result 0)) + (mapc (lambda (i) + (setq result + (+ result + (ceiling (1+ (length i)) width)))) + (split-string (or (current-message) "") "\n")) + result) + (count-screen-lines)))))) + (when (and (integerp max-mini-window-height) + (> height max-mini-window-height)) + (setq height max-mini-window-height)) + (exwm--log "%s" height) + (set-frame-height exwm-workspace--minibuffer height)))) + +(defun exwm-workspace--on-ConfigureNotify (data _synthetic) + "Adjust the container to fit the minibuffer frame. +DATA contains unmarshalled ConfigureNotify event data." + (let ((obj (make-instance 'xcb:ConfigureNotify)) y) + (xcb:unmarshal obj data) + (with-slots (window height) obj + (when (eq (frame-parameter exwm-workspace--minibuffer 'exwm-outer-id) + window) + (exwm--log) + (when (and (floatp max-mini-window-height) + (> height (* max-mini-window-height + (exwm-workspace--current-height)))) + (setq height (floor + (* max-mini-window-height + (exwm-workspace--current-height)))) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window window + :value-mask xcb:ConfigWindow:Height + :height height))) + (when (/= (exwm-workspace--count) (length exwm-workspace--workareas)) + ;; There is a chance the workareas are not updated timely. + (exwm-workspace--update-workareas)) + (with-slots ((y* y) (height* height)) + (exwm-workspace--workarea exwm-workspace-current-index) + (setq y (if (eq exwm-workspace-minibuffer-position 'top) + (- y* + exwm-workspace--attached-minibuffer-height) + (+ y* height* (- height) + exwm-workspace--attached-minibuffer-height)))) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window (frame-parameter exwm-workspace--minibuffer + 'exwm-container) + :value-mask (logior xcb:ConfigWindow:Y + xcb:ConfigWindow:Height) + :y y + :height height)) + (xcb:flush exwm--connection))))) + +(defun exwm-workspace--display-buffer (buffer alist) + "Display BUFFER as if the current workspace were selected. +ALIST is an action alist, as accepted by function `display-buffer'." + ;; Only when the floating minibuffer frame is selected. + ;; This also protect this functions from being recursively called. + (when (eq (selected-frame) exwm-workspace--minibuffer) + (with-selected-frame exwm-workspace--current + (display-buffer buffer alist)))) + +(defun exwm-workspace--show-minibuffer () + "Show the minibuffer frame." + (exwm--log) + ;; Cancel pending timer. + (when exwm-workspace--display-echo-area-timer + (cancel-timer exwm-workspace--display-echo-area-timer) + (setq exwm-workspace--display-echo-area-timer nil)) + ;; Show the minibuffer frame. + (unless (exwm-workspace--minibuffer-attached-p) + (exwm--set-geometry (frame-parameter exwm-workspace--minibuffer + 'exwm-container) + nil nil + (frame-pixel-width exwm-workspace--minibuffer) + (frame-pixel-height exwm-workspace--minibuffer))) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window (frame-parameter exwm-workspace--minibuffer + 'exwm-container) + :value-mask xcb:ConfigWindow:StackMode + :stack-mode xcb:StackMode:Above)) + (xcb:flush exwm--connection)) + +(defun exwm-workspace--hide-minibuffer () + "Hide the minibuffer frame." + (exwm--log) + ;; Hide the minibuffer frame. + (if (exwm-workspace--minibuffer-attached-p) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window (frame-parameter exwm-workspace--minibuffer + 'exwm-container) + :value-mask (logior (if exwm-manage--desktop + xcb:ConfigWindow:Sibling + 0) + xcb:ConfigWindow:StackMode) + :sibling exwm-manage--desktop + :stack-mode (if exwm-manage--desktop + xcb:StackMode:Above + xcb:StackMode:Below))) + (exwm--set-geometry (frame-parameter exwm-workspace--minibuffer + 'exwm-container) + nil nil 1 1)) + (xcb:flush exwm--connection)) + +(defun exwm-workspace--on-minibuffer-setup () + "Run in `minibuffer-setup-hook' to show the minibuffer and its container." + (exwm--log) + (when (and (= 1 (minibuffer-depth)) + (exwm--terminal-p)) + (add-hook 'post-command-hook #'exwm-workspace--update-minibuffer-height) + (exwm-workspace--show-minibuffer)) + ;; FIXME: This is a temporary fix for the *Completions* buffer not + ;; being correctly fitted by its displaying window. As with + ;; `exwm-workspace--display-buffer', the problem is caused by + ;; the fact that the minibuffer (rather than the workspace) + ;; frame is the 'selected frame'. `get-buffer-window' will + ;; fail to retrieve the correct window. It's likely there are + ;; other related issues. + ;; This is not required by Emacs 24. + (when (fboundp 'window-preserve-size) + (let ((window (get-buffer-window "*Completions*" + exwm-workspace--current))) + (when window + (fit-window-to-buffer window) + (window-preserve-size window))))) + +(defun exwm-workspace--on-minibuffer-exit () + "Run in `minibuffer-exit-hook' to hide the minibuffer container." + (exwm--log) + (when (and (= 1 (minibuffer-depth)) + (exwm--terminal-p)) + (remove-hook 'post-command-hook #'exwm-workspace--update-minibuffer-height) + (exwm-workspace--hide-minibuffer))) + +(defun exwm-workspace--on-echo-area-dirty () + "Run when new message arrives to show the echo area and its container." + (when (and (not (active-minibuffer-window)) + (or (current-message) + cursor-in-echo-area) + (exwm--terminal-p)) + (exwm-workspace--update-minibuffer-height t) + (exwm-workspace--show-minibuffer) + (unless (or (not exwm-workspace-display-echo-area-timeout) + exwm-input--during-command ;e.g. read-event + input-method-use-echo-area) + (setq exwm-workspace--display-echo-area-timer + (run-with-timer exwm-workspace-display-echo-area-timeout nil + #'exwm-workspace--echo-area-maybe-clear))))) + +(defun exwm-workspace--echo-area-maybe-clear () + "Eventually clear the echo area container." + (exwm--log) + (if (not (current-message)) + (exwm-workspace--on-echo-area-clear) + ;; Reschedule. + (cancel-timer exwm-workspace--display-echo-area-timer) + (setq exwm-workspace--display-echo-area-timer + (run-with-timer exwm-workspace-display-echo-area-timeout nil + #'exwm-workspace--echo-area-maybe-clear)))) + +(defun exwm-workspace--on-echo-area-clear () + "Run in `echo-area-clear-hook' to hide echo area container." + (when (exwm--terminal-p) + (unless (active-minibuffer-window) + (exwm-workspace--hide-minibuffer)) + (when exwm-workspace--display-echo-area-timer + (cancel-timer exwm-workspace--display-echo-area-timer) + (setq exwm-workspace--display-echo-area-timer nil)))) + +(defun exwm-workspace--set-desktop-geometry () + "Set _NET_DESKTOP_GEOMETRY." + (exwm--log) + ;; We don't support large desktop so it's the same with screen size. + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_DESKTOP_GEOMETRY + :window exwm--root + :width (x-display-pixel-width) + :height (x-display-pixel-height)))) + +(defun exwm-workspace--add-frame-as-workspace (frame) + "Configure frame FRAME to be treated as a workspace." + (exwm--log "%s" frame) + (setq exwm-workspace--list (nconc exwm-workspace--list (list frame))) + (let ((outer-id (string-to-number (frame-parameter frame + 'outer-window-id))) + (window-id (string-to-number (frame-parameter frame 'window-id))) + (container (xcb:generate-id exwm--connection)) + frame-colormap frame-visual frame-depth) + ;; Save window IDs + (set-frame-parameter frame 'exwm-outer-id outer-id) + (set-frame-parameter frame 'exwm-id window-id) + (set-frame-parameter frame 'exwm-container container) + ;; Copy RandR frame parameters from the first workspace to + ;; prevent potential problems. The values do not matter here as + ;; they'll be updated by the RandR module later. + (let ((w (car exwm-workspace--list))) + (dolist (param '(exwm-randr-monitor + exwm-geometry)) + (set-frame-parameter frame param (frame-parameter w param)))) + ;; Support transparency on the container X window when the Emacs frame + ;; does. Note that in addition to setting the visual, colormap and depth + ;; we must also reset the `:border-pixmap', as its default value is + ;; relative to the parent window, which might have a different depth. + (let* ((vdc (exwm--get-visual-depth-colormap exwm--connection outer-id))) + (setq frame-visual (car vdc)) + (setq frame-depth (cadr vdc)) + (setq frame-colormap (caddr vdc))) + (xcb:+request exwm--connection + (make-instance 'xcb:CreateWindow + :depth frame-depth + :wid container + :parent exwm--root + :x -1 + :y -1 + :width 1 + :height 1 + :border-width 0 + :class xcb:WindowClass:InputOutput + :visual frame-visual + :value-mask (logior xcb:CW:BackPixmap + xcb:CW:BorderPixel + xcb:CW:Colormap + xcb:CW:OverrideRedirect) + :background-pixmap xcb:BackPixmap:None + :border-pixel 0 + :colormap frame-colormap + :override-redirect 1)) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window container + :value-mask xcb:ConfigWindow:StackMode + :stack-mode xcb:StackMode:Below)) + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_WM_NAME + :window container + :data + (format "EXWM workspace %d frame container" + (exwm-workspace--position frame)))) + (xcb:+request exwm--connection + (make-instance 'xcb:ReparentWindow + :window outer-id :parent container :x 0 :y 0)) + (xcb:+request exwm--connection + (make-instance 'xcb:icccm:set-WM_STATE + :window outer-id + :state xcb:icccm:WM_STATE:NormalState + :icon xcb:Window:None)) + (xcb:+request exwm--connection + (make-instance 'xcb:MapWindow :window container))) + (xcb:flush exwm--connection) + ;; Delay making the workspace fullscreen until Emacs becomes idle + (exwm--defer 0 #'exwm-workspace--fullscreen-workspace frame) + ;; Update EWMH properties. + (exwm-workspace--update-ewmh-props) + (if exwm-workspace--create-silently + (setq exwm-workspace--switch-history-outdated t) + (let ((original-index exwm-workspace-current-index)) + (exwm-workspace-switch frame t) + (message "Created %s as workspace %d; switched from %d" + frame exwm-workspace-current-index original-index)) + (run-hooks 'exwm-workspace-list-change-hook))) + +(defun exwm-workspace--get-next-workspace (frame) + "Return the next workspace if workspace FRAME were removed. +Return nil if FRAME is the only workspace." + (let* ((index (exwm-workspace--position frame)) + (lastp (= index (1- (exwm-workspace--count)))) + (nextw (elt exwm-workspace--list (+ index (if lastp -1 +1))))) + (unless (eq frame nextw) + nextw))) + +(defun exwm-workspace--remove-frame-as-workspace (frame &optional quit) + "Stop treating FRAME as a workspace. +When QUIT is non-nil cleanup avoid communicating with the X server." + ;; TODO: restore all frame parameters (e.g. exwm-workspace, buffer-predicate, + ;; etc) + (exwm--log "Removing frame `%s' as workspace" frame) + (unless quit + (let* ((next-frame (exwm-workspace--get-next-workspace frame)) + (following-frames (cdr (memq frame exwm-workspace--list)))) + ;; Need to remove the workspace from the list for the correct calculation of + ;; indexes below. + (setq exwm-workspace--list (delete frame exwm-workspace--list)) + ;; Move the windows to the next workspace and switch to it. + (unless next-frame + ;; The user managed to delete the last workspace, so create a new one. + (exwm--log "Last workspace deleted; create a new one") + (let ((exwm-workspace--create-silently t)) + (setq next-frame (make-frame)))) + (dolist (pair exwm--id-buffer-alist) + (let ((other-frame (buffer-local-value 'exwm--frame (cdr pair)))) + ;; Move X windows to next-frame. + (when (eq other-frame frame) + (exwm-workspace-move-window next-frame (car pair))) + ;; Update the _NET_WM_DESKTOP property of each following X window. + (when (memq other-frame following-frames) + (exwm-workspace--set-desktop (car pair))))) + ;; If the current workspace is deleted, switch to next one. + (when (eq frame exwm-workspace--current) + (exwm-workspace-switch next-frame)))) + ;; Reparent out the frame. + (let ((outer-id (frame-parameter frame 'exwm-outer-id))) + (xcb:+request exwm--connection + (make-instance 'xcb:UnmapWindow + :window outer-id)) + (xcb:+request exwm--connection + (make-instance 'xcb:ReparentWindow + :window outer-id + :parent exwm--root + :x 0 + :y 0)) + ;; Reset the override-redirect. + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window outer-id + :value-mask xcb:CW:OverrideRedirect + :override-redirect 0)) + ;; Remove fullscreen state. + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_WM_STATE + :window outer-id + :data nil)) + (xcb:+request exwm--connection + (make-instance 'xcb:MapWindow + :window outer-id))) + ;; Destroy the container. + (xcb:+request exwm--connection + (make-instance 'xcb:DestroyWindow + :window (frame-parameter frame 'exwm-container))) + (xcb:flush exwm--connection) + ;; Update EWMH properties. + (exwm-workspace--update-ewmh-props) + ;; Update switch history. + (unless quit + (setq exwm-workspace--switch-history-outdated t) + (run-hooks 'exwm-workspace-list-change-hook))) + +(defun exwm-workspace--on-delete-frame (frame) + "Hook run upon `delete-frame' removing FRAME as a workspace." + (cond + ((not (exwm-workspace--workspace-p frame)) + (exwm--log "Frame `%s' is not a workspace" frame)) + (t + (exwm-workspace--remove-frame-as-workspace frame)))) + +(defun exwm-workspace--fullscreen-workspace (frame) + "Make workspace FRAME fullscreen. +Called from a timer." + (when (frame-live-p frame) + (set-frame-parameter frame 'fullscreen 'fullboth))) + +(defun exwm-workspace--on-after-make-frame (frame) + "Hook run upon `make-frame' that configures FRAME as a workspace." + (cond + ((exwm-workspace--workspace-p frame) + (exwm--log "Frame `%s' is already a workspace" frame)) + ((not (display-graphic-p frame)) + (exwm--log "Frame `%s' is not graphical" frame)) + ((not (eq (frame-terminal) exwm--terminal)) + (exwm--log "Frame `%s' is on a different terminal (%S instead of %S)" + frame + (frame-terminal frame) + exwm--terminal)) + ((not (string-equal + (replace-regexp-in-string "\\.0$" "" + (slot-value exwm--connection 'display)) + (replace-regexp-in-string "\\.0$" "" + (frame-parameter frame 'display)))) + (exwm--log "Frame `%s' is on a different DISPLAY (%S instead of %S)" + frame + (frame-parameter frame 'display) + (slot-value exwm--connection 'display))) + ((frame-parameter frame 'unsplittable) + ;; We create floating frames with the "unsplittable" parameter set. + ;; Though it may not be a floating frame, we won't treat an + ;; unsplittable frame as a workspace anyway. + (exwm--log "Frame `%s' is floating" frame)) + (t + (exwm--log "Adding frame `%s' as workspace" frame) + (exwm-workspace--add-frame-as-workspace frame)))) + +(defun exwm-workspace--update-ewmh-props () + "Update EWMH properties to match the workspace list." + (exwm--log) + (let ((num-workspaces (exwm-workspace--count))) + ;; Avoid setting 0 desktops. + (when (= 0 num-workspaces) + (setq num-workspaces 1)) + ;; Set _NET_NUMBER_OF_DESKTOPS. + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_NUMBER_OF_DESKTOPS + :window exwm--root :data num-workspaces)) + ;; Set _NET_DESKTOP_GEOMETRY. + (exwm-workspace--set-desktop-geometry) + ;; Update workareas. + (exwm-workspace--update-workareas)) + (xcb:flush exwm--connection)) + +(defun exwm-workspace--modify-all-x-frames-parameters (new-x-parameters) + "Modifies `window-system-default-frame-alist' for the X Window System. +NEW-X-PARAMETERS is an alist of frame parameters, merged into current +`window-system-default-frame-alist' for the X Window System. The parameters are +applied to all subsequently created X frames." + (exwm--log) + ;; The parameters are modified in place; take current + ;; ones or insert a new X-specific list. + (let ((x-parameters (or (assq 'x window-system-default-frame-alist) + (let ((new-x-parameters '(x))) + (push new-x-parameters + window-system-default-frame-alist) + new-x-parameters)))) + (setf (cdr x-parameters) + (append new-x-parameters (cdr x-parameters))))) + +(defun exwm-workspace--handle-focus-in (_orig-func _event) + "Replacement for `handle-focus-in'." + (interactive "e")) + +(defun exwm-workspace--handle-focus-out (_orig-func _event) + "Replacement for `handle-focus-out'." + (interactive "e")) + +(defun exwm-workspace--init-minibuffer-frame () + "Initialize minibuffer-only frame." + (exwm--log) + ;; Initialize workspaces without minibuffers. + (setq exwm-workspace--minibuffer + (make-frame '((window-system . x) (minibuffer . only) + (left . 10000) (right . 10000) + (width . 1) (height . 1)))) + ;; This is the only usable minibuffer frame. + (setq default-minibuffer-frame exwm-workspace--minibuffer) + (exwm-workspace--modify-all-x-frames-parameters + '((minibuffer . nil))) + (let ((outer-id (string-to-number + (frame-parameter exwm-workspace--minibuffer + 'outer-window-id))) + (window-id (string-to-number + (frame-parameter exwm-workspace--minibuffer + 'window-id))) + (container (xcb:generate-id exwm--connection))) + (set-frame-parameter exwm-workspace--minibuffer + 'exwm-outer-id outer-id) + (set-frame-parameter exwm-workspace--minibuffer 'exwm-id window-id) + (set-frame-parameter exwm-workspace--minibuffer 'exwm-container + container) + (xcb:+request exwm--connection + (make-instance 'xcb:CreateWindow + :depth 0 + :wid container + :parent exwm--root + :x 0 + :y 0 + :width 1 + :height 1 + :border-width 0 + :class xcb:WindowClass:InputOutput + :visual 0 + :value-mask (logior xcb:CW:BackPixmap + xcb:CW:OverrideRedirect) + :background-pixmap xcb:BackPixmap:ParentRelative + :override-redirect 1)) + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_WM_NAME + :window container + :data "EXWM minibuffer container")) + ;; Reparent the minibuffer frame to the container. + (xcb:+request exwm--connection + (make-instance 'xcb:ReparentWindow + :window outer-id :parent container :x 0 :y 0)) + ;; Map the container. + (xcb:+request exwm--connection + (make-instance 'xcb:MapWindow + :window container)) + ;; Attach event listener for monitoring the frame + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window outer-id + :value-mask xcb:CW:EventMask + :event-mask xcb:EventMask:StructureNotify)) + (xcb:+event exwm--connection 'xcb:ConfigureNotify + #'exwm-workspace--on-ConfigureNotify)) + ;; Show/hide minibuffer / echo area when they're active/inactive. + (add-hook 'minibuffer-setup-hook #'exwm-workspace--on-minibuffer-setup) + (add-hook 'minibuffer-exit-hook #'exwm-workspace--on-minibuffer-exit) + (setq exwm-workspace--timer + (run-with-idle-timer 0 t #'exwm-workspace--on-echo-area-dirty)) + (add-hook 'echo-area-clear-hook #'exwm-workspace--on-echo-area-clear) + ;; The default behavior of `display-buffer' (indirectly called by + ;; `minibuffer-completion-help') is not correct here. + (cl-pushnew '(exwm-workspace--display-buffer) display-buffer-alist + :test #'equal)) + +(defun exwm-workspace--exit-minibuffer-frame () + "Cleanup minibuffer-only frame." + (exwm--log) + ;; Only on minibuffer-frame. + (remove-hook 'minibuffer-setup-hook #'exwm-workspace--on-minibuffer-setup) + (remove-hook 'minibuffer-exit-hook #'exwm-workspace--on-minibuffer-exit) + (remove-hook 'echo-area-clear-hook #'exwm-workspace--on-echo-area-clear) + (when exwm-workspace--display-echo-area-timer + (cancel-timer exwm-workspace--display-echo-area-timer)) + (when exwm-workspace--timer + (cancel-timer exwm-workspace--timer) + (setq exwm-workspace--timer nil)) + (setq display-buffer-alist + (cl-delete '(exwm-workspace--display-buffer) display-buffer-alist + :test #'equal)) + (setq default-minibuffer-frame nil) + (when (frame-live-p exwm-workspace--minibuffer) ; might be already dead + (let ((id (frame-parameter exwm-workspace--minibuffer 'exwm-outer-id))) + (when (and exwm-workspace--minibuffer id + ;; Invoked from `exwm-manage--exit' upon disconnection. + (slot-value exwm--connection 'connected)) + (xcb:+request exwm--connection + (make-instance 'xcb:ReparentWindow + :window id + :parent exwm--root + :x 0 + :y 0))) + (setq exwm-workspace--minibuffer nil)))) + +(defun exwm-workspace--init () + "Initialize workspace module." + (exwm--log) + (exwm-workspace--init-switch-map) + ;; Prevent unexpected exit + (setq exwm-workspace--fullscreen-frame-count 0) + (exwm-workspace--modify-all-x-frames-parameters + '((internal-border-width . 0))) + (let ((initial-workspaces (frame-list))) + (if (not (exwm-workspace--minibuffer-own-frame-p)) + ;; Initialize workspaces with minibuffers. + (when (< 1 (length initial-workspaces)) + ;; Exclude the initial frame. + (dolist (i initial-workspaces) + (unless (frame-parameter i 'window-id) + (setq initial-workspaces (delq i initial-workspaces)))) + (let ((f (car initial-workspaces))) + ;; Remove the possible internal border. + (set-frame-parameter f 'internal-border-width 0))) + (exwm-workspace--init-minibuffer-frame) + ;; Remove/hide existing frames. + (dolist (f initial-workspaces) + (when (eq 'x (framep f)) ;do not delete the initial frame. + (delete-frame f))) + ;; Recreate one frame with the external minibuffer set. + (setq initial-workspaces (list (make-frame '((window-system . x)))))) + ;; Prevent `other-buffer' from selecting already displayed EXWM buffers. + (modify-all-frames-parameters + '((buffer-predicate . exwm-layout--other-buffer-predicate))) + ;; Create remaining workspaces. + (dotimes (_ (- exwm-workspace-number (length initial-workspaces))) + (nconc initial-workspaces (list (make-frame '((window-system . x)))))) + ;; Configure workspaces + (let ((exwm-workspace--create-silently t)) + (dolist (i initial-workspaces) + (exwm-workspace--add-frame-as-workspace i)))) + (xcb:flush exwm--connection) + ;; We have to advice `x-create-frame' or every call to it would hang EXWM + (advice-add 'x-create-frame :around #'exwm-workspace--x-create-frame) + ;; We have to manually handle focus-in and focus-out events for Emacs + ;; frames. + (advice-add 'handle-focus-in :around #'exwm-workspace--handle-focus-in) + (advice-add 'handle-focus-out :around #'exwm-workspace--handle-focus-out) + ;; Make new frames create new workspaces. + (add-hook 'after-make-frame-functions + #'exwm-workspace--on-after-make-frame) + (add-hook 'delete-frame-functions #'exwm-workspace--on-delete-frame) + (when (exwm-workspace--minibuffer-own-frame-p) + (add-hook 'exwm-input--event-hook + #'exwm-workspace--on-echo-area-clear)) + ;; Switch to the first workspace + (exwm-workspace-switch 0 t) + ;; Prevent frame parameters introduced by this module from being + ;; saved/restored. + (dolist (i '(exwm-active exwm-outer-id exwm-id exwm-container exwm-geometry + exwm-selected-window exwm-urgency fullscreen)) + (unless (assq i frameset-filter-alist) + (push (cons i :never) frameset-filter-alist)))) + +(defun exwm-workspace--exit () + "Exit the workspace module." + (exwm--log) + (when (exwm-workspace--minibuffer-own-frame-p) + (exwm-workspace--exit-minibuffer-frame)) + (advice-remove 'x-create-frame #'exwm-workspace--x-create-frame) + (advice-remove 'handle-focus-in #'exwm-workspace--handle-focus-in) + (advice-remove 'handle-focus-out #'exwm-workspace--handle-focus-out) + (remove-hook 'after-make-frame-functions + #'exwm-workspace--on-after-make-frame) + (remove-hook 'delete-frame-functions + #'exwm-workspace--on-delete-frame) + (when (exwm-workspace--minibuffer-own-frame-p) + (remove-hook 'exwm-input--event-hook + #'exwm-workspace--on-echo-area-clear)) + ;; Hide & reparent out all frames (save-set can't be used here since + ;; X windows will be re-mapped). + (when (slot-value exwm--connection 'connected) + (dolist (i exwm-workspace--list) + (when (frame-live-p i) ; might be already dead + (exwm-workspace--remove-frame-as-workspace i 'quit) + (modify-frame-parameters i '((exwm-selected-window . nil) + (exwm-urgency . nil) + (exwm-outer-id . nil) + (exwm-id . nil) + (exwm-container . nil) + ;; (internal-border-width . nil) ; integerp + (fullscreen . nil) + (buffer-predicate . nil)))))) + ;; Don't let dead frames linger. + (setq exwm-workspace--current nil) + (setq exwm-workspace-current-index 0) + (setq exwm-workspace--list nil)) + +(defun exwm-workspace--post-init () + "The second stage in the initialization of the workspace module." + (exwm--log) + ;; Wait until all workspace frames are resized. + (with-timeout (1) + (while (< exwm-workspace--fullscreen-frame-count (exwm-workspace--count)) + (accept-process-output nil 0.1))) + (setq exwm-workspace--fullscreen-frame-count nil)) + + + +(provide 'exwm-workspace) + +;;; exwm-workspace.el ends here diff --git a/exwm-xim.el b/exwm-xim.el new file mode 100644 index 0000000000..1f0c9c460b --- /dev/null +++ b/exwm-xim.el @@ -0,0 +1,810 @@ +;;; exwm-xim.el --- XIM Module for EXWM -*- lexical-binding: t -*- + +;; Copyright (C) 2019-2024 Free Software Foundation, Inc. + +;; Author: Chris Feng <chris.w.feng@gmail.com> + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This module adds XIM support for EXWM and allows sending characters +;; generated by any Emacs's builtin input method (info node `Input Methods') +;; to X windows. + +;; This module is essentially an X input method server utilizing Emacs as +;; its backend. It talks with X windows through the XIM protocol. The XIM +;; protocol is quite flexible by itself, stating that an implementation can +;; create network connections of various types as well as make use of an +;; existing X connection for communication, and that an IM server may +;; support multiple transport versions, various input styles and several +;; event flow modals, etc. Here we only make choices that are most popular +;; among other IM servers and more importantly, practical for Emacs to act +;; as an IM server: +;; +;; + Packets are transported on top of an X connection like most IMEs. +;; + Only transport version 0.0 (i.e. only-CM & Property-with-CM) is +;; supported (same as "IM Server Developers Kit", adopted by most IMEs). +;; + Only support static event flow, on-demand-synchronous method. +;; + Only "root-window" input style is supported. + +;; To use this module, first load and enable it as follows: +;; +;; (require 'exwm-xim) +;; (exwm-xim-enable) +;; +;; A keybinding for `toggle-input-method' is probably required to turn on & +;; off an input method (default to `default-input-method'). It's bound to +;; 'C-\' by default and can be made reachable when working with X windows: +;; +;; (push ?\C-\\ exwm-input-prefix-keys) +;; +;; It's also required (and error-prone) to setup environment variables to +;; make applications actually use this input method. Typically the +;; following lines should be inserted into '~/.xinitrc'. +;; +;; export XMODIFIERS=@im=exwm-xim +;; export GTK_IM_MODULE=xim +;; export QT_IM_MODULE=xim +;; export CLUTTER_IM_MODULE=xim + +;; References: +;; + XIM (http://www.x.org/releases/X11R7.6/doc/libX11/specs/XIM/xim.html) +;; + IMdkit (http://xorg.freedesktop.org/archive/unsupported/lib/IMdkit/) +;; + UIM (https://github.com/uim/uim) + +;;; Code: + +(require 'cl-lib) + +(require 'xcb-keysyms) +(require 'xcb-xim) + +(require 'exwm-core) +(require 'exwm-input) + +(defconst exwm-xim--locales + "@locale=\ +aa,af,ak,am,an,anp,ar,as,ast,ayc,az,be,bem,ber,bg,bhb,bho,bn,bo,br,brx,bs,byn,\ +ca,ce,cmn,crh,cs,csb,cv,cy,da,de,doi,dv,dz,el,en,es,et,eu,fa,ff,fi,fil,fo,fr,\ +fur,fy,ga,gd,gez,gl,gu,gv,ha,hak,he,hi,hne,hr,hsb,ht,hu,hy,ia,id,ig,ik,is,it,\ +iu,iw,ja,ka,kk,kl,km,kn,ko,kok,ks,ku,kw,ky,lb,lg,li,li,lij,lo,lt,lv,lzh,mag,\ +mai,mg,mhr,mi,mk,ml,mn,mni,mr,ms,mt,my,nan,nb,nds,ne,nhn,niu,nl,nn,nr,nso,oc,\ +om,or,os,pa,pa,pap,pl,ps,pt,quz,raj,ro,ru,rw,sa,sat,sc,sd,se,shs,si,sid,sk,sl,\ +so,sq,sr,ss,st,sv,sw,szl,ta,tcy,te,tg,th,the,ti,tig,tk,tl,tn,tr,ts,tt,ug,uk,\ +unm,ur,uz,ve,vi,wa,wae,wal,wo,xh,yi,yo,yue,zh,zu,\ +C,no" + "All supported locales (stolen from glibc).") + +(defconst exwm-xim--default-error + (make-instance 'xim:error + :im-id 0 + :ic-id 0 + :flag xim:error-flag:invalid-both + :error-code xim:error-code:bad-something + :length 0 + :type 0 + :detail nil) + "Default error returned to clients.") + +(defconst exwm-xim--default-im-attrs + (list (make-instance 'xim:XIMATTR + :id 0 + :type xim:ATTRIBUTE-VALUE-TYPE:xim-styles + :length (length xlib:XNQueryInputStyle) + :attribute xlib:XNQueryInputStyle)) + "Default IM attrs returned to clients.") + +(defconst exwm-xim--default-ic-attrs + (list (make-instance 'xim:XICATTR + :id 0 + :type xim:ATTRIBUTE-VALUE-TYPE:long-data + :length (length xlib:XNInputStyle) + :attribute xlib:XNInputStyle) + (make-instance 'xim:XICATTR + :id 1 + :type xim:ATTRIBUTE-VALUE-TYPE:window + :length (length xlib:XNClientWindow) + :attribute xlib:XNClientWindow) + ;; Required by e.g. xterm. + (make-instance 'xim:XICATTR + :id 2 + :type xim:ATTRIBUTE-VALUE-TYPE:window + :length (length xlib:XNFocusWindow) + :attribute xlib:XNFocusWindow)) + "Default IC attrs returned to clients.") + +(defconst exwm-xim--default-styles + (make-instance 'xim:XIMStyles + :number nil + :styles (list (logior xlib:XIMPreeditNothing + xlib:XIMStatusNothing))) + "Default styles: root-window, i.e. no preediting or status display support.") + +(defconst exwm-xim--default-attributes + (list (make-instance 'xim:XIMATTRIBUTE + :id 0 + :length nil + :value exwm-xim--default-styles)) + "Default IM/IC attributes returned to clients.") + +(defvar exwm-xim--conn nil + "The X connection for initiating other XIM connections.") +(defvar exwm-xim--event-xwin nil + "X window for initiating new XIM connections.") +(defvar exwm-xim--server-client-plist '(nil nil) + "Plist mapping server window to [X connection, client window, byte-order].") +(defvar exwm-xim--client-server-plist '(nil nil) + "Plist mapping client window to server window.") +(defvar exwm-xim--property-index 0 "For generating a unique property name.") +(defvar exwm-xim--im-id 0 "Last IM ID.") +(defvar exwm-xim--ic-id 0 "Last IC ID.") + +;; X11 atoms. +(defvar exwm-xim--@server nil) +(defvar exwm-xim--LOCALES nil) +(defvar exwm-xim--TRANSPORT nil) +(defvar exwm-xim--XIM_SERVERS nil) +(defvar exwm-xim--_XIM_PROTOCOL nil) +(defvar exwm-xim--_XIM_XCONNECT nil) + +(defvar exwm-xim-buffer-p nil + "Whether current buffer is used by exwm-xim.") +(make-variable-buffer-local 'exwm-xim-buffer-p) + +(defun exwm-xim--on-SelectionRequest (data _synthetic) + "Handle SelectionRequest events on IMS window. +DATA contains unmarshalled SelectionRequest event data. + +Such events would be received when clients query for LOCALES or TRANSPORT." + (exwm--log) + (let ((evt (make-instance 'xcb:SelectionRequest)) + value fake-event) + (xcb:unmarshal evt data) + (with-slots (time requestor selection target property) evt + (setq value (cond ((= target exwm-xim--LOCALES) + ;; Return supported locales. + exwm-xim--locales) + ((= target exwm-xim--TRANSPORT) + ;; Use XIM over an X connection. + "@transport=X/"))) + (when value + ;; Change the property. + (xcb:+request exwm-xim--conn + (make-instance 'xcb:ChangeProperty + :mode xcb:PropMode:Replace + :window requestor + :property property + :type target + :format 8 + :data-len (length value) + :data value)) + ;; Send a SelectionNotify event. + (setq fake-event (make-instance 'xcb:SelectionNotify + :time time + :requestor requestor + :selection selection + :target target + :property property)) + (xcb:+request exwm-xim--conn + (make-instance 'xcb:SendEvent + :propagate 0 + :destination requestor + :event-mask xcb:EventMask:NoEvent + :event (xcb:marshal fake-event exwm-xim--conn))) + (xcb:flush exwm-xim--conn))))) + +(cl-defun exwm-xim--on-ClientMessage-0 (data _synthetic) + "Handle ClientMessage event on IMS window (new connection). + +Such events would be received when clients request for _XIM_XCONNECT. +A new X connection and server window would be created to communicate with +this client." + (exwm--log) + (let ((evt (make-instance 'xcb:ClientMessage)) + conn client-xwin server-xwin) + (xcb:unmarshal evt data) + (with-slots (window type data) evt + (unless (= type exwm-xim--_XIM_XCONNECT) + ;; Only handle _XIM_XCONNECT. + (exwm--log "Ignore ClientMessage %s" type) + (cl-return-from exwm-xim--on-ClientMessage-0)) + (setq client-xwin (elt (slot-value data 'data32) 0) + ;; Create a new X connection and a new server window. + conn (xcb:connect) + server-xwin (xcb:generate-id conn)) + (set-process-query-on-exit-flag (slot-value conn 'process) nil) + ;; Store this client. + (plist-put exwm-xim--server-client-plist server-xwin + `[,conn ,client-xwin nil]) + (plist-put exwm-xim--client-server-plist client-xwin server-xwin) + ;; Select DestroyNotify events on this client window. + (xcb:+request exwm-xim--conn + (make-instance 'xcb:ChangeWindowAttributes + :window client-xwin + :value-mask xcb:CW:EventMask + :event-mask xcb:EventMask:StructureNotify)) + (xcb:flush exwm-xim--conn) + ;; Handle ClientMessage events from this new connection. + (xcb:+event conn 'xcb:ClientMessage #'exwm-xim--on-ClientMessage) + ;; Create a communication window. + (xcb:+request conn + (make-instance 'xcb:CreateWindow + :depth 0 + :wid server-xwin + :parent exwm--root + :x 0 + :y 0 + :width 1 + :height 1 + :border-width 0 + :class xcb:WindowClass:InputOutput + :visual 0 + :value-mask xcb:CW:OverrideRedirect + :override-redirect 1)) + (xcb:flush conn) + ;; Send connection establishment ClientMessage. + (setf window client-xwin + (slot-value data 'data32) `(,server-xwin 0 0 0 0)) + (slot-makeunbound data 'data8) + (slot-makeunbound data 'data16) + (xcb:+request exwm-xim--conn + (make-instance 'xcb:SendEvent + :propagate 0 + :destination client-xwin + :event-mask xcb:EventMask:NoEvent + :event (xcb:marshal evt exwm-xim--conn))) + (xcb:flush exwm-xim--conn)))) + +(cl-defun exwm-xim--on-ClientMessage (data _synthetic) + "Handle ClientMessage event on IMS communication window (request). + +Such events would be received when clients request for _XIM_PROTOCOL. +The actual XIM request is in client message data or a property." + (exwm--log) + (let ((evt (make-instance 'xcb:ClientMessage)) + conn client-xwin server-xwin) + (xcb:unmarshal evt data) + (with-slots (format window type data) evt + (unless (= type exwm-xim--_XIM_PROTOCOL) + (exwm--log "Ignore ClientMessage %s" type) + (cl-return-from exwm-xim--on-ClientMessage)) + (setq server-xwin window + conn (plist-get exwm-xim--server-client-plist server-xwin) + client-xwin (elt conn 1) + conn (elt conn 0)) + (cond ((= format 8) + ;; Data. + (exwm-xim--on-request (vconcat (slot-value data 'data8)) + conn client-xwin server-xwin)) + ((= format 32) + ;; Atom. + (with-slots (data32) data + (with-slots (value) + (xcb:+request-unchecked+reply conn + (make-instance 'xcb:GetProperty + :delete 1 + :window server-xwin + :property (elt data32 1) + :type xcb:GetPropertyType:Any + :long-offset 0 + :long-length (elt data32 0))) + (when (> (length value) 0) + (exwm-xim--on-request value conn client-xwin + server-xwin))))))))) + +(defun exwm-xim--on-request (data conn client-xwin server-xwin) + "Handle an XIM reuqest." + (exwm--log) + (let ((opcode (elt data 0)) + ;; Let-bind `xim:lsb' to make pack/unpack functions work correctly. + (xim:lsb (elt (plist-get exwm-xim--server-client-plist server-xwin) 2)) + req replies) + (cond ((= opcode xim:opcode:error) + (exwm--log "ERROR: %s" data)) + ((= opcode xim:opcode:connect) + (exwm--log "CONNECT") + (setq xim:lsb (= (elt data 4) xim:connect-byte-order:lsb-first)) + ;; Store byte-order. + (setf (elt (plist-get exwm-xim--server-client-plist server-xwin) 2) + xim:lsb) + (setq req (make-instance 'xim:connect)) + (xcb:unmarshal req data) + (if (and (= (slot-value req 'major-version) 1) + (= (slot-value req 'minor-version) 0) + ;; Do not support authentication. + (= (slot-value req 'number) 0)) + ;; Accept the connection. + (push (make-instance 'xim:connect-reply) replies) + ;; Deny it. + (push exwm-xim--default-error replies))) + ((memq opcode (list xim:opcode:auth-required + xim:opcode:auth-reply + xim:opcode:auth-next + xim:opcode:auth-ng)) + (exwm--log "AUTH: %d" opcode) + ;; Deny any attempt to make authentication. + (push exwm-xim--default-error replies)) + ((= opcode xim:opcode:disconnect) + (exwm--log "DISCONNECT") + ;; Gracefully disconnect from the client. + (exwm-xim--make-request (make-instance 'xim:disconnect-reply) + conn client-xwin) + ;; Destroy the communication window & connection. + (xcb:+request conn + (make-instance 'xcb:DestroyWindow + :window server-xwin)) + (xcb:disconnect conn) + ;; Clean up cache. + (cl-remf exwm-xim--server-client-plist server-xwin) + (cl-remf exwm-xim--client-server-plist client-xwin)) + ((= opcode xim:opcode:open) + (exwm--log "OPEN") + ;; Note: We make no check here. + (setq exwm-xim--im-id (if (< exwm-xim--im-id #xffff) + (1+ exwm-xim--im-id) + 1)) + (setq replies + (list + (make-instance 'xim:open-reply + :im-id exwm-xim--im-id + :im-attrs-length nil + :im-attrs exwm-xim--default-im-attrs + :ic-attrs-length nil + :ic-attrs exwm-xim--default-ic-attrs) + (make-instance 'xim:set-event-mask + :im-id exwm-xim--im-id + :ic-id 0 + ;; Static event flow. + :forward-event-mask xcb:EventMask:KeyPress + ;; on-demand-synchronous method. + :synchronous-event-mask + xcb:EventMask:NoEvent)))) + ((= opcode xim:opcode:close) + (exwm--log "CLOSE") + (setq req (make-instance 'xim:close)) + (xcb:unmarshal req data) + (push (make-instance 'xim:close-reply + :im-id (slot-value req 'im-id)) + replies)) + ((= opcode xim:opcode:trigger-notify) + (exwm--log "TRIGGER-NOTIFY") + ;; Only static event flow modal is supported. + (push exwm-xim--default-error replies)) + ((= opcode xim:opcode:encoding-negotiation) + (exwm--log "ENCODING-NEGOTIATION") + (setq req (make-instance 'xim:encoding-negotiation)) + (xcb:unmarshal req data) + (let ((index (cl-position "COMPOUND_TEXT" + (mapcar (lambda (i) (slot-value i 'name)) + (slot-value req 'names)) + :test #'equal))) + (unless index + ;; Fallback to portable character encoding (a subset of ASCII). + (setq index -1)) + (push (make-instance 'xim:encoding-negotiation-reply + :im-id (slot-value req 'im-id) + :category + xim:encoding-negotiation-reply-category:name + :index index) + replies))) + ((= opcode xim:opcode:query-extension) + (exwm--log "QUERY-EXTENSION") + (setq req (make-instance 'xim:query-extension)) + (xcb:unmarshal req data) + (push (make-instance 'xim:query-extension-reply + :im-id (slot-value req 'im-id) + ;; No extension support. + :length 0 + :extensions nil) + replies)) + ((= opcode xim:opcode:set-im-values) + (exwm--log "SET-IM-VALUES") + ;; There's only one possible input method attribute. + (setq req (make-instance 'xim:set-im-values)) + (xcb:unmarshal req data) + (push (make-instance 'xim:set-im-values-reply + :im-id (slot-value req 'im-id)) + replies)) + ((= opcode xim:opcode:get-im-values) + (exwm--log "GET-IM-VALUES") + (setq req (make-instance 'xim:get-im-values)) + (let (im-attributes-id) + (xcb:unmarshal req data) + (setq im-attributes-id (slot-value req 'im-attributes-id)) + (if (cl-notevery (lambda (i) (= i 0)) im-attributes-id) + ;; Only support one IM attributes. + (push (make-instance 'xim:error + :im-id (slot-value req 'im-id) + :ic-id 0 + :flag xim:error-flag:invalid-ic-id + :error-code xim:error-code:bad-something + :length 0 + :type 0 + :detail nil) + replies) + (push + (make-instance 'xim:get-im-values-reply + :im-id (slot-value req 'im-id) + :length nil + :im-attributes exwm-xim--default-attributes) + replies)))) + ((= opcode xim:opcode:create-ic) + (exwm--log "CREATE-IC") + (setq req (make-instance 'xim:create-ic)) + (xcb:unmarshal req data) + ;; Note: The ic-attributes slot is ignored. + (setq exwm-xim--ic-id (if (< exwm-xim--ic-id #xffff) + (1+ exwm-xim--ic-id) + 1)) + (push (make-instance 'xim:create-ic-reply + :im-id (slot-value req 'im-id) + :ic-id exwm-xim--ic-id) + replies)) + ((= opcode xim:opcode:destroy-ic) + (exwm--log "DESTROY-IC") + (setq req (make-instance 'xim:destroy-ic)) + (xcb:unmarshal req data) + (push (make-instance 'xim:destroy-ic-reply + :im-id (slot-value req 'im-id) + :ic-id (slot-value req 'ic-id)) + replies)) + ((= opcode xim:opcode:set-ic-values) + (exwm--log "SET-IC-VALUES") + (setq req (make-instance 'xim:set-ic-values)) + (xcb:unmarshal req data) + ;; We don't distinguish between input contexts. + (push (make-instance 'xim:set-ic-values-reply + :im-id (slot-value req 'im-id) + :ic-id (slot-value req 'ic-id)) + replies)) + ((= opcode xim:opcode:get-ic-values) + (exwm--log "GET-IC-VALUES") + (setq req (make-instance 'xim:get-ic-values)) + (xcb:unmarshal req data) + (push (make-instance 'xim:get-ic-values-reply + :im-id (slot-value req 'im-id) + :ic-id (slot-value req 'ic-id) + :length nil + :ic-attributes exwm-xim--default-attributes) + replies)) + ((= opcode xim:opcode:set-ic-focus) + (exwm--log "SET-IC-FOCUS") + ;; All input contexts are the same. + ) + ((= opcode xim:opcode:unset-ic-focus) + (exwm--log "UNSET-IC-FOCUS") + ;; All input contexts are the same. + ) + ((= opcode xim:opcode:forward-event) + (exwm--log "FORWARD-EVENT") + (setq req (make-instance 'xim:forward-event)) + (xcb:unmarshal req data) + (exwm-xim--handle-forward-event-request req xim:lsb conn + client-xwin)) + ((= opcode xim:opcode:sync) + (exwm--log "SYNC") + (setq req (make-instance 'xim:sync)) + (xcb:unmarshal req data) + (push (make-instance 'xim:sync-reply + :im-id (slot-value req 'im-id) + :ic-id (slot-value req 'ic-id)) + replies)) + ((= opcode xim:opcode:sync-reply) + (exwm--log "SYNC-REPLY")) + ((= opcode xim:opcode:reset-ic) + (exwm--log "RESET-IC") + ;; No context-specific data saved. + (setq req (make-instance 'xim:reset-ic)) + (xcb:unmarshal req data) + (push (make-instance 'xim:reset-ic-reply + :im-id (slot-value req 'im-id) + :ic-id (slot-value req 'ic-id) + :length 0 + :string "") + replies)) + ((memq opcode (list xim:opcode:str-conversion-reply + xim:opcode:preedit-start-reply + xim:opcode:preedit-caret-reply)) + (exwm--log "PREEDIT: %d" opcode) + ;; No preedit support. + (push exwm-xim--default-error replies)) + (t + (exwm--log "Bad protocol") + (push exwm-xim--default-error replies))) + ;; Actually send the replies. + (when replies + (mapc (lambda (reply) + (exwm-xim--make-request reply conn client-xwin)) + replies) + (xcb:flush conn)))) + +(defun exwm-xim--handle-forward-event-request (req lsb conn client-xwin) + (let ((im-func (with-current-buffer (window-buffer) + input-method-function)) + key-event keysym keysyms event result) + ;; Note: The flag slot is ignored. + ;; Do conversion in client's byte-order. + (let ((xcb:lsb lsb)) + (setq key-event (make-instance 'xcb:KeyPress)) + (xcb:unmarshal key-event (slot-value req 'event))) + (with-slots (detail state) key-event + (setq keysym (xcb:keysyms:keycode->keysym exwm-xim--conn detail + state)) + (when (/= (car keysym) 0) + (setq event (xcb:keysyms:keysym->event + exwm-xim--conn + (car keysym) + (logand state (lognot (cdr keysym))))))) + (while (or (slot-value req 'event) unread-command-events) + (unless (slot-value req 'event) + (setq event (pop unread-command-events)) + ;; Handle events in (t . EVENT) format. + (when (and (consp event) + (eq (car event) t)) + (setq event (cdr event)))) + (if (or (not im-func) + ;; `list' is the default method. + (eq im-func #'list) + (not event) + ;; Select only printable keys. + (not (integerp event)) (> #x20 event) (< #x7e event)) + ;; Either there is no active input method, or invalid key + ;; is detected. + (with-slots ((raw-event event) + im-id ic-id serial-number) + req + (if raw-event + (setq event raw-event) + (setq keysyms (xcb:keysyms:event->keysyms exwm-xim--conn event)) + (with-slots (detail state) key-event + (setf detail (xcb:keysyms:keysym->keycode exwm-xim--conn + (caar keysyms)) + state (cdar keysyms))) + (setq event (let ((xcb:lsb lsb)) + (xcb:marshal key-event conn)))) + (when event + (exwm-xim--make-request + (make-instance 'xim:forward-event + :im-id im-id + :ic-id ic-id + :flag xim:commit-flag:synchronous + :serial-number serial-number + :event event) + conn client-xwin))) + (when (eq exwm--selected-input-mode 'char-mode) + ;; Grab keyboard temporarily for char-mode. + (exwm-input--grab-keyboard)) + (unwind-protect + (with-temp-buffer + ;; This variable is used to test whether exwm-xim is enabled. + ;; Used by e.g. pyim-probe. + (setq-local exwm-xim-buffer-p t) + ;; Always show key strokes. + (let ((input-method-use-echo-area t) + (exwm-input-line-mode-passthrough t)) + (setq result (funcall im-func event)) + ;; Clear echo area for the input method. + (message nil) + ;; This also works for portable character encoding. + (setq result + (encode-coding-string (concat result) + 'compound-text-with-extensions)) + (exwm-xim--make-request + (make-instance 'xim:commit-x-lookup-chars + :im-id (slot-value req 'im-id) + :ic-id (slot-value req 'ic-id) + :flag (logior xim:commit-flag:synchronous + xim:commit-flag:x-lookup-chars) + :length (length result) + :string result) + conn client-xwin))) + (when (eq exwm--selected-input-mode 'char-mode) + (exwm-input--release-keyboard)))) + (xcb:flush conn) + (setf event nil + (slot-value req 'event) nil)))) + +(defun exwm-xim--make-request (req conn client-xwin) + "Make an XIM request REQ via connection CONN. + +CLIENT-XWIN would receive a ClientMessage event either telling the client +the request data or where to fetch the data." + (exwm--log) + (let ((data (xcb:marshal req)) + property format client-message-data client-message) + (if (<= (length data) 20) + ;; Send short requests directly with client messages. + (setq format 8 + ;; Pad to 20 bytes. + data (append data (make-list (- 20 (length data)) 0)) + client-message-data (make-instance 'xcb:ClientMessageData + :data8 data)) + ;; Send long requests with properties. + (setq property (exwm--intern-atom (format "_EXWM_XIM_%x" + exwm-xim--property-index))) + (cl-incf exwm-xim--property-index) + (xcb:+request conn + (make-instance 'xcb:ChangeProperty + :mode xcb:PropMode:Append + :window client-xwin + :property property + :type xcb:Atom:STRING + :format 8 + :data-len (length data) + :data data)) + ;; Also send a client message to notify the client about this property. + (setq format 32 + client-message-data (make-instance 'xcb:ClientMessageData + :data32 `(,(length data) + ,property + ;; Pad to 20 bytes. + 0 0 0)))) + ;; Send the client message. + (setq client-message (make-instance 'xcb:ClientMessage + :format format + :window client-xwin + :type exwm-xim--_XIM_PROTOCOL + :data client-message-data)) + (xcb:+request conn + (make-instance 'xcb:SendEvent + :propagate 0 + :destination client-xwin + :event-mask xcb:EventMask:NoEvent + :event (xcb:marshal client-message conn))))) + +(defun exwm-xim--on-DestroyNotify (data synthetic) + "Do cleanups on receiving DestroyNotify event. + +Such event would be received when the client window is destroyed." + (exwm--log) + (unless synthetic + (let ((evt (make-instance 'xcb:DestroyNotify)) + conn client-xwin server-xwin) + (xcb:unmarshal evt data) + (setq client-xwin (slot-value evt 'window) + server-xwin (plist-get exwm-xim--client-server-plist client-xwin)) + (when server-xwin + (setq conn (aref (plist-get exwm-xim--server-client-plist server-xwin) + 0)) + (cl-remf exwm-xim--server-client-plist server-xwin) + (cl-remf exwm-xim--client-server-plist client-xwin) + ;; Destroy the communication window & connection. + (xcb:+request conn + (make-instance 'xcb:DestroyWindow + :window server-xwin)) + (xcb:disconnect conn))))) + +(cl-defun exwm-xim--init () + "Initialize the XIM module." + (exwm--log) + (when exwm-xim--conn + (cl-return-from exwm-xim--init)) + ;; Initialize atoms. + (setq exwm-xim--@server (exwm--intern-atom "@server=exwm-xim") + exwm-xim--LOCALES (exwm--intern-atom "LOCALES") + exwm-xim--TRANSPORT (exwm--intern-atom "TRANSPORT") + exwm-xim--XIM_SERVERS (exwm--intern-atom "XIM_SERVERS") + exwm-xim--_XIM_PROTOCOL (exwm--intern-atom "_XIM_PROTOCOL") + exwm-xim--_XIM_XCONNECT (exwm--intern-atom "_XIM_XCONNECT")) + ;; Create a new connection and event window. + (setq exwm-xim--conn (xcb:connect) + exwm-xim--event-xwin (xcb:generate-id exwm-xim--conn)) + (set-process-query-on-exit-flag (slot-value exwm-xim--conn 'process) nil) + ;; Initialize xcb:keysyms module. + (xcb:keysyms:init exwm-xim--conn) + ;; Listen to SelectionRequest event for connection establishment. + (xcb:+event exwm-xim--conn 'xcb:SelectionRequest + #'exwm-xim--on-SelectionRequest) + ;; Listen to ClientMessage event on IMS window for new XIM connection. + (xcb:+event exwm-xim--conn 'xcb:ClientMessage #'exwm-xim--on-ClientMessage-0) + ;; Listen to DestroyNotify event to do cleanups. + (xcb:+event exwm-xim--conn 'xcb:DestroyNotify #'exwm-xim--on-DestroyNotify) + ;; Create the event window. + (xcb:+request exwm-xim--conn + (make-instance 'xcb:CreateWindow + :depth 0 + :wid exwm-xim--event-xwin + :parent exwm--root + :x 0 + :y 0 + :width 1 + :height 1 + :border-width 0 + :class xcb:WindowClass:InputOutput + :visual 0 + :value-mask xcb:CW:OverrideRedirect + :override-redirect 1)) + ;; Set the selection owner. + (xcb:+request exwm-xim--conn + (make-instance 'xcb:SetSelectionOwner + :owner exwm-xim--event-xwin + :selection exwm-xim--@server + :time xcb:Time:CurrentTime)) + ;; Set XIM_SERVERS property on the root window. + (xcb:+request exwm-xim--conn + (make-instance 'xcb:ChangeProperty + :mode xcb:PropMode:Prepend + :window exwm--root + :property exwm-xim--XIM_SERVERS + :type xcb:Atom:ATOM + :format 32 + :data-len 1 + :data (funcall (if xcb:lsb + #'xcb:-pack-u4-lsb + #'xcb:-pack-u4) + exwm-xim--@server))) + (xcb:flush exwm-xim--conn)) + +(cl-defun exwm-xim--exit () + "Exit the XIM module." + (exwm--log) + ;; Close IMS communication connections. + (mapc (lambda (i) + (when (vectorp i) + (when (slot-value (elt i 0) 'connected) + (xcb:disconnect (elt i 0))))) + exwm-xim--server-client-plist) + ;; Close the IMS connection. + (unless (and exwm-xim--conn + (slot-value exwm-xim--conn 'connected)) + (cl-return-from exwm-xim--exit)) + ;; Remove exwm-xim from XIM_SERVERS. + (let ((reply (xcb:+request-unchecked+reply exwm-xim--conn + (make-instance 'xcb:GetProperty + :delete 1 + :window exwm--root + :property exwm-xim--XIM_SERVERS + :type xcb:Atom:ATOM + :long-offset 0 + :long-length 1000))) + unpacked-reply pack unpack) + (unless reply + (cl-return-from exwm-xim--exit)) + (setq reply (slot-value reply 'value)) + (unless (> (length reply) 4) + (cl-return-from exwm-xim--exit)) + (setq reply (vconcat reply) + pack (if xcb:lsb #'xcb:-pack-u4-lsb #'xcb:-pack-u4) + unpack (if xcb:lsb #'xcb:-unpack-u4-lsb #'xcb:-unpack-u4)) + (dotimes (i (/ (length reply) 4)) + (push (funcall unpack reply (* i 4)) unpacked-reply)) + (setq unpacked-reply (delq exwm-xim--@server unpacked-reply) + reply (mapcar pack unpacked-reply)) + (xcb:+request exwm-xim--conn + (make-instance 'xcb:ChangeProperty + :mode xcb:PropMode:Replace + :window exwm--root + :property exwm-xim--XIM_SERVERS + :type xcb:Atom:ATOM + :format 32 + :data-len (length reply) + :data reply)) + (xcb:flush exwm-xim--conn)) + (xcb:disconnect exwm-xim--conn) + (setq exwm-xim--conn nil)) + +(defun exwm-xim-enable () + "Enable XIM support for EXWM." + (exwm--log) + (add-hook 'exwm-init-hook #'exwm-xim--init) + (add-hook 'exwm-exit-hook #'exwm-xim--exit)) + + + +(provide 'exwm-xim) + +;;; exwm-xim.el ends here diff --git a/exwm-xsettings.el b/exwm-xsettings.el new file mode 100644 index 0000000000..99d6b9c4ac --- /dev/null +++ b/exwm-xsettings.el @@ -0,0 +1,336 @@ +;;; exwm-xsettings.el --- XSETTINGS Module for EXWM -*- lexical-binding: t -*- + +;; Copyright (C) 2022-2024 Free Software Foundation, Inc. + +;; Author: Steven Allen <steven@stebalien.com> + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Implements the XSETTINGS protocol, allowing Emacs to manage the system theme, +;; fonts, icons, etc. +;; +;; This package can be configured as follows: +;; +;; (require 'exwm-xsettings) +;; (setq exwm-xsettings-theme '("Adwaita" . "Adwaita-dark") ;; light/dark +;; exwm-xsettings `(("Xft/HintStyle" . "hintslight") +;; ("Xft/RGBA" . "rgb") +;; ("Xft/lcdfilter" . "lcddefault") +;; ("Xft/Antialias" . 1) +;; ;; DPI is in 1024ths of an inch, so this is a DPI of +;; ;; 144, equivalent to ;; a scaling factor of 1.5 +;; ;; (144 = 1.5 * 96). +;; ("Xft/DPI" . ,(* 144 1024)) +;; ("Xft/Hinting" . 1))) +;; (exwm-xsettings-enable) +;; +;; To modify these settings at runtime, customize them with +;; `custom-set-variables' or `setopt' (Emacs 29+). E.g., the following will +;; immediately change the icon theme to "Papirus" at runtime, even in running +;; applications: +;; +;; (setopt exwm-xsettings-icon-theme "Papirus") + +;;; Code: + +(require 'xcb-ewmh) +(require 'xcb-xsettings) +(require 'exwm-core) + +(defvar exwm-xsettings--connection nil) +(defvar exwm-xsettings--XSETTINGS_SETTINGS-atom nil) +(defvar exwm-xsettings--XSETTINGS_S0-atom nil) +(defvar exwm-xsettings--selection-owner-window nil) +(defvar exwm-xsettings--serial 0) + +(defun exwm-xsettings--rgba-match (_widget value) + "Return t if VALUE is a valid RGBA color." + (and (numberp value) (<= 0 value 1))) + +(defun exwm-xsettings--custom-set (symbol value) + "Setter used by `exwm-xsettings' customization options. + +SYMBOL is the setting being updated and VALUE is the new value." + (set-default-toplevel-value symbol value) + (exwm-xsettings--update-settings)) + +(defgroup exwm-xsettings nil + "XSETTINGS." + :group 'exwm) + +(defcustom exwm-xsettings nil + "Alist of custom XSETTINGS. +These settings take precedence over `exwm-xsettings-theme' and +`exwm-xsettings-icon-theme'." + :type '(alist :key-type (string :tag "Name") + :value-type (choice :tag "Value" + (string :tag "String") + (integer :tag "Integer") + (list :tag "Color" + (number :tag "Red" + :type-error + "This field should contain a number between 0 and 1." + :match exwm-xsettings--rgba-match) + (number :tag "Green" + :type-error + "This field should contain a number between 0 and 1." + :match exwm-xsettings--rgba-match) + (number :tag "Blue" + :type-error + "This field should contain a number between 0 and 1." + :match exwm-xsettings--rgba-match) + (number :tag "Alpha" + :type-error + "This field should contain a number between 0 and 1." + :match exwm-xsettings--rgba-match + :value 1.0)))) + :initialize #'custom-initialize-default + :set #'exwm-xsettings--custom-set) + +(defcustom exwm-xsettings-theme nil + "The system-wide theme." + :type '(choice (string :tag "Theme") + (cons (string :tag "Light Theme") + (string :tag "Dark Theme"))) + :initialize #'custom-initialize-default + :set #'exwm-xsettings--custom-set) + +(defcustom exwm-xsettings-icon-theme nil + "The system-wide icon theme." + :type '(choice (string :tag "Icon Theme") + (cons (string :tag "Light Icon Theme") + (string :tag "Dark Icon Theme"))) + :initialize #'custom-initialize-default + :set #'exwm-xsettings--custom-set) + +(defalias 'exwm-xsettings--color-dark-p + (if (eval-when-compile (< emacs-major-version 29)) + ;; Borrowed from Emacs 29. + (lambda (rgb) + "Whether RGB is more readable against white than black." + (unless (<= 0 (apply #'min rgb) (apply #'max rgb) 1) + (error "RGB components %S not in [0,1]" rgb)) + (let* ((r (expt (nth 0 rgb) 2.2)) + (g (expt (nth 1 rgb) 2.2)) + (b (expt (nth 2 rgb) 2.2)) + (y (+ (* r 0.2126) (* g 0.7152) (* b 0.0722)))) + (< y 0.325))) + 'color-dark-p)) + +(defun exwm-xsettings--pick-theme (theme) + "Pick a light or dark theme from the given THEME. +If THEME is a string, it's returned directly. +If THEME is a cons of (LIGHT . DARK), the appropriate theme is picked based on +the default face's background color." + (pcase theme + ((cl-type string) theme) + (`(,(cl-type string) . ,(cl-type string)) + (if (exwm-xsettings--color-dark-p (color-name-to-rgb (face-background 'default))) + (cdr theme) (car theme))) + (_ (error "Expected theme to be a string or a pair of strings")))) + +(defun exwm-xsettings--get-settings () + "Get the current settings. +Combines `exwm-xsettings', `exwm-xsettings-theme' (if set), and +`exwm-xsettings-icon-theme' (if set)." + (cl-remove-duplicates + (append + exwm-xsettings + (when exwm-xsettings-theme + (list (cons "Net/ThemeName" (exwm-xsettings--pick-theme exwm-xsettings-theme)))) + (when exwm-xsettings-icon-theme + (list (cons "Net/IconThemeName" (exwm-xsettings--pick-theme exwm-xsettings-icon-theme))))) + :key 'car + :test 'string=)) + +(defun exwm-xsettings--make-settings (settings serial) + "Construct a new settings object. +SETTINGS is an alist of key/value pairs. +SERIAL is a sequence number." + (make-instance 'xcb:xsettings:-Settings + :byte-order (if xcb:lsb 0 1) + :serial serial + :settings-len (length settings) + :settings + (mapcar + (lambda (prop) + (let* ((name (car prop)) + (value (cdr prop)) + (common (list :name name + :name-len (length name) + :last-change-serial serial))) + (pcase value + ((cl-type string) + (apply #'make-instance 'xcb:xsettings:-SETTING_STRING + :value-len (length value) + :value value + common)) + ((cl-type integer) + (apply #'make-instance 'xcb:xsettings:-SETTING_INTEGER + :value value common)) + ((and (cl-type list) (app length (or 3 4))) + ;; Convert from RGB(A) to 16bit integers. + (setq value (mapcar (lambda (x) (round (* x #xffff))) value)) + (apply #'make-instance 'xcb:xsettings:-SETTING_COLOR + :red (pop value) + :green (pop value) + :blue (pop value) + :alpha (or (pop value) #xffff))) + (_ (error "Setting value must be a string, integer, or length 3-4 list"))))) + settings))) + +(defun exwm-xsettings--update-settings () + "Update the xsettings." + (when exwm-xsettings--connection + (setq exwm-xsettings--serial (1+ exwm-xsettings--serial)) + (let* ((settings (exwm-xsettings--get-settings)) + (bytes (xcb:marshal (exwm-xsettings--make-settings settings exwm-xsettings--serial)))) + (xcb:+request exwm-xsettings--connection + (make-instance 'xcb:ChangeProperty + :mode xcb:PropMode:Replace + :window exwm-xsettings--selection-owner-window + :property exwm-xsettings--XSETTINGS_SETTINGS-atom + :type exwm-xsettings--XSETTINGS_SETTINGS-atom + :format 8 + :data-len (length bytes) + :data bytes))) + (xcb:flush exwm-xsettings--connection))) + +(defun exwm-xsettings--on-theme-change (&rest _) + "Called when the Emacs theme is changed." + ;; We only bother updating the xsettings if changing the theme could effect + ;; the settings. + (when (or (consp exwm-xsettings-theme) (consp exwm-xsettings-icon-theme)) + (exwm-xsettings--update-settings))) + +(defun exwm-xsettings--on-SelectionClear (_data _synthetic) + "Called when another xsettings daemon takes over." + (exwm--log "XSETTINGS manager has been replaced.") + (exwm-xsettings--exit)) + +(cl-defun exwm-xsettings--init () + "Initialize the XSETTINGS module." + (exwm--log) + + (cl-assert (not exwm-xsettings--connection)) + + ;; Connect + (setq exwm-xsettings--connection (xcb:connect)) + (set-process-query-on-exit-flag (slot-value exwm-xsettings--connection + 'process) + nil) + + ;; Intern the atoms. + (setq exwm-xsettings--XSETTINGS_SETTINGS-atom + (exwm--intern-atom "_XSETTINGS_SETTINGS" exwm-xsettings--connection) + + exwm-xsettings--XSETTINGS_S0-atom + (exwm--intern-atom "_XSETTINGS_S0" exwm-xsettings--connection)) + + ;; Detect running XSETTINGS managers. + (with-slots (owner) + (xcb:+request-unchecked+reply exwm-xsettings--connection + (make-instance 'xcb:GetSelectionOwner + :selection exwm-xsettings--XSETTINGS_S0-atom)) + (when (/= owner xcb:Window:None) + (xcb:disconnect exwm-xsettings--connection) + (setq exwm-xsettings--connection nil) + (warn "[EXWM] Other XSETTINGS manager detected") + (cl-return-from exwm-xsettings--init))) + + (let ((id(xcb:generate-id exwm-xsettings--connection))) + (setq exwm-xsettings--selection-owner-window id) + + ;; Create a settings window. + (xcb:+request exwm-xsettings--connection + (make-instance 'xcb:CreateWindow + :wid id + :parent exwm--root + :class xcb:WindowClass:InputOnly + :x 0 + :y 0 + :width 1 + :height 1 + :border-width 0 + :depth 0 + :visual 0 + :value-mask xcb:CW:OverrideRedirect + :override-redirect 1)) + + ;; Set _NET_WM_NAME. + (xcb:+request exwm-xsettings--connection + (make-instance 'xcb:ewmh:set-_NET_WM_NAME + :window id + :data "EXWM: exwm-xsettings--selection-owner-window")) + + ;; Apply the XSETTINGS properties. + (exwm-xsettings--update-settings) + + ;; Take ownership and notify. + (xcb:+request exwm-xsettings--connection + (make-instance 'xcb:SetSelectionOwner + :owner id + :selection exwm-xsettings--XSETTINGS_S0-atom + :time xcb:Time:CurrentTime)) + (xcb:+request exwm-xsettings--connection + (make-instance 'xcb:SendEvent + :propagate 0 + :destination exwm--root + :event-mask xcb:EventMask:StructureNotify + :event (xcb:marshal + (make-instance 'xcb:xsettings:-ClientMessage + :window exwm--root + :time xcb:Time:CurrentTime + :selection exwm-xsettings--XSETTINGS_S0-atom + :owner id) + exwm-xsettings--connection))) + + ;; Detect loss of XSETTINGS ownership. + (xcb:+event exwm-xsettings--connection 'xcb:SelectionClear + #'exwm-xsettings--on-SelectionClear) + + (xcb:flush exwm-xsettings--connection)) + + ;; Update the xsettings if/when the theme changes. + (add-hook 'enable-theme-functions #'exwm-xsettings--on-theme-change) + (add-hook 'disable-theme-functions #'exwm-xsettings--on-theme-change)) + +(defun exwm-xsettings--exit () + "Exit the XSETTINGS module." + (exwm--log) + + (when exwm-xsettings--connection + (remove-hook 'enable-theme-functions #'exwm-xsettings--on-theme-change) + (remove-hook 'disable-theme-functions #'exwm-xsettings--on-theme-change) + + (xcb:disconnect exwm-xsettings--connection) + + (setq exwm-xsettings--connection nil + exwm-xsettings--XSETTINGS_SETTINGS-atom nil + exwm-xsettings--XSETTINGS_S0-atom nil + exwm-xsettings--selection-owner-window nil))) + +(defun exwm-xsettings-enable () + "Enable xsettings support for EXWM." + (exwm--log) + (add-hook 'exwm-init-hook #'exwm-xsettings--init) + (add-hook 'exwm-exit-hook #'exwm-xsettings--exit)) + +(provide 'exwm-xsettings) + +;;; exwm-xsettings.el ends here diff --git a/exwm.el b/exwm.el new file mode 100644 index 0000000000..c4900eab48 --- /dev/null +++ b/exwm.el @@ -0,0 +1,1113 @@ +;;; exwm.el --- Emacs X Window Manager -*- lexical-binding: t -*- + +;; Copyright (C) 2015-2024 Free Software Foundation, Inc. + +;; Author: Chris Feng <chris.w.feng@gmail.com> +;; Maintainer: Adrián Medraño Calvo <adrian@medranocalvo.com>, Steven Allen <steven@stebalien.com>, Daniel Mendler <mail@daniel-mendler.de> +;; Version: 0.28 +;; Package-Requires: ((emacs "27.1") (xelb "0.18")) +;; Keywords: unix +;; URL: https://github.com/emacs-exwm/exwm + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Overview +;; -------- +;; EXWM (Emacs X Window Manager) is a full-featured tiling X window manager +;; for Emacs built on top of [XELB](https://github.com/emacs-exwm/xelb). +;; It features: +;; + Fully keyboard-driven operations +;; + Hybrid layout modes (tiling & stacking) +;; + Dynamic workspace support +;; + ICCCM/EWMH compliance +;; Optional features: +;; + RandR (multi-monitor) support +;; + System tray +;; + Input method +;; + Background setting support +;; + XSETTINGS server + +;; Installation & configuration +;; ---------------------------- +;; Here are the minimal steps to get EXWM working: +;; 1. Install XELB and EXWM, and make sure they are in `load-path'. +;; 2. In '~/.emacs', add following lines (please modify accordingly): +;; +;; (require 'exwm) +;; (require 'exwm-config) +;; (exwm-config-example) +;; +;; 3. Link or copy the file 'xinitrc' to '~/.xinitrc'. +;; 4. Launch EXWM in a console (e.g. tty1) with +;; +;; xinit -- vt01 +;; +;; You should additionally hide the menu-bar, tool-bar, etc to increase the +;; usable space. Please check the wiki (https://github.com/emacs-exwm/exwm/wiki) +;; for more detailed instructions on installation, configuration, usage, etc. + +;; References: +;; + dwm (http://dwm.suckless.org/) +;; + i3 wm (https://i3wm.org/) +;; + Also see references within each required library. + +;;; Code: + +(require 'server) +(require 'exwm-core) +(require 'exwm-workspace) +(require 'exwm-layout) +(require 'exwm-floating) +(require 'exwm-manage) +(require 'exwm-input) + +(declare-function x-get-atom-name "C source code" (VALUE &optional FRAME)) + +(defgroup exwm nil + "Emacs X Window Manager." + :tag "EXWM" + :group 'applications + :prefix "exwm-") + +(defcustom exwm-init-hook nil + "Normal hook run when EXWM has just finished initialization." + :type 'hook) + +(defcustom exwm-exit-hook nil + "Normal hook run just before EXWM exits." + :type 'hook) + +(defcustom exwm-update-class-hook nil + "Normal hook run when window class is updated." + :type 'hook) + +(defcustom exwm-update-title-hook nil + "Normal hook run when window title is updated." + :type 'hook) + +(defcustom exwm-blocking-subrs + ;; `x-file-dialog' and `x-select-font' are missing on some Emacs builds, for + ;; example on the X11 Lucid build. + '(x-file-dialog x-popup-dialog x-select-font message-box message-or-box) + "Subrs (primitives) that would normally block EXWM." + :type '(repeat function)) + +(defcustom exwm-replace 'ask + "Whether to replace existing window manager." + :type '(radio (const :tag "Ask" ask) + (const :tag "Replace by default" t) + (const :tag "Do not replace" nil))) + +(defconst exwm--server-name "server-exwm" + "Name of the subordinate Emacs server.") + +(defvar exwm--server-timeout 1 + "Number of seconds to wait for the subordinate Emacs server to exit. +After this time, the server will be killed.") + +(defvar exwm--server-process nil "Process of the subordinate Emacs server.") + +(defun exwm-reset () + "Reset the state of the selected window (non-fullscreen, line-mode, etc)." + (interactive) + (exwm--log) + (with-current-buffer (window-buffer) + (when (derived-mode-p 'exwm-mode) + (when (exwm-layout--fullscreen-p) + (exwm-layout-unset-fullscreen)) + ;; Force refresh + (exwm-layout--refresh) + (call-interactively #'exwm-input-grab-keyboard)))) + +;;;###autoload +(defun exwm-restart () + "Restart EXWM." + (interactive) + (exwm--log) + (when (exwm--confirm-kill-emacs "Restart?" 'no-check) + (let* ((attr (process-attributes (emacs-pid))) + (args (cdr (assq 'args attr))) + (ppid (cdr (assq 'ppid attr))) + (pargs (cdr (assq 'args (process-attributes ppid))))) + (cond + ((= ppid 1) + ;; The parent is the init process. This probably means this + ;; instance is an emacsclient. Anyway, start a control instance + ;; to manage the subsequent ones. + (call-process (car command-line-args)) + (kill-emacs)) + ((string= args pargs) + ;; This is a subordinate instance. Return a magic number to + ;; inform the parent (control instance) to start another one. + (kill-emacs ?R)) + (t + ;; This is the control instance. Keep starting subordinate + ;; instances until told to exit. + ;; Run `server-force-stop' if it exists. + (run-hooks 'kill-emacs-hook) + (with-temp-buffer + (while (= ?R (shell-command-on-region (point) (point) args)))) + (kill-emacs)))))) + +(defun exwm--update-desktop (xwin) + "Update _NET_WM_DESKTOP. +Argument XWIN contains the X window of the `exwm-mode' buffer." + (exwm--log "#x%x" xwin) + (with-current-buffer (exwm--id->buffer xwin) + (let ((reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:ewmh:get-_NET_WM_DESKTOP + :window xwin))) + desktop) + (when reply + (setq desktop (slot-value reply 'value)) + (cond + ((and desktop (= desktop 4294967295.)) + (unless (or (not exwm--floating-frame) + (eq exwm--frame exwm-workspace--current) + (and exwm--desktop + (= desktop exwm--desktop))) + (exwm-layout--show xwin (frame-root-window exwm--floating-frame))) + (setq exwm--desktop desktop)) + ((and desktop + (< desktop (exwm-workspace--count)) + (if exwm--desktop + (/= desktop exwm--desktop) + (/= desktop (exwm-workspace--position exwm--frame)))) + (exwm-workspace-move-window desktop xwin)) + (t + (exwm-workspace--set-desktop xwin))))))) + +(defun exwm--update-window-type (id &optional force) + "Update `exwm-window-type' from _NET_WM_WINDOW_TYPE. +Argument ID contains the X window of the `exwm-mode' buffer. + +When FORCE is nil the update only takes place if +`exwm-window-type' is unset." + (exwm--log "#x%x" id) + (with-current-buffer (exwm--id->buffer id) + (unless (and exwm-window-type (not force)) + (let ((reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:ewmh:get-_NET_WM_WINDOW_TYPE + :window id)))) + (when reply ;nil when destroyed + (setq exwm-window-type (append (slot-value reply 'value) nil))))))) + +(defun exwm--update-class (id &optional force) + "Update `exwm-instance-name' and `exwm-class' from WM_CLASS. +Argument ID contains the X window of the `exwm-mode' buffer. + +When FORCE is nil the update only takes place if any of +`exwm-instance-name' or `exwm-class' is unset." + (exwm--log "#x%x" id) + (with-current-buffer (exwm--id->buffer id) + (unless (and exwm-instance-name exwm-class-name (not force)) + (let ((reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:icccm:get-WM_CLASS :window id)))) + (when reply ;nil when destroyed + (setq exwm-instance-name (slot-value reply 'instance-name) + exwm-class-name (slot-value reply 'class-name)) + (when (and exwm-instance-name exwm-class-name) + (run-hooks 'exwm-update-class-hook))))))) + +(defun exwm--update-utf8-title (id &optional force) + "Update `exwm-title' from _NET_WM_NAME. +Argument ID contains the X window of the `exwm-mode' buffer. + +When FORCE is nil the update only takes place if `exwm-title' is +unset." + (exwm--log "#x%x" id) + (with-current-buffer (exwm--id->buffer id) + (when (or force (not exwm-title)) + (let ((reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:ewmh:get-_NET_WM_NAME :window id)))) + (when reply ;nil when destroyed + (setq exwm-title (slot-value reply 'value)) + (when exwm-title + (setq exwm--title-is-utf8 t) + (run-hooks 'exwm-update-title-hook))))))) + +(defun exwm--update-ctext-title (id &optional force) + "Update `exwm-title' from WM_NAME. +Argument ID contains the X window of the `exwm-mode' buffer. + +When FORCE is nil the update only takes place if `exwm-title' is +unset." + (exwm--log "#x%x" id) + (with-current-buffer (exwm--id->buffer id) + (unless (or exwm--title-is-utf8 + (and exwm-title (not force))) + (let ((reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:icccm:get-WM_NAME :window id)))) + (when reply ;nil when destroyed + (setq exwm-title (slot-value reply 'value)) + (when exwm-title + (run-hooks 'exwm-update-title-hook))))))) + +(defun exwm--update-title (id) + "Update _NET_WM_NAME or WM_NAME. +Argument ID contains the X window of the `exwm-mode' buffer." + (exwm--log "#x%x" id) + (exwm--update-utf8-title id) + (exwm--update-ctext-title id)) + +(defun exwm--update-transient-for (id &optional force) + "Update `exwm-transient-for' from WM_TRANSIENT_FOR. +Argument ID contains the X window of the `exwm-mode' buffer. + +When FORCE is nil the update only takes place if `exwm-title' is +unset." + (exwm--log "#x%x" id) + (with-current-buffer (exwm--id->buffer id) + (unless (and exwm-transient-for (not force)) + (let ((reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:icccm:get-WM_TRANSIENT_FOR + :window id)))) + (when reply ;nil when destroyed + (setq exwm-transient-for (slot-value reply 'value))))))) + +(defun exwm--update-normal-hints (id &optional force) + "Update normal hints from WM_NORMAL_HINTS. +Argument ID contains the X window of the `exwm-mode' buffer. + +When FORCE is nil the update only takes place all of +`exwm--normal-hints-x exwm--normal-hints-y', +`exwm--normal-hints-width exwm--normal-hints-height', +`exwm--normal-hints-min-width exwm--normal-hints-min-height' and +`exwm--normal-hints-max-width exwm--normal-hints-max-height' are +unset." + (exwm--log "#x%x" id) + (with-current-buffer (exwm--id->buffer id) + (unless (and (not force) + (or exwm--normal-hints-x exwm--normal-hints-y + exwm--normal-hints-width exwm--normal-hints-height + exwm--normal-hints-min-width exwm--normal-hints-min-height + exwm--normal-hints-max-width exwm--normal-hints-max-height + ;; FIXME: other fields + )) + (let ((reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:icccm:get-WM_NORMAL_HINTS + :window id)))) + (when (and reply (slot-value reply 'flags)) ;nil when destroyed + (with-slots (flags x y width height min-width min-height max-width + max-height base-width base-height ;; win-gravity + ) + reply + (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:USPosition)) + (setq exwm--normal-hints-x x exwm--normal-hints-y y)) + (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:USSize)) + (setq exwm--normal-hints-width width + exwm--normal-hints-height height)) + (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:PMinSize)) + (setq exwm--normal-hints-min-width min-width + exwm--normal-hints-min-height min-height)) + (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:PMaxSize)) + (setq exwm--normal-hints-max-width max-width + exwm--normal-hints-max-height max-height)) + (unless (or exwm--normal-hints-min-width + (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:PBaseSize))) + (setq exwm--normal-hints-min-width base-width + exwm--normal-hints-min-height base-height)) + ;; (unless (= 0 (logand flags xcb:icccm:WM_SIZE_HINTS:PWinGravity)) + ;; (setq exwm--normal-hints-win-gravity win-gravity)) + (setq exwm--fixed-size + (and exwm--normal-hints-min-width + exwm--normal-hints-min-height + exwm--normal-hints-max-width + exwm--normal-hints-max-height + (/= 0 exwm--normal-hints-min-width) + (/= 0 exwm--normal-hints-min-height) + (= exwm--normal-hints-min-width + exwm--normal-hints-max-width) + (= exwm--normal-hints-min-height + exwm--normal-hints-max-height))))))))) + +(defun exwm--update-hints (id &optional force) + "Update hints from WM_HINTS. +Argument ID contains the X window of the `exwm-mode' buffer. + +When FORCE is nil the update only takes place if both of +`exwm--hints-input' and `exwm--hints-urgency' are unset." + (exwm--log "#x%x" id) + (with-current-buffer (exwm--id->buffer id) + (unless (and (not force) exwm--hints-input exwm--hints-urgency) + (let ((reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:icccm:get-WM_HINTS :window id)))) + (when (and reply (slot-value reply 'flags)) ;nil when destroyed + (with-slots (flags input initial-state) reply + (when flags + (unless (= 0 (logand flags xcb:icccm:WM_HINTS:InputHint)) + (setq exwm--hints-input (when input (= 1 input)))) + (unless (= 0 (logand flags xcb:icccm:WM_HINTS:StateHint)) + (setq exwm-state initial-state)) + (unless (= 0 (logand flags xcb:icccm:WM_HINTS:UrgencyHint)) + (setq exwm--hints-urgency t)))) + (when (and exwm--hints-urgency + (not (eq exwm--frame exwm-workspace--current))) + (unless (frame-parameter exwm--frame 'exwm-urgency) + (set-frame-parameter exwm--frame 'exwm-urgency t) + (setq exwm-workspace--switch-history-outdated t)))))))) + +(defun exwm--update-protocols (id &optional force) + "Update `exwm--protocols' from WM_PROTOCOLS. +Argument ID contains the X window of the `exwm-mode' buffer. + +When FORCE is nil the update only takes place if `exwm--protocols' +is unset." + (exwm--log "#x%x" id) + (with-current-buffer (exwm--id->buffer id) + (unless (and exwm--protocols (not force)) + (let ((reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:icccm:get-WM_PROTOCOLS + :window id)))) + (when reply ;nil when destroyed + (setq exwm--protocols (append (slot-value reply 'value) nil))))))) + +(defun exwm--update-struts-legacy (id) + "Update struts of X window ID from _NET_WM_STRUT." + (exwm--log "#x%x" id) + (let ((pair (assq id exwm-workspace--id-struts-alist)) + reply struts) + (unless (and pair (< 4 (length (cdr pair)))) + (setq reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:ewmh:get-_NET_WM_STRUT + :window id))) + (when reply + (setq struts (slot-value reply 'value)) + (if pair + (setcdr pair struts) + (push (cons id struts) exwm-workspace--id-struts-alist)) + (exwm-workspace--update-struts)) + ;; Update workareas. + (exwm-workspace--update-workareas) + ;; Update workspaces. + (dolist (f exwm-workspace--list) + (exwm-workspace--set-fullscreen f))))) + +(defun exwm--update-struts-partial (id) + "Update struts of X window ID from _NET_WM_STRUT_PARTIAL." + (exwm--log "#x%x" id) + (let ((reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:ewmh:get-_NET_WM_STRUT_PARTIAL + :window id))) + struts pair) + (when reply + (setq struts (slot-value reply 'value) + pair (assq id exwm-workspace--id-struts-alist)) + (if pair + (setcdr pair struts) + (push (cons id struts) exwm-workspace--id-struts-alist)) + (exwm-workspace--update-struts)) + ;; Update workareas. + (exwm-workspace--update-workareas) + ;; Update workspaces. + (dolist (f exwm-workspace--list) + (exwm-workspace--set-fullscreen f)))) + +(defun exwm--update-struts (id) + "Update struts of X window ID from _NET_WM_STRUT_PARTIAL or _NET_WM_STRUT." + (exwm--log "#x%x" id) + (exwm--update-struts-partial id) + (exwm--update-struts-legacy id)) + +(defun exwm--on-PropertyNotify (data _synthetic) + "Handle PropertyNotify event. +DATA contains unmarshalled PropertyNotify event data." + (let ((obj (make-instance 'xcb:PropertyNotify)) + atom id buffer) + (xcb:unmarshal obj data) + (setq id (slot-value obj 'window) + atom (slot-value obj 'atom)) + (exwm--log "atom=%s(%s)" (x-get-atom-name atom exwm-workspace--current) atom) + (setq buffer (exwm--id->buffer id)) + (if (not (buffer-live-p buffer)) + ;; Properties of unmanaged X windows. + (cond ((= atom xcb:Atom:_NET_WM_STRUT) + (exwm--update-struts-legacy id)) + ((= atom xcb:Atom:_NET_WM_STRUT_PARTIAL) + (exwm--update-struts-partial id))) + (with-current-buffer buffer + (cond ((= atom xcb:Atom:_NET_WM_WINDOW_TYPE) + (exwm--update-window-type id t)) + ((= atom xcb:Atom:WM_CLASS) + (exwm--update-class id t)) + ((= atom xcb:Atom:_NET_WM_NAME) + (exwm--update-utf8-title id t)) + ((= atom xcb:Atom:WM_NAME) + (exwm--update-ctext-title id t)) + ((= atom xcb:Atom:WM_TRANSIENT_FOR) + (exwm--update-transient-for id t)) + ((= atom xcb:Atom:WM_NORMAL_HINTS) + (exwm--update-normal-hints id t)) + ((= atom xcb:Atom:WM_HINTS) + (exwm--update-hints id t)) + ((= atom xcb:Atom:WM_PROTOCOLS) + (exwm--update-protocols id t)) + ((= atom xcb:Atom:_NET_WM_USER_TIME)) ;ignored + (t + (exwm--log "Unhandled: %s(%d)" + (x-get-atom-name atom exwm-workspace--current) + atom))))))) + +(defun exwm--on-ClientMessage (raw-data _synthetic) + "Handle ClientMessage event. +RAW-DATA contains unmarshalled ClientMessage event data." + (let ((obj (make-instance 'xcb:ClientMessage)) + type id data) + (xcb:unmarshal obj raw-data) + (setq type (slot-value obj 'type) + id (slot-value obj 'window) + data (slot-value (slot-value obj 'data) 'data32)) + (exwm--log "atom=%s(%s) id=#x%x data=%s" (x-get-atom-name type exwm-workspace--current) + type (or id 0) data) + (cond + ;; _NET_NUMBER_OF_DESKTOPS. + ((= type xcb:Atom:_NET_NUMBER_OF_DESKTOPS) + (let ((current (exwm-workspace--count)) + (requested (elt data 0))) + ;; Only allow increasing/decreasing the workspace number by 1. + (cond + ((< current requested) + (make-frame)) + ((and (> current requested) + (> current 1)) + (let ((frame (car (last exwm-workspace--list)))) + (delete-frame frame)))))) + ;; _NET_CURRENT_DESKTOP. + ((= type xcb:Atom:_NET_CURRENT_DESKTOP) + (exwm-workspace-switch (elt data 0))) + ;; _NET_ACTIVE_WINDOW. + ((= type xcb:Atom:_NET_ACTIVE_WINDOW) + (let ((buffer (exwm--id->buffer id)) + iconic window) + (if (buffer-live-p buffer) + ;; Either an `exwm-mode' buffer (an X window) or a floating frame. + (with-current-buffer buffer + (when (eq exwm--frame exwm-workspace--current) + (if exwm--floating-frame + (select-frame exwm--floating-frame) + (setq iconic (exwm-layout--iconic-state-p)) + (when iconic + ;; State change: iconic => normal. + (set-window-buffer (frame-selected-window exwm--frame) + (current-buffer))) + ;; Focus transfer. + (setq window (get-buffer-window nil t)) + (when (or iconic + (not (eq window (selected-window)))) + (select-window window))))) + ;; A workspace. + (dolist (f exwm-workspace--list) + (when (eq id (frame-parameter f 'exwm-outer-id)) + (x-focus-frame f t)))))) + ;; _NET_CLOSE_WINDOW. + ((= type xcb:Atom:_NET_CLOSE_WINDOW) + (let ((buffer (exwm--id->buffer id))) + (when (buffer-live-p buffer) + (exwm--defer 0 #'kill-buffer buffer)))) + ;; _NET_WM_MOVERESIZE + ((= type xcb:Atom:_NET_WM_MOVERESIZE) + (let ((direction (elt data 2)) + (buffer (exwm--id->buffer id))) + (unless (and buffer + (not (buffer-local-value 'exwm--floating-frame buffer))) + (cond ((= direction + xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_KEYBOARD) + ;; FIXME + ) + ((= direction + xcb:ewmh:_NET_WM_MOVERESIZE_MOVE_KEYBOARD) + ;; FIXME + ) + ((= direction xcb:ewmh:_NET_WM_MOVERESIZE_CANCEL) + (exwm-floating--stop-moveresize)) + ;; In case it's a workspace frame. + ((and (not buffer) + (catch 'break + (dolist (f exwm-workspace--list) + (when (or (eq id (frame-parameter f 'exwm-outer-id)) + (eq id (frame-parameter f 'exwm-id))) + (throw 'break t))) + nil))) + (t + ;; In case it's a floating frame, + ;; move the corresponding X window instead. + (unless buffer + (catch 'break + (dolist (pair exwm--id-buffer-alist) + (with-current-buffer (cdr pair) + (when + (and exwm--floating-frame + (or (eq id + (frame-parameter exwm--floating-frame + 'exwm-outer-id)) + (eq id + (frame-parameter exwm--floating-frame + 'exwm-id)))) + (setq id exwm--id) + (throw 'break nil)))))) + ;; Start to move it. + (exwm-floating--start-moveresize id direction)))))) + ;; _NET_REQUEST_FRAME_EXTENTS + ((= type xcb:Atom:_NET_REQUEST_FRAME_EXTENTS) + (let ((buffer (exwm--id->buffer id)) + top btm) + (if (or (not buffer) + (not (buffer-local-value 'exwm--floating-frame buffer))) + (setq top 0 + btm 0) + (setq top (window-header-line-height) + btm (window-mode-line-height))) + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_FRAME_EXTENTS + :window id + :left 0 + :right 0 + :top top + :bottom btm))) + (xcb:flush exwm--connection)) + ;; _NET_WM_DESKTOP. + ((= type xcb:Atom:_NET_WM_DESKTOP) + (let ((buffer (exwm--id->buffer id))) + (when (buffer-live-p buffer) + (exwm-workspace-move-window (elt data 0) id)))) + ;; _NET_WM_STATE + ((= type xcb:Atom:_NET_WM_STATE) + (let ((action (elt data 0)) + (props (list (elt data 1) (elt data 2))) + (buffer (exwm--id->buffer id)) + props-new) + ;; only support _NET_WM_STATE_FULLSCREEN / _NET_WM_STATE_ADD for frames + (when (and (not buffer) + (memq xcb:Atom:_NET_WM_STATE_FULLSCREEN props) + (= action xcb:ewmh:_NET_WM_STATE_ADD)) + (xcb:+request + exwm--connection + (make-instance 'xcb:ewmh:set-_NET_WM_STATE + :window id + :data (vector xcb:Atom:_NET_WM_STATE_FULLSCREEN))) + (xcb:flush exwm--connection)) + (when buffer ;ensure it's managed + (with-current-buffer buffer + ;; _NET_WM_STATE_FULLSCREEN + (when (or (memq xcb:Atom:_NET_WM_STATE_FULLSCREEN props) + (memq xcb:Atom:_NET_WM_STATE_ABOVE props)) + (cond ((= action xcb:ewmh:_NET_WM_STATE_ADD) + (unless (exwm-layout--fullscreen-p) + (exwm-layout-set-fullscreen id)) + (push xcb:Atom:_NET_WM_STATE_FULLSCREEN props-new)) + ((= action xcb:ewmh:_NET_WM_STATE_REMOVE) + (when (exwm-layout--fullscreen-p) + (exwm-layout-unset-fullscreen id))) + ((= action xcb:ewmh:_NET_WM_STATE_TOGGLE) + (if (exwm-layout--fullscreen-p) + (exwm-layout-unset-fullscreen id) + (exwm-layout-set-fullscreen id) + (push xcb:Atom:_NET_WM_STATE_FULLSCREEN props-new))))) + ;; _NET_WM_STATE_DEMANDS_ATTENTION + ;; FIXME: check (may require other properties set) + (when (memq xcb:Atom:_NET_WM_STATE_DEMANDS_ATTENTION props) + (when (= action xcb:ewmh:_NET_WM_STATE_ADD) + (unless (eq exwm--frame exwm-workspace--current) + (set-frame-parameter exwm--frame 'exwm-urgency t) + (setq exwm-workspace--switch-history-outdated t))) + ;; xcb:ewmh:_NET_WM_STATE_REMOVE? + ;; xcb:ewmh:_NET_WM_STATE_TOGGLE? + ) + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_WM_STATE + :window id :data (vconcat props-new))) + (xcb:flush exwm--connection))))) + ((= type xcb:Atom:WM_PROTOCOLS) + (let ((type (elt data 0))) + (cond ((= type xcb:Atom:_NET_WM_PING) + (setq exwm-manage--ping-lock nil)) + (t (exwm--log "Unhandled WM_PROTOCOLS of type: %d" type))))) + ((= type xcb:Atom:WM_CHANGE_STATE) + (let ((buffer (exwm--id->buffer id))) + (when (and (buffer-live-p buffer) + (= (elt data 0) xcb:icccm:WM_STATE:IconicState)) + (with-current-buffer buffer + (if exwm--floating-frame + (call-interactively #'exwm-floating-hide) + (bury-buffer)))))) + (t + (exwm--log "Unhandled: %s(%d)" + (x-get-atom-name type exwm-workspace--current) type))))) + +(defun exwm--on-SelectionClear (data _synthetic) + "Handle SelectionClear events. +DATA contains unmarshalled SelectionClear event data." + (exwm--log) + (let ((obj (make-instance 'xcb:SelectionClear)) + owner selection) + (xcb:unmarshal obj data) + (setq owner (slot-value obj 'owner) + selection (slot-value obj 'selection)) + (when (and (eq owner exwm--wmsn-window) + (eq selection xcb:Atom:WM_S0)) + (exwm-exit)))) + +(defun exwm--on-delete-terminal (terminal) + "Handle terminal being deleted without Emacs being killed. +This function is Hooked to `delete-terminal-functions'. + +TERMINAL is the terminal being (or that has been) deleted. + +This may happen when invoking `save-buffers-kill-terminal' within an emacsclient +session." + (when (eq terminal exwm--terminal) + (exwm-exit))) + +(defun exwm--init-icccm-ewmh () + "Initialize ICCCM/EWMH support." + (exwm--log) + ;; Handle PropertyNotify event + (xcb:+event exwm--connection 'xcb:PropertyNotify #'exwm--on-PropertyNotify) + ;; Handle relevant client messages + (xcb:+event exwm--connection 'xcb:ClientMessage #'exwm--on-ClientMessage) + ;; Handle SelectionClear + (xcb:+event exwm--connection 'xcb:SelectionClear #'exwm--on-SelectionClear) + ;; Set _NET_SUPPORTED + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_SUPPORTED + :window exwm--root + :data (vector + ;; Root windows properties. + xcb:Atom:_NET_SUPPORTED + xcb:Atom:_NET_CLIENT_LIST + xcb:Atom:_NET_CLIENT_LIST_STACKING + xcb:Atom:_NET_NUMBER_OF_DESKTOPS + xcb:Atom:_NET_DESKTOP_GEOMETRY + xcb:Atom:_NET_DESKTOP_VIEWPORT + xcb:Atom:_NET_CURRENT_DESKTOP + ;; xcb:Atom:_NET_DESKTOP_NAMES + xcb:Atom:_NET_ACTIVE_WINDOW + ;; xcb:Atom:_NET_WORKAREA + xcb:Atom:_NET_SUPPORTING_WM_CHECK + ;; xcb:Atom:_NET_VIRTUAL_ROOTS + ;; xcb:Atom:_NET_DESKTOP_LAYOUT + ;; xcb:Atom:_NET_SHOWING_DESKTOP + + ;; Other root window messages. + xcb:Atom:_NET_CLOSE_WINDOW + ;; xcb:Atom:_NET_MOVERESIZE_WINDOW + xcb:Atom:_NET_WM_MOVERESIZE + ;; xcb:Atom:_NET_RESTACK_WINDOW + xcb:Atom:_NET_REQUEST_FRAME_EXTENTS + + ;; Application window properties. + xcb:Atom:_NET_WM_NAME + ;; xcb:Atom:_NET_WM_VISIBLE_NAME + ;; xcb:Atom:_NET_WM_ICON_NAME + ;; xcb:Atom:_NET_WM_VISIBLE_ICON_NAME + xcb:Atom:_NET_WM_DESKTOP + ;; + xcb:Atom:_NET_WM_WINDOW_TYPE + ;; xcb:Atom:_NET_WM_WINDOW_TYPE_DESKTOP + xcb:Atom:_NET_WM_WINDOW_TYPE_DOCK + xcb:Atom:_NET_WM_WINDOW_TYPE_TOOLBAR + xcb:Atom:_NET_WM_WINDOW_TYPE_MENU + xcb:Atom:_NET_WM_WINDOW_TYPE_UTILITY + xcb:Atom:_NET_WM_WINDOW_TYPE_SPLASH + xcb:Atom:_NET_WM_WINDOW_TYPE_DIALOG + xcb:Atom:_NET_WM_WINDOW_TYPE_DROPDOWN_MENU + xcb:Atom:_NET_WM_WINDOW_TYPE_POPUP_MENU + xcb:Atom:_NET_WM_WINDOW_TYPE_TOOLTIP + xcb:Atom:_NET_WM_WINDOW_TYPE_NOTIFICATION + xcb:Atom:_NET_WM_WINDOW_TYPE_COMBO + xcb:Atom:_NET_WM_WINDOW_TYPE_DND + xcb:Atom:_NET_WM_WINDOW_TYPE_NORMAL + ;; + xcb:Atom:_NET_WM_STATE + ;; xcb:Atom:_NET_WM_STATE_MODAL + ;; xcb:Atom:_NET_WM_STATE_STICKY + ;; xcb:Atom:_NET_WM_STATE_MAXIMIZED_VERT + ;; xcb:Atom:_NET_WM_STATE_MAXIMIZED_HORZ + ;; xcb:Atom:_NET_WM_STATE_SHADED + ;; xcb:Atom:_NET_WM_STATE_SKIP_TASKBAR + ;; xcb:Atom:_NET_WM_STATE_SKIP_PAGER + xcb:Atom:_NET_WM_STATE_HIDDEN + xcb:Atom:_NET_WM_STATE_FULLSCREEN + ;; xcb:Atom:_NET_WM_STATE_ABOVE + ;; xcb:Atom:_NET_WM_STATE_BELOW + xcb:Atom:_NET_WM_STATE_DEMANDS_ATTENTION + ;; xcb:Atom:_NET_WM_STATE_FOCUSED + ;; + xcb:Atom:_NET_WM_ALLOWED_ACTIONS + xcb:Atom:_NET_WM_ACTION_MOVE + xcb:Atom:_NET_WM_ACTION_RESIZE + xcb:Atom:_NET_WM_ACTION_MINIMIZE + ;; xcb:Atom:_NET_WM_ACTION_SHADE + ;; xcb:Atom:_NET_WM_ACTION_STICK + ;; xcb:Atom:_NET_WM_ACTION_MAXIMIZE_HORZ + ;; xcb:Atom:_NET_WM_ACTION_MAXIMIZE_VERT + xcb:Atom:_NET_WM_ACTION_FULLSCREEN + xcb:Atom:_NET_WM_ACTION_CHANGE_DESKTOP + xcb:Atom:_NET_WM_ACTION_CLOSE + ;; xcb:Atom:_NET_WM_ACTION_ABOVE + ;; xcb:Atom:_NET_WM_ACTION_BELOW + ;; + xcb:Atom:_NET_WM_STRUT + xcb:Atom:_NET_WM_STRUT_PARTIAL + ;; xcb:Atom:_NET_WM_ICON_GEOMETRY + ;; xcb:Atom:_NET_WM_ICON + xcb:Atom:_NET_WM_PID + ;; xcb:Atom:_NET_WM_HANDLED_ICONS + ;; xcb:Atom:_NET_WM_USER_TIME + ;; xcb:Atom:_NET_WM_USER_TIME_WINDOW + xcb:Atom:_NET_FRAME_EXTENTS + ;; xcb:Atom:_NET_WM_OPAQUE_REGION + ;; xcb:Atom:_NET_WM_BYPASS_COMPOSITOR + + ;; Window manager protocols. + xcb:Atom:_NET_WM_PING + ;; xcb:Atom:_NET_WM_SYNC_REQUEST + ;; xcb:Atom:_NET_WM_FULLSCREEN_MONITORS + + ;; Other properties. + xcb:Atom:_NET_WM_FULL_PLACEMENT))) + ;; Create a child window for setting _NET_SUPPORTING_WM_CHECK + (let ((new-id (xcb:generate-id exwm--connection))) + (setq exwm--guide-window new-id) + (xcb:+request exwm--connection + (make-instance 'xcb:CreateWindow + :depth 0 + :wid new-id + :parent exwm--root + :x -1 + :y -1 + :width 1 + :height 1 + :border-width 0 + :class xcb:WindowClass:InputOnly + :visual 0 + :value-mask xcb:CW:OverrideRedirect + :override-redirect 1)) + ;; Set _NET_WM_NAME. Must be set to the name of the window manager, as + ;; required by wm-spec. + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_WM_NAME + :window new-id :data "EXWM")) + (dolist (i (list exwm--root new-id)) + ;; Set _NET_SUPPORTING_WM_CHECK + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_SUPPORTING_WM_CHECK + :window i :data new-id)))) + ;; Set _NET_DESKTOP_VIEWPORT (we don't support large desktop). + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_DESKTOP_VIEWPORT + :window exwm--root + :data [0 0])) + (xcb:flush exwm--connection)) + +(defun exwm--wmsn-acquire (replace) + "Acquire the WM_Sn selection. + +REPLACE specifies what to do in case there already is a window +manager. If t, replace it, if nil, abort and ask the user if `ask'." + (exwm--log "%s" replace) + (with-slots (owner) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetSelectionOwner + :selection xcb:Atom:WM_S0)) + (when (/= owner xcb:Window:None) + (when (eq replace 'ask) + (setq replace (yes-or-no-p "Replace existing window manager? "))) + (when (not replace) + (user-error "Other window manager detected"))) + (let ((new-owner (xcb:generate-id exwm--connection))) + (xcb:+request exwm--connection + (make-instance 'xcb:CreateWindow + :depth 0 + :wid new-owner + :parent exwm--root + :x -1 + :y -1 + :width 1 + :height 1 + :border-width 0 + :class xcb:WindowClass:CopyFromParent + :visual 0 + :value-mask 0 + :override-redirect 0)) + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_WM_NAME + :window new-owner :data "EXWM: exwm--wmsn-window")) + (xcb:+request-checked+request-check exwm--connection + (make-instance 'xcb:SetSelectionOwner + :selection xcb:Atom:WM_S0 + :owner new-owner + :time xcb:Time:CurrentTime)) + (with-slots (owner) + (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetSelectionOwner + :selection xcb:Atom:WM_S0)) + (unless (eq owner new-owner) + (error "Could not acquire ownership of WM selection"))) + ;; Wait for the other window manager to terminate. + (when (/= owner xcb:Window:None) + (let (reply) + (cl-dotimes (i exwm--wmsn-acquire-timeout) + (setq reply (xcb:+request-unchecked+reply exwm--connection + (make-instance 'xcb:GetGeometry :drawable owner))) + (when (not reply) + (cl-return)) + (message "Waiting for other window manager to quit... %ds" i) + (sleep-for 1)) + (when reply + (error "Other window manager did not release selection in time")))) + ;; announce + (let* ((cmd (make-instance 'xcb:ClientMessageData + :data32 (vector xcb:Time:CurrentTime + xcb:Atom:WM_S0 + new-owner + 0 + 0))) + (cm (make-instance 'xcb:ClientMessage + :window exwm--root + :format 32 + :type xcb:Atom:MANAGER + :data cmd)) + (se (make-instance 'xcb:SendEvent + :propagate 0 + :destination exwm--root + :event-mask xcb:EventMask:NoEvent + :event (xcb:marshal cm exwm--connection)))) + (xcb:+request exwm--connection se)) + (setq exwm--wmsn-window new-owner)))) + +;;;###autoload +(cl-defun exwm-init (&optional frame) + "Initialize EXWM. +FRAME, if given, indicates the X display EXWM should manage." + (interactive) + (exwm--log "%s" frame) + (if frame + ;; The frame might not be selected if it's created by emacslicnet. + (select-frame-set-input-focus frame) + (setq frame (selected-frame))) + (when (not (eq 'x (framep frame))) + (message "[EXWM] Not running under X environment") + (cl-return-from exwm-init)) + (when exwm--connection + (exwm--log "EXWM already running") + (cl-return-from exwm-init)) + (condition-case err + (progn + (exwm-enable 'undo) ;never initialize again + (setq exwm--terminal (frame-terminal frame)) + (setq exwm--connection (xcb:connect)) + (set-process-query-on-exit-flag (slot-value exwm--connection 'process) + nil) ;prevent query message on exit + (setq exwm--root + (slot-value (car (slot-value + (xcb:get-setup exwm--connection) 'roots)) + 'root)) + ;; Initialize ICCCM/EWMH support + (xcb:icccm:init exwm--connection t) + (xcb:ewmh:init exwm--connection t) + ;; Try to register window manager selection. + (exwm--wmsn-acquire exwm-replace) + (when (xcb:+request-checked+request-check exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window exwm--root + :value-mask xcb:CW:EventMask + :event-mask + xcb:EventMask:SubstructureRedirect)) + (error "Other window manager is running")) + ;; Disable some features not working well with EXWM + (setq use-dialog-box nil + confirm-kill-emacs #'exwm--confirm-kill-emacs) + (advice-add 'save-buffers-kill-terminal + :before-while #'exwm--confirm-kill-terminal) + ;; Clean up if the terminal is deleted. + (add-hook 'delete-terminal-functions 'exwm--on-delete-terminal) + (exwm--lock) + (exwm--init-icccm-ewmh) + (exwm-layout--init) + (exwm-floating--init) + (exwm-manage--init) + (exwm-workspace--init) + (exwm-input--init) + (exwm--unlock) + (exwm-workspace--post-init) + (exwm-input--post-init) + (run-hooks 'exwm-init-hook) + ;; Manage existing windows + (exwm-manage--scan)) + (user-error) + ((quit error) + (exwm-exit) + ;; Rethrow error + (warn "[EXWM] EXWM fails to start (%s: %s)" (car err) (cdr err))))) + + +;;;###autoload +(defun exwm-exit () + "Exit EXWM." + (interactive) + (exwm--log) + (run-hooks 'exwm-exit-hook) + (setq confirm-kill-emacs nil) + ;; Exit modules. + (when exwm--connection + (exwm-input--exit) + (exwm-manage--exit) + (exwm-workspace--exit) + (exwm-floating--exit) + (exwm-layout--exit) + (xcb:flush exwm--connection) + (xcb:disconnect exwm--connection)) + (setq exwm--connection nil) + (setq exwm--terminal nil) + (exwm--log "Exited")) + +;;;###autoload +(defun exwm-enable (&optional undo) + "Enable/Disable EXWM." + (exwm--log "%s" undo) + (pcase undo + (`undo ;prevent reinitialization + (remove-hook 'window-setup-hook #'exwm-init) + (remove-hook 'after-make-frame-functions #'exwm-init)) + (`undo-all ;attempt to revert everything + (remove-hook 'window-setup-hook #'exwm-init) + (remove-hook 'after-make-frame-functions #'exwm-init) + (remove-hook 'kill-emacs-hook #'exwm--server-stop) + (dolist (i exwm-blocking-subrs) + (advice-remove i #'exwm--server-eval-at))) + (_ ;enable EXWM + (setq frame-resize-pixelwise t ;mandatory; before init + window-resize-pixelwise t) + ;; Ignore unrecognized command line arguments. This can be helpful + ;; when EXWM is launched by some session manager. + (push #'vector command-line-functions) + ;; In case EXWM is to be started from a graphical Emacs instance. + (add-hook 'window-setup-hook #'exwm-init t) + ;; In case EXWM is to be started with emacsclient. + (add-hook 'after-make-frame-functions #'exwm-init t) + ;; Manage the subordinate Emacs server. + (add-hook 'kill-emacs-hook #'exwm--server-stop) + (dolist (i exwm-blocking-subrs) + (advice-add i :around #'exwm--server-eval-at))))) + +(defun exwm--server-stop () + "Stop the subordinate Emacs server." + (exwm--log) + (when exwm--server-process + (when (process-live-p exwm--server-process) + (cl-loop + initially (signal-process exwm--server-process 'TERM) + while (process-live-p exwm--server-process) + repeat (* 10 exwm--server-timeout) + do (sit-for 0.1))) + (delete-process exwm--server-process) + (setq exwm--server-process nil))) + +(defun exwm--server-eval-at (function &rest args) + "Wrapper of `server-eval-at' used to advice subrs. +FUNCTION is the function to be evaluated, ARGS are the arguments." + ;; Start the subordinate Emacs server if it's not alive + (exwm--log "%s %s" function args) + (unless (server-running-p exwm--server-name) + (when exwm--server-process (delete-process exwm--server-process)) + (setq exwm--server-process + (start-process exwm--server-name + nil + (car command-line-args) ;The executable file + "-d" (frame-parameter nil 'display) + "-Q" + (concat "--fg-daemon=" exwm--server-name) + "--eval" + ;; Create an invisible frame + "(make-frame '((window-system . x) (visibility)))")) + (while (not (server-running-p exwm--server-name)) + (sit-for 0.001))) + (server-eval-at + exwm--server-name + `(progn (select-frame (car (frame-list))) + (let ((result ,(nconc (list (make-symbol (subr-name function))) + args))) + (pcase (type-of result) + ;; Return the name of a buffer + (`buffer (buffer-name result)) + ;; We blindly convert all font objects to their XLFD names. This + ;; might cause problems of course, but it still has a chance to + ;; work (whereas directly passing font objects would merely + ;; raise errors). + ((or `font-entity `font-object `font-spec) + (font-xlfd-name result)) + ;; Passing following types makes little sense + ((or `compiled-function `finalizer `frame `hash-table `marker + `overlay `process `window `window-configuration)) + ;; Passing the name of a subr + (`subr (make-symbol (subr-name result))) + ;; For other types, return the value as-is. + (t result)))))) + +(defun exwm--confirm-kill-terminal (&optional _) + "Confirm before killing terminal." + ;; This is invoked instead of `save-buffers-kill-emacs' (C-x C-c) on client + ;; frames. + (if (exwm--terminal-p) + (exwm--confirm-kill-emacs "Kill terminal?") + t)) + +(defun exwm--confirm-kill-emacs (prompt &optional force) + "Confirm before exiting Emacs. +PROMPT a reason to present to the user. +If FORCE is nil, ask the user for confirmation. +If FORCE is the symbol `no-check', ask if there are unsaved buffers. +If FORCE is any other non-nil value, force killing of Emacs." + (exwm--log) + (when (cond + ((and force (not (eq force 'no-check))) + ;; Force killing Emacs. + t) + ((or (eq force 'no-check) (not exwm--id-buffer-alist)) + ;; Check if there's any unsaved file. + (pcase (catch 'break + (let ((kill-emacs-query-functions + (append kill-emacs-query-functions + (list (lambda () + (throw 'break 'break)))))) + (save-buffers-kill-emacs))) + (`break (y-or-n-p prompt)) + (x x))) + (t + (yes-or-no-p (format "[EXWM] %d X window(s) will be destroyed. %s" + (length exwm--id-buffer-alist) prompt)))) + ;; Run `kill-emacs-hook' (`server-force-stop' excluded) before Emacs + ;; frames are unmapped so that errors (if any) can be visible. + (if (memq #'server-force-stop kill-emacs-hook) + (progn + (setq kill-emacs-hook (delq #'server-force-stop kill-emacs-hook)) + (run-hooks 'kill-emacs-hook) + (setq kill-emacs-hook (list #'server-force-stop))) + (run-hooks 'kill-emacs-hook) + (setq kill-emacs-hook nil)) + ;; Exit each module, destroying all resources created by this connection. + (exwm-exit) + ;; Set the return value. + t)) + + + +(provide 'exwm) + +;;; exwm.el ends here diff --git a/favicon.ico b/favicon.ico deleted file mode 100644 index 56ff59384f..0000000000 --- a/favicon.ico +++ /dev/null Binary files differdiff --git a/filter.c b/filter.c deleted file mode 100644 index 70f5b74998..0000000000 --- a/filter.c +++ /dev/null @@ -1,457 +0,0 @@ -/* filter.c: filter framework functions - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "html.h" -#ifndef NO_LUA -#include <dlfcn.h> -#include <lua.h> -#include <lualib.h> -#include <lauxlib.h> -#endif - -static inline void reap_filter(struct cgit_filter *filter) -{ - if (filter && filter->cleanup) - filter->cleanup(filter); -} - -void cgit_cleanup_filters(void) -{ - int i; - reap_filter(ctx.cfg.about_filter); - reap_filter(ctx.cfg.commit_filter); - reap_filter(ctx.cfg.source_filter); - reap_filter(ctx.cfg.email_filter); - reap_filter(ctx.cfg.owner_filter); - reap_filter(ctx.cfg.auth_filter); - for (i = 0; i < cgit_repolist.count; ++i) { - reap_filter(cgit_repolist.repos[i].about_filter); - reap_filter(cgit_repolist.repos[i].commit_filter); - reap_filter(cgit_repolist.repos[i].source_filter); - reap_filter(cgit_repolist.repos[i].email_filter); - reap_filter(cgit_repolist.repos[i].owner_filter); - } -} - -static int open_exec_filter(struct cgit_filter *base, va_list ap) -{ - struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base; - int pipe_fh[2]; - int i; - - for (i = 0; i < filter->base.argument_count; i++) - filter->argv[i + 1] = va_arg(ap, char *); - - filter->old_stdout = chk_positive(dup(STDOUT_FILENO), - "Unable to duplicate STDOUT"); - chk_zero(pipe(pipe_fh), "Unable to create pipe to subprocess"); - filter->pid = chk_non_negative(fork(), "Unable to create subprocess"); - if (filter->pid == 0) { - close(pipe_fh[1]); - chk_non_negative(dup2(pipe_fh[0], STDIN_FILENO), - "Unable to use pipe as STDIN"); - execvp(filter->cmd, filter->argv); - die_errno("Unable to exec subprocess %s", filter->cmd); - } - close(pipe_fh[0]); - chk_non_negative(dup2(pipe_fh[1], STDOUT_FILENO), - "Unable to use pipe as STDOUT"); - close(pipe_fh[1]); - return 0; -} - -static int close_exec_filter(struct cgit_filter *base) -{ - struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base; - int i, exit_status = 0; - - chk_non_negative(dup2(filter->old_stdout, STDOUT_FILENO), - "Unable to restore STDOUT"); - close(filter->old_stdout); - if (filter->pid < 0) - goto done; - waitpid(filter->pid, &exit_status, 0); - if (WIFEXITED(exit_status)) - goto done; - die("Subprocess %s exited abnormally", filter->cmd); - -done: - for (i = 0; i < filter->base.argument_count; i++) - filter->argv[i + 1] = NULL; - return WEXITSTATUS(exit_status); - -} - -static void fprintf_exec_filter(struct cgit_filter *base, FILE *f, const char *prefix) -{ - struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base; - fprintf(f, "%sexec:%s\n", prefix, filter->cmd); -} - -static void cleanup_exec_filter(struct cgit_filter *base) -{ - struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base; - if (filter->argv) { - free(filter->argv); - filter->argv = NULL; - } - if (filter->cmd) { - free(filter->cmd); - filter->cmd = NULL; - } -} - -static struct cgit_filter *new_exec_filter(const char *cmd, int argument_count) -{ - struct cgit_exec_filter *f; - int args_size = 0; - - f = xmalloc(sizeof(*f)); - /* We leave argv for now and assign it below. */ - cgit_exec_filter_init(f, xstrdup(cmd), NULL); - f->base.argument_count = argument_count; - args_size = (2 + argument_count) * sizeof(char *); - f->argv = xmalloc(args_size); - memset(f->argv, 0, args_size); - f->argv[0] = f->cmd; - return &f->base; -} - -void cgit_exec_filter_init(struct cgit_exec_filter *filter, char *cmd, char **argv) -{ - memset(filter, 0, sizeof(*filter)); - filter->base.open = open_exec_filter; - filter->base.close = close_exec_filter; - filter->base.fprintf = fprintf_exec_filter; - filter->base.cleanup = cleanup_exec_filter; - filter->cmd = cmd; - filter->argv = argv; - /* The argument count for open_filter is zero by default, unless called from new_filter, above. */ - filter->base.argument_count = 0; -} - -#ifdef NO_LUA -void cgit_init_filters(void) -{ -} -#endif - -#ifndef NO_LUA -static ssize_t (*libc_write)(int fd, const void *buf, size_t count); -static ssize_t (*filter_write)(struct cgit_filter *base, const void *buf, size_t count) = NULL; -static struct cgit_filter *current_write_filter = NULL; - -void cgit_init_filters(void) -{ - libc_write = dlsym(RTLD_NEXT, "write"); - if (!libc_write) - die("Could not locate libc's write function"); -} - -ssize_t write(int fd, const void *buf, size_t count) -{ - if (fd != STDOUT_FILENO || !filter_write) - return libc_write(fd, buf, count); - return filter_write(current_write_filter, buf, count); -} - -static inline void hook_write(struct cgit_filter *filter, ssize_t (*new_write)(struct cgit_filter *base, const void *buf, size_t count)) -{ - /* We want to avoid buggy nested patterns. */ - assert(filter_write == NULL); - assert(current_write_filter == NULL); - current_write_filter = filter; - filter_write = new_write; -} - -static inline void unhook_write(void) -{ - assert(filter_write != NULL); - assert(current_write_filter != NULL); - filter_write = NULL; - current_write_filter = NULL; -} - -struct lua_filter { - struct cgit_filter base; - char *script_file; - lua_State *lua_state; -}; - -static void error_lua_filter(struct lua_filter *filter) -{ - die("Lua error in %s: %s", filter->script_file, lua_tostring(filter->lua_state, -1)); - lua_pop(filter->lua_state, 1); -} - -static ssize_t write_lua_filter(struct cgit_filter *base, const void *buf, size_t count) -{ - struct lua_filter *filter = (struct lua_filter *)base; - - lua_getglobal(filter->lua_state, "filter_write"); - lua_pushlstring(filter->lua_state, buf, count); - if (lua_pcall(filter->lua_state, 1, 0, 0)) { - error_lua_filter(filter); - errno = EIO; - return -1; - } - return count; -} - -static inline int hook_lua_filter(lua_State *lua_state, void (*fn)(const char *txt)) -{ - const char *str; - ssize_t (*save_filter_write)(struct cgit_filter *base, const void *buf, size_t count); - struct cgit_filter *save_filter; - - str = lua_tostring(lua_state, 1); - if (!str) - return 0; - - save_filter_write = filter_write; - save_filter = current_write_filter; - unhook_write(); - fn(str); - hook_write(save_filter, save_filter_write); - - return 0; -} - -static int html_lua_filter(lua_State *lua_state) -{ - return hook_lua_filter(lua_state, html); -} - -static int html_txt_lua_filter(lua_State *lua_state) -{ - return hook_lua_filter(lua_state, html_txt); -} - -static int html_attr_lua_filter(lua_State *lua_state) -{ - return hook_lua_filter(lua_state, html_attr); -} - -static int html_url_path_lua_filter(lua_State *lua_state) -{ - return hook_lua_filter(lua_state, html_url_path); -} - -static int html_url_arg_lua_filter(lua_State *lua_state) -{ - return hook_lua_filter(lua_state, html_url_arg); -} - -static int html_include_lua_filter(lua_State *lua_state) -{ - return hook_lua_filter(lua_state, (void (*)(const char *))html_include); -} - -static void cleanup_lua_filter(struct cgit_filter *base) -{ - struct lua_filter *filter = (struct lua_filter *)base; - - if (!filter->lua_state) - return; - - lua_close(filter->lua_state); - filter->lua_state = NULL; - if (filter->script_file) { - free(filter->script_file); - filter->script_file = NULL; - } -} - -static int init_lua_filter(struct lua_filter *filter) -{ - if (filter->lua_state) - return 0; - - if (!(filter->lua_state = luaL_newstate())) - return 1; - - luaL_openlibs(filter->lua_state); - - lua_pushcfunction(filter->lua_state, html_lua_filter); - lua_setglobal(filter->lua_state, "html"); - lua_pushcfunction(filter->lua_state, html_txt_lua_filter); - lua_setglobal(filter->lua_state, "html_txt"); - lua_pushcfunction(filter->lua_state, html_attr_lua_filter); - lua_setglobal(filter->lua_state, "html_attr"); - lua_pushcfunction(filter->lua_state, html_url_path_lua_filter); - lua_setglobal(filter->lua_state, "html_url_path"); - lua_pushcfunction(filter->lua_state, html_url_arg_lua_filter); - lua_setglobal(filter->lua_state, "html_url_arg"); - lua_pushcfunction(filter->lua_state, html_include_lua_filter); - lua_setglobal(filter->lua_state, "html_include"); - - if (luaL_dofile(filter->lua_state, filter->script_file)) { - error_lua_filter(filter); - lua_close(filter->lua_state); - filter->lua_state = NULL; - return 1; - } - return 0; -} - -static int open_lua_filter(struct cgit_filter *base, va_list ap) -{ - struct lua_filter *filter = (struct lua_filter *)base; - int i; - - if (init_lua_filter(filter)) - return 1; - - hook_write(base, write_lua_filter); - - lua_getglobal(filter->lua_state, "filter_open"); - for (i = 0; i < filter->base.argument_count; ++i) - lua_pushstring(filter->lua_state, va_arg(ap, char *)); - if (lua_pcall(filter->lua_state, filter->base.argument_count, 0, 0)) { - error_lua_filter(filter); - return 1; - } - return 0; -} - -static int close_lua_filter(struct cgit_filter *base) -{ - struct lua_filter *filter = (struct lua_filter *)base; - int ret = 0; - - lua_getglobal(filter->lua_state, "filter_close"); - if (lua_pcall(filter->lua_state, 0, 1, 0)) { - error_lua_filter(filter); - ret = -1; - } else { - ret = lua_tonumber(filter->lua_state, -1); - lua_pop(filter->lua_state, 1); - } - - unhook_write(); - return ret; -} - -static void fprintf_lua_filter(struct cgit_filter *base, FILE *f, const char *prefix) -{ - struct lua_filter *filter = (struct lua_filter *)base; - fprintf(f, "%slua:%s\n", prefix, filter->script_file); -} - - -static struct cgit_filter *new_lua_filter(const char *cmd, int argument_count) -{ - struct lua_filter *filter; - - filter = xmalloc(sizeof(*filter)); - memset(filter, 0, sizeof(*filter)); - filter->base.open = open_lua_filter; - filter->base.close = close_lua_filter; - filter->base.fprintf = fprintf_lua_filter; - filter->base.cleanup = cleanup_lua_filter; - filter->base.argument_count = argument_count; - filter->script_file = xstrdup(cmd); - - return &filter->base; -} - -#endif - - -int cgit_open_filter(struct cgit_filter *filter, ...) -{ - int result; - va_list ap; - if (!filter) - return 0; - va_start(ap, filter); - result = filter->open(filter, ap); - va_end(ap); - return result; -} - -int cgit_close_filter(struct cgit_filter *filter) -{ - if (!filter) - return 0; - return filter->close(filter); -} - -void cgit_fprintf_filter(struct cgit_filter *filter, FILE *f, const char *prefix) -{ - filter->fprintf(filter, f, prefix); -} - - - -static const struct { - const char *prefix; - struct cgit_filter *(*ctor)(const char *cmd, int argument_count); -} filter_specs[] = { - { "exec", new_exec_filter }, -#ifndef NO_LUA - { "lua", new_lua_filter }, -#endif -}; - -struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype) -{ - char *colon; - int i; - size_t len; - int argument_count; - - if (!cmd || !cmd[0]) - return NULL; - - colon = strchr(cmd, ':'); - len = colon - cmd; - /* - * In case we're running on Windows, don't allow a single letter before - * the colon. - */ - if (len == 1) - colon = NULL; - - switch (filtertype) { - case AUTH: - argument_count = 12; - break; - - case EMAIL: - argument_count = 2; - break; - - case OWNER: - argument_count = 0; - break; - - case SOURCE: - case ABOUT: - argument_count = 1; - break; - - case COMMIT: - default: - argument_count = 0; - break; - } - - /* If no prefix is given, exec filter is the default. */ - if (!colon) - return new_exec_filter(cmd, argument_count); - - for (i = 0; i < ARRAY_SIZE(filter_specs); i++) { - if (len == strlen(filter_specs[i].prefix) && - !strncmp(filter_specs[i].prefix, cmd, len)) - return filter_specs[i].ctor(colon + 1, argument_count); - } - - die("Invalid filter type: %.*s", (int) len, cmd); -} diff --git a/filters/about-formatting.sh b/filters/about-formatting.sh deleted file mode 100755 index 85daf9c26b..0000000000 --- a/filters/about-formatting.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/sh - -# This may be used with the about-filter or repo.about-filter setting in cgitrc. -# It passes formatting of about pages to differing programs, depending on the usage. - -# Markdown support requires python and markdown-python. -# RestructuredText support requires python and docutils. -# Man page support requires groff. - -# The following environment variables can be used to retrieve the configuration -# of the repository for which this script is called: -# CGIT_REPO_URL ( = repo.url setting ) -# CGIT_REPO_NAME ( = repo.name setting ) -# CGIT_REPO_PATH ( = repo.path setting ) -# CGIT_REPO_OWNER ( = repo.owner setting ) -# CGIT_REPO_DEFBRANCH ( = repo.defbranch setting ) -# CGIT_REPO_SECTION ( = section setting ) -# CGIT_REPO_CLONE_URL ( = repo.clone-url setting ) - -cd "$(dirname $0)/html-converters/" -case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in - *.markdown|*.mdown|*.md|*.mkd) exec ./md2html; ;; - *.rst) exec ./rst2html; ;; - *.[1-9]) exec ./man2html; ;; - *.htm|*.html) exec cat; ;; - *.txt|*) exec ./txt2html; ;; -esac diff --git a/filters/commit-links.sh b/filters/commit-links.sh deleted file mode 100755 index 58819524ce..0000000000 --- a/filters/commit-links.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh -# This script can be used to generate links in commit messages. -# -# To use this script, refer to this file with either the commit-filter or the -# repo.commit-filter options in cgitrc. -# -# The following environment variables can be used to retrieve the configuration -# of the repository for which this script is called: -# CGIT_REPO_URL ( = repo.url setting ) -# CGIT_REPO_NAME ( = repo.name setting ) -# CGIT_REPO_PATH ( = repo.path setting ) -# CGIT_REPO_OWNER ( = repo.owner setting ) -# CGIT_REPO_DEFBRANCH ( = repo.defbranch setting ) -# CGIT_REPO_SECTION ( = section setting ) -# CGIT_REPO_CLONE_URL ( = repo.clone-url setting ) -# - -regex='' - -# This expression generates links to commits referenced by their SHA1. -regex=$regex' -s|\b([0-9a-fA-F]{7,40})\b|<a href="./?id=\1">\1</a>|g' - -# This expression generates links to a fictional bugtracker. -regex=$regex' -s|#([0-9]+)\b|<a href="http://bugs.example.com/?bug=\1">#\1</a>|g' - -sed -re "$regex" diff --git a/filters/email-gravatar.lua b/filters/email-gravatar.lua deleted file mode 100644 index c39b490d6b..0000000000 --- a/filters/email-gravatar.lua +++ /dev/null @@ -1,35 +0,0 @@ --- This script may be used with the email-filter or repo.email-filter settings in cgitrc. --- It adds gravatar icons to author names. It is designed to be used with the lua: --- prefix in filters. It is much faster than the corresponding python script. --- --- Requirements: --- luaossl --- <http://25thandclement.com/~william/projects/luaossl.html> --- - -local digest = require("openssl.digest") - -function md5_hex(input) - local b = digest.new("md5"):final(input) - local x = "" - for i = 1, #b do - x = x .. string.format("%.2x", string.byte(b, i)) - end - return x -end - -function filter_open(email, page) - buffer = "" - md5 = md5_hex(email:sub(2, -2):lower()) -end - -function filter_close() - html("<img src='//www.gravatar.com/avatar/" .. md5 .. "?s=13&d=retro' width='13' height='13' alt='Gravatar' /> " .. buffer) - return 0 -end - -function filter_write(str) - buffer = buffer .. str -end - - diff --git a/filters/email-gravatar.py b/filters/email-gravatar.py deleted file mode 100755 index d70440ea54..0000000000 --- a/filters/email-gravatar.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 - -# Please prefer the email-gravatar.lua using lua: as a prefix over this script. This -# script is very slow, in comparison. -# -# This script may be used with the email-filter or repo.email-filter settings in cgitrc. -# -# The following environment variables can be used to retrieve the configuration -# of the repository for which this script is called: -# CGIT_REPO_URL ( = repo.url setting ) -# CGIT_REPO_NAME ( = repo.name setting ) -# CGIT_REPO_PATH ( = repo.path setting ) -# CGIT_REPO_OWNER ( = repo.owner setting ) -# CGIT_REPO_DEFBRANCH ( = repo.defbranch setting ) -# CGIT_REPO_SECTION ( = section setting ) -# CGIT_REPO_CLONE_URL ( = repo.clone-url setting ) -# -# It receives an email address on argv[1] and text on stdin. It prints -# to stdout that text prepended by a gravatar at 10pt. - -import sys -import hashlib -import codecs - -email = sys.argv[1].lower().strip() -if email[0] == '<': - email = email[1:] -if email[-1] == '>': - email = email[0:-1] - -page = sys.argv[2] - -sys.stdin = codecs.getreader("utf-8")(sys.stdin.detach()) -sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach()) - -md5 = hashlib.md5(email.encode()).hexdigest() -text = sys.stdin.read().strip() - -print("<img src='//www.gravatar.com/avatar/" + md5 + "?s=13&d=retro' width='13' height='13' alt='Gravatar' /> " + text) diff --git a/filters/email-libravatar.lua b/filters/email-libravatar.lua deleted file mode 100644 index 7336baf830..0000000000 --- a/filters/email-libravatar.lua +++ /dev/null @@ -1,36 +0,0 @@ --- This script may be used with the email-filter or repo.email-filter settings in cgitrc. --- It adds libravatar icons to author names. It is designed to be used with the lua: --- prefix in filters. --- --- Requirements: --- luaossl --- <http://25thandclement.com/~william/projects/luaossl.html> --- - -local digest = require("openssl.digest") - -function md5_hex(input) - local b = digest.new("md5"):final(input) - local x = "" - for i = 1, #b do - x = x .. string.format("%.2x", string.byte(b, i)) - end - return x -end - -function filter_open(email, page) - buffer = "" - md5 = md5_hex(email:sub(2, -2):lower()) -end - -function filter_close() - baseurl = os.getenv("HTTPS") and "https://seccdn.libravatar.org/" or "http://cdn.libravatar.org/" - html("<img src='" .. baseurl .. "avatar/" .. md5 .. "?s=13&d=retro' width='13' height='13' alt='Libravatar' /> " .. buffer) - return 0 -end - -function filter_write(str) - buffer = buffer .. str -end - - diff --git a/filters/file-authentication.lua b/filters/file-authentication.lua deleted file mode 100644 index 024880463c..0000000000 --- a/filters/file-authentication.lua +++ /dev/null @@ -1,359 +0,0 @@ --- This script may be used with the auth-filter. --- --- Requirements: --- luaossl --- <http://25thandclement.com/~william/projects/luaossl.html> --- luaposix --- <https://github.com/luaposix/luaposix> --- -local sysstat = require("posix.sys.stat") -local unistd = require("posix.unistd") -local rand = require("openssl.rand") -local hmac = require("openssl.hmac") - --- This file should contain a series of lines in the form of: --- username1:hash1 --- username2:hash2 --- username3:hash3 --- ... --- Hashes can be generated using something like `mkpasswd -m sha-512 -R 300000`. --- This file should not be world-readable. -local users_filename = "/etc/cgit-auth/users" - --- This file should contain a series of lines in the form of: --- groupname1:username1,username2,username3,... --- ... -local groups_filename = "/etc/cgit-auth/groups" - --- This file should contain a series of lines in the form of: --- reponame1:groupname1,groupname2,groupname3,... --- ... -local repos_filename = "/etc/cgit-auth/repos" - --- Set this to a path this script can write to for storing a persistent --- cookie secret, which should not be world-readable. -local secret_filename = "/var/cache/cgit/auth-secret" - --- --- --- Authentication functions follow below. Swap these out if you want different authentication semantics. --- --- - --- Looks up a hash for a given user. -function lookup_hash(user) - local line - for line in io.lines(users_filename) do - local u, h = string.match(line, "(.-):(.+)") - if u:lower() == user:lower() then - return h - end - end - return nil -end - --- Looks up users for a given repo. -function lookup_users(repo) - local users = nil - local groups = nil - local line, group, user - for line in io.lines(repos_filename) do - local r, g = string.match(line, "(.-):(.+)") - if r == repo then - groups = { } - for group in string.gmatch(g, "([^,]+)") do - groups[group:lower()] = true - end - break - end - end - if groups == nil then - return nil - end - for line in io.lines(groups_filename) do - local g, u = string.match(line, "(.-):(.+)") - if groups[g:lower()] then - if users == nil then - users = { } - end - for user in string.gmatch(u, "([^,]+)") do - users[user:lower()] = true - end - end - end - return users -end - - --- Sets HTTP cookie headers based on post and sets up redirection. -function authenticate_post() - local hash = lookup_hash(post["username"]) - local redirect = validate_value("redirect", post["redirect"]) - - if redirect == nil then - not_found() - return 0 - end - - redirect_to(redirect) - - if hash == nil or hash ~= unistd.crypt(post["password"], hash) then - set_cookie("cgitauth", "") - else - -- One week expiration time - local username = secure_value("username", post["username"], os.time() + 604800) - set_cookie("cgitauth", username) - end - - html("\n") - return 0 -end - - --- Returns 1 if the cookie is valid and 0 if it is not. -function authenticate_cookie() - accepted_users = lookup_users(cgit["repo"]) - if accepted_users == nil then - -- We return as valid if the repo is not protected. - return 1 - end - - local username = validate_value("username", get_cookie(http["cookie"], "cgitauth")) - if username == nil or not accepted_users[username:lower()] then - return 0 - else - return 1 - end -end - --- Prints the html for the login form. -function body() - html("<h2>Authentication Required</h2>") - html("<form method='post' action='") - html_attr(cgit["login"]) - html("'>") - html("<input type='hidden' name='redirect' value='") - html_attr(secure_value("redirect", cgit["url"], 0)) - html("' />") - html("<table>") - html("<tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr>") - html("<tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr>") - html("<tr><td colspan='2'><input value='Login' type='submit' /></td></tr>") - html("</table></form>") - - return 0 -end - - - --- --- --- Wrapper around filter API, exposing the http table, the cgit table, and the post table to the above functions. --- --- - -local actions = {} -actions["authenticate-post"] = authenticate_post -actions["authenticate-cookie"] = authenticate_cookie -actions["body"] = body - -function filter_open(...) - action = actions[select(1, ...)] - - http = {} - http["cookie"] = select(2, ...) - http["method"] = select(3, ...) - http["query"] = select(4, ...) - http["referer"] = select(5, ...) - http["path"] = select(6, ...) - http["host"] = select(7, ...) - http["https"] = select(8, ...) - - cgit = {} - cgit["repo"] = select(9, ...) - cgit["page"] = select(10, ...) - cgit["url"] = select(11, ...) - cgit["login"] = select(12, ...) - -end - -function filter_close() - return action() -end - -function filter_write(str) - post = parse_qs(str) -end - - --- --- --- Utility functions based on keplerproject/wsapi. --- --- - -function url_decode(str) - if not str then - return "" - end - str = string.gsub(str, "+", " ") - str = string.gsub(str, "%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end) - str = string.gsub(str, "\r\n", "\n") - return str -end - -function url_encode(str) - if not str then - return "" - end - str = string.gsub(str, "\n", "\r\n") - str = string.gsub(str, "([^%w ])", function(c) return string.format("%%%02X", string.byte(c)) end) - str = string.gsub(str, " ", "+") - return str -end - -function parse_qs(qs) - local tab = {} - for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do - tab[url_decode(key)] = url_decode(val) - end - return tab -end - -function get_cookie(cookies, name) - cookies = string.gsub(";" .. cookies .. ";", "%s*;%s*", ";") - return url_decode(string.match(cookies, ";" .. name .. "=(.-);")) -end - -function tohex(b) - local x = "" - for i = 1, #b do - x = x .. string.format("%.2x", string.byte(b, i)) - end - return x -end - --- --- --- Cookie construction and validation helpers. --- --- - -local secret = nil - --- Loads a secret from a file, creates a secret, or returns one from memory. -function get_secret() - if secret ~= nil then - return secret - end - local secret_file = io.open(secret_filename, "r") - if secret_file == nil then - local old_umask = sysstat.umask(63) - local temporary_filename = secret_filename .. ".tmp." .. tohex(rand.bytes(16)) - local temporary_file = io.open(temporary_filename, "w") - if temporary_file == nil then - os.exit(177) - end - temporary_file:write(tohex(rand.bytes(32))) - temporary_file:close() - unistd.link(temporary_filename, secret_filename) -- Intentionally fails in the case that another process is doing the same. - unistd.unlink(temporary_filename) - sysstat.umask(old_umask) - secret_file = io.open(secret_filename, "r") - end - if secret_file == nil then - os.exit(177) - end - secret = secret_file:read() - secret_file:close() - if secret:len() ~= 64 then - os.exit(177) - end - return secret -end - --- Returns value of cookie if cookie is valid. Otherwise returns nil. -function validate_value(expected_field, cookie) - local i = 0 - local value = "" - local field = "" - local expiration = 0 - local salt = "" - local chmac = "" - - if cookie == nil or cookie:len() < 3 or cookie:sub(1, 1) == "|" then - return nil - end - - for component in string.gmatch(cookie, "[^|]+") do - if i == 0 then - field = component - elseif i == 1 then - value = component - elseif i == 2 then - expiration = tonumber(component) - if expiration == nil then - expiration = -1 - end - elseif i == 3 then - salt = component - elseif i == 4 then - chmac = component - else - break - end - i = i + 1 - end - - if chmac == nil or chmac:len() == 0 then - return nil - end - - -- Lua hashes strings, so these comparisons are time invariant. - if chmac ~= tohex(hmac.new(get_secret(), "sha256"):final(field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt)) then - return nil - end - - if expiration == -1 or (expiration ~= 0 and expiration <= os.time()) then - return nil - end - - if url_decode(field) ~= expected_field then - return nil - end - - return url_decode(value) -end - -function secure_value(field, value, expiration) - if value == nil or value:len() <= 0 then - return "" - end - - local authstr = "" - local salt = tohex(rand.bytes(16)) - value = url_encode(value) - field = url_encode(field) - authstr = field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt - authstr = authstr .. "|" .. tohex(hmac.new(get_secret(), "sha256"):final(authstr)) - return authstr -end - -function set_cookie(cookie, value) - html("Set-Cookie: " .. cookie .. "=" .. value .. "; HttpOnly") - if http["https"] == "yes" or http["https"] == "on" or http["https"] == "1" then - html("; secure") - end - html("\n") -end - -function redirect_to(url) - html("Status: 302 Redirect\n") - html("Cache-Control: no-cache, no-store\n") - html("Location: " .. url .. "\n") -end - -function not_found() - html("Status: 404 Not Found\n") - html("Cache-Control: no-cache, no-store\n\n") -end diff --git a/filters/gentoo-ldap-authentication.lua b/filters/gentoo-ldap-authentication.lua deleted file mode 100644 index 673c88d102..0000000000 --- a/filters/gentoo-ldap-authentication.lua +++ /dev/null @@ -1,360 +0,0 @@ --- This script may be used with the auth-filter. Be sure to configure it as you wish. --- --- Requirements: --- luaossl --- <http://25thandclement.com/~william/projects/luaossl.html> --- lualdap >= 1.2 --- <https://git.zx2c4.com/lualdap/about/> --- luaposix --- <https://github.com/luaposix/luaposix> --- -local sysstat = require("posix.sys.stat") -local unistd = require("posix.unistd") -local lualdap = require("lualdap") -local rand = require("openssl.rand") -local hmac = require("openssl.hmac") - --- --- --- Configure these variables for your settings. --- --- - --- A list of password protected repositories, with which gentooAccess --- group is allowed to access each one. -local protected_repos = { - glouglou = "infra", - portage = "dev" -} - --- Set this to a path this script can write to for storing a persistent --- cookie secret, which should be guarded. -local secret_filename = "/var/cache/cgit/auth-secret" - - --- --- --- Authentication functions follow below. Swap these out if you want different authentication semantics. --- --- - --- Sets HTTP cookie headers based on post and sets up redirection. -function authenticate_post() - local redirect = validate_value("redirect", post["redirect"]) - - if redirect == nil then - not_found() - return 0 - end - - redirect_to(redirect) - - local groups = gentoo_ldap_user_groups(post["username"], post["password"]) - if groups == nil then - set_cookie("cgitauth", "") - else - -- One week expiration time - set_cookie("cgitauth", secure_value("gentoogroups", table.concat(groups, ","), os.time() + 604800)) - end - - html("\n") - return 0 -end - - --- Returns 1 if the cookie is valid and 0 if it is not. -function authenticate_cookie() - local required_group = protected_repos[cgit["repo"]] - if required_group == nil then - -- We return as valid if the repo is not protected. - return 1 - end - - local user_groups = validate_value("gentoogroups", get_cookie(http["cookie"], "cgitauth")) - if user_groups == nil or user_groups == "" then - return 0 - end - for group in string.gmatch(user_groups, "[^,]+") do - if group == required_group then - return 1 - end - end - return 0 -end - --- Prints the html for the login form. -function body() - html("<h2>Gentoo LDAP Authentication Required</h2>") - html("<form method='post' action='") - html_attr(cgit["login"]) - html("'>") - html("<input type='hidden' name='redirect' value='") - html_attr(secure_value("redirect", cgit["url"], 0)) - html("' />") - html("<table>") - html("<tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr>") - html("<tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr>") - html("<tr><td colspan='2'><input value='Login' type='submit' /></td></tr>") - html("</table></form>") - - return 0 -end - --- --- --- Gentoo LDAP support. --- --- - -function gentoo_ldap_user_groups(username, password) - -- Ensure the user is alphanumeric - if username == nil or username:match("%W") then - return nil - end - - local who = "uid=" .. username .. ",ou=devs,dc=gentoo,dc=org" - - local ldap, err = lualdap.open_simple { - uri = "ldap://ldap1.gentoo.org", - who = who, - password = password, - starttls = true, - certfile = "/var/www/uwsgi/cgit/gentoo-ldap/star.gentoo.org.crt", - keyfile = "/var/www/uwsgi/cgit/gentoo-ldap/star.gentoo.org.key", - cacertfile = "/var/www/uwsgi/cgit/gentoo-ldap/ca.pem" - } - if ldap == nil then - return nil - end - - local group_suffix = ".group" - local group_suffix_len = group_suffix:len() - local groups = {} - for dn, attribs in ldap:search { base = who, scope = "subtree" } do - local access = attribs["gentooAccess"] - if dn == who and access ~= nil then - for i, v in ipairs(access) do - local vlen = v:len() - if vlen > group_suffix_len and v:sub(-group_suffix_len) == group_suffix then - table.insert(groups, v:sub(1, vlen - group_suffix_len)) - end - end - end - end - - ldap:close() - - return groups -end - --- --- --- Wrapper around filter API, exposing the http table, the cgit table, and the post table to the above functions. --- --- - -local actions = {} -actions["authenticate-post"] = authenticate_post -actions["authenticate-cookie"] = authenticate_cookie -actions["body"] = body - -function filter_open(...) - action = actions[select(1, ...)] - - http = {} - http["cookie"] = select(2, ...) - http["method"] = select(3, ...) - http["query"] = select(4, ...) - http["referer"] = select(5, ...) - http["path"] = select(6, ...) - http["host"] = select(7, ...) - http["https"] = select(8, ...) - - cgit = {} - cgit["repo"] = select(9, ...) - cgit["page"] = select(10, ...) - cgit["url"] = select(11, ...) - cgit["login"] = select(12, ...) - -end - -function filter_close() - return action() -end - -function filter_write(str) - post = parse_qs(str) -end - - --- --- --- Utility functions based on keplerproject/wsapi. --- --- - -function url_decode(str) - if not str then - return "" - end - str = string.gsub(str, "+", " ") - str = string.gsub(str, "%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end) - str = string.gsub(str, "\r\n", "\n") - return str -end - -function url_encode(str) - if not str then - return "" - end - str = string.gsub(str, "\n", "\r\n") - str = string.gsub(str, "([^%w ])", function(c) return string.format("%%%02X", string.byte(c)) end) - str = string.gsub(str, " ", "+") - return str -end - -function parse_qs(qs) - local tab = {} - for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do - tab[url_decode(key)] = url_decode(val) - end - return tab -end - -function get_cookie(cookies, name) - cookies = string.gsub(";" .. cookies .. ";", "%s*;%s*", ";") - return string.match(cookies, ";" .. name .. "=(.-);") -end - -function tohex(b) - local x = "" - for i = 1, #b do - x = x .. string.format("%.2x", string.byte(b, i)) - end - return x -end - --- --- --- Cookie construction and validation helpers. --- --- - -local secret = nil - --- Loads a secret from a file, creates a secret, or returns one from memory. -function get_secret() - if secret ~= nil then - return secret - end - local secret_file = io.open(secret_filename, "r") - if secret_file == nil then - local old_umask = sysstat.umask(63) - local temporary_filename = secret_filename .. ".tmp." .. tohex(rand.bytes(16)) - local temporary_file = io.open(temporary_filename, "w") - if temporary_file == nil then - os.exit(177) - end - temporary_file:write(tohex(rand.bytes(32))) - temporary_file:close() - unistd.link(temporary_filename, secret_filename) -- Intentionally fails in the case that another process is doing the same. - unistd.unlink(temporary_filename) - sysstat.umask(old_umask) - secret_file = io.open(secret_filename, "r") - end - if secret_file == nil then - os.exit(177) - end - secret = secret_file:read() - secret_file:close() - if secret:len() ~= 64 then - os.exit(177) - end - return secret -end - --- Returns value of cookie if cookie is valid. Otherwise returns nil. -function validate_value(expected_field, cookie) - local i = 0 - local value = "" - local field = "" - local expiration = 0 - local salt = "" - local chmac = "" - - if cookie == nil or cookie:len() < 3 or cookie:sub(1, 1) == "|" then - return nil - end - - for component in string.gmatch(cookie, "[^|]+") do - if i == 0 then - field = component - elseif i == 1 then - value = component - elseif i == 2 then - expiration = tonumber(component) - if expiration == nil then - expiration = -1 - end - elseif i == 3 then - salt = component - elseif i == 4 then - chmac = component - else - break - end - i = i + 1 - end - - if chmac == nil or chmac:len() == 0 then - return nil - end - - -- Lua hashes strings, so these comparisons are time invariant. - if chmac ~= tohex(hmac.new(get_secret(), "sha256"):final(field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt)) then - return nil - end - - if expiration == -1 or (expiration ~= 0 and expiration <= os.time()) then - return nil - end - - if url_decode(field) ~= expected_field then - return nil - end - - return url_decode(value) -end - -function secure_value(field, value, expiration) - if value == nil or value:len() <= 0 then - return "" - end - - local authstr = "" - local salt = tohex(rand.bytes(16)) - value = url_encode(value) - field = url_encode(field) - authstr = field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt - authstr = authstr .. "|" .. tohex(hmac.new(get_secret(), "sha256"):final(authstr)) - return authstr -end - -function set_cookie(cookie, value) - html("Set-Cookie: " .. cookie .. "=" .. value .. "; HttpOnly") - if http["https"] == "yes" or http["https"] == "on" or http["https"] == "1" then - html("; secure") - end - html("\n") -end - -function redirect_to(url) - html("Status: 302 Redirect\n") - html("Cache-Control: no-cache, no-store\n") - html("Location: " .. url .. "\n") -end - -function not_found() - html("Status: 404 Not Found\n") - html("Cache-Control: no-cache, no-store\n\n") -end diff --git a/filters/html-converters/man2html b/filters/html-converters/man2html deleted file mode 100755 index 0ef7884181..0000000000 --- a/filters/html-converters/man2html +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -echo "<div style=\"font-family: monospace\">" -groff -mandoc -T html -P -r -P -l | egrep -v '(<html>|<head>|<meta|<title>|</title>|</head>|<body>|</body>|</html>|<!DOCTYPE|"http://www.w3.org)' -echo "</div>" diff --git a/filters/html-converters/md2html b/filters/html-converters/md2html deleted file mode 100755 index dc20f42a05..0000000000 --- a/filters/html-converters/md2html +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 -import markdown -import sys -import io -from pygments.formatters import HtmlFormatter -from markdown.extensions.toc import TocExtension -sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8') -sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') -sys.stdout.write(''' -<style> -.markdown-body { - font-size: 14px; - line-height: 1.6; - overflow: hidden; -} -.markdown-body>*:first-child { - margin-top: 0 !important; -} -.markdown-body>*:last-child { - margin-bottom: 0 !important; -} -.markdown-body a.absent { - color: #c00; -} -.markdown-body a.anchor { - display: block; - padding-left: 30px; - margin-left: -30px; - cursor: pointer; - position: absolute; - top: 0; - left: 0; - bottom: 0; -} -.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { - margin: 20px 0 10px; - padding: 0; - font-weight: bold; - -webkit-font-smoothing: antialiased; - cursor: text; - position: relative; -} -.markdown-body h1 .mini-icon-link, .markdown-body h2 .mini-icon-link, .markdown-body h3 .mini-icon-link, .markdown-body h4 .mini-icon-link, .markdown-body h5 .mini-icon-link, .markdown-body h6 .mini-icon-link { - display: none; - color: #000; -} -.markdown-body h1:hover a.anchor, .markdown-body h2:hover a.anchor, .markdown-body h3:hover a.anchor, .markdown-body h4:hover a.anchor, .markdown-body h5:hover a.anchor, .markdown-body h6:hover a.anchor { - text-decoration: none; - line-height: 1; - padding-left: 0; - margin-left: -22px; - top: 15%; -} -.markdown-body h1:hover a.anchor .mini-icon-link, .markdown-body h2:hover a.anchor .mini-icon-link, .markdown-body h3:hover a.anchor .mini-icon-link, .markdown-body h4:hover a.anchor .mini-icon-link, .markdown-body h5:hover a.anchor .mini-icon-link, .markdown-body h6:hover a.anchor .mini-icon-link { - display: inline-block; -} -div#cgit .markdown-body h1 a.toclink, div#cgit .markdown-body h2 a.toclink, div#cgit .markdown-body h3 a.toclink, div#cgit .markdown-body h4 a.toclink, div#cgit .markdown-body h5 a.toclink, div#cgit .markdown-body h6 a.toclink { - color: black; -} -.markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { - font-size: inherit; -} -.markdown-body h1 { - font-size: 28px; - color: #000; -} -.markdown-body h2 { - font-size: 24px; - border-bottom: 1px solid #ccc; - color: #000; -} -.markdown-body h3 { - font-size: 18px; -} -.markdown-body h4 { - font-size: 16px; -} -.markdown-body h5 { - font-size: 14px; -} -.markdown-body h6 { - color: #777; - font-size: 14px; -} -.markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre { - margin: 15px 0; -} -.markdown-body hr { - background: transparent url("/dirty-shade.png") repeat-x 0 0; - border: 0 none; - color: #ccc; - height: 4px; - padding: 0; -} -.markdown-body>h2:first-child, .markdown-body>h1:first-child, .markdown-body>h1:first-child+h2, .markdown-body>h3:first-child, .markdown-body>h4:first-child, .markdown-body>h5:first-child, .markdown-body>h6:first-child { - margin-top: 0; - padding-top: 0; -} -.markdown-body a:first-child h1, .markdown-body a:first-child h2, .markdown-body a:first-child h3, .markdown-body a:first-child h4, .markdown-body a:first-child h5, .markdown-body a:first-child h6 { - margin-top: 0; - padding-top: 0; -} -.markdown-body h1+p, .markdown-body h2+p, .markdown-body h3+p, .markdown-body h4+p, .markdown-body h5+p, .markdown-body h6+p { - margin-top: 0; -} -.markdown-body li p.first { - display: inline-block; -} -.markdown-body ul, .markdown-body ol { - padding-left: 30px; -} -.markdown-body ul.no-list, .markdown-body ol.no-list { - list-style-type: none; - padding: 0; -} -.markdown-body ul li>:first-child, .markdown-body ul li ul:first-of-type, .markdown-body ul li ol:first-of-type, .markdown-body ol li>:first-child, .markdown-body ol li ul:first-of-type, .markdown-body ol li ol:first-of-type { - margin-top: 0px; -} -.markdown-body ul li p:last-of-type, .markdown-body ol li p:last-of-type { - margin-bottom: 0; -} -.markdown-body ul ul, .markdown-body ul ol, .markdown-body ol ol, .markdown-body ol ul { - margin-bottom: 0; -} -.markdown-body dl { - padding: 0; -} -.markdown-body dl dt { - font-size: 14px; - font-weight: bold; - font-style: italic; - padding: 0; - margin: 15px 0 5px; -} -.markdown-body dl dt:first-child { - padding: 0; -} -.markdown-body dl dt>:first-child { - margin-top: 0px; -} -.markdown-body dl dt>:last-child { - margin-bottom: 0px; -} -.markdown-body dl dd { - margin: 0 0 15px; - padding: 0 15px; -} -.markdown-body dl dd>:first-child { - margin-top: 0px; -} -.markdown-body dl dd>:last-child { - margin-bottom: 0px; -} -.markdown-body blockquote { - border-left: 4px solid #DDD; - padding: 0 15px; - color: #777; -} -.markdown-body blockquote>:first-child { - margin-top: 0px; -} -.markdown-body blockquote>:last-child { - margin-bottom: 0px; -} -.markdown-body table th { - font-weight: bold; -} -.markdown-body table th, .markdown-body table td { - border: 1px solid #ccc; - padding: 6px 13px; -} -.markdown-body table tr { - border-top: 1px solid #ccc; - background-color: #fff; -} -.markdown-body table tr:nth-child(2n) { - background-color: #f8f8f8; -} -.markdown-body img { - max-width: 100%; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -.markdown-body span.frame { - display: block; - overflow: hidden; -} -.markdown-body span.frame>span { - border: 1px solid #ddd; - display: block; - float: left; - overflow: hidden; - margin: 13px 0 0; - padding: 7px; - width: auto; -} -.markdown-body span.frame span img { - display: block; - float: left; -} -.markdown-body span.frame span span { - clear: both; - color: #333; - display: block; - padding: 5px 0 0; -} -.markdown-body span.align-center { - display: block; - overflow: hidden; - clear: both; -} -.markdown-body span.align-center>span { - display: block; - overflow: hidden; - margin: 13px auto 0; - text-align: center; -} -.markdown-body span.align-center span img { - margin: 0 auto; - text-align: center; -} -.markdown-body span.align-right { - display: block; - overflow: hidden; - clear: both; -} -.markdown-body span.align-right>span { - display: block; - overflow: hidden; - margin: 13px 0 0; - text-align: right; -} -.markdown-body span.align-right span img { - margin: 0; - text-align: right; -} -.markdown-body span.float-left { - display: block; - margin-right: 13px; - overflow: hidden; - float: left; -} -.markdown-body span.float-left span { - margin: 13px 0 0; -} -.markdown-body span.float-right { - display: block; - margin-left: 13px; - overflow: hidden; - float: right; -} -.markdown-body span.float-right>span { - display: block; - overflow: hidden; - margin: 13px auto 0; - text-align: right; -} -.markdown-body code, .markdown-body tt { - margin: 0 2px; - padding: 0px 5px; - border: 1px solid #eaeaea; - background-color: #f8f8f8; - border-radius: 3px; -} -.markdown-body code { - white-space: nowrap; -} -.markdown-body pre>code { - margin: 0; - padding: 0; - white-space: pre; - border: none; - background: transparent; -} -.markdown-body .highlight pre, .markdown-body pre { - background-color: #f8f8f8; - border: 1px solid #ccc; - font-size: 13px; - line-height: 19px; - overflow: auto; - padding: 6px 10px; - border-radius: 3px; -} -.markdown-body pre code, .markdown-body pre tt { - margin: 0; - padding: 0; - background-color: transparent; - border: none; -} -''') -sys.stdout.write(HtmlFormatter(style='pastie').get_style_defs('.highlight')) -sys.stdout.write(''' -</style> -''') -sys.stdout.write("<div class='markdown-body'>") -sys.stdout.flush() -# Note: you may want to run this through bleach for sanitization -markdown.markdownFromFile( - output_format="html5", - extensions=[ - "markdown.extensions.fenced_code", - "markdown.extensions.codehilite", - "markdown.extensions.tables", - TocExtension(anchorlink=True)], - extension_configs={ - "markdown.extensions.codehilite":{"css_class":"highlight"}}) -sys.stdout.write("</div>") diff --git a/filters/html-converters/rst2html b/filters/html-converters/rst2html deleted file mode 100755 index 02d90f81c9..0000000000 --- a/filters/html-converters/rst2html +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -exec rst2html.py --template <(echo -e "%(stylesheet)s\n%(body_pre_docinfo)s\n%(docinfo)s\n%(body)s") diff --git a/filters/html-converters/txt2html b/filters/html-converters/txt2html deleted file mode 100755 index 495eeceb27..0000000000 --- a/filters/html-converters/txt2html +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -echo "<pre>" -sed "s|&|\\&|g;s|'|\\'|g;s|\"|\\"|g;s|<|\\<|g;s|>|\\>|g" -echo "</pre>" diff --git a/filters/owner-example.lua b/filters/owner-example.lua deleted file mode 100644 index 50fc25a8e5..0000000000 --- a/filters/owner-example.lua +++ /dev/null @@ -1,17 +0,0 @@ --- This script is an example of an owner-filter. It replaces the --- usual query link with one to a fictional homepage. This script may --- be used with the owner-filter or repo.owner-filter settings in --- cgitrc with the `lua:` prefix. - -function filter_open() - buffer = "" -end - -function filter_close() - html(string.format("<a href=\"%s\">%s</a>", "http://wiki.example.com/about/" .. buffer, buffer)) - return 0 -end - -function filter_write(str) - buffer = buffer .. str -end diff --git a/filters/simple-authentication.lua b/filters/simple-authentication.lua deleted file mode 100644 index 23d345763b..0000000000 --- a/filters/simple-authentication.lua +++ /dev/null @@ -1,314 +0,0 @@ --- This script may be used with the auth-filter. Be sure to configure it as you wish. --- --- Requirements: --- luaossl --- <http://25thandclement.com/~william/projects/luaossl.html> --- luaposix --- <https://github.com/luaposix/luaposix> --- -local sysstat = require("posix.sys.stat") -local unistd = require("posix.unistd") -local rand = require("openssl.rand") -local hmac = require("openssl.hmac") - --- --- --- Configure these variables for your settings. --- --- - --- A list of password protected repositories along with the users who can access them. -local protected_repos = { - glouglou = { laurent = true, jason = true }, - qt = { jason = true, bob = true } -} - --- A list of users and hashes, generated with `mkpasswd -m sha-512 -R 300000`. -local users = { - jason = "$6$rounds=300000$YYJct3n/o.ruYK$HhpSeuCuW1fJkpvMZOZzVizeLsBKcGA/aF2UPuV5v60JyH2MVSG6P511UMTj2F3H75.IT2HIlnvXzNb60FcZH1", - laurent = "$6$rounds=300000$dP0KNHwYb3JKigT$pN/LG7rWxQ4HniFtx5wKyJXBJUKP7R01zTNZ0qSK/aivw8ywGAOdfYiIQFqFhZFtVGvr11/7an.nesvm8iJUi.", - bob = "$6$rounds=300000$jCLCCt6LUpTz$PI1vvd1yaVYcCzqH8QAJFcJ60b6W/6sjcOsU7mAkNo7IE8FRGW1vkjF8I/T5jt/auv5ODLb1L4S2s.CAyZyUC" -} - --- Set this to a path this script can write to for storing a persistent --- cookie secret, which should be guarded. -local secret_filename = "/var/cache/cgit/auth-secret" - --- --- --- Authentication functions follow below. Swap these out if you want different authentication semantics. --- --- - --- Sets HTTP cookie headers based on post and sets up redirection. -function authenticate_post() - local hash = users[post["username"]] - local redirect = validate_value("redirect", post["redirect"]) - - if redirect == nil then - not_found() - return 0 - end - - redirect_to(redirect) - - if hash == nil or hash ~= unistd.crypt(post["password"], hash) then - set_cookie("cgitauth", "") - else - -- One week expiration time - local username = secure_value("username", post["username"], os.time() + 604800) - set_cookie("cgitauth", username) - end - - html("\n") - return 0 -end - - --- Returns 1 if the cookie is valid and 0 if it is not. -function authenticate_cookie() - accepted_users = protected_repos[cgit["repo"]] - if accepted_users == nil then - -- We return as valid if the repo is not protected. - return 1 - end - - local username = validate_value("username", get_cookie(http["cookie"], "cgitauth")) - if username == nil or not accepted_users[username:lower()] then - return 0 - else - return 1 - end -end - --- Prints the html for the login form. -function body() - html("<h2>Authentication Required</h2>") - html("<form method='post' action='") - html_attr(cgit["login"]) - html("'>") - html("<input type='hidden' name='redirect' value='") - html_attr(secure_value("redirect", cgit["url"], 0)) - html("' />") - html("<table>") - html("<tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr>") - html("<tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr>") - html("<tr><td colspan='2'><input value='Login' type='submit' /></td></tr>") - html("</table></form>") - - return 0 -end - - - --- --- --- Wrapper around filter API, exposing the http table, the cgit table, and the post table to the above functions. --- --- - -local actions = {} -actions["authenticate-post"] = authenticate_post -actions["authenticate-cookie"] = authenticate_cookie -actions["body"] = body - -function filter_open(...) - action = actions[select(1, ...)] - - http = {} - http["cookie"] = select(2, ...) - http["method"] = select(3, ...) - http["query"] = select(4, ...) - http["referer"] = select(5, ...) - http["path"] = select(6, ...) - http["host"] = select(7, ...) - http["https"] = select(8, ...) - - cgit = {} - cgit["repo"] = select(9, ...) - cgit["page"] = select(10, ...) - cgit["url"] = select(11, ...) - cgit["login"] = select(12, ...) - -end - -function filter_close() - return action() -end - -function filter_write(str) - post = parse_qs(str) -end - - --- --- --- Utility functions based on keplerproject/wsapi. --- --- - -function url_decode(str) - if not str then - return "" - end - str = string.gsub(str, "+", " ") - str = string.gsub(str, "%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end) - str = string.gsub(str, "\r\n", "\n") - return str -end - -function url_encode(str) - if not str then - return "" - end - str = string.gsub(str, "\n", "\r\n") - str = string.gsub(str, "([^%w ])", function(c) return string.format("%%%02X", string.byte(c)) end) - str = string.gsub(str, " ", "+") - return str -end - -function parse_qs(qs) - local tab = {} - for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do - tab[url_decode(key)] = url_decode(val) - end - return tab -end - -function get_cookie(cookies, name) - cookies = string.gsub(";" .. cookies .. ";", "%s*;%s*", ";") - return url_decode(string.match(cookies, ";" .. name .. "=(.-);")) -end - -function tohex(b) - local x = "" - for i = 1, #b do - x = x .. string.format("%.2x", string.byte(b, i)) - end - return x -end - --- --- --- Cookie construction and validation helpers. --- --- - -local secret = nil - --- Loads a secret from a file, creates a secret, or returns one from memory. -function get_secret() - if secret ~= nil then - return secret - end - local secret_file = io.open(secret_filename, "r") - if secret_file == nil then - local old_umask = sysstat.umask(63) - local temporary_filename = secret_filename .. ".tmp." .. tohex(rand.bytes(16)) - local temporary_file = io.open(temporary_filename, "w") - if temporary_file == nil then - os.exit(177) - end - temporary_file:write(tohex(rand.bytes(32))) - temporary_file:close() - unistd.link(temporary_filename, secret_filename) -- Intentionally fails in the case that another process is doing the same. - unistd.unlink(temporary_filename) - sysstat.umask(old_umask) - secret_file = io.open(secret_filename, "r") - end - if secret_file == nil then - os.exit(177) - end - secret = secret_file:read() - secret_file:close() - if secret:len() ~= 64 then - os.exit(177) - end - return secret -end - --- Returns value of cookie if cookie is valid. Otherwise returns nil. -function validate_value(expected_field, cookie) - local i = 0 - local value = "" - local field = "" - local expiration = 0 - local salt = "" - local chmac = "" - - if cookie == nil or cookie:len() < 3 or cookie:sub(1, 1) == "|" then - return nil - end - - for component in string.gmatch(cookie, "[^|]+") do - if i == 0 then - field = component - elseif i == 1 then - value = component - elseif i == 2 then - expiration = tonumber(component) - if expiration == nil then - expiration = -1 - end - elseif i == 3 then - salt = component - elseif i == 4 then - chmac = component - else - break - end - i = i + 1 - end - - if chmac == nil or chmac:len() == 0 then - return nil - end - - -- Lua hashes strings, so these comparisons are time invariant. - if chmac ~= tohex(hmac.new(get_secret(), "sha256"):final(field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt)) then - return nil - end - - if expiration == -1 or (expiration ~= 0 and expiration <= os.time()) then - return nil - end - - if url_decode(field) ~= expected_field then - return nil - end - - return url_decode(value) -end - -function secure_value(field, value, expiration) - if value == nil or value:len() <= 0 then - return "" - end - - local authstr = "" - local salt = tohex(rand.bytes(16)) - value = url_encode(value) - field = url_encode(field) - authstr = field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt - authstr = authstr .. "|" .. tohex(hmac.new(get_secret(), "sha256"):final(authstr)) - return authstr -end - -function set_cookie(cookie, value) - html("Set-Cookie: " .. cookie .. "=" .. value .. "; HttpOnly") - if http["https"] == "yes" or http["https"] == "on" or http["https"] == "1" then - html("; secure") - end - html("\n") -end - -function redirect_to(url) - html("Status: 302 Redirect\n") - html("Cache-Control: no-cache, no-store\n") - html("Location: " .. url .. "\n") -end - -function not_found() - html("Status: 404 Not Found\n") - html("Cache-Control: no-cache, no-store\n\n") -end diff --git a/filters/syntax-highlighting.py b/filters/syntax-highlighting.py deleted file mode 100755 index e912594c48..0000000000 --- a/filters/syntax-highlighting.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 - -# This script uses Pygments and Python3. You must have both installed -# for this to work. -# -# http://pygments.org/ -# http://python.org/ -# -# It may be used with the source-filter or repo.source-filter settings -# in cgitrc. -# -# The following environment variables can be used to retrieve the -# configuration of the repository for which this script is called: -# CGIT_REPO_URL ( = repo.url setting ) -# CGIT_REPO_NAME ( = repo.name setting ) -# CGIT_REPO_PATH ( = repo.path setting ) -# CGIT_REPO_OWNER ( = repo.owner setting ) -# CGIT_REPO_DEFBRANCH ( = repo.defbranch setting ) -# CGIT_REPO_SECTION ( = section setting ) -# CGIT_REPO_CLONE_URL ( = repo.clone-url setting ) - - -import sys -import io -from pygments import highlight -from pygments.util import ClassNotFound -from pygments.lexers import TextLexer -from pygments.lexers import guess_lexer -from pygments.lexers import guess_lexer_for_filename -from pygments.formatters import HtmlFormatter - - -sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8', errors='replace') -sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') -data = sys.stdin.read() -filename = sys.argv[1] -formatter = HtmlFormatter(style='pastie', nobackground=True) - -try: - lexer = guess_lexer_for_filename(filename, data) -except ClassNotFound: - # check if there is any shebang - if data[0:2] == '#!': - lexer = guess_lexer(data) - else: - lexer = TextLexer() -except TypeError: - lexer = TextLexer() - -# highlight! :-) -# printout pygments' css definitions as well -sys.stdout.write('<style>') -sys.stdout.write(formatter.get_style_defs('.highlight')) -sys.stdout.write('</style>') -sys.stdout.write(highlight(data, lexer, formatter, outfile=None)) diff --git a/filters/syntax-highlighting.sh b/filters/syntax-highlighting.sh deleted file mode 100755 index 840bc34fff..0000000000 --- a/filters/syntax-highlighting.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/bin/sh -# This script can be used to implement syntax highlighting in the cgit -# tree-view by referring to this file with the source-filter or repo.source- -# filter options in cgitrc. -# -# This script requires a shell supporting the ${var##pattern} syntax. -# It is supported by at least dash and bash, however busybox environments -# might have to use an external call to sed instead. -# -# Note: the highlight command (http://www.andre-simon.de/) uses css for syntax -# highlighting, so you'll probably want something like the following included -# in your css file: -# -# Style definition file generated by highlight 2.4.8, http://www.andre-simon.de/ -# -# table.blob .num { color:#2928ff; } -# table.blob .esc { color:#ff00ff; } -# table.blob .str { color:#ff0000; } -# table.blob .dstr { color:#818100; } -# table.blob .slc { color:#838183; font-style:italic; } -# table.blob .com { color:#838183; font-style:italic; } -# table.blob .dir { color:#008200; } -# table.blob .sym { color:#000000; } -# table.blob .kwa { color:#000000; font-weight:bold; } -# table.blob .kwb { color:#830000; } -# table.blob .kwc { color:#000000; font-weight:bold; } -# table.blob .kwd { color:#010181; } -# -# -# Style definition file generated by highlight 2.6.14, http://www.andre-simon.de/ -# -# body.hl { background-color:#ffffff; } -# pre.hl { color:#000000; background-color:#ffffff; font-size:10pt; font-family:'Courier New';} -# .hl.num { color:#2928ff; } -# .hl.esc { color:#ff00ff; } -# .hl.str { color:#ff0000; } -# .hl.dstr { color:#818100; } -# .hl.slc { color:#838183; font-style:italic; } -# .hl.com { color:#838183; font-style:italic; } -# .hl.dir { color:#008200; } -# .hl.sym { color:#000000; } -# .hl.line { color:#555555; } -# .hl.mark { background-color:#ffffbb;} -# .hl.kwa { color:#000000; font-weight:bold; } -# .hl.kwb { color:#830000; } -# .hl.kwc { color:#000000; font-weight:bold; } -# .hl.kwd { color:#010181; } -# -# -# Style definition file generated by highlight 3.8, http://www.andre-simon.de/ -# -# body.hl { background-color:#e0eaee; } -# pre.hl { color:#000000; background-color:#e0eaee; font-size:10pt; font-family:'Courier New';} -# .hl.num { color:#b07e00; } -# .hl.esc { color:#ff00ff; } -# .hl.str { color:#bf0303; } -# .hl.pps { color:#818100; } -# .hl.slc { color:#838183; font-style:italic; } -# .hl.com { color:#838183; font-style:italic; } -# .hl.ppc { color:#008200; } -# .hl.opt { color:#000000; } -# .hl.lin { color:#555555; } -# .hl.kwa { color:#000000; font-weight:bold; } -# .hl.kwb { color:#0057ae; } -# .hl.kwc { color:#000000; font-weight:bold; } -# .hl.kwd { color:#010181; } -# -# -# Style definition file generated by highlight 3.13, http://www.andre-simon.de/ -# -# body.hl { background-color:#e0eaee; } -# pre.hl { color:#000000; background-color:#e0eaee; font-size:10pt; font-family:'Courier New',monospace;} -# .hl.num { color:#b07e00; } -# .hl.esc { color:#ff00ff; } -# .hl.str { color:#bf0303; } -# .hl.pps { color:#818100; } -# .hl.slc { color:#838183; font-style:italic; } -# .hl.com { color:#838183; font-style:italic; } -# .hl.ppc { color:#008200; } -# .hl.opt { color:#000000; } -# .hl.ipl { color:#0057ae; } -# .hl.lin { color:#555555; } -# .hl.kwa { color:#000000; font-weight:bold; } -# .hl.kwb { color:#0057ae; } -# .hl.kwc { color:#000000; font-weight:bold; } -# .hl.kwd { color:#010181; } -# -# -# The following environment variables can be used to retrieve the configuration -# of the repository for which this script is called: -# CGIT_REPO_URL ( = repo.url setting ) -# CGIT_REPO_NAME ( = repo.name setting ) -# CGIT_REPO_PATH ( = repo.path setting ) -# CGIT_REPO_OWNER ( = repo.owner setting ) -# CGIT_REPO_DEFBRANCH ( = repo.defbranch setting ) -# CGIT_REPO_SECTION ( = section setting ) -# CGIT_REPO_CLONE_URL ( = repo.clone-url setting ) -# - -# store filename and extension in local vars -BASENAME="$1" -EXTENSION="${BASENAME##*.}" - -[ "${BASENAME}" = "${EXTENSION}" ] && EXTENSION=txt -[ -z "${EXTENSION}" ] && EXTENSION=txt - -# map Makefile and Makefile.* to .mk -[ "${BASENAME%%.*}" = "Makefile" ] && EXTENSION=mk - -# highlight versions 2 and 3 have different commandline options. Specifically, -# the -X option that is used for version 2 is replaced by the -O xhtml option -# for version 3. -# -# Version 2 can be found (for example) on EPEL 5, while version 3 can be -# found (for example) on EPEL 6. -# -# This is for version 2 -exec highlight --force -f -I -X -S "$EXTENSION" 2>/dev/null - -# This is for version 3 -#exec highlight --force -f -I -O xhtml -S "$EXTENSION" 2>/dev/null diff --git a/gen-version.sh b/gen-version.sh deleted file mode 100755 index 80cf49af48..0000000000 --- a/gen-version.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh - -# Get version-info specified in Makefile -V=$1 - -# Use `git describe` to get current version if we're inside a git repo -if test "$(git rev-parse --git-dir 2>/dev/null)" = '.git' -then - V=$(git describe --abbrev=4 HEAD 2>/dev/null) -fi - -new="CGIT_VERSION = $V" -old=$(cat VERSION 2>/dev/null) - -# Exit if VERSION is uptodate -test "$old" = "$new" && exit 0 - -# Update VERSION with new version-info -echo "$new" > VERSION -cat VERSION diff --git a/git b/git deleted file mode 160000 -Subproject 5fa0f5238b0cd46cfe7f6fa76c3f526ea98148d diff --git a/html.c b/html.c deleted file mode 100644 index 7f81965fdd..0000000000 --- a/html.c +++ /dev/null @@ -1,344 +0,0 @@ -/* html.c: helper functions for html output - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "html.h" -#include "url.h" - -/* Percent-encoding of each character, except: a-zA-Z0-9!$()*,./:;@- */ -static const char* url_escape_table[256] = { - "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07", - "%08", "%09", "%0a", "%0b", "%0c", "%0d", "%0e", "%0f", - "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17", - "%18", "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f", - "%20", NULL, "%22", "%23", NULL, "%25", "%26", "%27", - NULL, NULL, NULL, "%2b", NULL, NULL, NULL, NULL, - NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - NULL, NULL, NULL, NULL, "%3c", "%3d", "%3e", "%3f", - NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - NULL, NULL, NULL, NULL, "%5c", NULL, "%5e", NULL, - "%60", NULL, NULL, NULL, NULL, NULL, NULL, NULL, - NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - NULL, NULL, NULL, "%7b", "%7c", "%7d", NULL, "%7f", - "%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87", - "%88", "%89", "%8a", "%8b", "%8c", "%8d", "%8e", "%8f", - "%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97", - "%98", "%99", "%9a", "%9b", "%9c", "%9d", "%9e", "%9f", - "%a0", "%a1", "%a2", "%a3", "%a4", "%a5", "%a6", "%a7", - "%a8", "%a9", "%aa", "%ab", "%ac", "%ad", "%ae", "%af", - "%b0", "%b1", "%b2", "%b3", "%b4", "%b5", "%b6", "%b7", - "%b8", "%b9", "%ba", "%bb", "%bc", "%bd", "%be", "%bf", - "%c0", "%c1", "%c2", "%c3", "%c4", "%c5", "%c6", "%c7", - "%c8", "%c9", "%ca", "%cb", "%cc", "%cd", "%ce", "%cf", - "%d0", "%d1", "%d2", "%d3", "%d4", "%d5", "%d6", "%d7", - "%d8", "%d9", "%da", "%db", "%dc", "%dd", "%de", "%df", - "%e0", "%e1", "%e2", "%e3", "%e4", "%e5", "%e6", "%e7", - "%e8", "%e9", "%ea", "%eb", "%ec", "%ed", "%ee", "%ef", - "%f0", "%f1", "%f2", "%f3", "%f4", "%f5", "%f6", "%f7", - "%f8", "%f9", "%fa", "%fb", "%fc", "%fd", "%fe", "%ff" -}; - -char *fmt(const char *format, ...) -{ - static char buf[8][1024]; - static int bufidx; - int len; - va_list args; - - bufidx++; - bufidx &= 7; - - va_start(args, format); - len = vsnprintf(buf[bufidx], sizeof(buf[bufidx]), format, args); - va_end(args); - if (len > sizeof(buf[bufidx])) { - fprintf(stderr, "[html.c] string truncated: %s\n", format); - exit(1); - } - return buf[bufidx]; -} - -char *fmtalloc(const char *format, ...) -{ - struct strbuf sb = STRBUF_INIT; - va_list args; - - va_start(args, format); - strbuf_vaddf(&sb, format, args); - va_end(args); - - return strbuf_detach(&sb, NULL); -} - -void html_raw(const char *data, size_t size) -{ - if (write(STDOUT_FILENO, data, size) != size) - die_errno("write error on html output"); -} - -void html(const char *txt) -{ - html_raw(txt, strlen(txt)); -} - -void htmlf(const char *format, ...) -{ - va_list args; - struct strbuf buf = STRBUF_INIT; - - va_start(args, format); - strbuf_vaddf(&buf, format, args); - va_end(args); - html(buf.buf); - strbuf_release(&buf); -} - -void html_txtf(const char *format, ...) -{ - va_list args; - - va_start(args, format); - html_vtxtf(format, args); - va_end(args); -} - -void html_vtxtf(const char *format, va_list ap) -{ - va_list cp; - struct strbuf buf = STRBUF_INIT; - - va_copy(cp, ap); - strbuf_vaddf(&buf, format, cp); - va_end(cp); - html_txt(buf.buf); - strbuf_release(&buf); -} - -void html_txt(const char *txt) -{ - if (txt) - html_ntxt(txt, strlen(txt)); -} - -ssize_t html_ntxt(const char *txt, size_t len) -{ - const char *t = txt; - ssize_t slen; - - if (len > SSIZE_MAX) - return -1; - - slen = (ssize_t) len; - while (t && *t && slen--) { - int c = *t; - if (c == '<' || c == '>' || c == '&') { - html_raw(txt, t - txt); - if (c == '>') - html(">"); - else if (c == '<') - html("<"); - else if (c == '&') - html("&"); - txt = t + 1; - } - t++; - } - if (t != txt) - html_raw(txt, t - txt); - return slen; -} - -void html_attrf(const char *fmt, ...) -{ - va_list ap; - struct strbuf sb = STRBUF_INIT; - - va_start(ap, fmt); - strbuf_vaddf(&sb, fmt, ap); - va_end(ap); - - html_attr(sb.buf); - strbuf_release(&sb); -} - -void html_attr(const char *txt) -{ - const char *t = txt; - while (t && *t) { - int c = *t; - if (c == '<' || c == '>' || c == '\'' || c == '\"' || c == '&') { - html_raw(txt, t - txt); - if (c == '>') - html(">"); - else if (c == '<') - html("<"); - else if (c == '\'') - html("'"); - else if (c == '"') - html("""); - else if (c == '&') - html("&"); - txt = t + 1; - } - t++; - } - if (t != txt) - html(txt); -} - -void html_url_path(const char *txt) -{ - const char *t = txt; - while (t && *t) { - unsigned char c = *t; - const char *e = url_escape_table[c]; - if (e && c != '+' && c != '&') { - html_raw(txt, t - txt); - html(e); - txt = t + 1; - } - t++; - } - if (t != txt) - html(txt); -} - -void html_url_arg(const char *txt) -{ - const char *t = txt; - while (t && *t) { - unsigned char c = *t; - const char *e = url_escape_table[c]; - if (c == ' ') - e = "+"; - if (e) { - html_raw(txt, t - txt); - html(e); - txt = t + 1; - } - t++; - } - if (t != txt) - html(txt); -} - -void html_header_arg_in_quotes(const char *txt) -{ - const char *t = txt; - while (t && *t) { - unsigned char c = *t; - const char *e = NULL; - if (c == '\\') - e = "\\\\"; - else if (c == '\r') - e = "\\r"; - else if (c == '\n') - e = "\\n"; - else if (c == '"') - e = "\\\""; - if (e) { - html_raw(txt, t - txt); - html(e); - txt = t + 1; - } - t++; - } - if (t != txt) - html(txt); - -} - -void html_hidden(const char *name, const char *value) -{ - html("<input type='hidden' name='"); - html_attr(name); - html("' value='"); - html_attr(value); - html("'/>"); -} - -void html_option(const char *value, const char *text, const char *selected_value) -{ - html("<option value='"); - html_attr(value); - html("'"); - if (selected_value && !strcmp(selected_value, value)) - html(" selected='selected'"); - html(">"); - html_txt(text); - html("</option>\n"); -} - -void html_intoption(int value, const char *text, int selected_value) -{ - htmlf("<option value='%d'%s>", value, - value == selected_value ? " selected='selected'" : ""); - html_txt(text); - html("</option>"); -} - -void html_link_open(const char *url, const char *title, const char *class) -{ - html("<a href='"); - html_attr(url); - if (title) { - html("' title='"); - html_attr(title); - } - if (class) { - html("' class='"); - html_attr(class); - } - html("'>"); -} - -void html_link_close(void) -{ - html("</a>"); -} - -void html_fileperm(unsigned short mode) -{ - htmlf("%c%c%c", (mode & 4 ? 'r' : '-'), - (mode & 2 ? 'w' : '-'), (mode & 1 ? 'x' : '-')); -} - -int html_include(const char *filename) -{ - FILE *f; - char buf[4096]; - size_t len; - - if (!(f = fopen(filename, "r"))) { - fprintf(stderr, "[cgit] Failed to include file %s: %s (%d).\n", - filename, strerror(errno), errno); - return -1; - } - while ((len = fread(buf, 1, 4096, f)) > 0) - html_raw(buf, len); - fclose(f); - return 0; -} - -void http_parse_querystring(const char *txt, void (*fn)(const char *name, const char *value)) -{ - const char *t = txt; - - while (t && *t) { - char *name = url_decode_parameter_name(&t); - if (*name) { - char *value = url_decode_parameter_value(&t); - fn(name, value); - free(value); - } - free(name); - } -} diff --git a/html.h b/html.h deleted file mode 100644 index fa4de77587..0000000000 --- a/html.h +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef HTML_H -#define HTML_H - -#include "cgit.h" - -extern void html_raw(const char *txt, size_t size); -extern void html(const char *txt); - -__attribute__((format (printf,1,2))) -extern void htmlf(const char *format,...); - -__attribute__((format (printf,1,2))) -extern void html_txtf(const char *format,...); - -__attribute__((format (printf,1,0))) -extern void html_vtxtf(const char *format, va_list ap); - -__attribute__((format (printf,1,2))) -extern void html_attrf(const char *format,...); - -extern void html_txt(const char *txt); -extern ssize_t html_ntxt(const char *txt, size_t len); -extern void html_attr(const char *txt); -extern void html_url_path(const char *txt); -extern void html_url_arg(const char *txt); -extern void html_header_arg_in_quotes(const char *txt); -extern void html_hidden(const char *name, const char *value); -extern void html_option(const char *value, const char *text, const char *selected_value); -extern void html_intoption(int value, const char *text, int selected_value); -extern void html_link_open(const char *url, const char *title, const char *class); -extern void html_link_close(void); -extern void html_fileperm(unsigned short mode); -extern int html_include(const char *filename); - -extern void http_parse_querystring(const char *txt, void (*fn)(const char *name, const char *value)); - -#endif /* HTML_H */ diff --git a/parsing.c b/parsing.c deleted file mode 100644 index 7b3980e6b1..0000000000 --- a/parsing.c +++ /dev/null @@ -1,224 +0,0 @@ -/* parsing.c: parsing of config files - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" - -/* - * url syntax: [repo ['/' cmd [ '/' path]]] - * repo: any valid repo url, may contain '/' - * cmd: log | commit | diff | tree | view | blob | snapshot - * path: any valid path, may contain '/' - * - */ -void cgit_parse_url(const char *url) -{ - char *c, *cmd, *p; - struct cgit_repo *repo; - - if (!url || url[0] == '\0') - return; - - ctx.qry.page = NULL; - ctx.repo = cgit_get_repoinfo(url); - if (ctx.repo) { - ctx.qry.repo = ctx.repo->url; - return; - } - - cmd = NULL; - c = strchr(url, '/'); - while (c) { - c[0] = '\0'; - repo = cgit_get_repoinfo(url); - if (repo) { - ctx.repo = repo; - cmd = c; - } - c[0] = '/'; - c = strchr(c + 1, '/'); - } - - if (ctx.repo) { - ctx.qry.repo = ctx.repo->url; - p = strchr(cmd + 1, '/'); - if (p) { - p[0] = '\0'; - if (p[1]) - ctx.qry.path = trim_end(p + 1, '/'); - } - if (cmd[1]) - ctx.qry.page = xstrdup(cmd + 1); - } -} - -static char *substr(const char *head, const char *tail) -{ - char *buf; - - if (tail < head) - return xstrdup(""); - buf = xmalloc(tail - head + 1); - strlcpy(buf, head, tail - head + 1); - return buf; -} - -static void parse_user(const char *t, char **name, char **email, unsigned long *date, int *tz) -{ - struct ident_split ident; - unsigned email_len; - - if (!split_ident_line(&ident, t, strchrnul(t, '\n') - t)) { - *name = substr(ident.name_begin, ident.name_end); - - email_len = ident.mail_end - ident.mail_begin; - *email = xmalloc(strlen("<") + email_len + strlen(">") + 1); - xsnprintf(*email, email_len + 3, "<%.*s>", email_len, ident.mail_begin); - - if (ident.date_begin) - *date = strtoul(ident.date_begin, NULL, 10); - if (ident.tz_begin) - *tz = atoi(ident.tz_begin); - } -} - -#ifdef NO_ICONV -#define reencode(a, b, c) -#else -static const char *reencode(char **txt, const char *src_enc, const char *dst_enc) -{ - char *tmp; - - if (!txt) - return NULL; - - if (!*txt || !src_enc || !dst_enc) - return *txt; - - /* no encoding needed if src_enc equals dst_enc */ - if (!strcasecmp(src_enc, dst_enc)) - return *txt; - - tmp = reencode_string(*txt, dst_enc, src_enc); - if (tmp) { - free(*txt); - *txt = tmp; - } - return *txt; -} -#endif - -static const char *next_header_line(const char *p) -{ - p = strchr(p, '\n'); - if (!p) - return NULL; - return p + 1; -} - -static int end_of_header(const char *p) -{ - return !p || (*p == '\n'); -} - -struct commitinfo *cgit_parse_commit(struct commit *commit) -{ - const int sha1hex_len = 40; - struct commitinfo *ret; - const char *p = get_cached_commit_buffer(the_repository, commit, NULL); - const char *t; - - ret = xcalloc(1, sizeof(struct commitinfo)); - ret->commit = commit; - - if (!p) - return ret; - - if (!skip_prefix(p, "tree ", &p)) - die("Bad commit: %s", oid_to_hex(&commit->object.oid)); - p += sha1hex_len + 1; - - while (skip_prefix(p, "parent ", &p)) - p += sha1hex_len + 1; - - if (p && skip_prefix(p, "author ", &p)) { - parse_user(p, &ret->author, &ret->author_email, - &ret->author_date, &ret->author_tz); - p = next_header_line(p); - } - - if (p && skip_prefix(p, "committer ", &p)) { - parse_user(p, &ret->committer, &ret->committer_email, - &ret->committer_date, &ret->committer_tz); - p = next_header_line(p); - } - - if (p && skip_prefix(p, "encoding ", &p)) { - t = strchr(p, '\n'); - if (t) { - ret->msg_encoding = substr(p, t + 1); - p = t + 1; - } - } - - if (!ret->msg_encoding) - ret->msg_encoding = xstrdup("UTF-8"); - - while (!end_of_header(p)) - p = next_header_line(p); - while (p && *p == '\n') - p++; - if (!p) - return ret; - - t = strchrnul(p, '\n'); - ret->subject = substr(p, t); - while (*t == '\n') - t++; - ret->msg = xstrdup(t); - - reencode(&ret->author, ret->msg_encoding, PAGE_ENCODING); - reencode(&ret->author_email, ret->msg_encoding, PAGE_ENCODING); - reencode(&ret->committer, ret->msg_encoding, PAGE_ENCODING); - reencode(&ret->committer_email, ret->msg_encoding, PAGE_ENCODING); - reencode(&ret->subject, ret->msg_encoding, PAGE_ENCODING); - reencode(&ret->msg, ret->msg_encoding, PAGE_ENCODING); - - return ret; -} - -struct taginfo *cgit_parse_tag(struct tag *tag) -{ - void *data; - enum object_type type; - unsigned long size; - const char *p; - struct taginfo *ret = NULL; - - data = read_object_file(&tag->object.oid, &type, &size); - if (!data || type != OBJ_TAG) - goto cleanup; - - ret = xcalloc(1, sizeof(struct taginfo)); - - for (p = data; !end_of_header(p); p = next_header_line(p)) { - if (skip_prefix(p, "tagger ", &p)) { - parse_user(p, &ret->tagger, &ret->tagger_email, - &ret->tagger_date, &ret->tagger_tz); - } - } - - while (p && *p == '\n') - p++; - - if (p && *p) - ret->msg = xstrdup(p); - -cleanup: - free(data); - return ret; -} diff --git a/robots.txt b/robots.txt deleted file mode 100644 index 4ce948fec2..0000000000 --- a/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -User-agent: * -Disallow: /*/snapshot/* -Allow: / diff --git a/scan-tree.c b/scan-tree.c deleted file mode 100644 index 6a2f65a86b..0000000000 --- a/scan-tree.c +++ /dev/null @@ -1,270 +0,0 @@ -/* scan-tree.c - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "scan-tree.h" -#include "configfile.h" -#include "html.h" -#include <config.h> - -/* return 1 if path contains a objects/ directory and a HEAD file */ -static int is_git_dir(const char *path) -{ - struct stat st; - struct strbuf pathbuf = STRBUF_INIT; - int result = 0; - - strbuf_addf(&pathbuf, "%s/objects", path); - if (stat(pathbuf.buf, &st)) { - if (errno != ENOENT) - fprintf(stderr, "Error checking path %s: %s (%d)\n", - path, strerror(errno), errno); - goto out; - } - if (!S_ISDIR(st.st_mode)) - goto out; - - strbuf_reset(&pathbuf); - strbuf_addf(&pathbuf, "%s/HEAD", path); - if (stat(pathbuf.buf, &st)) { - if (errno != ENOENT) - fprintf(stderr, "Error checking path %s: %s (%d)\n", - path, strerror(errno), errno); - goto out; - } - if (!S_ISREG(st.st_mode)) - goto out; - - result = 1; -out: - strbuf_release(&pathbuf); - return result; -} - -static struct cgit_repo *repo; -static repo_config_fn config_fn; - -static void scan_tree_repo_config(const char *name, const char *value) -{ - config_fn(repo, name, value); -} - -static int gitconfig_config(const char *key, const char *value, void *cb) -{ - const char *name; - - if (!strcmp(key, "gitweb.owner")) - config_fn(repo, "owner", value); - else if (!strcmp(key, "gitweb.description")) - config_fn(repo, "desc", value); - else if (!strcmp(key, "gitweb.category")) - config_fn(repo, "section", value); - else if (!strcmp(key, "gitweb.homepage")) - config_fn(repo, "homepage", value); - else if (skip_prefix(key, "cgit.", &name)) - config_fn(repo, name, value); - - return 0; -} - -static char *xstrrchr(char *s, char *from, int c) -{ - while (from >= s && *from != c) - from--; - return from < s ? NULL : from; -} - -static void add_repo(const char *base, struct strbuf *path, repo_config_fn fn) -{ - struct stat st; - struct passwd *pwd; - size_t pathlen; - struct strbuf rel = STRBUF_INIT; - char *p, *slash; - int n; - size_t size; - - if (stat(path->buf, &st)) { - fprintf(stderr, "Error accessing %s: %s (%d)\n", - path->buf, strerror(errno), errno); - return; - } - - strbuf_addch(path, '/'); - pathlen = path->len; - - if (ctx.cfg.strict_export) { - strbuf_addstr(path, ctx.cfg.strict_export); - if(stat(path->buf, &st)) - return; - strbuf_setlen(path, pathlen); - } - - strbuf_addstr(path, "noweb"); - if (!stat(path->buf, &st)) - return; - strbuf_setlen(path, pathlen); - - if (!starts_with(path->buf, base)) - strbuf_addbuf(&rel, path); - else - strbuf_addstr(&rel, path->buf + strlen(base) + 1); - - if (!strcmp(rel.buf + rel.len - 5, "/.git")) - strbuf_setlen(&rel, rel.len - 5); - else if (rel.len && rel.buf[rel.len - 1] == '/') - strbuf_setlen(&rel, rel.len - 1); - - repo = cgit_add_repo(rel.buf); - config_fn = fn; - if (ctx.cfg.enable_git_config) { - strbuf_addstr(path, "config"); - git_config_from_file(gitconfig_config, path->buf, NULL); - strbuf_setlen(path, pathlen); - } - - if (ctx.cfg.remove_suffix) { - size_t urllen; - strip_suffix(repo->url, ".git", &urllen); - strip_suffix_mem(repo->url, &urllen, "/"); - repo->url[urllen] = '\0'; - } - repo->path = xstrdup(path->buf); - while (!repo->owner) { - if ((pwd = getpwuid(st.st_uid)) == NULL) { - fprintf(stderr, "Error reading owner-info for %s: %s (%d)\n", - path->buf, strerror(errno), errno); - break; - } - if (pwd->pw_gecos) - if ((p = strchr(pwd->pw_gecos, ','))) - *p = '\0'; - repo->owner = xstrdup(pwd->pw_gecos ? pwd->pw_gecos : pwd->pw_name); - } - - if (repo->desc == cgit_default_repo_desc || !repo->desc) { - strbuf_addstr(path, "description"); - if (!stat(path->buf, &st)) - readfile(path->buf, &repo->desc, &size); - strbuf_setlen(path, pathlen); - } - - if (ctx.cfg.section_from_path) { - n = ctx.cfg.section_from_path; - if (n > 0) { - slash = rel.buf - 1; - while (slash && n && (slash = strchr(slash + 1, '/'))) - n--; - } else { - slash = rel.buf + rel.len; - while (slash && n && (slash = xstrrchr(rel.buf, slash - 1, '/'))) - n++; - } - if (slash && !n) { - *slash = '\0'; - repo->section = xstrdup(rel.buf); - *slash = '/'; - if (starts_with(repo->name, repo->section)) { - repo->name += strlen(repo->section); - if (*repo->name == '/') - repo->name++; - } - } - } - - strbuf_addstr(path, "cgitrc"); - if (!stat(path->buf, &st)) - parse_configfile(path->buf, &scan_tree_repo_config); - - strbuf_release(&rel); -} - -static void scan_path(const char *base, const char *path, repo_config_fn fn) -{ - DIR *dir = opendir(path); - struct dirent *ent; - struct strbuf pathbuf = STRBUF_INIT; - size_t pathlen = strlen(path); - struct stat st; - - if (!dir) { - fprintf(stderr, "Error opening directory %s: %s (%d)\n", - path, strerror(errno), errno); - return; - } - - strbuf_add(&pathbuf, path, strlen(path)); - if (is_git_dir(pathbuf.buf)) { - add_repo(base, &pathbuf, fn); - goto end; - } - strbuf_addstr(&pathbuf, "/.git"); - if (is_git_dir(pathbuf.buf)) { - add_repo(base, &pathbuf, fn); - goto end; - } - /* - * Add one because we don't want to lose the trailing '/' when we - * reset the length of pathbuf in the loop below. - */ - pathlen++; - while ((ent = readdir(dir)) != NULL) { - if (ent->d_name[0] == '.') { - if (ent->d_name[1] == '\0') - continue; - if (ent->d_name[1] == '.' && ent->d_name[2] == '\0') - continue; - if (!ctx.cfg.scan_hidden_path) - continue; - } - strbuf_setlen(&pathbuf, pathlen); - strbuf_addstr(&pathbuf, ent->d_name); - if (stat(pathbuf.buf, &st)) { - fprintf(stderr, "Error checking path %s: %s (%d)\n", - pathbuf.buf, strerror(errno), errno); - continue; - } - if (S_ISDIR(st.st_mode)) - scan_path(base, pathbuf.buf, fn); - } -end: - strbuf_release(&pathbuf); - closedir(dir); -} - -void scan_projects(const char *path, const char *projectsfile, repo_config_fn fn) -{ - struct strbuf line = STRBUF_INIT; - FILE *projects; - int err; - - projects = fopen(projectsfile, "r"); - if (!projects) { - fprintf(stderr, "Error opening projectsfile %s: %s (%d)\n", - projectsfile, strerror(errno), errno); - return; - } - while (strbuf_getline(&line, projects) != EOF) { - if (!line.len) - continue; - strbuf_insert(&line, 0, "/", 1); - strbuf_insert(&line, 0, path, strlen(path)); - scan_path(path, line.buf, fn); - } - if ((err = ferror(projects))) { - fprintf(stderr, "Error reading from projectsfile %s: %s (%d)\n", - projectsfile, strerror(err), err); - } - fclose(projects); - strbuf_release(&line); -} - -void scan_tree(const char *path, repo_config_fn fn) -{ - scan_path(path, path, fn); -} diff --git a/scan-tree.h b/scan-tree.h deleted file mode 100644 index 1afbd4bbcd..0000000000 --- a/scan-tree.h +++ /dev/null @@ -1,2 +0,0 @@ -extern void scan_projects(const char *path, const char *projectsfile, repo_config_fn fn); -extern void scan_tree(const char *path, repo_config_fn fn); diff --git a/shared.c b/shared.c deleted file mode 100644 index 8115469a7c..0000000000 --- a/shared.c +++ /dev/null @@ -1,579 +0,0 @@ -/* shared.c: global vars + some callback functions - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" - -struct cgit_repolist cgit_repolist; -struct cgit_context ctx; - -int chk_zero(int result, char *msg) -{ - if (result != 0) - die_errno("%s", msg); - return result; -} - -int chk_positive(int result, char *msg) -{ - if (result <= 0) - die_errno("%s", msg); - return result; -} - -int chk_non_negative(int result, char *msg) -{ - if (result < 0) - die_errno("%s", msg); - return result; -} - -char *cgit_default_repo_desc = "[no description]"; -struct cgit_repo *cgit_add_repo(const char *url) -{ - struct cgit_repo *ret; - - if (++cgit_repolist.count > cgit_repolist.length) { - if (cgit_repolist.length == 0) - cgit_repolist.length = 8; - else - cgit_repolist.length *= 2; - cgit_repolist.repos = xrealloc(cgit_repolist.repos, - cgit_repolist.length * - sizeof(struct cgit_repo)); - } - - ret = &cgit_repolist.repos[cgit_repolist.count-1]; - memset(ret, 0, sizeof(struct cgit_repo)); - ret->url = trim_end(url, '/'); - ret->name = ret->url; - ret->path = NULL; - ret->desc = cgit_default_repo_desc; - ret->extra_head_content = NULL; - ret->owner = NULL; - ret->homepage = NULL; - ret->section = ctx.cfg.section; - ret->snapshots = ctx.cfg.snapshots; - ret->enable_blame = ctx.cfg.enable_blame; - ret->enable_commit_graph = ctx.cfg.enable_commit_graph; - ret->enable_log_filecount = ctx.cfg.enable_log_filecount; - ret->enable_log_linecount = ctx.cfg.enable_log_linecount; - ret->enable_remote_branches = ctx.cfg.enable_remote_branches; - ret->enable_subject_links = ctx.cfg.enable_subject_links; - ret->enable_html_serving = ctx.cfg.enable_html_serving; - ret->max_stats = ctx.cfg.max_stats; - ret->branch_sort = ctx.cfg.branch_sort; - ret->commit_sort = ctx.cfg.commit_sort; - ret->module_link = ctx.cfg.module_link; - ret->readme = ctx.cfg.readme; - ret->mtime = -1; - ret->about_filter = ctx.cfg.about_filter; - ret->commit_filter = ctx.cfg.commit_filter; - ret->source_filter = ctx.cfg.source_filter; - ret->email_filter = ctx.cfg.email_filter; - ret->owner_filter = ctx.cfg.owner_filter; - ret->clone_url = ctx.cfg.clone_url; - ret->submodules.strdup_strings = 1; - ret->hide = ret->ignore = 0; - return ret; -} - -struct cgit_repo *cgit_get_repoinfo(const char *url) -{ - int i; - struct cgit_repo *repo; - - for (i = 0; i < cgit_repolist.count; i++) { - repo = &cgit_repolist.repos[i]; - if (repo->ignore) - continue; - if (!strcmp(repo->url, url)) - return repo; - } - return NULL; -} - -void cgit_free_commitinfo(struct commitinfo *info) -{ - free(info->author); - free(info->author_email); - free(info->committer); - free(info->committer_email); - free(info->subject); - free(info->msg); - free(info->msg_encoding); - free(info); -} - -char *trim_end(const char *str, char c) -{ - int len; - - if (str == NULL) - return NULL; - len = strlen(str); - while (len > 0 && str[len - 1] == c) - len--; - if (len == 0) - return NULL; - return xstrndup(str, len); -} - -char *ensure_end(const char *str, char c) -{ - size_t len = strlen(str); - char *result; - - if (len && str[len - 1] == c) - return xstrndup(str, len); - - result = xmalloc(len + 2); - memcpy(result, str, len); - result[len] = '/'; - result[len + 1] = '\0'; - return result; -} - -void strbuf_ensure_end(struct strbuf *sb, char c) -{ - if (!sb->len || sb->buf[sb->len - 1] != c) - strbuf_addch(sb, c); -} - -void cgit_add_ref(struct reflist *list, struct refinfo *ref) -{ - size_t size; - - if (list->count >= list->alloc) { - list->alloc += (list->alloc ? list->alloc : 4); - size = list->alloc * sizeof(struct refinfo *); - list->refs = xrealloc(list->refs, size); - } - list->refs[list->count++] = ref; -} - -static struct refinfo *cgit_mk_refinfo(const char *refname, const struct object_id *oid) -{ - struct refinfo *ref; - - ref = xmalloc(sizeof (struct refinfo)); - ref->refname = xstrdup(refname); - ref->object = parse_object(the_repository, oid); - switch (ref->object->type) { - case OBJ_TAG: - ref->tag = cgit_parse_tag((struct tag *)ref->object); - break; - case OBJ_COMMIT: - ref->commit = cgit_parse_commit((struct commit *)ref->object); - break; - } - return ref; -} - -void cgit_free_taginfo(struct taginfo *tag) -{ - if (tag->tagger) - free(tag->tagger); - if (tag->tagger_email) - free(tag->tagger_email); - if (tag->msg) - free(tag->msg); - free(tag); -} - -static void cgit_free_refinfo(struct refinfo *ref) -{ - if (ref->refname) - free((char *)ref->refname); - switch (ref->object->type) { - case OBJ_TAG: - cgit_free_taginfo(ref->tag); - break; - case OBJ_COMMIT: - cgit_free_commitinfo(ref->commit); - break; - } - free(ref); -} - -void cgit_free_reflist_inner(struct reflist *list) -{ - int i; - - for (i = 0; i < list->count; i++) { - cgit_free_refinfo(list->refs[i]); - } - free(list->refs); -} - -int cgit_refs_cb(const char *refname, const struct object_id *oid, int flags, - void *cb_data) -{ - struct reflist *list = (struct reflist *)cb_data; - struct refinfo *info = cgit_mk_refinfo(refname, oid); - - if (info) - cgit_add_ref(list, info); - return 0; -} - -void cgit_diff_tree_cb(struct diff_queue_struct *q, - struct diff_options *options, void *data) -{ - int i; - - for (i = 0; i < q->nr; i++) { - if (q->queue[i]->status == 'U') - continue; - ((filepair_fn)data)(q->queue[i]); - } -} - -static int load_mmfile(mmfile_t *file, const struct object_id *oid) -{ - enum object_type type; - - if (is_null_oid(oid)) { - file->ptr = (char *)""; - file->size = 0; - } else { - file->ptr = read_object_file(oid, &type, - (unsigned long *)&file->size); - } - return 1; -} - -/* - * Receive diff-buffers from xdiff and concatenate them as - * needed across multiple callbacks. - * - * This is basically a copy of xdiff-interface.c/xdiff_outf(), - * ripped from git and modified to use globals instead of - * a special callback-struct. - */ -static char *diffbuf = NULL; -static int buflen = 0; - -static int filediff_cb(void *priv, mmbuffer_t *mb, int nbuf) -{ - int i; - - for (i = 0; i < nbuf; i++) { - if (mb[i].ptr[mb[i].size-1] != '\n') { - /* Incomplete line */ - diffbuf = xrealloc(diffbuf, buflen + mb[i].size); - memcpy(diffbuf + buflen, mb[i].ptr, mb[i].size); - buflen += mb[i].size; - continue; - } - - /* we have a complete line */ - if (!diffbuf) { - ((linediff_fn)priv)(mb[i].ptr, mb[i].size); - continue; - } - diffbuf = xrealloc(diffbuf, buflen + mb[i].size); - memcpy(diffbuf + buflen, mb[i].ptr, mb[i].size); - ((linediff_fn)priv)(diffbuf, buflen + mb[i].size); - free(diffbuf); - diffbuf = NULL; - buflen = 0; - } - if (diffbuf) { - ((linediff_fn)priv)(diffbuf, buflen); - free(diffbuf); - diffbuf = NULL; - buflen = 0; - } - return 0; -} - -int cgit_diff_files(const struct object_id *old_oid, - const struct object_id *new_oid, unsigned long *old_size, - unsigned long *new_size, int *binary, int context, - int ignorews, linediff_fn fn) -{ - mmfile_t file1, file2; - xpparam_t diff_params; - xdemitconf_t emit_params; - xdemitcb_t emit_cb; - - if (!load_mmfile(&file1, old_oid) || !load_mmfile(&file2, new_oid)) - return 1; - - *old_size = file1.size; - *new_size = file2.size; - - if ((file1.ptr && buffer_is_binary(file1.ptr, file1.size)) || - (file2.ptr && buffer_is_binary(file2.ptr, file2.size))) { - *binary = 1; - if (file1.size) - free(file1.ptr); - if (file2.size) - free(file2.ptr); - return 0; - } - - memset(&diff_params, 0, sizeof(diff_params)); - memset(&emit_params, 0, sizeof(emit_params)); - memset(&emit_cb, 0, sizeof(emit_cb)); - diff_params.flags = XDF_NEED_MINIMAL; - if (ignorews) - diff_params.flags |= XDF_IGNORE_WHITESPACE; - emit_params.ctxlen = context > 0 ? context : 3; - emit_params.flags = XDL_EMIT_FUNCNAMES; - emit_cb.out_line = filediff_cb; - emit_cb.priv = fn; - xdl_diff(&file1, &file2, &diff_params, &emit_params, &emit_cb); - if (file1.size) - free(file1.ptr); - if (file2.size) - free(file2.ptr); - return 0; -} - -void cgit_diff_tree(const struct object_id *old_oid, - const struct object_id *new_oid, - filepair_fn fn, const char *prefix, int ignorews) -{ - struct diff_options opt; - struct pathspec_item item; - - memset(&item, 0, sizeof(item)); - diff_setup(&opt); - opt.output_format = DIFF_FORMAT_CALLBACK; - opt.detect_rename = 1; - opt.rename_limit = ctx.cfg.renamelimit; - opt.flags.recursive = 1; - if (ignorews) - DIFF_XDL_SET(&opt, IGNORE_WHITESPACE); - opt.format_callback = cgit_diff_tree_cb; - opt.format_callback_data = fn; - if (prefix) { - item.match = xstrdup(prefix); - item.len = strlen(prefix); - opt.pathspec.nr = 1; - opt.pathspec.items = &item; - } - diff_setup_done(&opt); - - if (old_oid && !is_null_oid(old_oid)) - diff_tree_oid(old_oid, new_oid, "", &opt); - else - diff_root_tree_oid(new_oid, "", &opt); - diffcore_std(&opt); - diff_flush(&opt); - - free(item.match); -} - -void cgit_diff_commit(struct commit *commit, filepair_fn fn, const char *prefix) -{ - const struct object_id *old_oid = NULL; - - if (commit->parents) - old_oid = &commit->parents->item->object.oid; - cgit_diff_tree(old_oid, &commit->object.oid, fn, prefix, - ctx.qry.ignorews); -} - -int cgit_parse_snapshots_mask(const char *str) -{ - struct string_list tokens = STRING_LIST_INIT_DUP; - struct string_list_item *item; - const struct cgit_snapshot_format *f; - int rv = 0; - - /* favor legacy setting */ - if (atoi(str)) - return 1; - - if (strcmp(str, "all") == 0) - return INT_MAX; - - string_list_split(&tokens, str, ' ', -1); - string_list_remove_empty_items(&tokens, 0); - - for_each_string_list_item(item, &tokens) { - for (f = cgit_snapshot_formats; f->suffix; f++) { - if (!strcmp(item->string, f->suffix) || - !strcmp(item->string, f->suffix + 1)) { - rv |= cgit_snapshot_format_bit(f); - break; - } - } - } - - string_list_clear(&tokens, 0); - return rv; -} - -typedef struct { - char * name; - char * value; -} cgit_env_var; - -void cgit_prepare_repo_env(struct cgit_repo * repo) -{ - cgit_env_var env_vars[] = { - { .name = "CGIT_REPO_URL", .value = repo->url }, - { .name = "CGIT_REPO_NAME", .value = repo->name }, - { .name = "CGIT_REPO_PATH", .value = repo->path }, - { .name = "CGIT_REPO_OWNER", .value = repo->owner }, - { .name = "CGIT_REPO_DEFBRANCH", .value = repo->defbranch }, - { .name = "CGIT_REPO_SECTION", .value = repo->section }, - { .name = "CGIT_REPO_CLONE_URL", .value = repo->clone_url } - }; - int env_var_count = ARRAY_SIZE(env_vars); - cgit_env_var *p, *q; - static char *warn = "cgit warning: failed to set env: %s=%s\n"; - - p = env_vars; - q = p + env_var_count; - for (; p < q; p++) - if (p->value && setenv(p->name, p->value, 1)) - fprintf(stderr, warn, p->name, p->value); -} - -/* Read the content of the specified file into a newly allocated buffer, - * zeroterminate the buffer and return 0 on success, errno otherwise. - */ -int readfile(const char *path, char **buf, size_t *size) -{ - int fd, e; - struct stat st; - - fd = open(path, O_RDONLY); - if (fd == -1) - return errno; - if (fstat(fd, &st)) { - e = errno; - close(fd); - return e; - } - if (!S_ISREG(st.st_mode)) { - close(fd); - return EISDIR; - } - *buf = xmalloc(st.st_size + 1); - *size = read_in_full(fd, *buf, st.st_size); - e = errno; - (*buf)[*size] = '\0'; - close(fd); - return (*size == st.st_size ? 0 : e); -} - -static int is_token_char(char c) -{ - return isalnum(c) || c == '_'; -} - -/* Replace name with getenv(name), return pointer to zero-terminating char - */ -static char *expand_macro(char *name, int maxlength) -{ - char *value; - size_t len; - - len = 0; - value = getenv(name); - if (value) { - len = strlen(value) + 1; - if (len > maxlength) - len = maxlength; - strlcpy(name, value, len); - --len; - } - return name + len; -} - -#define EXPBUFSIZE (1024 * 8) - -/* Replace all tokens prefixed by '$' in the specified text with the - * value of the named environment variable. - * NB: the return value is a static buffer, i.e. it must be strdup'd - * by the caller. - */ -char *expand_macros(const char *txt) -{ - static char result[EXPBUFSIZE]; - char *p, *start; - int len; - - p = result; - start = NULL; - while (p < result + EXPBUFSIZE - 1 && txt && *txt) { - *p = *txt; - if (start) { - if (!is_token_char(*txt)) { - if (p - start > 0) { - *p = '\0'; - len = result + EXPBUFSIZE - start - 1; - p = expand_macro(start, len) - 1; - } - start = NULL; - txt--; - } - p++; - txt++; - continue; - } - if (*txt == '$') { - start = p; - txt++; - continue; - } - p++; - txt++; - } - *p = '\0'; - if (start && p - start > 0) { - len = result + EXPBUFSIZE - start - 1; - p = expand_macro(start, len); - *p = '\0'; - } - return result; -} - -char *get_mimetype_for_filename(const char *filename) -{ - char *ext, *mimetype, *token, line[1024], *saveptr; - FILE *file; - struct string_list_item *mime; - - if (!filename) - return NULL; - - ext = strrchr(filename, '.'); - if (!ext) - return NULL; - ++ext; - if (!ext[0]) - return NULL; - mime = string_list_lookup(&ctx.cfg.mimetypes, ext); - if (mime) - return xstrdup(mime->util); - - if (!ctx.cfg.mimetype_file) - return NULL; - file = fopen(ctx.cfg.mimetype_file, "r"); - if (!file) - return NULL; - while (fgets(line, sizeof(line), file)) { - if (!line[0] || line[0] == '#') - continue; - mimetype = strtok_r(line, " \t\r\n", &saveptr); - while ((token = strtok_r(NULL, " \t\r\n", &saveptr))) { - if (!strcasecmp(ext, token)) { - fclose(file); - return xstrdup(mimetype); - } - } - } - fclose(file); - return NULL; -} diff --git a/tests/.gitignore b/tests/.gitignore deleted file mode 100644 index 3fd2e965c8..0000000000 --- a/tests/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -trash\ directory.t* -test-results diff --git a/tests/Makefile b/tests/Makefile deleted file mode 100644 index 65e1117338..0000000000 --- a/tests/Makefile +++ /dev/null @@ -1,17 +0,0 @@ -include ../git/config.mak.uname --include ../cgit.conf - -SHELL_PATH ?= $(SHELL) -SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH)) - -T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh) - -all: $(T) - -$(T): - @'$(SHELL_PATH_SQ)' $@ $(CGIT_TEST_OPTS) - -clean: - $(RM) -rf trash - -.PHONY: $(T) clean diff --git a/tests/filters/dump.lua b/tests/filters/dump.lua deleted file mode 100644 index 1f15c93105..0000000000 --- a/tests/filters/dump.lua +++ /dev/null @@ -1,17 +0,0 @@ -function filter_open(...) - buffer = "" - for i = 1, select("#", ...) do - buffer = buffer .. select(i, ...) .. " " - end -end - -function filter_close() - html(buffer) - return 0 -end - -function filter_write(str) - buffer = buffer .. string.upper(str) -end - - diff --git a/tests/filters/dump.sh b/tests/filters/dump.sh deleted file mode 100755 index da6f7a1b18..0000000000 --- a/tests/filters/dump.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -[ "$#" -gt 0 ] && printf "%s " "$*" -tr '[:lower:]' '[:upper:]' diff --git a/tests/setup.sh b/tests/setup.sh deleted file mode 100755 index 7590f04944..0000000000 --- a/tests/setup.sh +++ /dev/null @@ -1,176 +0,0 @@ -# This file should be sourced by all test-scripts -# -# Main functions: -# prepare_tests(description) - setup for testing, i.e. create repos+config -# run_test(description, script) - run one test, i.e. eval script -# -# Helper functions -# cgit_query(querystring) - call cgit with the specified querystring -# cgit_url(url) - call cgit with the specified virtual url -# -# Example script: -# -# . setup.sh -# prepare_tests "html validation" -# run_test 'repo index' 'cgit_url "/" | tidy -e' -# run_test 'repo summary' 'cgit_url "/foo" | tidy -e' - -# We don't want to run Git commands through Valgrind, so we filter out the -# --valgrind option here and handle it ourselves. We copy the arguments -# assuming that none contain a newline, although other whitespace is -# preserved. -LF=' -' -test_argv= - -while test $# != 0 -do - case "$1" in - --va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind) - cgit_valgrind=t - test_argv="$test_argv${LF}--verbose" - ;; - *) - test_argv="$test_argv$LF$1" - ;; - esac - shift -done - -OLDIFS=$IFS -IFS=$LF -set -- $test_argv -IFS=$OLDIFS - -: ${TEST_DIRECTORY=$(pwd)/../git/t} -: ${TEST_OUTPUT_DIRECTORY=$(pwd)} -TEST_NO_CREATE_REPO=YesPlease -. "$TEST_DIRECTORY"/test-lib.sh - -# Prepend the directory containing cgit to PATH. -if test -n "$cgit_valgrind" -then - GIT_VALGRIND="$TEST_DIRECTORY/valgrind" - CGIT_VALGRIND=$(cd ../valgrind && pwd) - PATH="$CGIT_VALGRIND/bin:$PATH" - export GIT_VALGRIND CGIT_VALGRIND -else - PATH="$(pwd)/../..:$PATH" -fi - -FILTER_DIRECTORY=$(cd ../filters && pwd) - -if cgit --version | grep -F -q "[+] Lua scripting"; then - export CGIT_HAS_LUA=1 -else - export CGIT_HAS_LUA=0 -fi - -mkrepo() { - name=$1 - count=$2 - test_create_repo "$name" - ( - cd "$name" - n=1 - while test $n -le $count - do - echo $n >file-$n - git add file-$n - git commit -m "commit $n" - n=$(expr $n + 1) - done - if test "$3" = "testplus" - then - echo "hello" >a+b - git add a+b - git commit -m "add a+b" - git branch "1+2" - fi - ) -} - -setup_repos() -{ - rm -rf cache - mkdir -p cache - mkrepo repos/foo 5 >/dev/null - mkrepo repos/bar 50 >/dev/null - mkrepo repos/foo+bar 10 testplus >/dev/null - mkrepo "repos/with space" 2 >/dev/null - mkrepo repos/filter 5 testplus >/dev/null - cat >cgitrc <<EOF -virtual-root=/ -cache-root=$PWD/cache - -cache-size=1021 -snapshots=tar.gz tar.bz zip -enable-log-filecount=1 -enable-log-linecount=1 -summary-log=5 -summary-branches=5 -summary-tags=5 -clone-url=git://example.org/\$CGIT_REPO_URL.git -enable-filter-overrides=1 - -repo.url=foo -repo.path=$PWD/repos/foo/.git -# Do not specify a description for this repo, as it then will be assigned -# the constant value "[no description]" (which actually used to cause a -# segfault). - -repo.url=bar -repo.path=$PWD/repos/bar/.git -repo.desc=the bar repo - -repo.url=foo+bar -repo.path=$PWD/repos/foo+bar/.git -repo.desc=the foo+bar repo - -repo.url=with space -repo.path=$PWD/repos/with space/.git -repo.desc=spaced repo - -repo.url=filter-exec -repo.path=$PWD/repos/filter/.git -repo.desc=filtered repo -repo.about-filter=exec:$FILTER_DIRECTORY/dump.sh -repo.commit-filter=exec:$FILTER_DIRECTORY/dump.sh -repo.email-filter=exec:$FILTER_DIRECTORY/dump.sh -repo.source-filter=exec:$FILTER_DIRECTORY/dump.sh -repo.readme=master:a+b -EOF - - if [ $CGIT_HAS_LUA -eq 1 ]; then - cat >>cgitrc <<EOF -repo.url=filter-lua -repo.path=$PWD/repos/filter/.git -repo.desc=filtered repo -repo.about-filter=lua:$FILTER_DIRECTORY/dump.lua -repo.commit-filter=lua:$FILTER_DIRECTORY/dump.lua -repo.email-filter=lua:$FILTER_DIRECTORY/dump.lua -repo.source-filter=lua:$FILTER_DIRECTORY/dump.lua -repo.readme=master:a+b -EOF - fi -} - -cgit_query() -{ - CGIT_CONFIG="$PWD/cgitrc" QUERY_STRING="$1" cgit -} - -cgit_url() -{ - CGIT_CONFIG="$PWD/cgitrc" QUERY_STRING="url=$1" cgit -} - -strip_headers() { - while read -r line - do - test -z "$line" && break - done - cat -} - -test -z "$CGIT_TEST_NO_CREATE_REPOS" && setup_repos diff --git a/tests/t0001-validate-git-versions.sh b/tests/t0001-validate-git-versions.sh deleted file mode 100755 index 3200f31101..0000000000 --- a/tests/t0001-validate-git-versions.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/sh - -test_description='Check Git version is correct' -CGIT_TEST_NO_CREATE_REPOS=YesPlease -. ./setup.sh - -test_expect_success 'extract Git version from Makefile' ' - sed -n -e "/^GIT_VER[ ]*=/ { - s/^GIT_VER[ ]*=[ ]*// - p - }" ../../Makefile >makefile_version -' - -# Note that Git's GIT-VERSION-GEN script applies "s/-/./g" to the version -# string to produce the internal version in the GIT-VERSION-FILE, so we -# must apply the same transformation to the version in the Makefile before -# comparing them. -test_expect_success 'test Git version matches Makefile' ' - ( cat ../../git/GIT-VERSION-FILE || echo "No GIT-VERSION-FILE" ) | - sed -e "s/GIT_VERSION[ ]*=[ ]*//" -e "s/\\.dirty$//" >git_version && - sed -e "s/-/./g" makefile_version >makefile_git_version && - test_cmp git_version makefile_git_version -' - -test_expect_success 'test submodule version matches Makefile' ' - if ! test -e ../../git/.git - then - echo "git/ is not a Git repository" >&2 - else - ( - cd ../.. && - sm_sha1=$(git ls-files --stage -- git | - sed -e "s/^[0-9]* \\([0-9a-f]*\\) [0-9] .*$/\\1/") && - cd git && - git describe --match "v[0-9]*" $sm_sha1 - ) | sed -e "s/^v//" -e "s/-/./" >sm_version && - test_cmp sm_version makefile_version - fi -' - -test_done diff --git a/tests/t0010-validate-html.sh b/tests/t0010-validate-html.sh deleted file mode 100755 index ca08d69d14..0000000000 --- a/tests/t0010-validate-html.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/sh - -test_description='Validate html with tidy' -. ./setup.sh - - -test_url() -{ - tidy_opt="-eq" - test -z "$NO_TIDY_WARNINGS" || tidy_opt+=" --show-warnings no" - cgit_url "$1" >tidy-$test_count.tmp || return - sed -e "1,4d" tidy-$test_count.tmp >tidy-$test_count || return - "$tidy" $tidy_opt tidy-$test_count - rc=$? - - # tidy returns with exitcode 1 on warnings, 2 on error - if test $rc = 2 - then - false - else - : - fi -} - -tidy=`which tidy 2>/dev/null` -test -n "$tidy" || { - skip_all='Skipping html validation tests: tidy not found' - test_done - exit -} - -test_expect_success 'index page' 'test_url ""' -test_expect_success 'foo' 'test_url "foo"' -test_expect_success 'foo/log' 'test_url "foo/log"' -test_expect_success 'foo/tree' 'test_url "foo/tree"' -test_expect_success 'foo/tree/file-1' 'test_url "foo/tree/file-1"' -test_expect_success 'foo/commit' 'test_url "foo/commit"' -test_expect_success 'foo/diff' 'test_url "foo/diff"' - -test_done diff --git a/tests/t0020-validate-cache.sh b/tests/t0020-validate-cache.sh deleted file mode 100755 index 657765d897..0000000000 --- a/tests/t0020-validate-cache.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/sh - -test_description='Validate cache' -. ./setup.sh - -test_expect_success 'verify cache-size=0' ' - - rm -f cache/* && - sed -e "s/cache-size=1021$/cache-size=0/" cgitrc >cgitrc.tmp && - mv -f cgitrc.tmp cgitrc && - cgit_url "" && - cgit_url "foo" && - cgit_url "foo/refs" && - cgit_url "foo/tree" && - cgit_url "foo/log" && - cgit_url "foo/diff" && - cgit_url "foo/patch" && - cgit_url "bar" && - cgit_url "bar/refs" && - cgit_url "bar/tree" && - cgit_url "bar/log" && - cgit_url "bar/diff" && - cgit_url "bar/patch" && - ls cache >output && - test_line_count = 0 output -' - -test_expect_success 'verify cache-size=1' ' - - rm -f cache/* && - sed -e "s/cache-size=0$/cache-size=1/" cgitrc >cgitrc.tmp && - mv -f cgitrc.tmp cgitrc && - cgit_url "" && - cgit_url "foo" && - cgit_url "foo/refs" && - cgit_url "foo/tree" && - cgit_url "foo/log" && - cgit_url "foo/diff" && - cgit_url "foo/patch" && - cgit_url "bar" && - cgit_url "bar/refs" && - cgit_url "bar/tree" && - cgit_url "bar/log" && - cgit_url "bar/diff" && - cgit_url "bar/patch" && - ls cache >output && - test_line_count = 1 output -' - -test_expect_success 'verify cache-size=1021' ' - - rm -f cache/* && - sed -e "s/cache-size=1$/cache-size=1021/" cgitrc >cgitrc.tmp && - mv -f cgitrc.tmp cgitrc && - cgit_url "" && - cgit_url "foo" && - cgit_url "foo/refs" && - cgit_url "foo/tree" && - cgit_url "foo/log" && - cgit_url "foo/diff" && - cgit_url "foo/patch" && - cgit_url "bar" && - cgit_url "bar/refs" && - cgit_url "bar/tree" && - cgit_url "bar/log" && - cgit_url "bar/diff" && - cgit_url "bar/patch" && - ls cache >output && - test_line_count = 13 output && - cgit_url "foo/ls_cache" >output.full && - strip_headers <output.full >output && - test_line_count = 13 output && - # Check that ls_cache output is cached correctly - cgit_url "foo/ls_cache" >output.second && - test_cmp output.full output.second -' - -test_done diff --git a/tests/t0101-index.sh b/tests/t0101-index.sh deleted file mode 100755 index 82ef9b04e5..0000000000 --- a/tests/t0101-index.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -test_description='Check content on index page' -. ./setup.sh - -test_expect_success 'generate index page' 'cgit_url "" >tmp' -test_expect_success 'find foo repo' 'grep "foo" tmp' -test_expect_success 'find foo description' 'grep "\[no description\]" tmp' -test_expect_success 'find bar repo' 'grep "bar" tmp' -test_expect_success 'find bar description' 'grep "the bar repo" tmp' -test_expect_success 'find foo+bar repo' 'grep ">foo+bar<" tmp' -test_expect_success 'verify foo+bar link' 'grep "/foo+bar/" tmp' -test_expect_success 'verify "with%20space" link' 'grep "/with%20space/" tmp' -test_expect_success 'no tree-link' '! grep "foo/tree" tmp' -test_expect_success 'no log-link' '! grep "foo/log" tmp' - -test_done diff --git a/tests/t0102-summary.sh b/tests/t0102-summary.sh deleted file mode 100755 index b8864cb187..0000000000 --- a/tests/t0102-summary.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh - -test_description='Check content on summary page' -. ./setup.sh - -test_expect_success 'generate foo summary' 'cgit_url "foo" >tmp' -test_expect_success 'find commit 1' 'grep "commit 1" tmp' -test_expect_success 'find commit 5' 'grep "commit 5" tmp' -test_expect_success 'find branch master' 'grep "master" tmp' -test_expect_success 'no tags' '! grep "tags" tmp' -test_expect_success 'clone-url expanded correctly' ' - grep "git://example.org/foo.git" tmp -' - -test_expect_success 'generate bar summary' 'cgit_url "bar" >tmp' -test_expect_success 'no commit 45' '! grep "commit 45" tmp' -test_expect_success 'find commit 46' 'grep "commit 46" tmp' -test_expect_success 'find commit 50' 'grep "commit 50" tmp' -test_expect_success 'find branch master' 'grep "master" tmp' -test_expect_success 'no tags' '! grep "tags" tmp' -test_expect_success 'clone-url expanded correctly' ' - grep "git://example.org/bar.git" tmp -' - -test_done diff --git a/tests/t0103-log.sh b/tests/t0103-log.sh deleted file mode 100755 index bdf1435a16..0000000000 --- a/tests/t0103-log.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh - -test_description='Check content on log page' -. ./setup.sh - -test_expect_success 'generate foo/log' 'cgit_url "foo/log" >tmp' -test_expect_success 'find commit 1' 'grep "commit 1" tmp' -test_expect_success 'find commit 5' 'grep "commit 5" tmp' - -test_expect_success 'generate bar/log' 'cgit_url "bar/log" >tmp' -test_expect_success 'find commit 1' 'grep "commit 1" tmp' -test_expect_success 'find commit 50' 'grep "commit 50" tmp' - -test_expect_success 'generate "with%20space/log?qt=grep&q=commit+1"' ' - cgit_url "with+space/log&qt=grep&q=commit+1" >tmp -' -test_expect_success 'find commit 1' 'grep "commit 1" tmp' -test_expect_success 'find link with %20 in path' 'grep "/with%20space/log/?qt=grep" tmp' -test_expect_success 'find link with + in arg' 'grep "/log/?qt=grep&q=commit+1" tmp' -test_expect_success 'no links with space in path' '! grep "href=./with space/" tmp' -test_expect_success 'no links with space in arg' '! grep "q=commit 1" tmp' -test_expect_success 'commit 2 is not visible' '! grep "commit 2" tmp' - -test_done diff --git a/tests/t0104-tree.sh b/tests/t0104-tree.sh deleted file mode 100755 index 2e140f5939..0000000000 --- a/tests/t0104-tree.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh - -test_description='Check content on tree page' -. ./setup.sh - -test_expect_success 'generate bar/tree' 'cgit_url "bar/tree" >tmp' -test_expect_success 'find file-1' 'grep "file-1" tmp' -test_expect_success 'find file-50' 'grep "file-50" tmp' - -test_expect_success 'generate bar/tree/file-50' 'cgit_url "bar/tree/file-50" >tmp' - -test_expect_success 'find line 1' ' - grep "<a id=.n1. href=.#n1.>1</a>" tmp -' - -test_expect_success 'no line 2' ' - ! grep "<a id=.n2. href=.#n2.>2</a>" tmp -' - -test_expect_success 'generate foo+bar/tree' 'cgit_url "foo%2bbar/tree" >tmp' - -test_expect_success 'verify a+b link' ' - grep "/foo+bar/tree/a+b" tmp -' - -test_expect_success 'generate foo+bar/tree?h=1+2' 'cgit_url "foo%2bbar/tree&h=1%2b2" >tmp' - -test_expect_success 'verify a+b?h=1+2 link' ' - grep "/foo+bar/tree/a+b?h=1%2b2" tmp -' - -test_done diff --git a/tests/t0105-commit.sh b/tests/t0105-commit.sh deleted file mode 100755 index 9cdf55c025..0000000000 --- a/tests/t0105-commit.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/sh - -test_description='Check content on commit page' -. ./setup.sh - -test_expect_success 'generate foo/commit' 'cgit_url "foo/commit" >tmp' -test_expect_success 'find tree link' 'grep "<a href=./foo/tree/.>" tmp' -test_expect_success 'find parent link' 'grep -E "<a href=./foo/commit/\?id=.+>" tmp' - -test_expect_success 'find commit subject' ' - grep "<div class=.commit-subject.>commit 5<" tmp -' - -test_expect_success 'find commit msg' 'grep "<div class=.commit-msg.></div>" tmp' -test_expect_success 'find diffstat' 'grep "<table summary=.diffstat. class=.diffstat.>" tmp' - -test_expect_success 'find diff summary' ' - grep "1 files changed, 1 insertions, 0 deletions" tmp -' - -test_expect_success 'get root commit' ' - root=$(cd repos/foo && git rev-list --reverse HEAD | head -1) && - cgit_url "foo/commit&id=$root" >tmp && - grep "</html>" tmp -' - -test_expect_success 'root commit contains diffstat' ' - grep "<a href=./foo/diff/file-1.id=[0-9a-f]\{40\}.>file-1</a>" tmp -' - -test_expect_success 'root commit contains diff' ' - grep ">diff --git a/file-1 b/file-1<" tmp && - grep "<div class=.add.>+1</div>" tmp -' - -test_done diff --git a/tests/t0106-diff.sh b/tests/t0106-diff.sh deleted file mode 100755 index 82b645ec72..0000000000 --- a/tests/t0106-diff.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -test_description='Check content on diff page' -. ./setup.sh - -test_expect_success 'generate foo/diff' 'cgit_url "foo/diff" >tmp' -test_expect_success 'find diff header' 'grep "a/file-5 b/file-5" tmp' -test_expect_success 'find blob link' 'grep "<a href=./foo/tree/file-5?id=" tmp' -test_expect_success 'find added file' 'grep "new file mode 100644" tmp' - -test_expect_success 'find hunk header' ' - grep "<div class=.hunk.>@@ -0,0 +1 @@</div>" tmp -' - -test_expect_success 'find added line' ' - grep "<div class=.add.>+5</div>" tmp -' - -test_done diff --git a/tests/t0107-snapshot.sh b/tests/t0107-snapshot.sh deleted file mode 100755 index 6cf7aaa6fc..0000000000 --- a/tests/t0107-snapshot.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/sh - -test_description='Verify snapshot' -. ./setup.sh - -test_expect_success 'get foo/snapshot/master.tar.gz' ' - cgit_url "foo/snapshot/master.tar.gz" >tmp -' - -test_expect_success 'check html headers' ' - head -n 1 tmp | - grep "Content-Type: application/x-gzip" && - - head -n 2 tmp | - grep "Content-Disposition: inline; filename=.master.tar.gz." -' - -test_expect_success 'strip off the header lines' ' - strip_headers <tmp >master.tar.gz -' - -test_expect_success 'verify gzip format' ' - gunzip --test master.tar.gz -' - -test_expect_success 'untar' ' - rm -rf master && - tar -xzf master.tar.gz -' - -test_expect_success 'count files' ' - ls master/ >output && - test_line_count = 5 output -' - -test_expect_success 'verify untarred file-5' ' - grep "^5$" master/file-5 && - test_line_count = 1 master/file-5 -' - -test_expect_success 'get foo/snapshot/master.zip' ' - cgit_url "foo/snapshot/master.zip" >tmp -' - -test_expect_success 'check HTML headers (zip)' ' - head -n 1 tmp | - grep "Content-Type: application/x-zip" && - - head -n 2 tmp | - grep "Content-Disposition: inline; filename=.master.zip." -' - -test_expect_success 'strip off the header lines (zip)' ' - strip_headers <tmp >master.zip -' - -if test -n "$(which unzip 2>/dev/null)"; then - test_set_prereq UNZIP -else - say 'Skipping ZIP validation tests: unzip not found' -fi - -test_expect_success UNZIP 'verify zip format' ' - unzip -t master.zip -' - -test_expect_success UNZIP 'unzip' ' - rm -rf master && - unzip master.zip -' - -test_expect_success UNZIP 'count files (zip)' ' - ls master/ >output && - test_line_count = 5 output -' - -test_expect_success UNZIP 'verify unzipped file-5' ' - grep "^5$" master/file-5 && - test_line_count = 1 master/file-5 -' - -test_done diff --git a/tests/t0108-patch.sh b/tests/t0108-patch.sh deleted file mode 100755 index 013d68024d..0000000000 --- a/tests/t0108-patch.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/sh - -test_description='Check content on patch page' -. ./setup.sh - -test_expect_success 'generate foo/patch' ' - cgit_query "url=foo/patch" >tmp -' - -test_expect_success 'find `From:` line' ' - grep "^From: " tmp -' - -test_expect_success 'find `Date:` line' ' - grep "^Date: " tmp -' - -test_expect_success 'find `Subject:` line' ' - grep "^Subject: commit 5" tmp -' - -test_expect_success 'find `cgit` signature' ' - tail -2 tmp | head -1 | grep "^cgit" -' - -test_expect_success 'compare with output of git-format-patch(1)' ' - CGIT_VERSION=$(sed -n "s/CGIT_VERSION = //p" ../../VERSION) && - git --git-dir="$PWD/repos/foo/.git" format-patch --subject-prefix="" --signature="cgit $CGIT_VERSION" --stdout HEAD^ >tmp2 && - strip_headers <tmp >tmp_ && - test_cmp tmp_ tmp2 -' - -test_expect_success 'find initial commit' ' - root=$(git --git-dir="$PWD/repos/foo/.git" rev-list --max-parents=0 HEAD) -' - -test_expect_success 'generate patch for initial commit' ' - cgit_query "url=foo/patch&id=$root" >tmp -' - -test_expect_success 'find `cgit` signature' ' - tail -2 tmp | head -1 | grep "^cgit" -' - -test_expect_success 'generate patches for multiple commits' ' - id=$(git --git-dir="$PWD/repos/foo/.git" rev-parse HEAD) && - id2=$(git --git-dir="$PWD/repos/foo/.git" rev-parse HEAD~3) && - cgit_query "url=foo/patch&id=$id&id2=$id2" >tmp -' - -test_expect_success 'find `cgit` signature' ' - tail -2 tmp | head -1 | grep "^cgit" -' - -test_expect_success 'compare with output of git-format-patch(1)' ' - CGIT_VERSION=$(sed -n "s/CGIT_VERSION = //p" ../../VERSION) && - git --git-dir="$PWD/repos/foo/.git" format-patch -N --subject-prefix="" --signature="cgit $CGIT_VERSION" --stdout HEAD~3..HEAD >tmp2 && - strip_headers <tmp >tmp_ && - test_cmp tmp_ tmp2 -' - -test_done diff --git a/tests/t0109-gitconfig.sh b/tests/t0109-gitconfig.sh deleted file mode 100755 index 3ba668490d..0000000000 --- a/tests/t0109-gitconfig.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/sh - -test_description='Ensure that git does not access $HOME' -. ./setup.sh - -test -n "$(which strace 2>/dev/null)" || { - skip_all='Skipping access validation tests: strace not found' - test_done - exit -} - -test_no_home_access () { - non_existent_path="/path/to/some/place/that/does/not/possibly/exist" - while test -d "$non_existent_path"; do - non_existent_path="$non_existent_path/$(date +%N)" - done && - strace \ - -E HOME="$non_existent_path" \ - -E CGIT_CONFIG="$PWD/cgitrc" \ - -E QUERY_STRING="url=$1" \ - -e access -f -o strace.out cgit && - test_must_fail grep "$non_existent_path" strace.out -} - -test_no_home_access_success() { - test_expect_success "do not access \$HOME: $1" " - test_no_home_access '$1' - " -} - -test_no_home_access_success -test_no_home_access_success foo -test_no_home_access_success foo/refs -test_no_home_access_success foo/log -test_no_home_access_success foo/tree -test_no_home_access_success foo/tree/file-1 -test_no_home_access_success foo/commit -test_no_home_access_success foo/diff -test_no_home_access_success foo/patch -test_no_home_access_success foo/snapshot/master.tar.gz - -test_done diff --git a/tests/t0110-rawdiff.sh b/tests/t0110-rawdiff.sh deleted file mode 100755 index 66fa7d5d37..0000000000 --- a/tests/t0110-rawdiff.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/sh - -test_description='Check content on rawdiff page' -. ./setup.sh - -test_expect_success 'generate foo/rawdiff' ' - cgit_query "url=foo/rawdiff" >tmp -' - -test_expect_success 'compare with output of git-diff(1)' ' - git --git-dir="$PWD/repos/foo/.git" diff HEAD^.. >tmp2 && - sed "1,4d" tmp >tmp_ && - cmp tmp_ tmp2 -' - -test_expect_success 'find initial commit' ' - root=$(git --git-dir="$PWD/repos/foo/.git" rev-list --max-parents=0 HEAD) -' - -test_expect_success 'generate diff for initial commit' ' - cgit_query "url=foo/rawdiff&id=$root" >tmp -' - -test_expect_success 'compare with output of git-diff-tree(1)' ' - git --git-dir="$PWD/repos/foo/.git" diff-tree -p --no-commit-id --root "$root" >tmp2 && - sed "1,4d" tmp >tmp_ && - cmp tmp_ tmp2 -' - -test_expect_success 'generate diff for multiple commits' ' - id=$(git --git-dir="$PWD/repos/foo/.git" rev-parse HEAD) && - id2=$(git --git-dir="$PWD/repos/foo/.git" rev-parse HEAD~3) && - cgit_query "url=foo/rawdiff&id=$id&id2=$id2" >tmp -' - -test_expect_success 'compare with output of git-diff(1)' ' - git --git-dir="$PWD/repos/foo/.git" diff HEAD~3..HEAD >tmp2 && - sed "1,4d" tmp >tmp_ && - cmp tmp_ tmp2 -' - -test_done diff --git a/tests/t0111-filter.sh b/tests/t0111-filter.sh deleted file mode 100755 index 2fdc3669f4..0000000000 --- a/tests/t0111-filter.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/sh - -test_description='Check filtered content' -. ./setup.sh - -prefixes="exec" -if [ $CGIT_HAS_LUA -eq 1 ]; then - prefixes="$prefixes lua" -fi - -for prefix in $prefixes -do - test_expect_success "generate filter-$prefix/tree/a%2bb" " - cgit_url 'filter-$prefix/tree/a%2bb' >tmp - " - - test_expect_success "check whether the $prefix source filter works" ' - grep "<code>a+b HELLO$" tmp - ' - - test_expect_success "generate filter-$prefix/about/" " - cgit_url 'filter-$prefix/about/' >tmp - " - - test_expect_success "check whether the $prefix about filter works" ' - grep "<div id='"'"'summary'"'"'>a+b HELLO$" tmp - ' - - test_expect_success "generate filter-$prefix/commit/" " - cgit_url 'filter-$prefix/commit/' >tmp - " - - test_expect_success "check whether the $prefix commit filter works" ' - grep "<div class='"'"'commit-subject'"'"'>ADD A+B" tmp - ' - - test_expect_success "check whether the $prefix email filter works for authors" ' - grep "<author@example.com> commit A U THOR <AUTHOR@EXAMPLE.COM>" tmp - ' - - test_expect_success "check whether the $prefix email filter works for committers" ' - grep "<committer@example.com> commit C O MITTER <COMMITTER@EXAMPLE.COM>" tmp - ' -done - -test_done diff --git a/tests/valgrind/bin/cgit b/tests/valgrind/bin/cgit deleted file mode 100755 index dcdfbe5320..0000000000 --- a/tests/valgrind/bin/cgit +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -# Note that we currently use Git's suppression file and there are variables -# $GIT_VALGRIND and $CGIT_VALGRIND which point to different places. -exec valgrind -q --error-exitcode=126 \ - --suppressions="$GIT_VALGRIND/default.supp" \ - --gen-suppressions=all \ - --leak-check=no \ - --track-origins=yes \ - --log-fd=4 \ - --input-fd=4 \ - "$CGIT_VALGRIND/../../cgit" "$@" diff --git a/ui-atom.c b/ui-atom.c deleted file mode 100644 index 1056f36397..0000000000 --- a/ui-atom.c +++ /dev/null @@ -1,149 +0,0 @@ -/* ui-atom.c: functions for atom feeds - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-atom.h" -#include "html.h" -#include "ui-shared.h" - -static void add_entry(struct commit *commit, const char *host) -{ - char delim = '&'; - char *hex; - char *mail, *t, *t2; - struct commitinfo *info; - - info = cgit_parse_commit(commit); - hex = oid_to_hex(&commit->object.oid); - html("<entry>\n"); - html("<title>"); - html_txt(info->subject); - html("</title>\n"); - html("<updated>"); - html_txt(show_date(info->committer_date, 0, - date_mode_from_type(DATE_ISO8601_STRICT))); - html("</updated>\n"); - html("<author>\n"); - if (info->author) { - html("<name>"); - html_txt(info->author); - html("</name>\n"); - } - if (info->author_email && !ctx.cfg.noplainemail) { - mail = xstrdup(info->author_email); - t = strchr(mail, '<'); - if (t) - t++; - else - t = mail; - t2 = strchr(t, '>'); - if (t2) - *t2 = '\0'; - html("<email>"); - html_txt(t); - html("</email>\n"); - free(mail); - } - html("</author>\n"); - html("<published>"); - html_txt(show_date(info->author_date, 0, - date_mode_from_type(DATE_ISO8601_STRICT))); - html("</published>\n"); - if (host) { - char *pageurl; - html("<link rel='alternate' type='text/html' href='"); - html(cgit_httpscheme()); - html_attr(host); - pageurl = cgit_pageurl(ctx.repo->url, "commit", NULL); - html_attr(pageurl); - if (ctx.cfg.virtual_root) - delim = '?'; - html_attrf("%cid=%s", delim, hex); - html("'/>\n"); - free(pageurl); - } - htmlf("<id>%s</id>\n", hex); - html("<content type='text'>\n"); - html_txt(info->msg); - html("</content>\n"); - html("<content type='xhtml'>\n"); - html("<div xmlns='http://www.w3.org/1999/xhtml'>\n"); - html("<pre>\n"); - html_txt(info->msg); - html("</pre>\n"); - html("</div>\n"); - html("</content>\n"); - html("</entry>\n"); - cgit_free_commitinfo(info); -} - - -void cgit_print_atom(char *tip, const char *path, int max_count) -{ - char *host; - const char *argv[] = {NULL, tip, NULL, NULL, NULL}; - struct commit *commit; - struct rev_info rev; - int argc = 2; - - if (ctx.qry.show_all) - argv[1] = "--all"; - else if (!tip) - argv[1] = ctx.qry.head; - - if (path) { - argv[argc++] = "--"; - argv[argc++] = path; - } - - init_revisions(&rev, NULL); - rev.abbrev = DEFAULT_ABBREV; - rev.commit_format = CMIT_FMT_DEFAULT; - rev.verbose_header = 1; - rev.show_root_diff = 0; - rev.max_count = max_count; - setup_revisions(argc, argv, &rev, NULL); - prepare_revision_walk(&rev); - - host = cgit_hosturl(); - ctx.page.mimetype = "text/xml"; - ctx.page.charset = "utf-8"; - cgit_print_http_headers(); - html("<feed xmlns='http://www.w3.org/2005/Atom'>\n"); - html("<title>"); - html_txt(ctx.repo->name); - if (path) { - html("/"); - html_txt(path); - } - if (tip && !ctx.qry.show_all) { - html(", branch "); - html_txt(tip); - } - html("</title>\n"); - html("<subtitle>"); - html_txt(ctx.repo->desc); - html("</subtitle>\n"); - if (host) { - char *repourl = cgit_repourl(ctx.repo->url); - html("<link rel='alternate' type='text/html' href='"); - html(cgit_httpscheme()); - html_attr(host); - html_attr(repourl); - html("'/>\n"); - free(repourl); - } - while ((commit = get_revision(&rev)) != NULL) { - add_entry(commit, host); - free_commit_buffer(the_repository->parsed_objects, commit); - free_commit_list(commit->parents); - commit->parents = NULL; - } - html("</feed>\n"); - free(host); -} diff --git a/ui-atom.h b/ui-atom.h deleted file mode 100644 index dda953bbf4..0000000000 --- a/ui-atom.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef UI_ATOM_H -#define UI_ATOM_H - -extern void cgit_print_atom(char *tip, const char *path, int max_count); - -#endif diff --git a/ui-blame.c b/ui-blame.c deleted file mode 100644 index 644c30ad28..0000000000 --- a/ui-blame.c +++ /dev/null @@ -1,302 +0,0 @@ -/* ui-blame.c: functions for blame output - * - * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-blame.h" -#include "html.h" -#include "ui-shared.h" -#include "argv-array.h" -#include "blame.h" - - -static char *emit_suspect_detail(struct blame_origin *suspect) -{ - struct commitinfo *info; - struct strbuf detail = STRBUF_INIT; - - info = cgit_parse_commit(suspect->commit); - - strbuf_addf(&detail, "author %s", info->author); - if (!ctx.cfg.noplainemail) - strbuf_addf(&detail, " %s", info->author_email); - strbuf_addf(&detail, " %s\n", - show_date(info->author_date, info->author_tz, - cgit_date_mode(DATE_ISO8601))); - - strbuf_addf(&detail, "committer %s", info->committer); - if (!ctx.cfg.noplainemail) - strbuf_addf(&detail, " %s", info->committer_email); - strbuf_addf(&detail, " %s\n\n", - show_date(info->committer_date, info->committer_tz, - cgit_date_mode(DATE_ISO8601))); - - strbuf_addstr(&detail, info->subject); - - cgit_free_commitinfo(info); - return strbuf_detach(&detail, NULL); -} - -static void emit_blame_entry_hash(struct blame_entry *ent) -{ - struct blame_origin *suspect = ent->suspect; - struct object_id *oid = &suspect->commit->object.oid; - unsigned long line = 0; - - char *detail = emit_suspect_detail(suspect); - html("<span class='sha1'>"); - cgit_commit_link(find_unique_abbrev(oid, DEFAULT_ABBREV), detail, - NULL, ctx.qry.head, oid_to_hex(oid), suspect->path); - html("</span>"); - free(detail); - - while (line++ < ent->num_lines) - html("\n"); -} - -static void emit_blame_entry_linenumber(struct blame_entry *ent) -{ - const char *numberfmt = "<a id='n%1$d' href='#n%1$d'>%1$d</a>\n"; - - unsigned long lineno = ent->lno; - while (lineno < ent->lno + ent->num_lines) - htmlf(numberfmt, ++lineno); -} - -static void emit_blame_entry_line_background(struct blame_scoreboard *sb, - struct blame_entry *ent) -{ - unsigned long line; - size_t len, maxlen = 2; - const char* pos, *endpos; - - for (line = ent->lno; line < ent->lno + ent->num_lines; line++) { - html("\n"); - pos = blame_nth_line(sb, line); - endpos = blame_nth_line(sb, line + 1); - len = 0; - while (pos < endpos) { - len++; - if (*pos++ == '\t') - len = (len + 7) & ~7; - } - if (len > maxlen) - maxlen = len; - } - - for (len = 0; len < maxlen - 1; len++) - html(" "); -} - -struct walk_tree_context { - char *curr_rev; - int match_baselen; - int state; -}; - -static void print_object(const struct object_id *oid, const char *path, - const char *basename, const char *rev) -{ - enum object_type type; - char *buf; - unsigned long size; - struct argv_array rev_argv = ARGV_ARRAY_INIT; - struct rev_info revs; - struct blame_scoreboard sb; - struct blame_origin *o; - struct blame_entry *ent = NULL; - - type = oid_object_info(the_repository, oid, &size); - if (type == OBJ_BAD) { - cgit_print_error_page(404, "Not found", "Bad object name: %s", - oid_to_hex(oid)); - return; - } - - buf = read_object_file(oid, &type, &size); - if (!buf) { - cgit_print_error_page(500, "Internal server error", - "Error reading object %s", oid_to_hex(oid)); - return; - } - - argv_array_push(&rev_argv, "blame"); - argv_array_push(&rev_argv, rev); - init_revisions(&revs, NULL); - revs.diffopt.flags.allow_textconv = 1; - setup_revisions(rev_argv.argc, rev_argv.argv, &revs, NULL); - init_scoreboard(&sb); - sb.revs = &revs; - sb.repo = the_repository; - setup_scoreboard(&sb, path, &o); - o->suspects = blame_entry_prepend(NULL, 0, sb.num_lines, o); - prio_queue_put(&sb.commits, o->commit); - blame_origin_decref(o); - sb.ent = NULL; - sb.path = path; - assign_blame(&sb, 0); - blame_sort_final(&sb); - blame_coalesce(&sb); - - cgit_set_title_from_path(path); - - cgit_print_layout_start(); - htmlf("blob: %s (", oid_to_hex(oid)); - cgit_plain_link("plain", NULL, NULL, ctx.qry.head, rev, path); - html(") ("); - cgit_tree_link("tree", NULL, NULL, ctx.qry.head, rev, path); - html(")\n"); - - if (ctx.cfg.max_blob_size && size / 1024 > ctx.cfg.max_blob_size) { - htmlf("<div class='error'>blob size (%ldKB)" - " exceeds display size limit (%dKB).</div>", - size / 1024, ctx.cfg.max_blob_size); - goto cleanup; - } - - html("<table class='blame blob'>\n<tr>\n"); - - /* Commit hashes */ - html("<td class='hashes'>"); - for (ent = sb.ent; ent; ent = ent->next) { - html("<div class='alt'><pre>"); - emit_blame_entry_hash(ent); - html("</pre></div>"); - } - html("</td>\n"); - - /* Line numbers */ - if (ctx.cfg.enable_tree_linenumbers) { - html("<td class='linenumbers'>"); - for (ent = sb.ent; ent; ent = ent->next) { - html("<div class='alt'><pre>"); - emit_blame_entry_linenumber(ent); - html("</pre></div>"); - } - html("</td>\n"); - } - - html("<td class='lines'><div>"); - - /* Colored bars behind lines */ - html("<div>"); - for (ent = sb.ent; ent; ) { - struct blame_entry *e = ent->next; - html("<div class='alt'><pre>"); - emit_blame_entry_line_background(&sb, ent); - html("</pre></div>"); - free(ent); - ent = e; - } - html("</div>"); - - free((void *)sb.final_buf); - - /* Lines */ - html("<pre><code>"); - if (ctx.repo->source_filter) { - char *filter_arg = xstrdup(basename); - cgit_open_filter(ctx.repo->source_filter, filter_arg); - html_raw(buf, size); - cgit_close_filter(ctx.repo->source_filter); - free(filter_arg); - } else { - html_txt(buf); - } - html("</code></pre>"); - - html("</div></td>\n"); - - html("</tr>\n</table>\n"); - - cgit_print_layout_end(); - -cleanup: - free(buf); -} - -static int walk_tree(const struct object_id *oid, struct strbuf *base, - const char *pathname, unsigned mode, int stage, - void *cbdata) -{ - struct walk_tree_context *walk_tree_ctx = cbdata; - - if (base->len == walk_tree_ctx->match_baselen) { - if (S_ISREG(mode)) { - struct strbuf buffer = STRBUF_INIT; - strbuf_addbuf(&buffer, base); - strbuf_addstr(&buffer, pathname); - print_object(oid, buffer.buf, pathname, - walk_tree_ctx->curr_rev); - strbuf_release(&buffer); - walk_tree_ctx->state = 1; - } else if (S_ISDIR(mode)) { - walk_tree_ctx->state = 2; - } - } else if (base->len < INT_MAX - && (int)base->len > walk_tree_ctx->match_baselen) { - walk_tree_ctx->state = 2; - } else if (S_ISDIR(mode)) { - return READ_TREE_RECURSIVE; - } - return 0; -} - -static int basedir_len(const char *path) -{ - char *p = strrchr(path, '/'); - if (p) - return p - path + 1; - return 0; -} - -void cgit_print_blame(void) -{ - const char *rev = ctx.qry.sha1; - struct object_id oid; - struct commit *commit; - struct pathspec_item path_items = { - .match = ctx.qry.path, - .len = ctx.qry.path ? strlen(ctx.qry.path) : 0 - }; - struct pathspec paths = { - .nr = 1, - .items = &path_items - }; - struct walk_tree_context walk_tree_ctx = { - .state = 0 - }; - - if (!rev) - rev = ctx.qry.head; - - if (get_oid(rev, &oid)) { - cgit_print_error_page(404, "Not found", - "Invalid revision name: %s", rev); - return; - } - commit = lookup_commit_reference(the_repository, &oid); - if (!commit || parse_commit(commit)) { - cgit_print_error_page(404, "Not found", - "Invalid commit reference: %s", rev); - return; - } - - walk_tree_ctx.curr_rev = xstrdup(rev); - walk_tree_ctx.match_baselen = (path_items.match) ? - basedir_len(path_items.match) : -1; - - read_tree_recursive(the_repository, commit->maybe_tree, "", 0, 0, - &paths, walk_tree, &walk_tree_ctx); - if (!walk_tree_ctx.state) - cgit_print_error_page(404, "Not found", "Not found"); - else if (walk_tree_ctx.state == 2) - cgit_print_error_page(404, "No blame for folders", - "Blame is not available for folders."); - - free(walk_tree_ctx.curr_rev); -} diff --git a/ui-blame.h b/ui-blame.h deleted file mode 100644 index 5b97e03591..0000000000 --- a/ui-blame.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef UI_BLAME_H -#define UI_BLAME_H - -extern void cgit_print_blame(void); - -#endif /* UI_BLAME_H */ diff --git a/ui-blob.c b/ui-blob.c deleted file mode 100644 index 30e2d4bf5f..0000000000 --- a/ui-blob.c +++ /dev/null @@ -1,181 +0,0 @@ -/* ui-blob.c: show blob content - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-blob.h" -#include "html.h" -#include "ui-shared.h" - -struct walk_tree_context { - const char *match_path; - struct object_id *matched_oid; - unsigned int found_path:1; - unsigned int file_only:1; -}; - -static int walk_tree(const struct object_id *oid, struct strbuf *base, - const char *pathname, unsigned mode, int stage, void *cbdata) -{ - struct walk_tree_context *walk_tree_ctx = cbdata; - - if (walk_tree_ctx->file_only && !S_ISREG(mode)) - return READ_TREE_RECURSIVE; - if (strncmp(base->buf, walk_tree_ctx->match_path, base->len) - || strcmp(walk_tree_ctx->match_path + base->len, pathname)) - return READ_TREE_RECURSIVE; - oidcpy(walk_tree_ctx->matched_oid, oid); - walk_tree_ctx->found_path = 1; - return 0; -} - -int cgit_ref_path_exists(const char *path, const char *ref, int file_only) -{ - struct object_id oid; - unsigned long size; - struct pathspec_item path_items = { - .match = xstrdup(path), - .len = strlen(path) - }; - struct pathspec paths = { - .nr = 1, - .items = &path_items - }; - struct walk_tree_context walk_tree_ctx = { - .match_path = path, - .matched_oid = &oid, - .found_path = 0, - .file_only = file_only - }; - - if (get_oid(ref, &oid)) - goto done; - if (oid_object_info(the_repository, &oid, &size) != OBJ_COMMIT) - goto done; - read_tree_recursive(the_repository, lookup_commit_reference(the_repository, &oid)->maybe_tree, - "", 0, 0, &paths, walk_tree, &walk_tree_ctx); - -done: - free(path_items.match); - return walk_tree_ctx.found_path; -} - -int cgit_print_file(char *path, const char *head, int file_only) -{ - struct object_id oid; - enum object_type type; - char *buf; - unsigned long size; - struct commit *commit; - struct pathspec_item path_items = { - .match = path, - .len = strlen(path) - }; - struct pathspec paths = { - .nr = 1, - .items = &path_items - }; - struct walk_tree_context walk_tree_ctx = { - .match_path = path, - .matched_oid = &oid, - .found_path = 0, - .file_only = file_only - }; - - if (get_oid(head, &oid)) - return -1; - type = oid_object_info(the_repository, &oid, &size); - if (type == OBJ_COMMIT) { - commit = lookup_commit_reference(the_repository, &oid); - read_tree_recursive(the_repository, commit->maybe_tree, - "", 0, 0, &paths, walk_tree, &walk_tree_ctx); - if (!walk_tree_ctx.found_path) - return -1; - type = oid_object_info(the_repository, &oid, &size); - } - if (type == OBJ_BAD) - return -1; - buf = read_object_file(&oid, &type, &size); - if (!buf) - return -1; - buf[size] = '\0'; - html_raw(buf, size); - free(buf); - return 0; -} - -void cgit_print_blob(const char *hex, char *path, const char *head, int file_only) -{ - struct object_id oid; - enum object_type type; - char *buf; - unsigned long size; - struct commit *commit; - struct pathspec_item path_items = { - .match = path, - .len = path ? strlen(path) : 0 - }; - struct pathspec paths = { - .nr = 1, - .items = &path_items - }; - struct walk_tree_context walk_tree_ctx = { - .match_path = path, - .matched_oid = &oid, - .found_path = 0, - .file_only = file_only - }; - - if (hex) { - if (get_oid_hex(hex, &oid)) { - cgit_print_error_page(400, "Bad request", - "Bad hex value: %s", hex); - return; - } - } else { - if (get_oid(head, &oid)) { - cgit_print_error_page(404, "Not found", - "Bad ref: %s", head); - return; - } - } - - type = oid_object_info(the_repository, &oid, &size); - - if ((!hex) && type == OBJ_COMMIT && path) { - commit = lookup_commit_reference(the_repository, &oid); - read_tree_recursive(the_repository, commit->maybe_tree, - "", 0, 0, &paths, walk_tree, &walk_tree_ctx); - type = oid_object_info(the_repository, &oid, &size); - } - - if (type == OBJ_BAD) { - cgit_print_error_page(404, "Not found", - "Bad object name: %s", hex); - return; - } - - buf = read_object_file(&oid, &type, &size); - if (!buf) { - cgit_print_error_page(500, "Internal server error", - "Error reading object %s", hex); - return; - } - - buf[size] = '\0'; - if (buffer_is_binary(buf, size)) - ctx.page.mimetype = "application/octet-stream"; - else - ctx.page.mimetype = "text/plain"; - ctx.page.filename = path; - - html("X-Content-Type-Options: nosniff\n"); - html("Content-Security-Policy: default-src 'none'\n"); - cgit_print_http_headers(); - html_raw(buf, size); - free(buf); -} diff --git a/ui-blob.h b/ui-blob.h deleted file mode 100644 index 16847b20b1..0000000000 --- a/ui-blob.h +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef UI_BLOB_H -#define UI_BLOB_H - -extern int cgit_ref_path_exists(const char *path, const char *ref, int file_only); -extern int cgit_print_file(char *path, const char *head, int file_only); -extern void cgit_print_blob(const char *hex, char *path, const char *head, int file_only); - -#endif /* UI_BLOB_H */ diff --git a/ui-clone.c b/ui-clone.c deleted file mode 100644 index 5dccb63976..0000000000 --- a/ui-clone.c +++ /dev/null @@ -1,126 +0,0 @@ -/* ui-clone.c: functions for http cloning, based on - * git's http-backend.c by Shawn O. Pearce - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-clone.h" -#include "html.h" -#include "ui-shared.h" -#include "packfile.h" -#include "object-store.h" - -static int print_ref_info(const char *refname, const struct object_id *oid, - int flags, void *cb_data) -{ - struct object *obj; - - if (!(obj = parse_object(the_repository, oid))) - return 0; - - htmlf("%s\t%s\n", oid_to_hex(oid), refname); - if (obj->type == OBJ_TAG) { - if (!(obj = deref_tag(the_repository, obj, refname, 0))) - return 0; - htmlf("%s\t%s^{}\n", oid_to_hex(&obj->oid), refname); - } - return 0; -} - -static void print_pack_info(void) -{ - struct packed_git *pack; - char *offset; - - ctx.page.mimetype = "text/plain"; - ctx.page.filename = "objects/info/packs"; - cgit_print_http_headers(); - reprepare_packed_git(the_repository); - for (pack = get_packed_git(the_repository); pack; pack = pack->next) { - if (pack->pack_local) { - offset = strrchr(pack->pack_name, '/'); - if (offset && offset[1] != '\0') - ++offset; - else - offset = pack->pack_name; - htmlf("P %s\n", offset); - } - } -} - -static void send_file(const char *path) -{ - struct stat st; - - if (stat(path, &st)) { - switch (errno) { - case ENOENT: - cgit_print_error_page(404, "Not found", "Not found"); - break; - case EACCES: - cgit_print_error_page(403, "Forbidden", "Forbidden"); - break; - default: - cgit_print_error_page(400, "Bad request", "Bad request"); - } - return; - } - ctx.page.mimetype = "application/octet-stream"; - ctx.page.filename = path; - skip_prefix(path, ctx.repo->path, &ctx.page.filename); - skip_prefix(ctx.page.filename, "/", &ctx.page.filename); - cgit_print_http_headers(); - html_include(path); -} - -void cgit_clone_info(void) -{ - if (!ctx.qry.path || strcmp(ctx.qry.path, "refs")) { - cgit_print_error_page(400, "Bad request", "Bad request"); - return; - } - - ctx.page.mimetype = "text/plain"; - ctx.page.filename = "info/refs"; - cgit_print_http_headers(); - for_each_ref(print_ref_info, NULL); -} - -void cgit_clone_objects(void) -{ - char *p; - - if (!ctx.qry.path) - goto err; - - if (!strcmp(ctx.qry.path, "info/packs")) { - print_pack_info(); - return; - } - - /* Avoid directory traversal by forbidding "..", but also work around - * other funny business by just specifying a fairly strict format. For - * example, now we don't have to stress out about the Cygwin port. - */ - for (p = ctx.qry.path; *p; ++p) { - if (*p == '.' && *(p + 1) == '.') - goto err; - if (!isalnum(*p) && *p != '/' && *p != '.' && *p != '-') - goto err; - } - - send_file(git_path("objects/%s", ctx.qry.path)); - return; - -err: - cgit_print_error_page(400, "Bad request", "Bad request"); -} - -void cgit_clone_head(void) -{ - send_file(git_path("%s", "HEAD")); -} diff --git a/ui-clone.h b/ui-clone.h deleted file mode 100644 index 3e460a3dbc..0000000000 --- a/ui-clone.h +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef UI_CLONE_H -#define UI_CLONE_H - -void cgit_clone_info(void); -void cgit_clone_objects(void); -void cgit_clone_head(void); - -#endif /* UI_CLONE_H */ diff --git a/ui-commit.c b/ui-commit.c deleted file mode 100644 index 9a47b54c56..0000000000 --- a/ui-commit.c +++ /dev/null @@ -1,147 +0,0 @@ -/* ui-commit.c: generate commit view - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-commit.h" -#include "html.h" -#include "ui-shared.h" -#include "ui-diff.h" -#include "ui-log.h" - -void cgit_print_commit(char *hex, const char *prefix) -{ - struct commit *commit, *parent; - struct commitinfo *info, *parent_info; - struct commit_list *p; - struct strbuf notes = STRBUF_INIT; - struct object_id oid; - char *tmp, *tmp2; - int parents = 0; - - if (!hex) - hex = ctx.qry.head; - - if (get_oid(hex, &oid)) { - cgit_print_error_page(400, "Bad request", - "Bad object id: %s", hex); - return; - } - commit = lookup_commit_reference(the_repository, &oid); - if (!commit) { - cgit_print_error_page(404, "Not found", - "Bad commit reference: %s", hex); - return; - } - info = cgit_parse_commit(commit); - - format_display_notes(&oid, ¬es, PAGE_ENCODING, 0); - - load_ref_decorations(NULL, DECORATE_FULL_REFS); - - cgit_print_layout_start(); - cgit_print_diff_ctrls(); - html("<table summary='commit info' class='commit-info'>\n"); - html("<tr><th>author</th><td>"); - cgit_open_filter(ctx.repo->email_filter, info->author_email, "commit"); - html_txt(info->author); - if (!ctx.cfg.noplainemail) { - html(" "); - html_txt(info->author_email); - } - cgit_close_filter(ctx.repo->email_filter); - html("</td><td class='right'>"); - html_txt(show_date(info->author_date, info->author_tz, - cgit_date_mode(DATE_ISO8601))); - html("</td></tr>\n"); - html("<tr><th>committer</th><td>"); - cgit_open_filter(ctx.repo->email_filter, info->committer_email, "commit"); - html_txt(info->committer); - if (!ctx.cfg.noplainemail) { - html(" "); - html_txt(info->committer_email); - } - cgit_close_filter(ctx.repo->email_filter); - html("</td><td class='right'>"); - html_txt(show_date(info->committer_date, info->committer_tz, - cgit_date_mode(DATE_ISO8601))); - html("</td></tr>\n"); - html("<tr><th>commit</th><td colspan='2' class='sha1'>"); - tmp = oid_to_hex(&commit->object.oid); - cgit_commit_link(tmp, NULL, NULL, ctx.qry.head, tmp, prefix); - html(" ("); - cgit_patch_link("patch", NULL, NULL, NULL, tmp, prefix); - html(")</td></tr>\n"); - html("<tr><th>tree</th><td colspan='2' class='sha1'>"); - tmp = xstrdup(hex); - cgit_tree_link(oid_to_hex(&commit->maybe_tree->object.oid), NULL, NULL, - ctx.qry.head, tmp, NULL); - if (prefix) { - html(" /"); - cgit_tree_link(prefix, NULL, NULL, ctx.qry.head, tmp, prefix); - } - free(tmp); - html("</td></tr>\n"); - for (p = commit->parents; p; p = p->next) { - parent = lookup_commit_reference(the_repository, &p->item->object.oid); - if (!parent) { - html("<tr><td colspan='3'>"); - cgit_print_error("Error reading parent commit"); - html("</td></tr>"); - continue; - } - html("<tr><th>parent</th>" - "<td colspan='2' class='sha1'>"); - tmp = tmp2 = oid_to_hex(&p->item->object.oid); - if (ctx.repo->enable_subject_links) { - parent_info = cgit_parse_commit(parent); - tmp2 = parent_info->subject; - } - cgit_commit_link(tmp2, NULL, NULL, ctx.qry.head, tmp, prefix); - html(" ("); - cgit_diff_link("diff", NULL, NULL, ctx.qry.head, hex, - oid_to_hex(&p->item->object.oid), prefix); - html(")</td></tr>"); - parents++; - } - if (ctx.repo->snapshots) { - html("<tr><th>download</th><td colspan='2' class='sha1'>"); - cgit_print_snapshot_links(ctx.repo, hex, "<br/>"); - html("</td></tr>"); - } - html("</table>\n"); - html("<div class='commit-subject'>"); - cgit_open_filter(ctx.repo->commit_filter); - html_txt(info->subject); - cgit_close_filter(ctx.repo->commit_filter); - show_commit_decorations(commit); - html("</div>"); - html("<div class='commit-msg'>"); - cgit_open_filter(ctx.repo->commit_filter); - html_txt(info->msg); - cgit_close_filter(ctx.repo->commit_filter); - html("</div>"); - if (notes.len != 0) { - html("<div class='notes-header'>Notes</div>"); - html("<div class='notes'>"); - cgit_open_filter(ctx.repo->commit_filter); - html_txt(notes.buf); - cgit_close_filter(ctx.repo->commit_filter); - html("</div>"); - html("<div class='notes-footer'></div>"); - } - if (parents < 3) { - if (parents) - tmp = oid_to_hex(&commit->parents->item->object.oid); - else - tmp = NULL; - cgit_print_diff(ctx.qry.sha1, tmp, prefix, 0, 0); - } - strbuf_release(¬es); - cgit_free_commitinfo(info); - cgit_print_layout_end(); -} diff --git a/ui-commit.h b/ui-commit.h deleted file mode 100644 index 8198b4bacc..0000000000 --- a/ui-commit.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef UI_COMMIT_H -#define UI_COMMIT_H - -extern void cgit_print_commit(char *hex, const char *prefix); - -#endif /* UI_COMMIT_H */ diff --git a/ui-diff.c b/ui-diff.c deleted file mode 100644 index c60aefd1d6..0000000000 --- a/ui-diff.c +++ /dev/null @@ -1,501 +0,0 @@ -/* ui-diff.c: show diff between two blobs - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-diff.h" -#include "html.h" -#include "ui-shared.h" -#include "ui-ssdiff.h" - -struct object_id old_rev_oid[1]; -struct object_id new_rev_oid[1]; - -static int files, slots; -static int total_adds, total_rems, max_changes; -static int lines_added, lines_removed; - -static struct fileinfo { - char status; - struct object_id old_oid[1]; - struct object_id new_oid[1]; - unsigned short old_mode; - unsigned short new_mode; - char *old_path; - char *new_path; - unsigned int added; - unsigned int removed; - unsigned long old_size; - unsigned long new_size; - unsigned int binary:1; -} *items; - -static int use_ssdiff = 0; -static struct diff_filepair *current_filepair; -static const char *current_prefix; - -struct diff_filespec *cgit_get_current_old_file(void) -{ - return current_filepair->one; -} - -struct diff_filespec *cgit_get_current_new_file(void) -{ - return current_filepair->two; -} - -static void print_fileinfo(struct fileinfo *info) -{ - char *class; - - switch (info->status) { - case DIFF_STATUS_ADDED: - class = "add"; - break; - case DIFF_STATUS_COPIED: - class = "cpy"; - break; - case DIFF_STATUS_DELETED: - class = "del"; - break; - case DIFF_STATUS_MODIFIED: - class = "upd"; - break; - case DIFF_STATUS_RENAMED: - class = "mov"; - break; - case DIFF_STATUS_TYPE_CHANGED: - class = "typ"; - break; - case DIFF_STATUS_UNKNOWN: - class = "unk"; - break; - case DIFF_STATUS_UNMERGED: - class = "stg"; - break; - default: - die("bug: unhandled diff status %c", info->status); - } - - html("<tr>"); - html("<td class='mode'>"); - if (is_null_oid(info->new_oid)) { - cgit_print_filemode(info->old_mode); - } else { - cgit_print_filemode(info->new_mode); - } - - if (info->old_mode != info->new_mode && - !is_null_oid(info->old_oid) && - !is_null_oid(info->new_oid)) { - html("<span class='modechange'>["); - cgit_print_filemode(info->old_mode); - html("]</span>"); - } - htmlf("</td><td class='%s'>", class); - cgit_diff_link(info->new_path, NULL, NULL, ctx.qry.head, ctx.qry.sha1, - ctx.qry.sha2, info->new_path); - if (info->status == DIFF_STATUS_COPIED || info->status == DIFF_STATUS_RENAMED) { - htmlf(" (%s from ", - info->status == DIFF_STATUS_COPIED ? "copied" : "renamed"); - html_txt(info->old_path); - html(")"); - } - html("</td><td class='right'>"); - if (info->binary) { - htmlf("bin</td><td class='graph'>%ld -> %ld bytes", - info->old_size, info->new_size); - return; - } - htmlf("%d", info->added + info->removed); - html("</td><td class='graph'>"); - htmlf("<table summary='file diffstat' width='%d%%'><tr>", (max_changes > 100 ? 100 : max_changes)); - htmlf("<td class='add' style='width: %.1f%%;'/>", - info->added * 100.0 / max_changes); - htmlf("<td class='rem' style='width: %.1f%%;'/>", - info->removed * 100.0 / max_changes); - htmlf("<td class='none' style='width: %.1f%%;'/>", - (max_changes - info->removed - info->added) * 100.0 / max_changes); - html("</tr></table></td></tr>\n"); -} - -static void count_diff_lines(char *line, int len) -{ - if (line && (len > 0)) { - if (line[0] == '+') - lines_added++; - else if (line[0] == '-') - lines_removed++; - } -} - -static int show_filepair(struct diff_filepair *pair) -{ - /* Always show if we have no limiting prefix. */ - if (!current_prefix) - return 1; - - /* Show if either path in the pair begins with the prefix. */ - if (starts_with(pair->one->path, current_prefix) || - starts_with(pair->two->path, current_prefix)) - return 1; - - /* Otherwise we don't want to show this filepair. */ - return 0; -} - -static void inspect_filepair(struct diff_filepair *pair) -{ - int binary = 0; - unsigned long old_size = 0; - unsigned long new_size = 0; - - if (!show_filepair(pair)) - return; - - files++; - lines_added = 0; - lines_removed = 0; - cgit_diff_files(&pair->one->oid, &pair->two->oid, &old_size, &new_size, - &binary, 0, ctx.qry.ignorews, count_diff_lines); - if (files >= slots) { - if (slots == 0) - slots = 4; - else - slots = slots * 2; - items = xrealloc(items, slots * sizeof(struct fileinfo)); - } - items[files-1].status = pair->status; - oidcpy(items[files-1].old_oid, &pair->one->oid); - oidcpy(items[files-1].new_oid, &pair->two->oid); - items[files-1].old_mode = pair->one->mode; - items[files-1].new_mode = pair->two->mode; - items[files-1].old_path = xstrdup(pair->one->path); - items[files-1].new_path = xstrdup(pair->two->path); - items[files-1].added = lines_added; - items[files-1].removed = lines_removed; - items[files-1].old_size = old_size; - items[files-1].new_size = new_size; - items[files-1].binary = binary; - if (lines_added + lines_removed > max_changes) - max_changes = lines_added + lines_removed; - total_adds += lines_added; - total_rems += lines_removed; -} - -static void cgit_print_diffstat(const struct object_id *old_oid, - const struct object_id *new_oid, - const char *prefix) -{ - int i; - - html("<div class='diffstat-header'>"); - cgit_diff_link("Diffstat", NULL, NULL, ctx.qry.head, ctx.qry.sha1, - ctx.qry.sha2, NULL); - if (prefix) { - html(" (limited to '"); - html_txt(prefix); - html("')"); - } - html("</div>"); - html("<table summary='diffstat' class='diffstat'>"); - max_changes = 0; - cgit_diff_tree(old_oid, new_oid, inspect_filepair, prefix, - ctx.qry.ignorews); - for (i = 0; i<files; i++) - print_fileinfo(&items[i]); - html("</table>"); - html("<div class='diffstat-summary'>"); - htmlf("%d files changed, %d insertions, %d deletions", - files, total_adds, total_rems); - html("</div>"); -} - - -/* - * print a single line returned from xdiff - */ -static void print_line(char *line, int len) -{ - char *class = "ctx"; - char c = line[len-1]; - - if (line[0] == '+') - class = "add"; - else if (line[0] == '-') - class = "del"; - else if (line[0] == '@') - class = "hunk"; - - htmlf("<div class='%s'>", class); - line[len-1] = '\0'; - html_txt(line); - html("</div>"); - line[len-1] = c; -} - -static void header(const struct object_id *oid1, char *path1, int mode1, - const struct object_id *oid2, char *path2, int mode2) -{ - char *abbrev1, *abbrev2; - int subproject; - - subproject = (S_ISGITLINK(mode1) || S_ISGITLINK(mode2)); - html("<div class='head'>"); - html("diff --git a/"); - html_txt(path1); - html(" b/"); - html_txt(path2); - - if (mode1 == 0) - htmlf("<br/>new file mode %.6o", mode2); - - if (mode2 == 0) - htmlf("<br/>deleted file mode %.6o", mode1); - - if (!subproject) { - abbrev1 = xstrdup(find_unique_abbrev(oid1, DEFAULT_ABBREV)); - abbrev2 = xstrdup(find_unique_abbrev(oid2, DEFAULT_ABBREV)); - htmlf("<br/>index %s..%s", abbrev1, abbrev2); - free(abbrev1); - free(abbrev2); - if (mode1 != 0 && mode2 != 0) { - htmlf(" %.6o", mode1); - if (mode2 != mode1) - htmlf("..%.6o", mode2); - } - if (is_null_oid(oid1)) { - path1 = "dev/null"; - html("<br/>--- /"); - } else - html("<br/>--- a/"); - if (mode1 != 0) - cgit_tree_link(path1, NULL, NULL, ctx.qry.head, - oid_to_hex(old_rev_oid), path1); - else - html_txt(path1); - if (is_null_oid(oid2)) { - path2 = "dev/null"; - html("<br/>+++ /"); - } else - html("<br/>+++ b/"); - if (mode2 != 0) - cgit_tree_link(path2, NULL, NULL, ctx.qry.head, - oid_to_hex(new_rev_oid), path2); - else - html_txt(path2); - } - html("</div>"); -} - -static void filepair_cb(struct diff_filepair *pair) -{ - unsigned long old_size = 0; - unsigned long new_size = 0; - int binary = 0; - linediff_fn print_line_fn = print_line; - - if (!show_filepair(pair)) - return; - - current_filepair = pair; - if (use_ssdiff) { - cgit_ssdiff_header_begin(); - print_line_fn = cgit_ssdiff_line_cb; - } - header(&pair->one->oid, pair->one->path, pair->one->mode, - &pair->two->oid, pair->two->path, pair->two->mode); - if (use_ssdiff) - cgit_ssdiff_header_end(); - if (S_ISGITLINK(pair->one->mode) || S_ISGITLINK(pair->two->mode)) { - if (S_ISGITLINK(pair->one->mode)) - print_line_fn(fmt("-Subproject %s", oid_to_hex(&pair->one->oid)), 52); - if (S_ISGITLINK(pair->two->mode)) - print_line_fn(fmt("+Subproject %s", oid_to_hex(&pair->two->oid)), 52); - if (use_ssdiff) - cgit_ssdiff_footer(); - return; - } - if (cgit_diff_files(&pair->one->oid, &pair->two->oid, &old_size, - &new_size, &binary, ctx.qry.context, - ctx.qry.ignorews, print_line_fn)) - cgit_print_error("Error running diff"); - if (binary) { - if (use_ssdiff) - html("<tr><td colspan='4'>Binary files differ</td></tr>"); - else - html("Binary files differ"); - } - if (use_ssdiff) - cgit_ssdiff_footer(); -} - -void cgit_print_diff_ctrls(void) -{ - int i, curr; - - html("<div class='cgit-panel'>"); - html("<b>diff options</b>"); - html("<form method='get'>"); - cgit_add_hidden_formfields(1, 0, ctx.qry.page); - html("<table>"); - html("<tr><td colspan='2'/></tr>"); - html("<tr>"); - html("<td class='label'>context:</td>"); - html("<td class='ctrl'>"); - html("<select name='context' onchange='this.form.submit();'>"); - curr = ctx.qry.context; - if (!curr) - curr = 3; - for (i = 1; i <= 10; i++) - html_intoption(i, fmt("%d", i), curr); - for (i = 15; i <= 40; i += 5) - html_intoption(i, fmt("%d", i), curr); - html("</select>"); - html("</td>"); - html("</tr><tr>"); - html("<td class='label'>space:</td>"); - html("<td class='ctrl'>"); - html("<select name='ignorews' onchange='this.form.submit();'>"); - html_intoption(0, "include", ctx.qry.ignorews); - html_intoption(1, "ignore", ctx.qry.ignorews); - html("</select>"); - html("</td>"); - html("</tr><tr>"); - html("<td class='label'>mode:</td>"); - html("<td class='ctrl'>"); - html("<select name='dt' onchange='this.form.submit();'>"); - curr = ctx.qry.has_difftype ? ctx.qry.difftype : ctx.cfg.difftype; - html_intoption(0, "unified", curr); - html_intoption(1, "ssdiff", curr); - html_intoption(2, "stat only", curr); - html("</select></td></tr>"); - html("<tr><td/><td class='ctrl'>"); - html("<noscript><input type='submit' value='reload'/></noscript>"); - html("</td></tr></table>"); - html("</form>"); - html("</div>"); -} - -void cgit_print_diff(const char *new_rev, const char *old_rev, - const char *prefix, int show_ctrls, int raw) -{ - struct commit *commit, *commit2; - const struct object_id *old_tree_oid, *new_tree_oid; - diff_type difftype; - - /* - * If "follow" is set then the diff machinery needs to examine the - * entire commit to detect renames so we must limit the paths in our - * own callbacks and not pass the prefix to the diff machinery. - */ - if (ctx.qry.follow && ctx.cfg.enable_follow_links) { - current_prefix = prefix; - prefix = ""; - } else { - current_prefix = NULL; - } - - if (!new_rev) - new_rev = ctx.qry.head; - if (get_oid(new_rev, new_rev_oid)) { - cgit_print_error_page(404, "Not found", - "Bad object name: %s", new_rev); - return; - } - commit = lookup_commit_reference(the_repository, new_rev_oid); - if (!commit || parse_commit(commit)) { - cgit_print_error_page(404, "Not found", - "Bad commit: %s", oid_to_hex(new_rev_oid)); - return; - } - new_tree_oid = &commit->maybe_tree->object.oid; - - if (old_rev) { - if (get_oid(old_rev, old_rev_oid)) { - cgit_print_error_page(404, "Not found", - "Bad object name: %s", old_rev); - return; - } - } else if (commit->parents && commit->parents->item) { - oidcpy(old_rev_oid, &commit->parents->item->object.oid); - } else { - oidclr(old_rev_oid); - } - - if (!is_null_oid(old_rev_oid)) { - commit2 = lookup_commit_reference(the_repository, old_rev_oid); - if (!commit2 || parse_commit(commit2)) { - cgit_print_error_page(404, "Not found", - "Bad commit: %s", oid_to_hex(old_rev_oid)); - return; - } - old_tree_oid = &commit2->maybe_tree->object.oid; - } else { - old_tree_oid = NULL; - } - - if (raw) { - struct diff_options diffopt; - - diff_setup(&diffopt); - diffopt.output_format = DIFF_FORMAT_PATCH; - diffopt.flags.recursive = 1; - diff_setup_done(&diffopt); - - ctx.page.mimetype = "text/plain"; - cgit_print_http_headers(); - if (old_tree_oid) { - diff_tree_oid(old_tree_oid, new_tree_oid, "", - &diffopt); - } else { - diff_root_tree_oid(new_tree_oid, "", &diffopt); - } - diffcore_std(&diffopt); - diff_flush(&diffopt); - - return; - } - - difftype = ctx.qry.has_difftype ? ctx.qry.difftype : ctx.cfg.difftype; - use_ssdiff = difftype == DIFF_SSDIFF; - - if (show_ctrls) { - cgit_print_layout_start(); - cgit_print_diff_ctrls(); - } - - /* - * Clicking on a link to a file in the diff stat should show a diff - * of the file, showing the diff stat limited to a single file is - * pretty useless. All links from this point on will be to - * individual files, so we simply reset the difftype in the query - * here to avoid propagating DIFF_STATONLY to the individual files. - */ - if (difftype == DIFF_STATONLY) - ctx.qry.difftype = ctx.cfg.difftype; - - cgit_print_diffstat(old_rev_oid, new_rev_oid, prefix); - - if (difftype == DIFF_STATONLY) - return; - - if (use_ssdiff) { - html("<table summary='ssdiff' class='ssdiff'>"); - } else { - html("<table summary='diff' class='diff'>"); - html("<tr><td>"); - } - cgit_diff_tree(old_rev_oid, new_rev_oid, filepair_cb, prefix, - ctx.qry.ignorews); - if (!use_ssdiff) - html("</td></tr>"); - html("</table>"); - - if (show_ctrls) - cgit_print_layout_end(); -} diff --git a/ui-diff.h b/ui-diff.h deleted file mode 100644 index 39264a164f..0000000000 --- a/ui-diff.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef UI_DIFF_H -#define UI_DIFF_H - -extern void cgit_print_diff_ctrls(void); - -extern void cgit_print_diff(const char *new_hex, const char *old_hex, - const char *prefix, int show_ctrls, int raw); - -extern struct diff_filespec *cgit_get_current_old_file(void); -extern struct diff_filespec *cgit_get_current_new_file(void); - -extern struct object_id old_rev_oid[1]; -extern struct object_id new_rev_oid[1]; - -#endif /* UI_DIFF_H */ diff --git a/ui-log.c b/ui-log.c deleted file mode 100644 index dc5cb1eb6a..0000000000 --- a/ui-log.c +++ /dev/null @@ -1,550 +0,0 @@ -/* ui-log.c: functions for log output - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-log.h" -#include "html.h" -#include "ui-shared.h" -#include "argv-array.h" - -static int files, add_lines, rem_lines, lines_counted; - -/* - * The list of available column colors in the commit graph. - */ -static const char *column_colors_html[] = { - "<span class='column1'>", - "<span class='column2'>", - "<span class='column3'>", - "<span class='column4'>", - "<span class='column5'>", - "<span class='column6'>", - "</span>", -}; - -#define COLUMN_COLORS_HTML_MAX (ARRAY_SIZE(column_colors_html) - 1) - -static void count_lines(char *line, int size) -{ - if (size <= 0) - return; - - if (line[0] == '+') - add_lines++; - - else if (line[0] == '-') - rem_lines++; -} - -static void inspect_files(struct diff_filepair *pair) -{ - unsigned long old_size = 0; - unsigned long new_size = 0; - int binary = 0; - - files++; - if (ctx.repo->enable_log_linecount) - cgit_diff_files(&pair->one->oid, &pair->two->oid, &old_size, - &new_size, &binary, 0, ctx.qry.ignorews, - count_lines); -} - -void show_commit_decorations(struct commit *commit) -{ - const struct name_decoration *deco; - static char buf[1024]; - - buf[sizeof(buf) - 1] = 0; - deco = get_name_decoration(&commit->object); - if (!deco) - return; - html("<span class='decoration'>"); - while (deco) { - struct object_id peeled; - int is_annotated = 0; - strlcpy(buf, prettify_refname(deco->name), sizeof(buf)); - switch(deco->type) { - case DECORATION_NONE: - /* If the git-core doesn't recognize it, - * don't display anything. */ - break; - case DECORATION_REF_LOCAL: - cgit_log_link(buf, NULL, "branch-deco", buf, NULL, - ctx.qry.vpath, 0, NULL, NULL, - ctx.qry.showmsg, 0); - break; - case DECORATION_REF_TAG: - if (!peel_ref(deco->name, &peeled)) - is_annotated = !oidcmp(&commit->object.oid, &peeled); - cgit_tag_link(buf, NULL, is_annotated ? "tag-annotated-deco" : "tag-deco", buf); - break; - case DECORATION_REF_REMOTE: - if (!ctx.repo->enable_remote_branches) - break; - cgit_log_link(buf, NULL, "remote-deco", NULL, - oid_to_hex(&commit->object.oid), - ctx.qry.vpath, 0, NULL, NULL, - ctx.qry.showmsg, 0); - break; - default: - cgit_commit_link(buf, NULL, "deco", ctx.qry.head, - oid_to_hex(&commit->object.oid), - ctx.qry.vpath); - break; - } - deco = deco->next; - } - html("</span>"); -} - -static void handle_rename(struct diff_filepair *pair) -{ - /* - * After we have seen a rename, we generate links to the previous - * name of the file so that commit & diff views get fed the path - * that is correct for the commit they are showing, avoiding the - * need to walk the entire history leading back to every commit we - * show in order detect renames. - */ - if (0 != strcmp(ctx.qry.vpath, pair->two->path)) { - free(ctx.qry.vpath); - ctx.qry.vpath = xstrdup(pair->two->path); - } - inspect_files(pair); -} - -static int show_commit(struct commit *commit, struct rev_info *revs) -{ - struct commit_list *parents = commit->parents; - struct commit *parent; - int found = 0, saved_fmt; - struct diff_flags saved_flags = revs->diffopt.flags; - - /* Always show if we're not in "follow" mode with a single file. */ - if (!ctx.qry.follow) - return 1; - - /* - * In "follow" mode, we don't show merges. This is consistent with - * "git log --follow -- <file>". - */ - if (parents && parents->next) - return 0; - - /* - * If this is the root commit, do what rev_info tells us. - */ - if (!parents) - return revs->show_root_diff; - - /* When we get here we have precisely one parent. */ - parent = parents->item; - /* If we can't parse the commit, let print_commit() report an error. */ - if (parse_commit(parent)) - return 1; - - files = 0; - add_lines = 0; - rem_lines = 0; - - revs->diffopt.flags.recursive = 1; - diff_tree_oid(&parent->maybe_tree->object.oid, - &commit->maybe_tree->object.oid, - "", &revs->diffopt); - diffcore_std(&revs->diffopt); - - found = !diff_queue_is_empty(); - saved_fmt = revs->diffopt.output_format; - revs->diffopt.output_format = DIFF_FORMAT_CALLBACK; - revs->diffopt.format_callback = cgit_diff_tree_cb; - revs->diffopt.format_callback_data = handle_rename; - diff_flush(&revs->diffopt); - revs->diffopt.output_format = saved_fmt; - revs->diffopt.flags = saved_flags; - - lines_counted = 1; - return found; -} - -static void print_commit(struct commit *commit, struct rev_info *revs) -{ - struct commitinfo *info; - int columns = revs->graph ? 4 : 3; - struct strbuf graphbuf = STRBUF_INIT; - struct strbuf msgbuf = STRBUF_INIT; - - if (ctx.repo->enable_log_filecount) - columns++; - if (ctx.repo->enable_log_linecount) - columns++; - - if (revs->graph) { - /* Advance graph until current commit */ - while (!graph_next_line(revs->graph, &graphbuf)) { - /* Print graph segment in otherwise empty table row */ - html("<tr class='nohover'><td class='commitgraph'>"); - html(graphbuf.buf); - htmlf("</td><td colspan='%d' /></tr>\n", columns); - strbuf_setlen(&graphbuf, 0); - } - /* Current commit's graph segment is now ready in graphbuf */ - } - - info = cgit_parse_commit(commit); - htmlf("<tr%s>", ctx.qry.showmsg ? " class='logheader'" : ""); - - if (revs->graph) { - /* Print graph segment for current commit */ - html("<td class='commitgraph'>"); - html(graphbuf.buf); - html("</td>"); - strbuf_setlen(&graphbuf, 0); - } - else { - html("<td>"); - cgit_print_age(info->committer_date, info->committer_tz, TM_WEEK * 2); - html("</td>"); - } - - htmlf("<td%s>", ctx.qry.showmsg ? " class='logsubject'" : ""); - if (ctx.qry.showmsg) { - /* line-wrap long commit subjects instead of truncating them */ - size_t subject_len = strlen(info->subject); - - if (subject_len > ctx.cfg.max_msg_len && - ctx.cfg.max_msg_len >= 15) { - /* symbol for signaling line-wrap (in PAGE_ENCODING) */ - const char wrap_symbol[] = { ' ', 0xE2, 0x86, 0xB5, 0 }; - int i = ctx.cfg.max_msg_len - strlen(wrap_symbol); - - /* Rewind i to preceding space character */ - while (i > 0 && !isspace(info->subject[i])) - --i; - if (!i) /* Oops, zero spaces. Reset i */ - i = ctx.cfg.max_msg_len - strlen(wrap_symbol); - - /* add remainder starting at i to msgbuf */ - strbuf_add(&msgbuf, info->subject + i, subject_len - i); - strbuf_trim(&msgbuf); - strbuf_add(&msgbuf, "\n\n", 2); - - /* Place wrap_symbol at position i in info->subject */ - strlcpy(info->subject + i, wrap_symbol, subject_len - i + 1); - } - } - cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head, - oid_to_hex(&commit->object.oid), ctx.qry.vpath); - show_commit_decorations(commit); - html("</td><td>"); - cgit_open_filter(ctx.repo->email_filter, info->author_email, "log"); - html_txt(info->author); - cgit_close_filter(ctx.repo->email_filter); - - if (revs->graph) { - html("</td><td>"); - cgit_print_age(info->committer_date, info->committer_tz, TM_WEEK * 2); - } - - if (!lines_counted && (ctx.repo->enable_log_filecount || - ctx.repo->enable_log_linecount)) { - files = 0; - add_lines = 0; - rem_lines = 0; - cgit_diff_commit(commit, inspect_files, ctx.qry.vpath); - } - - if (ctx.repo->enable_log_filecount) - htmlf("</td><td>%d", files); - if (ctx.repo->enable_log_linecount) - htmlf("</td><td><span class='deletions'>-%d</span>/" - "<span class='insertions'>+%d</span>", rem_lines, add_lines); - - html("</td></tr>\n"); - - if ((revs->graph && !graph_is_commit_finished(revs->graph)) - || ctx.qry.showmsg) { /* Print a second table row */ - html("<tr class='nohover-highlight'>"); - - if (ctx.qry.showmsg) { - /* Concatenate commit message + notes in msgbuf */ - if (info->msg && *(info->msg)) { - strbuf_addstr(&msgbuf, info->msg); - strbuf_addch(&msgbuf, '\n'); - } - format_display_notes(&commit->object.oid, - &msgbuf, PAGE_ENCODING, 0); - strbuf_addch(&msgbuf, '\n'); - strbuf_ltrim(&msgbuf); - } - - if (revs->graph) { - int lines = 0; - - /* Calculate graph padding */ - if (ctx.qry.showmsg) { - /* Count #lines in commit message + notes */ - const char *p = msgbuf.buf; - lines = 1; - while ((p = strchr(p, '\n'))) { - p++; - lines++; - } - } - - /* Print graph padding */ - html("<td class='commitgraph'>"); - while (lines > 0 || !graph_is_commit_finished(revs->graph)) { - if (graphbuf.len) - html("\n"); - strbuf_setlen(&graphbuf, 0); - graph_next_line(revs->graph, &graphbuf); - html(graphbuf.buf); - lines--; - } - html("</td>\n"); - } - else - html("<td/>"); /* Empty 'Age' column */ - - /* Print msgbuf into remainder of table row */ - htmlf("<td colspan='%d'%s>\n", columns - (revs->graph ? 1 : 0), - ctx.qry.showmsg ? " class='logmsg'" : ""); - html_txt(msgbuf.buf); - html("</td></tr>\n"); - } - - strbuf_release(&msgbuf); - strbuf_release(&graphbuf); - cgit_free_commitinfo(info); -} - -static const char *disambiguate_ref(const char *ref, int *must_free_result) -{ - struct object_id oid; - struct strbuf longref = STRBUF_INIT; - - strbuf_addf(&longref, "refs/heads/%s", ref); - if (get_oid(longref.buf, &oid) == 0) { - *must_free_result = 1; - return strbuf_detach(&longref, NULL); - } - - *must_free_result = 0; - strbuf_release(&longref); - return ref; -} - -static char *next_token(char **src) -{ - char *result; - - if (!src || !*src) - return NULL; - while (isspace(**src)) - (*src)++; - if (!**src) - return NULL; - result = *src; - while (**src) { - if (isspace(**src)) { - **src = '\0'; - (*src)++; - break; - } - (*src)++; - } - return result; -} - -void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern, - const char *path, int pager, int commit_graph, int commit_sort) -{ - struct rev_info rev; - struct commit *commit; - struct argv_array rev_argv = ARGV_ARRAY_INIT; - int i, columns = commit_graph ? 4 : 3; - int must_free_tip = 0; - - /* rev_argv.argv[0] will be ignored by setup_revisions */ - argv_array_push(&rev_argv, "log_rev_setup"); - - if (!tip) - tip = ctx.qry.head; - tip = disambiguate_ref(tip, &must_free_tip); - argv_array_push(&rev_argv, tip); - - if (grep && pattern && *pattern) { - pattern = xstrdup(pattern); - if (!strcmp(grep, "grep") || !strcmp(grep, "author") || - !strcmp(grep, "committer")) { - argv_array_pushf(&rev_argv, "--%s=%s", grep, pattern); - } else if (!strcmp(grep, "range")) { - char *arg; - /* Split the pattern at whitespace and add each token - * as a revision expression. Do not accept other - * rev-list options. Also, replace the previously - * pushed tip (it's no longer relevant). - */ - argv_array_pop(&rev_argv); - while ((arg = next_token(&pattern))) { - if (*arg == '-') { - fprintf(stderr, "Bad range expr: %s\n", - arg); - break; - } - argv_array_push(&rev_argv, arg); - } - } - } - - if (!path || !ctx.cfg.enable_follow_links) { - /* - * If we don't have a path, "follow" is a no-op so make sure - * the variable is set to false to avoid needing to check - * both this and whether we have a path everywhere. - */ - ctx.qry.follow = 0; - } - - if (commit_graph && !ctx.qry.follow) { - argv_array_push(&rev_argv, "--graph"); - argv_array_push(&rev_argv, "--color"); - graph_set_column_colors(column_colors_html, - COLUMN_COLORS_HTML_MAX); - } - - if (commit_sort == 1) - argv_array_push(&rev_argv, "--date-order"); - else if (commit_sort == 2) - argv_array_push(&rev_argv, "--topo-order"); - - if (path && ctx.qry.follow) - argv_array_push(&rev_argv, "--follow"); - argv_array_push(&rev_argv, "--"); - if (path) - argv_array_push(&rev_argv, path); - - init_revisions(&rev, NULL); - rev.abbrev = DEFAULT_ABBREV; - rev.commit_format = CMIT_FMT_DEFAULT; - rev.verbose_header = 1; - rev.show_root_diff = 0; - rev.ignore_missing = 1; - rev.simplify_history = 1; - setup_revisions(rev_argv.argc, rev_argv.argv, &rev, NULL); - load_ref_decorations(NULL, DECORATE_FULL_REFS); - rev.show_decorations = 1; - rev.grep_filter.ignore_case = 1; - - rev.diffopt.detect_rename = 1; - rev.diffopt.rename_limit = ctx.cfg.renamelimit; - if (ctx.qry.ignorews) - DIFF_XDL_SET(&rev.diffopt, IGNORE_WHITESPACE); - - compile_grep_patterns(&rev.grep_filter); - prepare_revision_walk(&rev); - - if (pager) { - cgit_print_layout_start(); - html("<table class='list nowrap'>"); - } - - html("<tr class='nohover'>"); - if (commit_graph) - html("<th></th>"); - else - html("<th class='left'>Age</th>"); - html("<th class='left'>Commit message"); - if (pager) { - html(" ("); - cgit_log_link(ctx.qry.showmsg ? "Collapse" : "Expand", NULL, - NULL, ctx.qry.head, ctx.qry.sha1, - ctx.qry.vpath, ctx.qry.ofs, ctx.qry.grep, - ctx.qry.search, ctx.qry.showmsg ? 0 : 1, - ctx.qry.follow); - html(")"); - } - html("</th><th class='left'>Author</th>"); - if (rev.graph) - html("<th class='left'>Age</th>"); - if (ctx.repo->enable_log_filecount) { - html("<th class='left'>Files</th>"); - columns++; - } - if (ctx.repo->enable_log_linecount) { - html("<th class='left'>Lines</th>"); - columns++; - } - html("</tr>\n"); - - if (ofs<0) - ofs = 0; - - for (i = 0; i < ofs && (commit = get_revision(&rev)) != NULL; /* nop */) { - if (show_commit(commit, &rev)) - i++; - free_commit_buffer(the_repository->parsed_objects, commit); - free_commit_list(commit->parents); - commit->parents = NULL; - } - - for (i = 0; i < cnt && (commit = get_revision(&rev)) != NULL; /* nop */) { - /* - * In "follow" mode, we must count the files and lines the - * first time we invoke diff on a given commit, and we need - * to do that to see if the commit touches the path we care - * about, so we do it in show_commit. Hence we must clear - * lines_counted here. - * - * This has the side effect of avoiding running diff twice - * when we are both following renames and showing file - * and/or line counts. - */ - lines_counted = 0; - if (show_commit(commit, &rev)) { - i++; - print_commit(commit, &rev); - } - free_commit_buffer(the_repository->parsed_objects, commit); - free_commit_list(commit->parents); - commit->parents = NULL; - } - if (pager) { - html("</table><ul class='pager'>"); - if (ofs > 0) { - html("<li>"); - cgit_log_link("[prev]", NULL, NULL, ctx.qry.head, - ctx.qry.sha1, ctx.qry.vpath, - ofs - cnt, ctx.qry.grep, - ctx.qry.search, ctx.qry.showmsg, - ctx.qry.follow); - html("</li>"); - } - if ((commit = get_revision(&rev)) != NULL) { - html("<li>"); - cgit_log_link("[next]", NULL, NULL, ctx.qry.head, - ctx.qry.sha1, ctx.qry.vpath, - ofs + cnt, ctx.qry.grep, - ctx.qry.search, ctx.qry.showmsg, - ctx.qry.follow); - html("</li>"); - } - html("</ul>"); - cgit_print_layout_end(); - } else if ((commit = get_revision(&rev)) != NULL) { - htmlf("<tr class='nohover'><td colspan='%d'>", columns); - cgit_log_link("[...]", NULL, NULL, ctx.qry.head, NULL, - ctx.qry.vpath, 0, NULL, NULL, ctx.qry.showmsg, - ctx.qry.follow); - html("</td></tr>\n"); - } - - /* If we allocated tip then it is safe to cast away const. */ - if (must_free_tip) - free((char*) tip); -} diff --git a/ui-log.h b/ui-log.h deleted file mode 100644 index 325607cdab..0000000000 --- a/ui-log.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef UI_LOG_H -#define UI_LOG_H - -extern void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, - char *pattern, const char *path, int pager, - int commit_graph, int commit_sort); -extern void show_commit_decorations(struct commit *commit); - -#endif /* UI_LOG_H */ diff --git a/ui-patch.c b/ui-patch.c deleted file mode 100644 index 5a964108e5..0000000000 --- a/ui-patch.c +++ /dev/null @@ -1,98 +0,0 @@ -/* ui-patch.c: generate patch view - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-patch.h" -#include "html.h" -#include "ui-shared.h" - -/* two commit hashes with two dots in between and termination */ -#define REV_RANGE_LEN 2 * GIT_MAX_HEXSZ + 3 - -void cgit_print_patch(const char *new_rev, const char *old_rev, - const char *prefix) -{ - struct rev_info rev; - struct commit *commit; - struct object_id new_rev_oid, old_rev_oid; - char rev_range[REV_RANGE_LEN]; - const char *rev_argv[] = { NULL, "--reverse", "--format=email", rev_range, "--", prefix, NULL }; - int rev_argc = ARRAY_SIZE(rev_argv) - 1; - char *patchname; - - if (!prefix) - rev_argc--; - - if (!new_rev) - new_rev = ctx.qry.head; - - if (get_oid(new_rev, &new_rev_oid)) { - cgit_print_error_page(404, "Not found", - "Bad object id: %s", new_rev); - return; - } - commit = lookup_commit_reference(the_repository, &new_rev_oid); - if (!commit) { - cgit_print_error_page(404, "Not found", - "Bad commit reference: %s", new_rev); - return; - } - - if (old_rev) { - if (get_oid(old_rev, &old_rev_oid)) { - cgit_print_error_page(404, "Not found", - "Bad object id: %s", old_rev); - return; - } - if (!lookup_commit_reference(the_repository, &old_rev_oid)) { - cgit_print_error_page(404, "Not found", - "Bad commit reference: %s", old_rev); - return; - } - } else if (commit->parents && commit->parents->item) { - oidcpy(&old_rev_oid, &commit->parents->item->object.oid); - } else { - oidclr(&old_rev_oid); - } - - if (is_null_oid(&old_rev_oid)) { - memcpy(rev_range, oid_to_hex(&new_rev_oid), GIT_SHA1_HEXSZ + 1); - } else { - xsnprintf(rev_range, REV_RANGE_LEN, "%s..%s", oid_to_hex(&old_rev_oid), - oid_to_hex(&new_rev_oid)); - } - - patchname = fmt("%s.patch", rev_range); - ctx.page.mimetype = "text/plain"; - ctx.page.filename = patchname; - cgit_print_http_headers(); - - if (ctx.cfg.noplainemail) { - rev_argv[2] = "--format=format:From %H Mon Sep 17 00:00:00 " - "2001%nFrom: %an%nDate: %aD%n%w(78,0,1)Subject: " - "%s%n%n%w(0)%b"; - } - - init_revisions(&rev, NULL); - rev.abbrev = DEFAULT_ABBREV; - rev.verbose_header = 1; - rev.diff = 1; - rev.show_root_diff = 1; - rev.max_parents = 1; - rev.diffopt.output_format |= DIFF_FORMAT_DIFFSTAT | - DIFF_FORMAT_PATCH | DIFF_FORMAT_SUMMARY; - if (prefix) - rev.diffopt.stat_sep = fmt("(limited to '%s')\n\n", prefix); - setup_revisions(rev_argc, rev_argv, &rev, NULL); - prepare_revision_walk(&rev); - - while ((commit = get_revision(&rev)) != NULL) { - log_tree_commit(&rev, commit); - printf("-- \ncgit %s\n\n", cgit_version); - } -} diff --git a/ui-patch.h b/ui-patch.h deleted file mode 100644 index 7a6cacd5ac..0000000000 --- a/ui-patch.h +++ /dev/null @@ -1,7 +0,0 @@ -#ifndef UI_PATCH_H -#define UI_PATCH_H - -extern void cgit_print_patch(const char *new_rev, const char *old_rev, - const char *prefix); - -#endif /* UI_PATCH_H */ diff --git a/ui-plain.c b/ui-plain.c deleted file mode 100644 index b73c1cfed1..0000000000 --- a/ui-plain.c +++ /dev/null @@ -1,207 +0,0 @@ -/* ui-plain.c: functions for output of plain blobs by path - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-plain.h" -#include "html.h" -#include "ui-shared.h" - -struct walk_tree_context { - int match_baselen; - int match; -}; - -static int print_object(const struct object_id *oid, const char *path) -{ - enum object_type type; - char *buf, *mimetype; - unsigned long size; - - type = oid_object_info(the_repository, oid, &size); - if (type == OBJ_BAD) { - cgit_print_error_page(404, "Not found", "Not found"); - return 0; - } - - buf = read_object_file(oid, &type, &size); - if (!buf) { - cgit_print_error_page(404, "Not found", "Not found"); - return 0; - } - - mimetype = get_mimetype_for_filename(path); - ctx.page.mimetype = mimetype; - - if (!ctx.repo->enable_html_serving) { - html("X-Content-Type-Options: nosniff\n"); - html("Content-Security-Policy: default-src 'none'\n"); - if (mimetype) { - /* Built-in white list allows PDF and everything that isn't text/ and application/ */ - if ((!strncmp(mimetype, "text/", 5) || !strncmp(mimetype, "application/", 12)) && strcmp(mimetype, "application/pdf")) - ctx.page.mimetype = NULL; - } - } - - if (!ctx.page.mimetype) { - if (buffer_is_binary(buf, size)) { - ctx.page.mimetype = "application/octet-stream"; - ctx.page.charset = NULL; - } else { - ctx.page.mimetype = "text/plain"; - } - } - ctx.page.filename = path; - ctx.page.size = size; - ctx.page.etag = oid_to_hex(oid); - cgit_print_http_headers(); - html_raw(buf, size); - free(mimetype); - free(buf); - return 1; -} - -static char *buildpath(const char *base, int baselen, const char *path) -{ - if (path[0]) - return fmtalloc("%.*s%s/", baselen, base, path); - else - return fmtalloc("%.*s/", baselen, base); -} - -static void print_dir(const struct object_id *oid, const char *base, - int baselen, const char *path) -{ - char *fullpath, *slash; - size_t len; - - fullpath = buildpath(base, baselen, path); - slash = (fullpath[0] == '/' ? "" : "/"); - ctx.page.etag = oid_to_hex(oid); - cgit_print_http_headers(); - htmlf("<html><head><title>%s", slash); - html_txt(fullpath); - htmlf("</title></head>\n<body>\n<h2>%s", slash); - html_txt(fullpath); - html("</h2>\n<ul>\n"); - len = strlen(fullpath); - if (len > 1) { - fullpath[len - 1] = 0; - slash = strrchr(fullpath, '/'); - if (slash) - *(slash + 1) = 0; - else { - free(fullpath); - fullpath = NULL; - } - html("<li>"); - cgit_plain_link("../", NULL, NULL, ctx.qry.head, ctx.qry.sha1, - fullpath); - html("</li>\n"); - } - free(fullpath); -} - -static void print_dir_entry(const struct object_id *oid, const char *base, - int baselen, const char *path, unsigned mode) -{ - char *fullpath; - - fullpath = buildpath(base, baselen, path); - if (!S_ISDIR(mode) && !S_ISGITLINK(mode)) - fullpath[strlen(fullpath) - 1] = 0; - html(" <li>"); - if (S_ISGITLINK(mode)) { - cgit_submodule_link(NULL, fullpath, oid_to_hex(oid)); - } else - cgit_plain_link(path, NULL, NULL, ctx.qry.head, ctx.qry.sha1, - fullpath); - html("</li>\n"); - free(fullpath); -} - -static void print_dir_tail(void) -{ - html(" </ul>\n</body></html>\n"); -} - -static int walk_tree(const struct object_id *oid, struct strbuf *base, - const char *pathname, unsigned mode, int stage, void *cbdata) -{ - struct walk_tree_context *walk_tree_ctx = cbdata; - - if (base->len == walk_tree_ctx->match_baselen) { - if (S_ISREG(mode) || S_ISLNK(mode)) { - if (print_object(oid, pathname)) - walk_tree_ctx->match = 1; - } else if (S_ISDIR(mode)) { - print_dir(oid, base->buf, base->len, pathname); - walk_tree_ctx->match = 2; - return READ_TREE_RECURSIVE; - } - } else if (base->len < INT_MAX && (int)base->len > walk_tree_ctx->match_baselen) { - print_dir_entry(oid, base->buf, base->len, pathname, mode); - walk_tree_ctx->match = 2; - } else if (S_ISDIR(mode)) { - return READ_TREE_RECURSIVE; - } - - return 0; -} - -static int basedir_len(const char *path) -{ - char *p = strrchr(path, '/'); - if (p) - return p - path + 1; - return 0; -} - -void cgit_print_plain(void) -{ - const char *rev = ctx.qry.sha1; - struct object_id oid; - struct commit *commit; - struct pathspec_item path_items = { - .match = ctx.qry.path, - .len = ctx.qry.path ? strlen(ctx.qry.path) : 0 - }; - struct pathspec paths = { - .nr = 1, - .items = &path_items - }; - struct walk_tree_context walk_tree_ctx = { - .match = 0 - }; - - if (!rev) - rev = ctx.qry.head; - - if (get_oid(rev, &oid)) { - cgit_print_error_page(404, "Not found", "Not found"); - return; - } - commit = lookup_commit_reference(the_repository, &oid); - if (!commit || parse_commit(commit)) { - cgit_print_error_page(404, "Not found", "Not found"); - return; - } - if (!path_items.match) { - path_items.match = ""; - walk_tree_ctx.match_baselen = -1; - print_dir(&commit->maybe_tree->object.oid, "", 0, ""); - walk_tree_ctx.match = 2; - } - else - walk_tree_ctx.match_baselen = basedir_len(path_items.match); - read_tree_recursive(the_repository, commit->maybe_tree, - "", 0, 0, &paths, walk_tree, &walk_tree_ctx); - if (!walk_tree_ctx.match) - cgit_print_error_page(404, "Not found", "Not found"); - else if (walk_tree_ctx.match == 2) - print_dir_tail(); -} diff --git a/ui-plain.h b/ui-plain.h deleted file mode 100644 index 5bff07b83b..0000000000 --- a/ui-plain.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef UI_PLAIN_H -#define UI_PLAIN_H - -extern void cgit_print_plain(void); - -#endif /* UI_PLAIN_H */ diff --git a/ui-refs.c b/ui-refs.c deleted file mode 100644 index 456f610df4..0000000000 --- a/ui-refs.c +++ /dev/null @@ -1,219 +0,0 @@ -/* ui-refs.c: browse symbolic refs - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-refs.h" -#include "html.h" -#include "ui-shared.h" - -static inline int cmp_age(int age1, int age2) -{ - /* age1 and age2 are assumed to be non-negative */ - return age2 - age1; -} - -static int cmp_ref_name(const void *a, const void *b) -{ - struct refinfo *r1 = *(struct refinfo **)a; - struct refinfo *r2 = *(struct refinfo **)b; - - return strcmp(r1->refname, r2->refname); -} - -static int cmp_branch_age(const void *a, const void *b) -{ - struct refinfo *r1 = *(struct refinfo **)a; - struct refinfo *r2 = *(struct refinfo **)b; - - return cmp_age(r1->commit->committer_date, r2->commit->committer_date); -} - -static int get_ref_age(struct refinfo *ref) -{ - if (!ref->object) - return 0; - switch (ref->object->type) { - case OBJ_TAG: - return ref->tag ? ref->tag->tagger_date : 0; - case OBJ_COMMIT: - return ref->commit ? ref->commit->committer_date : 0; - } - return 0; -} - -static int cmp_tag_age(const void *a, const void *b) -{ - struct refinfo *r1 = *(struct refinfo **)a; - struct refinfo *r2 = *(struct refinfo **)b; - - return cmp_age(get_ref_age(r1), get_ref_age(r2)); -} - -static int print_branch(struct refinfo *ref) -{ - struct commitinfo *info = ref->commit; - char *name = (char *)ref->refname; - - if (!info) - return 1; - html("<tr><td>"); - cgit_log_link(name, NULL, NULL, name, NULL, NULL, 0, NULL, NULL, - ctx.qry.showmsg, 0); - html("</td><td>"); - - if (ref->object->type == OBJ_COMMIT) { - cgit_commit_link(info->subject, NULL, NULL, name, NULL, NULL); - html("</td><td>"); - cgit_open_filter(ctx.repo->email_filter, info->author_email, "refs"); - html_txt(info->author); - cgit_close_filter(ctx.repo->email_filter); - html("</td><td colspan='2'>"); - cgit_print_age(info->committer_date, info->committer_tz, -1); - } else { - html("</td><td></td><td>"); - cgit_object_link(ref->object); - } - html("</td></tr>\n"); - return 0; -} - -static void print_tag_header(void) -{ - html("<tr class='nohover'><th class='left'>Tag</th>" - "<th class='left'>Download</th>" - "<th class='left'>Author</th>" - "<th class='left' colspan='2'>Age</th></tr>\n"); -} - -static int print_tag(struct refinfo *ref) -{ - struct tag *tag = NULL; - struct taginfo *info = NULL; - char *name = (char *)ref->refname; - struct object *obj = ref->object; - - if (obj->type == OBJ_TAG) { - tag = (struct tag *)obj; - obj = tag->tagged; - info = ref->tag; - if (!info) - return 1; - } - - html("<tr><td>"); - cgit_tag_link(name, NULL, NULL, name); - html("</td><td>"); - if (ctx.repo->snapshots && (obj->type == OBJ_COMMIT)) - cgit_print_snapshot_links(ctx.repo, name, " "); - else - cgit_object_link(obj); - html("</td><td>"); - if (info) { - if (info->tagger) { - cgit_open_filter(ctx.repo->email_filter, info->tagger_email, "refs"); - html_txt(info->tagger); - cgit_close_filter(ctx.repo->email_filter); - } - } else if (ref->object->type == OBJ_COMMIT) { - cgit_open_filter(ctx.repo->email_filter, ref->commit->author_email, "refs"); - html_txt(ref->commit->author); - cgit_close_filter(ctx.repo->email_filter); - } - html("</td><td colspan='2'>"); - if (info) { - if (info->tagger_date > 0) - cgit_print_age(info->tagger_date, info->tagger_tz, -1); - } else if (ref->object->type == OBJ_COMMIT) { - cgit_print_age(ref->commit->commit->date, 0, -1); - } - html("</td></tr>\n"); - - return 0; -} - -static void print_refs_link(const char *path) -{ - html("<tr class='nohover'><td colspan='5'>"); - cgit_refs_link("[...]", NULL, NULL, ctx.qry.head, NULL, path); - html("</td></tr>"); -} - -void cgit_print_branches(int maxcount) -{ - struct reflist list; - int i; - - html("<tr class='nohover'><th class='left'>Branch</th>" - "<th class='left'>Commit message</th>" - "<th class='left'>Author</th>" - "<th class='left' colspan='2'>Age</th></tr>\n"); - - list.refs = NULL; - list.alloc = list.count = 0; - for_each_branch_ref(cgit_refs_cb, &list); - if (ctx.repo->enable_remote_branches) - for_each_remote_ref(cgit_refs_cb, &list); - - if (maxcount == 0 || maxcount > list.count) - maxcount = list.count; - - qsort(list.refs, list.count, sizeof(*list.refs), cmp_branch_age); - if (ctx.repo->branch_sort == 0) - qsort(list.refs, maxcount, sizeof(*list.refs), cmp_ref_name); - - for (i = 0; i < maxcount; i++) - print_branch(list.refs[i]); - - if (maxcount < list.count) - print_refs_link("heads"); - - cgit_free_reflist_inner(&list); -} - -void cgit_print_tags(int maxcount) -{ - struct reflist list; - int i; - - list.refs = NULL; - list.alloc = list.count = 0; - for_each_tag_ref(cgit_refs_cb, &list); - if (list.count == 0) - return; - qsort(list.refs, list.count, sizeof(*list.refs), cmp_tag_age); - if (!maxcount) - maxcount = list.count; - else if (maxcount > list.count) - maxcount = list.count; - print_tag_header(); - for (i = 0; i < maxcount; i++) - print_tag(list.refs[i]); - - if (maxcount < list.count) - print_refs_link("tags"); - - cgit_free_reflist_inner(&list); -} - -void cgit_print_refs(void) -{ - cgit_print_layout_start(); - html("<table class='list nowrap'>"); - - if (ctx.qry.path && starts_with(ctx.qry.path, "heads")) - cgit_print_branches(0); - else if (ctx.qry.path && starts_with(ctx.qry.path, "tags")) - cgit_print_tags(0); - else { - cgit_print_branches(0); - html("<tr class='nohover'><td colspan='5'> </td></tr>"); - cgit_print_tags(0); - } - html("</table>"); - cgit_print_layout_end(); -} diff --git a/ui-refs.h b/ui-refs.h deleted file mode 100644 index 1d4a54a2c8..0000000000 --- a/ui-refs.h +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef UI_REFS_H -#define UI_REFS_H - -extern void cgit_print_branches(int maxcount); -extern void cgit_print_tags(int maxcount); -extern void cgit_print_refs(void); - -#endif /* UI_REFS_H */ diff --git a/ui-repolist.c b/ui-repolist.c deleted file mode 100644 index 7cf763891f..0000000000 --- a/ui-repolist.c +++ /dev/null @@ -1,379 +0,0 @@ -/* ui-repolist.c: functions for generating the repolist page - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-repolist.h" -#include "html.h" -#include "ui-shared.h" - -static time_t read_agefile(const char *path) -{ - time_t result; - size_t size; - char *buf = NULL; - struct strbuf date_buf = STRBUF_INIT; - - if (readfile(path, &buf, &size)) { - free(buf); - return -1; - } - - if (parse_date(buf, &date_buf) == 0) - result = strtoul(date_buf.buf, NULL, 10); - else - result = 0; - free(buf); - strbuf_release(&date_buf); - return result; -} - -static int get_repo_modtime(const struct cgit_repo *repo, time_t *mtime) -{ - struct strbuf path = STRBUF_INIT; - struct stat s; - struct cgit_repo *r = (struct cgit_repo *)repo; - - if (repo->mtime != -1) { - *mtime = repo->mtime; - return 1; - } - strbuf_addf(&path, "%s/%s", repo->path, ctx.cfg.agefile); - if (stat(path.buf, &s) == 0) { - *mtime = read_agefile(path.buf); - if (*mtime) { - r->mtime = *mtime; - goto end; - } - } - - strbuf_reset(&path); - strbuf_addf(&path, "%s/refs/heads/%s", repo->path, - repo->defbranch ? repo->defbranch : "master"); - if (stat(path.buf, &s) == 0) { - *mtime = s.st_mtime; - r->mtime = *mtime; - goto end; - } - - strbuf_reset(&path); - strbuf_addf(&path, "%s/%s", repo->path, "packed-refs"); - if (stat(path.buf, &s) == 0) { - *mtime = s.st_mtime; - r->mtime = *mtime; - goto end; - } - - *mtime = 0; - r->mtime = *mtime; -end: - strbuf_release(&path); - return (r->mtime != 0); -} - -static void print_modtime(struct cgit_repo *repo) -{ - time_t t; - if (get_repo_modtime(repo, &t)) - cgit_print_age(t, 0, -1); -} - -static int is_match(struct cgit_repo *repo) -{ - if (!ctx.qry.search) - return 1; - if (repo->url && strcasestr(repo->url, ctx.qry.search)) - return 1; - if (repo->name && strcasestr(repo->name, ctx.qry.search)) - return 1; - if (repo->desc && strcasestr(repo->desc, ctx.qry.search)) - return 1; - if (repo->owner && strcasestr(repo->owner, ctx.qry.search)) - return 1; - return 0; -} - -static int is_in_url(struct cgit_repo *repo) -{ - if (!ctx.qry.url) - return 1; - if (repo->url && starts_with(repo->url, ctx.qry.url)) - return 1; - return 0; -} - -static int is_visible(struct cgit_repo *repo) -{ - if (repo->hide || repo->ignore) - return 0; - if (!(is_match(repo) && is_in_url(repo))) - return 0; - return 1; -} - -static int any_repos_visible(void) -{ - int i; - - for (i = 0; i < cgit_repolist.count; i++) { - if (is_visible(&cgit_repolist.repos[i])) - return 1; - } - return 0; -} - -static void print_sort_header(const char *title, const char *sort) -{ - char *currenturl = cgit_currenturl(); - html("<th class='left'><a href='"); - html_attr(currenturl); - htmlf("?s=%s", sort); - if (ctx.qry.search) { - html("&q="); - html_url_arg(ctx.qry.search); - } - htmlf("'>%s</a></th>", title); - free(currenturl); -} - -static void print_header(void) -{ - html("<tr class='nohover'>"); - print_sort_header("Name", "name"); - print_sort_header("Description", "desc"); - if (ctx.cfg.enable_index_owner) - print_sort_header("Owner", "owner"); - print_sort_header("Idle", "idle"); - if (ctx.cfg.enable_index_links) - html("<th class='left'>Links</th>"); - html("</tr>\n"); -} - - -static void print_pager(int items, int pagelen, char *search, char *sort) -{ - int i, ofs; - char *class = NULL; - html("<ul class='pager'>"); - for (i = 0, ofs = 0; ofs < items; i++, ofs = i * pagelen) { - class = (ctx.qry.ofs == ofs) ? "current" : NULL; - html("<li>"); - cgit_index_link(fmt("[%d]", i + 1), fmt("Page %d", i + 1), - class, search, sort, ofs, 0); - html("</li>"); - } - html("</ul>"); -} - -static int cmp(const char *s1, const char *s2) -{ - if (s1 && s2) { - if (ctx.cfg.case_sensitive_sort) - return strcmp(s1, s2); - else - return strcasecmp(s1, s2); - } - if (s1 && !s2) - return -1; - if (s2 && !s1) - return 1; - return 0; -} - -static int sort_name(const void *a, const void *b) -{ - const struct cgit_repo *r1 = a; - const struct cgit_repo *r2 = b; - - return cmp(r1->name, r2->name); -} - -static int sort_desc(const void *a, const void *b) -{ - const struct cgit_repo *r1 = a; - const struct cgit_repo *r2 = b; - - return cmp(r1->desc, r2->desc); -} - -static int sort_owner(const void *a, const void *b) -{ - const struct cgit_repo *r1 = a; - const struct cgit_repo *r2 = b; - - return cmp(r1->owner, r2->owner); -} - -static int sort_idle(const void *a, const void *b) -{ - const struct cgit_repo *r1 = a; - const struct cgit_repo *r2 = b; - time_t t1, t2; - - t1 = t2 = 0; - get_repo_modtime(r1, &t1); - get_repo_modtime(r2, &t2); - return t2 - t1; -} - -static int sort_section(const void *a, const void *b) -{ - const struct cgit_repo *r1 = a; - const struct cgit_repo *r2 = b; - int result; - - result = cmp(r1->section, r2->section); - if (!result) { - if (!strcmp(ctx.cfg.repository_sort, "age")) - result = sort_idle(r1, r2); - if (!result) - result = cmp(r1->name, r2->name); - } - return result; -} - -struct sortcolumn { - const char *name; - int (*fn)(const void *a, const void *b); -}; - -static const struct sortcolumn sortcolumn[] = { - {"section", sort_section}, - {"name", sort_name}, - {"desc", sort_desc}, - {"owner", sort_owner}, - {"idle", sort_idle}, - {NULL, NULL} -}; - -static int sort_repolist(char *field) -{ - const struct sortcolumn *column; - - for (column = &sortcolumn[0]; column->name; column++) { - if (strcmp(field, column->name)) - continue; - qsort(cgit_repolist.repos, cgit_repolist.count, - sizeof(struct cgit_repo), column->fn); - return 1; - } - return 0; -} - - -void cgit_print_repolist(void) -{ - int i, columns = 3, hits = 0, header = 0; - char *last_section = NULL; - char *section; - char *repourl; - int sorted = 0; - - if (!any_repos_visible()) { - cgit_print_error_page(404, "Not found", "No repositories found"); - return; - } - - if (ctx.cfg.enable_index_links) - ++columns; - if (ctx.cfg.enable_index_owner) - ++columns; - - ctx.page.title = ctx.cfg.root_title; - cgit_print_http_headers(); - cgit_print_docstart(); - cgit_print_pageheader(); - - if (ctx.qry.sort) - sorted = sort_repolist(ctx.qry.sort); - else if (ctx.cfg.section_sort) - sort_repolist("section"); - - html("<table summary='repository list' class='list nowrap'>"); - for (i = 0; i < cgit_repolist.count; i++) { - ctx.repo = &cgit_repolist.repos[i]; - if (!is_visible(ctx.repo)) - continue; - hits++; - if (hits <= ctx.qry.ofs) - continue; - if (hits > ctx.qry.ofs + ctx.cfg.max_repo_count) - continue; - if (!header++) - print_header(); - section = ctx.repo->section; - if (section && !strcmp(section, "")) - section = NULL; - if (!sorted && - ((last_section == NULL && section != NULL) || - (last_section != NULL && section == NULL) || - (last_section != NULL && section != NULL && - strcmp(section, last_section)))) { - htmlf("<tr class='nohover-highlight'><td colspan='%d' class='reposection'>", - columns); - html_txt(section); - html("</td></tr>"); - last_section = section; - } - htmlf("<tr><td class='%s'>", - !sorted && section ? "sublevel-repo" : "toplevel-repo"); - cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL); - html("</td><td>"); - repourl = cgit_repourl(ctx.repo->url); - html_link_open(repourl, NULL, NULL); - free(repourl); - if (html_ntxt(ctx.repo->desc, ctx.cfg.max_repodesc_len) < 0) - html("..."); - html_link_close(); - html("</td><td>"); - if (ctx.cfg.enable_index_owner) { - if (ctx.repo->owner_filter) { - cgit_open_filter(ctx.repo->owner_filter); - html_txt(ctx.repo->owner); - cgit_close_filter(ctx.repo->owner_filter); - } else { - char *currenturl = cgit_currenturl(); - html("<a href='"); - html_attr(currenturl); - html("?q="); - html_url_arg(ctx.repo->owner); - html("'>"); - html_txt(ctx.repo->owner); - html("</a>"); - free(currenturl); - } - html("</td><td>"); - } - print_modtime(ctx.repo); - html("</td>"); - if (ctx.cfg.enable_index_links) { - html("<td>"); - cgit_summary_link("summary", NULL, "button", NULL); - cgit_log_link("log", NULL, "button", NULL, NULL, NULL, - 0, NULL, NULL, ctx.qry.showmsg, 0); - cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL); - html("</td>"); - } - html("</tr>\n"); - } - html("</table>"); - if (hits > ctx.cfg.max_repo_count) - print_pager(hits, ctx.cfg.max_repo_count, ctx.qry.search, ctx.qry.sort); - cgit_print_docend(); -} - -void cgit_print_site_readme(void) -{ - cgit_print_layout_start(); - if (!ctx.cfg.root_readme) - goto done; - cgit_open_filter(ctx.cfg.about_filter, ctx.cfg.root_readme); - html_include(ctx.cfg.root_readme); - cgit_close_filter(ctx.cfg.about_filter); -done: - cgit_print_layout_end(); -} diff --git a/ui-repolist.h b/ui-repolist.h deleted file mode 100644 index 1b6b3227db..0000000000 --- a/ui-repolist.h +++ /dev/null @@ -1,7 +0,0 @@ -#ifndef UI_REPOLIST_H -#define UI_REPOLIST_H - -extern void cgit_print_repolist(void); -extern void cgit_print_site_readme(void); - -#endif /* UI_REPOLIST_H */ diff --git a/ui-shared.c b/ui-shared.c deleted file mode 100644 index d2358f2928..0000000000 --- a/ui-shared.c +++ /dev/null @@ -1,1210 +0,0 @@ -/* ui-shared.c: common web output functions - * - * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-shared.h" -#include "cmd.h" -#include "html.h" -#include "version.h" - -static const char cgit_doctype[] = -"<!DOCTYPE html>\n"; - -static char *http_date(time_t t) -{ - static char day[][4] = - {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; - static char month[][4] = - {"Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; - struct tm *tm = gmtime(&t); - return fmt("%s, %02d %s %04d %02d:%02d:%02d GMT", day[tm->tm_wday], - tm->tm_mday, month[tm->tm_mon], 1900 + tm->tm_year, - tm->tm_hour, tm->tm_min, tm->tm_sec); -} - -void cgit_print_error(const char *fmt, ...) -{ - va_list ap; - va_start(ap, fmt); - cgit_vprint_error(fmt, ap); - va_end(ap); -} - -void cgit_vprint_error(const char *fmt, va_list ap) -{ - va_list cp; - html("<div class='error'>"); - va_copy(cp, ap); - html_vtxtf(fmt, cp); - va_end(cp); - html("</div>\n"); -} - -const char *cgit_httpscheme(void) -{ - if (ctx.env.https && !strcmp(ctx.env.https, "on")) - return "https://"; - else - return "http://"; -} - -char *cgit_hosturl(void) -{ - if (ctx.env.http_host) - return xstrdup(ctx.env.http_host); - if (!ctx.env.server_name) - return NULL; - if (!ctx.env.server_port || atoi(ctx.env.server_port) == 80) - return xstrdup(ctx.env.server_name); - return fmtalloc("%s:%s", ctx.env.server_name, ctx.env.server_port); -} - -char *cgit_currenturl(void) -{ - const char *root = cgit_rooturl(); - - if (!ctx.qry.url) - return xstrdup(root); - if (root[0] && root[strlen(root) - 1] == '/') - return fmtalloc("%s%s", root, ctx.qry.url); - return fmtalloc("%s/%s", root, ctx.qry.url); -} - -char *cgit_currentfullurl(void) -{ - const char *root = cgit_rooturl(); - const char *orig_query = ctx.env.query_string ? ctx.env.query_string : ""; - size_t len = strlen(orig_query); - char *query = xmalloc(len + 2), *start_url, *ret; - - /* Remove all url=... parts from query string */ - memcpy(query + 1, orig_query, len + 1); - query[0] = '?'; - start_url = query; - while ((start_url = strstr(start_url, "url=")) != NULL) { - if (start_url[-1] == '?' || start_url[-1] == '&') { - const char *end_url = strchr(start_url, '&'); - if (end_url) - memmove(start_url, end_url + 1, strlen(end_url)); - else - start_url[0] = '\0'; - } else - ++start_url; - } - if (!query[1]) - query[0] = '\0'; - - if (!ctx.qry.url) - ret = fmtalloc("%s%s", root, query); - else if (root[0] && root[strlen(root) - 1] == '/') - ret = fmtalloc("%s%s%s", root, ctx.qry.url, query); - else - ret = fmtalloc("%s/%s%s", root, ctx.qry.url, query); - free(query); - return ret; -} - -const char *cgit_rooturl(void) -{ - if (ctx.cfg.virtual_root) - return ctx.cfg.virtual_root; - else - return ctx.cfg.script_name; -} - -const char *cgit_loginurl(void) -{ - static const char *login_url; - if (!login_url) - login_url = fmtalloc("%s?p=login", cgit_rooturl()); - return login_url; -} - -char *cgit_repourl(const char *reponame) -{ - if (ctx.cfg.virtual_root) - return fmtalloc("%s%s/", ctx.cfg.virtual_root, reponame); - else - return fmtalloc("?r=%s", reponame); -} - -char *cgit_fileurl(const char *reponame, const char *pagename, - const char *filename, const char *query) -{ - struct strbuf sb = STRBUF_INIT; - char *delim; - - if (ctx.cfg.virtual_root) { - strbuf_addf(&sb, "%s%s/%s/%s", ctx.cfg.virtual_root, reponame, - pagename, (filename ? filename:"")); - delim = "?"; - } else { - strbuf_addf(&sb, "?url=%s/%s/%s", reponame, pagename, - (filename ? filename : "")); - delim = "&"; - } - if (query) - strbuf_addf(&sb, "%s%s", delim, query); - return strbuf_detach(&sb, NULL); -} - -char *cgit_pageurl(const char *reponame, const char *pagename, - const char *query) -{ - return cgit_fileurl(reponame, pagename, NULL, query); -} - -const char *cgit_repobasename(const char *reponame) -{ - /* I assume we don't need to store more than one repo basename */ - static char rvbuf[1024]; - int p; - const char *rv; - size_t len; - - len = strlcpy(rvbuf, reponame, sizeof(rvbuf)); - if (len >= sizeof(rvbuf)) - die("cgit_repobasename: truncated repository name '%s'", reponame); - p = len - 1; - /* strip trailing slashes */ - while (p && rvbuf[p] == '/') - rvbuf[p--] = '\0'; - /* strip trailing .git */ - if (p >= 3 && starts_with(&rvbuf[p-3], ".git")) { - p -= 3; - rvbuf[p--] = '\0'; - } - /* strip more trailing slashes if any */ - while (p && rvbuf[p] == '/') - rvbuf[p--] = '\0'; - /* find last slash in the remaining string */ - rv = strrchr(rvbuf, '/'); - if (rv) - return ++rv; - return rvbuf; -} - -const char *cgit_snapshot_prefix(const struct cgit_repo *repo) -{ - if (repo->snapshot_prefix) - return repo->snapshot_prefix; - - return cgit_repobasename(repo->url); -} - -static void site_url(const char *page, const char *search, const char *sort, int ofs, int always_root) -{ - char *delim = "?"; - - if (always_root || page) - html_attr(cgit_rooturl()); - else { - char *currenturl = cgit_currenturl(); - html_attr(currenturl); - free(currenturl); - } - - if (page) { - htmlf("?p=%s", page); - delim = "&"; - } - if (search) { - html(delim); - html("q="); - html_attr(search); - delim = "&"; - } - if (sort) { - html(delim); - html("s="); - html_attr(sort); - delim = "&"; - } - if (ofs) { - html(delim); - htmlf("ofs=%d", ofs); - } -} - -static void site_link(const char *page, const char *name, const char *title, - const char *class, const char *search, const char *sort, int ofs, int always_root) -{ - html("<a"); - if (title) { - html(" title='"); - html_attr(title); - html("'"); - } - if (class) { - html(" class='"); - html_attr(class); - html("'"); - } - html(" href='"); - site_url(page, search, sort, ofs, always_root); - html("'>"); - html_txt(name); - html("</a>"); -} - -void cgit_index_link(const char *name, const char *title, const char *class, - const char *pattern, const char *sort, int ofs, int always_root) -{ - site_link(NULL, name, title, class, pattern, sort, ofs, always_root); -} - -static char *repolink(const char *title, const char *class, const char *page, - const char *head, const char *path) -{ - char *delim = "?"; - - html("<a"); - if (title) { - html(" title='"); - html_attr(title); - html("'"); - } - if (class) { - html(" class='"); - html_attr(class); - html("'"); - } - html(" href='"); - if (ctx.cfg.virtual_root) { - html_url_path(ctx.cfg.virtual_root); - html_url_path(ctx.repo->url); - if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/') - html("/"); - if (page) { - html_url_path(page); - html("/"); - if (path) - html_url_path(path); - } - } else { - html_url_path(ctx.cfg.script_name); - html("?url="); - html_url_arg(ctx.repo->url); - if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/') - html("/"); - if (page) { - html_url_arg(page); - html("/"); - if (path) - html_url_arg(path); - } - delim = "&"; - } - if (head && ctx.repo->defbranch && strcmp(head, ctx.repo->defbranch)) { - html(delim); - html("h="); - html_url_arg(head); - delim = "&"; - } - return fmt("%s", delim); -} - -static void reporevlink(const char *page, const char *name, const char *title, - const char *class, const char *head, const char *rev, - const char *path) -{ - char *delim; - - delim = repolink(title, class, page, head, path); - if (rev && ctx.qry.head != NULL && strcmp(rev, ctx.qry.head)) { - html(delim); - html("id="); - html_url_arg(rev); - } - html("'>"); - html_txt(name); - html("</a>"); -} - -void cgit_summary_link(const char *name, const char *title, const char *class, - const char *head) -{ - reporevlink(NULL, name, title, class, head, NULL, NULL); -} - -void cgit_tag_link(const char *name, const char *title, const char *class, - const char *tag) -{ - reporevlink("tag", name, title, class, tag, NULL, NULL); -} - -void cgit_tree_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, const char *path) -{ - reporevlink("tree", name, title, class, head, rev, path); -} - -void cgit_plain_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, const char *path) -{ - reporevlink("plain", name, title, class, head, rev, path); -} - -void cgit_blame_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, const char *path) -{ - reporevlink("blame", name, title, class, head, rev, path); -} - -void cgit_log_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, const char *path, - int ofs, const char *grep, const char *pattern, int showmsg, - int follow) -{ - char *delim; - - delim = repolink(title, class, "log", head, path); - if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) { - html(delim); - html("id="); - html_url_arg(rev); - delim = "&"; - } - if (grep && pattern) { - html(delim); - html("qt="); - html_url_arg(grep); - delim = "&"; - html(delim); - html("q="); - html_url_arg(pattern); - } - if (ofs > 0) { - html(delim); - html("ofs="); - htmlf("%d", ofs); - delim = "&"; - } - if (showmsg) { - html(delim); - html("showmsg=1"); - delim = "&"; - } - if (follow) { - html(delim); - html("follow=1"); - } - html("'>"); - html_txt(name); - html("</a>"); -} - -void cgit_commit_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, const char *path) -{ - char *delim; - - delim = repolink(title, class, "commit", head, path); - if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) { - html(delim); - html("id="); - html_url_arg(rev); - delim = "&"; - } - if (ctx.qry.difftype) { - html(delim); - htmlf("dt=%d", ctx.qry.difftype); - delim = "&"; - } - if (ctx.qry.context > 0 && ctx.qry.context != 3) { - html(delim); - html("context="); - htmlf("%d", ctx.qry.context); - delim = "&"; - } - if (ctx.qry.ignorews) { - html(delim); - html("ignorews=1"); - delim = "&"; - } - if (ctx.qry.follow) { - html(delim); - html("follow=1"); - } - html("'>"); - if (name[0] != '\0') { - if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) { - html_ntxt(name, ctx.cfg.max_msg_len - 3); - html("..."); - } else - html_txt(name); - } else - html_txt("(no commit message)"); - html("</a>"); -} - -void cgit_refs_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, const char *path) -{ - reporevlink("refs", name, title, class, head, rev, path); -} - -void cgit_snapshot_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, - const char *archivename) -{ - reporevlink("snapshot", name, title, class, head, rev, archivename); -} - -void cgit_diff_link(const char *name, const char *title, const char *class, - const char *head, const char *new_rev, const char *old_rev, - const char *path) -{ - char *delim; - - delim = repolink(title, class, "diff", head, path); - if (new_rev && ctx.qry.head != NULL && strcmp(new_rev, ctx.qry.head)) { - html(delim); - html("id="); - html_url_arg(new_rev); - delim = "&"; - } - if (old_rev) { - html(delim); - html("id2="); - html_url_arg(old_rev); - delim = "&"; - } - if (ctx.qry.difftype) { - html(delim); - htmlf("dt=%d", ctx.qry.difftype); - delim = "&"; - } - if (ctx.qry.context > 0 && ctx.qry.context != 3) { - html(delim); - html("context="); - htmlf("%d", ctx.qry.context); - delim = "&"; - } - if (ctx.qry.ignorews) { - html(delim); - html("ignorews=1"); - delim = "&"; - } - if (ctx.qry.follow) { - html(delim); - html("follow=1"); - } - html("'>"); - html_txt(name); - html("</a>"); -} - -void cgit_patch_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, const char *path) -{ - reporevlink("patch", name, title, class, head, rev, path); -} - -void cgit_stats_link(const char *name, const char *title, const char *class, - const char *head, const char *path) -{ - reporevlink("stats", name, title, class, head, NULL, path); -} - -static void cgit_self_link(char *name, const char *title, const char *class) -{ - if (!strcmp(ctx.qry.page, "repolist")) - cgit_index_link(name, title, class, ctx.qry.search, ctx.qry.sort, - ctx.qry.ofs, 1); - else if (!strcmp(ctx.qry.page, "summary")) - cgit_summary_link(name, title, class, ctx.qry.head); - else if (!strcmp(ctx.qry.page, "tag")) - cgit_tag_link(name, title, class, ctx.qry.has_sha1 ? - ctx.qry.sha1 : ctx.qry.head); - else if (!strcmp(ctx.qry.page, "tree")) - cgit_tree_link(name, title, class, ctx.qry.head, - ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL, - ctx.qry.path); - else if (!strcmp(ctx.qry.page, "plain")) - cgit_plain_link(name, title, class, ctx.qry.head, - ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL, - ctx.qry.path); - else if (!strcmp(ctx.qry.page, "blame")) - cgit_blame_link(name, title, class, ctx.qry.head, - ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL, - ctx.qry.path); - else if (!strcmp(ctx.qry.page, "log")) - cgit_log_link(name, title, class, ctx.qry.head, - ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL, - ctx.qry.path, ctx.qry.ofs, - ctx.qry.grep, ctx.qry.search, - ctx.qry.showmsg, ctx.qry.follow); - else if (!strcmp(ctx.qry.page, "commit")) - cgit_commit_link(name, title, class, ctx.qry.head, - ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL, - ctx.qry.path); - else if (!strcmp(ctx.qry.page, "patch")) - cgit_patch_link(name, title, class, ctx.qry.head, - ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL, - ctx.qry.path); - else if (!strcmp(ctx.qry.page, "refs")) - cgit_refs_link(name, title, class, ctx.qry.head, - ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL, - ctx.qry.path); - else if (!strcmp(ctx.qry.page, "snapshot")) - cgit_snapshot_link(name, title, class, ctx.qry.head, - ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL, - ctx.qry.path); - else if (!strcmp(ctx.qry.page, "diff")) - cgit_diff_link(name, title, class, ctx.qry.head, - ctx.qry.sha1, ctx.qry.sha2, - ctx.qry.path); - else if (!strcmp(ctx.qry.page, "stats")) - cgit_stats_link(name, title, class, ctx.qry.head, - ctx.qry.path); - else { - /* Don't known how to make link for this page */ - repolink(title, class, ctx.qry.page, ctx.qry.head, ctx.qry.path); - html("><!-- cgit_self_link() doesn't know how to make link for page '"); - html_txt(ctx.qry.page); - html("' -->"); - html_txt(name); - html("</a>"); - } -} - -void cgit_object_link(struct object *obj) -{ - char *page, *shortrev, *fullrev, *name; - - fullrev = oid_to_hex(&obj->oid); - shortrev = xstrdup(fullrev); - shortrev[10] = '\0'; - if (obj->type == OBJ_COMMIT) { - cgit_commit_link(fmt("commit %s...", shortrev), NULL, NULL, - ctx.qry.head, fullrev, NULL); - return; - } else if (obj->type == OBJ_TREE) - page = "tree"; - else if (obj->type == OBJ_TAG) - page = "tag"; - else - page = "blob"; - name = fmt("%s %s...", type_name(obj->type), shortrev); - reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL); -} - -static struct string_list_item *lookup_path(struct string_list *list, - const char *path) -{ - struct string_list_item *item; - - while (path && path[0]) { - if ((item = string_list_lookup(list, path))) - return item; - if (!(path = strchr(path, '/'))) - break; - path++; - } - return NULL; -} - -void cgit_submodule_link(const char *class, char *path, const char *rev) -{ - struct string_list *list; - struct string_list_item *item; - char tail, *dir; - size_t len; - - len = 0; - tail = 0; - list = &ctx.repo->submodules; - item = lookup_path(list, path); - if (!item) { - len = strlen(path); - tail = path[len - 1]; - if (tail == '/') { - path[len - 1] = 0; - item = lookup_path(list, path); - } - } - if (item || ctx.repo->module_link) { - html("<a "); - if (class) - htmlf("class='%s' ", class); - html("href='"); - if (item) { - html_attrf(item->util, rev); - } else { - dir = strrchr(path, '/'); - if (dir) - dir++; - else - dir = path; - html_attrf(ctx.repo->module_link, dir, rev); - } - html("'>"); - html_txt(path); - html("</a>"); - } else { - html("<span"); - if (class) - htmlf(" class='%s'", class); - html(">"); - html_txt(path); - html("</span>"); - } - html_txtf(" @ %.7s", rev); - if (item && tail) - path[len - 1] = tail; -} - -const struct date_mode *cgit_date_mode(enum date_mode_type type) -{ - static struct date_mode mode; - mode.type = type; - mode.local = ctx.cfg.local_time; - return &mode; -} - -static void print_rel_date(time_t t, int tz, double value, - const char *class, const char *suffix) -{ - htmlf("<span class='%s' title='", class); - html_attr(show_date(t, tz, cgit_date_mode(DATE_ISO8601))); - htmlf("'>%.0f %s</span>", value, suffix); -} - -void cgit_print_age(time_t t, int tz, time_t max_relative) -{ - time_t now, secs; - - if (!t) - return; - time(&now); - secs = now - t; - if (secs < 0) - secs = 0; - - if (secs > max_relative && max_relative >= 0) { - html("<span title='"); - html_attr(show_date(t, tz, cgit_date_mode(DATE_ISO8601))); - html("'>"); - html_txt(show_date(t, tz, cgit_date_mode(DATE_SHORT))); - html("</span>"); - return; - } - - if (secs < TM_HOUR * 2) { - print_rel_date(t, tz, secs * 1.0 / TM_MIN, "age-mins", "min."); - return; - } - if (secs < TM_DAY * 2) { - print_rel_date(t, tz, secs * 1.0 / TM_HOUR, "age-hours", "hours"); - return; - } - if (secs < TM_WEEK * 2) { - print_rel_date(t, tz, secs * 1.0 / TM_DAY, "age-days", "days"); - return; - } - if (secs < TM_MONTH * 2) { - print_rel_date(t, tz, secs * 1.0 / TM_WEEK, "age-weeks", "weeks"); - return; - } - if (secs < TM_YEAR * 2) { - print_rel_date(t, tz, secs * 1.0 / TM_MONTH, "age-months", "months"); - return; - } - print_rel_date(t, tz, secs * 1.0 / TM_YEAR, "age-years", "years"); -} - -void cgit_print_http_headers(void) -{ - if (ctx.env.no_http && !strcmp(ctx.env.no_http, "1")) - return; - - if (ctx.page.status) - htmlf("Status: %d %s\n", ctx.page.status, ctx.page.statusmsg); - if (ctx.page.mimetype && ctx.page.charset) - htmlf("Content-Type: %s; charset=%s\n", ctx.page.mimetype, - ctx.page.charset); - else if (ctx.page.mimetype) - htmlf("Content-Type: %s\n", ctx.page.mimetype); - if (ctx.page.size) - htmlf("Content-Length: %zd\n", ctx.page.size); - if (ctx.page.filename) { - html("Content-Disposition: inline; filename=\""); - html_header_arg_in_quotes(ctx.page.filename); - html("\"\n"); - } - if (!ctx.env.authenticated) - html("Cache-Control: no-cache, no-store\n"); - htmlf("Last-Modified: %s\n", http_date(ctx.page.modified)); - htmlf("Expires: %s\n", http_date(ctx.page.expires)); - if (ctx.page.etag) - htmlf("ETag: \"%s\"\n", ctx.page.etag); - html("\n"); - if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD")) - exit(0); -} - -void cgit_redirect(const char *url, bool permanent) -{ - htmlf("Status: %d %s\n", permanent ? 301 : 302, permanent ? "Moved" : "Found"); - html("Location: "); - html_url_path(url); - html("\n\n"); -} - -static void print_rel_vcs_link(const char *url) -{ - html("<link rel='vcs-git' href='"); - html_attr(url); - html("' title='"); - html_attr(ctx.repo->name); - html(" Git repository'/>\n"); -} - -void cgit_print_docstart(void) -{ - char *host = cgit_hosturl(); - - if (ctx.cfg.embedded) { - if (ctx.cfg.header) - html_include(ctx.cfg.header); - return; - } - - html(cgit_doctype); - html("<html lang='en'>\n"); - html("<head>\n"); - html("<title>"); - html_txt(ctx.page.title); - html("</title>\n"); - htmlf("<meta name='generator' content='cgit %s'/>\n", cgit_version); - if (ctx.cfg.robots && *ctx.cfg.robots) - htmlf("<meta name='robots' content='%s'/>\n", ctx.cfg.robots); - html("<link rel='stylesheet' type='text/css' href='"); - html_attr(ctx.cfg.css); - html("'/>\n"); - if (ctx.cfg.favicon) { - html("<link rel='shortcut icon' href='"); - html_attr(ctx.cfg.favicon); - html("'/>\n"); - } - if (host && ctx.repo && ctx.qry.head) { - char *fileurl; - struct strbuf sb = STRBUF_INIT; - strbuf_addf(&sb, "h=%s", ctx.qry.head); - - html("<link rel='alternate' title='Atom feed' href='"); - html(cgit_httpscheme()); - html_attr(host); - fileurl = cgit_fileurl(ctx.repo->url, "atom", ctx.qry.vpath, - sb.buf); - html_attr(fileurl); - html("' type='application/atom+xml'/>\n"); - strbuf_release(&sb); - free(fileurl); - } - if (ctx.repo) - cgit_add_clone_urls(print_rel_vcs_link); - if (ctx.cfg.head_include) - html_include(ctx.cfg.head_include); - if (ctx.repo && ctx.repo->extra_head_content) - html(ctx.repo->extra_head_content); - html("</head>\n"); - html("<body>\n"); - if (ctx.cfg.header) - html_include(ctx.cfg.header); - free(host); -} - -void cgit_print_docend(void) -{ - html("</div> <!-- class=content -->\n"); - if (ctx.cfg.embedded) { - html("</div> <!-- id=cgit -->\n"); - if (ctx.cfg.footer) - html_include(ctx.cfg.footer); - return; - } - if (ctx.cfg.footer) - html_include(ctx.cfg.footer); - else { - htmlf("<div class='footer'>generated by <a href='https://git.zx2c4.com/cgit/about/'>cgit %s</a> " - "(<a href='https://git-scm.com/'>git %s</a>) at ", cgit_version, git_version_string); - html_txt(show_date(time(NULL), 0, cgit_date_mode(DATE_ISO8601))); - html("</div>\n"); - } - html("</div> <!-- id=cgit -->\n"); - html("</body>\n</html>\n"); -} - -void cgit_print_error_page(int code, const char *msg, const char *fmt, ...) -{ - va_list ap; - ctx.page.expires = ctx.cfg.cache_dynamic_ttl; - ctx.page.status = code; - ctx.page.statusmsg = msg; - cgit_print_layout_start(); - va_start(ap, fmt); - cgit_vprint_error(fmt, ap); - va_end(ap); - cgit_print_layout_end(); -} - -void cgit_print_layout_start(void) -{ - cgit_print_http_headers(); - cgit_print_docstart(); - cgit_print_pageheader(); -} - -void cgit_print_layout_end(void) -{ - cgit_print_docend(); -} - -static void add_clone_urls(void (*fn)(const char *), char *txt, char *suffix) -{ - struct strbuf **url_list = strbuf_split_str(txt, ' ', 0); - int i; - - for (i = 0; url_list[i]; i++) { - strbuf_rtrim(url_list[i]); - if (url_list[i]->len == 0) - continue; - if (suffix && *suffix) - strbuf_addf(url_list[i], "/%s", suffix); - fn(url_list[i]->buf); - } - - strbuf_list_free(url_list); -} - -void cgit_add_clone_urls(void (*fn)(const char *)) -{ - if (ctx.repo->clone_url) - add_clone_urls(fn, expand_macros(ctx.repo->clone_url), NULL); - else if (ctx.cfg.clone_prefix) - add_clone_urls(fn, ctx.cfg.clone_prefix, ctx.repo->url); -} - -static int print_branch_option(const char *refname, const struct object_id *oid, - int flags, void *cb_data) -{ - char *name = (char *)refname; - html_option(name, name, ctx.qry.head); - return 0; -} - -void cgit_add_hidden_formfields(int incl_head, int incl_search, - const char *page) -{ - if (!ctx.cfg.virtual_root) { - struct strbuf url = STRBUF_INIT; - - strbuf_addf(&url, "%s/%s", ctx.qry.repo, page); - if (ctx.qry.vpath) - strbuf_addf(&url, "/%s", ctx.qry.vpath); - html_hidden("url", url.buf); - strbuf_release(&url); - } - - if (incl_head && ctx.qry.head && ctx.repo->defbranch && - strcmp(ctx.qry.head, ctx.repo->defbranch)) - html_hidden("h", ctx.qry.head); - - if (ctx.qry.sha1) - html_hidden("id", ctx.qry.sha1); - if (ctx.qry.sha2) - html_hidden("id2", ctx.qry.sha2); - if (ctx.qry.showmsg) - html_hidden("showmsg", "1"); - - if (incl_search) { - if (ctx.qry.grep) - html_hidden("qt", ctx.qry.grep); - if (ctx.qry.search) - html_hidden("q", ctx.qry.search); - } -} - -static const char *hc(const char *page) -{ - if (!ctx.qry.page) - return NULL; - - return strcmp(ctx.qry.page, page) ? NULL : "active"; -} - -static void cgit_print_path_crumbs(char *path) -{ - char *old_path = ctx.qry.path; - char *p = path, *q, *end = path + strlen(path); - int levels = 0; - - ctx.qry.path = NULL; - cgit_self_link("root", NULL, NULL); - ctx.qry.path = p = path; - while (p < end) { - if (!(q = strchr(p, '/')) || levels > 15) - q = end; - *q = '\0'; - html_txt("/"); - cgit_self_link(p, NULL, NULL); - if (q < end) - *q = '/'; - p = q + 1; - ++levels; - } - ctx.qry.path = old_path; -} - -static void print_header(void) -{ - char *logo = NULL, *logo_link = NULL; - - html("<table id='header'>\n"); - html("<tr>\n"); - - if (ctx.repo && ctx.repo->logo && *ctx.repo->logo) - logo = ctx.repo->logo; - else - logo = ctx.cfg.logo; - if (ctx.repo && ctx.repo->logo_link && *ctx.repo->logo_link) - logo_link = ctx.repo->logo_link; - else - logo_link = ctx.cfg.logo_link; - if (logo && *logo) { - html("<td class='logo' rowspan='2'><a href='"); - if (logo_link && *logo_link) - html_attr(logo_link); - else - html_attr(cgit_rooturl()); - html("'><img src='"); - html_attr(logo); - html("' alt='cgit logo'/></a></td>\n"); - } - - html("<td class='main'>"); - if (ctx.repo) { - cgit_index_link("index", NULL, NULL, NULL, NULL, 0, 1); - html(" : "); - cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL); - if (ctx.env.authenticated) { - html("</td><td class='form'>"); - html("<form method='get'>\n"); - cgit_add_hidden_formfields(0, 1, ctx.qry.page); - html("<select name='h' onchange='this.form.submit();'>\n"); - for_each_branch_ref(print_branch_option, ctx.qry.head); - if (ctx.repo->enable_remote_branches) - for_each_remote_ref(print_branch_option, ctx.qry.head); - html("</select> "); - html("<input type='submit' value='switch'/>"); - html("</form>"); - } - } else - html_txt(ctx.cfg.root_title); - html("</td></tr>\n"); - - html("<tr><td class='sub'>"); - if (ctx.repo) { - html_txt(ctx.repo->desc); - html("</td><td class='sub right'>"); - html_txt(ctx.repo->owner); - } else { - if (ctx.cfg.root_desc) - html_txt(ctx.cfg.root_desc); - } - html("</td></tr></table>\n"); -} - -void cgit_print_pageheader(void) -{ - html("<div id='cgit'>"); - if (!ctx.env.authenticated || !ctx.cfg.noheader) - print_header(); - - html("<table class='tabs'><tr><td>\n"); - if (ctx.env.authenticated && ctx.repo) { - if (ctx.repo->readme.nr) - reporevlink("about", "about", NULL, - hc("about"), ctx.qry.head, NULL, - NULL); - cgit_summary_link("summary", NULL, hc("summary"), - ctx.qry.head); - cgit_refs_link("refs", NULL, hc("refs"), ctx.qry.head, - ctx.qry.sha1, NULL); - cgit_log_link("log", NULL, hc("log"), ctx.qry.head, - NULL, ctx.qry.vpath, 0, NULL, NULL, - ctx.qry.showmsg, ctx.qry.follow); - if (ctx.qry.page && !strcmp(ctx.qry.page, "blame")) - cgit_blame_link("blame", NULL, hc("blame"), ctx.qry.head, - ctx.qry.sha1, ctx.qry.vpath); - else - cgit_tree_link("tree", NULL, hc("tree"), ctx.qry.head, - ctx.qry.sha1, ctx.qry.vpath); - cgit_commit_link("commit", NULL, hc("commit"), - ctx.qry.head, ctx.qry.sha1, ctx.qry.vpath); - cgit_diff_link("diff", NULL, hc("diff"), ctx.qry.head, - ctx.qry.sha1, ctx.qry.sha2, ctx.qry.vpath); - if (ctx.repo->max_stats) - cgit_stats_link("stats", NULL, hc("stats"), - ctx.qry.head, ctx.qry.vpath); - if (ctx.repo->homepage) { - html("<a href='"); - html_attr(ctx.repo->homepage); - html("'>homepage</a>"); - } - html("</td><td class='form'>"); - html("<form class='right' method='get' action='"); - if (ctx.cfg.virtual_root) { - char *fileurl = cgit_fileurl(ctx.qry.repo, "log", - ctx.qry.vpath, NULL); - html_url_path(fileurl); - free(fileurl); - } - html("'>\n"); - cgit_add_hidden_formfields(1, 0, "log"); - html("<select name='qt'>\n"); - html_option("grep", "log msg", ctx.qry.grep); - html_option("author", "author", ctx.qry.grep); - html_option("committer", "committer", ctx.qry.grep); - html_option("range", "range", ctx.qry.grep); - html("</select>\n"); - html("<input class='txt' type='search' size='10' name='q' value='"); - html_attr(ctx.qry.search); - html("'/>\n"); - html("<input type='submit' value='search'/>\n"); - html("</form>\n"); - } else if (ctx.env.authenticated) { - char *currenturl = cgit_currenturl(); - site_link(NULL, "index", NULL, hc("repolist"), NULL, NULL, 0, 1); - if (ctx.cfg.root_readme) - site_link("about", "about", NULL, hc("about"), - NULL, NULL, 0, 1); - html("</td><td class='form'>"); - html("<form method='get' action='"); - html_attr(currenturl); - html("'>\n"); - html("<input type='search' name='q' size='10' value='"); - html_attr(ctx.qry.search); - html("'/>\n"); - html("<input type='submit' value='search'/>\n"); - html("</form>"); - free(currenturl); - } - html("</td></tr></table>\n"); - if (ctx.env.authenticated && ctx.repo && ctx.qry.vpath) { - html("<div class='path'>"); - html("path: "); - cgit_print_path_crumbs(ctx.qry.vpath); - if (ctx.cfg.enable_follow_links && !strcmp(ctx.qry.page, "log")) { - html(" ("); - ctx.qry.follow = !ctx.qry.follow; - cgit_self_link(ctx.qry.follow ? "follow" : "unfollow", - NULL, NULL); - ctx.qry.follow = !ctx.qry.follow; - html(")"); - } - html("</div>"); - } - html("<div class='content'>"); -} - -void cgit_print_filemode(unsigned short mode) -{ - if (S_ISDIR(mode)) - html("d"); - else if (S_ISLNK(mode)) - html("l"); - else if (S_ISGITLINK(mode)) - html("m"); - else - html("-"); - html_fileperm(mode >> 6); - html_fileperm(mode >> 3); - html_fileperm(mode); -} - -void cgit_compose_snapshot_prefix(struct strbuf *filename, const char *base, - const char *ref) -{ - struct object_id oid; - - /* - * Prettify snapshot names by stripping leading "v" or "V" if the tag - * name starts with {v,V}[0-9] and the prettify mapping is injective, - * i.e. each stripped tag can be inverted without ambiguities. - */ - if (get_oid(fmt("refs/tags/%s", ref), &oid) == 0 && - (ref[0] == 'v' || ref[0] == 'V') && isdigit(ref[1]) && - ((get_oid(fmt("refs/tags/%s", ref + 1), &oid) == 0) + - (get_oid(fmt("refs/tags/v%s", ref + 1), &oid) == 0) + - (get_oid(fmt("refs/tags/V%s", ref + 1), &oid) == 0) == 1)) - ref++; - - strbuf_addf(filename, "%s-%s", base, ref); -} - -void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref, - const char *separator) -{ - const struct cgit_snapshot_format *f; - struct strbuf filename = STRBUF_INIT; - const char *basename; - size_t prefixlen; - - basename = cgit_snapshot_prefix(repo); - if (starts_with(ref, basename)) - strbuf_addstr(&filename, ref); - else - cgit_compose_snapshot_prefix(&filename, basename, ref); - - prefixlen = filename.len; - for (f = cgit_snapshot_formats; f->suffix; f++) { - if (!(repo->snapshots & cgit_snapshot_format_bit(f))) - continue; - strbuf_setlen(&filename, prefixlen); - strbuf_addstr(&filename, f->suffix); - cgit_snapshot_link(filename.buf, NULL, NULL, NULL, NULL, - filename.buf); - if (cgit_snapshot_get_sig(ref, f)) { - strbuf_addstr(&filename, ".asc"); - html(" ("); - cgit_snapshot_link("sig", NULL, NULL, NULL, NULL, - filename.buf); - html(")"); - } else if (starts_with(f->suffix, ".tar") && cgit_snapshot_get_sig(ref, &cgit_snapshot_formats[0])) { - strbuf_setlen(&filename, strlen(filename.buf) - strlen(f->suffix)); - strbuf_addstr(&filename, ".tar.asc"); - html(" ("); - cgit_snapshot_link("sig", NULL, NULL, NULL, NULL, - filename.buf); - html(")"); - } - html(separator); - } - strbuf_release(&filename); -} - -void cgit_set_title_from_path(const char *path) -{ - struct strbuf sb = STRBUF_INIT; - const char *slash, *last_slash; - - if (!path) - return; - - for (last_slash = path + strlen(path); (slash = memrchr(path, '/', last_slash - path)) != NULL; last_slash = slash) { - strbuf_add(&sb, slash + 1, last_slash - slash - 1); - strbuf_addstr(&sb, " \xc2\xab "); - } - strbuf_add(&sb, path, last_slash - path); - strbuf_addf(&sb, " - %s", ctx.page.title); - ctx.page.title = strbuf_detach(&sb, NULL); -} diff --git a/ui-shared.h b/ui-shared.h deleted file mode 100644 index 6964873a63..0000000000 --- a/ui-shared.h +++ /dev/null @@ -1,87 +0,0 @@ -#ifndef UI_SHARED_H -#define UI_SHARED_H - -extern const char *cgit_httpscheme(void); -extern char *cgit_hosturl(void); -extern const char *cgit_rooturl(void); -extern char *cgit_currenturl(void); -extern char *cgit_currentfullurl(void); -extern const char *cgit_loginurl(void); -extern char *cgit_repourl(const char *reponame); -extern char *cgit_fileurl(const char *reponame, const char *pagename, - const char *filename, const char *query); -extern char *cgit_pageurl(const char *reponame, const char *pagename, - const char *query); - -extern void cgit_add_clone_urls(void (*fn)(const char *)); - -extern void cgit_index_link(const char *name, const char *title, - const char *class, const char *pattern, const char *sort, int ofs, int always_root); -extern void cgit_summary_link(const char *name, const char *title, - const char *class, const char *head); -extern void cgit_tag_link(const char *name, const char *title, - const char *class, const char *tag); -extern void cgit_tree_link(const char *name, const char *title, - const char *class, const char *head, - const char *rev, const char *path); -extern void cgit_plain_link(const char *name, const char *title, - const char *class, const char *head, - const char *rev, const char *path); -extern void cgit_blame_link(const char *name, const char *title, - const char *class, const char *head, - const char *rev, const char *path); -extern void cgit_log_link(const char *name, const char *title, - const char *class, const char *head, const char *rev, - const char *path, int ofs, const char *grep, - const char *pattern, int showmsg, int follow); -extern void cgit_commit_link(const char *name, const char *title, - const char *class, const char *head, - const char *rev, const char *path); -extern void cgit_patch_link(const char *name, const char *title, - const char *class, const char *head, - const char *rev, const char *path); -extern void cgit_refs_link(const char *name, const char *title, - const char *class, const char *head, - const char *rev, const char *path); -extern void cgit_snapshot_link(const char *name, const char *title, - const char *class, const char *head, - const char *rev, const char *archivename); -extern void cgit_diff_link(const char *name, const char *title, - const char *class, const char *head, - const char *new_rev, const char *old_rev, - const char *path); -extern void cgit_stats_link(const char *name, const char *title, - const char *class, const char *head, - const char *path); -extern void cgit_object_link(struct object *obj); - -extern void cgit_submodule_link(const char *class, char *path, - const char *rev); - -extern void cgit_print_layout_start(void); -extern void cgit_print_layout_end(void); - -__attribute__((format (printf,1,2))) -extern void cgit_print_error(const char *fmt, ...); -__attribute__((format (printf,1,0))) -extern void cgit_vprint_error(const char *fmt, va_list ap); -extern const struct date_mode *cgit_date_mode(enum date_mode_type type); -extern void cgit_print_age(time_t t, int tz, time_t max_relative); -extern void cgit_print_http_headers(void); -extern void cgit_redirect(const char *url, bool permanent); -extern void cgit_print_docstart(void); -extern void cgit_print_docend(void); -__attribute__((format (printf,3,4))) -extern void cgit_print_error_page(int code, const char *msg, const char *fmt, ...); -extern void cgit_print_pageheader(void); -extern void cgit_print_filemode(unsigned short mode); -extern void cgit_compose_snapshot_prefix(struct strbuf *filename, - const char *base, const char *ref); -extern void cgit_print_snapshot_links(const struct cgit_repo *repo, - const char *ref, const char *separator); -extern const char *cgit_snapshot_prefix(const struct cgit_repo *repo); -extern void cgit_add_hidden_formfields(int incl_head, int incl_search, - const char *page); - -extern void cgit_set_title_from_path(const char *path); -#endif /* UI_SHARED_H */ diff --git a/ui-snapshot.c b/ui-snapshot.c deleted file mode 100644 index 9461d51a59..0000000000 --- a/ui-snapshot.c +++ /dev/null @@ -1,302 +0,0 @@ -/* ui-snapshot.c: generate snapshot of a commit - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-snapshot.h" -#include "html.h" -#include "ui-shared.h" - -static int write_archive_type(const char *format, const char *hex, const char *prefix) -{ - struct argv_array argv = ARGV_ARRAY_INIT; - const char **nargv; - int result; - argv_array_push(&argv, "snapshot"); - argv_array_push(&argv, format); - if (prefix) { - struct strbuf buf = STRBUF_INIT; - strbuf_addstr(&buf, prefix); - strbuf_addch(&buf, '/'); - argv_array_push(&argv, "--prefix"); - argv_array_push(&argv, buf.buf); - strbuf_release(&buf); - } - argv_array_push(&argv, hex); - /* - * Now we need to copy the pointers to arguments into a new - * structure because write_archive will rearrange its arguments - * which may result in duplicated/missing entries causing leaks - * or double-frees in argv_array_clear. - */ - nargv = xmalloc(sizeof(char *) * (argv.argc + 1)); - /* argv_array guarantees a trailing NULL entry. */ - memcpy(nargv, argv.argv, sizeof(char *) * (argv.argc + 1)); - - result = write_archive(argv.argc, nargv, NULL, the_repository, NULL, 0); - argv_array_clear(&argv); - free(nargv); - return result; -} - -static int write_tar_archive(const char *hex, const char *prefix) -{ - return write_archive_type("--format=tar", hex, prefix); -} - -static int write_zip_archive(const char *hex, const char *prefix) -{ - return write_archive_type("--format=zip", hex, prefix); -} - -static int write_compressed_tar_archive(const char *hex, - const char *prefix, - char *filter_argv[]) -{ - int rv; - struct cgit_exec_filter f; - cgit_exec_filter_init(&f, filter_argv[0], filter_argv); - - cgit_open_filter(&f.base); - rv = write_tar_archive(hex, prefix); - cgit_close_filter(&f.base); - return rv; -} - -static int write_tar_gzip_archive(const char *hex, const char *prefix) -{ - char *argv[] = { "gzip", "-n", NULL }; - return write_compressed_tar_archive(hex, prefix, argv); -} - -static int write_tar_bzip2_archive(const char *hex, const char *prefix) -{ - char *argv[] = { "bzip2", NULL }; - return write_compressed_tar_archive(hex, prefix, argv); -} - -static int write_tar_xz_archive(const char *hex, const char *prefix) -{ - char *argv[] = { "xz", NULL }; - return write_compressed_tar_archive(hex, prefix, argv); -} - -const struct cgit_snapshot_format cgit_snapshot_formats[] = { - /* .tar must remain the 0 index */ - { ".tar", "application/x-tar", write_tar_archive }, - { ".tar.gz", "application/x-gzip", write_tar_gzip_archive }, - { ".tar.bz2", "application/x-bzip2", write_tar_bzip2_archive }, - { ".tar.xz", "application/x-xz", write_tar_xz_archive }, - { ".zip", "application/x-zip", write_zip_archive }, - { NULL } -}; - -static struct notes_tree snapshot_sig_notes[ARRAY_SIZE(cgit_snapshot_formats)]; - -const struct object_id *cgit_snapshot_get_sig(const char *ref, - const struct cgit_snapshot_format *f) -{ - struct notes_tree *tree; - struct object_id oid; - - if (get_oid(ref, &oid)) - return NULL; - - tree = &snapshot_sig_notes[f - &cgit_snapshot_formats[0]]; - if (!tree->initialized) { - struct strbuf notes_ref = STRBUF_INIT; - - strbuf_addf(¬es_ref, "refs/notes/signatures/%s", - f->suffix + 1); - - init_notes(tree, notes_ref.buf, combine_notes_ignore, 0); - strbuf_release(¬es_ref); - } - - return get_note(tree, &oid); -} - -static const struct cgit_snapshot_format *get_format(const char *filename) -{ - const struct cgit_snapshot_format *fmt; - - for (fmt = cgit_snapshot_formats; fmt->suffix; fmt++) { - if (ends_with(filename, fmt->suffix)) - return fmt; - } - return NULL; -} - -const unsigned cgit_snapshot_format_bit(const struct cgit_snapshot_format *f) -{ - return BIT(f - &cgit_snapshot_formats[0]); -} - -static int make_snapshot(const struct cgit_snapshot_format *format, - const char *hex, const char *prefix, - const char *filename) -{ - struct object_id oid; - - if (get_oid(hex, &oid)) { - cgit_print_error_page(404, "Not found", - "Bad object id: %s", hex); - return 1; - } - if (!lookup_commit_reference(the_repository, &oid)) { - cgit_print_error_page(400, "Bad request", - "Not a commit reference: %s", hex); - return 1; - } - ctx.page.etag = oid_to_hex(&oid); - ctx.page.mimetype = xstrdup(format->mimetype); - ctx.page.filename = xstrdup(filename); - cgit_print_http_headers(); - init_archivers(); - format->write_func(hex, prefix); - return 0; -} - -static int write_sig(const struct cgit_snapshot_format *format, - const char *hex, const char *archive, - const char *filename) -{ - const struct object_id *note = cgit_snapshot_get_sig(hex, format); - enum object_type type; - unsigned long size; - char *buf; - - if (!note) { - cgit_print_error_page(404, "Not found", - "No signature for %s", archive); - return 0; - } - - buf = read_object_file(note, &type, &size); - if (!buf) { - cgit_print_error_page(404, "Not found", "Not found"); - return 0; - } - - html("X-Content-Type-Options: nosniff\n"); - html("Content-Security-Policy: default-src 'none'\n"); - ctx.page.etag = oid_to_hex(note); - ctx.page.mimetype = xstrdup("application/pgp-signature"); - ctx.page.filename = xstrdup(filename); - cgit_print_http_headers(); - - html_raw(buf, size); - free(buf); - return 0; -} - -/* Try to guess the requested revision from the requested snapshot name. - * First the format extension is stripped, e.g. "cgit-0.7.2.tar.gz" become - * "cgit-0.7.2". If this is a valid commit object name we've got a winner. - * Otherwise, if the snapshot name has a prefix matching the result from - * repo_basename(), we strip the basename and any following '-' and '_' - * characters ("cgit-0.7.2" -> "0.7.2") and check the resulting name once - * more. If this still isn't a valid commit object name, we check if pre- - * pending a 'v' or a 'V' to the remaining snapshot name ("0.7.2" -> - * "v0.7.2") gives us something valid. - */ -static const char *get_ref_from_filename(const struct cgit_repo *repo, - const char *filename, - const struct cgit_snapshot_format *format) -{ - const char *reponame; - struct object_id oid; - struct strbuf snapshot = STRBUF_INIT; - int result = 1; - - strbuf_addstr(&snapshot, filename); - strbuf_setlen(&snapshot, snapshot.len - strlen(format->suffix)); - - if (get_oid(snapshot.buf, &oid) == 0) - goto out; - - reponame = cgit_snapshot_prefix(repo); - if (starts_with(snapshot.buf, reponame)) { - const char *new_start = snapshot.buf; - new_start += strlen(reponame); - while (new_start && (*new_start == '-' || *new_start == '_')) - new_start++; - strbuf_splice(&snapshot, 0, new_start - snapshot.buf, "", 0); - } - - if (get_oid(snapshot.buf, &oid) == 0) - goto out; - - strbuf_insert(&snapshot, 0, "v", 1); - if (get_oid(snapshot.buf, &oid) == 0) - goto out; - - strbuf_splice(&snapshot, 0, 1, "V", 1); - if (get_oid(snapshot.buf, &oid) == 0) - goto out; - - result = 0; - strbuf_release(&snapshot); - -out: - return result ? strbuf_detach(&snapshot, NULL) : NULL; -} - -void cgit_print_snapshot(const char *head, const char *hex, - const char *filename, int dwim) -{ - const struct cgit_snapshot_format* f; - const char *sig_filename = NULL; - char *adj_filename = NULL; - char *prefix = NULL; - - if (!filename) { - cgit_print_error_page(400, "Bad request", - "No snapshot name specified"); - return; - } - - if (ends_with(filename, ".asc")) { - sig_filename = filename; - - /* Strip ".asc" from filename for common format processing */ - adj_filename = xstrdup(filename); - adj_filename[strlen(adj_filename) - 4] = '\0'; - filename = adj_filename; - } - - f = get_format(filename); - if (!f || (!sig_filename && !(ctx.repo->snapshots & cgit_snapshot_format_bit(f)))) { - cgit_print_error_page(400, "Bad request", - "Unsupported snapshot format: %s", filename); - return; - } - - if (!hex && dwim) { - hex = get_ref_from_filename(ctx.repo, filename, f); - if (hex == NULL) { - cgit_print_error_page(404, "Not found", "Not found"); - return; - } - prefix = xstrdup(filename); - prefix[strlen(filename) - strlen(f->suffix)] = '\0'; - } - - if (!hex) - hex = head; - - if (!prefix) - prefix = xstrdup(cgit_snapshot_prefix(ctx.repo)); - - if (sig_filename) - write_sig(f, hex, filename, sig_filename); - else - make_snapshot(f, hex, prefix, filename); - - free(prefix); - free(adj_filename); -} diff --git a/ui-snapshot.h b/ui-snapshot.h deleted file mode 100644 index a8deec3697..0000000000 --- a/ui-snapshot.h +++ /dev/null @@ -1,7 +0,0 @@ -#ifndef UI_SNAPSHOT_H -#define UI_SNAPSHOT_H - -extern void cgit_print_snapshot(const char *head, const char *hex, - const char *filename, int dwim); - -#endif /* UI_SNAPSHOT_H */ diff --git a/ui-ssdiff.c b/ui-ssdiff.c deleted file mode 100644 index af8bc9e065..0000000000 --- a/ui-ssdiff.c +++ /dev/null @@ -1,420 +0,0 @@ -#include "cgit.h" -#include "ui-ssdiff.h" -#include "html.h" -#include "ui-shared.h" -#include "ui-diff.h" - -extern int use_ssdiff; - -static int current_old_line, current_new_line; -static int **L = NULL; - -struct deferred_lines { - int line_no; - char *line; - struct deferred_lines *next; -}; - -static struct deferred_lines *deferred_old, *deferred_old_last; -static struct deferred_lines *deferred_new, *deferred_new_last; - -static void create_or_reset_lcs_table(void) -{ - int i; - - if (L != NULL) { - memset(*L, 0, sizeof(int) * MAX_SSDIFF_SIZE); - return; - } - - // xcalloc will die if we ran out of memory; - // not very helpful for debugging - L = (int**)xcalloc(MAX_SSDIFF_M, sizeof(int *)); - *L = (int*)xcalloc(MAX_SSDIFF_SIZE, sizeof(int)); - - for (i = 1; i < MAX_SSDIFF_M; i++) { - L[i] = *L + i * MAX_SSDIFF_N; - } -} - -static char *longest_common_subsequence(char *A, char *B) -{ - int i, j, ri; - int m = strlen(A); - int n = strlen(B); - int tmp1, tmp2; - int lcs_length; - char *result; - - // We bail if the lines are too long - if (m >= MAX_SSDIFF_M || n >= MAX_SSDIFF_N) - return NULL; - - create_or_reset_lcs_table(); - - for (i = m; i >= 0; i--) { - for (j = n; j >= 0; j--) { - if (A[i] == '\0' || B[j] == '\0') { - L[i][j] = 0; - } else if (A[i] == B[j]) { - L[i][j] = 1 + L[i + 1][j + 1]; - } else { - tmp1 = L[i + 1][j]; - tmp2 = L[i][j + 1]; - L[i][j] = (tmp1 > tmp2 ? tmp1 : tmp2); - } - } - } - - lcs_length = L[0][0]; - result = xmalloc(lcs_length + 2); - memset(result, 0, sizeof(*result) * (lcs_length + 2)); - - ri = 0; - i = 0; - j = 0; - while (i < m && j < n) { - if (A[i] == B[j]) { - result[ri] = A[i]; - ri += 1; - i += 1; - j += 1; - } else if (L[i + 1][j] >= L[i][j + 1]) { - i += 1; - } else { - j += 1; - } - } - - return result; -} - -static int line_from_hunk(char *line, char type) -{ - char *buf1, *buf2; - int len, res; - - buf1 = strchr(line, type); - if (buf1 == NULL) - return 0; - buf1 += 1; - buf2 = strchr(buf1, ','); - if (buf2 == NULL) - return 0; - len = buf2 - buf1; - buf2 = xmalloc(len + 1); - strlcpy(buf2, buf1, len + 1); - res = atoi(buf2); - free(buf2); - return res; -} - -static char *replace_tabs(char *line) -{ - char *prev_buf = line; - char *cur_buf; - size_t linelen = strlen(line); - int n_tabs = 0; - int i; - char *result; - size_t result_len; - - if (linelen == 0) { - result = xmalloc(1); - result[0] = '\0'; - return result; - } - - for (i = 0; i < linelen; i++) { - if (line[i] == '\t') - n_tabs += 1; - } - result_len = linelen + n_tabs * 8; - result = xmalloc(result_len + 1); - result[0] = '\0'; - - for (;;) { - cur_buf = strchr(prev_buf, '\t'); - if (!cur_buf) { - linelen = strlen(result); - strlcpy(&result[linelen], prev_buf, result_len - linelen + 1); - break; - } else { - linelen = strlen(result); - strlcpy(&result[linelen], prev_buf, cur_buf - prev_buf + 1); - linelen = strlen(result); - memset(&result[linelen], ' ', 8 - (linelen % 8)); - result[linelen + 8 - (linelen % 8)] = '\0'; - } - prev_buf = cur_buf + 1; - } - return result; -} - -static int calc_deferred_lines(struct deferred_lines *start) -{ - struct deferred_lines *item = start; - int result = 0; - while (item) { - result += 1; - item = item->next; - } - return result; -} - -static void deferred_old_add(char *line, int line_no) -{ - struct deferred_lines *item = xmalloc(sizeof(struct deferred_lines)); - item->line = xstrdup(line); - item->line_no = line_no; - item->next = NULL; - if (deferred_old) { - deferred_old_last->next = item; - deferred_old_last = item; - } else { - deferred_old = deferred_old_last = item; - } -} - -static void deferred_new_add(char *line, int line_no) -{ - struct deferred_lines *item = xmalloc(sizeof(struct deferred_lines)); - item->line = xstrdup(line); - item->line_no = line_no; - item->next = NULL; - if (deferred_new) { - deferred_new_last->next = item; - deferred_new_last = item; - } else { - deferred_new = deferred_new_last = item; - } -} - -static void print_part_with_lcs(char *class, char *line, char *lcs) -{ - int line_len = strlen(line); - int i, j; - char c[2] = " "; - int same = 1; - - j = 0; - for (i = 0; i < line_len; i++) { - c[0] = line[i]; - if (same) { - if (line[i] == lcs[j]) - j += 1; - else { - same = 0; - htmlf("<span class='%s'>", class); - } - } else if (line[i] == lcs[j]) { - same = 1; - html("</span>"); - j += 1; - } - html_txt(c); - } - if (!same) - html("</span>"); -} - -static void print_ssdiff_line(char *class, - int old_line_no, - char *old_line, - int new_line_no, - char *new_line, int individual_chars) -{ - char *lcs = NULL; - - if (old_line) - old_line = replace_tabs(old_line + 1); - if (new_line) - new_line = replace_tabs(new_line + 1); - if (individual_chars && old_line && new_line) - lcs = longest_common_subsequence(old_line, new_line); - html("<tr>\n"); - if (old_line_no > 0) { - struct diff_filespec *old_file = cgit_get_current_old_file(); - char *lineno_str = fmt("n%d", old_line_no); - char *id_str = fmt("id=%s#%s", is_null_oid(&old_file->oid)?"HEAD":oid_to_hex(old_rev_oid), lineno_str); - char *fileurl = cgit_fileurl(ctx.repo->url, "tree", old_file->path, id_str); - html("<td class='lineno'><a href='"); - html(fileurl); - htmlf("'>%s</a>", lineno_str + 1); - html("</td>"); - htmlf("<td class='%s'>", class); - free(fileurl); - } else if (old_line) - htmlf("<td class='lineno'></td><td class='%s'>", class); - else - htmlf("<td class='lineno'></td><td class='%s_dark'>", class); - if (old_line) { - if (lcs) - print_part_with_lcs("del", old_line, lcs); - else - html_txt(old_line); - } - - html("</td>\n"); - if (new_line_no > 0) { - struct diff_filespec *new_file = cgit_get_current_new_file(); - char *lineno_str = fmt("n%d", new_line_no); - char *id_str = fmt("id=%s#%s", is_null_oid(&new_file->oid)?"HEAD":oid_to_hex(new_rev_oid), lineno_str); - char *fileurl = cgit_fileurl(ctx.repo->url, "tree", new_file->path, id_str); - html("<td class='lineno'><a href='"); - html(fileurl); - htmlf("'>%s</a>", lineno_str + 1); - html("</td>"); - htmlf("<td class='%s'>", class); - free(fileurl); - } else if (new_line) - htmlf("<td class='lineno'></td><td class='%s'>", class); - else - htmlf("<td class='lineno'></td><td class='%s_dark'>", class); - if (new_line) { - if (lcs) - print_part_with_lcs("add", new_line, lcs); - else - html_txt(new_line); - } - - html("</td></tr>"); - if (lcs) - free(lcs); - if (new_line) - free(new_line); - if (old_line) - free(old_line); -} - -static void print_deferred_old_lines(void) -{ - struct deferred_lines *iter_old, *tmp; - iter_old = deferred_old; - while (iter_old) { - print_ssdiff_line("del", iter_old->line_no, - iter_old->line, -1, NULL, 0); - tmp = iter_old->next; - free(iter_old); - iter_old = tmp; - } -} - -static void print_deferred_new_lines(void) -{ - struct deferred_lines *iter_new, *tmp; - iter_new = deferred_new; - while (iter_new) { - print_ssdiff_line("add", -1, NULL, - iter_new->line_no, iter_new->line, 0); - tmp = iter_new->next; - free(iter_new); - iter_new = tmp; - } -} - -static void print_deferred_changed_lines(void) -{ - struct deferred_lines *iter_old, *iter_new, *tmp; - int n_old_lines = calc_deferred_lines(deferred_old); - int n_new_lines = calc_deferred_lines(deferred_new); - int individual_chars = (n_old_lines == n_new_lines ? 1 : 0); - - iter_old = deferred_old; - iter_new = deferred_new; - while (iter_old || iter_new) { - if (iter_old && iter_new) - print_ssdiff_line("changed", iter_old->line_no, - iter_old->line, - iter_new->line_no, iter_new->line, - individual_chars); - else if (iter_old) - print_ssdiff_line("changed", iter_old->line_no, - iter_old->line, -1, NULL, 0); - else if (iter_new) - print_ssdiff_line("changed", -1, NULL, - iter_new->line_no, iter_new->line, 0); - if (iter_old) { - tmp = iter_old->next; - free(iter_old); - iter_old = tmp; - } - - if (iter_new) { - tmp = iter_new->next; - free(iter_new); - iter_new = tmp; - } - } -} - -void cgit_ssdiff_print_deferred_lines(void) -{ - if (!deferred_old && !deferred_new) - return; - if (deferred_old && !deferred_new) - print_deferred_old_lines(); - else if (!deferred_old && deferred_new) - print_deferred_new_lines(); - else - print_deferred_changed_lines(); - deferred_old = deferred_old_last = NULL; - deferred_new = deferred_new_last = NULL; -} - -/* - * print a single line returned from xdiff - */ -void cgit_ssdiff_line_cb(char *line, int len) -{ - char c = line[len - 1]; - line[len - 1] = '\0'; - if (line[0] == '@') { - current_old_line = line_from_hunk(line, '-'); - current_new_line = line_from_hunk(line, '+'); - } - - if (line[0] == ' ') { - if (deferred_old || deferred_new) - cgit_ssdiff_print_deferred_lines(); - print_ssdiff_line("ctx", current_old_line, line, - current_new_line, line, 0); - current_old_line += 1; - current_new_line += 1; - } else if (line[0] == '+') { - deferred_new_add(line, current_new_line); - current_new_line += 1; - } else if (line[0] == '-') { - deferred_old_add(line, current_old_line); - current_old_line += 1; - } else if (line[0] == '@') { - html("<tr><td colspan='4' class='hunk'>"); - html_txt(line); - html("</td></tr>"); - } else { - html("<tr><td colspan='4' class='ctx'>"); - html_txt(line); - html("</td></tr>"); - } - line[len - 1] = c; -} - -void cgit_ssdiff_header_begin(void) -{ - current_old_line = -1; - current_new_line = -1; - html("<tr><td class='space' colspan='4'><div></div></td></tr>"); - html("<tr><td class='head' colspan='4'>"); -} - -void cgit_ssdiff_header_end(void) -{ - html("</td></tr>"); -} - -void cgit_ssdiff_footer(void) -{ - if (deferred_old || deferred_new) - cgit_ssdiff_print_deferred_lines(); - html("<tr><td class='foot' colspan='4'></td></tr>"); -} diff --git a/ui-ssdiff.h b/ui-ssdiff.h deleted file mode 100644 index 11f2714407..0000000000 --- a/ui-ssdiff.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef UI_SSDIFF_H -#define UI_SSDIFF_H - -/* - * ssdiff line limits - */ -#ifndef MAX_SSDIFF_M -#define MAX_SSDIFF_M 128 -#endif - -#ifndef MAX_SSDIFF_N -#define MAX_SSDIFF_N 128 -#endif -#define MAX_SSDIFF_SIZE ((MAX_SSDIFF_M) * (MAX_SSDIFF_N)) - -extern void cgit_ssdiff_print_deferred_lines(void); - -extern void cgit_ssdiff_line_cb(char *line, int len); - -extern void cgit_ssdiff_header_begin(void); -extern void cgit_ssdiff_header_end(void); - -extern void cgit_ssdiff_footer(void); - -#endif /* UI_SSDIFF_H */ diff --git a/ui-stats.c b/ui-stats.c deleted file mode 100644 index 7272a61a2f..0000000000 --- a/ui-stats.c +++ /dev/null @@ -1,426 +0,0 @@ -#include "cgit.h" -#include "ui-stats.h" -#include "html.h" -#include "ui-shared.h" - -struct authorstat { - long total; - struct string_list list; -}; - -#define DAY_SECS (60 * 60 * 24) -#define WEEK_SECS (DAY_SECS * 7) - -static void trunc_week(struct tm *tm) -{ - time_t t = timegm(tm); - t -= ((tm->tm_wday + 6) % 7) * DAY_SECS; - gmtime_r(&t, tm); -} - -static void dec_week(struct tm *tm) -{ - time_t t = timegm(tm); - t -= WEEK_SECS; - gmtime_r(&t, tm); -} - -static void inc_week(struct tm *tm) -{ - time_t t = timegm(tm); - t += WEEK_SECS; - gmtime_r(&t, tm); -} - -static char *pretty_week(struct tm *tm) -{ - static char buf[10]; - - strftime(buf, sizeof(buf), "W%V %G", tm); - return buf; -} - -static void trunc_month(struct tm *tm) -{ - tm->tm_mday = 1; -} - -static void dec_month(struct tm *tm) -{ - tm->tm_mon--; - if (tm->tm_mon < 0) { - tm->tm_year--; - tm->tm_mon = 11; - } -} - -static void inc_month(struct tm *tm) -{ - tm->tm_mon++; - if (tm->tm_mon > 11) { - tm->tm_year++; - tm->tm_mon = 0; - } -} - -static char *pretty_month(struct tm *tm) -{ - static const char *months[] = { - "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" - }; - return fmt("%s %d", months[tm->tm_mon], tm->tm_year + 1900); -} - -static void trunc_quarter(struct tm *tm) -{ - trunc_month(tm); - while (tm->tm_mon % 3 != 0) - dec_month(tm); -} - -static void dec_quarter(struct tm *tm) -{ - dec_month(tm); - dec_month(tm); - dec_month(tm); -} - -static void inc_quarter(struct tm *tm) -{ - inc_month(tm); - inc_month(tm); - inc_month(tm); -} - -static char *pretty_quarter(struct tm *tm) -{ - return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900); -} - -static void trunc_year(struct tm *tm) -{ - trunc_month(tm); - tm->tm_mon = 0; -} - -static void dec_year(struct tm *tm) -{ - tm->tm_year--; -} - -static void inc_year(struct tm *tm) -{ - tm->tm_year++; -} - -static char *pretty_year(struct tm *tm) -{ - return fmt("%d", tm->tm_year + 1900); -} - -static const struct cgit_period periods[] = { - {'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week}, - {'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month}, - {'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter}, - {'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year}, -}; - -/* Given a period code or name, return a period index (1, 2, 3 or 4) - * and update the period pointer to the correcsponding struct. - * If no matching code is found, return 0. - */ -int cgit_find_stats_period(const char *expr, const struct cgit_period **period) -{ - int i; - char code = '\0'; - - if (!expr) - return 0; - - if (strlen(expr) == 1) - code = expr[0]; - - for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++) - if (periods[i].code == code || !strcmp(periods[i].name, expr)) { - if (period) - *period = &periods[i]; - return i + 1; - } - return 0; -} - -const char *cgit_find_stats_periodname(int idx) -{ - if (idx > 0 && idx < 4) - return periods[idx - 1].name; - else - return ""; -} - -static void add_commit(struct string_list *authors, struct commit *commit, - const struct cgit_period *period) -{ - struct commitinfo *info; - struct string_list_item *author, *item; - struct authorstat *authorstat; - struct string_list *items; - char *tmp; - struct tm *date; - time_t t; - uintptr_t *counter; - - info = cgit_parse_commit(commit); - tmp = xstrdup(info->author); - author = string_list_insert(authors, tmp); - if (!author->util) - author->util = xcalloc(1, sizeof(struct authorstat)); - else - free(tmp); - authorstat = author->util; - items = &authorstat->list; - t = info->committer_date; - date = gmtime(&t); - period->trunc(date); - tmp = xstrdup(period->pretty(date)); - item = string_list_insert(items, tmp); - counter = (uintptr_t *)&item->util; - if (*counter) - free(tmp); - (*counter)++; - - authorstat->total++; - cgit_free_commitinfo(info); -} - -static int cmp_total_commits(const void *a1, const void *a2) -{ - const struct string_list_item *i1 = a1; - const struct string_list_item *i2 = a2; - const struct authorstat *auth1 = i1->util; - const struct authorstat *auth2 = i2->util; - - return auth2->total - auth1->total; -} - -/* Walk the commit DAG and collect number of commits per author per - * timeperiod into a nested string_list collection. - */ -static struct string_list collect_stats(const struct cgit_period *period) -{ - struct string_list authors; - struct rev_info rev; - struct commit *commit; - const char *argv[] = {NULL, ctx.qry.head, NULL, NULL, NULL, NULL}; - int argc = 3; - time_t now; - long i; - struct tm *tm; - char tmp[11]; - - time(&now); - tm = gmtime(&now); - period->trunc(tm); - for (i = 1; i < period->count; i++) - period->dec(tm); - strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm); - argv[2] = xstrdup(fmt("--since=%s", tmp)); - if (ctx.qry.path) { - argv[3] = "--"; - argv[4] = ctx.qry.path; - argc += 2; - } - init_revisions(&rev, NULL); - rev.abbrev = DEFAULT_ABBREV; - rev.commit_format = CMIT_FMT_DEFAULT; - rev.max_parents = 1; - rev.verbose_header = 1; - rev.show_root_diff = 0; - setup_revisions(argc, argv, &rev, NULL); - prepare_revision_walk(&rev); - memset(&authors, 0, sizeof(authors)); - while ((commit = get_revision(&rev)) != NULL) { - add_commit(&authors, commit, period); - free_commit_buffer(the_repository->parsed_objects, commit); - free_commit_list(commit->parents); - commit->parents = NULL; - } - return authors; -} - -static void print_combined_authorrow(struct string_list *authors, int from, - int to, const char *name, - const char *leftclass, - const char *centerclass, - const char *rightclass, - const struct cgit_period *period) -{ - struct string_list_item *author; - struct authorstat *authorstat; - struct string_list *items; - struct string_list_item *date; - time_t now; - long i, j, total, subtotal; - struct tm *tm; - char *tmp; - - time(&now); - tm = gmtime(&now); - period->trunc(tm); - for (i = 1; i < period->count; i++) - period->dec(tm); - - total = 0; - htmlf("<tr><td class='%s'>%s</td>", leftclass, - fmt(name, to - from + 1)); - for (j = 0; j < period->count; j++) { - tmp = period->pretty(tm); - period->inc(tm); - subtotal = 0; - for (i = from; i <= to; i++) { - author = &authors->items[i]; - authorstat = author->util; - items = &authorstat->list; - date = string_list_lookup(items, tmp); - if (date) - subtotal += (uintptr_t)date->util; - } - htmlf("<td class='%s'>%ld</td>", centerclass, subtotal); - total += subtotal; - } - htmlf("<td class='%s'>%ld</td></tr>", rightclass, total); -} - -static void print_authors(struct string_list *authors, int top, - const struct cgit_period *period) -{ - struct string_list_item *author; - struct authorstat *authorstat; - struct string_list *items; - struct string_list_item *date; - time_t now; - long i, j, total; - struct tm *tm; - char *tmp; - - time(&now); - tm = gmtime(&now); - period->trunc(tm); - for (i = 1; i < period->count; i++) - period->dec(tm); - - html("<table class='stats'><tr><th>Author</th>"); - for (j = 0; j < period->count; j++) { - tmp = period->pretty(tm); - htmlf("<th>%s</th>", tmp); - period->inc(tm); - } - html("<th>Total</th></tr>\n"); - - if (top <= 0 || top > authors->nr) - top = authors->nr; - - for (i = 0; i < top; i++) { - author = &authors->items[i]; - html("<tr><td class='left'>"); - html_txt(author->string); - html("</td>"); - authorstat = author->util; - items = &authorstat->list; - total = 0; - for (j = 0; j < period->count; j++) - period->dec(tm); - for (j = 0; j < period->count; j++) { - tmp = period->pretty(tm); - period->inc(tm); - date = string_list_lookup(items, tmp); - if (!date) - html("<td>0</td>"); - else { - htmlf("<td>%lu</td>", (uintptr_t)date->util); - total += (uintptr_t)date->util; - } - } - htmlf("<td class='sum'>%ld</td></tr>", total); - } - - if (top < authors->nr) - print_combined_authorrow(authors, top, authors->nr - 1, - "Others (%ld)", "left", "", "sum", period); - - print_combined_authorrow(authors, 0, authors->nr - 1, "Total", - "total", "sum", "sum", period); - html("</table>"); -} - -/* Create a sorted string_list with one entry per author. The util-field - * for each author is another string_list which is used to calculate the - * number of commits per time-interval. - */ -void cgit_show_stats(void) -{ - struct string_list authors; - const struct cgit_period *period; - int top, i; - const char *code = "w"; - - if (ctx.qry.period) - code = ctx.qry.period; - - i = cgit_find_stats_period(code, &period); - if (!i) { - cgit_print_error_page(404, "Not found", - "Unknown statistics type: %c", code[0]); - return; - } - if (i > ctx.repo->max_stats) { - cgit_print_error_page(400, "Bad request", - "Statistics type disabled: %s", period->name); - return; - } - authors = collect_stats(period); - qsort(authors.items, authors.nr, sizeof(struct string_list_item), - cmp_total_commits); - - top = ctx.qry.ofs; - if (!top) - top = 10; - - cgit_print_layout_start(); - html("<div class='cgit-panel'>"); - html("<b>stat options</b>"); - html("<form method='get'>"); - cgit_add_hidden_formfields(1, 0, "stats"); - html("<table><tr><td colspan='2'/></tr>"); - if (ctx.repo->max_stats > 1) { - html("<tr><td class='label'>Period:</td>"); - html("<td class='ctrl'><select name='period' onchange='this.form.submit();'>"); - for (i = 0; i < ctx.repo->max_stats; i++) - html_option(fmt("%c", periods[i].code), - periods[i].name, fmt("%c", period->code)); - html("</select></td></tr>"); - } - html("<tr><td class='label'>Authors:</td>"); - html("<td class='ctrl'><select name='ofs' onchange='this.form.submit();'>"); - html_intoption(10, "10", top); - html_intoption(25, "25", top); - html_intoption(50, "50", top); - html_intoption(100, "100", top); - html_intoption(-1, "all", top); - html("</select></td></tr>"); - html("<tr><td/><td class='ctrl'>"); - html("<noscript><input type='submit' value='Reload'/></noscript>"); - html("</td></tr></table>"); - html("</form>"); - html("</div>"); - htmlf("<h2>Commits per author per %s", period->name); - if (ctx.qry.path) { - html(" (path '"); - html_txt(ctx.qry.path); - html("')"); - } - html("</h2>"); - print_authors(&authors, top, period); - cgit_print_layout_end(); -} - diff --git a/ui-stats.h b/ui-stats.h deleted file mode 100644 index 0e61b03da3..0000000000 --- a/ui-stats.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef UI_STATS_H -#define UI_STATS_H - -#include "cgit.h" - -struct cgit_period { - const char code; - const char *name; - int max_periods; - int count; - - /* Convert a tm value to the first day in the period */ - void (*trunc)(struct tm *tm); - - /* Update tm value to start of next/previous period */ - void (*dec)(struct tm *tm); - void (*inc)(struct tm *tm); - - /* Pretty-print a tm value */ - char *(*pretty)(struct tm *tm); -}; - -extern int cgit_find_stats_period(const char *expr, const struct cgit_period **period); -extern const char *cgit_find_stats_periodname(int idx); - -extern void cgit_show_stats(void); - -#endif /* UI_STATS_H */ diff --git a/ui-summary.c b/ui-summary.c deleted file mode 100644 index 947812a814..0000000000 --- a/ui-summary.c +++ /dev/null @@ -1,148 +0,0 @@ -/* ui-summary.c: functions for generating repo summary page - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-summary.h" -#include "html.h" -#include "ui-blob.h" -#include "ui-log.h" -#include "ui-plain.h" -#include "ui-refs.h" -#include "ui-shared.h" - -static int urls; - -static void print_url(const char *url) -{ - int columns = 3; - - if (ctx.repo->enable_log_filecount) - columns++; - if (ctx.repo->enable_log_linecount) - columns++; - - if (urls++ == 0) { - htmlf("<tr class='nohover'><td colspan='%d'> </td></tr>", columns); - htmlf("<tr class='nohover'><th class='left' colspan='%d'>Clone</th></tr>\n", columns); - } - - htmlf("<tr><td colspan='%d'><a rel='vcs-git' href='", columns); - html_url_path(url); - html("' title='"); - html_attr(ctx.repo->name); - html(" Git repository'>"); - html_txt(url); - html("</a></td></tr>\n"); -} - -void cgit_print_summary(void) -{ - int columns = 3; - - if (ctx.repo->enable_log_filecount) - columns++; - if (ctx.repo->enable_log_linecount) - columns++; - - cgit_print_layout_start(); - html("<table summary='repository info' class='list nowrap'>"); - cgit_print_branches(ctx.cfg.summary_branches); - htmlf("<tr class='nohover'><td colspan='%d'> </td></tr>", columns); - cgit_print_tags(ctx.cfg.summary_tags); - if (ctx.cfg.summary_log > 0) { - htmlf("<tr class='nohover'><td colspan='%d'> </td></tr>", columns); - cgit_print_log(ctx.qry.head, 0, ctx.cfg.summary_log, NULL, - NULL, NULL, 0, 0, 0); - } - urls = 0; - cgit_add_clone_urls(print_url); - html("</table>"); - cgit_print_layout_end(); -} - -/* The caller must free the return value. */ -static char* append_readme_path(const char *filename, const char *ref, const char *path) -{ - char *file, *base_dir, *full_path, *resolved_base = NULL, *resolved_full = NULL; - /* If a subpath is specified for the about page, make it relative - * to the directory containing the configured readme. */ - - file = xstrdup(filename); - base_dir = dirname(file); - if (!strcmp(base_dir, ".") || !strcmp(base_dir, "..")) { - if (!ref) { - free(file); - return NULL; - } - full_path = xstrdup(path); - } else - full_path = fmtalloc("%s/%s", base_dir, path); - - if (!ref) { - resolved_base = realpath(base_dir, NULL); - resolved_full = realpath(full_path, NULL); - if (!resolved_base || !resolved_full || !starts_with(resolved_full, resolved_base)) { - free(full_path); - full_path = NULL; - } - } - - free(file); - free(resolved_base); - free(resolved_full); - - return full_path; -} - -void cgit_print_repo_readme(const char *path) -{ - char *filename, *ref, *mimetype; - int free_filename = 0; - - mimetype = get_mimetype_for_filename(path); - if (mimetype && (!strncmp(mimetype, "image/", 6) || !strncmp(mimetype, "video/", 6))) { - ctx.page.mimetype = mimetype; - ctx.page.charset = NULL; - cgit_print_plain(); - free(mimetype); - return; - } - free(mimetype); - - cgit_print_layout_start(); - if (ctx.repo->readme.nr == 0) - goto done; - - filename = ctx.repo->readme.items[0].string; - ref = ctx.repo->readme.items[0].util; - - if (path) { - free_filename = 1; - filename = append_readme_path(filename, ref, path); - if (!filename) - goto done; - } - - /* Print the calculated readme, either from the git repo or from the - * filesystem, while applying the about-filter. - */ - html("<div id='summary'>"); - cgit_open_filter(ctx.repo->about_filter, filename); - if (ref) - cgit_print_file(filename, ref, 1); - else - html_include(filename); - cgit_close_filter(ctx.repo->about_filter); - - html("</div>"); - if (free_filename) - free(filename); - -done: - cgit_print_layout_end(); -} diff --git a/ui-summary.h b/ui-summary.h deleted file mode 100644 index cba696af53..0000000000 --- a/ui-summary.h +++ /dev/null @@ -1,7 +0,0 @@ -#ifndef UI_SUMMARY_H -#define UI_SUMMARY_H - -extern void cgit_print_summary(void); -extern void cgit_print_repo_readme(const char *path); - -#endif /* UI_SUMMARY_H */ diff --git a/ui-tag.c b/ui-tag.c deleted file mode 100644 index 846d5b141f..0000000000 --- a/ui-tag.c +++ /dev/null @@ -1,120 +0,0 @@ -/* ui-tag.c: display a tag - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-tag.h" -#include "html.h" -#include "ui-shared.h" - -static void print_tag_content(char *buf) -{ - char *p; - - if (!buf) - return; - - html("<div class='commit-subject'>"); - p = strchr(buf, '\n'); - if (p) - *p = '\0'; - html_txt(buf); - html("</div>"); - if (p) { - html("<div class='commit-msg'>"); - html_txt(++p); - html("</div>"); - } -} - -static void print_download_links(char *revname) -{ - html("<tr><th>download</th><td class='sha1'>"); - cgit_print_snapshot_links(ctx.repo, revname, "<br/>"); - html("</td></tr>"); -} - -void cgit_print_tag(char *revname) -{ - struct strbuf fullref = STRBUF_INIT; - struct object_id oid; - struct object *obj; - - if (!revname) - revname = ctx.qry.head; - - strbuf_addf(&fullref, "refs/tags/%s", revname); - if (get_oid(fullref.buf, &oid)) { - cgit_print_error_page(404, "Not found", - "Bad tag reference: %s", revname); - goto cleanup; - } - obj = parse_object(the_repository, &oid); - if (!obj) { - cgit_print_error_page(500, "Internal server error", - "Bad object id: %s", oid_to_hex(&oid)); - goto cleanup; - } - if (obj->type == OBJ_TAG) { - struct tag *tag; - struct taginfo *info; - - tag = lookup_tag(the_repository, &oid); - if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) { - cgit_print_error_page(500, "Internal server error", - "Bad tag object: %s", revname); - goto cleanup; - } - cgit_print_layout_start(); - html("<table class='commit-info'>\n"); - html("<tr><td>tag name</td><td>"); - html_txt(revname); - htmlf(" (%s)</td></tr>\n", oid_to_hex(&oid)); - if (info->tagger_date > 0) { - html("<tr><td>tag date</td><td>"); - html_txt(show_date(info->tagger_date, info->tagger_tz, - cgit_date_mode(DATE_ISO8601))); - html("</td></tr>\n"); - } - if (info->tagger) { - html("<tr><td>tagged by</td><td>"); - cgit_open_filter(ctx.repo->email_filter, info->tagger_email, "tag"); - html_txt(info->tagger); - if (info->tagger_email && !ctx.cfg.noplainemail) { - html(" "); - html_txt(info->tagger_email); - } - cgit_close_filter(ctx.repo->email_filter); - html("</td></tr>\n"); - } - html("<tr><td>tagged object</td><td class='sha1'>"); - cgit_object_link(tag->tagged); - html("</td></tr>\n"); - if (ctx.repo->snapshots) - print_download_links(revname); - html("</table>\n"); - print_tag_content(info->msg); - cgit_print_layout_end(); - cgit_free_taginfo(info); - } else { - cgit_print_layout_start(); - html("<table class='commit-info'>\n"); - html("<tr><td>tag name</td><td>"); - html_txt(revname); - html("</td></tr>\n"); - html("<tr><td>tagged object</td><td class='sha1'>"); - cgit_object_link(obj); - html("</td></tr>\n"); - if (ctx.repo->snapshots) - print_download_links(revname); - html("</table>\n"); - cgit_print_layout_end(); - } - -cleanup: - strbuf_release(&fullref); -} diff --git a/ui-tag.h b/ui-tag.h deleted file mode 100644 index d295cdcdbd..0000000000 --- a/ui-tag.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef UI_TAG_H -#define UI_TAG_H - -extern void cgit_print_tag(char *revname); - -#endif /* UI_TAG_H */ diff --git a/ui-tree.c b/ui-tree.c deleted file mode 100644 index 84eb17d647..0000000000 --- a/ui-tree.c +++ /dev/null @@ -1,388 +0,0 @@ -/* ui-tree.c: functions for tree output - * - * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - */ - -#include "cgit.h" -#include "ui-tree.h" -#include "html.h" -#include "ui-shared.h" - -struct walk_tree_context { - char *curr_rev; - char *match_path; - int state; -}; - -static void print_text_buffer(const char *name, char *buf, unsigned long size) -{ - unsigned long lineno, idx; - const char *numberfmt = "<a id='n%1$d' href='#n%1$d'>%1$d</a>\n"; - - html("<table summary='blob content' class='blob'>\n"); - - if (ctx.cfg.enable_tree_linenumbers) { - html("<tr><td class='linenumbers'><pre>"); - idx = 0; - lineno = 0; - - if (size) { - htmlf(numberfmt, ++lineno); - while (idx < size - 1) { // skip absolute last newline - if (buf[idx] == '\n') - htmlf(numberfmt, ++lineno); - idx++; - } - } - html("</pre></td>\n"); - } - else { - html("<tr>\n"); - } - - if (ctx.repo->source_filter) { - char *filter_arg = xstrdup(name); - html("<td class='lines'><pre><code>"); - cgit_open_filter(ctx.repo->source_filter, filter_arg); - html_raw(buf, size); - cgit_close_filter(ctx.repo->source_filter); - free(filter_arg); - html("</code></pre></td></tr></table>\n"); - return; - } - - html("<td class='lines'><pre><code>"); - html_txt(buf); - html("</code></pre></td></tr></table>\n"); -} - -#define ROWLEN 32 - -static void print_binary_buffer(char *buf, unsigned long size) -{ - unsigned long ofs, idx; - static char ascii[ROWLEN + 1]; - - html("<table summary='blob content' class='bin-blob'>\n"); - html("<tr><th>ofs</th><th>hex dump</th><th>ascii</th></tr>"); - for (ofs = 0; ofs < size; ofs += ROWLEN, buf += ROWLEN) { - htmlf("<tr><td class='right'>%04lx</td><td class='hex'>", ofs); - for (idx = 0; idx < ROWLEN && ofs + idx < size; idx++) - htmlf("%*s%02x", - idx == 16 ? 4 : 1, "", - buf[idx] & 0xff); - html(" </td><td class='hex'>"); - for (idx = 0; idx < ROWLEN && ofs + idx < size; idx++) - ascii[idx] = isgraph(buf[idx]) ? buf[idx] : '.'; - ascii[idx] = '\0'; - html_txt(ascii); - html("</td></tr>\n"); - } - html("</table>\n"); -} - -static void print_object(const struct object_id *oid, const char *path, const char *basename, const char *rev) -{ - enum object_type type; - char *buf; - unsigned long size; - - type = oid_object_info(the_repository, oid, &size); - if (type == OBJ_BAD) { - cgit_print_error_page(404, "Not found", - "Bad object name: %s", oid_to_hex(oid)); - return; - } - - buf = read_object_file(oid, &type, &size); - if (!buf) { - cgit_print_error_page(500, "Internal server error", - "Error reading object %s", oid_to_hex(oid)); - return; - } - - cgit_set_title_from_path(path); - - cgit_print_layout_start(); - htmlf("blob: %s (", oid_to_hex(oid)); - cgit_plain_link("plain", NULL, NULL, ctx.qry.head, - rev, path); - if (ctx.repo->enable_blame) { - html(") ("); - cgit_blame_link("blame", NULL, NULL, ctx.qry.head, - rev, path); - } - html(")\n"); - - if (ctx.cfg.max_blob_size && size / 1024 > ctx.cfg.max_blob_size) { - htmlf("<div class='error'>blob size (%ldKB) exceeds display size limit (%dKB).</div>", - size / 1024, ctx.cfg.max_blob_size); - return; - } - - if (buffer_is_binary(buf, size)) - print_binary_buffer(buf, size); - else - print_text_buffer(basename, buf, size); - - free(buf); -} - -struct single_tree_ctx { - struct strbuf *path; - struct object_id oid; - char *name; - size_t count; -}; - -static int single_tree_cb(const struct object_id *oid, struct strbuf *base, - const char *pathname, unsigned mode, int stage, - void *cbdata) -{ - struct single_tree_ctx *ctx = cbdata; - - if (++ctx->count > 1) - return -1; - - if (!S_ISDIR(mode)) { - ctx->count = 2; - return -1; - } - - ctx->name = xstrdup(pathname); - oidcpy(&ctx->oid, oid); - strbuf_addf(ctx->path, "/%s", pathname); - return 0; -} - -static void write_tree_link(const struct object_id *oid, char *name, - char *rev, struct strbuf *fullpath) -{ - size_t initial_length = fullpath->len; - struct tree *tree; - struct single_tree_ctx tree_ctx = { - .path = fullpath, - .count = 1, - }; - struct pathspec paths = { - .nr = 0 - }; - - oidcpy(&tree_ctx.oid, oid); - - while (tree_ctx.count == 1) { - cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head, rev, - fullpath->buf); - - tree = lookup_tree(the_repository, &tree_ctx.oid); - if (!tree) - return; - - free(tree_ctx.name); - tree_ctx.name = NULL; - tree_ctx.count = 0; - - read_tree_recursive(the_repository, tree, "", 0, 1, - &paths, single_tree_cb, &tree_ctx); - - if (tree_ctx.count != 1) - break; - - html(" / "); - name = tree_ctx.name; - } - - strbuf_setlen(fullpath, initial_length); -} - -static int ls_item(const struct object_id *oid, struct strbuf *base, - const char *pathname, unsigned mode, int stage, void *cbdata) -{ - struct walk_tree_context *walk_tree_ctx = cbdata; - char *name; - struct strbuf fullpath = STRBUF_INIT; - struct strbuf class = STRBUF_INIT; - enum object_type type; - unsigned long size = 0; - - name = xstrdup(pathname); - strbuf_addf(&fullpath, "%s%s%s", ctx.qry.path ? ctx.qry.path : "", - ctx.qry.path ? "/" : "", name); - - if (!S_ISGITLINK(mode)) { - type = oid_object_info(the_repository, oid, &size); - if (type == OBJ_BAD) { - htmlf("<tr><td colspan='3'>Bad object: %s %s</td></tr>", - name, - oid_to_hex(oid)); - free(name); - return 0; - } - } - - html("<tr><td class='ls-mode'>"); - cgit_print_filemode(mode); - html("</td><td>"); - if (S_ISGITLINK(mode)) { - cgit_submodule_link("ls-mod", fullpath.buf, oid_to_hex(oid)); - } else if (S_ISDIR(mode)) { - write_tree_link(oid, name, walk_tree_ctx->curr_rev, - &fullpath); - } else { - char *ext = strrchr(name, '.'); - strbuf_addstr(&class, "ls-blob"); - if (ext) - strbuf_addf(&class, " %s", ext + 1); - cgit_tree_link(name, NULL, class.buf, ctx.qry.head, - walk_tree_ctx->curr_rev, fullpath.buf); - } - htmlf("</td><td class='ls-size'>%li</td>", size); - - html("<td>"); - cgit_log_link("log", NULL, "button", ctx.qry.head, - walk_tree_ctx->curr_rev, fullpath.buf, 0, NULL, NULL, - ctx.qry.showmsg, 0); - if (ctx.repo->max_stats) - cgit_stats_link("stats", NULL, "button", ctx.qry.head, - fullpath.buf); - if (!S_ISGITLINK(mode)) - cgit_plain_link("plain", NULL, "button", ctx.qry.head, - walk_tree_ctx->curr_rev, fullpath.buf); - if (!S_ISDIR(mode) && ctx.repo->enable_blame) - cgit_blame_link("blame", NULL, "button", ctx.qry.head, - walk_tree_ctx->curr_rev, fullpath.buf); - html("</td></tr>\n"); - free(name); - strbuf_release(&fullpath); - strbuf_release(&class); - return 0; -} - -static void ls_head(void) -{ - cgit_print_layout_start(); - html("<table summary='tree listing' class='list'>\n"); - html("<tr class='nohover'>"); - html("<th class='left'>Mode</th>"); - html("<th class='left'>Name</th>"); - html("<th class='right'>Size</th>"); - html("<th/>"); - html("</tr>\n"); -} - -static void ls_tail(void) -{ - html("</table>\n"); - cgit_print_layout_end(); -} - -static void ls_tree(const struct object_id *oid, const char *path, struct walk_tree_context *walk_tree_ctx) -{ - struct tree *tree; - struct pathspec paths = { - .nr = 0 - }; - - tree = parse_tree_indirect(oid); - if (!tree) { - cgit_print_error_page(404, "Not found", - "Not a tree object: %s", oid_to_hex(oid)); - return; - } - - ls_head(); - read_tree_recursive(the_repository, tree, "", 0, 1, - &paths, ls_item, walk_tree_ctx); - ls_tail(); -} - - -static int walk_tree(const struct object_id *oid, struct strbuf *base, - const char *pathname, unsigned mode, int stage, void *cbdata) -{ - struct walk_tree_context *walk_tree_ctx = cbdata; - - if (walk_tree_ctx->state == 0) { - struct strbuf buffer = STRBUF_INIT; - - strbuf_addbuf(&buffer, base); - strbuf_addstr(&buffer, pathname); - if (strcmp(walk_tree_ctx->match_path, buffer.buf)) - return READ_TREE_RECURSIVE; - - if (S_ISDIR(mode)) { - walk_tree_ctx->state = 1; - cgit_set_title_from_path(buffer.buf); - strbuf_release(&buffer); - ls_head(); - return READ_TREE_RECURSIVE; - } else { - walk_tree_ctx->state = 2; - print_object(oid, buffer.buf, pathname, walk_tree_ctx->curr_rev); - strbuf_release(&buffer); - return 0; - } - } - ls_item(oid, base, pathname, mode, stage, walk_tree_ctx); - return 0; -} - -/* - * Show a tree or a blob - * rev: the commit pointing at the root tree object - * path: path to tree or blob - */ -void cgit_print_tree(const char *rev, char *path) -{ - struct object_id oid; - struct commit *commit; - struct pathspec_item path_items = { - .match = path, - .len = path ? strlen(path) : 0 - }; - struct pathspec paths = { - .nr = path ? 1 : 0, - .items = &path_items - }; - struct walk_tree_context walk_tree_ctx = { - .match_path = path, - .state = 0 - }; - - if (!rev) - rev = ctx.qry.head; - - if (get_oid(rev, &oid)) { - cgit_print_error_page(404, "Not found", - "Invalid revision name: %s", rev); - return; - } - commit = lookup_commit_reference(the_repository, &oid); - if (!commit || parse_commit(commit)) { - cgit_print_error_page(404, "Not found", - "Invalid commit reference: %s", rev); - return; - } - - walk_tree_ctx.curr_rev = xstrdup(rev); - - if (path == NULL) { - ls_tree(&commit->maybe_tree->object.oid, NULL, &walk_tree_ctx); - goto cleanup; - } - - read_tree_recursive(the_repository, commit->maybe_tree, "", 0, 0, - &paths, walk_tree, &walk_tree_ctx); - if (walk_tree_ctx.state == 1) - ls_tail(); - else if (walk_tree_ctx.state == 2) - cgit_print_layout_end(); - else - cgit_print_error_page(404, "Not found", "Path not found"); - -cleanup: - free(walk_tree_ctx.curr_rev); -} diff --git a/ui-tree.h b/ui-tree.h deleted file mode 100644 index bbd34e3566..0000000000 --- a/ui-tree.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef UI_TREE_H -#define UI_TREE_H - -extern void cgit_print_tree(const char *rev, char *path); - -#endif /* UI_TREE_H */ diff --git a/xinitrc b/xinitrc new file mode 100644 index 0000000000..591e419914 --- /dev/null +++ b/xinitrc @@ -0,0 +1,20 @@ +# Disable access control for the current user. +xhost +SI:localuser:$USER + +# Make Java applications aware this is a non-reparenting window manager. +export _JAVA_AWT_WM_NONREPARENTING=1 + +# Set default cursor. +xsetroot -cursor_name left_ptr + +# Set keyboard repeat rate. +xset r rate 200 60 + +# Uncomment the following block to use the exwm-xim module. +#export XMODIFIERS=@im=exwm-xim +#export GTK_IM_MODULE=xim +#export QT_IM_MODULE=xim +#export CLUTTER_IM_MODULE=xim + +# Finally start Emacs +exec emacs |