diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/README.md b/README.md index 7191c35..d28e5a5 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# GreenshotMac +# Snaggle A native, Apple-silicon macOS screenshot tool modelled on [Greenshot](https://github.com/greenshot/greenshot) for Windows. Written in Swift with AppKit, SwiftUI and ScreenCaptureKit — no Mono, no Wine, no .NET runtime. The original C# sources are kept in `reference-greenshot/` purely as the behavioural reference -for the port. GreenshotMac is a rewrite, not a transpile: Greenshot's Win32 foundations +for the port. Snaggle is a rewrite, not a transpile: Greenshot's Win32 foundations (GDI+, WinForms, `RegisterHotKey`, the shell tray API) have no direct macOS counterparts. ## Requirements @@ -17,9 +17,9 @@ for the port. GreenshotMac is a rewrite, not a transpile: Greenshot's Win32 foun ## Building ```bash -open GreenshotMac.xcodeproj # then ⌘R +open Snaggle.xcodeproj # then ⌘R # or -xcodebuild -project GreenshotMac.xcodeproj -scheme GreenshotMac -configuration Release build +xcodebuild -project Snaggle.xcodeproj -scheme Snaggle -configuration Release build ``` The target is configured for ad-hoc signing (`CODE_SIGN_IDENTITY = "-"`) so it builds without a @@ -30,9 +30,43 @@ again after some rebuilds. On first launch macOS asks for **Screen Recording** permission (System Settings › Privacy & Security › Screen Recording). Nothing can be captured until it is granted. +## Installing (for people downloading a release) + +Snaggle is ad-hoc signed rather than notarised, because notarisation requires a paid Apple +Developer account. It runs perfectly well; macOS just asks you to confirm it once. + +1. Drag **Snaggle** to Applications and open it. macOS will refuse the first time, saying the + developer cannot be verified. +2. Open **System Settings → Privacy & Security**, scroll to Security, and click **Open Anyway** + next to the message about Snaggle. + +Control-clicking an app and choosing Open no longer bypasses Gatekeeper — Apple removed that +shortcut in macOS 15. If you prefer the terminal, `xattr -dr com.apple.quarantine +/Applications/Snaggle.app` does the same job in one step. + +Snaggle then needs **Screen Recording** permission (System Settings → Privacy & Security → +Screen Recording). Grant it, then quit and reopen the app — macOS only hands the permission to +a freshly launched process. + +Snaggle lives in the menu bar, not the Dock: look for the viewfinder icon. + +## Building a release + +```bash +./scripts/release.sh # ad-hoc signed, builds build/Snaggle.dmg +./scripts/release.sh --developer-id # Developer ID + notarisation (paid account) +``` + +The DMG contains the app, an Applications symlink and `INSTALL.txt`. For the notarised path, +set your team ID in `scripts/ExportOptions.plist` and store a notary credential profile first. + +The app icon is generated, not hand-drawn: edit the colours or shapes in +`scripts/make_appicon.py` and run `python3 scripts/make_appicon.py` to rewrite every size in +`Snaggle/Resources/Assets.xcassets/AppIcon.appiconset`. + ## Using it -GreenshotMac is a menu bar app (`LSUIElement`), so it has no Dock icon. The camera icon in the +Snaggle is a menu bar app (`LSUIElement`), so it has no Dock icon. The camera icon in the menu bar holds every command. Default global hotkeys — Greenshot's PrintScreen-based defaults don't exist on a Mac keyboard, @@ -91,7 +125,7 @@ Supported variables: `${capturetime}`, `${now}`, `${title}`, `${appname}`, `${us ## How the port maps onto Greenshot -| Greenshot (Windows) | GreenshotMac | +| Greenshot (Windows) | Snaggle | | --- | --- | | `MainForm` + tray icon | `StatusMenuController` (`NSStatusItem`) | | `CaptureForm` | `CaptureOverlay` (one borderless window per display) | @@ -112,6 +146,17 @@ the speech-bubble and SVG/emoji containers, `.gst` templates, and Greenshot's la The destination layer is a single `switch` in `Destinations.swift`, so adding an upload target is a small, self-contained change. -## Licence +## Licence and attribution -Greenshot is GPL-3.0; this port keeps the same licence. +Snaggle is a derivative work of [Greenshot](https://github.com/greenshot/greenshot) +(Copyright © 2007-2026 Thomas Braun, Jens Klingen, Robin Krom), reimplemented in Swift for +macOS, and is released under the **GNU General Public License v3.0** — the full text is in +[`LICENSE`](LICENSE). + +That means anyone may use, study, modify and redistribute Snaggle, including commercially, +provided the source stays available under the same licence. If you distribute a built copy of +Snaggle, you must also make this source available to whoever receives it. + +"Greenshot" is a trademark of the Greenshot project and is used here only to describe Snaggle's +origin. Snaggle is not affiliated with or endorsed by the Greenshot project, and it does not use +their name, logo or branding for itself. diff --git a/GreenshotMac.xcodeproj/project.pbxproj b/Snaggle.xcodeproj/project.pbxproj similarity index 80% rename from GreenshotMac.xcodeproj/project.pbxproj rename to Snaggle.xcodeproj/project.pbxproj index 297e266..8b5774d 100644 --- a/GreenshotMac.xcodeproj/project.pbxproj +++ b/Snaggle.xcodeproj/project.pbxproj @@ -7,26 +7,27 @@ objects = { /* Begin PBXFileReference section */ - A10000000000000000000001 /* GreenshotMac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GreenshotMac.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A10000000000000000000001 /* Snaggle.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Snaggle.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - A10000000000000000000010 /* Exceptions for "GreenshotMac" folder in "GreenshotMac" target */ = { + A10000000000000000000010 /* Exceptions for "Snaggle" folder in "Snaggle" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Resources/Info.plist, + Resources/Snaggle.entitlements, ); - target = A10000000000000000000004 /* GreenshotMac */; + target = A10000000000000000000004 /* Snaggle */; }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - A10000000000000000000002 /* GreenshotMac */ = { + A10000000000000000000002 /* Snaggle */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( - A10000000000000000000010 /* Exceptions for "GreenshotMac" folder in "GreenshotMac" target */, + A10000000000000000000010 /* Exceptions for "Snaggle" folder in "Snaggle" target */, ); - path = GreenshotMac; + path = Snaggle; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ @@ -45,7 +46,7 @@ A10000000000000000000000 = { isa = PBXGroup; children = ( - A10000000000000000000002 /* GreenshotMac */, + A10000000000000000000002 /* Snaggle */, A10000000000000000000003 /* Products */, ); sourceTree = ""; @@ -53,7 +54,7 @@ A10000000000000000000003 /* Products */ = { isa = PBXGroup; children = ( - A10000000000000000000001 /* GreenshotMac.app */, + A10000000000000000000001 /* Snaggle.app */, ); name = Products; sourceTree = ""; @@ -61,9 +62,9 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - A10000000000000000000004 /* GreenshotMac */ = { + A10000000000000000000004 /* Snaggle */ = { isa = PBXNativeTarget; - buildConfigurationList = A10000000000000000000009 /* Build configuration list for PBXNativeTarget "GreenshotMac" */; + buildConfigurationList = A10000000000000000000009 /* Build configuration list for PBXNativeTarget "Snaggle" */; buildPhases = ( A10000000000000000000006 /* Sources */, A10000000000000000000005 /* Frameworks */, @@ -74,11 +75,11 @@ dependencies = ( ); fileSystemSynchronizedGroups = ( - A10000000000000000000002 /* GreenshotMac */, + A10000000000000000000002 /* Snaggle */, ); - name = GreenshotMac; - productName = GreenshotMac; - productReference = A10000000000000000000001 /* GreenshotMac.app */; + name = Snaggle; + productName = Snaggle; + productReference = A10000000000000000000001 /* Snaggle.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -96,7 +97,7 @@ }; }; }; - buildConfigurationList = A10000000000000000000011 /* Build configuration list for PBXProject "GreenshotMac" */; + buildConfigurationList = A10000000000000000000011 /* Build configuration list for PBXProject "Snaggle" */; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -110,7 +111,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( - A10000000000000000000004 /* GreenshotMac */, + A10000000000000000000004 /* Snaggle */, ); }; /* End PBXProject section */ @@ -176,20 +177,21 @@ A10000000000000000000014 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_ENTITLEMENTS = GreenshotMac/Resources/GreenshotMac.entitlements; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Snaggle/Resources/Snaggle.entitlements; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = GreenshotMac/Resources/Info.plist; + INFOPLIST_FILE = Snaggle/Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = org.greenshot.mac.GreenshotMac; + PRODUCT_BUNDLE_IDENTIFIER = io.github.aetafoya.Snaggle; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; }; @@ -198,20 +200,21 @@ A10000000000000000000015 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_ENTITLEMENTS = GreenshotMac/Resources/GreenshotMac.entitlements; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Snaggle/Resources/Snaggle.entitlements; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = GreenshotMac/Resources/Info.plist; + INFOPLIST_FILE = Snaggle/Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = org.greenshot.mac.GreenshotMac; + PRODUCT_BUNDLE_IDENTIFIER = io.github.aetafoya.Snaggle; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; }; @@ -220,7 +223,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - A10000000000000000000009 /* Build configuration list for PBXNativeTarget "GreenshotMac" */ = { + A10000000000000000000009 /* Build configuration list for PBXNativeTarget "Snaggle" */ = { isa = XCConfigurationList; buildConfigurations = ( A10000000000000000000014 /* Debug */, @@ -229,7 +232,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - A10000000000000000000011 /* Build configuration list for PBXProject "GreenshotMac" */ = { + A10000000000000000000011 /* Build configuration list for PBXProject "Snaggle" */ = { isa = XCConfigurationList; buildConfigurations = ( A10000000000000000000012 /* Debug */, diff --git a/GreenshotMac.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Snaggle.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from GreenshotMac.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Snaggle.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/GreenshotMac.xcodeproj/xcshareddata/xcschemes/GreenshotMac.xcscheme b/Snaggle.xcodeproj/xcshareddata/xcschemes/Snaggle.xcscheme similarity index 81% rename from GreenshotMac.xcodeproj/xcshareddata/xcschemes/GreenshotMac.xcscheme rename to Snaggle.xcodeproj/xcshareddata/xcschemes/Snaggle.xcscheme index f010b20..8bb7ead 100644 --- a/GreenshotMac.xcodeproj/xcshareddata/xcschemes/GreenshotMac.xcscheme +++ b/Snaggle.xcodeproj/xcshareddata/xcschemes/Snaggle.xcscheme @@ -6,9 +6,9 @@ + BuildableName = "Snaggle.app" + BlueprintName = "Snaggle" + ReferencedContainer = "container:Snaggle.xcodeproj"> @@ -22,9 +22,9 @@ + BuildableName = "Snaggle.app" + BlueprintName = "Snaggle" + ReferencedContainer = "container:Snaggle.xcodeproj"> @@ -33,9 +33,9 @@ + BuildableName = "Snaggle.app" + BlueprintName = "Snaggle" + ReferencedContainer = "container:Snaggle.xcodeproj"> diff --git a/GreenshotMac/App/AppDelegate.swift b/Snaggle/App/AppDelegate.swift similarity index 92% rename from GreenshotMac/App/AppDelegate.swift rename to Snaggle/App/AppDelegate.swift index c7c7ac6..b984958 100644 --- a/GreenshotMac/App/AppDelegate.swift +++ b/Snaggle/App/AppDelegate.swift @@ -1,5 +1,5 @@ // AppDelegate.swift -// GreenshotMac +// Snaggle import AppKit @@ -12,9 +12,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { // A second copy would install its own menu bar item and register the same global // hotkeys, which the first copy already owns. Hand over to the running instance. if let other = AppDelegate.otherRunningInstance() { - NSLog("GreenshotMac: another instance is already running (pid \(other.processIdentifier)); exiting.") + NSLog("Snaggle: another instance is already running (pid \(other.processIdentifier)); exiting.") other.activate() - Notifier.show(title: "GreenshotMac is already running", + Notifier.show(title: "Snaggle is already running", body: "Use the camera icon in the menu bar.") NSApp.terminate(nil) return diff --git a/GreenshotMac/App/CaptureCoordinator.swift b/Snaggle/App/CaptureCoordinator.swift similarity index 98% rename from GreenshotMac/App/CaptureCoordinator.swift rename to Snaggle/App/CaptureCoordinator.swift index d2c5e86..6b22555 100644 --- a/GreenshotMac/App/CaptureCoordinator.swift +++ b/Snaggle/App/CaptureCoordinator.swift @@ -1,5 +1,5 @@ // CaptureCoordinator.swift -// GreenshotMac +// Snaggle // // Drives the capture flows, the macOS counterpart of Greenshot's CaptureHelper. @@ -157,7 +157,7 @@ final class CaptureCoordinator { alert.informativeText = """ macOS requires explicit permission before an app can capture the screen. - Open System Settings › Privacy & Security › Screen Recording, enable GreenshotMac, then try again. + Open System Settings › Privacy & Security › Screen Recording, enable Snaggle, then try again. """ alert.addButton(withTitle: "Open System Settings") alert.addButton(withTitle: "Cancel") diff --git a/GreenshotMac/App/HotkeyBindings.swift b/Snaggle/App/HotkeyBindings.swift similarity index 96% rename from GreenshotMac/App/HotkeyBindings.swift rename to Snaggle/App/HotkeyBindings.swift index 364e8f0..ab640fb 100644 --- a/GreenshotMac/App/HotkeyBindings.swift +++ b/Snaggle/App/HotkeyBindings.swift @@ -1,5 +1,5 @@ // HotkeyBindings.swift -// GreenshotMac +// Snaggle // // Maps the four capture actions onto global hotkeys and keeps them registered. @@ -82,5 +82,5 @@ final class HotkeyBindings { } extension Notification.Name { - static let hotkeysChanged = Notification.Name("GreenshotMacHotkeysChanged") + static let hotkeysChanged = Notification.Name("SnaggleHotkeysChanged") } diff --git a/GreenshotMac/App/MainMenuBuilder.swift b/Snaggle/App/MainMenuBuilder.swift similarity index 93% rename from GreenshotMac/App/MainMenuBuilder.swift rename to Snaggle/App/MainMenuBuilder.swift index f46bfc2..21a9d32 100644 --- a/GreenshotMac/App/MainMenuBuilder.swift +++ b/Snaggle/App/MainMenuBuilder.swift @@ -1,5 +1,5 @@ // MainMenuBuilder.swift -// GreenshotMac +// Snaggle // // A menu bar for the editor windows; the app itself lives in the status bar. @@ -14,11 +14,11 @@ enum MainMenuBuilder { // Application menu let appMenuItem = NSMenuItem() let appMenu = NSMenu() - appMenu.addItem(withTitle: "About GreenshotMac", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "") + appMenu.addItem(withTitle: "About Snaggle", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "") appMenu.addItem(.separator()) - appMenu.addItem(withTitle: "Hide GreenshotMac", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h") + appMenu.addItem(withTitle: "Hide Snaggle", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h") appMenu.addItem(.separator()) - appMenu.addItem(withTitle: "Quit GreenshotMac", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") + appMenu.addItem(withTitle: "Quit Snaggle", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") appMenuItem.submenu = appMenu mainMenu.addItem(appMenuItem) diff --git a/GreenshotMac/App/StatusMenuController.swift b/Snaggle/App/StatusMenuController.swift similarity index 93% rename from GreenshotMac/App/StatusMenuController.swift rename to Snaggle/App/StatusMenuController.swift index 0bb442d..ae58fdc 100644 --- a/GreenshotMac/App/StatusMenuController.swift +++ b/Snaggle/App/StatusMenuController.swift @@ -1,5 +1,5 @@ // StatusMenuController.swift -// GreenshotMac +// Snaggle // // The menu bar item, equivalent to Greenshot's tray icon and context menu. @@ -14,9 +14,9 @@ final class StatusMenuController: NSObject, NSMenuDelegate { override init() { super.init() if let button = statusItem.button { - button.image = NSImage(systemSymbolName: "camera.viewfinder", accessibilityDescription: "GreenshotMac") + button.image = NSImage(systemSymbolName: "camera.viewfinder", accessibilityDescription: "Snaggle") button.image?.isTemplate = true - button.toolTip = "GreenshotMac" + button.toolTip = "Snaggle" } statusItem.menu = buildMenu() } @@ -49,8 +49,8 @@ final class StatusMenuController: NSObject, NSMenuDelegate { menu.addItem(item(title: "Preferences…", action: #selector(showPreferences), hotkey: nil)) menu.addItem(.separator()) - menu.addItem(item(title: "About GreenshotMac", action: #selector(showAbout), hotkey: nil)) - menu.addItem(item(title: "Quit GreenshotMac", action: #selector(quit), hotkey: nil)) + menu.addItem(item(title: "About Snaggle", action: #selector(showAbout), hotkey: nil)) + menu.addItem(item(title: "Quit Snaggle", action: #selector(quit), hotkey: nil)) return menu } @@ -113,7 +113,7 @@ final class StatusMenuController: NSObject, NSMenuDelegate { @objc private func showAbout() { NSApp.activate(ignoringOtherApps: true) NSApp.orderFrontStandardAboutPanel(options: [ - .applicationName: "GreenshotMac", + .applicationName: "Snaggle", .credits: NSAttributedString( string: "A native macOS screenshot tool modelled on Greenshot for Windows.\n" + "Licensed under the GNU General Public License v3.", diff --git a/GreenshotMac/App/main.swift b/Snaggle/App/main.swift similarity index 97% rename from GreenshotMac/App/main.swift rename to Snaggle/App/main.swift index 7e5fed7..06889b7 100644 --- a/GreenshotMac/App/main.swift +++ b/Snaggle/App/main.swift @@ -1,5 +1,5 @@ // main.swift -// GreenshotMac +// Snaggle import AppKit diff --git a/GreenshotMac/Capture/CaptureEngine.swift b/Snaggle/Capture/CaptureEngine.swift similarity index 99% rename from GreenshotMac/Capture/CaptureEngine.swift rename to Snaggle/Capture/CaptureEngine.swift index 0b51c7f..bda86c5 100644 --- a/GreenshotMac/Capture/CaptureEngine.swift +++ b/Snaggle/Capture/CaptureEngine.swift @@ -1,5 +1,5 @@ // CaptureEngine.swift -// GreenshotMac +// Snaggle // // ScreenCaptureKit replacement for Greenshot's WindowCapture / GDI capture code. diff --git a/GreenshotMac/Capture/CaptureOverlay.swift b/Snaggle/Capture/CaptureOverlay.swift similarity index 99% rename from GreenshotMac/Capture/CaptureOverlay.swift rename to Snaggle/Capture/CaptureOverlay.swift index 8632d2c..4a2462c 100644 --- a/GreenshotMac/Capture/CaptureOverlay.swift +++ b/Snaggle/Capture/CaptureOverlay.swift @@ -1,5 +1,5 @@ // CaptureOverlay.swift -// GreenshotMac +// Snaggle // // The equivalent of Greenshot's CaptureForm: a frozen, dimmed copy of the desktop // on which the user drags a region or clicks a window. diff --git a/GreenshotMac/Capture/ScreenGeometry.swift b/Snaggle/Capture/ScreenGeometry.swift similarity index 99% rename from GreenshotMac/Capture/ScreenGeometry.swift rename to Snaggle/Capture/ScreenGeometry.swift index 044c219..e60899e 100644 --- a/GreenshotMac/Capture/ScreenGeometry.swift +++ b/Snaggle/Capture/ScreenGeometry.swift @@ -1,5 +1,5 @@ // ScreenGeometry.swift -// GreenshotMac +// Snaggle // // Conversions between AppKit's bottom-left global space and CoreGraphics' // top-left global space, plus display lookups. diff --git a/GreenshotMac/Core/Appearance.swift b/Snaggle/Core/Appearance.swift similarity index 98% rename from GreenshotMac/Core/Appearance.swift rename to Snaggle/Core/Appearance.swift index ddea11d..536bd44 100644 --- a/GreenshotMac/Core/Appearance.swift +++ b/Snaggle/Core/Appearance.swift @@ -1,5 +1,5 @@ // Appearance.swift -// GreenshotMac +// Snaggle // // Appearance handling and the shared material/shape vocabulary for the UI. // Everything here follows the system appearance by default; the user can override it. @@ -167,5 +167,5 @@ enum Theme { extension Notification.Name { /// Posted when the appearance override changes, so open windows can re-apply it. - static let appearanceChanged = Notification.Name("GreenshotMacAppearanceChanged") + static let appearanceChanged = Notification.Name("SnaggleAppearanceChanged") } diff --git a/GreenshotMac/Core/Capture.swift b/Snaggle/Core/Capture.swift similarity index 92% rename from GreenshotMac/Core/Capture.swift rename to Snaggle/Core/Capture.swift index 7dc78a5..1c7ff84 100644 --- a/GreenshotMac/Core/Capture.swift +++ b/Snaggle/Core/Capture.swift @@ -1,5 +1,5 @@ // Capture.swift -// GreenshotMac +// Snaggle // // Equivalent of Greenshot's ICapture / ICaptureDetails. @@ -65,7 +65,7 @@ enum CaptureError: LocalizedError { var errorDescription: String? { switch self { case .permissionDenied: - return "GreenshotMac needs Screen Recording permission. Open System Settings › Privacy & Security › Screen Recording and enable GreenshotMac." + return "Snaggle needs Screen Recording permission. Open System Settings › Privacy & Security › Screen Recording and enable Snaggle." case .noContent: return "Nothing was available to capture." case .cancelled: diff --git a/GreenshotMac/Core/Configuration.swift b/Snaggle/Core/Configuration.swift similarity index 99% rename from GreenshotMac/Core/Configuration.swift rename to Snaggle/Core/Configuration.swift index c119103..b722c62 100644 --- a/GreenshotMac/Core/Configuration.swift +++ b/Snaggle/Core/Configuration.swift @@ -1,5 +1,5 @@ // Configuration.swift -// GreenshotMac +// Snaggle // // Port of Greenshot's ICoreConfiguration / IEditorConfiguration onto UserDefaults. diff --git a/GreenshotMac/Core/FilenamePattern.swift b/Snaggle/Core/FilenamePattern.swift similarity index 99% rename from GreenshotMac/Core/FilenamePattern.swift rename to Snaggle/Core/FilenamePattern.swift index cf9b0c9..1a0e550 100644 --- a/GreenshotMac/Core/FilenamePattern.swift +++ b/Snaggle/Core/FilenamePattern.swift @@ -1,5 +1,5 @@ // FilenamePattern.swift -// GreenshotMac +// Snaggle // // Port of Greenshot's FilenameHelper: ${variable:parameters} expansion. // diff --git a/GreenshotMac/Core/HotkeyManager.swift b/Snaggle/Core/HotkeyManager.swift similarity index 99% rename from GreenshotMac/Core/HotkeyManager.swift rename to Snaggle/Core/HotkeyManager.swift index 438aca5..10a628a 100644 --- a/GreenshotMac/Core/HotkeyManager.swift +++ b/Snaggle/Core/HotkeyManager.swift @@ -1,5 +1,5 @@ // HotkeyManager.swift -// GreenshotMac +// Snaggle // // System wide hotkeys through the Carbon hotkey API, the macOS counterpart of // Greenshot's HotkeyControl (RegisterHotKey on Win32). diff --git a/GreenshotMac/Core/ImageExporter.swift b/Snaggle/Core/ImageExporter.swift similarity index 99% rename from GreenshotMac/Core/ImageExporter.swift rename to Snaggle/Core/ImageExporter.swift index ad87521..78885f2 100644 --- a/GreenshotMac/Core/ImageExporter.swift +++ b/Snaggle/Core/ImageExporter.swift @@ -1,5 +1,5 @@ // ImageExporter.swift -// GreenshotMac +// Snaggle // // Port of Greenshot's ImageIO: encoding a capture to the configured output format // and writing it to the configured directory using the filename pattern. diff --git a/GreenshotMac/Core/LaunchAtLogin.swift b/Snaggle/Core/LaunchAtLogin.swift similarity index 84% rename from GreenshotMac/Core/LaunchAtLogin.swift rename to Snaggle/Core/LaunchAtLogin.swift index 5c0f40f..a5723ef 100644 --- a/GreenshotMac/Core/LaunchAtLogin.swift +++ b/Snaggle/Core/LaunchAtLogin.swift @@ -1,5 +1,5 @@ // LaunchAtLogin.swift -// GreenshotMac +// Snaggle import Foundation import ServiceManagement @@ -19,7 +19,7 @@ enum LaunchAtLogin { } } } catch { - NSLog("GreenshotMac: could not change launch at login: \(error.localizedDescription)") + NSLog("Snaggle: could not change launch at login: \(error.localizedDescription)") } } } diff --git a/GreenshotMac/Destinations/DestinationPicker.swift b/Snaggle/Destinations/DestinationPicker.swift similarity index 99% rename from GreenshotMac/Destinations/DestinationPicker.swift rename to Snaggle/Destinations/DestinationPicker.swift index 7db65cf..ea76368 100644 --- a/GreenshotMac/Destinations/DestinationPicker.swift +++ b/Snaggle/Destinations/DestinationPicker.swift @@ -1,5 +1,5 @@ // DestinationPicker.swift -// GreenshotMac +// Snaggle // // Port of Greenshot's destination picker dialog: a preview of the capture, // one large button per destination, and "always use this destination". @@ -173,7 +173,7 @@ extension DestinationID { /// Shorter labels for the picker buttons, matching Greenshot's wording. var pickerTitle: String { switch self { - case .editor: return "Edit in Greenshot" + case .editor: return "Edit in Snaggle" case .file: return "Save to Disk" case .fileWithDialog: return "Save As…" case .clipboard: return "Copy to Clipboard" diff --git a/GreenshotMac/Destinations/Destinations.swift b/Snaggle/Destinations/Destinations.swift similarity index 98% rename from GreenshotMac/Destinations/Destinations.swift rename to Snaggle/Destinations/Destinations.swift index d80e742..f39c697 100644 --- a/GreenshotMac/Destinations/Destinations.swift +++ b/Snaggle/Destinations/Destinations.swift @@ -1,5 +1,5 @@ // Destinations.swift -// GreenshotMac +// Snaggle // // Port of Greenshot's IDestination implementations for macOS. @@ -182,7 +182,7 @@ enum Destinations { details: capture.details, format: configuration.outputFileFormat) let directory = FileManager.default.temporaryDirectory - .appendingPathComponent("GreenshotMac", isDirectory: true) + .appendingPathComponent("Snaggle", isDirectory: true) let url = ImageExporter.uniqueURL(directory.appendingPathComponent(name)) do { try ImageExporter.write(capture.image, to: url, format: configuration.outputFileFormat) @@ -199,7 +199,7 @@ enum Destinations { static func presentError(_ error: Error) { let alert = NSAlert() - alert.messageText = "GreenshotMac" + alert.messageText = "Snaggle" alert.informativeText = error.localizedDescription alert.alertStyle = .warning alert.addButton(withTitle: "OK") diff --git a/GreenshotMac/Destinations/Notifier.swift b/Snaggle/Destinations/Notifier.swift similarity index 98% rename from GreenshotMac/Destinations/Notifier.swift rename to Snaggle/Destinations/Notifier.swift index f21f7c4..92b9fe4 100644 --- a/GreenshotMac/Destinations/Notifier.swift +++ b/Snaggle/Destinations/Notifier.swift @@ -1,5 +1,5 @@ // Notifier.swift -// GreenshotMac +// Snaggle // // Small wrapper around UserNotifications with a Finder-reveal fallback. diff --git a/GreenshotMac/Editor/EditorCanvasView.swift b/Snaggle/Editor/EditorCanvasView.swift similarity index 99% rename from GreenshotMac/Editor/EditorCanvasView.swift rename to Snaggle/Editor/EditorCanvasView.swift index 925e811..a3f1f26 100644 --- a/GreenshotMac/Editor/EditorCanvasView.swift +++ b/Snaggle/Editor/EditorCanvasView.swift @@ -1,5 +1,5 @@ // EditorCanvasView.swift -// GreenshotMac +// Snaggle // // The drawing canvas: hit testing, creation, moving, resizing and inline text editing. diff --git a/GreenshotMac/Editor/EditorWindowController.swift b/Snaggle/Editor/EditorWindowController.swift similarity index 99% rename from GreenshotMac/Editor/EditorWindowController.swift rename to Snaggle/Editor/EditorWindowController.swift index 2653603..55d47f4 100644 --- a/GreenshotMac/Editor/EditorWindowController.swift +++ b/Snaggle/Editor/EditorWindowController.swift @@ -1,5 +1,5 @@ // EditorWindowController.swift -// GreenshotMac +// Snaggle // // Port of Greenshot's ImageEditorForm, laid out like a classic image editor: // a vertical tool rail on the left, a contextual options bar across the top. diff --git a/GreenshotMac/Editor/Elements.swift b/Snaggle/Editor/Elements.swift similarity index 99% rename from GreenshotMac/Editor/Elements.swift rename to Snaggle/Editor/Elements.swift index a84ed79..24b37e0 100644 --- a/GreenshotMac/Editor/Elements.swift +++ b/Snaggle/Editor/Elements.swift @@ -1,5 +1,5 @@ // Elements.swift -// GreenshotMac +// Snaggle // // Port of Greenshot's DrawableContainer hierarchy. Every element lives in // image point coordinates with a top-left origin, matching the flipped canvas. diff --git a/GreenshotMac/Editor/ElementsFilters.swift b/Snaggle/Editor/ElementsFilters.swift similarity index 99% rename from GreenshotMac/Editor/ElementsFilters.swift rename to Snaggle/Editor/ElementsFilters.swift index f97fccb..7352698 100644 --- a/GreenshotMac/Editor/ElementsFilters.swift +++ b/Snaggle/Editor/ElementsFilters.swift @@ -1,5 +1,5 @@ // ElementsFilters.swift -// GreenshotMac +// Snaggle // // Text, highlight, obfuscation, step counter and crop elements. diff --git a/GreenshotMac/Editor/StyleBarView.swift b/Snaggle/Editor/StyleBarView.swift similarity index 99% rename from GreenshotMac/Editor/StyleBarView.swift rename to Snaggle/Editor/StyleBarView.swift index fe14f7d..cdf4faa 100644 --- a/GreenshotMac/Editor/StyleBarView.swift +++ b/Snaggle/Editor/StyleBarView.swift @@ -1,5 +1,5 @@ // StyleBarView.swift -// GreenshotMac +// Snaggle // // The options row beneath the toolbar. It keeps a constant height whether or not the // active tool has options, so selecting a tool never shifts the canvas. Colour lives diff --git a/GreenshotMac/Editor/Surface.swift b/Snaggle/Editor/Surface.swift similarity index 99% rename from GreenshotMac/Editor/Surface.swift rename to Snaggle/Editor/Surface.swift index 16a7dd1..ea7ce77 100644 --- a/GreenshotMac/Editor/Surface.swift +++ b/Snaggle/Editor/Surface.swift @@ -1,5 +1,5 @@ // Surface.swift -// GreenshotMac +// Snaggle // // Port of Greenshot's Surface: the image plus its drawable elements, undo and rendering. diff --git a/GreenshotMac/Preferences/HotkeyRecorder.swift b/Snaggle/Preferences/HotkeyRecorder.swift similarity index 99% rename from GreenshotMac/Preferences/HotkeyRecorder.swift rename to Snaggle/Preferences/HotkeyRecorder.swift index 681f73d..af5145c 100644 --- a/GreenshotMac/Preferences/HotkeyRecorder.swift +++ b/Snaggle/Preferences/HotkeyRecorder.swift @@ -1,5 +1,5 @@ // HotkeyRecorder.swift -// GreenshotMac +// Snaggle // // A small AppKit control that records the next key combination, wrapped for SwiftUI. diff --git a/GreenshotMac/Preferences/PreferencesView.swift b/Snaggle/Preferences/PreferencesView.swift similarity index 99% rename from GreenshotMac/Preferences/PreferencesView.swift rename to Snaggle/Preferences/PreferencesView.swift index 0d43cb5..1d569ed 100644 --- a/GreenshotMac/Preferences/PreferencesView.swift +++ b/Snaggle/Preferences/PreferencesView.swift @@ -1,5 +1,5 @@ // PreferencesView.swift -// GreenshotMac +// Snaggle // // Port of Greenshot's SettingsForm. diff --git a/GreenshotMac/Preferences/PreferencesWindowController.swift b/Snaggle/Preferences/PreferencesWindowController.swift similarity index 88% rename from GreenshotMac/Preferences/PreferencesWindowController.swift rename to Snaggle/Preferences/PreferencesWindowController.swift index 5f4d29f..f9c6668 100644 --- a/GreenshotMac/Preferences/PreferencesWindowController.swift +++ b/Snaggle/Preferences/PreferencesWindowController.swift @@ -1,5 +1,5 @@ // PreferencesWindowController.swift -// GreenshotMac +// Snaggle import AppKit import SwiftUI @@ -10,7 +10,7 @@ final class PreferencesWindowController: NSWindowController { convenience init() { let hosting = NSHostingController(rootView: PreferencesView()) let window = NSWindow(contentViewController: hosting) - window.title = "GreenshotMac Preferences" + window.title = "Snaggle Preferences" window.styleMask = [.titled, .closable] window.setContentSize(NSSize(width: 552, height: 412)) window.center() diff --git a/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..64dc11e --- /dev/null +++ b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "icon_16x16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "icon_16x16@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "icon_32x32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "icon_32x32@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "icon_128x128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "icon_128x128@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "icon_256x256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "icon_256x256@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "icon_512x512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "icon_512x512@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 0000000..2400341 Binary files /dev/null and b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 0000000..fed8532 Binary files /dev/null and b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png new file mode 100644 index 0000000..84f519c Binary files /dev/null and b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 0000000..5bf568c Binary files /dev/null and b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 0000000..fed8532 Binary files /dev/null and b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png new file mode 100644 index 0000000..246732a Binary files /dev/null and b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 0000000..5bf568c Binary files /dev/null and b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 0000000..65d604d Binary files /dev/null and b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 0000000..246732a Binary files /dev/null and b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png new file mode 100644 index 0000000..2d10c59 Binary files /dev/null and b/Snaggle/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/GreenshotMac/Resources/Info.plist b/Snaggle/Resources/Info.plist similarity index 100% rename from GreenshotMac/Resources/Info.plist rename to Snaggle/Resources/Info.plist diff --git a/GreenshotMac/Resources/GreenshotMac.entitlements b/Snaggle/Resources/Snaggle.entitlements similarity index 100% rename from GreenshotMac/Resources/GreenshotMac.entitlements rename to Snaggle/Resources/Snaggle.entitlements diff --git a/docs/INSTALL.txt b/docs/INSTALL.txt new file mode 100644 index 0000000..6623de5 --- /dev/null +++ b/docs/INSTALL.txt @@ -0,0 +1,28 @@ +Installing Snaggle +================== + +1. Drag Snaggle to the Applications folder. + +2. Open it. macOS will say Snaggle "is not opened because it is from an + unidentified developer" (or cannot be verified). This is expected: Snaggle is + free software and is not signed with a paid Apple Developer certificate. + +3. Open System Settings > Privacy & Security, scroll down to Security, and click + "Open Anyway" next to the message about Snaggle. Confirm with your password + or Touch ID. + + (Control-clicking the app and choosing Open no longer works on macOS 15 and + later - Apple removed that shortcut.) + +4. Snaggle needs Screen Recording permission to capture anything. The first + capture triggers the request; enable Snaggle under + System Settings > Privacy & Security > Screen Recording, then quit and + reopen the app. + +Snaggle lives in the menu bar, not the Dock - look for the viewfinder icon. + +Prefer the terminal? This removes the quarantine flag in one step: + + xattr -dr com.apple.quarantine /Applications/Snaggle.app + +Snaggle is free software under the GNU GPL v3. Source: diff --git a/scripts/ExportOptions.plist b/scripts/ExportOptions.plist new file mode 100644 index 0000000..460b31e --- /dev/null +++ b/scripts/ExportOptions.plist @@ -0,0 +1,14 @@ + + + + + + method + developer-id + signingStyle + automatic + teamID + REPLACE_WITH_TEAM_ID + + diff --git a/scripts/build.sh b/scripts/build.sh old mode 100755 new mode 100644 index c005911..cd18960 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,17 +1,17 @@ #!/usr/bin/env bash -# Builds GreenshotMac and drops the .app in build/Release. +# Builds Snaggle and drops the .app in build/Release. set -euo pipefail cd "$(dirname "$0")/.." xcodebuild \ - -project GreenshotMac.xcodeproj \ - -scheme GreenshotMac \ + -project Snaggle.xcodeproj \ + -scheme Snaggle \ -configuration "${1:-Release}" \ -destination 'platform=macOS,arch=arm64' \ -derivedDataPath build/DerivedData \ build -APP="build/DerivedData/Build/Products/${1:-Release}/GreenshotMac.app" +APP="build/DerivedData/Build/Products/${1:-Release}/Snaggle.app" echo echo "Built: $APP" echo "Run it with: open \"$APP\"" diff --git a/scripts/make_appicon.py b/scripts/make_appicon.py new file mode 100644 index 0000000..fb1d8cb --- /dev/null +++ b/scripts/make_appicon.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Generates Snaggle's app icon set. + +Draws an original mark - a rounded square with a capture viewfinder - at high +resolution, then writes every size macOS asks for into the asset catalogue. +Run it again after editing the colours or shapes below: + + python3 scripts/make_appicon.py +""" + +from pathlib import Path +from PIL import Image, ImageDraw, ImageFilter + +SUPERSAMPLE = 4 +BASE = 1024 +OUTPUT = Path(__file__).resolve().parent.parent / "Snaggle" / "Resources" / "Assets.xcassets" / "AppIcon.appiconset" + +# Deep to bright green, reading as one surface rather than a flat fill. +TOP_COLOR = (46, 173, 92) +BOTTOM_COLOR = (18, 106, 62) +MARK_COLOR = (255, 255, 255, 242) + + +def vertical_gradient(size, top, bottom): + gradient = Image.new("RGB", (1, size), top) + for y in range(size): + blend = y / max(1, size - 1) + gradient.putpixel((0, y), tuple( + round(top[channel] + (bottom[channel] - top[channel]) * blend) for channel in range(3) + )) + return gradient.resize((size, size)) + + +def draw_icon(size): + scale = size / BASE + canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + + # macOS leaves a margin around the rounded square and uses a large corner radius. + margin = round(100 * scale) + side = size - margin * 2 + radius = round(185 * scale) + + mask = Image.new("L", (size, size), 0) + ImageDraw.Draw(mask).rounded_rectangle( + [margin, margin, size - margin, size - margin], radius=radius, fill=255) + + body = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + body.paste(vertical_gradient(size, TOP_COLOR, BOTTOM_COLOR), (0, 0)) + body.putalpha(mask) + + # Soft highlight across the top, blurred so there is no hard glossy edge. + highlight = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + ImageDraw.Draw(highlight).ellipse( + [margin - side * 0.35, margin - side * 0.95, size - margin + side * 0.35, margin + side * 0.40], + fill=(255, 255, 255, 34)) + highlight = highlight.filter(ImageFilter.GaussianBlur(round(60 * scale))) + highlight.putalpha(Image.composite(highlight.split()[3], Image.new("L", (size, size), 0), mask)) + body = Image.alpha_composite(body, highlight) + + canvas = Image.alpha_composite(canvas, body) + + # The mark: four viewfinder corners around an aperture ring. + draw = ImageDraw.Draw(canvas) + stroke = max(1, round(46 * scale)) + centre = size / 2 + reach = side * 0.30 # how far the corners sit from the centre + arm = side * 0.13 # length of each corner arm + + def round_cap(x, y): + draw.ellipse([x - stroke / 2, y - stroke / 2, x + stroke / 2, y + stroke / 2], fill=MARK_COLOR) + + for x_sign in (-1, 1): + for y_sign in (-1, 1): + x = centre + x_sign * reach + y = centre + y_sign * reach + # One polyline per corner keeps the elbow smooth instead of notched. + start = (x - x_sign * arm, y) + elbow = (x, y) + end = (x, y - y_sign * arm) + draw.line([start, elbow, end], fill=MARK_COLOR, width=stroke, joint="curve") + for point in (start, elbow, end): + round_cap(*point) + + ring = side * 0.135 + draw.ellipse([centre - ring, centre - ring, centre + ring, centre + ring], + outline=MARK_COLOR, width=stroke) + + # Shadow under the rounded square, the way system icons sit on a surface. + shadow = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + ImageDraw.Draw(shadow).rounded_rectangle( + [margin, margin + round(18 * scale), size - margin, size - margin + round(18 * scale)], + radius=radius, fill=(0, 0, 0, 70)) + shadow = shadow.filter(ImageFilter.GaussianBlur(round(18 * scale))) + return Image.alpha_composite(shadow, canvas) + + +def main(): + OUTPUT.mkdir(parents=True, exist_ok=True) + master = draw_icon(BASE * SUPERSAMPLE).resize((BASE, BASE), Image.LANCZOS) + + # (point size, scale) pairs macOS expects for a Mac app icon. + entries = [] + for point, scales in [(16, [1, 2]), (32, [1, 2]), (128, [1, 2]), (256, [1, 2]), (512, [1, 2])]: + for factor in scales: + pixels = point * factor + name = f"icon_{point}x{point}{'@2x' if factor == 2 else ''}.png" + master.resize((pixels, pixels), Image.LANCZOS).save(OUTPUT / name) + entries.append(f""" {{ + "filename" : "{name}", + "idiom" : "mac", + "scale" : "{factor}x", + "size" : "{point}x{point}" + }}""") + + contents = "{\n \"images\" : [\n" + ",\n".join(entries) + \ + "\n ],\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n" + (OUTPUT / "Contents.json").write_text(contents) + print(f"Wrote {len(entries)} icon sizes to {OUTPUT}") + + +if __name__ == "__main__": + main() diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..a2bdc7a --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# +# Builds a distributable Snaggle.app and wraps it in a DMG. +# +# ./scripts/release.sh # ad-hoc signed (no Apple Developer account needed) +# ./scripts/release.sh --developer-id # Developer ID + notarisation (paid account) +# +# The ad-hoc build runs anywhere, but macOS marks it as unverified: whoever downloads it has +# to allow it once under System Settings > Privacy & Security. See README "Installing". +# +set -euo pipefail +cd "$(dirname "$0")/.." + +MODE="adhoc" +[[ "${1:-}" == "--developer-id" ]] && MODE="developer-id" + +# Xcode itself is required; the Command Line Tools package alone cannot build a project. +if ! xcodebuild -version >/dev/null 2>&1; then + echo "error: xcodebuild needs a full Xcode installation." >&2 + echo "Active developer directory: $(xcode-select -p 2>/dev/null)" >&2 + echo >&2 + echo "Point it at Xcode and try again:" >&2 + echo " sudo xcode-select -s /Applications/Xcode.app/Contents/Developer" >&2 + exit 1 +fi + +BUILD_DIR="build" +DERIVED="$BUILD_DIR/DerivedData" +EXPORT_DIR="$BUILD_DIR/export" +DMG="$BUILD_DIR/Snaggle.dmg" +NOTARY_PROFILE="${NOTARY_PROFILE:-SnaggleNotary}" + +rm -rf "$EXPORT_DIR" "$DMG" +mkdir -p "$BUILD_DIR" + +if [[ "$MODE" == "adhoc" ]]; then + echo "==> Building (ad-hoc signature)" + xcodebuild \ + -project Snaggle.xcodeproj \ + -scheme Snaggle \ + -configuration Release \ + -destination 'platform=macOS,arch=arm64' \ + -derivedDataPath "$DERIVED" \ + CODE_SIGN_IDENTITY="-" \ + CODE_SIGN_STYLE=Manual \ + DEVELOPMENT_TEAM="" \ + build + + mkdir -p "$EXPORT_DIR" + cp -R "$DERIVED/Build/Products/Release/Snaggle.app" "$EXPORT_DIR/" + APP="$EXPORT_DIR/Snaggle.app" + + # Apple silicon refuses to run a binary with no signature at all, so re-sign ad-hoc + # to be certain every nested item is covered. + echo "==> Ad-hoc signing" + codesign --force --deep --sign - --options runtime "$APP" + codesign --verify --verbose=2 "$APP" +else + ARCHIVE="$BUILD_DIR/Snaggle.xcarchive" + rm -rf "$ARCHIVE" + + echo "==> Archiving" + xcodebuild archive \ + -project Snaggle.xcodeproj \ + -scheme Snaggle \ + -configuration Release \ + -destination 'platform=macOS,arch=arm64' \ + -archivePath "$ARCHIVE" + + echo "==> Exporting" + xcodebuild -exportArchive \ + -archivePath "$ARCHIVE" \ + -exportOptionsPlist scripts/ExportOptions.plist \ + -exportPath "$EXPORT_DIR" + APP="$EXPORT_DIR/Snaggle.app" + + echo "==> Notarising (this can take a few minutes)" + ZIP="$BUILD_DIR/Snaggle-notarize.zip" + rm -f "$ZIP" + ditto -c -k --keepParent "$APP" "$ZIP" + xcrun notarytool submit "$ZIP" --keychain-profile "$NOTARY_PROFILE" --wait + xcrun stapler staple "$APP" + rm -f "$ZIP" +fi + +echo "==> Building DMG" +STAGING="$BUILD_DIR/dmg-staging" +rm -rf "$STAGING" +mkdir -p "$STAGING" +cp -R "$APP" "$STAGING/" +ln -s /Applications "$STAGING/Applications" +cp docs/INSTALL.txt "$STAGING/" 2>/dev/null || true +hdiutil create -volname "Snaggle" -srcfolder "$STAGING" -ov -format UDZO "$DMG" +rm -rf "$STAGING" + +echo +echo "App: $APP" +echo "DMG: $DMG" +if [[ "$MODE" == "adhoc" ]]; then + echo + echo "Ad-hoc signed. Tell people to open it once via" + echo " System Settings > Privacy & Security > Open Anyway" + echo "Control-click > Open no longer works on macOS 15 and later." +fi