├── .ert-runner ├── .gitignore ├── LICENSE ├── README.md ├── default.nix ├── org-runbook-ivy.el ├── org-runbook.el ├── runbook.org ├── shell.nix └── test ├── bookmark.org ├── no-commands ├── fundamental-mode.org └── org-runbook.org ├── one-command └── fundamental-mode.org ├── org-runbook-test.el ├── test-helper.el └── test-runbook.org /.ert-runner: -------------------------------------------------------------------------------- 1 | -l test/org-runbook-test.el 2 | --reporter dot -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled 2 | *.elc 3 | 4 | # Packaging 5 | .cask 6 | 7 | # Backup files 8 | *~ 9 | 10 | # Undo-tree save-files 11 | *.~undo-tree 12 | .DS_Store 13 | result -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # org-runbook.el 2 | [![License](https://img.shields.io/badge/license-GPL_3-green.svg)](https://www.gnu.org/licenses/gpl-3.0.txt) 3 | [![MELPA](https://melpa.org/packages/org-runbook-badge.svg)](https://melpa.org/#/org-runbook) 4 | [![Version](https://img.shields.io/github/v/tag/tyler-dodge/org-runbook)](https://github.com/tyler-dodge/org-runbook/releases) 5 | 6 | --- 7 | 8 | Library for looking up and executing commands from org files corresponding to the current buffer. 9 | 10 | ## Installation 11 | 12 | Org-runbook is available on [MELPA](http://melpa.org) 13 | 14 | M-x `package-install` [RET] `org-runbook` [RET] 15 | 16 | ## Usage 17 | 18 | ### Example 19 | 20 | org-runbook lets you take org files structured like 21 | 22 | #### MAJOR-MODE.org 23 | ``` 24 | * Build 25 | #+BEGIN_SRC shell 26 | cd {{project_root}} 27 | #+END_SRC 28 | 29 | ** Quick 30 | #+BEGIN_SRC shell 31 | make quick 32 | #+END_SRC 33 | 34 | ** Clean 35 | #+BEGIN_SRC shell 36 | make clean 37 | #+END_SRC 38 | 39 | ** Prod 40 | #+BEGIN_SRC shell 41 | make prod 42 | #+END_SRC 43 | ``` 44 | 45 | and exposes them for easy access in buffers with corresponding major mode. 46 | So, the function [org-runbook-execute](#org-runbook-execute) has the following completions when the current buffer's major mode is MAJOR-MODE: 47 | 48 | ``` 49 | Build >> Quick 50 | Build >> Clean 51 | Build >> Prod 52 | ``` 53 | 54 | Each of these commands is the concatenation of the path of the tree. So for example, Build >> Quick would resolve to: 55 | 56 | ``` 57 | cd {{project_root}} 58 | make quick 59 | ``` 60 | 61 | If projectile-mode is installed, org-runbook also pulls the file named PROJECTILE-PROJECT-NAME.org. 62 | 63 | All files in [org-runbook-files] are also pulled. 64 | 65 | ### runbook org file search order 66 | 67 | org-runbook search the org files for runbook in the following order. 68 | 69 | 1. Current File if the file is an org file. 70 | 2. `org-runbook-project-directory`/.org 71 | 3. /runbook.org 72 | 4. `org-runbook-modes-directory`/.org 73 | 5. `org-runbook-files` 74 | 75 | The current search list can be seen by calling `org-runbook-org-file-list` 76 | 77 | ### Eshell Support 78 | 79 | Install the eshell commands by calling 80 | ``` 81 | (org-runbook-install-eshell) 82 | ``` 83 | 84 | Calling `org-runbook` from eshell with no args outputs the available commands 85 | ``` 86 | ~ $ org-runbook 87 | ``` 88 | 89 | Any of the command names can be passed as an argument to org-runbook, 90 | and it will evaluate the corresponding command in eshell. 91 | 92 | ``` 93 | ~ $ org-runbook 'Build >> Quick' 94 | ``` 95 | 96 | The view flag generates portable output for exporting from org-runbook to bash. 97 | 98 | ``` 99 | ~ $ org-runbook --view 'Build >> Quick' 100 | ``` 101 | 102 | ### Placeholders 103 | Commands will resolve placeholders before evaluating. 104 | 105 | * {{project_root}} - the projectile-project-root of the buffer that called `org-runbook-execute' 106 | 107 | * {{current_file}} - the file that the buffer that called org-runbook-execute was visiting. If the the buffer is a non file buffer, current_file is default-directory 108 | 109 | ### Interactive Commands 110 | 111 | org-runbook exposes a few commands meant to be example entry points using completing read. 112 | 113 | * [org-runbook-ivy](#org-runbook-ivy) Prompt for command completion and execute the selected command. The rest of the interactive commands 114 | are accesible through this via the extra actions. 115 | 116 | * [org-runbook-execute](#org-runbook-execute) Prompt for command completion and execute the selected command. 117 | 118 | * [org-runbook-view](#org-runbook-view) Prompt for command completion and view the selected command fully resolved. 119 | 120 | * [org-runbook-goto](#org-runbook-goto) Prompt for command completion and goto where the selected command is defined. 121 | 122 | ## API 123 | 124 | ### Commands 125 | 126 | * [org-runbook-targets](#org-runbook-targets) Return the runbook commands corresponding to the current buffer. 127 | Intended to provide completions for completing-read functions 128 | 129 | * [org-runbook-execute-target-action](#org-runbook-execute-target-action) Execute the command. 130 | Expects the command to be one of the elements of (org-runbook-targets) 131 | 132 | * [org-runbook-view-target-action](#org-runbook-view-target-action) View the command. 133 | Expects the command to be one of the elements of (org-runbook-targets) 134 | 135 | * [org-runbook-goto-target-action](#org-runbook-goto-target-action) Switch to the file where the command is defined. 136 | Expects the command to be one of the elements of (org-runbook-targets) 137 | 138 | ### Org Files 139 | * [org-runbook-switch-to-projectile-file](#org-runbook-switch-to-projectile-file) Switch current buffer to the file corresponding to the current buffer's projectile mode. 140 | 141 | * [org-runbook-switch-to-major-mode-file](#org-runbook-switch-to-major-mode-file) Switch current buffer to the file corresponding to the current buffer's major mode mode. 142 | 143 | ## Customization 144 | 145 | * [org-runbook-files](#org-runbook-files) Global file list used by org runbook. 146 | When resolving commands for the current buffer, org-runbook appends org-runbook-files with the major mode org file and the projectile org file. 147 | 148 | * [org-runbook-project-directory](#org-runbook-project-directory) Directory used to lookup the org file corresponding to the current project. 149 | org-runbook-projectile-file joins org-runbook-project-directory 150 | with the projectile-project-name for the current buffer. 151 | 152 | * [org-runbook-modes-directory](#org-runbook-modes-directory) Directory used to lookup the org file for the current major mode. 153 | org-runbook-major-mode-file joins org-runbook-modes-directory 154 | with the symbol-name of the major-mode for the current buffer. 155 | 156 | * [org-runbook-view-mode-buffer](#org-runbook-view-mode-bxuffer) Buffer used for org-runbook-view-command-action to display the resolved command. 157 | 158 | * [org-runbook-execute-command-action](#org-runbook-execute-command-action) Function called to handle executing the given runbook. 159 | It is provided as a single argument the plist output of org-runbook--shell-command-for-candidate. 160 | 161 | ## Contributing 162 | 163 | Contributions welcome, but forking preferred. 164 | I plan to actively maintain this, but I will be prioritizing features that impact me first. 165 | 166 | I'll look at most pull requests eventually, but there is no SLA on those being accepted. 167 | 168 | Also, I will only respond to pull requests on a case by case basis. 169 | I have no obligation to comment on, justify not accepting, or accept any given pull request. 170 | Feel free to start a fork that has more support in that area. 171 | 172 | If there's a great pull request that I'm slow on accepting, feel free to fork and rename the project. 173 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import {}; 3 | lib = pkgs.fetchFromGitHub { 4 | owner = "tyler-dodge"; 5 | repo = "emacs-package-nix-build"; 6 | rev = "eb109da5900436c7b2ec2a61818a0fc7e2fdce8a"; 7 | hash = "sha256-Iq9VMffjSumE7imFMvHqb0Ydjrfh25fQDD+COBzdt68="; 8 | }; 9 | org-runbook-target = { 10 | name = "org-runbook.el"; 11 | file = ./org-runbook.el; 12 | }; 13 | org-runbook-ivy-target = { 14 | name = "org-runbook-ivy.el"; 15 | file = ./org-runbook-ivy.el; 16 | }; 17 | in import lib { 18 | package = { 19 | name = "org-runbook"; 20 | test_target = ./test; 21 | targets = [ 22 | org-runbook-target 23 | org-runbook-ivy-target 24 | ]; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /org-runbook-ivy.el: -------------------------------------------------------------------------------- 1 | ;;; org-runbook-ivy.el --- Ivy Extension for Org mode for runbooks -*- lexical-binding: t -*- 2 | 3 | ;; Author: Tyler Dodge 4 | ;; Version: 1.1 5 | ;; This program is free software; you can redistribute it and/or modify 6 | ;; it under the terms of the GNU General Public License as published by 7 | ;; the Free Software Foundation, either version 3 of the License, or 8 | ;; (at your option) any later version. 9 | 10 | ;; This program is distributed in the hope that it will be useful, 11 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ;; GNU General Public License for more details. 14 | 15 | ;; You should have received a copy of the GNU General Public License 16 | ;; along with this program. If not, see . 17 | 18 | ;;; Commentary: 19 | ;;; 20 | ;;; 21 | ;;; Code: 22 | 23 | (require 'org-runbook) 24 | (require 'ivy) 25 | 26 | 27 | ;;;###autoload 28 | (defun org-runbook-ivy (arg) 29 | "Prompt for command completion and execute the selected command. 30 | Given a prefix ARG, this shows all available commands. 31 | 32 | The rest of the interactive commands are accesible through this via 33 | the extra actions. See `ivy-dispatching-done'." 34 | (interactive "P") 35 | (if arg (org-runbook-search) 36 | (ivy-read "Command" 37 | (cl-loop for target in (org-runbook-targets) 38 | append 39 | (->> target 40 | (org-runbook-file-targets) 41 | (-map #'org-runbook-target--to-ivy-target))) 42 | :action 'org-runbook-multiaction 43 | :caller 'org-runbook-ivy))) 44 | 45 | ;;;###autoload 46 | (defun org-runbook-search () 47 | "Lookup the targets in all known `org-runbook' files." 48 | (interactive) 49 | (ivy-read "Target" 50 | (cl-loop for target in (org-runbook-all-targets) 51 | collect (org-runbook-target--to-ivy-target target t)) 52 | :caller 'org-runbook-search 53 | :action 'org-runbook-multiaction)) 54 | 55 | (defun org-runbook-target--to-ivy-target (target &optional include-file-name-p) 56 | "Convert a `org-runbook-target' TARGET into a cons cell for use with ivy. 57 | When INCLUDE-FILE-NAME-P is non-nil, cdr will be suffixed TARGET's target-buffer file name." 58 | (--> target 59 | (cons (concat 60 | (->> it (org-runbook-command-target-name)) 61 | (when include-file-name-p 62 | (concat " - " 63 | (substring-no-properties 64 | (buffer-file-name (org-runbook-command-target-buffer it)))))) 65 | it))) 66 | 67 | (defun org-runbook-multiaction (x) 68 | "Add X to list of selected buffers `swiper-multi-buffers'. 69 | If X is already part of the list, remove it instead. Quit the selection if 70 | X is selected by either `ivy-done', `ivy-alt-done' or `ivy-immediate-done', 71 | otherwise continue prompting for buffers." 72 | (cond 73 | ((eq this-command 'ivy-done) (org-runbook-execute-target-action (cdr x))) 74 | (t (org-runbook-view-target-action (cdr x))))) 75 | 76 | (cl-loop 77 | for command in (list 'org-runbook-ivy 'org-runbook-search) 78 | do 79 | (ivy-set-actions 80 | command 81 | `( 82 | ("o" org-runbook-multiaction "Execute Target") 83 | ("g" (lambda (target) (org-runbook-goto-target-action (cdr target))) "Goto Target") 84 | ("p" (lambda (&rest arg) (org-runbook-switch-to-projectile-file)) "Switch to Projectile File") 85 | ("y" (lambda (&rest arg) (org-runbook-switch-to-major-mode-file)) "Switch to Major Mode File") 86 | ("c" (lambda (target) (org-runbook-kill-full-command-target-action (cdr target))) "Add full command to kill ring") 87 | ("e" (lambda (target) (org-runbook-eshell-full-command-target-action (cdr target))) "Run full command in eshell") 88 | ("r" (lambda (&rest arg) (org-runbook-switch-to-projectile-root-file)) "Switch to Project Root File") 89 | ("n" (lambda (&rest arg) (org-runbook-switch-to-projectile-root-file)) "Switch to Project Root File") 90 | ("v" (lambda (target) (org-runbook-view-target-action (cdr target))) "View Target")))) 91 | 92 | 93 | (ivy-set-actions 'org-runbook-bookmarks 94 | '(("o" org-runbook-bookmark--goto-link-action "Open Link") 95 | ("v" org-runbook-bookmark--view-action "View Bookmark") 96 | ("g" org-runbook-bookmark--goto-source-action "Goto Source"))) 97 | 98 | (provide 'org-runbook-ivy) 99 | ;;; org-runbook-ivy.el ends here 100 | -------------------------------------------------------------------------------- /org-runbook.el: -------------------------------------------------------------------------------- 1 | ;;; org-runbook.el --- Org mode for runbooks -*- lexical-binding: t -*- 2 | 3 | ;; Author: Tyler Dodge 4 | ;; Version: 1.1 5 | ;; Keywords: convenience, processes, terminals, files 6 | ;; Package-Requires: ((emacs "27.1") (seq "2.3") (f "0.20.0") (s "1.12.0") (dash "2.17.0") (mustache "0.24") (ht "0.9") (ivy "0.8.0")) 7 | ;; URL: https://github.com/tyler-dodge/org-runbook 8 | ;; Git-Repository: git://github.com/tyler-dodge/org-runbook.git 9 | ;; This program is free software; you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; This program is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with this program. If not, see . 21 | 22 | ;;; 23 | ;;; 24 | ;;; Commentary: 25 | ;; org-runbook provides heirarchical runbook commands from org file accessible directly from buffers. 26 | ;; Main entry points include `org-runbook-execute', `org-runbook-switch-to-major-mode-file', 27 | ;; and `org-runbook-switch-to-projectile-file' 28 | ;; 29 | ;; org-runbook lets you take org files structured like 30 | 31 | ;; #### MAJOR-MODE.org 32 | ;; ``` 33 | ;; * Build 34 | ;; #+BEGIN_SRC shell 35 | ;; cd {{project_root}} 36 | ;; #+END_SRC 37 | 38 | ;; ** Quick 39 | ;; #+BEGIN_SRC shell 40 | ;; make quick 41 | ;; #+END_SRC 42 | 43 | ;; ** Clean 44 | ;; #+BEGIN_SRC shell 45 | ;; make clean 46 | ;; #+END_SRC 47 | 48 | ;; ** Prod 49 | ;; #+BEGIN_SRC shell 50 | ;; make prod 51 | ;; #+END_SRC 52 | ;; ``` 53 | ;; and exposes them for easy access in buffers with corresponding major mode. 54 | ;; So, the function [org-runbook-execute](org-runbook-execute) has the following completions when the current buffer's major mode is MAJOR-MODE: 55 | ;; ``` 56 | ;; Build >> Quick 57 | ;; Build >> Clean 58 | ;; Build >> Prod 59 | ;; ``` 60 | ;; Each of these commands is the concatenation of the path of the tree. So for example, Build >> Quick would resolve to: 61 | ;; ``` 62 | ;; cd {{project_root}} 63 | ;; make quick 64 | ;; ``` 65 | ;; If projectile-mode is installed, org-runbook also pulls the file named PROJECTILE-PROJECT-NAME.org. 66 | ;; All files in [org-runbook-files] are also pulled. 67 | ;; Commands will resolve placeholders before evaluating. Currently the only available placeholder is {{project_root}} 68 | ;; which corresponds to the projectile-project-root of the buffer that called `org-runbook-execute' 69 | ;;; Code: 70 | 71 | ;; External Dependencies 72 | (require 'seq) 73 | (require 'f) 74 | (require 's) 75 | (require 'dash) 76 | (require 'mustache) 77 | (require 'ht) 78 | 79 | ;; Emacs Dependencies 80 | (require 'pulse) 81 | (require 'rx) 82 | (require 'org) 83 | (require 'org-capture) 84 | (require 'ox) 85 | (require 'ob-core) 86 | (require 'pcase) 87 | (require 'subr-x) 88 | (require 'eshell) 89 | (require 'esh-mode) 90 | (require 'cl-lib) 91 | 92 | 93 | ;; Optional Dependencies 94 | (require 'projectile nil t) 95 | (declare-function projectile-project-name "ext:projectile.el" (&optional project)) 96 | (require 'evil nil t) 97 | 98 | (defgroup org-runbook nil "Org Runbook Options." :group 'org) 99 | 100 | (defcustom org-runbook-files nil 101 | "Global files used by org runbook. 102 | When resolving commands for the current buffer, `org-runbook' appends 103 | `org-runbook-files' with the major mode org file and the projectile 104 | org file." 105 | :group 'org-runbook 106 | :type 'list) 107 | 108 | (defcustom org-runbook-project-directory (expand-file-name (f-join user-emacs-directory "runbook" "projects")) 109 | "Directory used to lookup the org file corresponding to the current project. 110 | `org-runbook-projectile-file' joins `org-runbook-project-directory' 111 | with the function `projectile-project-name' for the current buffer." 112 | :group 'org-runbook 113 | :type 'directory) 114 | 115 | (defcustom org-runbook-modes-directory (expand-file-name (f-join user-emacs-directory "runbook" "modes")) 116 | "Directory used to lookup the org file for the current major mode. 117 | `org-runbook-major-mode-file' joins `org-runbook-modes-directory' 118 | with the `symbol-name' of the `major-mode' for the current buffer." 119 | :group 'org-runbook 120 | :type 'directory) 121 | 122 | (defcustom org-runbook-view-mode-buffer "*compile-command*" 123 | "Buffer used to display the resolved command. 124 | 125 | Used by `org-runbook-view-target-action'" 126 | :group 'org-runbook 127 | :type 'string) 128 | 129 | (defcustom org-runbook-execute-command-action #'org-runbook-command-execute-shell 130 | "Function called to handle executing the given runbook. 131 | It is provided as a single argument the plist output of 132 | `org-runbook--shell-command-for-target'." 133 | :type 'function 134 | :group 'org-runbook) 135 | 136 | (defcustom org-runbook-process-connection-type nil 137 | "The process connection type to default to in org-runbook. 138 | The pty flag is ignored since it's already enabled if this is t." 139 | :type 'boolean 140 | :group 'org-runbook) 141 | 142 | (defcustom org-runbook-project-root-file "runbook.org" 143 | "The file that's looked up at the root of the current project." 144 | :group 'org-runbook 145 | :type 'string) 146 | 147 | 148 | (defface org-runbook-view-var-substitution 149 | '((t :inverse-video t)) 150 | "Face for highlighting the substituted variables. 151 | Used when viewing an org-runbook command." 152 | :group 'org-runbook) 153 | 154 | (defface org-runbook-bookmark-link-highlight 155 | '((t :inverse-video t)) 156 | "Face for highlighting the the boomkark links for `org-runbook-bookmarks'." 157 | :group 'org-runbook) 158 | 159 | (defvar org-runbook--target-history nil "History for org-runbook completing read for targets.") 160 | 161 | (defvar org-runbook--last-command-ht (ht) 162 | "Mapping from projectile root to the last command. 163 | If projectile is not imported, this uses the default directory. 164 | 165 | Used by `org-runbook-repeat-command'.") 166 | 167 | (defvar-local org-runbook-view--section nil 168 | "Tracks the section point is currently on in `org-runbook-view-mode'.") 169 | 170 | (defvar-local org-runbook--goto-default-directory nil 171 | "Tracks the default directory. 172 | Set when any of the switch to org-runbook functions are used.") 173 | 174 | (cl-defstruct (org-runbook-command-target (:constructor org-runbook-command-target-create) 175 | (:copier org-runbook-command-target-copy)) 176 | name 177 | point 178 | buffer) 179 | 180 | (cl-defstruct (org-runbook-subcommand (:constructor org-runbook-subcommand-create) 181 | (:copier org-runbook-subcommand-copy)) 182 | heading 183 | target 184 | command) 185 | 186 | (cl-defstruct (org-runbook-elisp-subcommand 187 | (:constructor org-runbook-elisp-subcommand-create) 188 | (:copier org-runbook-elisp-subcommand-copy)) 189 | heading 190 | target 191 | elisp) 192 | 193 | (cl-defstruct (org-runbook-command (:constructor org-runbook-command-create) 194 | (:copier org-runbook-command-copy)) 195 | name 196 | full-command 197 | target 198 | subcommands 199 | pty 200 | org-properties) 201 | 202 | (cl-defstruct (org-runbook-bookmark (:constructor org-runbook-bookmark-create) 203 | (:copier org-runbook-bookmark-copy)) 204 | name 205 | full-text 206 | target 207 | links) 208 | 209 | (cl-defstruct (org-runbook-file (:constructor org-runbook-file-create) 210 | (:copier org-runbook-file-copy)) 211 | name 212 | file 213 | targets) 214 | 215 | (defun org-runbook--completing-read () 216 | "Prompt user for a runbook command." 217 | (let ((target-map 218 | (->> (org-runbook-targets) 219 | (--map (org-runbook-file-targets it)) 220 | (-flatten) 221 | (--map (cons (org-runbook-command-target-name it) it)) 222 | (ht<-alist)))) 223 | (when (eq (ht-size target-map) 0) (org-runbook--no-commands-error)) 224 | (when-let (key (completing-read "Runbook:" target-map nil t nil 'org-runbook--target-history)) 225 | (ht-get target-map key)))) 226 | 227 | ;;;###autoload 228 | (defun org-runbook-install-eshell () 229 | "Add eshell aliases for org-runbook." 230 | (add-to-list 'eshell-complex-commands "org-runbook") 231 | (defalias 'eshell/org-runbook #'org-runbook-eshell)) 232 | 233 | ;;;###autoload 234 | (defun org-runbook-execute () 235 | "Prompt for command completion and execute the selected command." 236 | (interactive) 237 | (-some-> (org-runbook--completing-read) org-runbook-execute-target-action)) 238 | 239 | ;;;###autoload 240 | (defun org-runbook-view () 241 | "Prompt for command completion and view the selected command." 242 | (interactive) 243 | (-some-> (org-runbook--completing-read) org-runbook-view-target-action)) 244 | 245 | ;;;###autoload 246 | (defun org-runbook-goto () 247 | "Prompt for command completion and goto the selected command's location." 248 | (interactive) 249 | (-some-> (org-runbook--completing-read) org-runbook-goto-target-action)) 250 | 251 | ;;;###autoload 252 | (defun org-runbook-repeat () 253 | "Repeat the last command for the current projectile project. 254 | 255 | Use `default-directory' if projectile is unavailable." 256 | (interactive) 257 | (let ((command (ht-get org-runbook--last-command-ht (org-runbook--project-root)))) 258 | (if command (funcall org-runbook-execute-command-action command) 259 | (org-runbook-execute)))) 260 | 261 | ;;;###autoload 262 | (defun org-runbook-org-file-list () 263 | "Return the org file list in the correct order. 264 | Context dependent on which buffer it is called in." 265 | (-let* ((major-mode-file (list (cons (symbol-name major-mode) (org-runbook-major-mode-file t)))) 266 | (current-buffer-file (when (eq major-mode 'org-mode) 267 | (list (cons "*current buffer*" 268 | (buffer-file-name))))) 269 | (projectile-file (list (when (fboundp 'projectile-project-name) 270 | (cons (concat "*Project " (projectile-project-name org-runbook--goto-default-directory) "*") 271 | (org-runbook-projectile-file t))))) 272 | (project-root-file (list (when (fboundp 'projectile-project-name) 273 | (cons 274 | "Project Root Runbook" 275 | (f-join (org-runbook--project-root) 276 | org-runbook-project-root-file))))) 277 | (global-files (--map (cons it it) org-runbook-files)) 278 | (org-files 279 | (seq-uniq (-flatten (append current-buffer-file projectile-file project-root-file major-mode-file global-files)) 280 | (lambda (lhs rhs) (string= (cdr lhs) (cdr rhs)))))) 281 | org-files)) 282 | 283 | ;;;###autoload 284 | (defun org-runbook-targets () 285 | "Return the runbook commands corresponding to the current buffer." 286 | (save-excursion 287 | (let* ((org-files (org-runbook-org-file-list))) 288 | (cl-loop for file in org-files 289 | append 290 | (save-excursion 291 | (-let* (((name . file) file) 292 | (targets (when (-some-> file f-exists-p) 293 | (set-buffer (or (find-buffer-visiting file) (find-file-noselect file))) 294 | (org-runbook--targets-in-buffer)))) 295 | (when targets 296 | (-> (org-runbook-file-create 297 | :name name 298 | :file file 299 | :targets targets) 300 | list)))))))) 301 | 302 | (defun org-runbook-bookmarks () 303 | "List the bookmarks in org-runbook files interactively." 304 | (interactive) 305 | (let ((org-runbook-bookmark-context-size 2)) 306 | (ivy-read "Bookmark: " 307 | (->> 308 | (cl-loop for file in (org-runbook--org-files) 309 | append 310 | (progn 311 | (let ((buffer (find-file-noselect file))) 312 | (with-current-buffer buffer 313 | (org-runbook--bookmarks-in-buffer))))) 314 | (--map 315 | (let ((bookmark it) 316 | (text (org-runbook-bookmark-full-text it)) 317 | (name (org-runbook-bookmark-name it))) 318 | (->> 319 | (org-runbook-bookmark-links it) 320 | (--map (cons 321 | (let ((link it)) 322 | (-let [(beg . end) (get-text-property 0 :substring link)] 323 | (with-temp-buffer 324 | (erase-buffer) 325 | (insert text) 326 | (if (not beg) 327 | (progn 328 | (forward-line org-runbook-bookmark-context-size) 329 | (delete-char (- (point-max) (point)))) 330 | (set-text-properties (+ (point-min) beg) (+ (point-min) end) '(face org-runbook-bookmark-link-highlight)) 331 | (goto-char beg) 332 | (save-excursion 333 | (forward-line 2) 334 | (delete-char (- (point-max) (point)))) 335 | (save-excursion 336 | (forward-line (- org-runbook-bookmark-context-size)) 337 | (unless (bobp) 338 | (delete-char (- (point-min) (point))) 339 | (insert name) 340 | (insert "\n")))) 341 | (save-excursion 342 | (goto-char (point-min)) 343 | (while (re-search-forward (rx ":BOOKMARK:") nil t) 344 | (replace-match ""))) 345 | (buffer-string)))) 346 | (list :link it :bookmark bookmark)))))) 347 | (-flatten-n 1)) 348 | :caller 'org-runbook-bookmarks 349 | :action #'org-runbook-bookmark--goto-link-action))) 350 | 351 | 352 | (defun org-runbook-bookmark--goto-source-action (select) 353 | "Go to the SELECT bookmark's runbook org file source." 354 | (pcase (plist-get (cdr select) :bookmark) 355 | ((cl-struct org-runbook-bookmark (target (cl-struct org-runbook-command-target point buffer))) 356 | (-some--> 357 | (display-buffer buffer) 358 | (set-window-point it point))))) 359 | 360 | (defun org-runbook-bookmark--view-action (select) 361 | "View the SELECT bookmark's text." 362 | (pcase (plist-get (cdr select) :bookmark) 363 | ((cl-struct org-runbook-bookmark full-text) 364 | (display-buffer (--> "*runbook-bookmark-view*" (or (get-buffer it) (generate-new-buffer it)) 365 | (prog1 it 366 | (with-current-buffer it 367 | (let ((inhibit-read-only t)) 368 | (erase-buffer) 369 | (insert full-text) 370 | (org-mode) 371 | (read-only-mode t))))))))) 372 | 373 | (defun org-runbook-bookmark--goto-link-action (select) 374 | "Goto the location of SELECT's bookmark." 375 | (org-link-open-from-string (plist-get (cdr select) :link))) 376 | 377 | (defun org-runbook-bookmark--external-browser-action (select) 378 | "Goto the location of SELECT's bookmark with a browser." 379 | (shell-command-to-string 380 | (s-join " " (list "open" (shell-quote-argument (plist-get (cdr select) :link)))))) 381 | 382 | 383 | (defun org-runbook--org-files () 384 | "Return all of the context depenedent runbook org files." 385 | (append (f-files org-runbook-project-directory) 386 | (f-files org-runbook-modes-directory) 387 | nil)) 388 | 389 | (defun org-runbook-all-targets () 390 | "Lists all of the targets available in the project and modes directories." 391 | (cl-loop for file in 392 | (org-runbook--org-files) 393 | append 394 | (let ((buffer (find-file-noselect file))) 395 | (progn 396 | (with-current-buffer buffer 397 | (org-runbook--targets-in-buffer)))))) 398 | 399 | (defun org-runbook-target-at-point () 400 | "Return the `org-runbook-command-target' at point." 401 | (cl-loop for target = (org-runbook--targets-in-buffer) then (cdr target) 402 | while (-some--> (cadr target) (> (point) (org-runbook-command-target-point it))) 403 | finally return (car target))) 404 | 405 | (defun org-runbook-targets-from-file-by-name (file-name) 406 | "Find file named FILE-NAME in org-runbook project or modes directories. 407 | Returns all the targets in that file. nil if the file does not exist." 408 | (interactive) 409 | (let ((matcher (lambda (text) (string= (f-filename text) file-name)))) 410 | (with-current-buffer 411 | (find-file-noselect 412 | (-some-> 413 | (append 414 | (f-files org-runbook-project-directory matcher) 415 | (f-files org-runbook-modes-directory matcher) 416 | nil) 417 | cl-first)) 418 | (org-runbook--targets-in-buffer)))) 419 | 420 | ;;;###autoload 421 | (defun org-runbook-switch-to-major-mode-file () 422 | "Switch current buffer to the major mode file. 423 | 424 | This file corresponds to the current buffer's major mode." 425 | (interactive) 426 | (find-file (org-runbook-major-mode-file))) 427 | 428 | ;;;###autoload 429 | (defun org-runbook-switch-to-projectile-file (&optional noselect) 430 | "Switch current buffer to the global project file. 431 | This file corresponds to the current buffer's projectile name. 432 | 433 | When NOSELECT is non-nil use `find-file-noselect' instead of `find-file'." 434 | (interactive) 435 | (let ((start-directory default-directory)) 436 | (prog1 437 | (if noselect 438 | (find-file-noselect (org-runbook-projectile-file)) 439 | (find-file (org-runbook-projectile-file))) 440 | (setq-local org-runbook--goto-default-directory start-directory)))) 441 | 442 | ;;;###autoload 443 | (defun org-runbook-switch-to-projectile-root-file () 444 | "Switch current buffer to the project root file. 445 | This corresponds to the `projectile-project-root' of 446 | the current buffer." 447 | (interactive) 448 | (let ((start-directory default-directory)) 449 | (find-file (f-join (org-runbook--project-root) "runbook.org")) 450 | (setq-local org-runbook--goto-default-directory start-directory))) 451 | 452 | ;;;###autoload 453 | (defun org-runbook-capture-target-major-mode-file () 454 | "Capture target for org runbook major mode file." 455 | (org-runbook-switch-to-major-mode-file) 456 | (goto-char (point-max))) 457 | 458 | ;;;###autoload 459 | (defun org-runbook-capture-target-projectile-file () 460 | "Capture target for org runbook projectile name file." 461 | (set-buffer (org-runbook-switch-to-projectile-file t)) 462 | (goto-char (point-max))) 463 | 464 | (defun org-runbook--find-or-create-eshell-buffer () 465 | "Find or create an eshell buffer. 466 | The eshell buffer is ready to execute a command." 467 | (or 468 | (cl-loop for buffer being the buffers 469 | if (and (eq (buffer-local-value 'major-mode buffer) 'eshell-mode) 470 | (not (get-buffer-process buffer))) 471 | return buffer) 472 | (eshell))) 473 | 474 | ;;;###autoload 475 | (defun org-runbook-eshell (&rest args) 476 | "Call org-runbook from eshell. 477 | 478 | ARGS is concatenated with \">>\" and used to lookup the command to execute. 479 | 480 | Finds the target whose name matches arg concatenated with spaces. 481 | Executes that command in the buffer." 482 | (let* ((targets (-flatten (--map (org-runbook-file-targets it) (org-runbook-targets)))) 483 | (available-commands (cl-loop for target in targets 484 | concat (format "- \"%s\"\n" (org-runbook-command-target-name target)))) 485 | (context-string (concat 486 | "# Search Path: 487 | 488 | " 489 | (->> (org-runbook-org-file-list) 490 | (--map (concat "- " (cdr it))) 491 | (s-join "\n")) 492 | " 493 | 494 | " 495 | (format "# Available Commands: 496 | 497 | %s" available-commands)))) 498 | (condition-case err 499 | (eshell-eval-using-options 500 | "org-runbook" 501 | args 502 | `((nil "help" nil nil "Show help") 503 | (nil "view" nil view-p "Output command to stdout. Output should be valid for to be read as a bash script.") 504 | :usage "COMMAND-NAME-PREFIX" 505 | :post-usage ,context-string 506 | :show-usage t) 507 | (-let* ((arg-string (s-join " " (--map (format "%s" it) args)))) 508 | (if-let ((command 509 | (and args 510 | (--find 511 | (string-prefix-p arg-string (org-runbook-command-target-name it)) 512 | targets)))) 513 | (cond (view-p 514 | (concat 515 | "#!/bin/env bash" 516 | "\n# File: " (buffer-file-name (org-runbook-command-target-buffer command)) 517 | "\n# Command Name: " (org-runbook-command-name (org-runbook--shell-command-for-target command)) 518 | "\n" 519 | (org-runbook-command-full-command (org-runbook--shell-command-for-target command)) 520 | "\n")) 521 | (t 522 | (let* ((fullcommand (org-runbook-command-full-command (org-runbook--shell-command-for-target command))) 523 | (script-name (org-runbook--temp-script-file-for-command-string fullcommand))) 524 | (goto-char (point-max)) 525 | (throw 'eshell-replace-command `(eshell-named-command "sh" (list ,script-name)))))) 526 | (user-error "Unable to find command with prefix %s. 527 | 528 | %s" arg-string context-string)))) 529 | (error (user-error "%s" (error-message-string err)))))) 530 | 531 | ;;;###autoload 532 | (defun org-runbook--noop (&rest _) 533 | "Perform a No-op and ignore all arguments.") 534 | 535 | (defun org-runbook--temp-script-file-for-command-string (command-string) 536 | "Return an executable temp file with the script in COMMAND-STRING." 537 | (let ((script-name 538 | (make-temp-file "org-runbook"))) 539 | (prog1 script-name 540 | (with-temp-file script-name 541 | (insert "#!/usr/bin/env bash 542 | ") 543 | (when command-string (insert command-string)))))) 544 | 545 | (defun org-runbook-eshell-full-command-target-action (target) 546 | "Take the selected command and run it in eshell. 547 | Expects TARGET to be a `org-runbook-command-target'. 548 | It will attempt to run the command in an existing eshell buffer before 549 | creating a new one." 550 | (unless (org-runbook-command-target-p target) (error "Unexpected type provided: %s" target)) 551 | (let ((command (org-runbook-command-full-command (org-runbook--shell-command-for-target target))) 552 | (eshell (org-runbook--find-or-create-eshell-buffer))) 553 | 554 | (with-current-buffer eshell 555 | (goto-char (point-max)) 556 | (insert "sh ") 557 | (insert (org-runbook--temp-script-file-for-command-string command)) 558 | (eshell-send-input)))) 559 | 560 | (defun org-runbook-kill-full-command-target-action (target) 561 | "Yank the selected command into the kill ring. 562 | 563 | Expects TARGET to be a `org-runbook-command-target'." 564 | (unless (org-runbook-command-target-p target) (error "Unexpected type provided: %s" target)) 565 | (kill-new (substring-no-properties (org-runbook-command-full-command (org-runbook--shell-command-for-target target))))) 566 | 567 | (defun org-runbook-view-target-action (target) 568 | "View the selected command. Expects TARGET to be a `org-runbook-command-target'." 569 | (unless (org-runbook-command-target-p target) (error "Unexpected type provided: %s" target)) 570 | (pcase-let* ((count 0) 571 | (displayed-headings (ht)) 572 | ((cl-struct org-runbook-command subcommands) (org-runbook--shell-command-for-target target)) 573 | (buffer (or (get-buffer org-runbook-view-mode-buffer) 574 | (generate-new-buffer org-runbook-view-mode-buffer)))) 575 | (display-buffer buffer) 576 | (set-buffer buffer) 577 | 578 | (org-runbook-view-mode) 579 | (setq-local inhibit-read-only t) 580 | (erase-buffer) 581 | (->> subcommands 582 | (-map 583 | (pcase-lambda ((and section (cl-struct org-runbook-subcommand heading command))) 584 | (setq count (1+ count)) 585 | (--> (concat 586 | (unless (ht-get displayed-headings heading nil) 587 | (ht-set displayed-headings heading t) 588 | (concat (s-repeat count "*") 589 | " " 590 | heading 591 | "\n\n")) 592 | (if (listp command) 593 | "(deferred:nextc\n it\n (lambda ()\n " 594 | "#+BEGIN_SRC shell\n\n") 595 | (format (if (listp command) "%S" "%s") command) 596 | (if (listp command) 597 | ")" 598 | "\n#+END_SRC") 599 | "\n") 600 | (propertize it 'section section)))) 601 | (s-join "\n") 602 | (insert)) 603 | (setq-local inhibit-read-only nil))) 604 | 605 | (defun org-runbook-execute-target-action (target) 606 | "Execute the `org-runbook' compile TARGET from helm. 607 | Expects COMMAND to be of the form (:command :name)." 608 | (let ((command (org-runbook--shell-command-for-target target))) 609 | (ht-set org-runbook--last-command-ht 610 | (org-runbook--project-root) 611 | command) 612 | (let ((default-directory (or org-runbook--goto-default-directory default-directory))) 613 | (funcall org-runbook-execute-command-action command)))) 614 | 615 | (defun org-runbook-command-execute-shell (command) 616 | "Execute the COMMAND in shell." 617 | (org-runbook--validate-command command) 618 | (pcase-let (((cl-struct org-runbook-command full-command name) command)) 619 | ;; Intentionally not shell quoting full-command since it's a script 620 | (let ((process-connection-type (or org-runbook-process-connection-type 621 | (org-runbook-command-pty command))) 622 | (buffer-name (concat "*" name "*")) 623 | (script-file (org-runbook--temp-script-file-for-command-string full-command))) 624 | (start-process-shell-command buffer-name (or (get-buffer buffer-name) (generate-new-buffer buffer-name)) 625 | (concat "sh " script-file))))) 626 | 627 | (defun org-runbook-goto-target-action (command) 628 | "Goto the position referenced by COMMAND. 629 | Expects COMMAND to ether be a `org-runbook-subcommand' 630 | or a `org-runbook-command-target'." 631 | (--> (pcase command 632 | ((or (cl-struct org-runbook-subcommand (target (cl-struct org-runbook-command-target point buffer))) 633 | (cl-struct org-runbook-command-target point buffer)) 634 | (list :buffer buffer :point point))) 635 | (-let [(&plist :buffer :point) it] 636 | (display-buffer buffer) 637 | (set-buffer buffer) 638 | (goto-char point) 639 | (pulse-momentary-highlight-one-line (point)) 640 | buffer))) 641 | 642 | (defun org-runbook--targets-in-buffer () 643 | "Get all targets by walking up the org subtree in order. 644 | Return `org-runbook-command-target'." 645 | (font-lock-ensure (point-min) (point-max)) 646 | (save-mark-and-excursion 647 | (goto-char (point-min)) 648 | (let* ((known-commands (ht))) 649 | (cl-loop while (re-search-forward (rx line-start (* whitespace) "#+BEGIN_SRC" (* whitespace) (or "shell" "emacs-lisp" "compile-queue")) nil t) 650 | append 651 | (let* ((headings (save-excursion 652 | (append 653 | (list (org-runbook--get-heading)) 654 | (save-excursion 655 | (cl-loop while (org-up-heading-safe) 656 | append (list (org-runbook--get-heading))))))) 657 | (name (->> headings 658 | (-map 's-trim) 659 | (reverse) 660 | (s-join " >> ")))) 661 | (unless (ht-get known-commands name nil) 662 | (ht-set known-commands name t) 663 | (list (org-runbook-command-target-create 664 | :name name 665 | :buffer (current-buffer) 666 | :point (save-excursion 667 | (unless (org-at-heading-p) (re-search-backward (regexp-quote (org-runbook--get-heading)))) 668 | (point)))))))))) 669 | 670 | (defun org-runbook--bookmarks-in-buffer () 671 | "Get all the sections with the header bookmark." 672 | (font-lock-ensure (point-min) (point-max)) 673 | (save-mark-and-excursion 674 | (goto-char (point-min)) 675 | (cl-loop while (re-search-forward (rx ":BOOKMARK:") nil t) 676 | append 677 | (let* ((pt (save-excursion (forward-line 0) (point))) 678 | (end (org-end-of-subtree)) 679 | (headline (save-excursion (goto-char pt) (s-trim (thing-at-point 'line)))) 680 | (full-text (s-trim (buffer-substring pt end))) 681 | (links (save-mark-and-excursion 682 | (goto-char pt) 683 | (cl-loop while (re-search-forward org-link-any-re end t) 684 | append (-some--> 685 | (get-text-property (match-beginning 0) 'htmlize-link) 686 | (plist-get it :uri) 687 | (propertize 688 | it :substring (cons (- (match-beginning 0) pt) 689 | (- (match-end 0) pt))) 690 | (list it)))))) 691 | (list 692 | (org-runbook-bookmark-create 693 | :name (s-replace ":BOOKMARK:" "" headline) 694 | :full-text full-text 695 | :links (or links (list (save-excursion (goto-char pt) (org-store-link nil)))) 696 | :target (org-runbook-command-target-create 697 | :point pt 698 | :buffer (current-buffer)))))))) 699 | 700 | ;;;###autoload 701 | (defun org-runbook-add-org-capture-template () 702 | "Add the org-runbook capture templates to `org-capture-templates'." 703 | (add-to-list 'org-capture-templates 704 | (list "b" "Add bookmark for this location to the org runbook project file." 705 | 'entry 706 | '(function 707 | org-runbook-capture-target-projectile-file) 708 | "* %? :BOOKMARK:\n\n%l"))) 709 | 710 | (defun org-runbook--get-heading () 711 | "Call `org-get-heading' with default arguments." 712 | (substring-no-properties (org-get-heading t t))) 713 | 714 | (defun org-runbook-major-mode-file (&optional no-ensure) 715 | "Target which will append to the `major-mode' runbook for the current buffer. 716 | Ensures the file exists unless NO-ENSURE is non-nil." 717 | (let ((file (f-join org-runbook-project-directory (concat (symbol-name major-mode) ".org")))) 718 | (if no-ensure file (org-runbook--ensure-file file)))) 719 | 720 | (defun org-runbook-projectile-file (&optional no-ensure) 721 | "Return path of the org runbook file for the current projectile project. 722 | Ensures the file exists unless NO-ENSURE is non-nil." 723 | (unless (fboundp 'projectile-project-name) 724 | (user-error "Projectile must be installed for org-runbook-projectile-file")) 725 | (let ((file (f-join org-runbook-project-directory (concat (projectile-project-name org-runbook--goto-default-directory) ".org")))) 726 | (if no-ensure file (org-runbook--ensure-file file)))) 727 | 728 | (defun org-runbook--ensure-file (file) 729 | "Create the FILE if it doesn't exist. Return the fully expanded FILE name." 730 | (let ((full-file (expand-file-name file))) 731 | (unless (f-exists-p full-file) 732 | (mkdir (f-parent full-file) t) 733 | (f-touch full-file)) 734 | full-file)) 735 | 736 | (defvar org-runbook-view-mode-map 737 | (-doto (make-sparse-keymap) 738 | (define-key (kbd "") #'org-runbook-view--open-at-point))) 739 | 740 | (define-derived-mode org-runbook-view-mode org-mode "compile view" 741 | "Mode for viewing resolved org-runbook commands." 742 | (read-only-mode 1) 743 | (view-mode 1)) 744 | 745 | (defun org-runbook--project-root () 746 | "Return the current project root. 747 | If projectile is defined, use `projectile-project-root', 748 | otherwise `default-directory'." 749 | (or (and (fboundp 'projectile-project-root) (projectile-project-root org-runbook--goto-default-directory)) 750 | default-directory)) 751 | 752 | (defun org-runbook--project-name () 753 | "Return the current project name. 754 | If projectile is defined, `projectile-project-name', 755 | otherwise `default-directory'." 756 | (string-remove-suffix 757 | "/" 758 | (or (and (fboundp 'projectile-project-root) (projectile-project-name org-runbook--goto-default-directory)) 759 | (directory-file-name default-directory)))) 760 | 761 | (defun org-runbook-view--open-at-point () 762 | "Switch buffer to the file referenced at point in `org-runbook-view-mode'." 763 | (interactive) 764 | (or (-some-> (get-text-property (point) 'section) org-runbook-goto-target-action) 765 | (user-error "No known section at point"))) 766 | 767 | (defun org-runbook--shell-command-for-target (target) 768 | "Return the `org-runbook-command' for a TARGET. 769 | TARGET is a `org-runbook-command-target'." 770 | (unless (org-runbook-command-target-p target) (error "Unexpected type passed %s" target)) 771 | (save-excursion 772 | (pcase-let (((cl-struct org-runbook-command-target name buffer point) target)) 773 | (let* ((project-root (org-runbook--project-root)) 774 | (project-name (org-runbook--project-name)) 775 | (source-buffer-file-name (or (buffer-file-name buffer) default-directory)) 776 | (has-pty-tag nil) 777 | (properties nil) 778 | (subcommands nil)) 779 | (set-buffer buffer) 780 | (goto-char point) 781 | (save-excursion 782 | (let* ((at-root nil)) 783 | (while (not at-root) 784 | (let* ((start-heading (org-runbook--get-heading)) 785 | (start (save-excursion (forward-line 1) (outline-previous-heading) (point))) 786 | (group nil)) 787 | (save-excursion 788 | (end-of-line) 789 | (setq properties 790 | (append properties 791 | (org-entry-properties) 792 | nil)) 793 | (while (and (re-search-forward (rx "#+BEGIN_SRC" (* whitespace) (or "shell" "emacs-lisp" "compile-queue")) nil t) 794 | (eq (save-excursion (outline-previous-heading) (point)) start)) 795 | (setq has-pty-tag (or has-pty-tag (-contains-p (org-runbook--get-tags) "PTY"))) 796 | (let* ((src-block-info (org-babel-get-src-block-info nil (org-element-context)))) 797 | (pcase (car src-block-info) 798 | ((pred (s-starts-with-p "emacs-lisp")) 799 | (push 800 | (org-runbook-elisp-subcommand-create 801 | :heading start-heading 802 | :target (org-runbook-command-target-create 803 | :buffer (current-buffer) 804 | :point (point)) 805 | :elisp 806 | (read 807 | (concat 808 | "(progn " 809 | (buffer-substring-no-properties 810 | (save-excursion (forward-line 1) (point)) 811 | (save-excursion (re-search-forward (rx "#+END_SRC")) (beginning-of-line) (point))) 812 | ")"))) 813 | group)) 814 | ((or (pred (string= "compile-queue")) (pred (s-starts-with-p "shell"))) 815 | (push 816 | (org-runbook-subcommand-create 817 | :heading start-heading 818 | :target (org-runbook-command-target-create 819 | :buffer (current-buffer) 820 | :point (point)) 821 | :command 822 | (s-replace-all 823 | '((""" . "\"") 824 | ("<" . "<") 825 | ("'" . "'") 826 | ("&" . "&") 827 | (">" . ">")) 828 | (mustache-render 829 | (buffer-substring-no-properties 830 | (save-excursion (forward-line 1) (point)) 831 | (save-excursion (re-search-forward (rx "#+END_SRC")) (beginning-of-line) (point))) 832 | (--doto (ht<-alist (->> (car (cdr (cdr src-block-info))) 833 | (--map (cons (symbol-name (car it)) (format "%s" (cdr it)))) 834 | (--filter (not (s-starts-with-p ":" (car it)))))) 835 | (ht-set it "project_root" (-some--> (substring-no-properties project-root) 836 | (s-chop-prefix (or (file-remote-p project-root) "") it))) 837 | (ht-set it "project_name" project-name) 838 | (ht-set it "current_file" (substring-no-properties source-buffer-file-name)) 839 | (ht-set it "context" (format "%s" (ht->plist it))) 840 | 841 | (cl-loop 842 | for key in (ht-keys it) 843 | do 844 | (ht-set it 845 | key 846 | (propertize 847 | (ht-get it key) 848 | 'font-lock-face 'org-runbook-view-var-substitution 849 | 'face 'org-runbook-view-var-substitution))))))) 850 | group)))) 851 | (forward-line 1))) 852 | (setq subcommands (append (reverse group) subcommands nil)) 853 | (goto-char start)) 854 | (setq at-root (not (org-up-heading-safe)))))) 855 | (org-runbook-command-create 856 | :name name 857 | :pty (or has-pty-tag (alist-get "PTY" properties nil nil #'string=)) 858 | :org-properties properties 859 | :target (-some->> subcommands (-filter #'org-runbook-subcommand-p) last car org-runbook-subcommand-target) 860 | :full-command 861 | (-some->> subcommands 862 | (--filter (and it (org-runbook-subcommand-p it))) 863 | (--map (org-runbook-subcommand-command it)) 864 | (--filter it) 865 | (--map (s-trim it)) 866 | (s-join ";\n")) 867 | :subcommands subcommands))))) 868 | 869 | (defun org-runbook-command-get-property (command property) 870 | "Get the value of PROPERTY from the org-properties of the COMMAND." 871 | (alist-get property 872 | (org-runbook-command-org-properties command) 873 | nil nil #'string=)) 874 | 875 | (defun org-runbook--no-commands-error () 876 | "Error representing that no commands were found for the current buffer." 877 | (if (fboundp 'projectile-project-name) 878 | (user-error "No Commands Defined For Runbook. (Major Mode: %s, Project: %s)" 879 | (symbol-name major-mode) 880 | (projectile-project-name)) 881 | (user-error "No Commands Defined For Runbook. (Major Mode: %s)" 882 | (symbol-name major-mode)))) 883 | 884 | (defun org-runbook--validate-command (command) 885 | "Validates COMMAND and throws errors if it doesn't match spec." 886 | (unless command (error "Command cannot be nil")) 887 | (unless (org-runbook-command-p command) (error "Unexepected type for command %s" command)) 888 | t) 889 | 890 | (defun org-runbook--get-tags () 891 | "Get tags for the current heading." 892 | (save-excursion 893 | (outline-back-to-heading) 894 | (org-get-tags))) 895 | 896 | 897 | (defun org-runbook--export-filter-headlines (data _ __) 898 | "Filter org-runbook specific tags in DATA." 899 | (-some->> data (s-replace-all '((":PTY:" . ""))))) 900 | 901 | (defun org-runbook--export-filter-body (data _ __) 902 | "Filter org-runbook specific substitutions in DATA." 903 | (-some->> data (s-replace-all '(("{{project_root}}" . "."))))) 904 | 905 | ;;;###autoload 906 | (defun org-runbook-setup-export () 907 | "Set up org-export to ignore unnecessary tags." 908 | (add-to-list 'org-export-filter-body-functions 'org-runbook--export-filter-body) 909 | (setq org-export-with-tags nil)) 910 | 911 | (when (boundp 'evil-motion-state-modes) 912 | (add-to-list 'evil-motion-state-modes 'org-runbook-view-mode)) 913 | 914 | (provide 'org-runbook) 915 | ;;; org-runbook.el ends here 916 | -------------------------------------------------------------------------------- /runbook.org: -------------------------------------------------------------------------------- 1 | * Test Eshell 2 | 3 | #+BEGIN_SRC compile-queue 4 | echo A 5 | sleep 5 6 | echo B 7 | #+END_SRC 8 | 9 | * nix-build 10 | #+BEGIN_SRC compile-queue 11 | set -o errexit 12 | set -o pipefail 13 | set -o nounset 14 | cd {{project_root}} 15 | #+END_SRC 16 | 17 | ** Test 27.1 :PTY: 18 | #+BEGIN_SRC compile-queue 19 | nix-build -A test.emacs_27_1 20 | #+END_SRC 21 | 22 | ** Test 27.2 :PTY: 23 | #+BEGIN_SRC compile-queue 24 | nix-build -A test.emacs_27_2 25 | #+END_SRC 26 | 27 | ** Test 28.1 :PTY: 28 | #+BEGIN_SRC compile-queue 29 | nix-build -A test.emacs_28_1 30 | #+END_SRC 31 | 32 | ** Package Lint :PTY: 33 | #+BEGIN_SRC compile-queue 34 | nix-build -A package_lint.emacs_28_1 35 | #+END_SRC 36 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | let 4 | versions = (import ./default.nix).versions; 5 | in pkgs.mkShell { 6 | packages = [ 7 | (versions.latest (epkgs: []) (epkgs: [])) 8 | ]; 9 | } 10 | -------------------------------------------------------------------------------- /test/bookmark.org: -------------------------------------------------------------------------------- 1 | * Test :BOOKMARK: 2 | Test 3 | http://google.com 4 | 5 | * Test 2 :BOOKMARK: 6 | Test 7 | http://google.com 8 | -------------------------------------------------------------------------------- /test/no-commands/fundamental-mode.org: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyler-dodge/org-runbook/7ada3903a56266d60541d59ae92410e8ab6fe836/test/no-commands/fundamental-mode.org -------------------------------------------------------------------------------- /test/no-commands/org-runbook.org: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyler-dodge/org-runbook/7ada3903a56266d60541d59ae92410e8ab6fe836/test/no-commands/org-runbook.org -------------------------------------------------------------------------------- /test/one-command/fundamental-mode.org: -------------------------------------------------------------------------------- 1 | * Test 2 | #+BEGIN_SRC shell 3 | echo test 4 | #+END_SRC 5 | -------------------------------------------------------------------------------- /test/org-runbook-test.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t -*- 2 | 3 | (require 'cl-lib) 4 | (require 'org) 5 | (require 'el-mock) 6 | 7 | (when (require 'undercover nil t) 8 | (undercover "*.el")) 9 | (require 'org-runbook (expand-file-name "org-runbook.el")) 10 | (require 'org-runbook-ivy (expand-file-name "org-runbook-ivy.el")) 11 | 12 | (ert-deftest org-runbook-exists () 13 | "Sanity check to make sure expected symbols are exported." 14 | (should (fboundp 'org-runbook-execute))) 15 | 16 | (ert-deftest org-runbook--validate-command () 17 | "Tests to verify validation" 18 | (should (org-runbook--validate-command (org-runbook-command-create))) 19 | (should-error (org-runbook--validate-command nil)) 20 | (should-error (org-runbook--validate-command "test"))) 21 | 22 | (ert-deftest org-runbook-execute-no-commands () 23 | "org-runbook-execute should throw an error when no commands are available" 24 | (with-temp-buffer 25 | (fundamental-mode) 26 | (setq-local org-runbook-modes-directory (relative-to-test-directory "no-commands")) 27 | (setq-local org-runbook-project-directory (relative-to-test-directory "no-commands")) 28 | (org-runbook--output-configuration) 29 | (should-error (org-runbook-execute)))) 30 | 31 | (ert-deftest org-runbook-execute-one-command () 32 | "org-runbook-execute should execute the command referenced in the corresponding org file." 33 | (with-temp-buffer 34 | (fundamental-mode) 35 | (setq-local org-runbook-modes-directory (relative-to-test-directory "one-command")) 36 | (setq-local org-runbook-project-directory (relative-to-test-directory "one-command")) 37 | (setq-local org-runbook-execute-command-action #'org-runbook-command-execute-message) 38 | (org-runbook--output-configuration) 39 | (setq-local completing-read-function (lambda (_ collection &rest _) (-some-> collection ht-keys cl-first))) 40 | (should (org-runbook-execute)) 41 | (should (string= (org-runbook-command-full-command org-runbook-command-last-command) "echo test")))) 42 | 43 | (ert-deftest org-runbook-view-one-command () 44 | "org-runbook-execute should execute the command referenced in the corresponding org file." 45 | (with-temp-buffer 46 | (should-error (org-runbook-view-target-action nil)) 47 | (fundamental-mode) 48 | (setq-local org-runbook-modes-directory (relative-to-test-directory "one-command")) 49 | (setq-local org-runbook-project-directory (relative-to-test-directory "one-command")) 50 | (setq-local org-runbook-execute-command-action #'org-runbook-command-execute-message) 51 | (save-window-excursion 52 | (org-runbook-switch-to-major-mode-file)) 53 | (org-runbook--output-configuration) 54 | (setq-local completing-read-function (lambda (_ collection &rest _) (-some-> collection ht-keys cl-first))) 55 | (org-runbook-view) 56 | (should (eq (get-buffer org-runbook-view-mode-buffer) (current-buffer))) 57 | (goto-char (point-min)) 58 | (should (re-search-forward "echo test" nil t)) 59 | (org-runbook-view--open-at-point) 60 | (should (s-contains-p "fundamental-mode.org" (buffer-file-name))) 61 | (goto-char (point-min)) 62 | (should (re-search-forward "echo test" nil t)))) 63 | (defun -message (&rest body) 64 | (if (eq (length body) 1) 65 | (prog1 (car body) (message "%s" (car body))) 66 | (prog1 (car (last body)) (apply 'message body)))) 67 | 68 | (ert-deftest org-runbook-execute-command-from-org-runbook-files () 69 | "org-runbook-execute should execute the command referenced in the corresponding org file." 70 | (with-temp-buffer 71 | (fundamental-mode) 72 | (setq-local org-runbook-modes-directory (relative-to-test-directory "one-command")) 73 | (setq-local org-runbook-project-directory (relative-to-test-directory "one-command")) 74 | (setq-local org-runbook-execute-command-action #'org-runbook-command-execute-message) 75 | (setq-local org-runbook-files (list (relative-to-test-directory "test-runbook.org"))) 76 | (org-runbook--output-configuration) 77 | (setq-local completing-read-function (lambda (_ collection &rest _) 78 | (-some->> collection (ht-keys) 79 | (--first (string= it "Test Data 2 >> Test Data B"))))) 80 | (should (org-runbook-execute)) 81 | (should (string= (org-runbook-command-full-command org-runbook-command-last-command) "echo test-runbook-2-B")))) 82 | 83 | (ert-deftest org-runbook-goto-one-command () 84 | "org-runbook-execute should execute the command referenced in the corresponding org file." 85 | (with-temp-buffer 86 | (fundamental-mode) 87 | (setq-local org-runbook-modes-directory (relative-to-test-directory "one-command")) 88 | (setq-local org-runbook-project-directory (relative-to-test-directory "one-command")) 89 | (setq-local org-runbook-execute-command-action #'org-runbook-command-execute-message) 90 | (org-runbook--output-configuration) 91 | (setq-local completing-read-function (lambda (_ collection &rest _) (-some-> collection ht-keys cl-first))) 92 | (org-runbook-goto) 93 | (should (s-contains-p "fundamental-mode.org" (buffer-file-name))) 94 | (goto-char (point-min)) 95 | (should (re-search-forward "echo test" nil t)))) 96 | 97 | (ert-deftest org-runbook-switch-to-file-functions () 98 | "org-runbook-switch-to-* functions should work correctly" 99 | (with-temp-buffer 100 | (fundamental-mode) 101 | (setq-local org-runbook-modes-directory (relative-to-test-directory "one-command")) 102 | (setq-local org-runbook-project-directory (relative-to-test-directory "one-command")) 103 | (let ((expected-file-name (expand-file-name (f-join org-runbook-modes-directory "fundamental-mode.org")))) 104 | (org-runbook-switch-to-major-mode-file) 105 | (should (string= (buffer-file-name) expected-file-name))) 106 | (let ((expected-file-name (expand-file-name (f-join org-runbook-project-directory "project-file.org"))) 107 | (projectile-project-function (symbol-function #'projectile-project-name))) 108 | (with-mock 109 | (stub projectile-project-name => "project-file") 110 | (org-runbook-switch-to-projectile-file) 111 | (should (string= (buffer-file-name) expected-file-name)))))) 112 | 113 | (ert-deftest org-runbook-switch-to-capture-target-file-functions () 114 | "org-runbook-switch-to-* functions should work correctly" 115 | (with-temp-buffer 116 | (fundamental-mode) 117 | (setq-local org-runbook-modes-directory (relative-to-test-directory "one-command")) 118 | (setq-local org-runbook-project-directory (relative-to-test-directory "one-command")) 119 | (let ((expected-file-name (expand-file-name (f-join org-runbook-modes-directory "fundamental-mode.org")))) 120 | (org-runbook-capture-target-major-mode-file) 121 | (should (eq (point) (point-max))) 122 | (should (string= (buffer-file-name) expected-file-name))) 123 | 124 | (let ((expected-file-name (expand-file-name (f-join org-runbook-project-directory "project-file.org"))) 125 | (projectile-project-function (symbol-function #'projectile-project-name))) 126 | (with-mock 127 | (stub projectile-project-name => "project-file") 128 | (org-runbook-capture-target-projectile-file) 129 | (should (eq (point) (point-max))) 130 | (should (string= (buffer-file-name) expected-file-name)))))) 131 | 132 | (ert-deftest org-runbook-should-execute-in-file-buffers () 133 | "Should use default-directory for project_root in file-buffers." 134 | (find-file (relative-to-test-directory "test-runbook.org")) 135 | (should-error (org-runbook-command-execute-eshell nil)) 136 | (should-error (org-runbook-command-execute-shell nil)) 137 | (setq-local org-runbook-modes-directory (relative-to-test-directory "no-commands")) 138 | (setq-local org-runbook-project-directory (relative-to-test-directory "no-commands")) 139 | (setq-local org-runbook-execute-command-action #'org-runbook-command-execute-shell) 140 | (org-runbook--output-configuration) 141 | (setq-local completing-read-function (lambda (_ collection &rest _) (-some--> collection 142 | (ht-keys it) 143 | (sort it #'string<) 144 | (cl-first it)))) 145 | (with-mock 146 | (mock (start-process-shell-command "*Test Data 1 >> Test Data A*" * *) => t :times 1) 147 | (should (org-runbook-execute)))) 148 | 149 | (ert-deftest org-runbook-view--open-at-point-error () 150 | "org-runbook-view--open-at-point should throw an error if there isn't a section'" 151 | (should-error (with-temp-buffer (org-runbook-view--open-at-point)))) 152 | 153 | 154 | (ert-deftest org-runbook-execute-shell-functions () 155 | "Test org-runbook-execute-eshell and org-runbook-execute-shell." 156 | (with-temp-buffer 157 | (fundamental-mode) 158 | (should-error (org-runbook-command-execute-eshell nil)) 159 | (should-error (org-runbook-command-execute-shell nil)) 160 | (setq-local org-runbook-modes-directory (relative-to-test-directory "one-command")) 161 | (setq-local org-runbook-project-directory (relative-to-test-directory "one-command")) 162 | (setq-local org-runbook-execute-command-action #'org-runbook-command-execute-shell) 163 | (org-runbook--output-configuration) 164 | (setq-local completing-read-function (lambda (_ collection &rest _) (-some-> collection ht-keys cl-first))) 165 | (with-mock 166 | (mock (start-process-shell-command "*Test*" * *) => t :times 1) 167 | (should (org-runbook-execute))))) 168 | 169 | (ert-deftest org-runbook--projectile-should-be-optional () 170 | "org-runbook should work without projectile-project-name bound" 171 | (let ((project-name (symbol-function #'projectile-project-name)) 172 | (project-root (symbol-function #'projectile-project-root))) 173 | (with-temp-buffer 174 | (fundamental-mode) 175 | (unwind-protect 176 | (progn 177 | (fset 'projectile-project-name nil) 178 | (fset 'projectile-project-root nil) 179 | (should-error (org-runbook-projectile-file)) 180 | (setq-local org-runbook-modes-directory (relative-to-test-directory "one-command")) 181 | (setq-local org-runbook-project-directory (relative-to-test-directory "one-command")) 182 | (setq-local org-runbook-execute-command-action #'org-runbook-command-execute-shell) 183 | (org-runbook--output-configuration) 184 | (setq-local completing-read-function (lambda (_ collection &rest _) (-some-> collection ht-keys cl-first))) 185 | (with-mock 186 | (mock (start-process-shell-command "*Test*" * *) => t :times 1) 187 | (should (org-runbook-execute))) 188 | (setq-local org-runbook-modes-directory (relative-to-test-directory "no-commands")) 189 | (setq-local org-runbook-project-directory (relative-to-test-directory "no-commands")) 190 | (should-error (org-runbook-execute))) 191 | (fset 'projectile-project-root project-root) 192 | (fset 'projectile-project-name project-name))))) 193 | 194 | (ert-deftest org-runbook--should-not-create-files () 195 | "org-runbook should not create files." 196 | (with-temp-buffer 197 | (fundamental-mode) 198 | (progn 199 | (setq-local org-runbook-modes-directory (relative-to-test-directory "one-command")) 200 | (setq-local org-runbook-project-directory (relative-to-test-directory "one-command")) 201 | (setq-local org-runbook-execute-command-action #'org-runbook-command-execute-shell) 202 | (org-runbook--output-configuration) 203 | (setq-local completing-read-function (lambda (_ collection &rest _) (-some-> collection ht-keys cl-first))) 204 | (with-mock 205 | (mock (start-process-shell-command "*Test*" * *) => t :times 1) 206 | (should (org-runbook-execute))) 207 | (should (not (f-exists-p (relative-to-test-directory "one-command/org-runbook.org"))))))) 208 | 209 | (provide 'org-runbook-test) 210 | -------------------------------------------------------------------------------- /test/test-helper.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t -*- 2 | 3 | (defun relative-to-test-directory (file) 4 | (-> 5 | (or (-some--> (and (f-exists-p (expand-file-name "test")) "test") 6 | (f-join it file)) 7 | file) 8 | expand-file-name)) 9 | 10 | (defvar org-runbook-command-last-command nil "test variable for `org-runbook-command-execute-message'") 11 | (defun org-runbook-command-execute-message (command) 12 | "Stubbed out execute-message function. formats COMMAND and outputs as a message. 13 | Also sets `org-runbook-command-last-command'" 14 | (org-runbook--validate-command command) 15 | (setq org-runbook-command-last-command command) 16 | (pcase-let (((cl-struct org-runbook-command full-command) command)) 17 | (message "%s" full-command) 18 | t)) 19 | 20 | (defun org-runbook--output-configuration () 21 | (message "modes directory: %s, project directory: %s, org-version: %s" 22 | org-runbook-modes-directory 23 | org-runbook-project-directory 24 | (org-version))) 25 | 26 | (defun org-runbook--test-first-target () 27 | (->> (org-runbook-targets) 28 | (-map #'org-runbook-file-targets) 29 | (-flatten) 30 | (car))) 31 | -------------------------------------------------------------------------------- /test/test-runbook.org: -------------------------------------------------------------------------------- 1 | * Test Data 1 2 | ** Test Data A 3 | #+BEGIN_SRC shell 4 | echo test-runbook-1-A 5 | #+END_SRC 6 | 7 | ** Test Data B 8 | #+BEGIN_SRC shell 9 | echo test-runbook-1-B 10 | #+END_SRC 11 | 12 | * Test Data 2 13 | ** Test Data A 14 | #+BEGIN_SRC shell 15 | echo test-runbook-2-A 16 | #+END_SRC 17 | 18 | ** Test Data B 19 | #+BEGIN_SRC shell 20 | echo test-runbook-2-B 21 | #+END_SRC 22 | 23 | --------------------------------------------------------------------------------