├── .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 | [](https://www.gnu.org/licenses/gpl-3.0.txt)
3 | [](https://melpa.org/#/org-runbook)
4 | [](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 |
--------------------------------------------------------------------------------