diff --git a/MAINTAINERS b/MAINTAINERS index 8a7a8efde..039847a25 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -33,6 +33,13 @@ M: Pavel Roschin W: S: Maintained +autorun +P: +g: @WrapEarnPass +M: +W: +S: Maintained + codenav P: Federico Reghenzani g: diff --git a/Makefile.am b/Makefile.am index a3f1c10f8..3b5b3da1f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -23,6 +23,10 @@ if ENABLE_AUTOMARK SUBDIRS += automark endif +if ENABLE_AUTORUN +SUBDIRS += autorun +endif + if ENABLE_CODENAV SUBDIRS += codenav endif diff --git a/autorun/AUTHORS b/autorun/AUTHORS new file mode 100644 index 000000000..e71ce8490 --- /dev/null +++ b/autorun/AUTHORS @@ -0,0 +1 @@ +WrapEarnPass diff --git a/autorun/CMakeLists.txt b/autorun/CMakeLists.txt new file mode 100644 index 000000000..adfd5cba0 --- /dev/null +++ b/autorun/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.27) + +set(CMAKE_EXPORT_COMPILE_COMMANDS on) + +set(CMAKE_INSTALL_PREFIX "$ENV{HOME}/.local/lib/geany") + +project( + autorun + VERSION 0.0.1 + LANGUAGES C + DESCRIPTION "Geany action interceptor plugin") + +set(CMAKE_C_FLAGS "-Wall") +set(CMAKE_C_FLAGS_DEBUG "-g") +set(CMAKE_C_FLAGS_RELEASE "-O3") + +set(CMAKE_CXX_FLAGS "-Wall") +set(CMAKE_CXX_FLAGS_DEBUG "-g" ) +set(CMAKE_CXX_FLAGS_RELEASE "-O3") + +add_compile_definitions(LOCALEDIR="/usr/share/locale") +# the `pkg_check_modules` function is created with this call +find_package(PkgConfig REQUIRED) +# these calls create special `PkgConfig::` variables +pkg_check_modules(geany REQUIRED IMPORTED_TARGET GLOBAL geany) + +add_library(${PROJECT_NAME} SHARED src/plugin_main.c src/autorun.c src/utils.c + src/spawn.c src/menu.c src/project.c) +# prevent cmake from prefixing the shared library +set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") +target_link_libraries(${PROJECT_NAME} PRIVATE PkgConfig::geany) +# install to users bin +install(TARGETS ${PROJECT_NAME} DESTINATION plugins) diff --git a/autorun/COPYING b/autorun/COPYING new file mode 100644 index 000000000..8c4c849e2 --- /dev/null +++ b/autorun/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/autorun/ChangeLog b/autorun/ChangeLog new file mode 100644 index 000000000..e69de29bb diff --git a/autorun/Makefile.am b/autorun/Makefile.am new file mode 100644 index 000000000..971a7ec71 --- /dev/null +++ b/autorun/Makefile.am @@ -0,0 +1,4 @@ +include $(top_srcdir)/build/vars.auxfiles.mk + +SUBDIRS = src +plugin = autorun diff --git a/autorun/NEWS b/autorun/NEWS new file mode 100644 index 000000000..19c297890 --- /dev/null +++ b/autorun/NEWS @@ -0,0 +1,2 @@ +Jun 2026 +* Created plugin, initial commit (work in progress) diff --git a/autorun/README b/autorun/README new file mode 100644 index 000000000..383f1dcdf --- /dev/null +++ b/autorun/README @@ -0,0 +1,167 @@ +========= +Auto-run +========= + +.. contents:: + +About +===== + +The Auto-run plugin is an extension that makes it possible to add action handlers to +geany. The initial release targets document-save and document-before-save + +Usage +===== + +Enabling the plugin +------------------- +The plugin can be enabled in the plugin manager. +There will also be a new entry in the "Tools" menu. + +The Auto-run menu +------------------ +The Auto-run menu allows you to manage the Project level Auto-run configs. + +Auto-run replacements +--------------------- + +Auto-run supports the following replacements in CM and WD +%f - the basename of the file being saved. +%a - the absolute path of the file being saved. +%d - the absolute path of the directory of the file being saved. +%p - the absolute path of the base directory of the currently open project. + +The BS Intercepted Action by default does not need the %a argument as it +operates on the contents of the Scintilla editor. + +Why would BS targets support %a then? +Place the %a replacement in the BS target if you know the CM does not honor STDIN. +In that case Auto-run will dump the contents of Scintilla to a temp file. + +clang-format, which reads stdin, would like to the filename for formatting rules. +e.g. clang-format --assume-filename='%f' + +cmake-format, which does not read stdin +e.g. cmake-format '%a' + +Ensure you're setting the correct quoting for arguments. Windows expects " quoted filenames +Linux expects ' quoted filenames + +Manually setting Auto-run configs +--------------------------------- +You can manually set Project level Auto-run configs under the keyfile heading [autorun], +using https://wiki.geany.org/howtos/configurebuildmenu#build-menu style entries. +The following is RFC2119 compliant. + +FILETYPE_IA_NN_TA + +FILETYPE is a Geany filetype label, e.g. (C|C++|C#|JSON) from geany/filetype_extensions.conf + +IA(Intercepted Action) = (BS|OS), +* BS is Geany document-before-save +* OS is Geany document-save (on save) + +NN = (00|01|...) +NN is a two digit number allowing for multiple tasks for a given entry. + +TA(Target Action) = (CM|WD) +* CM is the command to run +* WD is the working directory to run from. +CM SHOULD be quoted appropriate to the OS' expectations. +WD MUST NOT be quoted on Linux. + +C_BS_00_CM = formatter +C_BS_00_WD = %d +C_OS_00_CM = linter '%a' +C_OS_00_WD = %p +#Run formatter before file write, and then linter on the newly written %d/%f from %p +#when attempting to save a C file type. + +C++_OS_00_CM = cppcheck '%f' --output-file='%p/cppcheck.log' +C++_OS_00_WD = %d +C++_OS_01_CM = clang-tidy -p '%p' --format-style=file '%f' +C++_OS_01_WD = %p +#Run cppcheck on %f from %d, and clang-tidy on %f from %p after file write when saving a C++ file type. + +Set Auto-run actions across Projects +------------------------------------ +To set Autorun across Projects without having to edit each .geany file, you can use filedefs +https://www.geany.org/manual/#filetype-definition-files under the keyfile heading [autorun] +with a syntax similar to the Project configs. + +IA_NN_TA +where +IA = (BS|OS), NN = (00|01|...), TA=(CM|WD) + +BS_00_CM = formatter +OS_00_CM = linter '%f' + + +When Auto-run loads, it will scan all User config dir filedefs/filetypes.* +for any [autorun] sections and set actions for all matching filetypes. +User config dir filedefs settings will be overridden by any Project settings. + +Example: +(userconfigdir)/filedefs/filetypes.c +BS_00_CM = formatter '%f' +(userprojectdir)/project.geany +CBS_00_CM = fancyformatter '%f' + +Autorun will run fancyformatter before save for C files as long as project.geany is open. + +All Auto-run output will be in the Geany Compiler status window. + +Known issues +============ + +Auto-run runs before-save targets synchronously. This may cause Geany to freeze. + Move slow commands to on-save to run asynchronously. +geany/geany#4603 may impact reloading the Project configs in realtime. + Tools>Auto-run>Reload will force a reload. +Compiler won't follow links unless at least one Build command has run. + https://github.com/geany/geany/issues/4609 +Auto-run attempts a best effort to make filenames safe for the OS, but +some external tools do not correctly handle file and directory names on Linux + https://gitlab.kitware.com/cmake/cmake/-/work_items/27918 + https://github.com/llvm/llvm-project/issues/207396 + +License +======= + +The Auto-run plugin is distributed 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. A copy +of this license can be found in the file COPYING included with the source +code of this program. + +Downloads +========= + +The Auto-run plugin is part of the combined Geany Plugins release. +For more information and downloads, please visit +http://plugins.geany.org/geany-plugins/ + +Development Code +================ + +Get the code from:: + + git clone https://github.com/geany/geany-plugins.git + +Ideas, questions, patches and bug reports +========================================= + +Please post any ideas, feature requests, questions and bugs in the +github issue tracker. + +History +======= + +This is a short release history showing the major changes: + +**0.1:** + Created plugin + Implemented filetypes and project config readers + Implemented sync and async launch for handlers + Implemented Project menu, and Tool menu + Tested against Geany 2.1 and 2.2 diff --git a/autorun/THANKS b/autorun/THANKS new file mode 100644 index 000000000..b180f4bf0 --- /dev/null +++ b/autorun/THANKS @@ -0,0 +1,17 @@ +What's this file about? +----------------------- +This file lists all external people that have contributed to this project. + +Workbench Plugin: +------------------------- +This plugin is heavily based on working with the Workbench plugin. +Thanks to LarsGit223. + +Project Organizer Plugin: +------------------------- +This plugin is heavily based on working with the Project Organizer plugin. +Thanks to Jiri Techet. + +Geany and Geany plugins: +------------------------ +Thanks to all Geany developers for creating the best IDE a dev could ask for. diff --git a/autorun/src/.clang-format b/autorun/src/.clang-format new file mode 100644 index 000000000..4608ee667 --- /dev/null +++ b/autorun/src/.clang-format @@ -0,0 +1,214 @@ +Language: Cpp +BasedOnStyle: 'LLVM' +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignArrayOfStructures: None +AlignConsecutiveAssignments: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: true +AlignConsecutiveBitFields: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: false +AlignConsecutiveDeclarations: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: false +AlignConsecutiveMacros: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: false +AlignEscapedNewlines: Right +AlignOperands: Align +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortEnumsOnASingleLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +AttributeMacros: + - __capability +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeConceptDeclarations: Always +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 170 +CommentPragmas: '^ IWYU pragma:' +QualifierAlignment: Leave +CompactNamespaces: false +ConstructorInitializerIndentWidth: 0 +ContinuationIndentWidth: 0 +Cpp11BracedListStyle: false +DeriveLineEnding: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +PackConstructorInitializers: BinPack +ConstructorInitializerAllOnOneLineOrOnePerLine: false +AllowAllConstructorInitializersOnNextLine: true +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IfMacros: + - KJ_IF_MAYBE +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '(Test)?$' +IncludeIsMainSourceRegex: '' +IndentAccessModifiers: false +IndentCaseLabels: false +IndentCaseBlocks: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentExternBlock: AfterExternBlock +IndentRequiresClause: true +IndentWidth: 2 +IndentWrappedFunctionNames: false +InsertBraces: true +InsertTrailingCommas: None +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +LambdaBodyIndentation: Signature +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 0 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakOpenParenthesis: 0 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PenaltyIndentedWhitespace: 0 +PointerAlignment: Left +PPIndentWidth: -1 +ReferenceAlignment: Pointer +ReflowComments: true +RemoveBracesLLVM: false +RequiresClausePosition: OwnLine +SeparateDefinitionBlocks: Leave +ShortNamespaceLines: 1 +SortIncludes: CaseSensitive +SortJavaStaticImport: Before +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeParensOptions: + AfterControlStatements: true + AfterForeachMacros: true + AfterFunctionDefinitionName: false + AfterFunctionDeclarationName: false + AfterIfMacros: true + AfterOverloadedOperator: false + AfterRequiresInClause: false + AfterRequiresInExpression: false + BeforeNonEmptyParentheses: false +SpaceAroundPointerQualifiers: After +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: Never +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +BitFieldColonSpacing: Both +Standard: Latest +StatementAttributeLikeMacros: + - Q_EMIT +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 2 +UseCRLF: false +UseTab: Always +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE + - NS_SWIFT_NAME + - CF_SWIFT_NAME + + diff --git a/autorun/src/Makefile.am b/autorun/src/Makefile.am new file mode 100644 index 000000000..39d27419d --- /dev/null +++ b/autorun/src/Makefile.am @@ -0,0 +1,21 @@ +include $(top_srcdir)/build/vars.build.mk +plugin = autorun + +geanyplugins_LTLIBRARIES = autorun.la + +autorun_la_SOURCES = \ + plugin_main.c \ + autorun.c \ + utils.c \ + spawn.c \ + menu.c \ + project.c + +autorun_la_CPPFLAGS = $(AM_CPPFLAGS) \ + -DG_LOG_DOMAIN=\"Autorun\" +autorun_la_CFLAGS = $(AM_CFLAGS) $(WORKBENCH_CFLAGS) \ + -I$(top_srcdir)/utils/src +autorun_la_LIBADD = $(COMMONLIBS) \ + $(AUTORUN_LIBS) \ + $(top_builddir)/utils/src/libgeanypluginutils.la +include $(top_srcdir)/build/cppcheck.mk diff --git a/autorun/src/autorun.c b/autorun/src/autorun.c new file mode 100644 index 000000000..35c424481 --- /dev/null +++ b/autorun/src/autorun.c @@ -0,0 +1,193 @@ +/* + * Copyright 2026 WrapEarnPass + * + * 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. + */ + +#include + +#include "autorun.h" +#include "utils.h" + +AUTORUN_GLOBALS* autorun_globals; + +/* allocate an AUTORUN_CMD struct + * should be cleaned up with a matching call to autorun_cmd_free */ +AUTORUN_CMD* autorun_cmd_new() { + AUTORUN_CMD* cmd = NULL; + cmd = g_new0(AUTORUN_CMD, 1); + cmd->file_type = NULL; + cmd->interceptor = NULL; + cmd->command = NULL; + cmd->working_dir = NULL; + cmd->invalid = TRUE; + cmd->order = 0; + return cmd; +} + +/* free an AUTORUN_CMD in a safe manner */ +void autorun_cmd_free(AUTORUN_CMD* cmd) { + /* do not check for cmd->invalid because it could be a partial command + * or invalidated after assembly. */ + if (!cmd) { + // who are you and why are you in my attic? + return; + } + // free the command properties + if (cmd->file_type) { + cmd->file_type = NULL; + } + if (cmd->interceptor) { + g_free(cmd->interceptor); + cmd->interceptor = NULL; + } + if (cmd->command) { + g_free(cmd->command); + cmd->command = NULL; + } + if (cmd->working_dir) { + g_free(cmd->working_dir); + cmd->working_dir = NULL; + } + // and the struct itself + g_free(cmd); +} + +/* helper to free a GSList of AUTORUN_CMD */ +void autorun_cmd_list_free(GSList* command_list) { + if (!command_list) { + // why are you naked? + return; + } + g_slist_free_full(command_list, (GDestroyNotify)autorun_cmd_free); +} + +/* establish the autorun_globals */ +void autorun_globals_init(GeanyPlugin* plugin) { + if (!autorun_globals) { + autorun_globals = g_new0(AUTORUN_GLOBALS, 1); + autorun_globals->plugin = plugin; + autorun_globals->data = plugin->geany_data; + autorun_globals->filedef_commands = NULL; + autorun_globals->project_commands = NULL; + autorun_globals->dirtybit = FALSE; + autorun_globals->children = 0; + } +} + +/* teardown any linked properties from autorun_globals */ +void autorun_globals_free(void) { + if (autorun_globals) { + if (autorun_globals->filedef_commands) { + autorun_cmd_list_free(autorun_globals->filedef_commands); + autorun_globals->filedef_commands = NULL; + } + if (autorun_globals->project_commands) { + autorun_cmd_list_free(autorun_globals->project_commands); + autorun_globals->project_commands = NULL; + } + autorun_globals->data = NULL; + autorun_globals->plugin = NULL; + g_free(autorun_globals); + autorun_globals = NULL; + } +} + +/* helper function to process .config/geany/filedefs/ + * to load any [autorun] handlers */ +void load_filedefs(void) { + // load the config dir + gchar* filedef_path = g_build_path(G_DIR_SEPARATOR_S, autorun_globals->data->app->configdir, GEANY_FILEDEFS_SUBDIR, NULL); + guint filedef_len; + GSList* file_list = utils_get_file_list(filedef_path, &filedef_len, NULL); + // find any filetypes.FILE + if (filedef_len > 0) { + GSList* node = NULL; + foreach_slist(node, file_list) { + if (g_str_has_prefix(node->data, "filetypes.") && g_strcmp0(node->data, "filetypes.README") != 0) { + // we have files to process + gsize key_len; + gchar* filedef_file = g_build_filename(G_DIR_SEPARATOR_S, filedef_path, (gchar*)node->data, NULL); + GKeyFile* config = g_key_file_new(); + g_key_file_load_from_file(config, filedef_file, G_KEY_FILE_NONE, NULL); + GError* gerr = NULL; + gchar** handlers = g_key_file_get_keys(config, "autorun", &key_len, &gerr); + // if the keyfile has an [autorun] section + if (!gerr) { + gchar** handler_key; + foreach_strv(handler_key, handlers) { + if (g_str_has_suffix(*handler_key, "CM")) { + AUTORUN_CMD* cmd = autorun_cmd_new(); + cmd->file_type = filetypes_detect_from_file(node->data); + parse_intercept_actions(*handler_key, config, cmd); + + if (!cmd->invalid) { + // add the command backwards + autorun_globals->filedef_commands = g_slist_prepend(autorun_globals->filedef_commands, cmd); + } else { + // free the command + autorun_cmd_free(cmd); + } + } + } + // flip the GSList around + autorun_globals->filedef_commands = g_slist_reverse(autorun_globals->filedef_commands); + } else { + g_free(gerr); + } + g_key_file_free(config); + g_free(filedef_file); + g_free(handlers); + } + } + } + + // cleanup + g_slist_free_full(file_list, (GDestroyNotify)g_free); + g_free(filedef_path); +} + +/* helper function to process Project .geany files + * to load any [autorun] handlers */ +void load_projectdefs(GKeyFile* config) { + gsize key_len; + GError* gerr = NULL; + gchar** handlers = g_key_file_get_keys(config, "autorun", &key_len, &gerr); + // if the keyfile has an [autorun] section + if (!gerr) { + gchar** handler_key; + // load any handlers + foreach_strv(handler_key, handlers) { + // over any existing [autorun] handlers + if (g_str_has_suffix(*handler_key, "CM")) { + AUTORUN_CMD* cmd = autorun_cmd_new(); + parse_intercept_actions(*handler_key, config, cmd); + if (!cmd->invalid) { + // add the command backwards + autorun_globals->project_commands = g_slist_prepend(autorun_globals->project_commands, cmd); + } else { + // free the command + autorun_cmd_free(cmd); + } + } + } + // flip the GSList around + autorun_globals->project_commands = g_slist_reverse(autorun_globals->project_commands); + } else { + g_free(gerr); + } + // The caller of g_key_file_get_keys takes ownership of the returned data, and is responsible for freeing it. + g_free(handlers); +} diff --git a/autorun/src/autorun.h b/autorun/src/autorun.h new file mode 100644 index 000000000..d0ebd43b7 --- /dev/null +++ b/autorun/src/autorun.h @@ -0,0 +1,52 @@ +/* + * Copyright 2026 WrapEarnPass + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef __AUTORUN_H__ +#define __AUTORUN_H__ + +#include + +typedef struct { + GeanyPlugin* plugin; + GeanyData* data; + GSList* filedef_commands; + GSList* project_commands; + gboolean dirtybit; + gint children; +} AUTORUN_GLOBALS; + +typedef struct { + GeanyFiletype* file_type; + gchar* interceptor; + gushort order; + gchar* command; + gchar* working_dir; + gboolean invalid; +} AUTORUN_CMD; + +extern AUTORUN_GLOBALS* autorun_globals; + +void autorun_globals_init(GeanyPlugin* plugin); +void autorun_globals_free(void); +void autorun_cmd_list_free(GSList* command_list); +void autorun_cmd_free(AUTORUN_CMD* cmd); +AUTORUN_CMD* autorun_cmd_new(void); +void load_filedefs(void); +void load_projectdefs(GKeyFile* config); + +#endif diff --git a/autorun/src/menu.c b/autorun/src/menu.c new file mode 100644 index 000000000..e421c0825 --- /dev/null +++ b/autorun/src/menu.c @@ -0,0 +1,88 @@ +/* + * Copyright 2026 WrapEarnPass + * + * 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. + */ + +#include + +#include "autorun.h" +#include "menu.h" + +typedef struct { + GtkWidget* menu; + GtkWidget* root_item; + GtkWidget* item_reload; +} AUTORUN_MENU_DATA; +static AUTORUN_MENU_DATA menu_data; + +/* The function handles the menu item "Reload" */ +static void autorun_menu_reload_cb(G_GNUC_UNUSED GtkMenuItem* menuitem, G_GNUC_UNUSED gpointer user_data) { + // force unloading of any loaded project configs + autorun_cmd_list_free(autorun_globals->project_commands); + autorun_globals->project_commands = NULL; + // force unloading of any filedef configs. + autorun_cmd_list_free(autorun_globals->filedef_commands); + autorun_globals->filedef_commands = NULL; + msgwin_status_add(_("Reloading Auto-run filetypes.* configs")); + // load filedefs + load_filedefs(); + // load project + if(autorun_globals->data->app->project && autorun_globals->data->app->project->file_name){ + GKeyFile* config = g_key_file_new(); + gboolean ret = g_key_file_load_from_file(config, autorun_globals->data->app->project->file_name, G_KEY_FILE_NONE, NULL); + if (ret) { + msgwin_status_add(_("Reloading Auto-run project configs")); + load_projectdefs(config); + } + g_key_file_free(config); + } +} + +/* initialize the Auto-run Tool menu */ +gboolean menu_init(void) { + // Create menu and root item/label + menu_data.menu = gtk_menu_new(); + menu_data.root_item = gtk_menu_item_new_with_label("Auto-run"); + gtk_widget_show(menu_data.root_item); + + // Create new menu item "Reload" + GtkWidget* icon = gtk_image_new_from_icon_name("view-refresh", GTK_ICON_SIZE_MENU); + /* + * deprecated since gtk3.10, but it looks like crap using + * the "functionally equivalent" menu( box (img,label) ) + */ + menu_data.item_reload = gtk_image_menu_item_new_with_label(_("Reload")); + gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(menu_data.item_reload), icon); + gtk_widget_show_all(menu_data.item_reload); + gtk_menu_shell_append(GTK_MENU_SHELL(menu_data.menu), menu_data.item_reload); + g_signal_connect(menu_data.item_reload, "activate", G_CALLBACK(autorun_menu_reload_cb), NULL); + + /* Add our menu to the main window (left of the help menu) */ + gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_data.root_item), menu_data.menu); + gtk_container_add(GTK_CONTAINER(autorun_globals->data->main_widgets->tools_menu), menu_data.root_item); + + return TRUE; +} + +/* Gtk widget cleanup for Auto-run Tool menu */ +void menu_cleanup(void) { + gtk_widget_destroy(GTK_WIDGET(menu_data.item_reload)); + menu_data.item_reload = NULL; + gtk_widget_destroy(GTK_WIDGET(menu_data.root_item)); + menu_data.root_item = NULL; + // since this is attached to main_widgets, attempting to destroy it creates an assertion error + menu_data.menu = NULL; +} diff --git a/autorun/src/menu.h b/autorun/src/menu.h new file mode 100644 index 000000000..a7650df3c --- /dev/null +++ b/autorun/src/menu.h @@ -0,0 +1,27 @@ +/* + * Copyright 2026 WrapEarnPass + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef __AUTORUN_MENU_H__ +#define __AUTORUN_MENU_H__ + +#include + +gboolean menu_init(void); +void menu_cleanup(void); + +#endif diff --git a/autorun/src/plugin_main.c b/autorun/src/plugin_main.c new file mode 100644 index 000000000..3bcda55a0 --- /dev/null +++ b/autorun/src/plugin_main.c @@ -0,0 +1,192 @@ +/* + * Copyright 2026 WrapEarnPass + * + * 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. + */ + +#include +#include + +#include "autorun.h" +#include "menu.h" +#include "project.h" +#include "spawn.h" + +/* Handler to read any Project declared Auto-run configs */ +static void on_project_open(G_GNUC_UNUSED GObject* obj, GKeyFile* config, G_GNUC_UNUSED gpointer user_data) { + if (!autorun_globals->project_commands) { + load_projectdefs(config); + } +} + +/* Handler to read any Project declared Auto-run configs */ +static void on_project_save(G_GNUC_UNUSED GObject* obj, GKeyFile* config, G_GNUC_UNUSED gpointer user_data) { + // write notebook to .geany keyfile + // has to be here, and not project-dialog-confirmed because Geany manages the write to the GKeyFile + project_save_properties_tab(config); + // ensure any changes from the project config are picked up before the next execution + autorun_globals->dirtybit = TRUE; +} + +/* Handler to clear any old Project declared Auto-run configs */ +static void on_project_close(G_GNUC_UNUSED GObject* obj, G_GNUC_UNUSED gpointer user_data) { + // unload project handlers + autorun_cmd_list_free(autorun_globals->project_commands); + autorun_globals->project_commands = NULL; +} + +/* Handler to run any applicable Auto-run configs after a write */ +static void on_doc_save(G_GNUC_UNUSED GObject* obj, GeanyDocument* doc, G_GNUC_UNUSED gpointer user_data) { + // make sure we update the projectdefs if the config is dirty + if (autorun_globals->dirtybit) { + if(autorun_globals->data->app->project && autorun_globals->data->app->project->file_name ){ + // reload the projectdefs + GKeyFile* config = g_key_file_new(); + gboolean ret = g_key_file_load_from_file(config, autorun_globals->data->app->project->file_name, G_KEY_FILE_NONE, NULL); + if (ret) { + load_projectdefs(config); + } + g_key_file_free(config); + } + autorun_globals->dirtybit = FALSE; + } + + if (sci_get_length(doc->editor->sci) < 1) { + // no point in processing a file so short + return; + } + /* + * dont reset the msgwin here, as document-save + * is the second stage of document-before-save + * and the messages will all be logically related + * to the user selecting File>Save + */ + dispatch_run_async(doc); +} + +/* Handler to run any applicable Auto-run configs before a write */ +static void on_doc_before_save(G_GNUC_UNUSED GObject* obj, GeanyDocument* doc, G_GNUC_UNUSED gpointer user_data) { + // make sure we update the projectdefs if the config is dirty + if (autorun_globals->dirtybit) { + if(autorun_globals->data->app->project && autorun_globals->data->app->project->file_name ){ + // reload the projectdefs + GKeyFile* config = g_key_file_new(); + gboolean ret = g_key_file_load_from_file(config, autorun_globals->data->app->project->file_name, G_KEY_FILE_NONE, NULL); + if (ret) { + load_projectdefs(config); + } + g_key_file_free(config); + } + autorun_globals->dirtybit = FALSE; + } + + if (!doc->changed) { + return; + } + if (sci_get_length(doc->editor->sci) < 1) { + // no point in processing a file so short + return; + } + // status is used for return codes, so dont clear that one + // compiler is used for all output + msgwin_clear_tab(MSG_COMPILER); + dispatch_run_sync(doc); + // no point in ui_progress_bar here, as + // sync processes lock Geany up until they finish +} + +/* callback to populate Project Auto-run tab for current file type. */ +static void on_project_dialog_open(G_GNUC_UNUSED GObject* obj, GtkWidget* notebook, G_GNUC_UNUSED gpointer user_data) { + // attach to the Project Properties if needed + project_show_properties_tab(notebook); +} + +/* callback to refresh Auto-run configs after Project save */ +static void on_project_dialog_confirm(G_GNUC_UNUSED GObject* obj, GtkWidget* notebook, G_GNUC_UNUSED gpointer user_data) { + // mark the config for reload + autorun_globals->dirtybit = TRUE; + // hide from the Build menu + project_hide_properties_tab(notebook); +} +/* callback to detach Auto-run tab from Project + * workaround for geany/geany#4606 */ +static void on_project_dialog_close(G_GNUC_UNUSED GObject* obj, GtkWidget* notebook, G_GNUC_UNUSED gpointer user_data) { + // detach from the Project Properties + project_hide_properties_tab(notebook); +} + +// clang-format off +PluginCallback plugin_callbacks[] = { + { "project-open", (GCallback)&on_project_open, TRUE, NULL }, + { "project-save", (GCallback)&on_project_save, TRUE, NULL }, + { "project-close", (GCallback)&on_project_close, TRUE, NULL }, + { "document-save", (GCallback)&on_doc_save, TRUE, NULL }, + { "document-before-save", (GCallback)&on_doc_before_save, TRUE, NULL }, + { "project-dialog-open", (GCallback) & on_project_dialog_open, FALSE, NULL}, + { "project-dialog-confirmed", (GCallback) & on_project_dialog_confirm, TRUE, NULL}, + { "project-dialog-close", (GCallback) & on_project_dialog_close, TRUE, NULL}, + { NULL, NULL, FALSE, NULL } +}; +// clang-format on + +/* Bring up plugin and load filetypes.FILE that exist for Auto-run */ +static gboolean autorun_init(GeanyPlugin* plugin, G_GNUC_UNUSED gpointer pdata) { + if (!autorun_globals) { + autorun_globals_init(plugin); + load_filedefs(); + + if (autorun_globals->data->app->project && autorun_globals->data->app->project->file_name) { + // if initialized while a project is already open, manually ingest the project + GKeyFile* config = g_key_file_new(); + gboolean ret = g_key_file_load_from_file(config, autorun_globals->data->app->project->file_name, G_KEY_FILE_NONE, NULL); + if (ret) { + load_projectdefs(config); + } + g_key_file_free(config); + } + } + menu_init(); + project_properties_tab_init(); + return TRUE; +} + +/* ensure destruction of any Auto-run objects */ +static void autorun_cleanup(G_GNUC_UNUSED GeanyPlugin* plugin, G_GNUC_UNUSED gpointer pdata) { + if (autorun_globals) { + autorun_globals_free(); + } + menu_cleanup(); + project_properties_tab_cleanup(); +} + +G_MODULE_EXPORT +void geany_load_module(GeanyPlugin* plugin) { + // who am we? + plugin->info->name = "Auto-run"; + plugin->info->description = _("Geany action interceptor plugin"); + plugin->info->version = "0.1"; + plugin->info->author = "WrapEarnPass"; + + // english do you speak? + main_locale_init(LOCALEDIR, GETTEXT_PACKAGE); + + // what am we do? + plugin->funcs->init = autorun_init; + plugin->funcs->cleanup = autorun_cleanup; + plugin->funcs->callbacks = plugin_callbacks; + + // go forth and come fifth. + GEANY_PLUGIN_REGISTER(plugin, 225); +} diff --git a/autorun/src/project.c b/autorun/src/project.c new file mode 100644 index 000000000..a63568b95 --- /dev/null +++ b/autorun/src/project.c @@ -0,0 +1,275 @@ +/* + * Copyright 2026 WrapEarnPass + * + * 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. + */ +#include + +#include "autorun.h" +#include "project.h" +#include "utils.h" + +// container for GtkWidget references +typedef struct { + GtkWidget* notebook; + GtkWidget* tab; + GtkWidget* before_save_command; + GtkWidget* before_save_workdir; + GtkWidget* on_save_command; + GtkWidget* on_save_workdir; +} AUTORUN_PROJECT_DATA; +static AUTORUN_PROJECT_DATA project_data; + +/* callback to save changes to the Project to disk + * config keyfile provided by Geany */ +void project_save_properties_tab(GKeyFile* config) { + // grab the current filetype + if (!document_get_current()) { + // Auto-run only supports file specific handlers + return; + } + gchar* filetype_name = document_get_current()->file_type->name; + gchar *before_save_command, *before_save_workdir, *on_save_command, *on_save_workdir; + before_save_command = g_strdup(gtk_entry_get_text(GTK_ENTRY(project_data.before_save_command))); + before_save_workdir = g_strdup(gtk_entry_get_text(GTK_ENTRY(project_data.before_save_workdir))); + on_save_command = g_strdup(gtk_entry_get_text(GTK_ENTRY(project_data.on_save_command))); + on_save_workdir = g_strdup(gtk_entry_get_text(GTK_ENTRY(project_data.on_save_workdir))); + + // write the configs to file. + gchar *bs_cm_key, *bs_wd_key, *os_cm_key, *os_wd_key; + bs_cm_key = g_strconcat(filetype_name, "_", "BS", "_", "00", "_", "CM", NULL); + if (before_save_command && strlen(before_save_command) > 1) { + // set it + g_key_file_set_string(config, "autorun", bs_cm_key, before_save_command); + } else { + // delete it + g_key_file_remove_key(config, "autorun", bs_cm_key, NULL); + } + bs_wd_key = g_strconcat(filetype_name, "_", "BS", "_", "00", "_", "WD", NULL); + if (before_save_workdir && strlen(before_save_command) > 1) { + // set it + g_key_file_set_string(config, "autorun", bs_wd_key, before_save_workdir); + } else { + // delete it + g_key_file_remove_key(config, "autorun", bs_wd_key, NULL); + } + os_cm_key = g_strconcat(filetype_name, "_", "OS", "_", "00", "_", "CM", NULL); + if (on_save_command && strlen(before_save_command) > 1) { + // set it + g_key_file_set_string(config, "autorun", os_cm_key, on_save_command); + } else { + // delete it + g_key_file_remove_key(config, "autorun", os_cm_key, NULL); + } + os_wd_key = g_strconcat(filetype_name, "_", "OS", "_", "00", "_", "WD", NULL); + if (on_save_workdir && strlen(before_save_command) > 1) { + // set it + g_key_file_set_string(config, "autorun", os_wd_key, on_save_workdir); + } else { + // delete it + g_key_file_remove_key(config, "autorun", os_wd_key, NULL); + } + + g_free(before_save_command); + g_free(before_save_workdir); + g_free(on_save_command); + g_free(on_save_workdir); + g_free(bs_cm_key); + g_free(bs_wd_key); + g_free(os_cm_key); + g_free(os_wd_key); +} + +/* ensure that the Project>Auto-run is hidden when not in use + * notebook a reference to Project Properties from Geany */ +void project_hide_properties_tab(GtkWidget* notebook) { + gtk_notebook_detach_tab(GTK_NOTEBOOK(notebook), project_data.tab); + project_data.notebook = NULL; +} + +/* ensure that the Project>Auto-run is shown when needed + * notebook a reference to Project Properties from Geany */ +void project_show_properties_tab(GtkWidget* notebook) { + // grab the current filetype + if (g_strcmp0("None", document_get_current()->file_type->name) == 0 || !autorun_globals->data->app->project) { + // Auto-run only supports file specific handlers + // hide the tab + return; + } + + // um, are we on this noteboot? + if (!project_data.notebook) { + GtkWidget* label = gtk_label_new("Auto-run"); + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), GTK_WIDGET(g_object_ref(project_data.tab)), label); + gtk_widget_show_all(notebook); + project_data.notebook = notebook; + } + + gchar* filetype_name = document_get_current()->file_type->name; + + // attach the configs to the project + GKeyFile* config = g_key_file_new(); + gboolean ret = g_key_file_load_from_file(config, autorun_globals->data->app->project->file_name, G_KEY_FILE_NONE, NULL); + if (ret) { + gchar *before_save_command, *before_save_workdir, *on_save_command, *on_save_workdir; + + // read the current configs + gchar* key_name; + /* this looks wierd, but _ BS OS CM are all fixed strings and prevents + * static allocation of a fixed string only used here. */ + key_name = g_strconcat(filetype_name, "_", "BS", "_", "00", "_", "CM", NULL); + before_save_command = utils_get_setting_string(config, "autorun", key_name, ""); + g_free(key_name); + key_name = g_strconcat(filetype_name, "_", "BS", "_", "00", "_", "WD", NULL); + before_save_workdir = utils_get_setting_string(config, "autorun", key_name, ""); + g_free(key_name); + key_name = g_strconcat(filetype_name, "_", "OS", "_", "00", "_", "CM", NULL); + on_save_command = utils_get_setting_string(config, "autorun", key_name, ""); + g_free(key_name); + key_name = g_strconcat(filetype_name, "_", "OS", "_", "00", "_", "WD", NULL); + on_save_workdir = utils_get_setting_string(config, "autorun", key_name, ""); + g_free(key_name); + + // update the notebook + gtk_entry_set_text(GTK_ENTRY(project_data.before_save_command), before_save_command); + gtk_entry_set_text(GTK_ENTRY(project_data.before_save_workdir), before_save_workdir); + gtk_entry_set_text(GTK_ENTRY(project_data.on_save_command), on_save_command); + gtk_entry_set_text(GTK_ENTRY(project_data.on_save_workdir), on_save_workdir); + + if (before_save_command) { + g_free(before_save_command); + } + if (before_save_workdir) { + g_free(before_save_workdir); + } + if (on_save_command) { + g_free(on_save_command); + } + if (on_save_workdir) { + g_free(on_save_workdir); + } + } + g_key_file_free(config); +} + +/* Gtk teardown for Auto-run tab */ +void project_properties_tab_cleanup(void) { + gtk_widget_destroy(GTK_WIDGET(project_data.before_save_command)); + project_data.before_save_command = NULL; + gtk_widget_destroy(GTK_WIDGET(project_data.before_save_workdir)); + project_data.before_save_workdir = NULL; + gtk_widget_destroy(GTK_WIDGET(project_data.on_save_command)); + project_data.on_save_command = NULL; + gtk_widget_destroy(GTK_WIDGET(project_data.on_save_workdir)); + project_data.on_save_workdir = NULL; + g_object_ref_sink(project_data.tab); + gtk_widget_destroy(GTK_WIDGET(project_data.tab)); + project_data.tab = NULL; + // noteboot isn't ours. + project_data.notebook = NULL; +} + +/* Gtk setup for Auto-run tab */ +void project_properties_tab_init(void) { + // do we need to build a tab? + if (project_data.before_save_command) { + // this smells familiar, lets not. + return; + } + GtkWidget *vbox, *hbox, *ebox, *table_box; + GtkWidget *label, *seperator; + GtkSizeGroup* size_group; + + vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + + table_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_box_set_spacing(GTK_BOX(table_box), 6); + + size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + + seperator = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL); + gtk_box_pack_start(GTK_BOX(table_box), seperator, TRUE, FALSE, 0); + + label = gtk_label_new(_("Before Save Command")); + gtk_label_set_xalign(GTK_LABEL(label), 0.0); + gtk_size_group_add_widget(size_group, label); + project_data.before_save_command = gtk_entry_new(); + ui_entry_add_clear_icon(GTK_ENTRY(project_data.before_save_command)); + gtk_widget_set_tooltip_text( + project_data.before_save_command, + _("supports replacements: %f = filename, %a = filename with path, %d = path, %p = project directory") + ); + + ebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start(GTK_BOX(ebox), label, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(ebox), project_data.before_save_command, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(table_box), ebox, TRUE, FALSE, 0); + + label = gtk_label_new(_("Working Directory")); + gtk_label_set_xalign(GTK_LABEL(label), 0.0); + gtk_size_group_add_widget(size_group, label); + project_data.before_save_workdir = gtk_entry_new(); + ui_entry_add_clear_icon(GTK_ENTRY(project_data.before_save_workdir)); + gtk_widget_set_tooltip_text( + project_data.before_save_workdir, + _("supports replacements: %f = filename, %a = filename with path, %d = path, %p = project directory") + ); + + ebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start(GTK_BOX(ebox), label, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(ebox), project_data.before_save_workdir, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(table_box), ebox, TRUE, FALSE, 0); + + seperator = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL); + gtk_box_pack_start(GTK_BOX(table_box), seperator, TRUE, FALSE, 0); + + label = gtk_label_new(_("On Save Command")); + gtk_label_set_xalign(GTK_LABEL(label), 0.0); + gtk_size_group_add_widget(size_group, label); + project_data.on_save_command = gtk_entry_new(); + ui_entry_add_clear_icon(GTK_ENTRY(project_data.on_save_command)); + gtk_widget_set_tooltip_text( + project_data.on_save_command, + _("supports replacements: %f = filename, %a = filename with path, %d = path, %p = project directory") + ); + ebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start(GTK_BOX(ebox), label, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(ebox), project_data.on_save_command, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(table_box), ebox, TRUE, FALSE, 0); + label = gtk_label_new(_("Working Directory")); + gtk_label_set_xalign(GTK_LABEL(label), 0.0); + gtk_size_group_add_widget(size_group, label); + project_data.on_save_workdir = gtk_entry_new(); + ui_entry_add_clear_icon(GTK_ENTRY(project_data.on_save_workdir)); + gtk_widget_set_tooltip_text( + project_data.on_save_workdir, + _("supports replacements: %f = filename, %a = filename with path, %d = path, %p = project directory") + ); + ebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start(GTK_BOX(ebox), label, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(ebox), project_data.on_save_workdir, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(table_box), ebox, TRUE, FALSE, 0); + + seperator = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL); + gtk_box_pack_start(GTK_BOX(table_box), seperator, TRUE, FALSE, 0); + + gtk_box_pack_start(GTK_BOX(vbox), table_box, FALSE, FALSE, 6); + + hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start(GTK_BOX(hbox), vbox, TRUE, TRUE, 6); + + // grab a reference to the new tab, so we can detach/retach later. + project_data.tab = GTK_WIDGET(g_object_ref(hbox)); +} diff --git a/autorun/src/project.h b/autorun/src/project.h new file mode 100644 index 000000000..0d0fcaba7 --- /dev/null +++ b/autorun/src/project.h @@ -0,0 +1,30 @@ +/* + * Copyright 2026 WrapEarnPass + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef __AUTORUN_PROJECT_H__ +#define __AUTORUN_PROJECT_H__ + +#include + +void project_save_properties_tab(GKeyFile* config); +void project_show_properties_tab(GtkWidget* notebook); +void project_hide_properties_tab(GtkWidget* notebook); +void project_properties_tab_cleanup(void); +void project_properties_tab_init(void); + +#endif diff --git a/autorun/src/spawn.c b/autorun/src/spawn.c new file mode 100644 index 000000000..f9fa0780d --- /dev/null +++ b/autorun/src/spawn.c @@ -0,0 +1,361 @@ +/* + * Copyright 2026 WrapEarnPass + * + * 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. + */ + +#include +#include + +#include "autorun.h" +#include "spawn.h" +#include "utils.h" + +// container for spawn_with_callbacks IO +typedef struct { + GString* stdout_str; + GString* stderr_str; + GeanyDocument* doc; + +} AUTORUN_ASYNC_DATA; + +/* prep a command for running + * interceptor which command type to run + * doc which doc to run against + * command the returned prepped commands, owned by the caller */ +static void parse_command(const gchar* interceptor, GeanyDocument* doc, GSList** command) { + GSList* command_list = NULL; + + // build the commands backwads + GSList* elem = NULL; + foreach_slist(elem, autorun_globals->filedef_commands) { + AUTORUN_CMD* cmd = ((AUTORUN_CMD*)elem->data); + if (!cmd->invalid && g_strcmp0(cmd->interceptor, interceptor) == 0 && cmd->file_type == doc->file_type) { + command_list = g_slist_prepend(command_list, cmd); + } + } + foreach_slist(elem, autorun_globals->project_commands) { + AUTORUN_CMD* cmd = ((AUTORUN_CMD*)elem->data); + if (!cmd->invalid && g_strcmp0(cmd->interceptor, interceptor) == 0 && cmd->file_type == doc->file_type) { + command_list = g_slist_prepend(command_list, cmd); + } + } + // now flip it on them. + command_list = g_slist_reverse(command_list); + if (g_slist_length(command_list) > 0) { + // now if there are any cmds in scope, honor order + GSList* command_order[g_slist_length(command_list)] = {}; + foreach_slist(elem, command_list) { + AUTORUN_CMD* cmd = ((AUTORUN_CMD*)elem->data); + command_order[cmd->order] = elem; + } + + for (guint i = 0; i < g_slist_length(command_list); i++) { + if (!command_order[i]) { + break; // stop on the first null; + } + AUTORUN_CMD* cmd = (AUTORUN_CMD*)command_order[i]->data; + + // prep replacements. + gchar* target_dir; + if (doc->real_path) { + target_dir = g_path_get_dirname(doc->real_path); + } else { + // where else but here? + target_dir = g_strdup("."); + } + gchar* target_projdir; + if (autorun_globals->data->app->project && autorun_globals->data->app->project->base_path) { + target_projdir = utils_get_locale_from_utf8(autorun_globals->data->app->project->base_path); + } else { + // fold %p to %d if no project. + target_projdir = g_strdup(target_dir); + } + gchar* target_file = NULL; + GFile* tmpfile = NULL; + GFileIOStream* iostream = NULL; + gboolean success = FALSE; + + if (g_strcmp0(interceptor, "BS") == 0 && g_strrstr(cmd->command, "%a")) { + //%a means something special + tmpfile = g_file_new_tmp("ar.aXXXXXX", &iostream, NULL); + if (tmpfile) { + gchar* before_contents; + // dont need the iostream, but gtk wont make a temp without it. + g_io_stream_close((GIOStream*)iostream, NULL, NULL); + target_file = g_file_get_path(tmpfile); + // remove the trailing \0 on write + gint con_len = sci_get_length(doc->editor->sci); + before_contents = sci_get_contents(doc->editor->sci, con_len); + success = g_file_set_contents_full(target_file, before_contents, con_len - 1, G_FILE_SET_CONTENTS_CONSISTENT, 0660, NULL); + g_free(before_contents); + + } else { + success = FALSE; + } + } else { + //%a just means file. + success = TRUE; + target_file = utils_get_locale_from_utf8(doc->file_name); + } + + // prep for escape and replace + gchar* target_name = g_path_get_basename(target_file); + + // OS specific escape handling. + gchar* escaped_name = escape_filename(target_name); + g_free(target_name); + gchar* escaped_file = escape_filename(target_file); + gchar* escaped_dir = escape_filename(target_dir); + gchar* escaped_projdir = escape_filename(target_projdir); + + // do the replacement bits + GString* command_str = g_string_new(cmd->command); + // replace %f + g_string_replace(command_str, "%f", escaped_name, 0); + // replace %a + g_string_replace(command_str, "%a", escaped_file, 0); + + GString* working_dir_str = g_string_new(cmd->working_dir); + // replace %d + g_string_replace(command_str, "%d", escaped_dir, 0); + g_string_replace(working_dir_str, "%d", target_dir, 0); + + // replace %p + g_string_replace(command_str, "%p", escaped_projdir, 0); + g_string_replace(working_dir_str, "%p", target_projdir, 0); + + if (success) { + success = spawn_check_command(command_str->str, TRUE, NULL); + } + if (success) { + // attach this command to the outgoing GSList + AUTORUN_CMD* cmdout = autorun_cmd_new(); + cmdout->command = g_string_free_and_steal(command_str); + cmdout->working_dir = g_string_free_and_steal(working_dir_str); + if (tmpfile) { + cmdout->interceptor = g_strdup(target_file); + } else { + cmdout->interceptor = NULL; + } + cmdout->invalid = FALSE; + *command = g_slist_append(*command, cmdout); + } else { + // cant assemble this command, don't try again + cmd->invalid = TRUE; + msgwin_status_add(_("Cannot parse command '%s'"), command_str->str); + } + // cleanup + g_free(target_dir); + g_free(escaped_dir); + g_free(target_projdir); + g_free(escaped_projdir); + if (tmpfile) { + g_object_unref(tmpfile); + } + g_free(target_file); + g_free(escaped_file); + } + // cleanup + // autorun_cmd_list_free(command_list); + // we dont own the contents of command_list. + g_slist_free(command_list); + } +} + +/* spawn_with_callbacks IO callback + * string the value provided by the IO + * condition what type of IO this was + * data the associated data for this callback */ +static void stdio_cb(GString* string, GIOCondition condition, gpointer data) { + // dup the string to data. + if (condition == G_IO_IN || condition == G_IO_PRI) { + g_string_append((GString*)data, string->str); + } +} +/* spawn_with_callbacks child exit IO callback + * pid child pid + * wait_status exit type + * user_data the associated data for this child */ +static void stdioend_cb(G_GNUC_UNUSED GPid pid, G_GNUC_UNUSED gint wait_status, gpointer user_data) { + // spawn_with_callbacks runs this last and closes the child + AUTORUN_ASYNC_DATA* exit_data = (AUTORUN_ASYNC_DATA*)user_data; + // our stdout/stderr is complete. Display it. + parse_output(exit_data->stdout_str->str); + g_string_free(exit_data->stdout_str, TRUE); + parse_output(exit_data->stderr_str->str); + g_string_free(exit_data->stderr_str, TRUE); + // if all the children have stopped + if (--autorun_globals->children < 1) { + // flip the ui_progress_bar back + ui_progress_bar_stop(); + ui_set_statusbar(FALSE, _("Auto-run finished %s"), exit_data->doc->file_name); + autorun_globals->children = 0; + } + g_free(exit_data); +} + +/* function to dispatch a command asynchronously + * doc the GeanyDocument to run against */ +void dispatch_run_async(GeanyDocument* doc) { + GSList* runnables = NULL; + // get commands in scope + parse_command("OS", doc, &runnables); + GSList* runnable; + ui_progress_bar_start("Auto-run"); + foreach_slist(runnable, runnables) { + AUTORUN_CMD* current_cmd = (AUTORUN_CMD*)runnable->data; + + gchar** env; + env = utils_copy_environment(NULL, "GEANY_FUNCNAME", __func__, NULL); + GError* error = NULL; + + gchar* status_msg = g_strdup_printf(_("Running %s (from %s)"), current_cmd->command, current_cmd->working_dir); + msgwin_compiler_add_string(COLOR_BLUE, status_msg); + g_free(status_msg); + GString* stdout_data = g_string_new(NULL); + GString* stderr_data = g_string_new(NULL); + AUTORUN_ASYNC_DATA* end_data = g_new0(AUTORUN_ASYNC_DATA, 1); + end_data->stdout_str = stdout_data; + end_data->stderr_str = stderr_data; + end_data->doc = doc; + GPid* child_pid = NULL; + gboolean success = FALSE; + // set the the ui_statusbar and record a child + ui_set_statusbar(FALSE, _("Auto-run running %s"), current_cmd->command); + ++autorun_globals->children; + + success = spawn_with_callbacks(current_cmd->working_dir, current_cmd->command, NULL, env, SPAWN_ASYNC | SPAWN_LINE_BUFFERED, NULL, NULL, stdio_cb, stdout_data, 0, + stdio_cb, stderr_data, 0, stdioend_cb, end_data, child_pid, &error); + + // cleanup anything we dont need in processing. + g_strfreev(env); + + if (!success) { + // if that was the only child and it failed + if (--autorun_globals->children < 1) { + // flip the ui_progress_bar back + ui_progress_bar_stop(); + autorun_globals->children = 0; + } + // update status with refused to run + msgwin_status_add(_("Command failed with %s"), error->message); + g_error_free(error); + } + } + autorun_cmd_list_free(runnables); +} + +/* function to dispatch a command synchronously + * doc the GeanyDocument to run against + * + * only intended for intercept type BS, as it locks up the editor */ +void dispatch_run_sync(GeanyDocument* doc) { + GSList* runnables = NULL; + parse_command("BS", doc, &runnables); + GSList* runnable; + foreach_slist(runnable, runnables) { + AUTORUN_CMD* current_cmd = (AUTORUN_CMD*)runnable->data; + // pull the tempfile from the overloaded cmd->interceptor + gboolean has_tmpfile = (current_cmd->interceptor) ? TRUE : FALSE; + gchar* tmpfile_path = current_cmd->interceptor; + + gchar** env; + env = utils_copy_environment(NULL, "GEANY_FUNCNAME", __func__, NULL); + gint ret; + GError* error = NULL; + GString* stdout_data = g_string_new(NULL); + GString* stderr_data = g_string_new(NULL); + SpawnWriteData* stdin_data; + + gint con_len = sci_get_length(doc->editor->sci); + gchar* before_contents = sci_get_contents(doc->editor->sci, con_len); + if (!has_tmpfile) { + // need to send stdin. + stdin_data = g_new0(SpawnWriteData, 1); + stdin_data->ptr = before_contents; + stdin_data->size = con_len; + } else { + // no stdin + stdin_data = NULL; + } + gchar* status_msg = g_strdup_printf(_("Running %s (from %s)"), current_cmd->command, current_cmd->working_dir); + msgwin_compiler_add_string(COLOR_BLUE, status_msg); + g_free(status_msg); + // runnit + gboolean success = spawn_sync(current_cmd->working_dir, current_cmd->command, NULL, env, stdin_data, stdout_data, stderr_data, &ret, &error); + + // cleanup anything we dont need in processing. + if (stdin_data) { + g_free(stdin_data); + } + g_strfreev(env); + + if (success && ret == 0) { + // ran and didn't throw an error + // do we need to update the editor? + // could be in tmpfile, or stdout + gchar* read_tmp = NULL; + if (has_tmpfile) { + g_file_get_contents(tmpfile_path, &read_tmp, NULL, NULL); + } + gint cursor_at = sci_get_current_position(doc->editor->sci); + // check for the stupid case + if (strlen(stdout_data->str) > 0 && has_tmpfile && g_strcmp0(read_tmp, before_contents) != 0 && g_strcmp0(stdout_data->str, read_tmp) != 0) { + /* if there are changes to the tempfile AND stdout + * this command is stupid. there is no way to know + * which output is the output to output. */ + msgwin_status_add(_("Command failed with inconsistent output.")); + } else if (strlen(stdout_data->str) > 0) { + sci_set_text(doc->editor->sci, stdout_data->str); + // put ze cursor bek + sci_set_current_position(doc->editor->sci, cursor_at, TRUE); + } else if (has_tmpfile && g_strcmp0(read_tmp, before_contents) != 0) { + sci_set_text(doc->editor->sci, read_tmp); + // put ze cursor bek + sci_set_current_position(doc->editor->sci, cursor_at, TRUE); + } + if (read_tmp) { + g_free(read_tmp); + } + // dont update the editor if it ran with an error. + } else if (!success) { + // refused to run + msgwin_status_add(_("Command failed with %s"), error->message); + ui_set_statusbar(FALSE, _("Auto-run before-save interceptor failed.")); + g_error_free(error); + } + + /* GUI output formatting + * stdout goes to the scintilla editor + * only stderr needs processing */ + if (strlen(stderr_data->str) > 0) { + parse_output(stderr_data->str); + } + + // cleanup + if (has_tmpfile) { + g_unlink(tmpfile_path); + } + + g_string_free(stdout_data, TRUE); + g_string_free(stderr_data, TRUE); + + if (before_contents) { + g_free(before_contents); + } + } + autorun_cmd_list_free(runnables); +} diff --git a/autorun/src/spawn.h b/autorun/src/spawn.h new file mode 100644 index 000000000..a5316ec52 --- /dev/null +++ b/autorun/src/spawn.h @@ -0,0 +1,27 @@ +/* + * Copyright 2026 WrapEarnPass + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef __AUTORUN_SPAWN_H__ +#define __AUTORUN_SPAWN_H__ + +#include + +void dispatch_run_async(GeanyDocument* doc); +void dispatch_run_sync(GeanyDocument* doc); + +#endif diff --git a/autorun/src/utils.c b/autorun/src/utils.c new file mode 100644 index 000000000..9e2daf4c1 --- /dev/null +++ b/autorun/src/utils.c @@ -0,0 +1,195 @@ +/* + * Copyright 2026 WrapEarnPass + * + * 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. + */ + +#include + +#include "autorun.h" +#include "utils.h" + +/* Read a filedef or keyfile for Intercept Actions + * key which interceptor to read (should end with CM) + * key_file to get remaining values from + * cmd AUTORUN_CMD struct to store data to */ +void parse_intercept_actions(gchar* key, GKeyFile* key_file, AUTORUN_CMD* cmd) { + // key should be a (filetype_)action_number_flag string + if (!key || !cmd) { + return; + } + // is this a key? + if (!g_strstr_len(key, -1, "_")) { + // no + cmd->invalid = TRUE; + return; + } + cmd->invalid = FALSE; + // what is this key? + gchar** tokens; + tokens = g_strsplit(key, "_", 4); + + switch (g_strv_length(tokens)) { + // strv of 3 is filedef, 4 is project + case 3: { + // the caller has to set filetype + if (g_strcmp0(tokens[0], "BS") == 0) { + cmd->interceptor = g_strdup(tokens[0]); + } else if (g_strcmp0(tokens[0], "OS") == 0) { + cmd->interceptor = g_strdup(tokens[0]); + } else { + cmd->invalid = TRUE; + } + + gint offset = atoi(tokens[1]); + if (offset < 0 || offset > 99) { + cmd->invalid = TRUE; + } else { + cmd->order = offset; + } + if (g_strcmp0(tokens[2], "CM") == 0) { + cmd->command = utils_get_setting_string(key_file, "autorun", key, ""); + if (strlen(cmd->command) < 2) { + // GKeyFile doesnt actually delete keys, but sets them to "" + cmd->invalid = TRUE; + break; + } + gchar* next_key; + next_key = g_strconcat(tokens[0], "_", tokens[1], "_", "WD", NULL); + cmd->working_dir = utils_get_setting_string(key_file, "autorun", next_key, ""); + g_free(next_key); + } else { + cmd->invalid = TRUE; + } + break; + } + case 4: { + cmd->file_type = filetypes_lookup_by_name(tokens[0]); + + if (g_strcmp0(tokens[1], "BS") == 0) { + cmd->interceptor = g_strdup(tokens[1]); + } else if (g_strcmp0(tokens[1], "OS") == 0) { + cmd->interceptor = g_strdup(tokens[1]); + } else { + cmd->invalid = TRUE; + } + + gint offset = atoi(tokens[2]); + if (offset < 0 || offset > 99) { + cmd->invalid = TRUE; + } else { + cmd->order = offset; + } + + if (g_strcmp0(tokens[3], "CM") == 0) { + cmd->command = utils_get_setting_string(key_file, "autorun", key, ""); + gchar* next_key; + next_key = g_strconcat(tokens[0], "_", tokens[1], "_", tokens[2], "_", "WD", NULL); + cmd->working_dir = utils_get_setting_string(key_file, "autorun", next_key, ""); + g_free(next_key); + } else { + cmd->invalid = TRUE; + } + break; + } + default: { + cmd->invalid = TRUE; + } + } + + g_strfreev(tokens); +} + +/* split stdout and stderr into blocks and format them for Compiler + * input the current data to parse */ +void parse_output(gchar* input) { + // make sure we're using utf8 + gchar* output = encodings_convert_to_utf8(input, -1, NULL); + // split the output on \n + gchar** lines = g_regex_split_simple("\R", output, 0, G_REGEX_MATCH_NEWLINE_ANYCRLF); + gchar** line = NULL; + + // foreach line, if it contains a filename, it is red. + foreach_strv(line, lines) { + guint i; + gboolean found = FALSE; + GeanyData* geany_data = autorun_globals->data; + foreach_document(i) { + GeanyDocument* doc_test = documents[i]; + gchar* locale_name = utils_get_locale_from_utf8(doc_test->file_name); + gchar* base_test = g_path_get_basename(locale_name); + if (g_strrstr(*line, base_test)) { + // contains a known filename + found = TRUE; + break; + } + if (locale_name) { + g_free(locale_name); + } + if (base_test) { + g_free(base_test); + } + } + /* make known filenames clicky (perhaps) + * the compiler requires each newline to be its own message + * or the compiler clicky breaks. + * the compiler requires each clicky to be RED + * or the compiler clicky breaks. */ + if (found) { + msgwin_compiler_add_string(COLOR_RED, *line); + } else { + msgwin_compiler_add_string(COLOR_BLACK, *line); + } + } + + line = NULL; + g_strfreev(lines); + if (output) { + g_free(output); + } +} +/* attempt to escape a filename or directory so it is safe inside of + * a platform specific quote + * the caller owns the returned string */ +gchar* escape_filename(gchar* filename) { + GString* filename_str = g_string_new(filename); +#ifdef G_OS_WIN32 + /*the goal is that the output from this function should work inside of a + double-quoted execve argument. + * From https://www.robvanderwoude.com/escapechars.php*/ + // % %% + g_string_replace(filename_str, "%", "%%", 0); + // ^ ^^ May not always be required in doublequoted strings, but it won't hurt + g_string_replace(filename_str, "^", "^^", 0); + // & ^& + g_string_replace(filename_str, "&", "^&", 0); + // < ^< + g_string_replace(filename_str, "<", "^<", 0); + // > ^> + g_string_replace(filename_str, ">", "^>", 0); + // | ^| + g_string_replace(filename_str, "|", "^|", 0); +#else + /*the goal is that the output from this function should work inside of a + single quoted execve argument. + File: `'~!@#$%^&*()_-+={}[] \ |:;"<>?,.c (created through GUI) + ls suggested + '`'\''~!@#$%^&*()_-+={}[] \ |:;"<>?,.c' + */ + // ' '\'' + g_string_replace(filename_str, "'", "'\\''", 0); +#endif + return g_string_free_and_steal(filename_str); +} diff --git a/autorun/src/utils.h b/autorun/src/utils.h new file mode 100644 index 000000000..f9991f63a --- /dev/null +++ b/autorun/src/utils.h @@ -0,0 +1,29 @@ +/* + * Copyright 2026 WrapEarnPass + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#ifndef __AUTORUN_UTILS_H__ +#define __AUTORUN_UTILS_H__ + +#include + +#include "autorun.h" + +void parse_intercept_actions(gchar* key, GKeyFile* key_file, AUTORUN_CMD* cmd); +void parse_output(gchar* output); +gchar* escape_filename(gchar* filename); + +#endif diff --git a/build/autorun.m4 b/build/autorun.m4 new file mode 100644 index 000000000..448856ce2 --- /dev/null +++ b/build/autorun.m4 @@ -0,0 +1,9 @@ + AC_DEFUN([GP_CHECK_AUTORUN], + [ + GP_ARG_DISABLE([autorun], [auto]) + GP_COMMIT_PLUGIN_STATUS([Autorun]) + AC_CONFIG_FILES([ + autorun/Makefile + autorun/src/Makefile + ]) + ]) diff --git a/configure.ac b/configure.ac index 1e8148a96..5bd32fd29 100644 --- a/configure.ac +++ b/configure.ac @@ -36,6 +36,7 @@ dnl plugin checks GP_CHECK_ADDONS GP_CHECK_AUTOCLOSE GP_CHECK_AUTOMARK +GP_CHECK_AUTORUN GP_CHECK_CODENAV GP_CHECK_COMMANDER GP_CHECK_DEBUGGER diff --git a/po/POTFILES.in b/po/POTFILES.in index 983ad9e62..071e83091 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -19,6 +19,12 @@ autoclose/src/autoclose.c # Automark automark/src/automark.c +# Autorun +autorun/src/plugin_main.c +autorun/src/menu.c +autorun/src/project.c +autorun/src/spawn.c + # codenav codenav/src/codenavigation.c codenav/src/goto_file.c